Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge 2.16.x up into 3.0.x #10795

Merged
merged 31 commits into from
Jun 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
cc3d872
revert: transform backed enum to value
Gwemox Jun 5, 2023
2afe2dc
Merge pull request #10758 from Gwemox/partial-revert-enum-id-hash
greg0ire Jun 5, 2023
6c9b29f
Don't call canEmulateSchemas in SchemaTool when possible (#10762)
nicolas-grekas Jun 5, 2023
3827dd7
Don't call deprecated getSQLResultCasing and usesSequenceEmulatedIden…
nicolas-grekas Jun 6, 2023
5c74795
Merge 2.15.x into 2.16.x (#10765)
derrabus Jun 6, 2023
33675ff
fix: Update baseline and assertions for OneToManyPersister
wtfzdotnet Jun 6, 2023
3c0d140
OneToManyPersister does not take custom identifier types into account…
wtfzdotnet Jun 6, 2023
cbf45dd
PHPStan 1.10.18, Psalm 5.12.0 (#10771)
derrabus Jun 8, 2023
c3106f9
Restore document proxy state to uninitialized on load exception (#10645)
notrix Jun 9, 2023
e5174af
Document how to produce DTOs with a result set mapping
greg0ire Jun 11, 2023
1adb5c0
Merge pull request #10774 from greg0ire/document-dto-in-rsm
greg0ire Jun 12, 2023
41f704c
Merge pull request #10775 from doctrine/2.15.x
greg0ire Jun 13, 2023
fe8e313
Merge pull request #10747 from wtfzdotnet/feature/fix-one-to-many-cus…
greg0ire Jun 16, 2023
5132f0d
Stop using $message argument
greg0ire Jun 18, 2023
fcc5c10
Rely on partial objects less when in tests
greg0ire Jun 18, 2023
98b4048
Defer removing removed entities from to-many collections until after …
mpdude Jun 5, 2023
930fa83
Test expected query counts in ManyToManyBasicAssociationTest
mpdude Jun 5, 2023
ee0b3f2
Write tests more concisely
mpdude Jun 21, 2023
ba089e5
Avoid unnecessary changes
mpdude Jun 5, 2023
c235901
Move a check up (continue early)
mpdude Jun 6, 2023
d220494
Improve documentation on exact behaviour of many-to-many deletion ope…
mpdude Jun 6, 2023
829d5fb
Merge pull request #10780 from greg0ire/avoid-partial
greg0ire Jun 22, 2023
f2abf61
Merge pull request #10763 from mpdude/defer-collection-entity-removal…
greg0ire Jun 22, 2023
4c3bd20
Fix missing setFilterSchemaAssetsExpression in phpdoc (#10776)
dmitryuk Jun 22, 2023
5f6501f
Merge remote-tracking branch 'origin/2.15.x' into 2.15.x-merge-up-int…
greg0ire Jun 22, 2023
6c0a5ec
Merge pull request #10787 from doctrine/2.15.x-merge-up-into-2.16.x_X…
greg0ire Jun 22, 2023
5114dce
Work around slevomat/coding-standard issues
greg0ire Jun 23, 2023
0e06d6b
Apply latest coding standard rules
greg0ire Jun 23, 2023
f76bab2
Merge pull request #10790 from greg0ire/slevomat-cs-upgrade
greg0ire Jun 23, 2023
70bcff7
Merge pull request #10794 from doctrine/2.15.x
greg0ire Jun 23, 2023
84a87a6
Merge remote-tracking branch 'origin/2.16.x' into 3.0.x
greg0ire Jun 23, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@
"require-dev": {
"doctrine/coding-standard": "^12.0",
"phpbench/phpbench": "^1.0",
"phpstan/phpstan": "1.10.14",
"phpstan/phpstan": "1.10.18",
"phpunit/phpunit": "^10.0.14",
"psr/log": "^1 || ^2 || ^3",
"squizlabs/php_codesniffer": "3.7.2",
"symfony/cache": "^5.4 || ^6.0",
"symfony/cache": "^5.4 || ^6.2",
"symfony/var-exporter": "^5.4 || ^6.2",
"vimeo/psalm": "5.11.0"
"vimeo/psalm": "5.12.0"
},
"suggest": {
"ext-dom": "Provides support for XSD validation for XML mapping files",
Expand Down
18 changes: 18 additions & 0 deletions docs/en/reference/association-mapping.rst
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,15 @@ Generated MySQL Schema:
replaced by one-to-many/many-to-one associations between the 3
participating classes.

.. note::

For many-to-many associations, the ORM takes care of managing rows
in the join table connecting both sides. Due to the way it deals
with entity removals, database-level constraints may not work the
way one might intuitively assume. Thus, be sure not to miss the section
on :ref:`join table management <remove_object_many_to_many_join_tables>`.


Many-To-Many, Bidirectional
---------------------------

Expand Down Expand Up @@ -646,6 +655,15 @@ one is bidirectional.
The MySQL schema is exactly the same as for the Many-To-Many
uni-directional case above.

.. note::

For many-to-many associations, the ORM takes care of managing rows
in the join table connecting both sides. Due to the way it deals
with entity removals, database-level constraints may not work the
way one might intuitively assume. Thus, be sure not to miss the section
on :ref:`join table management <remove_object_many_to_many_join_tables>`.


Owning and Inverse Side on a ManyToMany Association
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
34 changes: 34 additions & 0 deletions docs/en/reference/native-sql.rst
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,40 @@ The first parameter is the name of the column in the SQL result set
and the second parameter is the result alias under which the value
of the column will be placed in the transformed Doctrine result.

Special case: DTOs
...................

You can also use ``ResultSetMapping`` to map the results of a native SQL
query to a DTO (Data Transfer Object). This is done by adding scalar
results for each argument of the DTO's constructor, then filling the
``newObjectMappings`` property of the ``ResultSetMapping`` with
information about where to map each scalar result:

.. code-block:: php

<?php

$rsm = new ResultSetMapping();
$rsm->addScalarResult('name', 1, 'string');
$rsm->addScalarResult('email', 2, 'string');
$rsm->addScalarResult('city', 3, 'string');
$rsm->newObjectMappings['name'] = [
'className' => CmsUserDTO::class,
'objIndex' => 0, // a result can contain many DTOs, this is the index of the DTO to map to
'argIndex' => 0, // each scalar result can be mapped to a different argument of the DTO constructor
];
$rsm->newObjectMappings['email'] = [
'className' => CmsUserDTO::class,
'objIndex' => 0,
'argIndex' => 1,
];
$rsm->newObjectMappings['city'] = [
'className' => CmsUserDTO::class,
'objIndex' => 0,
'argIndex' => 2,
];


Meta results
~~~~~~~~~~~~

Expand Down
50 changes: 43 additions & 7 deletions docs/en/reference/working-with-objects.rst
Original file line number Diff line number Diff line change
Expand Up @@ -286,17 +286,53 @@ as follows:
After an entity has been removed, its in-memory state is the same as
before the removal, except for generated identifiers.

Removing an entity will also automatically delete any existing
records in many-to-many join tables that link this entity. The
action taken depends on the value of the ``@joinColumn`` mapping
attribute "onDelete". Either Doctrine issues a dedicated ``DELETE``
statement for records of each join table or it depends on the
foreign key semantics of onDelete="CASCADE".
During the ``EntityManager#flush()`` operation, the removed entity
will also be removed from all collections in entities currently
loaded into memory.

.. _remove_object_many_to_many_join_tables:

Join-table management when removing from many-to-many collections
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Regarding existing rows in many-to-many join tables that refer to
an entity being removed, the following applies.

When the entity being removed does not declare the many-to-many association
itself (that is, the many-to-many association is unidirectional and
the entity is on the inverse side), the ORM has no reasonable way to
detect associations targeting the entity's class. Thus, no ORM-level handling
of join-table rows is attempted and database-level constraints apply.
In case of database-level ``ON DELETE RESTRICT`` constraints, the
``EntityManager#flush()`` operation may abort and a ``ConstraintViolationException``
may be thrown. No in-memory collections will be modified in this case.
With ``ON DELETE CASCADE``, the RDBMS will take care of removing rows
from join tables.

When the entity being removed is part of bi-directional many-to-many
association, either as the owning or inverse side, the ORM will
delete rows from join tables before removing the entity itself. That means
database-level ``ON DELETE RESTRICT`` constraints on join tables are not
effective, since the join table rows are removed first. Removal of join table
rows happens through specialized methods in entity and collection persister
classes and take one query per entity and join table. In case the association
uses a ``@JoinColumn`` configuration with ``onDelete="CASCADE"``, instead
of using a dedicated ``DELETE`` query the database-level operation will be
relied upon.

.. note::

In case you rely on database-level ``ON DELETE RESTRICT`` constraints,
be aware that by making many-to-many associations bidirectional the
assumed protection may be lost.


Performance of different deletion strategies
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Deleting an object with all its associated objects can be achieved
in multiple ways with very different performance impacts.


1. If an association is marked as ``CASCADE=REMOVE`` Doctrine ORM
will fetch this association. If its a Single association it will
pass this entity to
Expand Down
1 change: 0 additions & 1 deletion lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,6 @@ static function (string $fieldName) use ($data, $class) {
* that belongs to a particular component/class. Afterwards, all these chunks
* are processed, one after the other. For each chunk of class data only one of the
* following code paths is executed:
*
* Path A: The data chunk belongs to a joined/associated object and the association
* is collection-valued.
* Path B: The data chunk belongs to a joined/associated object and the association
Expand Down
1 change: 0 additions & 1 deletion lib/Doctrine/ORM/Mapping/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use BackedEnum;
use BadMethodCallException;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Deprecations\Deprecation;
use Doctrine\Instantiator\Instantiator;
use Doctrine\Instantiator\InstantiatorInterface;
use Doctrine\ORM\Cache\Exception\NonCacheableEntityAssociation;
Expand Down
1 change: 1 addition & 0 deletions lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ private function completeIdGeneratorMapping(ClassMetadata $class): void
case ClassMetadata::GENERATOR_TYPE_IDENTITY:
$sequenceName = null;
$fieldName = $class->identifier ? $class->getSingleIdentifierFieldName() : null;
$platform = $this->getTargetPlatform();

$generator = $fieldName && $class->fieldMappings[$fieldName]->type === 'bigint'
? new BigIntegerIdentityGenerator()
Expand Down
11 changes: 10 additions & 1 deletion lib/Doctrine/ORM/Persisters/Collection/OneToManyPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use function array_values;
use function assert;
use function implode;
use function is_int;
use function is_string;

/**
Expand Down Expand Up @@ -154,16 +155,22 @@ private function deleteEntityCollection(PersistentCollection $collection): int
$targetClass = $this->em->getClassMetadata($mapping->targetEntity);
$columns = [];
$parameters = [];
$types = [];

foreach ($this->em->getMetadataFactory()->getOwningSide($mapping)->joinColumns as $joinColumn) {
$columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
$parameters[] = $identifier[$sourceClass->getFieldForColumn($joinColumn->referencedColumnName)];
$types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $sourceClass, $this->em);
}

$statement = 'DELETE FROM ' . $this->quoteStrategy->getTableName($targetClass, $this->platform)
. ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';

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

assert(is_int($numAffected));

return $numAffected;
}

/**
Expand Down Expand Up @@ -228,6 +235,8 @@ private function deleteJoinedEntityCollection(PersistentCollection $collection):

$this->conn->executeStatement($statement);

assert(is_int($numDeleted));

return $numDeleted;
}

Expand Down
10 changes: 1 addition & 9 deletions lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -241,10 +241,6 @@ public function executeInserts(): array
$paramIndex = 1;

foreach ($insertData[$tableName] as $column => $value) {
if ($value instanceof BackedEnum) {
$value = $value->value;
}

$stmt->bindValue($paramIndex++, $value, $this->columnTypes[$column]);
}
}
Expand Down Expand Up @@ -503,7 +499,7 @@ final protected function updateTable(
protected function deleteJoinTableRecords(array $identifier, array $types): void
{
foreach ($this->class->associationMappings as $mapping) {
if (! $mapping->isManyToMany()) {
if (! $mapping->isManyToMany() || $mapping->isOnDeleteCascade) {
continue;
}

Expand Down Expand Up @@ -539,10 +535,6 @@ protected function deleteJoinTableRecords(array $identifier, array $types): void
$otherKeys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
}

if (isset($mapping->isOnDeleteCascade)) {
continue;
}

$joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform);

$this->conn->delete($joinTableName, array_combine($keys, $identifier), $types);
Expand Down
13 changes: 12 additions & 1 deletion lib/Doctrine/ORM/Proxy/ProxyFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use ReflectionProperty;
use Symfony\Component\VarExporter\ProxyHelper;
use Symfony\Component\VarExporter\VarExporter;
use Throwable;

use function array_flip;
use function str_replace;
Expand Down Expand Up @@ -192,7 +193,17 @@ private function createInitializer(ClassMetadata $classMetadata, EntityPersister

$identifier = $classMetadata->getIdentifierValues($proxy);

if ($entityPersister->loadById($identifier, $proxy) === null) {
try {
$entity = $entityPersister->loadById($identifier, $proxy);
} catch (Throwable $exception) {
$proxy->__setInitializer($initializer);
$proxy->__setCloner($cloner);
$proxy->__setInitialized(false);

throw $exception;
}

if ($entity === null) {
$proxy->__setInitializer($initializer);
$proxy->__setCloner($cloner);
$proxy->__setInitialized(false);
Expand Down
11 changes: 7 additions & 4 deletions lib/Doctrine/ORM/Query/Exec/AbstractSqlExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

namespace Doctrine\ORM\Query\Exec;

use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Result;
use Doctrine\DBAL\Types\Type;

Expand All @@ -15,6 +17,8 @@
* @link http://www.doctrine-project.org
*
* @todo Rename: AbstractSQLExecutor
* @psalm-type WrapperParameterType = string|Type|ParameterType::*|ArrayParameterType::*
* @psalm-type WrapperParameterTypeArray = array<int<0, max>, WrapperParameterType>|array<string, WrapperParameterType>
*/
abstract class AbstractSqlExecutor
{
Expand Down Expand Up @@ -49,10 +53,9 @@ public function removeQueryCacheProfile(): void
/**
* Executes all sql statements.
*
* @param Connection $conn The database connection that is used to execute the queries.
* @psalm-param list<mixed>|array<string, mixed> $params The parameters.
* @psalm-param array<int, int|string|Type|null>|
* array<string, int|string|Type|null> $types The parameter types.
* @param Connection $conn The database connection that is used to execute the queries.
* @param list<mixed>|array<string, mixed> $params The parameters.
* @psalm-param WrapperParameterTypeArray $types The parameter types.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ported as #10796

*/
abstract public function execute(Connection $conn, array $params, array $types): Result|int;
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ protected function configure(): void
by the ORM, you can use a DBAL functionality to filter the tables and sequences down
on a global level:
$config->setFilterSchemaAssetsExpression($regexp);
$config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool {
if ($assetName instanceof AbstractAsset) {
$assetName = $assetName->getName();
}
return !str_starts_with($assetName, 'audit_');
});
EOT);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ protected function configure(): void
by the ORM, you can use a DBAL functionality to filter the tables and sequences down
on a global level:
$config->setFilterSchemaAssetsExpression($regexp);
$config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool {
if ($assetName instanceof AbstractAsset) {
$assetName = $assetName->getName();
}
return !str_starts_with($assetName, 'audit_');
});
EOT);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@ protected function configure(): void
by the ORM, you can use a DBAL functionality to filter the tables and sequences down
on a global level:
$config->setFilterSchemaAssetsExpression($regexp);
$config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool {
if ($assetName instanceof AbstractAsset) {
$assetName = $assetName->getName();
}
return !str_starts_with($assetName, 'audit_');
});
EOT);
}

Expand Down
Loading