mirror of
https://github.com/doctrine/orm.git
synced 2026-04-25 15:38:10 +02:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf449bef7d | |||
| 152f91fa33 | |||
| 14d1eb5340 | |||
| da0998c401 | |||
| cc011d8215 | |||
| d831c126c9 | |||
| 10eae1a7ff | |||
| 125e18cf24 | |||
| 8fba9d6868 | |||
| 7f4f1cda71 | |||
| 1c905b0e0a | |||
| 7901790b97 | |||
| cef1d2d740 | |||
| 8126882305 | |||
| a9513692cb | |||
| 52c3d9d82a | |||
| 2f46e5a130 | |||
| a3fa1d7faa | |||
| c7c57be0c2 | |||
| 721794fb9c | |||
| 70477d81e9 | |||
| 1ae74b8ec5 | |||
| 09b4a75ed3 |
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user