Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow DateTimeImmutable be part of composite key #10830

Open
wants to merge 11 commits into
base: 2.15.x
Choose a base branch
from
8 changes: 4 additions & 4 deletions docs/en/tutorials/composite-primary-keys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ General Considerations
Every entity with a composite key cannot use an id generator other than "NONE". That means
the ID fields have to have their values set before you call ``EntityManager#persist($entity)``.

Primitive Types only
~~~~~~~~~~~~~~~~~~~~
Allowed Types
~~~~~~~~~~~~~

You can have composite keys as long as they only consist of the primitive types
greg0ire marked this conversation as resolved.
Show resolved Hide resolved
``integer`` and ``string``. Suppose you want to create a database of cars and use the model-name
and year of production as primary keys:
``integer`` and ``string`` or object of ``DateTimeImmutable``. Suppose you want to create a database of cars and use
the model-name and year of production as primary keys:

.. configuration-block::

Expand Down
23 changes: 22 additions & 1 deletion lib/Doctrine/ORM/Cache/EntityCacheKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@

namespace Doctrine\ORM\Cache;

use function array_map;
use function implode;
use function is_scalar;
use function ksort;
use function md5;
use function serialize;
use function str_replace;
use function strtolower;

Expand Down Expand Up @@ -43,6 +47,23 @@ public function __construct($entityClass, array $identifier)
$this->identifier = $identifier;
$this->entityClass = $entityClass;

parent::__construct(str_replace('\\', '.', strtolower($entityClass) . '_' . implode(' ', $identifier)));
parent::__construct(
str_replace(
'\\',
'.',
strtolower($entityClass) . '_' . $this->serializeIdentifier($identifier)
)
);
}

/** @param array<int|string|object> $identifier */
private function serializeIdentifier(array $identifier): string
{
return implode(' ', array_map(
static function ($id) {
return is_scalar($id) ? $id : md5(serialize($id));
},
$identifier
));
}
}
10 changes: 10 additions & 0 deletions lib/Doctrine/ORM/UnitOfWork.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace Doctrine\ORM;

use BackedEnum;
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
Expand Down Expand Up @@ -1636,6 +1638,10 @@
return $value->value;
}

if ($value instanceof DateTimeImmutable) {
return $value->format(DateTime::ATOM);
}

return $value;
},
$identifier
Expand Down Expand Up @@ -2921,6 +2927,10 @@
$joinColumnValue = $joinColumnValue->value;
}

if ($joinColumnValue instanceof DateTimeImmutable) {
$joinColumnValue = $joinColumnValue->format(DateTime::ATOM);

Check warning on line 2931 in lib/Doctrine/ORM/UnitOfWork.php

View check run for this annotation

Codecov / codecov/patch

lib/Doctrine/ORM/UnitOfWork.php#L2931

Added line #L2931 was not covered by tests
}

