Skip to content

Commit

Permalink
Merge branch '2.19.x' into 2.20.x
Browse files Browse the repository at this point in the history
* 2.19.x:
  Fix OneToManyPersister::deleteEntityCollection missing discriminator column/value. (GH-11500)
  Skip joined entity creation for empty relation (#10889)
  ci: maintained and stable mariadb version (11.4 current lts) (#11490)
  fix(docs): use string value in `addAttribute`
  Replace assertion with exception (#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.
  • Loading branch information
derrabus committed Jun 18, 2024
2 parents 066ec1a + cc2ad19 commit 83851a9
Show file tree
Hide file tree
Showing 17 changed files with 421 additions and 38 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ jobs:
- "default"
- "3@dev"
mariadb-version:
- "10.9"
- "11.4"
extension:
- "mysqli"
- "pdo_mysql"
Expand All @@ -204,11 +204,11 @@ jobs:
mariadb:
image: "mariadb:${{ matrix.mariadb-version }}"
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: "doctrine_tests"
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: yes
MARIADB_DATABASE: "doctrine_tests"

options: >-
--health-cmd "mysqladmin ping --silent"
--health-cmd "healthcheck.sh --connect --innodb_initialized"
ports:
- "3306:3306"
Expand Down
11 changes: 2 additions & 9 deletions .github/workflows/phpbench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,8 @@ jobs:
coverage: "pcov"
ini-values: "zend.assertions=1, apc.enable_cli=1"

- name: "Cache dependencies installed with composer"
uses: "actions/cache@v3"
with:
path: "~/.composer/cache"
key: "php-${{ matrix.php-version }}-composer-locked-${{ hashFiles('composer.lock') }}"
restore-keys: "php-${{ matrix.php-version }}-composer-locked-"

- name: "Install dependencies with composer"
run: "composer update --no-interaction --no-progress"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v3"

- name: "Run PHPBench"
run: "vendor/bin/phpbench run --report=default"
20 changes: 11 additions & 9 deletions docs/en/reference/transactions-and-concurrency.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,29 +88,31 @@ requirement.

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

.. code-block:: php
<?php
// transactional with Connection instance
// $conn instanceof Connection
$conn->transactional(function($conn) {
// ... do some work
$user = new User;
$user->setName('George');
});
// transactional with EntityManager instance
// $em instanceof EntityManager
$em->transactional(function($em) {
$em->wrapInTransaction(function($em) {
// ... do some work
$user = new User;
$user->setName('George');
$em->persist($user);
});
.. warning::

For historical reasons, ``EntityManager#transactional($func)`` will return
``true`` whenever the return value of ``$func`` is loosely false.
Some examples of this include ``array()``, ``"0"``, ``""``, ``0``, and
``null``.

The difference between ``Connection#transactional($func)`` and
``EntityManager#transactional($func)`` is that the latter
abstraction flushes the ``EntityManager`` prior to transaction
Expand Down
2 changes: 1 addition & 1 deletion docs/en/tutorials/composite-primary-keys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ We keep up the example of an Article with arbitrary attributes, the mapping look
#[OneToMany(targetEntity: ArticleAttribute::class, mappedBy: 'article', cascade: ['ALL'], indexBy: 'attribute')]
private Collection $attributes;
public function addAttribute(string $name, ArticleAttribute $value): void
public function addAttribute(string $name, string $value): void
{
$this->attributes[$name] = new ArticleAttribute($name, $value, $this);
}
Expand Down
4 changes: 3 additions & 1 deletion psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1494,7 +1494,9 @@
<code><![CDATA[__wakeup]]></code>
</UndefinedInterfaceMethod>
<UndefinedMethod>
<code><![CDATA[self::createLazyGhost($initializer, $skippedProperties)]]></code>
<code><![CDATA[self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void {
$initializer($object, $identifier);
}, $skippedProperties)]]></code>
</UndefinedMethod>
<UnresolvableInclude>
<code><![CDATA[require $fileName]]></code>
Expand Down
12 changes: 8 additions & 4 deletions src/Internal/Hydration/ObjectHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -367,11 +367,15 @@ protected function hydrateRowData(array $row, array &$result)
$parentObject = $this->resultPointers[$parentAlias];
} else {
// Parent object of relation not found, mark as not-fetched again
$element = $this->getEntity($data, $dqlAlias);
if (isset($nonemptyComponents[$dqlAlias])) {
$element = $this->getEntity($data, $dqlAlias);

// Update result pointer and provide initial fetch data for parent
$this->resultPointers[$dqlAlias] = $element;
$rowData['data'][$parentAlias][$relationField] = $element;
// Update result pointer and provide initial fetch data for parent
$this->resultPointers[$dqlAlias] = $element;
$rowData['data'][$parentAlias][$relationField] = $element;
} else {
$element = null;
}

// Mark as not-fetched again
unset($this->_hints['fetched'][$parentAlias][$relationField]);
Expand Down
15 changes: 14 additions & 1 deletion src/Persisters/Collection/OneToManyPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
use Doctrine\Common\Collections\Criteria;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\EntityNotFoundException;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Utility\PersisterHelper;

Expand Down Expand Up @@ -166,7 +168,11 @@ public function loadCriteria(PersistentCollection $collection, Criteria $criteri
throw new BadMethodCallException('Filtering a collection by Criteria is not supported by this CollectionPersister.');
}

/** @throws DBALException */
/**
* @throws DBALException
* @throws EntityNotFoundException
* @throws MappingException
*/
private function deleteEntityCollection(PersistentCollection $collection): int
{
$mapping = $collection->getMapping();
Expand All @@ -186,6 +192,13 @@ private function deleteEntityCollection(PersistentCollection $collection): int
$statement = 'DELETE FROM ' . $this->quoteStrategy->getTableName($targetClass, $this->platform)
. ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';

if ($targetClass->isInheritanceTypeSingleTable()) {
$discriminatorColumn = $targetClass->getDiscriminatorColumn();
$statement .= ' AND ' . $discriminatorColumn['name'] . ' = ?';
$parameters[] = $targetClass->discriminatorValue;
$types[] = $discriminatorColumn['type'];
}

$numAffected = $this->conn->executeStatement($statement, $parameters, $types);

assert(is_int($numAffected));
Expand Down
13 changes: 7 additions & 6 deletions src/Proxy/ProxyFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -354,15 +354,14 @@ private function createInitializer(ClassMetadata $classMetadata, EntityPersister
/**
* Creates a closure capable of initializing a proxy
*
* @return Closure(InternalProxy, InternalProxy):void
* @return Closure(InternalProxy, array):void
*
* @throws EntityNotFoundException
*/
private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure
{
return static function (InternalProxy $proxy) use ($entityPersister, $classMetadata, $identifierFlattener): void {
$identifier = $classMetadata->getIdentifierValues($proxy);
$original = $entityPersister->loadById($identifier);
return static function (InternalProxy $proxy, array $identifier) use ($entityPersister, $classMetadata, $identifierFlattener): void {
$original = $entityPersister->loadById($identifier);

if ($original === null) {
throw EntityNotFoundException::fromClassNameAndIdentifier(
Expand All @@ -378,7 +377,7 @@ private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersi
$class = $entityPersister->getClassMetadata();

foreach ($class->getReflectionProperties() as $property) {
if (! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) {
if (isset($identifier[$property->name]) || ! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) {
continue;
}

Expand Down Expand Up @@ -468,7 +467,9 @@ private function getProxyFactory(string $className): Closure
$identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers);

$proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy {
$proxy = self::createLazyGhost($initializer, $skippedProperties);
$proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void {
$initializer($object, $identifier);
}, $skippedProperties);

foreach ($identifierFields as $idField => $reflector) {
if (! isset($identifier[$idField])) {
Expand Down
5 changes: 4 additions & 1 deletion src/Query/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -2926,7 +2926,10 @@ public function ArithmeticPrimary()
return new AST\ParenthesisExpression($expr);
}

assert($this->lexer->lookahead !== null);
if ($this->lexer->lookahead === null) {
$this->syntaxError('ArithmeticPrimary');
}

switch ($this->lexer->lookahead->type) {
case TokenType::T_COALESCE:
case TokenType::T_NULLIF:
Expand Down
4 changes: 3 additions & 1 deletion src/Query/SqlWalker.php
Original file line number Diff line number Diff line change
Expand Up @@ -1062,7 +1062,9 @@ public function walkJoinAssociationDeclaration($joinAssociationDeclaration, $joi
}
}

if ($relation['fetch'] === ClassMetadata::FETCH_EAGER && $condExpr !== null) {
$fetchMode = $this->query->getHint('fetchMode')[$assoc['sourceEntity']][$assoc['fieldName']] ?? $relation['fetch'];

if ($fetchMode === ClassMetadata::FETCH_EAGER && $condExpr !== null) {
throw QueryException::eagerFetchJoinWithNotAllowed($assoc['sourceEntity'], $assoc['fieldName']);
}

Expand Down
52 changes: 52 additions & 0 deletions tests/Tests/Models/ECommerce/ECommerceProduct2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\ECommerce;

use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\Index;
use Doctrine\ORM\Mapping\Table;

/**
* ECommerceProduct2
* Resets the id when being cloned.
*
* @Entity
* @Table(name="ecommerce_products",indexes={@Index(name="name_idx", columns={"name"})})
*/
class ECommerceProduct2
{
/**
* @var int|null
* @Column(type="integer")
* @Id
* @GeneratedValue
*/
private $id;

/**
* @var string
* @Column(type="string", length=50, nullable=true)
*/
private $name;

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

public function getName(): string
{
return $this->name;
}

public function __clone()
{
$this->id = null;
$this->name = 'Clone of ' . $this->name;
}
}
8 changes: 8 additions & 0 deletions tests/Tests/ORM/Functional/EagerFetchCollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ public function testSubselectFetchJoinWithNotAllowed(): void
$query->getResult();
}

public function testSubselectFetchJoinWithAllowedWhenOverriddenNotEager(): void
{
$query = $this->_em->createQuery('SELECT o, c FROM ' . EagerFetchOwner::class . ' o JOIN o.children c WITH c.id = 1');
$query->setFetchMode(EagerFetchChild::class, 'owner', ORM\ClassMetadata::FETCH_LAZY);

$this->assertIsString($query->getSql());
}

public function testEagerFetchWithIterable(): void
{
$this->createOwnerWithChildren(2);
Expand Down
2 changes: 1 addition & 1 deletion tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ protected function setUp(): void
public function testPersistUpdate(): void
{
// Considering case (a)
$proxy = $this->_em->getProxyFactory()->getProxy(CmsUser::class, ['id' => 123]);
$proxy = $this->_em->getProxyFactory()->getProxy(CmsUser::class, ['id' => $this->user->getId()]);

$proxy->id = null;
$proxy->username = 'ocra';
Expand Down
19 changes: 19 additions & 0 deletions tests/Tests/ORM/Functional/ReferenceProxyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Doctrine\ORM\Proxy\InternalProxy;
use Doctrine\Tests\Models\Company\CompanyAuction;
use Doctrine\Tests\Models\ECommerce\ECommerceProduct;
use Doctrine\Tests\Models\ECommerce\ECommerceProduct2;
use Doctrine\Tests\Models\ECommerce\ECommerceShipping;
use Doctrine\Tests\OrmFunctionalTestCase;

Expand Down Expand Up @@ -112,6 +113,24 @@ public function testCloneProxy(): void
self::assertFalse($entity->isCloned);
}

public function testCloneProxyWithResetId(): void
{
$id = $this->createProduct();

$entity = $this->_em->getReference(ECommerceProduct2::class, $id);
assert($entity instanceof ECommerceProduct2);

$clone = clone $entity;
assert($clone instanceof ECommerceProduct2);

self::assertEquals($id, $entity->getId());
self::assertEquals('Doctrine Cookbook', $entity->getName());

self::assertFalse($this->_em->contains($clone));
self::assertNull($clone->getId());
self::assertEquals('Clone of Doctrine Cookbook', $clone->getName());
}

/** @group DDC-733 */
public function testInitializeProxy(): void
{
Expand Down
Loading

0 comments on commit 83851a9

Please sign in to comment.