Compare commits

...

23 Commits
3.6.0 ... 3.6.2

Author SHA1 Message Date
vvaswani
4262eb495b fix: update index to be serialized in __sleep() (#12366)
Signed-off-by: Vikram Vaswani <2571660+vvaswani@users.noreply.github.com`>
2026-01-30 22:41:41 +01:00
Grégoire Paris
d3b47d2cbb Merge pull request #12355 from doctrine/2.20.x
Merge 2.20.x up into 3.6.x
2026-01-25 12:48:28 +01:00
Grégoire Paris
026f5bfe1b Merge pull request #12350 from greg0ire/missing-order-by
Add missing ORDER BY clause
2026-01-18 10:41:46 +01:00
Grégoire Paris
0b0f2f4d86 Add missing ORDER BY clause
This causes transient failures with PostgreSQL. Order is not guaranteed.
2026-01-17 13:39:44 +01:00
Grégoire Paris
0bd839a720 Merge pull request #12345 from greg0ire/3.6.x
Merge 2.20.x up into 3.6.x
2026-01-16 18:27:03 +01:00
Grégoire Paris
b65004fc26 Merge remote-tracking branch 'origin/2.20.x' into 3.6.x 2026-01-16 18:24:38 +01:00
Grégoire Paris
d2418ab074 Merge pull request #12344 from greg0ire/update-baseline
Update PHPStan baseline
2026-01-15 23:38:15 +01:00
Grégoire Paris
39a05e31c9 Update PHPStan baseline
This is caused by the release of doctrine/collections 2.7.0. The error
message is a bit shorter now.
2026-01-15 20:13:05 +01:00
sasezaki
ab156a551c Update phpstan-dbal2 to phpstan-dbal3 in .gitattributes (#12343) 2026-01-12 12:53:04 +01:00
Grégoire Paris
2148940290 Merge pull request #12335 from greg0ire/gh-12166
Avoid lazy object initialization when initializing read-only property
2026-01-09 06:28:15 +01:00
Grégoire Paris
d3538095fd Merge pull request #12337 from greg0ire/3.6.x
Merge 2.20.x up into 3.6.x
2026-01-09 06:26:06 +01:00
Grégoire Paris
0c1bf14729 Merge remote-tracking branch 'origin/2.20.x' into 3.6.x 2026-01-08 08:55:21 +01:00
Grégoire Paris
3b8c23c51d Merge pull request #12336 from greg0ire/dmwydo
Stop mocking EventManager
2026-01-08 07:19:16 +01:00
Grégoire Paris
60d4ea694a Stop mocking EventManager
It is defined outside this repository, so let us not mock what we do not
own.
2026-01-07 21:52:47 +01:00
Grégoire Paris
e923bbc932 Avoid lazy object initialization when initializing read-only property
Initializing e.g. a readonly ID does not require loading any data from
the database. However, calling isInitialized() on the reflection of a
readonly property triggers the native lazy object initialization.
If we have a lazy property at hand, then the property cannot be initialized
already, so it is safe to skip the call.
2026-01-07 13:04:14 +01:00
n0099
8cbd34c666 Merge pull request #12060 from n0099/patch-1
Update doc `dql-custom-walkers.rst` with an output walker to interpolate parameters into SQL
2026-01-01 18:27:48 +01:00
Vladislav Sultanov
8bdefef6d1 Handle int-backed enums for values stored as string values in MySQL ENUM columns (#12275)
* FIX: Handle int-backed enums for values stored as string values in MySQL ENUM columns

Related issue: doctrine#12274

* FIX: Apply coding standard changes

Related issue: doctrine#12274

* FIX: Add unit test cases

Related issue: doctrine#12274
2025-12-30 18:21:10 +01:00
Grégoire Paris
0f8730a6e5 Merge pull request #12331 from greg0ire/a-the
Fix grammatical errors
2025-12-24 11:39:26 +01:00
Grégoire Paris
62477b5d42 Update branch metadata (#12327)
* Update branch metadata

3.6.0 has been released. As a consequence:

- 3.7.x is the next minor branch;
- 3.6.x is the current branch;
- 3.5.x is no longer maintained.

* doc: drop old releases

We should reduce the number of versions we have, so let's remove docs
for versions that have less than 2k downloads per day.
2025-12-21 00:29:57 +01:00
HypeMC
12116aa3c2 Fix docs regarding query hints (#12328) 2025-12-21 00:29:08 +01:00
Grégoire Paris
0aeddd0592 Fix grammatical errors 2025-12-20 15:34:16 +01:00
Grégoire Paris
2491c4b20d Merge pull request #12329 from HypeMC/fix-result-cache-examples
Fix result cache examples in docs
2025-12-20 09:14:39 +01:00
HypeMC
08d6167243 Fix result cache examples in docs 2025-12-20 01:21:11 +01:00
25 changed files with 257 additions and 154 deletions

View File

@@ -12,42 +12,17 @@
"upcoming": true
},
{
"name": "3.6",
"branchName": "3.6.x",
"slug": "3.6",
"name": "3.7",
"branchName": "3.7.x",
"slug": "3.7",
"upcoming": true
},
{
"name": "3.5",
"branchName": "3.5.x",
"slug": "3.5",
"name": "3.6",
"branchName": "3.6.x",
"slug": "3.6",
"current": true
},
{
"name": "3.4",
"slug": "3.4",
"maintained": false
},
{
"name": "3.3",
"slug": "3.3",
"maintained": false
},
{
"name": "3.2",
"slug": "3.2",
"maintained": false
},
{
"name": "3.1",
"slug": "3.1",
"maintained": false
},
{
"name": "3.0",
"slug": "3.0",
"maintained": false
},
{
"name": "2.21",
"branchName": "2.21.x",
@@ -89,26 +64,6 @@
"name": "2.14",
"slug": "2.14",
"maintained": false
},
{
"name": "2.13",
"slug": "2.13",
"maintained": false
},
{
"name": "2.12",
"slug": "2.12",
"maintained": false
},
{
"name": "2.11",
"slug": "2.11",
"maintained": false
},
{
"name": "2.10",
"slug": "2.10",
"maintained": false
}
]
}

2
.gitattributes vendored
View File

@@ -15,6 +15,6 @@ phpcs.xml.dist export-ignore
phpbench.json export-ignore
phpstan.neon export-ignore
phpstan-baseline.neon export-ignore
phpstan-dbal2.neon export-ignore
phpstan-dbal3.neon export-ignore
phpstan-params.neon export-ignore
phpstan-persistence2.neon export-ignore

View File

@@ -1,7 +1,7 @@
| [4.0.x][4.0] | [3.6.x][3.6] | [3.5.x][3.5] | [2.21.x][2.21] | [2.20.x][2.20] |
| [4.0.x][4.0] | [3.7.x][3.7] | [3.6.x][3.6] | [2.21.x][2.21] | [2.20.x][2.20] |
|:------------------------------------------------------:|:------------------------------------------------------:|:------------------------------------------------------:|:--------------------------------------------------------:|:--------------------------------------------------------:|
| [![Build status][4.0 image]][4.0 workflow] | [![Build status][3.6 image]][3.6 workflow] | [![Build status][3.5 image]][3.5 workflow] | [![Build status][2.21 image]][2.21 workflow] | [![Build status][2.20 image]][2.20 workflow] |
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.6 coverage image]][3.6 coverage] | [![Coverage Status][3.5 coverage image]][3.5 coverage] | [![Coverage Status][2.21 coverage image]][2.21 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] |
| [![Build status][4.0 image]][4.0 workflow] | [![Build status][3.7 image]][3.7 workflow] | [![Build status][3.6 image]][3.6 workflow] | [![Build status][2.21 image]][2.21 workflow] | [![Build status][2.20 image]][2.20 workflow] |
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.7 coverage image]][3.7 coverage] | [![Coverage Status][3.6 coverage image]][3.6 coverage] | [![Coverage Status][2.21 coverage image]][2.21 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] |
Doctrine ORM is an object-relational mapper for PHP 8.1+ that provides transparent persistence
for PHP objects. It sits on top of a powerful database abstraction layer (DBAL). One of its key features
@@ -21,16 +21,16 @@ without requiring unnecessary code duplication.
[4.0 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A4.0.x
[4.0 coverage image]: https://codecov.io/gh/doctrine/orm/branch/4.0.x/graph/badge.svg
[4.0 coverage]: https://codecov.io/gh/doctrine/orm/branch/4.0.x
[3.7 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.7.x
[3.7]: https://github.com/doctrine/orm/tree/3.7.x
[3.7 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A3.7.x
[3.7 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.7.x/graph/badge.svg
[3.7 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.7.x
[3.6 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.6.x
[3.6]: https://github.com/doctrine/orm/tree/3.6.x
[3.6 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A3.6.x
[3.6 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.6.x/graph/badge.svg
[3.6 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.6.x
[3.5 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.5.x
[3.5]: https://github.com/doctrine/orm/tree/3.5.x
[3.5 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A3.5.x
[3.5 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.5.x/graph/badge.svg
[3.5 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.5.x
[2.21 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.21.x
[2.21]: https://github.com/doctrine/orm/tree/2.21.x
[2.21 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A2.21.x

View File

@@ -2301,7 +2301,7 @@ from 2.0 have to configure the annotation driver if they don't use `Configuratio
## Scalar mappings can now be omitted from DQL result
You are now allowed to mark scalar SELECT expressions as HIDDEN an they are not hydrated anymore.
You are now allowed to mark scalar SELECT expressions as HIDDEN and they are not hydrated anymore.
Example:
SELECT u, SUM(a.id) AS HIDDEN numArticles FROM User u LEFT JOIN u.Articles a ORDER BY numArticles DESC HAVING numArticles > 10

View File

@@ -40,7 +40,8 @@ Now this is all awfully technical, so let me come to some use-cases
fast to keep you motivated. Using walker implementation you can for
example:
- Modify the Output walker to get the raw SQL via ``Query->getSQL()``
with interpolated parameters.
- Modify the AST to generate a Count Query to be used with a
paginator for any given DQL query.
- Modify the Output Walker to generate vendor-specific SQL
@@ -50,7 +51,7 @@ example:
- Modify the Output walker to pretty print the SQL for debugging
purposes.
In this cookbook-entry I will show examples of the first two
In this cookbook-entry I will show examples of the first three
points. There are probably much more use-cases.
Generic count query for pagination
@@ -223,3 +224,39 @@ huge benefits with using vendor specific features. This would still
allow you write DQL queries instead of NativeQueries to make use of
vendor specific features.
Modifying the Output Walker to get the raw SQL with interpolated parameters
---------------------------------------------------------------------------
Sometimes we may want to log or trace the raw SQL being generated from its DQL
for profiling slow queries afterwards or audit queries that changed many rows
``$query->getSQL()`` will give us the prepared statement being passed to database
with all values of SQL parameters being replaced by positional ``?`` or named ``:name``
as parameters are interpolated into prepared statements by the database while executing the SQL.
``$query->getParameters()`` will give us details about SQL parameters that we've provided.
So we can create an output walker to interpolate all SQL parameters that will be
passed into prepared statement in PHP before database handle them internally:
.. literalinclude:: dql-custom-walkers/InterpolateParametersSQLOutputWalker.php
:language: php
Then you may get the raw SQL with this output walker:
.. code-block:: php
<?php
$query
->where('t.int IN (:ints)')->setParameter(':ints', [1, 2])
->orWhere('t.string IN (?0)')->setParameter(0, ['3', '4'])
->orWhere("t.bool = ?1")->setParameter('?1', true)
->orWhere("t.string = :string")->setParameter(':string', 'ABC')
->setHint(\Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER, InterpolateParametersSQLOutputWalker::class)
->getSQL();
The where clause of the returned SQL should be like:
.. code-block:: sql
WHERE t0_.int IN (1, 2)
OR t0_.string IN ('3', '4')
OR t0_.bool = 1
OR t0_.string = 'ABC'

View File

@@ -0,0 +1,47 @@
<?php
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Types\BooleanType;
use Doctrine\DBAL\Types\Exception\ValueNotConvertible;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Query\AST;
use Doctrine\ORM\Query\SqlOutputWalker;
class InterpolateParametersSQLOutputWalker extends SqlOutputWalker
{
/** {@inheritdoc} */
public function walkInputParameter(AST\InputParameter $inputParam): string
{
$parameter = $this->getQuery()->getParameter($inputParam->name);
if ($parameter === null) {
return '?';
}
$value = $parameter->getValue();
/** @var ParameterType|ArrayParameterType|int|string $typeName */
/** @see \Doctrine\ORM\Query\ParameterTypeInferer::inferType() */
$typeName = $parameter->getType();
$platform = $this->getConnection()->getDatabasePlatform();
$processParameterType = static fn(ParameterType $type) => static fn($value): string =>
(match ($type) { /** @see Type::getBindingType() */
ParameterType::NULL => 'NULL',
ParameterType::INTEGER => $value,
ParameterType::BOOLEAN => (new BooleanType())->convertToDatabaseValue($value, $platform),
ParameterType::STRING, ParameterType::ASCII => $platform->quoteStringLiteral($value),
default => throw new ValueNotConvertible($value, $type->name)
});
if (is_string($typeName) && Type::hasType($typeName)) {
return Type::getType($typeName)->convertToDatabaseValue($value, $platform);
}
if ($typeName instanceof ParameterType) {
return $processParameterType($typeName)($value);
}
if ($typeName instanceof ArrayParameterType && is_array($value)) {
$type = ArrayParameterType::toElementParameterType($typeName);
return implode(', ', array_map($processParameterType($type), $value));
}
throw new ValueNotConvertible($value, $typeName);
}
}

View File

@@ -1411,8 +1411,7 @@ Result Cache API:
$query->setResultCacheDriver(new ApcCache());
$query->useResultCache(true)
->setResultCacheLifeTime(3600);
$query->enableResultCache(3600);
$result = $query->getResult(); // cache miss
@@ -1422,8 +1421,8 @@ Result Cache API:
$query->setResultCacheId('my_query_result');
$result = $query->getResult(); // saved in given result cache id.
// or call useResultCache() with all parameters:
$query->useResultCache(true, 3600, 'my_query_result');
// or call enableResultCache() with all parameters:
$query->enableResultCache(3600, 'my_query_result');
$result = $query->getResult(); // cache hit!
// Introspection

View File

@@ -344,10 +344,10 @@ the Query object which can be retrieved from ``EntityManager#createQuery()``.
Executing a Query
^^^^^^^^^^^^^^^^^
The QueryBuilder is a builder object only - it has no means of actually
executing the Query. Additionally a set of parameters such as query hints
cannot be set on the QueryBuilder itself. This is why you always have to convert
a querybuilder instance into a Query object:
The QueryBuilder is only a builder object - it has no means of actually
executing the Query. Additional functionality, such as enabling the result cache,
cannot be set on the QueryBuilder itself. This is why you must always convert
a QueryBuilder instance into a Query object:
.. code-block:: php
@@ -355,9 +355,8 @@ a querybuilder instance into a Query object:
// $qb instanceof QueryBuilder
$query = $qb->getQuery();
// Set additional Query options
$query->setQueryHint('foo', 'bar');
$query->useResultCache('my_cache_id');
// Enable the result cache
$query->enableResultCache(3600, 'my_custom_id');
// Execute Query
$result = $query->getResult();

View File

@@ -133,7 +133,7 @@ Caching mode
* Read Write cache employs locks before update/delete.
* Use if data needs to be updated.
* Slowest strategy.
* To use it a the cache region implementation must support locking.
* To use it the cache region implementation must support locking.
Built-in cached persisters

View File

@@ -22,7 +22,7 @@ have to register them yourself.
All the commands of the Doctrine Console require access to the
``EntityManager``. You have to inject it into the console application.
Here is an example of a the project-specific ``bin/doctrine`` binary.
Here is an example of a project-specific ``bin/doctrine`` binary.
.. code-block:: php

View File

@@ -619,7 +619,7 @@ parameters:
path: src/EntityRepository.php
-
message: '#^Method Doctrine\\ORM\\EntityRepository\:\:matching\(\) should return Doctrine\\Common\\Collections\\AbstractLazyCollection\<int, T of object\>&Doctrine\\Common\\Collections\\Selectable\<int, T of object\> but returns Doctrine\\ORM\\LazyCriteriaCollection\<\(int\|string\), object\>\.$#'
message: '#^Method Doctrine\\ORM\\EntityRepository\:\:matching\(\) should return Doctrine\\Common\\Collections\\AbstractLazyCollection\<int, T of object\> but returns Doctrine\\ORM\\LazyCriteriaCollection\<\(int\|string\), object\>\.$#'
identifier: return.type
count: 1
path: src/EntityRepository.php

View File

@@ -1065,7 +1065,7 @@ abstract class AbstractQuery
}
/**
* Executes the query and returns a the resulting Statement object.
* Executes the query and returns the resulting Statement object.
*
* @return Result|int The executed database statement that holds
* the results, or an integer indicating how

View File

@@ -17,6 +17,7 @@ use Doctrine\ORM\UnitOfWork;
use Generator;
use LogicException;
use ReflectionClass;
use ReflectionEnum;
use function array_key_exists;
use function array_keys;
@@ -597,13 +598,18 @@ abstract class AbstractHydrator
*/
final protected function buildEnum(mixed $value, string $enumType): BackedEnum|array
{
$reflection = new ReflectionEnum($enumType);
$isIntBacked = $reflection->isBacked() && $reflection->getBackingType()->getName() === 'int';
if (is_array($value)) {
return array_map(
static fn ($value) => $enumType::from($value),
static fn ($value) => $enumType::from($isIntBacked ? (int) $value : $value),
$value,
);
}
$value = $isIntBacked ? (int) $value : $value;
return $enumType::from($value);
}
}

