Skip to content

Commit 1c9c422

Browse files
committed
Merge branch '3.3.x' into 4.0.x
* 3.3.x: Fix OneToManyPersister::deleteEntityCollection missing discriminator column/value. (doctrineGH-11500) Skip joined entity creation for empty relation (doctrine#10889) ci: maintained and stable mariadb version (11.4 current lts) (doctrine#11490) fix(docs): use string value in `addAttribute` Replace assertion with exception (doctrine#11489) Use ramsey/composer-install in PHPBench workflow update EntityManager#transactional to EntityManager#wrapInTransaction Fix cloning entities Consider usage of setFetchMode when checking for simultaneous usage of fetch-mode EAGER and WITH condition. Update branch metadata (doctrine#11474)
2 parents 00b29a1 + 41cb5fb commit 1c9c422

18 files changed

+366
-38
lines changed

.doctrine-project.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,23 @@
1111
"slug": "latest",
1212
"upcoming": true
1313
},
14+
{
15+
"name": "3.3",
16+
"branchName": "3.3.x",
17+
"slug": "3.3",
18+
"upcoming": true
19+
},
1420
{
1521
"name": "3.2",
1622
"branchName": "3.2.x",
1723
"slug": "3.2",
18-
"upcoming": true
24+
"current": true
1925
},
2026
{
2127
"name": "3.1",
2228
"branchName": "3.1.x",
2329
"slug": "3.1",
24-
"current": true
30+
"maintained": false
2531
},
2632
{
2733
"name": "3.0",

.github/workflows/continuous-integration.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ jobs:
182182
- "default"
183183
- "4@dev"
184184
mariadb-version:
185-
- "10.9"
185+
- "11.4"
186186
extension:
187187
- "mysqli"
188188
- "pdo_mysql"
@@ -191,11 +191,11 @@ jobs:
191191
mariadb:
192192
image: "mariadb:${{ matrix.mariadb-version }}"
193193
env:
194-
MYSQL_ALLOW_EMPTY_PASSWORD: yes
195-
MYSQL_DATABASE: "doctrine_tests"
194+
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: yes
195+
MARIADB_DATABASE: "doctrine_tests"
196196

197197
options: >-
198-
--health-cmd "mysqladmin ping --silent"
198+
--health-cmd "healthcheck.sh --connect --innodb_initialized"
199199
200200
ports:
201201
- "3306:3306"

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
| [4.0.x][4.0] | [3.2.x][3.2] | [3.1.x][3.1] | [2.20.x][2.20] | [2.19.x][2.19] |
1+
| [4.0.x][4.0] | [3.3.x][3.3] | [3.2.x][3.2] | [2.20.x][2.20] | [2.19.x][2.19] |
22
|:------------------------------------------------------:|:------------------------------------------------------:|:------------------------------------------------------:|:--------------------------------------------------------:|:--------------------------------------------------------:|
3-
| [![Build status][4.0 image]][4.0] | [![Build status][3.2 image]][3.2] | [![Build status][3.1 image]][3.1] | [![Build status][2.20 image]][2.20] | [![Build status][2.19 image]][2.19] |
4-
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.2 coverage image]][3.2 coverage] | [![Coverage Status][3.1 coverage image]][3.1 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] | [![Coverage Status][2.19 coverage image]][2.19 coverage] |
3+
| [![Build status][4.0 image]][4.0] | [![Build status][3.3 image]][3.3] | [![Build status][3.2 image]][3.2] | [![Build status][2.20 image]][2.20] | [![Build status][2.19 image]][2.19] |
4+
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.3 coverage image]][3.3 coverage] | [![Coverage Status][3.2 coverage image]][3.2 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] | [![Coverage Status][2.19 coverage image]][2.19 coverage] |
55