if ($targetClass->containsForeignIdentifier) {
$associatedId[$targetClass->getFieldForColumn($targetColumn)] = $joinColumnValue;
} else {
Expand Down
4 changes: 4 additions & 0 deletions lib/Doctrine/ORM/Utility/IdentifierFlattener.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace Doctrine\ORM\Utility;

use BackedEnum;
use DateTime;
use DateTimeImmutable;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\UnitOfWork;
use Doctrine\Persistence\Mapping\ClassMetadataFactory;
Expand Down Expand Up @@ -79,6 +81,8 @@ public function flattenIdentifier(ClassMetadata $class, array $id): array
} else {
if ($id[$field] instanceof BackedEnum) {
$flatId[$field] = $id[$field]->value;
} elseif ($id[$field] instanceof DateTimeImmutable) {
$flatId[$field] = $id[$field]->format(DateTime::ATOM);
} else {
$flatId[$field] = $id[$field];
}
Expand Down
83 changes: 83 additions & 0 deletions tests/Doctrine/Tests/Models/DateTimeCompositeKey/Article.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\DateTimeCompositeKey;

use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToMany;

#[Entity]
class Article
{
#[Id]
#[Column]
#[GeneratedValue]
private int|null $id = null;
#[Column]
private string $title;

#[Column]
private string $content;

/** @var Collection<int, ArticleAudit> */
#[OneToMany(targetEntity: ArticleAudit::class, mappedBy: 'article', cascade: ['ALL'])]
private Collection $audit;

public function __construct(string $title, string $content)
{
$this->title = $title;
$this->content = $content;
$this->audit = new ArrayCollection();
}

public function changeTitle(string $newTitle): void
{
$this->title = $newTitle;
$this->updateAudit('title');
}

public function changeContent(string $newContent): void
{
$this->content = $newContent;
$this->updateAudit('content');
}

public function getId(): ?int
{
return $this->id;
}

/**
* @return Collection<int, ArticleAudit>
*/
public function getAudit(): Collection
{
return $this->audit;
}

public function getTitle(): string
{
return $this->title;
}

public function getContent(): string
{
return $this->content;
}

private function updateAudit(string $changedKey): void
{
$this->audit[] = new ArticleAudit(
new DateTimeImmutable(),
$changedKey,
$this
);
}
}
49 changes: 49 additions & 0 deletions tests/Doctrine/Tests/Models/DateTimeCompositeKey/ArticleAudit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\DateTimeCompositeKey;

use DateTimeImmutable;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\ManyToOne;

#[Entity]
class ArticleAudit
{
#[Id]
#[ManyToOne(targetEntity: Article::class, inversedBy: 'audit')]
private Article $article;

#[Id]
#[Column]
private DateTimeImmutable $issuedAt;

#[Id]
#[Column]
private string $changedKey;

public function __construct(DateTimeImmutable $issuedAt, string $changedKey, Article $article)
{
$this->issuedAt = $issuedAt;
$this->changedKey = $changedKey;
$this->article = $article;
}

public function getArticle(): Article
{
return $this->article;
}

public function getIssuedAt(): DateTimeImmutable
{
return $this->issuedAt;
}

public function getChangedKey(): string
{
return $this->changedKey;
}
}
43 changes: 35 additions & 8 deletions tests/Doctrine/Tests/ORM/Cache/CacheKeyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Doctrine\Tests\ORM\Cache;

use DateTimeImmutable;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\Cache\CacheKey;
use Doctrine\ORM\Cache\CollectionCacheKey;
Expand All @@ -15,19 +16,19 @@ class CacheKeyTest extends DoctrineTestCase
{
use VerifyDeprecations;

public function testEntityCacheKeyIdentifierCollision(): void
/**
* @dataProvider collisionEntityCacheKeyDataProvider
*/
public function testEntityCacheKeyIdentifierCollision(EntityCacheKey $key1, EntityCacheKey $key2): void
{
$key1 = new EntityCacheKey('Foo', ['id' => 1]);
$key2 = new EntityCacheKey('Bar', ['id' => 1]);

self::assertNotEquals($key1->hash, $key2->hash);
}

public function testEntityCacheKeyIdentifierType(): void
/**
* @dataProvider equalEntityCacheKeyDataProvider
*/
public function testEntityCacheKeyIdentifierType(EntityCacheKey $key1, EntityCacheKey $key2): void
{
$key1 = new EntityCacheKey('Foo', ['id' => 1]);
$key2 = new EntityCacheKey('Foo', ['id' => '1']);

self::assertEquals($key1->hash, $key2->hash);
}

Expand Down Expand Up @@ -94,4 +95,30 @@ public function __construct()

self::assertSame('my-hash', $key->hash);
}

public function collisionEntityCacheKeyDataProvider(): iterable
{
yield [
new EntityCacheKey('Foo', ['id' => 1]),
new EntityCacheKey('Bar', ['id' => 1]),
];

yield [
new EntityCacheKey('Foo', ['id' => 1, 'dt' => new DateTimeImmutable('2022-01-03')]),
new EntityCacheKey('Bar', ['id' => 1, 'dt' => new DateTimeImmutable('2022-01-03')]),
];
}

public function equalEntityCacheKeyDataProvider(): iterable
{
yield [
new EntityCacheKey('Foo', ['id' => 1]),
new EntityCacheKey('Foo', ['id' => '1']),
];

yield [
new EntityCacheKey('Foo', ['id' => 1, 'dt' => new DateTimeImmutable('2022-01-03')]),
new EntityCacheKey('Foo', ['id' => '1', 'dt' => new DateTimeImmutable('2022-01-03')]),
];
}
}
33 changes: 33 additions & 0 deletions tests/Doctrine/Tests/ORM/UnitOfWorkTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Doctrine\Tests\ORM;

use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\EventManager;
Expand Down Expand Up @@ -923,6 +924,38 @@ public function testRemovedEntityIsRemovedFromOneToManyCollection(): void
self::assertFalse($user->phonenumbers->isDirty());
self::assertEmpty($user->phonenumbers->getSnapshot());
}

/**
* @dataProvider identifierValuesDataProvider
*/
public function testIdentifierHashGetter(array $givenIds, string $expectedHash): void
{
$hash = UnitOfWork::getIdHashByIdentifier($givenIds);

self::assertSame($expectedHash, $hash);
}

public static function identifierValuesDataProvider(): iterable
{
return [
[
[13],
'13',
],
[
[14, 'test'],
'14 test',
],
[
[
13,
'a2bd21fe-56fa-45a4-acc1-db4d2d93e49d',
new DateTimeImmutable('2012-12-21 21:21:00'),
],
'13 a2bd21fe-56fa-45a4-acc1-db4d2d93e49d 2012-12-21T21:21:00+00:00',
],
];
}
}

/** @Entity */
Expand Down
Loading