Skip to content

Commit

Permalink
JoinColumn > Allow setting column options like charset and collation
Browse files Browse the repository at this point in the history
  • Loading branch information
ruudk committed Apr 14, 2022
1 parent f7fe5ad commit 0f01e47
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 6 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
11 changes: 9 additions & 2 deletions lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -735,19 +735,26 @@ private function getMethodCallbacks(ReflectionMethod $method): array
* nullable: bool,
* onDelete: mixed,
* columnDefinition: string|null,
* referencedColumnName: string
* referencedColumnName: string,
* options?: 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
11 changes: 9 additions & 2 deletions lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -645,19 +645,26 @@ private function getMethodCallbacks(ReflectionMethod $method): array
* nullable: bool,
* onDelete: mixed,
* columnDefinition: string|null,
* referencedColumnName: string
* referencedColumnName: string,
* options?: 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
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;
}
}
2 changes: 2 additions & 0 deletions lib/Doctrine/ORM/Tools/SchemaTool.php
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,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 utf8 COLLATE `utf8_unicode_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"})
* })
*/
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;
}
39 changes: 39 additions & 0 deletions tests/Doctrine/Tests/ORM/Mapping/AttributeReaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -36,11 +40,46 @@ public function testItThrowsWhenGettingNonRepeatableAnnotationWithTheWrongMethod
);
$reader->getPropertyAnnotationCollection($property, ORM\Id::class);
}

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')]
#[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 */
Expand Down

0 comments on commit 0f01e47

Please sign in to comment.