From f3d6d4bfa69fe5a0b13063f07c20f7c4f18433be Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Tue, 26 Apr 2022 11:57:31 +0200 Subject: [PATCH] Allow setting column options like `charset` and `collation` everywhere This makes it possible to set custom options on the following: * `JoinTable` * `JoinColumn` * `InverseJoinColumn` --- docs/en/reference/attributes-reference.rst | 4 + .../ORM/Mapping/Driver/AnnotationDriver.php | 15 +- .../ORM/Mapping/Driver/AttributeDriver.php | 15 +- lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php | 15 +- .../ORM/Mapping/InverseJoinColumn.php | 10 +- lib/Doctrine/ORM/Mapping/JoinColumn.php | 10 +- lib/Doctrine/ORM/Mapping/JoinTable.php | 10 +- lib/Doctrine/ORM/Tools/SchemaTool.php | 8 + .../ORM/Functional/Ticket/GH6823Test.php | 141 ++++++++++++++++++ .../Tests/ORM/Mapping/AttributeReaderTest.php | 51 +++++++ 10 files changed, 271 insertions(+), 8 deletions(-) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/GH6823Test.php diff --git a/docs/en/reference/attributes-reference.rst b/docs/en/reference/attributes-reference.rst index 603c1c04845..e7e015d4acd 100644 --- a/docs/en/reference/attributes-reference.rst +++ b/docs/en/reference/attributes-reference.rst @@ -207,6 +207,8 @@ Optional parameters: - ``comment``: The comment of the column in the schema (might not be supported by all vendors). + - ``charset``: The charset of the column (only supported by Mysql, PostgreSQL, Sqlite and SQLServer). + - ``collation``: The collation of the column (only supported by Mysql, PostgreSQL, Sqlite and SQLServer). - ``check``: Adds a check constraint type to the column (might not @@ -681,6 +683,8 @@ Optional parameters: "columnDefinition" attribute on :ref:`#[Column] ` also sets the related ``#[JoinColumn]``'s columnDefinition. This is necessary to make foreign keys work. +- **options**: + See "options" attribute on :ref:`#[Column] `. Example: diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index bdd1784c7c8..113161f121c 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -610,6 +610,10 @@ private function loadRelationShipMapping( 'schema' => $joinTableAnnot->schema, ]; + if ($joinTableAnnot->options) { + $joinTable['options'] = $joinTableAnnot->options; + } + foreach ($joinTableAnnot->joinColumns as $joinColumn) { $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumn); } @@ -735,12 +739,13 @@ private function getMethodCallbacks(ReflectionMethod $method): array * nullable: bool, * onDelete: mixed, * columnDefinition: string|null, - * referencedColumnName: string + * referencedColumnName: string, + * options?: array * } */ private function joinColumnToArray(Mapping\JoinColumn $joinColumn): array { - return [ + $mapping = [ 'name' => $joinColumn->name, 'unique' => $joinColumn->unique, 'nullable' => $joinColumn->nullable, @@ -748,6 +753,12 @@ private function joinColumnToArray(Mapping\JoinColumn $joinColumn): array 'columnDefinition' => $joinColumn->columnDefinition, 'referencedColumnName' => $joinColumn->referencedColumnName, ]; + + if ($joinColumn->options) { + $mapping['options'] = $joinColumn->options; + } + + return $mapping; } /** diff --git a/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php index 99d92a0534a..63ebde806e1 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php @@ -407,6 +407,10 @@ public function loadMetadataForClass($className, ClassMetadata $metadata): void 'name' => $joinTableAttribute->name, 'schema' => $joinTableAttribute->schema, ]; + + if ($joinTableAttribute->options) { + $joinTable['options'] = $joinTableAttribute->options; + } } foreach ($this->reader->getPropertyAnnotationCollection($property, Mapping\JoinColumn::class) as $joinColumn) { @@ -645,12 +649,13 @@ private function getMethodCallbacks(ReflectionMethod $method): array * nullable: bool, * onDelete: mixed, * columnDefinition: string|null, - * referencedColumnName: string + * referencedColumnName: string, + * options?: array * } */ private function joinColumnToArray($joinColumn): array { - return [ + $mapping = [ 'name' => $joinColumn->name, 'unique' => $joinColumn->unique, 'nullable' => $joinColumn->nullable, @@ -658,6 +663,12 @@ private function joinColumnToArray($joinColumn): array 'columnDefinition' => $joinColumn->columnDefinition, 'referencedColumnName' => $joinColumn->referencedColumnName, ]; + + if ($joinColumn->options) { + $mapping['options'] = $joinColumn->options; + } + + return $mapping; } /** diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php index 03c599480f5..dac50cd810a 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -584,6 +584,10 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) $joinTable['schema'] = (string) $joinTableElement['schema']; } + if (isset($joinTableElement->options)) { + $joinTable['options'] = $this->parseOptions($joinTableElement->options->children()); + } + foreach ($joinTableElement->{'join-columns'}->{'join-column'} as $joinColumnElement) { $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumnElement); } @@ -663,6 +667,10 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) 'schema' => (string) $joinTableElement['schema'], ]; + if (isset($joinTableElement->options)) { + $joinTable['options'] = $this->parseOptions($joinTableElement->options->children()); + } + if (isset($joinTableElement->{'join-columns'})) { foreach ($joinTableElement->{'join-columns'}->{'join-column'} as $joinColumnElement) { $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumnElement); @@ -767,7 +775,8 @@ private function parseOptions(SimpleXMLElement $options): array * unique?: bool, * nullable?: bool, * onDelete?: string, - * columnDefinition?: string + * columnDefinition?: string, + * options?: array * } */ private function joinColumnToArray(SimpleXMLElement $joinColumnElement): array @@ -793,6 +802,10 @@ private function joinColumnToArray(SimpleXMLElement $joinColumnElement): array $joinColumn['columnDefinition'] = (string) $joinColumnElement['column-definition']; } + if (isset($joinColumnElement['options'])) { + $joinColumn['options'] = $this->parseOptions($joinColumnElement['options']->children()); + } + return $joinColumn; } diff --git a/lib/Doctrine/ORM/Mapping/InverseJoinColumn.php b/lib/Doctrine/ORM/Mapping/InverseJoinColumn.php index 9875717ca6f..09ff59d222b 100644 --- a/lib/Doctrine/ORM/Mapping/InverseJoinColumn.php +++ b/lib/Doctrine/ORM/Mapping/InverseJoinColumn.php @@ -35,6 +35,12 @@ final class InverseJoinColumn implements Annotation */ public $fieldName; + /** @var array */ + public $options = []; + + /** + * @param array $options + */ public function __construct( ?string $name = null, string $referencedColumnName = 'id', @@ -42,7 +48,8 @@ public function __construct( bool $nullable = true, $onDelete = null, ?string $columnDefinition = null, - ?string $fieldName = null + ?string $fieldName = null, + array $options = [] ) { $this->name = $name; $this->referencedColumnName = $referencedColumnName; @@ -51,5 +58,6 @@ public function __construct( $this->onDelete = $onDelete; $this->columnDefinition = $columnDefinition; $this->fieldName = $fieldName; + $this->options = $options; } } diff --git a/lib/Doctrine/ORM/Mapping/JoinColumn.php b/lib/Doctrine/ORM/Mapping/JoinColumn.php index dfe6388f0ad..0cf70971a20 100644 --- a/lib/Doctrine/ORM/Mapping/JoinColumn.php +++ b/lib/Doctrine/ORM/Mapping/JoinColumn.php @@ -40,6 +40,12 @@ final class JoinColumn implements Annotation */ public $fieldName; + /** @var array */ + public $options = []; + + /** + * @param array $options + */ public function __construct( ?string $name = null, string $referencedColumnName = 'id', @@ -47,7 +53,8 @@ public function __construct( bool $nullable = true, $onDelete = null, ?string $columnDefinition = null, - ?string $fieldName = null + ?string $fieldName = null, + array $options = [] ) { $this->name = $name; $this->referencedColumnName = $referencedColumnName; @@ -56,5 +63,6 @@ public function __construct( $this->onDelete = $onDelete; $this->columnDefinition = $columnDefinition; $this->fieldName = $fieldName; + $this->options = $options; } } diff --git a/lib/Doctrine/ORM/Mapping/JoinTable.php b/lib/Doctrine/ORM/Mapping/JoinTable.php index 587500aabae..75a2d30270b 100644 --- a/lib/Doctrine/ORM/Mapping/JoinTable.php +++ b/lib/Doctrine/ORM/Mapping/JoinTable.php @@ -27,11 +27,18 @@ final class JoinTable implements Annotation /** @var array<\Doctrine\ORM\Mapping\JoinColumn> */ public $inverseJoinColumns = []; + /** @var array */ + public $options = []; + + /** + * @param array $options + */ public function __construct( ?string $name = null, ?string $schema = null, $joinColumns = [], - $inverseJoinColumns = [] + $inverseJoinColumns = [], + array $options = [] ) { $this->name = $name; $this->schema = $schema; @@ -39,5 +46,6 @@ public function __construct( $this->inverseJoinColumns = $inverseJoinColumns instanceof JoinColumn ? [$inverseJoinColumns] : $inverseJoinColumns; + $this->options = $options; } } diff --git a/lib/Doctrine/ORM/Tools/SchemaTool.php b/lib/Doctrine/ORM/Tools/SchemaTool.php index 87653f84607..42c36a7d1e9 100644 --- a/lib/Doctrine/ORM/Tools/SchemaTool.php +++ b/lib/Doctrine/ORM/Tools/SchemaTool.php @@ -579,6 +579,12 @@ private function gatherRelationsSql( $this->quoteStrategy->getJoinTableName($mapping, $foreignClass, $this->platform) ); + if (isset($joinTable['options'])) { + foreach ($joinTable['options'] as $key => $val) { + $theJoinTable->addOption($key, $val); + } + } + $primaryKeyColumns = []; // Build first FK constraint (relation table => source table) @@ -728,6 +734,8 @@ private function gatherRelationJoinColumns( $columnOptions['precision'] = $fieldMapping['precision']; } + $columnOptions = $this->gatherColumnOptions($joinColumn) + $columnOptions; + $theJoinTable->addColumn($quotedColumnName, $fieldMapping['type'], $columnOptions); } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH6823Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH6823Test.php new file mode 100644 index 00000000000..c41105efe4d --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH6823Test.php @@ -0,0 +1,141 @@ +_em->getConnection()->getDatabasePlatform() instanceof MySQLPlatform) { + self::markTestSkipped('This test is useful for all databases, but designed only for mysql.'); + } + + if (method_exists(AbstractPlatform::class, 'getGuidExpression')) { + self::markTestSkipped('Test valid for doctrine/dbal:3.x only.'); + } + + $this->createSchemaForModels( + GH6823User::class, + GH6823Group::class, + GH6823Status::class + ); + + self::assertEquals('CREATE TABLE gh6823_user (id VARCHAR(255) NOT NULL, group_id VARCHAR(255) CHARACTER SET ascii DEFAULT NULL COLLATE `ascii_general_ci`, status_id VARCHAR(255) CHARACTER SET latin1 DEFAULT NULL COLLATE `latin1_bin`, INDEX IDX_70DD1774FE54D947 (group_id), INDEX IDX_70DD17746BF700BD (status_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_bin` ENGINE = InnoDB', $this->getLastLoggedQuery(6)['sql']); + self::assertEquals('CREATE TABLE gh6823_user_tags (user_id VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_bin`, tag_id VARCHAR(255) CHARACTER SET latin1 NOT NULL COLLATE `latin1_bin`, INDEX IDX_596B1281A76ED395 (user_id), INDEX IDX_596B1281BAD26311 (tag_id), PRIMARY KEY(user_id, tag_id)) DEFAULT CHARACTER SET ascii COLLATE `ascii_general_ci` ENGINE = InnoDB', $this->getLastLoggedQuery(5)['sql']); + self::assertEquals('CREATE TABLE gh6823_group (id VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET ascii COLLATE `ascii_general_ci` ENGINE = InnoDB', $this->getLastLoggedQuery(4)['sql']); + self::assertEquals('CREATE TABLE gh6823_status (id VARCHAR(255) CHARACTER SET latin1 NOT NULL COLLATE `latin1_bin`, PRIMARY KEY(id)) DEFAULT CHARACTER SET koi8r COLLATE `koi8r_bin` ENGINE = InnoDB', $this->getLastLoggedQuery(3)['sql']); + self::assertEquals('ALTER TABLE gh6823_user ADD CONSTRAINT FK_70DD1774FE54D947 FOREIGN KEY (group_id) REFERENCES gh6823_group (id)', $this->getLastLoggedQuery(2)['sql']); + self::assertEquals('ALTER TABLE gh6823_user ADD CONSTRAINT FK_70DD17746BF700BD FOREIGN KEY (status_id) REFERENCES gh6823_status (id)', $this->getLastLoggedQuery(1)['sql']); + self::assertEquals('ALTER TABLE gh6823_user_tags ADD CONSTRAINT FK_596B1281A76ED395 FOREIGN KEY (user_id) REFERENCES gh6823_user (id)', $this->getLastLoggedQuery(0)['sql']); + } +} + +/** + * @Entity + * @Table(name="gh6823_user", options={ + * "charset"="utf8mb4", + * "collation"="utf8mb4_bin" + * }) + */ +class GH6823User +{ + /** + * @var string + * @Id + * @Column(type="string") + */ + public $id; + + /** + * @var GH6823Group + * @ManyToOne(targetEntity="GH6823Group") + * @JoinColumn(name="group_id", referencedColumnName="id", options={"charset"="ascii", "collation"="ascii_general_ci"}) + */ + public $group; + + /** + * @var GH6823Status + * @ManyToOne(targetEntity="GH6823Status") + * @JoinColumn(name="status_id", referencedColumnName="id", options={"charset"="latin1", "collation"="latin1_bin"}) + */ + public $status; + + /** + * @var Collection + * @ManyToMany(targetEntity="GH6823Tag") + * @JoinTable(name="gh6823_user_tags", joinColumns={ + * @JoinColumn(name="user_id", referencedColumnName="id", options={"charset"="utf8mb4", "collation"="utf8mb4_bin"}) + * }, inverseJoinColumns={ + * @JoinColumn(name="tag_id", referencedColumnName="id", options={"charset"="latin1", "collation"="latin1_bin"}) + * }, options={"charset"="ascii", "collation"="ascii_general_ci"}) + */ + public $tags; +} + +/** + * @Entity + * @Table(name="gh6823_group", options={ + * "charset"="ascii", + * "collation"="ascii_general_ci" + * }) + */ +class GH6823Group +{ + /** + * @var string + * @Id + * @Column(type="string") + */ + public $id; +} + +/** + * @Entity + * @Table(name="gh6823_status", options={ + * "charset"="koi8r", + * "collation"="koi8r_bin" + * }) + */ +class GH6823Status +{ + /** + * @var string + * @Id + * @Column(type="string", options={"charset"="latin1", "collation"="latin1_bin"}) + */ + public $id; +} + +/** + * @Entity + * @Table(name="gh6823_tag", options={ + * "charset"="koi8r", + * "collation"="koi8r_bin" + * }) + */ +class GH6823Tag +{ + /** + * @var string + * @Id + * @Column(type="string", options={"charset"="latin1", "collation"="latin1_bin"}) + */ + public $id; +} diff --git a/tests/Doctrine/Tests/ORM/Mapping/AttributeReaderTest.php b/tests/Doctrine/Tests/ORM/Mapping/AttributeReaderTest.php index 3d339b35992..00401529702 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/AttributeReaderTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/AttributeReaderTest.php @@ -6,6 +6,10 @@ use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping\Driver\AttributeReader; +use Doctrine\ORM\Mapping\InverseJoinColumn; +use Doctrine\ORM\Mapping\JoinColumn; +use Doctrine\ORM\Mapping\JoinTable; +use Doctrine\ORM\Mapping\ManyToMany; use LogicException; use PHPUnit\Framework\TestCase; use ReflectionProperty; @@ -36,11 +40,58 @@ public function testItThrowsWhenGettingNonRepeatableAnnotationWithTheWrongMethod ); $reader->getPropertyAnnotationCollection($property, ORM\Id::class); } + + public function testJoinTableOptions(): void + { + $reader = new AttributeReader(); + $property = new ReflectionProperty(TestEntity::class, 'tags'); + + $joinTable = $reader->getPropertyAnnotation($property, ORM\JoinTable::class); + self::assertSame([ + 'charset' => 'ascii', + 'collation' => 'ascii_general_ci', + ], $joinTable->options); + } + + public function testJoinColumnOptions(): void + { + $reader = new AttributeReader(); + $property = new ReflectionProperty(TestEntity::class, 'tags'); + + $joinColumns = $reader->getPropertyAnnotationCollection($property, ORM\JoinColumn::class); + self::assertCount(1, $joinColumns); + self::assertSame([ + 'charset' => 'latin1', + 'collation' => 'latin1_swedish_ci', + ], $joinColumns[0]->options); + + $inverseJoinColumns = $reader->getPropertyAnnotationCollection($property, ORM\InverseJoinColumn::class); + self::assertCount(1, $inverseJoinColumns); + self::assertSame([ + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_bin', + ], $inverseJoinColumns[0]->options); + } } #[ORM\Entity] #[ORM\Index(name: 'bar', columns: ['id'])] class TestEntity +{ + #[ORM\Id, ORM\Column(type: 'integer'), ORM\GeneratedValue] + /** @var int */ + public $id; + + /** @var mixed */ + #[ManyToMany(targetEntity: TestTag::class)] + #[JoinTable(name: 'artist_tags', options: ['charset' => 'ascii', 'collation' => 'ascii_general_ci'])] + #[JoinColumn(name: 'artist_id', referencedColumnName: 'id', options: ['charset' => 'latin1', 'collation' => 'latin1_swedish_ci'])] + #[InverseJoinColumn(name: 'tag_id', referencedColumnName: 'id', options: ['charset' => 'utf8mb4', 'collation' => 'utf8mb4_bin'])] + public $tags; +} + +#[ORM\Entity] +class TestTag { #[ORM\Id, ORM\Column(type: 'integer'), ORM\GeneratedValue] /** @var int */