Compare commits

...

23 Commits

Author SHA1 Message Date
Grégoire Paris bf449bef7d Merge pull request #10737 from nicolas-grekas/lexer-deprec 2023-06-01 11:35:50 +02:00
Nicolas Grekas 152f91fa33 Fix deprecations from doctrine/lexer 2023-06-01 11:22:36 +02:00
Grégoire Paris 14d1eb5340 Document pdo_sqlite requirement for tests (#10734) 2023-06-01 08:57:07 +02:00
Grégoire Paris da0998c401 Add missing underscore to RST links (#10731) 2023-05-30 23:00:28 +02:00
Grégoire Paris cc011d8215 Merge pull request #10725 from doctrine/2.14.x
Merge 2.14.x up into 2.15.x
2023-05-24 14:08:21 +02:00
Grégoire Paris d831c126c9 Merge pull request #10643 from htto/bugfix/sti-with-abstract-intermediate
Fix single table inheritance with intermediate abstract class(es)
2023-05-24 11:58:11 +02:00
Grégoire Paris 10eae1a7ff Merge pull request #10722 from phansys/query_single_result_exception
Fix and improve functional test cases expecting `NonUniqueResultException` from `AbstractQuery::getSingleScalarResult()`
2023-05-23 22:46:18 +02:00
Javier Spagnoletti 125e18cf24 Fix and improve functional test cases expecting NonUniqueResultException from AbstractQuery::getSingleScalarResult() 2023-05-23 05:32:38 -03:00
Grégoire Paris 8fba9d6868 Merge pull request #10708 from mbabker/2.15-link-fix 2023-05-16 17:06:17 +02:00
Michael Babker 7f4f1cda71 Correct docs link 2023-05-16 09:38:09 -05:00
Grégoire Paris 1c905b0e0a Merge pull request #10702 from greg0ire/fix-build 2023-05-15 11:53:09 +02:00
Grégoire Paris 7901790b97 Adapt to latest coding standards 2023-05-15 11:42:13 +02:00
Grégoire Paris cef1d2d740 Merge pull request #10666 from MatTheCat/inherited-readonly-properties
Create `ReflectionReadonlyProperty` from their declaring class so their value can be set
2023-05-12 07:50:03 +02:00
Grégoire Paris 8126882305 Merge pull request #10486 from mpdude/fix-to-many-update-on-delete
Fix to-many collections left in dirty state after entities are removed by the UoW
2023-05-12 00:11:56 +02:00
Matthias Pigulla a9513692cb Fix to-many collections left in dirty state after entities are removed by the UoW 2023-05-12 00:02:44 +02:00
Grégoire Paris 52c3d9d82a Merge pull request #10508 from Gwemox/fix-enum-identity
Fix id hash of entity with enum as identifier
2023-05-12 00:00:20 +02:00
MatTheCat 2f46e5a130 Rename test class and method 2023-05-11 09:36:43 +02:00
Mathieu a3fa1d7faa Replace assertions by @doesNotPerformAssertions 2023-05-11 09:33:12 +02:00
MatTheCat c7c57be0c2 Create ReflectionReadonlyProperty from their declaring class so their value can be set 2023-05-11 09:33:12 +02:00
MatTheCat 721794fb9c Add test case 2023-05-11 09:33:12 +02:00
Terence Eden 70477d81e9 Documentation typo (#10686)
The past tense of spin is spun, not "spinned"

https://en.wiktionary.org/wiki/spin#English
2023-05-08 12:22:09 +02:00
Heiko Przybyl 1ae74b8ec5 Fix single table inheritance with intermediate abstract class(es)
Fixes #10625
2023-04-20 15:37:42 +02:00
Thibault Buathier 09b4a75ed3 Fix id hash of entity with enum as identifier
When an entity have a backed enum as identifier, `UnitOfWork` tries to
cast to string when generating the hash of the id.
This fix calls `->value` when identifier is a `BackedEnum`.
Fixes #10471
Fixes #10334
2023-04-03 17:20:48 +02:00
25 changed files with 660 additions and 56 deletions
+5
View File
@@ -40,6 +40,11 @@ cd orm
composer install
```
You will also need to enable the PHP extension that provides the SQLite driver
for PDO: `pdo_sqlite`. How to do so depends on your system, but checking that it
is enabled can universally be done with `php -m`: that command should list the
extension.
To run the testsuite against another database, copy the ``phpunit.xml.dist``
to for example ``mysql.phpunit.xml`` and edit the parameters. You can
take a look at the ``ci/github/phpunit`` directory for some examples. Then run:
+1 -1
View File
@@ -35,7 +35,7 @@ have to be used.
superclass, since they require the "many" side to hold the foreign
key.
It is, however, possible to use the :doc:```ResolveTargetEntityListener`` <cookbook/resolve-target-entity-listener>`
It is, however, possible to use the :doc:`ResolveTargetEntityListener <cookbook/resolve-target-entity-listener>`
to replace references to a mapped superclass with an entity class at runtime.
As long as there is only one entity subclass inheriting from the mapped
superclass and all references to the mapped superclass are resolved to that
@@ -166,10 +166,10 @@ As long as the results are consistent with what a solution _without_ traits woul
have produced, this is probably fine.
However, to mention known limitations, it is currently not possible to use "class"
level `annotations <https://github.com/doctrine/orm/pull/1517>` or
level `annotations <https://github.com/doctrine/orm/pull/1517>`_ or
`attributes <https://github.com/doctrine/orm/issues/8868>` on traits, and attempts to
improve parser support for traits as `here <https://github.com/doctrine/annotations/pull/102>`
or `there <https://github.com/doctrine/annotations/pull/63>` have been abandoned
improve parser support for traits as `here <https://github.com/doctrine/annotations/pull/102>`_
or `there <https://github.com/doctrine/annotations/pull/63>`_ have been abandoned
due to complexity.
XML mapping configuration probably needs to completely re-configure or otherwise
@@ -22,5 +22,5 @@ In this workflow you would modify the database schema first and then
regenerate the PHP code to use with this schema. You need a flexible
code-generator for this task.
We spinned off a subproject, Doctrine CodeGenerator, that will fill this gap and
We spun off a subproject, Doctrine CodeGenerator, that will fill this gap and
allow you to do *Database First* development.
+2 -2
View File
@@ -322,7 +322,7 @@ data in your storage, and later in your application when the data is loaded agai
.. note::
This method, although very common, is inappropriate for Domain Driven
Design (`DDD <https://en.wikipedia.org/wiki/Domain-driven_design>`)
Design (`DDD <https://en.wikipedia.org/wiki/Domain-driven_design>`_)
where methods should represent real business operations and not simple
property change, And business invariants should be maintained both in the
application state (entities in this case) and in the database, with no
@@ -449,7 +449,7 @@ entity.
.. note::
A `DTO <https://en.wikipedia.org/wiki/Data_transfer_object>` is an object
A `DTO <https://en.wikipedia.org/wiki/Data_transfer_object>`_ is an object
that only carries data without any logic. Its only goal is to be transferred
from one service to another.
A ``DTO`` often represents data sent by a client and that has to be validated,
@@ -14,6 +14,7 @@ use Doctrine\Persistence\Proxy;
use function array_fill_keys;
use function array_keys;
use function array_map;
use function count;
use function is_array;
use function key;
@@ -284,13 +285,17 @@ class ObjectHydrator extends AbstractHydrator
$class = $this->_metadataCache[$className];
if ($class->isIdentifierComposite) {
$idHash = '';
foreach ($class->identifier as $fieldName) {
$idHash .= ' ' . (isset($class->associationMappings[$fieldName])
? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
: $data[$fieldName]);
}
$idHash = UnitOfWork::getIdHashByIdentifier(
array_map(
/** @return mixed */
static function (string $fieldName) use ($data, $class) {
return isset($class->associationMappings[$fieldName])
? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
: $data[$fieldName];
},
$class->identifier
)
);
return $this->_uow->tryGetByIdHash(ltrim($idHash), $class->rootEntityName);
} elseif (isset($class->associationMappings[$class->identifier[0]])) {
@@ -3862,7 +3862,14 @@ class ClassMetadataInfo implements ClassMetadata
{
$reflectionProperty = $reflService->getAccessibleProperty($class, $field);
if ($reflectionProperty !== null && PHP_VERSION_ID >= 80100 && $reflectionProperty->isReadOnly()) {
$reflectionProperty = new ReflectionReadonlyProperty($reflectionProperty);
$declaringClass = $reflectionProperty->getDeclaringClass()->name;
if ($declaringClass !== $class) {
$reflectionProperty = $reflService->getAccessibleProperty($declaringClass, $field);
}
if ($reflectionProperty !== null) {
$reflectionProperty = new ReflectionReadonlyProperty($reflectionProperty);
}
}
return $reflectionProperty;
@@ -273,6 +273,10 @@ class BasicEntityPersister implements EntityPersister
$paramIndex = 1;
foreach ($insertData[$tableName] as $column => $value) {
if ($value instanceof BackedEnum) {
$value = $value->value;
}
$stmt->bindValue($paramIndex++, $value, $this->columnTypes[$column]);
}
}
@@ -10,6 +10,9 @@ use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Utility\PersisterHelper;
use function array_flip;
use function array_intersect;
use function array_map;
use function array_unshift;
use function implode;
/**
@@ -145,16 +148,13 @@ class SingleTablePersister extends AbstractEntityInheritancePersister
/** @return string */
protected function getSelectConditionDiscriminatorValueSQL()
{
$values = [];
$values = array_map(
[$this->conn, 'quote'],
array_flip(array_intersect($this->class->discriminatorMap, $this->class->subClasses))
);
if ($this->class->discriminatorValue !== null) { // discriminators can be 0
$values[] = $this->conn->quote($this->class->discriminatorValue);
}
$discrValues = array_flip($this->class->discriminatorMap);
foreach ($this->class->subClasses as $subclassName) {
$values[] = $this->conn->quote($discrValues[$subclassName]);
array_unshift($values, $this->conn->quote($this->class->discriminatorValue));
}
$discColumnName = $this->class->getDiscriminatorColumn()['name'];
+4 -4
View File
@@ -582,7 +582,7 @@ class Parser
assert($this->lexer->lookahead !== null);
return in_array(
$this->lexer->lookahead['type'],
$this->lexer->lookahead->type,
[Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME],
true
);
@@ -978,7 +978,7 @@ class Parser
$this->match(Lexer::T_IDENTIFIER);
assert($this->lexer->token !== null);
return $this->lexer->token['value'];
return $this->lexer->token->value;
}
$this->match(Lexer::T_ALIASED_NAME);
@@ -989,10 +989,10 @@ class Parser
'doctrine/orm',
'https://github.com/doctrine/orm/issues/8818',
'Short namespace aliases such as "%s" are deprecated and will be removed in Doctrine ORM 3.0.',
$this->lexer->token['value']
$this->lexer->token->value
);
[$namespaceAlias, $simpleClassName] = explode(':', $this->lexer->token['value']);
[$namespaceAlias, $simpleClassName] = explode(':', $this->lexer->token->value);
return $this->em->getConfiguration()->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName;
}
+59 -21
View File
@@ -916,13 +916,6 @@ class UnitOfWork implements PropertyChangedListener
return;
}
if ($value instanceof PersistentCollection && $value->isDirty()) {
$coid = spl_object_id($value);
$this->collectionUpdates[$coid] = $value;
$this->visitedCollections[$coid] = $value;
}
// Look through the entities, and in any of their associations,
// for transient (new) entities, recursively. ("Persistence by reachability")
// Unwrap. Uninitialized collections will simply be empty.
@@ -983,6 +976,13 @@ class UnitOfWork implements PropertyChangedListener
// during changeset calculation anyway, since they are in the identity map.
}
}
if ($value instanceof PersistentCollection && $value->isDirty()) {
$coid = spl_object_id($value);
$this->collectionUpdates[$coid] = $value;
$this->visitedCollections[$coid] = $value;
}
}
/**
@@ -1565,14 +1565,8 @@ class UnitOfWork implements PropertyChangedListener
public function addToIdentityMap($entity)
{
$classMetadata = $this->em->getClassMetadata(get_class($entity));
$identifier = $this->entityIdentifiers[spl_object_id($entity)];
if (empty($identifier) || in_array(null, $identifier, true)) {
throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity);
}
$idHash = implode(' ', $identifier);
$className = $classMetadata->rootEntityName;
$idHash = $this->getIdHashByEntity($entity);
$className = $classMetadata->rootEntityName;
if (isset($this->identityMap[$className][$idHash])) {
return false;
@@ -1583,6 +1577,50 @@ class UnitOfWork implements PropertyChangedListener
return true;
}
/**
* Gets the id hash of an entity by its identifier.
*
* @param array<string|int, mixed> $identifier The identifier of an entity
*
* @return string The entity id hash.
*/
final public static function getIdHashByIdentifier(array $identifier): string
{
return implode(
' ',
array_map(
static function ($value) {
if ($value instanceof BackedEnum) {
return $value->value;
}
return $value;
},
$identifier
)
);
}
/**
* Gets the id hash of an entity.
*
* @param object $entity The entity managed by Unit Of Work
*
* @return string The entity id hash.
*/
public function getIdHashByEntity($entity): string
{
$identifier = $this->entityIdentifiers[spl_object_id($entity)];
if (empty($identifier) || in_array(null, $identifier, true)) {
$classMetadata = $this->em->getClassMetadata(get_class($entity));
throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity);
}
return self::getIdHashByIdentifier($identifier);
}
/**
* Gets the state of an entity with regard to the current unit of work.
*
@@ -1685,7 +1723,7 @@ class UnitOfWork implements PropertyChangedListener
{
$oid = spl_object_id($entity);
$classMetadata = $this->em->getClassMetadata(get_class($entity));
$idHash = implode(' ', $this->entityIdentifiers[$oid]);
$idHash = self::getIdHashByIdentifier($this->entityIdentifiers[$oid]);
if ($idHash === '') {
throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'remove from identity map');
@@ -1755,7 +1793,7 @@ class UnitOfWork implements PropertyChangedListener
}
$classMetadata = $this->em->getClassMetadata(get_class($entity));
$idHash = implode(' ', $this->entityIdentifiers[$oid]);
$idHash = self::getIdHashByIdentifier($this->entityIdentifiers[$oid]);
return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]);
}
@@ -2712,7 +2750,7 @@ class UnitOfWork implements PropertyChangedListener
$class = $this->em->getClassMetadata($className);
$id = $this->identifierFlattener->flattenIdentifier($class, $data);
$idHash = implode(' ', $id);
$idHash = self::getIdHashByIdentifier($id);
if (isset($this->identityMap[$class->rootEntityName][$idHash])) {
$entity = $this->identityMap[$class->rootEntityName][$idHash];
@@ -2871,7 +2909,7 @@ class UnitOfWork implements PropertyChangedListener
// Check identity map first
// FIXME: Can break easily with composite keys if join column values are in
// wrong order. The correct order is the one in ClassMetadata#identifier.
$relatedIdHash = implode(' ', $associatedId);
$relatedIdHash = self::getIdHashByIdentifier($associatedId);
switch (true) {
case isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash]):
@@ -3156,7 +3194,7 @@ class UnitOfWork implements PropertyChangedListener
*/
public function tryGetById($id, $rootClassName)
{
$idHash = implode(' ', (array) $id);
$idHash = self::getIdHashByIdentifier((array) $id);
return $this->identityMap[$rootClassName][$idHash] ?? false;
}
@@ -3538,7 +3576,7 @@ class UnitOfWork implements PropertyChangedListener
$id1 = $this->entityIdentifiers[$oid1] ?? $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity1));
$id2 = $this->entityIdentifiers[$oid2] ?? $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity2));
return $id1 === $id2 || implode(' ', $id1) === implode(' ', $id2);
return $id1 === $id2 || self::getIdHashByIdentifier($id1) === self::getIdHashByIdentifier($id2);
}
/** @throws ORMInvalidArgumentException */
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\GH10334;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
/**
* @Entity
*/
class GH10334Foo
{
/**
* @var GH10334FooCollection
* @Id
* @ManyToOne(targetEntity="GH10334FooCollection", inversedBy="foos")
* @JoinColumn(name="foo_collection_id", referencedColumnName="id", nullable = false)
* @GeneratedValue
*/
protected $collection;
/**
* @var GH10334ProductTypeId
* @Id
* @Column(type="string", enumType="Doctrine\Tests\Models\GH10334\GH10334ProductTypeId")
*/
protected $productTypeId;
public function __construct(GH10334FooCollection $collection, GH10334ProductTypeId $productTypeId)
{
$this->collection = $collection;
$this->productTypeId = $productTypeId;
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\GH10334;
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 GH10334FooCollection
{
/**
* @var int
* @Id
* @Column(type="integer")
* @GeneratedValue
*/
protected $id;
/**
* @OneToMany(targetEntity="GH10334Foo", mappedBy="collection", cascade={"persist", "remove"})
* @var Collection<GH10334Foo> $foos
*/
private $foos;
public function __construct()
{
$this->foos = new ArrayCollection();
}
/**
* @return Collection<GH10334Foo>
*/
public function getFoos(): Collection
{
return $this->foos;
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\GH10334;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
/**
* @Entity
*/
class GH10334Product
{
/**
* @var int
* @Id
* @Column(name="product_id", type="integer")
* @GeneratedValue()
*/
protected $id;
/**
* @var string
* @Column(name="name", type="string")
*/
private $name;
/**
* @var GH10334ProductType $productType
* @ManyToOne(targetEntity="GH10334ProductType", inversedBy="products")
* @JoinColumn(name="product_type_id", referencedColumnName="id", nullable = false)
*/
private $productType;
public function __construct(string $name, GH10334ProductType $productType)
{
$this->name = $name;
$this->productType = $productType;
}
public function getProductType(): GH10334ProductType
{
return $this->productType;
}
public function setProductType(GH10334ProductType $productType): void
{
$this->productType = $productType;
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\GH10334;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToMany;
/**
* @Entity
*/
class GH10334ProductType
{
/**
* @var GH10334ProductTypeId
* @Id
* @Column(type="string", enumType="Doctrine\Tests\Models\GH10334\GH10334ProductTypeId")
*/
protected $id;
/**
* @var float
* @Column(type="float")
*/
private $value;
/**
* @OneToMany(targetEntity="GH10334Product", mappedBy="productType", cascade={"persist", "remove"})
* @var Collection $products
*/
private $products;
public function __construct(GH10334ProductTypeId $id, float $value)
{
$this->id = $id;
$this->value = $value;
$this->products = new ArrayCollection();
}
public function getId(): GH10334ProductTypeId
{
return $this->id;
}
public function addProduct(GH10334Product $product): void
{
$product->setProductType($this);
$this->products->add($product);
}
}
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\GH10334;
enum GH10334ProductTypeId: string
{
case Jean = 'jean';
case Short = 'short';
}
@@ -28,7 +28,7 @@ class ManyToManyEventTest extends OrmFunctionalTestCase
$evm->addEventListener(Events::postUpdate, $this->listener);
}
public function testListenerShouldBeNotifiedOnlyWhenUpdating(): void
public function testListenerShouldBeNotifiedWhenNewCollectionEntryAdded(): void
{
$user = $this->createNewValidUser();
$this->_em->persist($user);
@@ -44,6 +44,23 @@ class ManyToManyEventTest extends OrmFunctionalTestCase
self::assertTrue($this->listener->wasNotified);
}
public function testListenerShouldBeNotifiedWhenNewCollectionEntryRemoved(): void
{
$user = $this->createNewValidUser();
$group = new CmsGroup();
$group->name = 'admins';
$user->addGroup($group);
$this->_em->persist($user);
$this->_em->flush();
self::assertFalse($this->listener->wasNotified);
$this->_em->remove($group);
$this->_em->flush();
self::assertTrue($this->listener->wasNotified);
}
private function createNewValidUser(): CmsUser
{
$user = new CmsUser();
@@ -486,13 +486,35 @@ class QueryTest extends OrmFunctionalTestCase
public function testGetSingleScalarResultThrowsExceptionOnNoResult(): void
{
$this->expectException('Doctrine\ORM\NoResultException');
$this->_em->createQuery('select a from Doctrine\Tests\Models\CMS\CmsArticle a')
$this->_em->createQuery('select a.id from Doctrine\Tests\Models\CMS\CmsArticle a')
->getSingleScalarResult();
}
public function testGetSingleScalarResultThrowsExceptionOnSingleRowWithMultipleColumns(): void
{
$user = new CmsUser();
$user->name = 'Javier';
$user->username = 'phansys';
$user->status = 'developer';
$this->_em->persist($user);
$this->_em->flush();
$this->_em->clear();
$this->expectException(NonUniqueResultException::class);
$this->expectExceptionMessage(
'The query returned a row containing multiple columns. Change the query or use a different result function'
. ' like getScalarResult().'
);
$this->_em->createQuery('select u from Doctrine\Tests\Models\CMS\CmsUser u')
->setMaxResults(1)
->getSingleScalarResult();
}
public function testGetSingleScalarResultThrowsExceptionOnNonUniqueResult(): void
{
$this->expectException('Doctrine\ORM\NonUniqueResultException');
$user = new CmsUser();
$user->name = 'Guilherme';
$user->username = 'gblanco';
@@ -515,7 +537,12 @@ class QueryTest extends OrmFunctionalTestCase
$this->_em->flush();
$this->_em->clear();
$this->_em->createQuery('select a from Doctrine\Tests\Models\CMS\CmsArticle a')
$this->expectException(NonUniqueResultException::class);
$this->expectExceptionMessage(
'The query returned multiple rows. Change the query or use a different result function like getScalarResult().'
);
$this->_em->createQuery('select a.id from Doctrine\Tests\Models\CMS\CmsArticle a')
->getSingleScalarResult();
}
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH10049;
use Doctrine\Tests\OrmFunctionalTestCase;
/**
* @requires PHP 8.1
*/
class GH10049Test extends OrmFunctionalTestCase
{
public function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
ReadOnlyPropertyOwner::class,
ReadOnlyPropertyInheritor::class
);
}
/**
* @doesNotPerformAssertions
*/
public function testInheritedReadOnlyPropertyValueCanBeSet(): void
{
$child = new ReadOnlyPropertyInheritor(10049);
$this->_em->persist($child);
$this->_em->flush();
$this->_em->clear();
$this->_em->find(ReadOnlyPropertyInheritor::class, 10049);
}
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH10049;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class ReadOnlyPropertyInheritor extends ReadOnlyPropertyOwner
{
}
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH10049;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\MappedSuperclass
*/
abstract class ReadOnlyPropertyOwner
{
public function __construct(
/**
* @ORM\Id
* @ORM\Column(type="integer")
*/
public readonly int $id
) {
}
}
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Tests\Models\GH10334\GH10334Foo;
use Doctrine\Tests\Models\GH10334\GH10334FooCollection;
use Doctrine\Tests\Models\GH10334\GH10334Product;
use Doctrine\Tests\Models\GH10334\GH10334ProductType;
use Doctrine\Tests\Models\GH10334\GH10334ProductTypeId;
use Doctrine\Tests\OrmFunctionalTestCase;
/**
* @group GH10334Test
* @requires PHP 8.1
*/
class GH10334Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([GH10334FooCollection::class, GH10334Foo::class, GH10334ProductType::class, GH10334Product::class]);
}
public function testTicket(): void
{
$collection = new GH10334FooCollection();
$foo = new GH10334Foo($collection, GH10334ProductTypeId::Jean);
$foo2 = new GH10334Foo($collection, GH10334ProductTypeId::Short);
$this->_em->persist($collection);
$this->_em->persist($foo);
$this->_em->persist($foo2);
$this->_em->flush();
$this->_em->clear();
$result = $this->_em
->getRepository(GH10334FooCollection::class)
->createQueryBuilder('collection')
->leftJoin('collection.foos', 'foo')->addSelect('foo')
->getQuery()
->getResult();
$this->_em
->getRepository(GH10334FooCollection::class)
->createQueryBuilder('collection')
->leftJoin('collection.foos', 'foo')->addSelect('foo')
->getQuery()
->getResult();
$this->assertCount(1, $result);
$this->assertCount(2, $result[0]->getFoos());
}
public function testGetChildWithBackedEnumId(): void
{
$jean = new GH10334ProductType(GH10334ProductTypeId::Jean, 23.5);
$short = new GH10334ProductType(GH10334ProductTypeId::Short, 45.2);
$product = new GH10334Product('Extra Large Blue', $jean);
$jean->addProduct($product);
$this->_em->persist($jean);
$this->_em->persist($short);
$this->_em->persist($product);
$this->_em->flush();
$this->_em->clear();
$entity = $this->_em->find(GH10334Product::class, 1);
self::assertNotNull($entity);
self::assertSame($entity->getProductType()->getId(), GH10334ProductTypeId::Jean);
}
}
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
/**
* @group GH-10625
*/
class GH10625Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
GH10625Root::class,
GH10625Middle::class,
GH10625Leaf::class
);
}
/**
* @dataProvider queryClasses
*/
public function testLoadFieldsFromAllClassesInHierarchy(string $queryClass): void
{
$entity = new GH10625Leaf();
$this->_em->persist($entity);
$this->_em->flush();
$this->_em->clear();
$loadedEntity = $this->_em->find($queryClass, $entity->id);
self::assertNotNull($loadedEntity);
self::assertInstanceOf(GH10625Leaf::class, $loadedEntity);
}
public static function queryClasses(): array
{
return [
'query via root entity' => [GH10625Root::class],
'query via intermediate entity' => [GH10625Middle::class],
'query via leaf entity' => [GH10625Leaf::class],
];
}
}
/**
* @ORM\Entity
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorMap({ "1": "GH10625Leaf"})
* ^- This DiscriminatorMap contains the single non-abstract Entity class only
*/
abstract class GH10625Root
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
}
/**
* @ORM\Entity
*/
abstract class GH10625Middle extends GH10625Root
{
}
/**
* @ORM\Entity
*/
class GH10625Leaf extends GH10625Middle
{
}
@@ -5,8 +5,12 @@ declare(strict_types=1);
use Doctrine\ORM\Events;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\Tests\ORM\Tools\Export;
use Doctrine\Tests\ORM\Tools\Export\Address;
use Doctrine\Tests\ORM\Tools\Export\AddressListener;
use Doctrine\Tests\ORM\Tools\Export\Cart;
use Doctrine\Tests\ORM\Tools\Export\Group;
use Doctrine\Tests\ORM\Tools\Export\GroupListener;
use Doctrine\Tests\ORM\Tools\Export\Phonenumber;
use Doctrine\Tests\ORM\Tools\Export\UserListener;
$metadata->setInheritanceType(ClassMetadata::INHERITANCE_TYPE_NONE);
@@ -57,13 +61,13 @@ $metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_AUTO);
$metadata->mapManyToOne(
[
'fieldName' => 'mainGroup',
'targetEntity' => Export\Group::class,
'targetEntity' => Group::class,
]
);
$metadata->mapOneToOne(
[
'fieldName' => 'address',
'targetEntity' => Export\Address::class,
'targetEntity' => Address::class,
'inversedBy' => 'user',
'cascade' =>
[0 => 'persist'],
@@ -84,7 +88,7 @@ $metadata->mapOneToOne(
$metadata->mapOneToOne(
[
'fieldName' => 'cart',
'targetEntity' => Export\Cart::class,
'targetEntity' => Cart::class,
'mappedBy' => 'user',
'cascade' =>
[0 => 'persist'],
@@ -96,7 +100,7 @@ $metadata->mapOneToOne(
$metadata->mapOneToMany(
[
'fieldName' => 'phonenumbers',
'targetEntity' => Export\Phonenumber::class,
'targetEntity' => Phonenumber::class,
'cascade' =>
[
1 => 'persist',
@@ -31,6 +31,7 @@ use Doctrine\Tests\Mocks\ConnectionMock;
use Doctrine\Tests\Mocks\EntityManagerMock;
use Doctrine\Tests\Mocks\EntityPersisterMock;
use Doctrine\Tests\Mocks\UnitOfWorkMock;
use Doctrine\Tests\Models\CMS\CmsGroup;
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\Tests\Models\Forum\ForumAvatar;
@@ -866,6 +867,62 @@ class UnitOfWorkTest extends OrmTestCase
$this->expectException(EntityNotFoundException::class);
$this->_unitOfWork->getEntityIdentifier(new stdClass());
}
public function testRemovedEntityIsRemovedFromManyToManyCollection(): void
{
$group = new CmsGroup();
$group->name = 'test';
$this->_unitOfWork->persist($group);
$user = new CmsUser();
$user->name = 'test';
$user->groups->add($group);
$this->_unitOfWork->persist($user);
$this->_unitOfWork->commit();
self::assertFalse($user->groups->isDirty());
$this->_unitOfWork->remove($group);
$this->_unitOfWork->commit();
// Test that the removed entity has been removed from the many to many collection
self::assertEmpty(
$user->groups,
'the removed entity should have been removed from the many to many collection'
);
// Collection is clean, snapshot has been updated
self::assertFalse($user->groups->isDirty());
self::assertEmpty($user->groups->getSnapshot());
}
public function testRemovedEntityIsRemovedFromOneToManyCollection(): void
{
$user = new CmsUser();
$user->name = 'test';
$phonenumber = new CmsPhonenumber();
$phonenumber->phonenumber = '0800-123456';
$user->addPhonenumber($phonenumber);
$this->_unitOfWork->persist($user);
$this->_unitOfWork->persist($phonenumber);
$this->_unitOfWork->commit();
self::assertFalse($user->phonenumbers->isDirty());
$this->_unitOfWork->remove($phonenumber);
$this->_unitOfWork->commit();
// Test that the removed entity has been removed from the one to many collection
self::assertEmpty($user->phonenumbers);
// Collection is clean, snapshot has been updated
self::assertFalse($user->phonenumbers->isDirty());
self::assertEmpty($user->phonenumbers->getSnapshot());
}
}
/** @Entity */