View File

@@ -142,7 +142,7 @@ final class FieldMapping implements ArrayAccess
{
$serialized = ['type', 'fieldName', 'columnName'];
foreach (['nullable', 'notInsertable', 'notUpdatable', 'id', 'unique', 'version', 'quoted'] as $boolKey) {
foreach (['nullable', 'notInsertable', 'notUpdatable', 'id', 'unique', 'version', 'quoted', 'index'] as $boolKey) {
if ($this->$boolKey) {
$serialized[] = $boolKey;
}

View File

@@ -179,7 +179,7 @@ class MappingException extends PersistenceMappingException implements ORMExcepti
public static function joinTableRequired(string $fieldName): self
{
return new self(sprintf("The mapping of field '%s' requires an the 'joinTable' attribute.", $fieldName));
return new self(sprintf("The mapping of field '%s' requires the 'joinTable' attribute.", $fieldName));
}
/**

View File

@@ -10,6 +10,8 @@ use ReflectionProperty;
use function sprintf;
use const PHP_VERSION_ID;
/** @internal */
class ReadonlyAccessor implements PropertyAccessor
{
@@ -26,7 +28,12 @@ class ReadonlyAccessor implements PropertyAccessor
public function setValue(object $object, mixed $value): void
{
if (! $this->reflectionProperty->isInitialized($object)) {
/* For lazy properties, skip the isInitialized() check
because it would trigger the initialization of the whole object. */
if (
PHP_VERSION_ID >= 80400 && $this->reflectionProperty->isLazy($object)
|| ! $this->reflectionProperty->isInitialized($object)
) {
$this->parent->setValue($object, $value);
return;

View File

@@ -678,7 +678,7 @@ SQL,
public function testDifferentResultLengthsDoNotRequireExtraQueryCacheEntries(): void
{
$dql = 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id >= :id';
$dql = 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id >= :id ORDER BY u.id';
$query = $this->_em->createQuery($dql);
$query->setMaxResults(10);

View File

@@ -50,7 +50,7 @@ class DDC1452Test extends OrmFunctionalTestCase
$this->_em->flush();
$this->_em->clear();
$dql = 'SELECT a, b, ba FROM ' . __NAMESPACE__ . '\DDC1452EntityA AS a LEFT JOIN a.entitiesB AS b LEFT JOIN b.entityATo AS ba';
$dql = 'SELECT a, b, ba FROM ' . __NAMESPACE__ . '\DDC1452EntityA AS a LEFT JOIN a.entitiesB AS b LEFT JOIN b.entityATo AS ba ORDER BY a.id';
$results = $this->_em->createQuery($dql)->setMaxResults(1)->getResult();
self::assertSame($results[0], $results[0]->entitiesB[0]->entityAFrom);

View File

@@ -49,7 +49,7 @@ class DDC2359Test extends TestCase
$entityManager->expects(self::any())->method('getConnection')->willReturn($connection);
$entityManager
->method('getEventManager')
->willReturn($this->createMock(EventManager::class));
->willReturn(new EventManager());
$metadataFactory->method('newClassMetadataInstance')->willReturn($mockMetadata);
$metadataFactory->expects(self::once())->method('wakeupReflection');

View File

@@ -34,7 +34,7 @@ class GH10387Test extends OrmTestCase
{
yield 'hierarchy with Entity classes only' => [[GH10387EntitiesOnlyRoot::class, GH10387EntitiesOnlyMiddle::class, GH10387EntitiesOnlyLeaf::class]];
yield 'MappedSuperclass in the middle of the hierarchy' => [[GH10387MappedSuperclassRoot::class, GH10387MappedSuperclassMiddle::class, GH10387MappedSuperclassLeaf::class]];
yield 'abstract entity the the root and in the middle of the hierarchy' => [[GH10387AbstractEntitiesRoot::class, GH10387AbstractEntitiesMiddle::class, GH10387AbstractEntitiesLeaf::class]];
yield 'abstract entity at the root and in the middle of the hierarchy' => [[GH10387AbstractEntitiesRoot::class, GH10387AbstractEntitiesMiddle::class, GH10387AbstractEntitiesLeaf::class]];
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH12166;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH12166Test extends OrmFunctionalTestCase
{
public function testProxyWithReadonlyIdIsNotInitializedImmediately(): void
{
$this->createSchemaForModels(LazyEntityWithReadonlyId::class);
$this->_em->persist(new LazyEntityWithReadonlyId(123, 'Test Name'));
$this->_em->flush();
$this->_em->clear();
$proxy = $this->_em->getReference(LazyEntityWithReadonlyId::class, 123);
$reflClass = $this->_em->getClassMetadata(LazyEntityWithReadonlyId::class)->reflClass;
self::assertTrue(
$this->isUninitializedObject($proxy),
'Proxy should remain uninitialized after creation',
);
$id = $proxy->getId();
self::assertSame(123, $id);
self::assertTrue(
$this->isUninitializedObject($proxy),
'Proxy should remain uninitialized after accessing ID',
);
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH12166;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\Table;
#[Entity]
#[Table(name: 'gh12166_lazy_entity')]
class LazyEntityWithReadonlyId
{
#[Column(type: 'integer')]
#[Id]
private readonly int $id;
#[Column(type: 'string')]
private string $name;
public function __construct(int $id, string $name)
{
$this->id = $id;
$this->name = $name;
}
public function getId(): int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
}

View File

@@ -23,7 +23,7 @@ class GH6394Test extends OrmFunctionalTestCase
}
/**
* Test the the version of an entity can be fetched, when the id field and
* Test the version of an entity can be fetched, when the id field and
* the id column are different.
*/
#[Group('6393')]

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Hydration;
use BackedEnum;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
@@ -12,6 +13,8 @@ use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Events;
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Tests\Models\Enums\AccessLevel;
use Doctrine\Tests\Models\Enums\UserStatus;
use Doctrine\Tests\Models\Hydration\SimpleEntity;
use Doctrine\Tests\OrmFunctionalTestCase;
use LogicException;
@@ -24,7 +27,7 @@ use function iterator_to_array;
#[CoversClass(AbstractHydrator::class)]
class AbstractHydratorTest extends OrmFunctionalTestCase
{
private EventManager&MockObject $mockEventManager;
private EventManager $eventManager;
private Result&MockObject $mockResult;
private ResultSetMapping&MockObject $mockResultMapping;
private DummyHydrator $hydrator;
@@ -35,7 +38,7 @@ class AbstractHydratorTest extends OrmFunctionalTestCase
$mockConnection = $this->createMock(Connection::class);
$mockEntityManagerInterface = $this->createMock(EntityManagerInterface::class);
$this->mockEventManager = $this->createMock(EventManager::class);
$this->eventManager = new EventManager();
$this->mockResult = $this->createMock(Result::class);
$this->mockResultMapping = $this->createMock(ResultSetMapping::class);
@@ -44,7 +47,7 @@ class AbstractHydratorTest extends OrmFunctionalTestCase
->willReturn($this->createMock(AbstractPlatform::class));
$mockEntityManagerInterface
->method('getEventManager')
->willReturn($this->mockEventManager);
->willReturn($this->eventManager);
$mockEntityManagerInterface
->method('getConnection')
->willReturn($mockConnection);
@@ -63,85 +66,47 @@ class AbstractHydratorTest extends OrmFunctionalTestCase
#[Group('#1515')]
public function testOnClearEventListenerIsDetachedOnCleanup(): void
{
$eventListenerHasBeenRegistered = false;
$this
->mockEventManager
->expects(self::once())
->method('addEventListener')
->with([Events::onClear], $this->hydrator)
->willReturnCallback(function () use (&$eventListenerHasBeenRegistered): void {
$this->assertFalse($eventListenerHasBeenRegistered);
$eventListenerHasBeenRegistered = true;
});
$this
->mockEventManager
->expects(self::once())
->method('removeEventListener')
->with([Events::onClear], $this->hydrator)
->willReturnCallback(function () use (&$eventListenerHasBeenRegistered): void {
$this->assertTrue($eventListenerHasBeenRegistered);
});
iterator_to_array($this->hydrator->toIterable($this->mockResult, $this->mockResultMapping));
$iterator = $this->hydrator->toIterable($this->mockResult, $this->mockResultMapping);
iterator_to_array($iterator);
self::assertTrue($this->hydrator->hasListener);
self::assertFalse($this->eventManager->hasListeners(Events::onClear));
}
#[Group('#6623')]
public function testHydrateAllRegistersAndClearsAllAttachedListeners(): void
{
$eventListenerHasBeenRegistered = false;
$this
->mockEventManager
->expects(self::once())
->method('addEventListener')
->with([Events::onClear], $this->hydrator)
->willReturnCallback(function () use (&$eventListenerHasBeenRegistered): void {
$this->assertFalse($eventListenerHasBeenRegistered);
$eventListenerHasBeenRegistered = true;
});
$this
->mockEventManager
->expects(self::once())
->method('removeEventListener')
->with([Events::onClear], $this->hydrator)
->willReturnCallback(function () use (&$eventListenerHasBeenRegistered): void {
$this->assertTrue($eventListenerHasBeenRegistered);
});
$this->hydrator->hydrateAll($this->mockResult, $this->mockResultMapping);
self::assertTrue($this->hydrator->hasListener);
self::assertFalse($this->eventManager->hasListeners(Events::onClear));
}
#[Group('#8482')]
public function testHydrateAllClearsAllAttachedListenersEvenOnError(): void
{
$eventListenerHasBeenRegistered = false;
$this
->mockEventManager
->expects(self::once())
->method('addEventListener')
->with([Events::onClear], $this->hydrator)
->willReturnCallback(function () use (&$eventListenerHasBeenRegistered): void {
$this->assertFalse($eventListenerHasBeenRegistered);
$eventListenerHasBeenRegistered = true;
});
$this
->mockEventManager
->expects(self::once())
->method('removeEventListener')
->with([Events::onClear], $this->hydrator)
->willReturnCallback(function () use (&$eventListenerHasBeenRegistered): void {
$this->assertTrue($eventListenerHasBeenRegistered);
});
$this->hydrator->throwException = true;
$this->expectException(LogicException::class);
$this->hydrator->hydrateAll($this->mockResult, $this->mockResultMapping);
self::assertTrue($this->hydrator->hasListener);
self::assertFalse($this->eventManager->hasListeners(Events::onClear));
}
public function testEnumCastsIntegerBackedEnumValues(): void
{
$accessLevel = $this->hydrator->buildEnumForTesting('2', AccessLevel::class);
$userStatus = $this->hydrator->buildEnumForTesting('active', UserStatus::class);
self::assertSame(AccessLevel::User, $accessLevel);
self::assertSame(UserStatus::Active, $userStatus);
}
public function testEnumCastsIntegerBackedEnumArrayValues(): void
{
$accessLevels = $this->hydrator->buildEnumForTesting(['1', '2'], AccessLevel::class);
$userStatus = $this->hydrator->buildEnumForTesting(['active', 'inactive'], UserStatus::class);
self::assertSame([AccessLevel::Admin, AccessLevel::User], $accessLevels);
self::assertSame([UserStatus::Active, UserStatus::Inactive], $userStatus);
}
public function testToIterableIfYieldAndBreakBeforeFinishAlwaysCleansUp(): void
@@ -177,6 +142,12 @@ class AbstractHydratorTest extends OrmFunctionalTestCase
class DummyHydrator extends AbstractHydrator
{
public bool $throwException = false;
public bool $hasListener = false;
public function buildEnumForTesting(mixed $value, string $enumType): BackedEnum|array
{
return $this->buildEnum($value, $enumType);
}
/** @return array{} */
protected function hydrateAllData(): array
@@ -187,4 +158,9 @@ class DummyHydrator extends AbstractHydrator
return [];
}
public function prepare(): void
{
$this->hasListener = $this->em->getEventManager()->hasListeners(Events::onClear);
}
}

View File

@@ -33,6 +33,7 @@ final class FieldMappingTest extends TestCase
$mapping->precision = 10;
$mapping->scale = 2;
$mapping->unique = true;
$mapping->index = true;
$mapping->inherited = self::class;
$mapping->originalClass = self::class;
$mapping->originalField = 'id';
@@ -57,6 +58,7 @@ final class FieldMappingTest extends TestCase
self::assertSame(10, $resurrectedMapping->precision);
self::assertSame(2, $resurrectedMapping->scale);
self::assertTrue($resurrectedMapping->unique);
self::assertTrue($resurrectedMapping->index);
self::assertSame(self::class, $resurrectedMapping->inherited);
self::assertSame(self::class, $resurrectedMapping->originalClass);
self::assertSame('id', $resurrectedMapping->originalField);