66
[<h1 align="center">🇺🇦 UKRAINE NEEDS YOUR HELP NOW!</h1>](https://www.doctrine-project.org/stop-war.html)
77

@@ -22,14 +22,14 @@ without requiring unnecessary code duplication.
2222
[4.0]: https://github.com/doctrine/orm/tree/4.0.x
2323
[4.0 coverage image]: https://codecov.io/gh/doctrine/orm/branch/4.0.x/graph/badge.svg
2424
[4.0 coverage]: https://codecov.io/gh/doctrine/orm/branch/4.0.x
25+
[3.3 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.3.x
26+
[3.3]: https://github.com/doctrine/orm/tree/3.3.x
27+
[3.3 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.3.x/graph/badge.svg
28+
[3.3 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.3.x
2529
[3.2 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.2.x
2630
[3.2]: https://github.com/doctrine/orm/tree/3.2.x
2731
[3.2 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.2.x/graph/badge.svg
2832
[3.2 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.2.x
29-
[3.1 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.1.x
30-
[3.1]: https://github.com/doctrine/orm/tree/3.1.x
31-
[3.1 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.1.x/graph/badge.svg
32-
[3.1 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.1.x
3333
[2.20 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.20.x
3434
[2.20]: https://github.com/doctrine/orm/tree/2.20.x
3535
[2.20 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.20.x/graph/badge.svg

docs/en/reference/transactions-and-concurrency.rst

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,29 +88,31 @@ requirement.
8888

8989
A more convenient alternative for explicit transaction demarcation is the use
9090
of provided control abstractions in the form of
91-
``Connection#transactional($func)`` and ``EntityManager#transactional($func)``.
91+
``Connection#transactional($func)`` and ``EntityManager#wrapInTransaction($func)``.
9292
When used, these control abstractions ensure that you never forget to rollback
9393
the transaction, in addition to the obvious code reduction. An example that is
9494
functionally equivalent to the previously shown code looks as follows:
9595

9696
.. code-block:: php
9797
9898
<?php
99+
// transactional with Connection instance
100+
// $conn instanceof Connection
101+
$conn->transactional(function($conn) {
102+
// ... do some work
103+
$user = new User;
104+
$user->setName('George');
105+
});
106+
107+
// transactional with EntityManager instance
99108
// $em instanceof EntityManager
100-
$em->transactional(function($em) {
109+
$em->wrapInTransaction(function($em) {
101110
// ... do some work
102111
$user = new User;
103112
$user->setName('George');
104113
$em->persist($user);
105114
});
106115
107-
.. warning::
108-
109-
For historical reasons, ``EntityManager#transactional($func)`` will return
110-
``true`` whenever the return value of ``$func`` is loosely false.
111-
Some examples of this include ``array()``, ``"0"``, ``""``, ``0``, and
112-
``null``.
113-
114116
The difference between ``Connection#transactional($func)`` and
115117
``EntityManager#transactional($func)`` is that the latter
116118
abstraction flushes the ``EntityManager`` prior to transaction

docs/en/tutorials/composite-primary-keys.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ We keep up the example of an Article with arbitrary attributes, the mapping look
145145
#[OneToMany(targetEntity: ArticleAttribute::class, mappedBy: 'article', cascade: ['ALL'], indexBy: 'attribute')]
146146
private Collection $attributes;
147147
148-
public function addAttribute(string $name, ArticleAttribute $value): void
148+
public function addAttribute(string $name, string $value): void
149149
{
150150
$this->attributes[$name] = new ArticleAttribute($name, $value, $this);
151151
}

psalm-baseline.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -738,7 +738,9 @@
738738
<code><![CDATA[$autoGenerate > 4]]></code>
739739
</TypeDoesNotContainType>
740740
<UndefinedMethod>
741-
<code><![CDATA[self::createLazyGhost($initializer, $skippedProperties)]]></code>
741+
<code><![CDATA[self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void {
742+
$initializer($object, $identifier);
743+
}, $skippedProperties)]]></code>
742744
</UndefinedMethod>
743745
<UnresolvableInclude>
744746
<code><![CDATA[require $fileName]]></code>

src/Internal/Hydration/ObjectHydrator.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -356,11 +356,15 @@ protected function hydrateRowData(array $row, array &$result): void
356356
$parentObject = $this->resultPointers[$parentAlias];
357357
} else {
358358
// Parent object of relation not found, mark as not-fetched again
359-
$element = $this->getEntity($data, $dqlAlias);
359+
if (isset($nonemptyComponents[$dqlAlias])) {
360+
$element = $this->getEntity($data, $dqlAlias);
360361

361-
// Update result pointer and provide initial fetch data for parent
362-
$this->resultPointers[$dqlAlias] = $element;
363-
$rowData['data'][$parentAlias][$relationField] = $element;
362+
// Update result pointer and provide initial fetch data for parent
363+
$this->resultPointers[$dqlAlias] = $element;
364+
$rowData['data'][$parentAlias][$relationField] = $element;
365+
} else {
366+
$element = null;
367+
}
364368

365369
// Mark as not-fetched again
366370
unset($this->hints['fetched'][$parentAlias][$relationField]);

src/Persisters/Collection/OneToManyPersister.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use Doctrine\Common\Collections\Criteria;
99
use Doctrine\DBAL\Exception as DBALException;
1010
use Doctrine\DBAL\Types\Type;
11+
use Doctrine\ORM\EntityNotFoundException;
12+
use Doctrine\ORM\Mapping\MappingException;
1113
use Doctrine\ORM\Mapping\OneToManyAssociationMapping;
1214
use Doctrine\ORM\PersistentCollection;
1315
use Doctrine\ORM\Utility\PersisterHelper;
@@ -146,7 +148,11 @@ public function loadCriteria(PersistentCollection $collection, Criteria $criteri
146148
throw new BadMethodCallException('Filtering a collection by Criteria is not supported by this CollectionPersister.');
147149
}
148150

149-
/** @throws DBALException */
151+
/**
152+
* @throws DBALException
153+
* @throws EntityNotFoundException
154+
* @throws MappingException
155+
*/
150156
private function deleteEntityCollection(PersistentCollection $collection): int
151157
{
152158
$mapping = $this->getMapping($collection);
@@ -166,6 +172,13 @@ private function deleteEntityCollection(PersistentCollection $collection): int
166172
$statement = 'DELETE FROM ' . $this->quoteStrategy->getTableName($targetClass, $this->platform)
167173
. ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
168174

175+
if ($targetClass->isInheritanceTypeSingleTable()) {
176+
$discriminatorColumn = $targetClass->getDiscriminatorColumn();
177+
$statement .= ' AND ' . $discriminatorColumn['name'] . ' = ?';
178+
$parameters[] = $targetClass->discriminatorValue;
179+
$types[] = $discriminatorColumn['type'];
180+
}
181+
169182
$numAffected = $this->conn->executeStatement($statement, $parameters, $types);
170183

171184
assert(is_int($numAffected));

src/Proxy/ProxyFactory.php

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,15 +210,14 @@ protected function skipClass(ClassMetadata $metadata): bool
210210
/**
211211
* Creates a closure capable of initializing a proxy
212212
*
213-
* @return Closure(InternalProxy, InternalProxy):void
213+
* @return Closure(InternalProxy, array):void
214214
*
215215
* @throws EntityNotFoundException
216216
*/
217217
private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure
218218
{
219-
return static function (InternalProxy $proxy) use ($entityPersister, $classMetadata, $identifierFlattener): void {
220-
$identifier = $classMetadata->getIdentifierValues($proxy);
221-
$original = $entityPersister->loadById($identifier);
219+
return static function (InternalProxy $proxy, array $identifier) use ($entityPersister, $classMetadata, $identifierFlattener): void {
220+
$original = $entityPersister->loadById($identifier);
222221

223222
if ($original === null) {
224223
throw EntityNotFoundException::fromClassNameAndIdentifier(
@@ -234,7 +233,7 @@ private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersi
234233
$class = $entityPersister->getClassMetadata();
235234

236235
foreach ($class->getReflectionProperties() as $property) {
237-
if (! $property || ! $class->hasField($property->getName()) && ! $class->hasAssociation($property->getName())) {
236+
if (! $property || isset($identifier[$property->getName()]) || ! $class->hasField($property->getName()) && ! $class->hasAssociation($property->getName())) {
238237
continue;
239238
}
240239

@@ -283,7 +282,9 @@ private function getProxyFactory(string $className): Closure
283282
$identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers);
284283

285284
$proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy {
286-
$proxy = self::createLazyGhost($initializer, $skippedProperties);
285+
$proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void {
286+
$initializer($object, $identifier);
287+
}, $skippedProperties);
287288

288289
foreach ($identifierFields as $idField => $reflector) {
289290
if (! isset($identifier[$idField])) {

src/Query/Parser.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2563,7 +2563,10 @@ public function ArithmeticPrimary(): AST\Node|string
25632563
return new AST\ParenthesisExpression($expr);
25642564
}
25652565

2566-
assert($this->lexer->lookahead !== null);
2566+
if ($this->lexer->lookahead === null) {
2567+
$this->syntaxError('ArithmeticPrimary');
2568+
}
2569+
25672570
switch ($this->lexer->lookahead->type) {
25682571
case TokenType::T_COALESCE:
25692572
case TokenType::T_NULLIF:

src/Query/SqlWalker.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -911,7 +911,9 @@ public function walkJoinAssociationDeclaration(
911911
}
912912
}
913913

914-
if ($relation->fetch === ClassMetadata::FETCH_EAGER && $condExpr !== null) {
914+
$fetchMode = $this->query->getHint('fetchMode')[$assoc->sourceEntity][$assoc->fieldName] ?? $relation->fetch;
915+
916+
if ($fetchMode === ClassMetadata::FETCH_EAGER && $condExpr !== null) {
915917
throw QueryException::eagerFetchJoinWithNotAllowed($assoc->sourceEntity, $assoc->fieldName);
916918
}
917919

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Models\ECommerce;
6+
7+
use Doctrine\ORM\Mapping\Column;
8+
use Doctrine\ORM\Mapping\Entity;
9+
use Doctrine\ORM\Mapping\GeneratedValue;
10+
use Doctrine\ORM\Mapping\Id;
11+
use Doctrine\ORM\Mapping\Index;
12+
use Doctrine\ORM\Mapping\Table;
13+
14+
/**
15+
* ECommerceProduct2
16+
* Resets the id when being cloned.
17+
*/
18+
#[Entity]
19+
#[Table(name: 'ecommerce_products')]
20+
#[Index(name: 'name_idx', columns: ['name'])]
21+
class ECommerceProduct2
22+
{
23+
#[Column]
24+
#[Id]
25+
#[GeneratedValue]
26+
private int|null $id = null;
27+
28+
#[Column(length: 50, nullable: true)]
29+
private string|null $name = null;
30+
31+
public function getId(): int|null
32+
{
33+
return $this->id;
34+
}
35+
36+
public function getName(): string|null
37+
{
38+
return $this->name;
39+
}
40+
41+
public function __clone()
42+
{
43+
$this->id = null;
44+
$this->name = 'Clone of ' . $this->name;
45+
}
46+
}

tests/Tests/ORM/Functional/EagerFetchCollectionTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@ public function testSubselectFetchJoinWithNotAllowed(): void
8989
$query->getResult();
9090
}
9191

92+
public function testSubselectFetchJoinWithAllowedWhenOverriddenNotEager(): void
93+
{
94+
$query = $this->_em->createQuery('SELECT o, c FROM ' . EagerFetchOwner::class . ' o JOIN o.children c WITH c.id = 1');
95+
$query->setFetchMode(EagerFetchChild::class, 'owner', ORM\ClassMetadata::FETCH_LAZY);
96+
97+
$this->assertIsString($query->getSql());
98+
}
99+
92100
public function testEagerFetchWithIterable(): void
93101
{
94102
$this->createOwnerWithChildren(2);

tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ protected function setUp(): void
5858
public function testPersistUpdate(): void
5959
{
6060
// Considering case (a)
61-
$proxy = $this->_em->getProxyFactory()->getProxy(CmsUser::class, ['id' => 123]);
61+
$proxy = $this->_em->getProxyFactory()->getProxy(CmsUser::class, ['id' => $this->user->getId()]);
6262

6363
$proxy->id = null;
6464
$proxy->username = 'ocra';

tests/Tests/ORM/Functional/ReferenceProxyTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Doctrine\ORM\Proxy\InternalProxy;
1010
use Doctrine\Tests\Models\Company\CompanyAuction;
1111
use Doctrine\Tests\Models\ECommerce\ECommerceProduct;
12+
use Doctrine\Tests\Models\ECommerce\ECommerceProduct2;
1213
use Doctrine\Tests\Models\ECommerce\ECommerceShipping;
1314
use Doctrine\Tests\OrmFunctionalTestCase;
1415
use PHPUnit\Framework\Attributes\Group;
@@ -112,6 +113,24 @@ public function testCloneProxy(): void
112113
self::assertFalse($entity->isCloned);
113114
}
114115

116+
public function testCloneProxyWithResetId(): void
117+
{
118+
$id = $this->createProduct();
119+
120+
$entity = $this->_em->getReference(ECommerceProduct2::class, $id);
121+
assert($entity instanceof ECommerceProduct2);
122+
123+
$clone = clone $entity;
124+
assert($clone instanceof ECommerceProduct2);
125+
126+
self::assertEquals($id, $entity->getId());
127+
self::assertEquals('Doctrine Cookbook', $entity->getName());
128+
129+
self::assertFalse($this->_em->contains($clone));
130+
self::assertNull($clone->getId());
131+
self::assertEquals('Clone of Doctrine Cookbook', $clone->getName());
132+
}
133+
115134
#[Group('DDC-733')]
116135
public function testInitializeProxy(): void
117136
{

0 commit comments

Comments
 (0)