Skip to content

Commit

Permalink
Allow setting column options like charset and collation everywhere
Browse files Browse the repository at this point in the history
This makes it possible to set custom options on the following:
* `JoinTable`
* `JoinColumn`
* `InverseJoinColumn`
  • Loading branch information
ruudk committed Apr 26, 2022
1 parent f7fe5ad commit f3d6d4b
Show file tree
Hide file tree
Showing 10 changed files with 271 additions and 8 deletions.
4 changes: 4 additions & 0 deletions docs/en/reference/attributes-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -681,6 +683,8 @@ Optional parameters:
"columnDefinition" attribute on :ref:`#[Column] <attrref_column>` also sets
the related ``#[JoinColumn]``'s columnDefinition. This is necessary to
make foreign keys work.
- **options**:
See "options" attribute on :ref:`#[Column] <attrref_column>`.

Example:

Expand Down
15 changes: 13 additions & 2 deletions lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -735,19 +739,26 @@ private function getMethodCallbacks(ReflectionMethod $method): array
* nullable: bool,
* onDelete: mixed,
* columnDefinition: string|null,
* referencedColumnName: string
* referencedColumnName: string,
* options?: array<string, mixed>
* }
*/
private function joinColumnToArray(Mapping\JoinColumn $joinColumn): array
{
return [
$mapping = [
'name' => $joinColumn->name,
'unique' => $joinColumn->unique,
'nullable' => $joinColumn->nullable,
'onDelete' => $joinColumn->onDelete,
'columnDefinition' => $joinColumn->columnDefinition,
'referencedColumnName' => $joinColumn->referencedColumnName,
];

if ($joinColumn->options) {
$mapping['options'] = $joinColumn->options;
}

return $mapping;
}

/**
Expand Down
15 changes: 13 additions & 2 deletions lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -645,19 +649,26 @@ private function getMethodCallbacks(ReflectionMethod $method): array
* nullable: bool,
* onDelete: mixed,
* columnDefinition: string|null,
* referencedColumnName: string
* referencedColumnName: string,
* options?: array<string, mixed>
* }
*/
private function joinColumnToArray($joinColumn): array
{
return [
$mapping = [
'name' => $joinColumn->name,
'unique' => $joinColumn->unique,
'nullable' => $joinColumn->nullable,
'onDelete' => $joinColumn->onDelete,
'columnDefinition' => $joinColumn->columnDefinition,
'referencedColumnName' => $joinColumn->referencedColumnName,
];

if ($joinColumn->options) {
$mapping['options'] = $joinColumn->options;
}

return $mapping;
}

/**
Expand Down
15 changes: 14 additions & 1 deletion lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -767,7 +775,8 @@ private function parseOptions(SimpleXMLElement $options): array
* unique?: bool,
* nullable?: bool,
* onDelete?: string,
* columnDefinition?: string
* columnDefinition?: string,
* options?: array<string, mixed>
* }
*/
private function joinColumnToArray(SimpleXMLElement $joinColumnElement): array
Expand All @@ -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;
}

Expand Down
10 changes: 9 additions & 1 deletion lib/Doctrine/ORM/Mapping/InverseJoinColumn.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,21 @@ final class InverseJoinColumn implements Annotation
*/
public $fieldName;

/** @var array<string, mixed> */
public $options = [];

/**
* @param array<string, mixed> $options
*/
public function __construct(
?string $name = null,
string $referencedColumnName = 'id',
bool $unique = false,
bool $nullable = true,
$onDelete = null,
?string $columnDefinition = null,
?string $fieldName = null
?string $fieldName = null,
array $options = []
) {
$this->name = $name;
$this->referencedColumnName = $referencedColumnName;
Expand All @@ -51,5 +58,6 @@ public function __construct(
$this->onDelete = $onDelete;
$this->columnDefinition = $columnDefinition;
$this->fieldName = $fieldName;
$this->options = $options;
}
}
10 changes: 9 additions & 1 deletion lib/Doctrine/ORM/Mapping/JoinColumn.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,21 @@ final class JoinColumn implements Annotation
*/
public $fieldName;

/** @var array<string, mixed> */
public $options = [];

/**
* @param array<string, mixed> $options
*/
public function __construct(
?string $name = null,
string $referencedColumnName = 'id',
bool $unique = false,
bool $nullable = true,
$onDelete = null,
?string $columnDefinition = null,
?string $fieldName = null
?string $fieldName = null,
array $options = []
) {
$this->name = $name;
$this->referencedColumnName = $referencedColumnName;
Expand All @@ -56,5 +63,6 @@ public function __construct(
$this->onDelete = $onDelete;
$this->columnDefinition = $columnDefinition;
$this->fieldName = $fieldName;
$this->options = $options;
}
}
10 changes: 9 additions & 1 deletion lib/Doctrine/ORM/Mapping/JoinTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,25 @@ final class JoinTable implements Annotation
/** @var array<\Doctrine\ORM\Mapping\JoinColumn> */
public $inverseJoinColumns = [];

/** @var array<string, mixed> */
public $options = [];

/**
* @param array<string, mixed> $options
*/
public function __construct(
?string $name = null,
?string $schema = null,
$joinColumns = [],
$inverseJoinColumns = []
$inverseJoinColumns = [],
array $options = []
) {
$this->name = $name;
$this->schema = $schema;
$this->joinColumns = $joinColumns instanceof JoinColumn ? [$joinColumns] : $joinColumns;
$this->inverseJoinColumns = $inverseJoinColumns instanceof JoinColumn
? [$inverseJoinColumns]
: $inverseJoinColumns;
$this->options = $options;
}
}
8 changes: 8 additions & 0 deletions lib/Doctrine/ORM/Tools/SchemaTool.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -728,6 +734,8 @@ private function gatherRelationJoinColumns(
$columnOptions['precision'] = $fieldMapping['precision'];
}

$columnOptions = $this->gatherColumnOptions($joinColumn) + $columnOptions;

$theJoinTable->addColumn($quotedColumnName, $fieldMapping['type'], $columnOptions);
}

Expand Down
141 changes: 141 additions & 0 deletions tests/Doctrine/Tests/ORM/Functional/Ticket/GH6823Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Functional\Ticket;

use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\JoinTable;
use Doctrine\ORM\Mapping\ManyToMany;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\Table;
use Doctrine\Tests\OrmFunctionalTestCase;

use function method_exists;

class GH6823Test extends OrmFunctionalTestCase
{
public function testCharsetCollationWhenCreatingForeignRelations(): void
{
if (! $this->_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<int, GH6823Tag>
* @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;
}
Loading

0 comments on commit f3d6d4b

Please sign in to comment.