Compare commits

...

43 Commits

Author SHA1 Message Date
Grégoire Paris
609647a51a Merge pull request #11015 from greg0ire/phase2-optim
Make phpdoc accurate
2023-10-21 19:35:18 +02:00
Grégoire Paris
293299a314 Make phpdoc accurate
When transforming these phpdoc types into native types, things break
down. They are correct according to the EBNF, but in practice, there are
so-called phase 2 optimizations that allow using ConditionalPrimary,
ConditionalFactor and ConditionalTerm instances in places where
ConditionalExpression is used.
2023-10-21 19:15:03 +02:00
Grégoire Paris
0b7fe1862e Merge pull request #11018 from stof/fix_result_set_builder_enum
Fix the support for enum types in the ResultSetMappingBuilder
2023-10-18 22:27:01 +02:00
Christophe Coevoet
866283d1a7 Fix the support for enum types in the ResultSetMappingBuilder 2023-10-18 10:04:20 +02:00
Grégoire Paris
3676e3c571 Merge pull request #11007 from greg0ire/refresh-archi-docs
Address split of doctrine/common
2023-10-17 21:41:52 +02:00
Grégoire Paris
d84f607487 Address split of doctrine/common
doctrine/common has been split in several packages. A lot of what was
true about doctrine/common is true about doctrine/persistence today, so
let us simply reuse the existing paragraphs and mention persistence
instead of common.
2023-10-17 20:02:34 +02:00
Alexander M. Turek
42af7cabb7 Cover calling AbstractQuery::setParameter() with an array parameter (#10996) 2023-10-11 16:04:47 +02:00
Serhii Petrov
32192c7b01 Test against php 8.3 (#10963) 2023-10-09 12:43:52 +02:00
Grégoire Paris
77843e45f3 Merge pull request #10972 from salehhashemi1992/ci/update-checkout-to-v4
update actions/checkout to v4
2023-10-08 18:44:13 +02:00
salehhashemi1992
1919eea0a9 update checkout version to version 4 2023-10-08 11:04:22 +03:30
Alexander M. Turek
62ed63bbbe PHPStan 1.10.35, Psalm 5.15.0 (#10958) 2023-09-29 08:56:45 +02:00
Danny van Kooten
081ec2ad26 docs: in text, refer to attributes when talking about metadata (#10956)
Co-authored-by: Danny van Kooten <dannyvankooten@users.noreply.github.com>
2023-09-28 21:49:15 +02:00
Adrien Crivelli
e9537f4cde Fix bullet list layout (#10951) 2023-09-19 21:01:37 +02:00
Marko Kaznovac
38ad3925e2 docs[query-builder]: fix rendering of Doctrine\DBAL\ParameterType::* (#10945) 2023-09-15 00:17:48 +02:00
Marko Kaznovac
858b01f85e tests[ORMSetupTest]: testCacheNamespaceShouldBeGeneratedForApcu requires enabled apc (#10940) 2023-09-10 22:57:36 +02:00
Grégoire Paris
9f555ea8fb Merge pull request #10933 from kaznovac/patch-2
docs: use modern named arguments syntax
2023-09-08 22:02:11 +02:00
Marko Kaznovac
1f8c02f345 docs: use modern named arguments syntax
use official named arguments syntax in example instead of pre php 8 codestyle for 'named' arguments
2023-09-08 13:47:11 +02:00
Grégoire Paris
d81afdb6e3 Merge pull request #10930 from greg0ire/improve-doc-job
Improve doc job
2023-09-02 23:15:56 +02:00
Grégoire Paris
0628204b43 Ignore "Unknown directive" error
We have a lot of errors about "Unknown directive" that we should make
known when implementing guides for Doctrine, but cannot address by
modifying the docs.

The unknown directives are:

- configuration-block
- toc
- tocheader
- sectionauthor
2023-09-02 19:29:05 +02:00
Grégoire Paris
816ecc6d6b Use a stable release
0.1.0 has been published 3 weeks ago. This means we no longer need to
  use dev stability
2023-09-02 19:02:59 +02:00
Grégoire Paris
f66263d859 Remove output directory argument
It is no actually necessary at all.
2023-09-02 19:01:46 +02:00
Grégoire Paris
8aa5aa2f57 Merge pull request #10929 from kaznovac/patch-1
tutorials[getting-started]: example fix bug id type definition
2023-09-02 18:52:43 +02:00
Marko Kaznovac
96e31a3b30 tutorials[getting-started]: example fix bug id type definition 2023-09-02 17:48:29 +02:00
Grégoire Paris
a60a273423 Merge pull request #10808 from oscmarb/verifiy-hint-defer-eager-load-is-true
Verify UnitOfWork::HINT_DEFEREAGERLOAD exists and is true
2023-09-01 07:52:11 +02:00
Grégoire Paris
17500f56ea Merge pull request #10923 from kaznovac/patch-1
basic-mapping: fix new-line rendered in output
2023-08-27 20:21:56 +02:00
Marko Kaznovac
fc2f724e2d basic-mapping: fix new-line rendered in output 2023-08-27 19:17:04 +02:00
Óscar Martínez
7986fc64dd Verify UnitOfWork::HINT_DEFEREAGERLOAD exists and is true 2023-08-25 09:51:02 +02:00
Grégoire Paris
2f9e98754b Merge pull request #10915 from mpdude/post-events-later
Mitigate problems with `EntityManager::flush()` reentrance since 2.16.0 (Take 2)
2023-08-25 07:47:25 +02:00
Sergii Dolgushev
bb5524099c Use required classes for Lifecycle Callback examples (#10916)
* Use required classes for Lifecycle Callback examples

* Coding Style fixes

---------

Co-authored-by: Sergii Dolgushev <Sergii.Dolgushev@secondwaveds.com>
2023-08-23 22:47:15 +02:00
Grégoire Paris
3a8cafe228 Add space before backquote (#10918)
According to the RST docs,

> [inline markup] it must be separated from surrounding text by non-word
> characters. Use a backslash escaped space to work around that: thisis\ *one*\ word.

Because we were missing a space before backquotes here, the links were
not rendered. Escaping the space allow not to actually produce a space
in the output.

See https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#inline-markup
2023-08-23 21:42:35 +02:00
Matthias Pigulla
8259a16681 Mitigate problems with EntityManager::flush() reentrance since 2.16.0 (Take 2)
The changes from #10547, which landed in 2.16.0, cause problems for users calling `EntityManager::flush()` from within `postPersist` event listeners.

* When `UnitOfWork::commit()` is re-entered, the "inner" (reentrant) call will start working through all changesets. Eventually, it finishes with all insertions being performed and `UoW::$entityInsertions` being empty. After return, the entity insertion order, an array computed at the beginning of `UnitOfWork::executeInserts()`, still contains entities that now have been processed already. This leads to a strange-looking SQL error where the number of parameters used does not match the number of parameters bound. This has been reported as #10869.

* The fixes made to the commit order computation may lead to a different entity insertion order than previously. `postPersist` listener code may be affected by this when accessing generated IDs for other entities than the one the event has been dispatched for. This ID may not yet be available when the insertion order is different from the one that was used before 2.16. This has been mentioned in https://github.com/doctrine/orm/pull/10906#issuecomment-1682417987.

This PR suggests to address both issues by dispatching the `postPersist` event only after _all_ new entities have their rows inserted into the database. Likewise, dispatch `postRemove` only after _all_ deletions have been executed.

This solves the first issue because the sequence of insertions or deletions has been processed completely _before_ we start calling event listeners. This way, potential changes made by listeners will no longer be relevant.

Regarding the second issue, I think deferring `postPersist` a bit until _all_ entities have been inserted does not violate any promises given, hence is not a BC break. In 2.15, this event was raised after all insertions _for a particular class_ had been processed - so, it was never an "immediate" event for every single entity. #10547 moved the event handling to directly after every single insertion. Now, this PR moves it back a bit to after _all_ insertions.
2023-08-23 07:55:21 +02:00
David Arenas
5577d51c44 Add back throws annotation to getSingleScalarResult (#10907)
Fix regression introduced in #10870

`$result = $this->execute(null, $hydrationMode);` in `getSingleResult` can still throw NoResultException exception.
2023-08-13 13:01:30 +02:00
Eduardo Rocha
d1922a3065 Fix link on known issues docs (#10904) 2023-08-10 21:41:31 +02:00
Alexander M. Turek
597a63a86c PHPStan 1.10.28, Psalm 5.14.1 (#10895) 2023-08-09 15:05:08 +02:00
Bei Xiao
6b220e3c90 Fix return type of getSingleScalarResult (#10870) 2023-08-09 11:42:00 +02:00
Matthias Pigulla
6de4b68705 Use a dedicated exception for the check added in #10785 (#10881)
This adds a dedicated exception for the case that objects with colliding identities are to be put into the identity map.

Implements #10872.
2023-08-09 11:38:35 +02:00
Matthias Pigulla
16c0151831 Document more clearly that the insert order is an implementation detail (#10883) 2023-08-09 11:36:05 +02:00
Matthias Pigulla
440b244ebc Fix broken changeset computation for entities loaded through fetch=EAGER + using inheritance (#10884)
#10880 reports a case where the changes from #10785 cause entity updates to be missed.

Upon closer inspection, this change seems to be causing it:

https://github.com/doctrine/orm/pull/10785/files#diff-55a900494fc8033ab498c53929716caf0aa39d6bdd7058e7d256787a24412ee4L2990-L3003

The code was changed to use `registerManaged()` instead, which basically does the same things, but (since #10785) also includes an additional check against duplicate entity instances.

But, one detail slipped through tests and reviews: `registerManaged()` also updates `\Doctrine\ORM\UnitOfWork::$originalEntityData`, which is used to compute entity changesets. An empty array `[]` was passed for $data here.

This will make the changeset computation assume that a partial object was loaded and effectively ignore all field updates here:

a616914887/lib/Doctrine/ORM/UnitOfWork.php (L762-L764)

I think that, effectively, it is sufficient to call `registerManaged()` only in the two cases where a proxy was created.

Calling `registerManaged()` with `[]` as data for a proxy object is consistent with e. g. `\Doctrine\ORM\EntityManager::getReference()`.

In the case that a full entity has to be loaded, we need not call `registerManaged()` at all, since that will already happen inside `EntityManager::find()` (or, more specifically, `UnitOfWork::createEntity()` called inside it).

Note that the test case has to make some provisions so that we actually reach this case:
* Load an entity that uses `fetch="EAGER"` on a to-one association
* That association being against a class that uses inheritance (why's that?)
2023-08-09 11:34:53 +02:00
Matthias Pigulla
a616914887 Turn identity map collisions from exception to deprecation notice (#10878)
In #10785, a check was added that prevents entity instances from getting into the identity map when another object for the same ID is already being tracked.

This caused regressions for users that work with application-provided IDs and expect this condition to fail with `UniqueConstraintViolationExceptions` when flushing to the database.

Thus, this PR turns the exception into a deprecation notice. Users can opt-in to the new behavior. In 3.0, the exception will be used.

Implements #10871.
2023-08-04 14:06:02 +02:00
Dieter Beck
fd0bdc69b0 Add possibility to set reportFieldsWhereDeclared to true in ORMSetup (#10865)
Otherwise it is impossible to avoid a deprecation warning when using ORMSetup::createAttributeMetadataConfiguration()
2023-08-02 14:34:13 +02:00
Michael Olšavský
f50803ccb9 Fix UnitOfWork->originalEntityData is missing not-modified collections after computeChangeSet (#9301)
* Fix original data incomplete after flush

* Apply suggestions from code review

Co-authored-by: Alexander M. Turek <me@derrabus.de>

---------

Co-authored-by: Alexander M. Turek <me@derrabus.de>
2023-08-02 13:44:15 +02:00
Matthias Pigulla
eeefc6bc0f Add an UPGRADE notice about the potential changes in commit order (#10866) 2023-08-02 13:42:49 +02:00
Grégoire Paris
710dde83aa Update branch metadata (#10862) 2023-08-01 14:56:34 +02:00
61 changed files with 1088 additions and 223 deletions

View File

@@ -12,21 +12,27 @@
"upcoming": true
},
{
"name": "2.16",
"branchName": "2.16.x",
"slug": "2.16",
"name": "2.17",
"branchName": "2.17.x",
"slug": "2.17",
"upcoming": true
},
{
"name": "2.15",
"branchName": "2.15.x",
"slug": "2.15",
"name": "2.16",
"branchName": "2.16.x",
"slug": "2.16",
"current": true,
"aliases": [
"current",
"stable"
]
},
{
"name": "2.15",
"branchName": "2.15.x",
"slug": "2.15",
"maintained": false
},
{
"name": "2.14",
"branchName": "2.14.x",

View File

@@ -39,6 +39,7 @@ jobs:
- "8.0"
- "8.1"
- "8.2"
- "8.3"
dbal-version:
- "default"
extension:
@@ -62,7 +63,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"
with:
fetch-depth: 2
@@ -111,6 +112,7 @@ jobs:
matrix:
php-version:
- "8.2"
- "8.3"
dbal-version:
- "default"
- "3@dev"
@@ -143,7 +145,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"
with:
fetch-depth: 2
@@ -183,6 +185,7 @@ jobs:
matrix:
php-version:
- "8.2"
- "8.3"
dbal-version:
- "default"
- "3@dev"
@@ -212,7 +215,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"
with:
fetch-depth: 2
@@ -252,6 +255,7 @@ jobs:
matrix:
php-version:
- "8.2"
- "8.3"
dbal-version:
- "default"
- "3@dev"
@@ -281,7 +285,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"
with:
fetch-depth: 2
@@ -333,7 +337,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"
with:
fetch-depth: 2
@@ -363,7 +367,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"
with:
fetch-depth: 2

View File

@@ -21,7 +21,7 @@ jobs:
steps:
- name: "Checkout code"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
@@ -33,10 +33,7 @@ jobs:
run: "rm composer.json"
- name: "Require phpdocumentor/guides-cli"
run: "composer require --dev phpdocumentor/guides-cli dev-main@dev --no-update"
- name: "Configure minimum stability"
run: "composer config minimum-stability dev"
run: "composer require --dev phpdocumentor/guides-cli --no-update"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
@@ -48,4 +45,4 @@ jobs:
printf '%s\n%s\n\n%s\n' "Dummy title" "===========" "$(cat docs/en/sidebar.rst)" > docs/en/sidebar.rst
- name: "Run guides-cli"
run: "vendor/bin/guides -vvv --no-progress docs/en /tmp/test 2>&1 | ( ! grep WARNING )"
run: "vendor/bin/guides -vvv --no-progress docs/en 2>&1 | grep -v 'Unknown directive' | ( ! grep WARNING )"

View File

@@ -36,7 +36,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"
with:
fetch-depth: 2

View File

@@ -42,7 +42,7 @@ jobs:
steps:
- name: "Checkout code"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
@@ -83,7 +83,7 @@ jobs:
steps:
- name: "Checkout code"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"

View File

@@ -1,5 +1,31 @@
# Upgrade to 2.16
## Deprecated accepting duplicate IDs in the identity map
For any given entity class and ID value, there should be only one object instance
representing the entity.
In https://github.com/doctrine/orm/pull/10785, a check was added that will guard this
in the identity map. The most probable cause for violations of this rule are collisions
of application-provided IDs.
In ORM 2.16.0, the check was added by throwing an exception. In ORM 2.16.1, this will be
changed to a deprecation notice. ORM 3.0 will make it an exception again. Use
`\Doctrine\ORM\Configuration::setRejectIdCollisionInIdentityMap()` if you want to opt-in
to the new mode.
## Potential changes to the order in which `INSERT`s are executed
In https://github.com/doctrine/orm/pull/10547, the commit order computation was improved
to fix a series of bugs where a correct (working) commit order was previously not found.
Also, the new computation may get away with fewer queries being executed: By inserting
referred-to entities first and using their ID values for foreign key fields in subsequent
`INSERT` statements, additional `UPDATE` statements that were previously necessary can be
avoided.
When using database-provided, auto-incrementing IDs, this may lead to IDs being assigned
to entities in a different order than it was previously the case.
## Deprecated `\Doctrine\ORM\Internal\CommitOrderCalculator` and related classes
With changes made to the commit order computation, the internal classes

View File

@@ -42,14 +42,14 @@
"doctrine/annotations": "^1.13 || ^2",
"doctrine/coding-standard": "^9.0.2 || ^12.0",
"phpbench/phpbench": "^0.16.10 || ^1.0",
"phpstan/phpstan": "~1.4.10 || 1.10.25",
"phpstan/phpstan": "~1.4.10 || 1.10.35",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6",
"psr/log": "^1 || ^2 || ^3",
"squizlabs/php_codesniffer": "3.7.2",
"symfony/cache": "^4.4 || ^5.4 || ^6.0",
"symfony/var-exporter": "^4.4 || ^5.4 || ^6.2",
"symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0",
"vimeo/psalm": "4.30.0 || 5.13.1"
"vimeo/psalm": "4.30.0 || 5.15.0"
},
"conflict": {
"doctrine/annotations": "<1.13 || >= 3.0"

View File

@@ -211,7 +211,7 @@ Now look at the following test-code for our entities:
{
public function testAddEntry()
{
$account = new Account("123456", $maxCredit = 200);
$account = new Account("123456", maxCredit: 200);
$this->assertEquals(0, $account->getBalance());
$account->addEntry(500);
@@ -223,7 +223,7 @@ Now look at the following test-code for our entities:
public function testExceedMaxLimit()
{
$account = new Account("123456", $maxCredit = 200);
$account = new Account("123456", maxCredit: 200);
$this->expectException(Exception::class);
$account->addEntry(-1000);

View File

@@ -13,7 +13,7 @@ for all our domain objects.
.. note::
The notify change tracking policy is deprecated and will be removed in ORM 3.0.
(`Details <https://github.com/doctrine/orm/issues/8383>`_)
(\ `Details <https://github.com/doctrine/orm/issues/8383>`_)
Implementing NotifyPropertyChanged
----------------------------------

View File

@@ -58,6 +58,10 @@ First Attributes:
.. code-block:: php
<?php
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\HasLifecycleCallbacks;
use Doctrine\ORM\Mapping\PrePersist;
use Doctrine\ORM\Mapping\PreUpdate;
#[Entity]
#[HasLifecycleCallbacks]

View File

@@ -29,7 +29,7 @@ steps of configuration.
$config = new Configuration;
$config->setMetadataCache($metadataCache);
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities']);
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true);
$config->setMetadataDriverImpl($driverImpl);
$config->setQueryCache($queryCache);
$config->setProxyDir('/path/to/myproject/lib/MyProject/Proxies');
@@ -134,7 +134,7 @@ The attribute driver can be injected in the ``Doctrine\ORM\Configuration``:
<?php
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities']);
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true);
$config->setMetadataDriverImpl($driverImpl);
The path information to the entities is required for the attribute

View File

@@ -24,28 +24,34 @@ performance it is also recommended that you use APC with PHP.
Doctrine ORM Packages
-------------------
Doctrine ORM is divided into three main packages.
Doctrine ORM is divided into four main packages.
- Common
- DBAL (includes Common)
- ORM (includes DBAL+Common)
- `Collections <https://www.doctrine-project.org/projects/doctrine-collections/en/stable/index.html>`_
- `Event Manager <https://www.doctrine-project.org/projects/doctrine-event-manager/en/stable/index.html>`_
- `Persistence <https://www.doctrine-project.org/projects/doctrine-persistence/en/stable/index.html>`_
- `DBAL <https://www.doctrine-project.org/projects/doctrine-dbal/en/stable/index.html>`_
- ORM (depends on DBAL+Persistence+Collections)
This manual mainly covers the ORM package, sometimes touching parts
of the underlying DBAL and Common packages. The Doctrine code base
of the underlying DBAL and Persistence packages. The Doctrine code base
is split in to these packages for a few reasons and they are to...
- ...make things more maintainable and decoupled
- ...allow you to use the code in Doctrine Common without the ORM
or DBAL
- ...allow you to use the code in Doctrine Persistence and Collections
without the ORM or DBAL
- ...allow you to use the DBAL without the ORM
The Common Package
~~~~~~~~~~~~~~~~~~
Collection, Event Manager and Persistence
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The Common package contains highly reusable components that have no
dependencies beyond the package itself (and PHP, of course). The
root namespace of the Common package is ``Doctrine\Common``.
The Collection, Event Manager and Persistence packages contain highly
reusable components that have no dependencies beyond the packages
themselves (and PHP, of course). The root namespace of the Persistence
package is ``Doctrine\Persistence``. The root namespace of the
Collection package is ``Doctrine\Common\Collections``, for historical
reasons. The root namespace of the Event Manager package is just
``Doctrine\Common``, also for historical reasons.
The DBAL Package
~~~~~~~~~~~~~~~~
@@ -199,5 +205,3 @@ typical implementation of the
to keep track of all the things that need to be done the next time
``flush`` is invoked. You usually do not directly interact with a
``UnitOfWork`` but with the ``EntityManager`` instead.

View File

@@ -47,8 +47,7 @@ mapping metadata:
- :doc:`Attributes <attributes-reference>`
- :doc:`XML <xml-mapping>`
- :doc:`PHP code <php-mapping>`
- :doc:`Docblock Annotations <annotations-reference>` (deprecated and
will be removed in ``doctrine/orm`` 3.0)
- :doc:`Docblock Annotations <annotations-reference>` (deprecated and will be removed in ``doctrine/orm`` 3.0)
- :doc:`YAML <yaml-mapping>` (deprecated and will be removed in ``doctrine/orm`` 3.0.)
This manual will usually show mapping metadata via attributes, though

View File

@@ -63,7 +63,7 @@ Notify
.. note::
The notify change tracking policy is deprecated and will be removed in ORM 3.0.
(`Details <https://github.com/doctrine/orm/issues/8383>`_)
(\ `Details <https://github.com/doctrine/orm/issues/8383>`_)
This policy is based on the assumption that the entities notify
interested listeners of changes to their properties. For that

View File

@@ -1381,7 +1381,7 @@ Result Cache API:
$query->setResultCacheDriver(new ApcCache());
$query->useResultCache(true)
->setResultCacheLifeTime($seconds = 3600);
->setResultCacheLifeTime(3600);
$result = $query->getResult(); // cache miss
@@ -1392,7 +1392,7 @@ Result Cache API:
$result = $query->getResult(); // saved in given result cache id.
// or call useResultCache() with all parameters:
$query->useResultCache(true, $seconds = 3600, 'my_query_result');
$query->useResultCache(true, 3600, 'my_query_result');
$result = $query->getResult(); // cache hit!
// Introspection
@@ -1425,7 +1425,7 @@ userland:
reloading this data. Partially loaded objects have to be passed to
``EntityManager::refresh()`` if they are to be reloaded fully from
the database. This query hint is deprecated and will be removed
in the future (`Details <https://github.com/doctrine/orm/issues/8471>`_)
in the future (\ `Details <https://github.com/doctrine/orm/issues/8471>`_)
- ``Query::HINT_REFRESH`` - This query is used internally by
``EntityManager::refresh()`` and can be used in userland as well.
If you specify this hint and a query returns the data for an entity
@@ -1457,7 +1457,7 @@ several methods to interact with it:
- ``Query::setQueryCacheDriver($driver)`` - Allows to set a Cache
instance
- ``Query::setQueryCacheLifeTime($seconds = 3600)`` - Set lifetime
- ``Query::setQueryCacheLifeTime($seconds)`` - Set lifetime
of the query caching.
- ``Query::expireQueryCache($bool)`` - Enforce the expiring of the
query cache if set to true.

View File

@@ -215,6 +215,10 @@ specific to a particular entity class's lifecycle.
<?php
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Event\PrePersistEventArgs;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\HasLifecycleCallbacks;
use Doctrine\ORM\Mapping\PrePersist;
use Doctrine\ORM\Mapping\PreUpdate;
#[Entity]
#[HasLifecycleCallbacks]
@@ -453,13 +457,12 @@ prePersist
There are two ways for the ``prePersist`` event to be triggered:
- One is obviously when you call ``EntityManager::persist()``. The
event is also called for all :ref:`cascaded associations<transitive-persistence>`.
- The other is inside the
``flush()`` method when changes to associations are computed and
this association is marked as :ref:`cascade: persist<transitive-persistence>`. Any new entity found
during this operation is also persisted and ``prePersist`` called
on it. This is called :ref:`persistence by reachability<persistence-by-reachability>`.
- One is when you call ``EntityManager::persist()``. The
event is also called for all :ref:`cascaded associations<transitive-persistence>`.
- The other is inside the ``flush()`` method when changes to associations are computed and
this association is marked as :ref:`cascade: persist<transitive-persistence>`. Any new entity found
during this operation is also persisted and ``prePersist`` called
on it. This is called :ref:`persistence by reachability<persistence-by-reachability>`.
In both cases you get passed a ``PrePersistEventArgs`` instance
which has access to the entity and the entity manager.
@@ -705,13 +708,21 @@ not directly mapped by Doctrine.
- The ``postUpdate`` event occurs after the database
update operations to entity data. It is not called for a DQL
``UPDATE`` statement.
- The ``postPersist`` event occurs for an entity after
the entity has been made persistent. It will be invoked after the
database insert operation for that entity. A generated primary key value for
the entity will be available in the postPersist event.
- The ``postPersist`` event occurs for an entity after the entity has
been made persistent. It will be invoked after all database insert
operations for new entities have been performed. Generated primary
key values will be available for all entities at the time this
event is triggered.
- The ``postRemove`` event occurs for an entity after the
entity has been deleted. It will be invoked after the database
delete operations. It is not called for a DQL ``DELETE`` statement.
entity has been deleted. It will be invoked after all database
delete operations for entity rows have been executed. This event is
not called for a DQL ``DELETE`` statement.
.. note::
At the time ``postPersist`` is called, there may still be collection and/or
"extra" updates pending. The database may not yet be completely in
sync with the entity states in memory, not even for the new entities.
.. warning::
@@ -720,6 +731,19 @@ not directly mapped by Doctrine.
cascade remove relations. In this case, you should load yourself the proxy in
the associated ``pre*`` event.
.. warning::
Making changes to entities and calling ``EntityManager::flush()`` from within
``post*`` event handlers is strongly discouraged, and might be deprecated and
eventually prevented in the future.
The reason is that it causes re-entrance into ``UnitOfWork::commit()`` while a commit
is currently being processed. The ``UnitOfWork`` was never designed to support this,
and its behavior in this situation is not covered by any tests.
This may lead to entity or collection updates being missed, applied only in parts and
changes being lost at the end of the commit phase.
.. _reference-events-post-load:
postLoad

View File

@@ -167,7 +167,7 @@ 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
`attributes <https://github.com/doctrine/orm/issues/8868>` on traits, and attempts to
`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
due to complexity.

View File

@@ -6,7 +6,7 @@ Partial Objects
Creating Partial Objects through DQL is deprecated and
will be removed in the future, use data transfer object
support in DQL instead. (`Details
support in DQL instead. (\ `Details
<https://github.com/doctrine/orm/issues/8471>`_)
A partial object is an object whose state is not fully initialized

View File

@@ -253,7 +253,7 @@ Calling ``setParameter()`` automatically infers which type you are setting as
value. This works for integers, arrays of strings/integers, DateTime instances
and for managed entities. If you want to set a type explicitly you can call
the third argument to ``setParameter()`` explicitly. It accepts either a DBAL
Doctrine\DBAL\ParameterType::* or a DBAL Type name for conversion.
``Doctrine\DBAL\ParameterType::*`` or a DBAL Type name for conversion.
.. note::

View File

@@ -137,7 +137,7 @@ optimize the performance of the Flush Operation:
.. note::
Flush only a single entity with ``$entityManager->flush($entity)`` is deprecated and will be removed in ORM 3.0.
(`Details <https://github.com/doctrine/orm/issues/8459>`_)
(\ `Details <https://github.com/doctrine/orm/issues/8459>`_)
Query Internals
---------------

View File

@@ -192,6 +192,11 @@ be properly synchronized with the database when
database in the most efficient way and a single, short transaction,
taking care of maintaining referential integrity.
.. note::
Do not make any assumptions in your code about the number of queries
it takes to flush changes, about the ordering of ``INSERT``, ``UPDATE``
and ``DELETE`` queries or the order in which entities will be processed.
Example:

View File

@@ -18,7 +18,7 @@ before. There are some prerequisites for the tutorial that have to be
installed:
- PHP (latest stable version)
- Composer Package Manager (`Install Composer
- Composer Package Manager (\ `Install Composer
<https://getcomposer.org/doc/00-intro.md>`_)
The code of this tutorial is `available on Github <https://github.com/doctrine/doctrine2-orm-tutorial>`_.
@@ -321,7 +321,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
@@ -735,7 +735,7 @@ classes. We'll store them in ``src/Bug.php`` and ``src/User.php``, respectively.
#[ORM\Id]
#[ORM\Column(type: 'integer')]
#[ORM\GeneratedValue]
private int $id;
private int|null $id;
#[ORM\Column(type: 'string')]
private string $description;
@@ -1199,21 +1199,21 @@ which translates the YYYY-mm-dd HH:mm:ss database format
into a PHP DateTime instance and back.
After the field definitions, the two qualified references to the
user entity are defined. They are created by the ``many-to-one``
tag. The class name of the related entity has to be specified with
the ``target-entity`` attribute, which is enough information for
the database mapper to access the foreign-table. Since
user entity are defined. They are created by the ``ManyToOne``
attribute. The class name of the related entity has to be specified with
the ``targetEntity`` parameter, which is enough information for
the database mapper to access the foreign table. Since
``reporter`` and ``engineer`` are on the owning side of a
bi-directional relation, we also have to specify the ``inversed-by``
attribute. They have to point to the field names on the inverse
side of the relationship. We will see in the next example that the ``inversed-by``
attribute has a counterpart ``mapped-by`` which makes that
bi-directional relation, we also have to specify the ``inversedBy``
parameter. They have to point to the field names on the inverse
side of the relationship. We will see in the next example that the ``inversedBy``
parameter has a counterpart ``mappedBy`` which makes that
the inverse side.
The last definition is for the ``Bug#products`` collection. It
holds all products where the specific bug occurs. Again
you have to define the ``target-entity`` and ``field`` attributes
on the ``many-to-many`` tag.
you have to define the ``targetEntity`` and ``field`` parameters
on the ``ManyToMany`` attribute.
Finally, we'll add metadata mappings for the ``User`` entity.
@@ -1336,7 +1336,7 @@ Finally, we'll add metadata mappings for the ``User`` entity.
targetEntity: Bug
mappedBy: engineer
Here are some new things to mention about the ``one-to-many`` tags.
Here are some new things to mention about the ``OneToMany`` attribute.
Remember that we discussed about the inverse and owning side. Now
both reportedBugs and assignedBugs are inverse relations, which
means the join details have already been defined on the owning

View File

@@ -15,7 +15,7 @@ has a very simple API and implements the SPL interfaces ``Countable`` and
->setFirstResult(0)
->setMaxResults(100);
$paginator = new Paginator($query, $fetchJoinCollection = true);
$paginator = new Paginator($query, fetchJoinCollection: true);
$c = count($paginator);
foreach ($paginator as $post) {
@@ -36,10 +36,10 @@ correct result:
This behavior is only necessary if you actually fetch join a to-many
collection. You can disable this behavior by setting the
``$fetchJoinCollection`` flag to ``false``; in that case only 2 instead of the 3 queries
``fetchJoinCollection`` argument to ``false``; in that case only 2 instead of the 3 queries
described are executed. We hope to automate the detection for this in
the future.
.. note::
``$fetchJoinCollection`` flag set to ``true`` might affect results if you use aggregations in your query.
``fetchJoinCollection`` argument set to ``true`` might affect results if you use aggregations in your query.

View File

@@ -1010,7 +1010,7 @@ abstract class AbstractQuery
*
* Alias for getSingleResult(HYDRATE_SINGLE_SCALAR).
*
* @return bool|float|int|string The scalar result.
* @return bool|float|int|string|null The scalar result.
*
* @throws NoResultException If the query returned no result.
* @throws NonUniqueResultException If the query result is not unique.

View File

@@ -1117,4 +1117,14 @@ class Configuration extends \Doctrine\DBAL\Configuration
$this->_attributes['isLazyGhostObjectEnabled'] = $flag;
}
public function setRejectIdCollisionInIdentityMap(bool $flag): void
{
$this->_attributes['rejectIdCollisionInIdentityMap'] = $flag;
}
public function isRejectIdCollisionInIdentityMapEnabled(): bool
{
return $this->_attributes['rejectIdCollisionInIdentityMap'] ?? false;
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use function get_class;
use function sprintf;
final class EntityIdentityCollisionException extends ORMException
{
/**
* @param object $existingEntity
* @param object $newEntity
*/
public static function create($existingEntity, $newEntity, string $idHash): self
{
return new self(
sprintf(
<<<'EXCEPTION'
While adding an entity of class %s with an ID hash of "%s" to the identity map,
another object of class %s was already present for the same ID. This exception
is a safeguard against an internal inconsistency - IDs should uniquely map to
entity object instances. This problem may occur if:
- you use application-provided IDs and reuse ID values;
- database-provided IDs are reassigned after truncating the database without
clearing the EntityManager;
- you might have been using EntityManager#getReference() to create a reference
for a nonexistent ID that was subsequently (by the RDBMS) assigned to another
entity.
Otherwise, it might be an ORM-internal inconsistency, please report it.
EXCEPTION
,
get_class($newEntity),
$idHash,
get_class($existingEntity)
)
);
}
}

View File

@@ -128,7 +128,7 @@ class XmlDriver extends FileDriver
// Evaluate named queries
if (isset($xmlRoot->{'named-queries'})) {
foreach ($xmlRoot->{'named-queries'}->{'named-query'} as $namedQueryElement) {
foreach ($xmlRoot->{'named-queries'}->{'named-query'} ?? [] as $namedQueryElement) {
$metadata->addNamedQuery(
[
'name' => (string) $namedQueryElement['name'],
@@ -140,7 +140,7 @@ class XmlDriver extends FileDriver
// Evaluate native named queries
if (isset($xmlRoot->{'named-native-queries'})) {
foreach ($xmlRoot->{'named-native-queries'}->{'named-native-query'} as $nativeQueryElement) {
foreach ($xmlRoot->{'named-native-queries'}->{'named-native-query'} ?? [] as $nativeQueryElement) {
$metadata->addNamedNativeQuery(
[
'name' => isset($nativeQueryElement['name']) ? (string) $nativeQueryElement['name'] : null,
@@ -154,7 +154,7 @@ class XmlDriver extends FileDriver
// Evaluate sql result set mapping
if (isset($xmlRoot->{'sql-result-set-mappings'})) {
foreach ($xmlRoot->{'sql-result-set-mappings'}->{'sql-result-set-mapping'} as $rsmElement) {
foreach ($xmlRoot->{'sql-result-set-mappings'}->{'sql-result-set-mapping'} ?? [] as $rsmElement) {
$entities = [];
$columns = [];
foreach ($rsmElement as $entityElement) {
@@ -240,7 +240,7 @@ class XmlDriver extends FileDriver
// Evaluate <indexes...>
if (isset($xmlRoot->indexes)) {
$metadata->table['indexes'] = [];
foreach ($xmlRoot->indexes->index as $indexXml) {
foreach ($xmlRoot->indexes->index ?? [] as $indexXml) {
$index = [];
if (isset($indexXml['columns']) && ! empty($indexXml['columns'])) {
@@ -283,7 +283,7 @@ class XmlDriver extends FileDriver
// Evaluate <unique-constraints..>
if (isset($xmlRoot->{'unique-constraints'})) {
$metadata->table['uniqueConstraints'] = [];
foreach ($xmlRoot->{'unique-constraints'}->{'unique-constraint'} as $uniqueXml) {
foreach ($xmlRoot->{'unique-constraints'}->{'unique-constraint'} ?? [] as $uniqueXml) {
$unique = [];
if (isset($uniqueXml['columns']) && ! empty($uniqueXml['columns'])) {
@@ -370,7 +370,7 @@ class XmlDriver extends FileDriver
// Evaluate <id ...> mappings
$associationIds = [];
foreach ($xmlRoot->id as $idElement) {
foreach ($xmlRoot->id ?? [] as $idElement) {
if (isset($idElement['association-key']) && $this->evaluateBoolean($idElement['association-key'])) {
$associationIds[(string) $idElement['name']] = true;
continue;
@@ -439,7 +439,7 @@ class XmlDriver extends FileDriver
if (isset($oneToOneElement->{'join-column'})) {
$joinColumns[] = $this->joinColumnToArray($oneToOneElement->{'join-column'});
} elseif (isset($oneToOneElement->{'join-columns'})) {
foreach ($oneToOneElement->{'join-columns'}->{'join-column'} as $joinColumnElement) {
foreach ($oneToOneElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
$joinColumns[] = $this->joinColumnToArray($joinColumnElement);
}
}
@@ -490,7 +490,7 @@ class XmlDriver extends FileDriver
if (isset($oneToManyElement->{'order-by'})) {
$orderBy = [];
foreach ($oneToManyElement->{'order-by'}->{'order-by-field'} as $orderByField) {
foreach ($oneToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) {
$orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
? (string) $orderByField['direction']
: Criteria::ASC;
@@ -542,7 +542,7 @@ class XmlDriver extends FileDriver
if (isset($manyToOneElement->{'join-column'})) {
$joinColumns[] = $this->joinColumnToArray($manyToOneElement->{'join-column'});
} elseif (isset($manyToOneElement->{'join-columns'})) {
foreach ($manyToOneElement->{'join-columns'}->{'join-column'} as $joinColumnElement) {
foreach ($manyToOneElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
$joinColumns[] = $this->joinColumnToArray($joinColumnElement);
}
}
@@ -601,11 +601,11 @@ class XmlDriver extends FileDriver
$joinTable['options'] = $this->parseOptions($joinTableElement->options->children());
}
foreach ($joinTableElement->{'join-columns'}->{'join-column'} as $joinColumnElement) {
foreach ($joinTableElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
$joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumnElement);
}
foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} as $joinColumnElement) {
foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
$joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumnElement);
}
@@ -618,7 +618,7 @@ class XmlDriver extends FileDriver
if (isset($manyToManyElement->{'order-by'})) {
$orderBy = [];
foreach ($manyToManyElement->{'order-by'}->{'order-by-field'} as $orderByField) {
foreach ($manyToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) {
$orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
? (string) $orderByField['direction']
: Criteria::ASC;
@@ -644,9 +644,9 @@ class XmlDriver extends FileDriver
// Evaluate association-overrides
if (isset($xmlRoot->{'attribute-overrides'})) {
foreach ($xmlRoot->{'attribute-overrides'}->{'attribute-override'} as $overrideElement) {
foreach ($xmlRoot->{'attribute-overrides'}->{'attribute-override'} ?? [] as $overrideElement) {
$fieldName = (string) $overrideElement['name'];
foreach ($overrideElement->field as $field) {
foreach ($overrideElement->field ?? [] as $field) {
$mapping = $this->columnToArray($field);
$mapping['fieldName'] = $fieldName;
$metadata->setAttributeOverride($fieldName, $mapping);
@@ -656,14 +656,14 @@ class XmlDriver extends FileDriver
// Evaluate association-overrides
if (isset($xmlRoot->{'association-overrides'})) {
foreach ($xmlRoot->{'association-overrides'}->{'association-override'} as $overrideElement) {
foreach ($xmlRoot->{'association-overrides'}->{'association-override'} ?? [] as $overrideElement) {
$fieldName = (string) $overrideElement['name'];
$override = [];
// Check for join-columns
if (isset($overrideElement->{'join-columns'})) {
$joinColumns = [];
foreach ($overrideElement->{'join-columns'}->{'join-column'} as $joinColumnElement) {
foreach ($overrideElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
$joinColumns[] = $this->joinColumnToArray($joinColumnElement);
}
@@ -685,13 +685,13 @@ class XmlDriver extends FileDriver
}
if (isset($joinTableElement->{'join-columns'})) {
foreach ($joinTableElement->{'join-columns'}->{'join-column'} as $joinColumnElement) {
foreach ($joinTableElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
$joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumnElement);
}
}
if (isset($joinTableElement->{'inverse-join-columns'})) {
foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} as $joinColumnElement) {
foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
$joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumnElement);
}
}
@@ -715,14 +715,14 @@ class XmlDriver extends FileDriver
// Evaluate <lifecycle-callbacks...>
if (isset($xmlRoot->{'lifecycle-callbacks'})) {
foreach ($xmlRoot->{'lifecycle-callbacks'}->{'lifecycle-callback'} as $lifecycleCallback) {
foreach ($xmlRoot->{'lifecycle-callbacks'}->{'lifecycle-callback'} ?? [] as $lifecycleCallback) {
$metadata->addLifecycleCallback((string) $lifecycleCallback['method'], constant('Doctrine\ORM\Events::' . (string) $lifecycleCallback['type']));
}
}
// Evaluate entity listener
if (isset($xmlRoot->{'entity-listeners'})) {
foreach ($xmlRoot->{'entity-listeners'}->{'entity-listener'} as $listenerElement) {
foreach ($xmlRoot->{'entity-listeners'}->{'entity-listener'} ?? [] as $listenerElement) {
$className = (string) $listenerElement['class'];
// Evaluate the listener using naming convention.
if ($listenerElement->count() === 0) {
@@ -744,16 +744,14 @@ class XmlDriver extends FileDriver
/**
* Parses (nested) option elements.
*
* @param SimpleXMLElement $options The XML element.
*
* @return mixed[] The options array.
* @psalm-return array<int|string, array<int|string, mixed|string>|bool|string>
*/
private function parseOptions(SimpleXMLElement $options): array
private function parseOptions(?SimpleXMLElement $options): array
{
$array = [];
foreach ($options as $option) {
foreach ($options ?? [] as $option) {
if ($option->count()) {
$value = $this->parseOptions($option->children());
} else {
@@ -816,7 +814,7 @@ class XmlDriver extends FileDriver
}
if (isset($joinColumnElement['options'])) {
$joinColumn['options'] = $this->parseOptions($joinColumnElement['options']->children());
$joinColumn['options'] = $this->parseOptions($joinColumnElement['options'] ? $joinColumnElement['options']->children() : null);
}
return $joinColumn;
@@ -944,7 +942,10 @@ class XmlDriver extends FileDriver
private function getCascadeMappings(SimpleXMLElement $cascadeElement): array
{
$cascades = [];
foreach ($cascadeElement->children() as $action) {
$children = $cascadeElement->children();
assert($children !== null);
foreach ($children as $action) {
// According to the JPA specifications, XML uses "cascade-persist"
// instead of "persist". Here, both variations
// are supported because YAML, Annotation and Attribute use "persist"
@@ -969,19 +970,19 @@ class XmlDriver extends FileDriver
if (isset($xmlElement->entity)) {
foreach ($xmlElement->entity as $entityElement) {
/** @psalm-var class-string */
/** @psalm-var class-string $entityName */
$entityName = (string) $entityElement['name'];
$result[$entityName] = $entityElement;
}
} elseif (isset($xmlElement->{'mapped-superclass'})) {
foreach ($xmlElement->{'mapped-superclass'} as $mappedSuperClass) {
/** @psalm-var class-string */
/** @psalm-var class-string $className */
$className = (string) $mappedSuperClass['name'];
$result[$className] = $mappedSuperClass;
}
} elseif (isset($xmlElement->embeddable)) {
foreach ($xmlElement->embeddable as $embeddableElement) {
/** @psalm-var class-string */
/** @psalm-var class-string $embeddableName */
$embeddableName = (string) $embeddableElement['name'];
$result[$embeddableName] = $embeddableElement;
}

View File

@@ -30,7 +30,10 @@ class UnderscoreNamingStrategy implements NamingStrategy
/** @var int */
private $case;
/** @var string */
/**
* @var string
* @psalm-var non-empty-string
*/
private $pattern;
/**

View File

@@ -101,10 +101,11 @@ final class ORMSetup
array $paths,
bool $isDevMode = false,
?string $proxyDir = null,
?CacheItemPoolInterface $cache = null
?CacheItemPoolInterface $cache = null,
bool $reportFieldsWhereDeclared = false
): Configuration {
$config = self::createConfiguration($isDevMode, $proxyDir, $cache);
$config->setMetadataDriverImpl(new AttributeDriver($paths));
$config->setMetadataDriverImpl(new AttributeDriver($paths, $reportFieldsWhereDeclared));
return $config;
}

View File

@@ -9,7 +9,7 @@ namespace Doctrine\ORM\Query\AST;
*
* @link www.doctrine-project.org
*/
class ConditionalFactor extends Node
class ConditionalFactor extends Node implements Phase2OptimizableConditional
{
/** @var bool */
public $not = false;

View File

@@ -9,12 +9,12 @@ namespace Doctrine\ORM\Query\AST;
*
* @link www.doctrine-project.org
*/
class ConditionalPrimary extends Node
class ConditionalPrimary extends Node implements Phase2OptimizableConditional
{
/** @var Node|null */
public $simpleConditionalExpression;
/** @var ConditionalExpression|null */
/** @var ConditionalExpression|Phase2OptimizableConditional|null */
public $conditionalExpression;
/** @return bool */

View File

@@ -9,7 +9,7 @@ namespace Doctrine\ORM\Query\AST;
*
* @link www.doctrine-project.org
*/
class ConditionalTerm extends Node
class ConditionalTerm extends Node implements Phase2OptimizableConditional
{
/** @var mixed[] */
public $conditionalFactors = [];

View File

@@ -6,10 +6,10 @@ namespace Doctrine\ORM\Query\AST;
class HavingClause extends Node
{
/** @var ConditionalExpression */
/** @var ConditionalExpression|Phase2OptimizableConditional */
public $conditionalExpression;
/** @param ConditionalExpression $conditionalExpression */
/** @param ConditionalExpression|Phase2OptimizableConditional $conditionalExpression */
public function __construct($conditionalExpression)
{
$this->conditionalExpression = $conditionalExpression;

View File

@@ -25,7 +25,7 @@ class Join extends Node
/** @var Node|null */
public $joinAssociationDeclaration = null;
/** @var ConditionalExpression|null */
/** @var ConditionalExpression|Phase2OptimizableConditional|null */
public $conditionalExpression = null;
/**

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Query\AST;
/**
* Marks types that can be used in place of a ConditionalExpression as a phase
* 2 optimization.
*
* @internal
*
* @psalm-inheritors ConditionalPrimary|ConditionalFactor|ConditionalTerm
*/
interface Phase2OptimizableConditional
{
}

View File

@@ -11,15 +11,15 @@ namespace Doctrine\ORM\Query\AST;
*/
class WhenClause extends Node
{
/** @var ConditionalExpression */
/** @var ConditionalExpression|Phase2OptimizableConditional */
public $caseConditionExpression;
/** @var mixed */
public $thenScalarExpression = null;
/**
* @param ConditionalExpression $caseConditionExpression
* @param mixed $thenScalarExpression
* @param ConditionalExpression|Phase2OptimizableConditional $caseConditionExpression
* @param mixed $thenScalarExpression
*/
public function __construct($caseConditionExpression, $thenScalarExpression)
{

View File

@@ -11,10 +11,10 @@ namespace Doctrine\ORM\Query\AST;
*/
class WhereClause extends Node
{
/** @var ConditionalExpression|ConditionalTerm */
/** @var ConditionalExpression|Phase2OptimizableConditional */
public $conditionalExpression;
/** @param ConditionalExpression $conditionalExpression */
/** @param ConditionalExpression|Phase2OptimizableConditional $conditionalExpression */
public function __construct($conditionalExpression)
{
$this->conditionalExpression = $conditionalExpression;

View File

@@ -154,6 +154,11 @@ class ResultSetMappingBuilder extends ResultSetMapping
}
$this->addFieldResult($alias, $columnAlias, $propertyName);
$enumType = $classMetadata->getFieldMapping($propertyName)['enumType'] ?? null;
if (! empty($enumType)) {
$this->addEnumResult($columnAlias, $enumType);
}
}
foreach ($classMetadata->associationMappings as $associationMapping) {

View File

@@ -1010,9 +1010,9 @@ class SqlWalker implements TreeWalker
/**
* Walks down a JoinAssociationDeclaration AST node, thereby generating the appropriate SQL.
*
* @param AST\JoinAssociationDeclaration $joinAssociationDeclaration
* @param int $joinType
* @param AST\ConditionalExpression $condExpr
* @param AST\JoinAssociationDeclaration $joinAssociationDeclaration
* @param int $joinType
* @param AST\ConditionalExpression|AST\Phase2OptimizableConditional $condExpr
* @psalm-param AST\Join::JOIN_TYPE_* $joinType
*
* @return string
@@ -2048,7 +2048,7 @@ class SqlWalker implements TreeWalker
/**
* Walk down a ConditionalExpression AST node, thereby generating the appropriate SQL.
*
* @param AST\ConditionalExpression $condExpr
* @param AST\ConditionalExpression|AST\Phase2OptimizableConditional $condExpr
*
* @return string
*
@@ -2068,7 +2068,7 @@ class SqlWalker implements TreeWalker
/**
* Walks down a ConditionalTerm AST node, thereby generating the appropriate SQL.
*
* @param AST\ConditionalTerm $condTerm
* @param AST\ConditionalTerm|AST\ConditionalFactor|AST\ConditionalPrimary $condTerm
*
* @return string
*
@@ -2088,7 +2088,7 @@ class SqlWalker implements TreeWalker
/**
* Walks down a ConditionalFactor AST node, thereby generating the appropriate SQL.
*
* @param AST\ConditionalFactor $factor
* @param AST\ConditionalFactor|AST\ConditionalPrimary $factor
*
* @return string The SQL.
*

View File

@@ -1376,7 +1376,7 @@ public function __construct(<params>)
$this->staticReflection[$metadata->name]['methods'][] = strtolower($methodName);
$var = sprintf('%sMethodTemplate', $type);
$template = static::$$var;
$template = (string) static::$$var;
$methodTypeHint = '';
$types = Type::getTypesMap();
@@ -1695,7 +1695,7 @@ public function __construct(<params>)
}
if (isset($fieldMapping['options']['comment']) && $fieldMapping['options']['comment']) {
$options[] = '"comment"="' . str_replace('"', '""', $fieldMapping['options']['comment']) . '"';
$options[] = '"comment"="' . str_replace('"', '""', (string) $fieldMapping['options']['comment']) . '"';
}
if (isset($fieldMapping['options']['collation']) && $fieldMapping['options']['collation']) {

View File

@@ -404,7 +404,7 @@ class LimitSubqueryOutputWalker extends SqlWalker
/**
* @return string[][]
* @psalm-return array{0: list<string>, 1: list<string>}
* @psalm-return array{0: list<non-empty-string>, 1: list<string>}
*/
private function generateSqlAliasReplacements(): array
{

View File

@@ -6,7 +6,6 @@ namespace Doctrine\ORM\Tools\Pagination;
use Doctrine\ORM\Query\AST\ArithmeticExpression;
use Doctrine\ORM\Query\AST\ConditionalExpression;
use Doctrine\ORM\Query\AST\ConditionalFactor;
use Doctrine\ORM\Query\AST\ConditionalPrimary;
use Doctrine\ORM\Query\AST\ConditionalTerm;
use Doctrine\ORM\Query\AST\InListExpression;
@@ -96,10 +95,7 @@ class WhereInWalker extends TreeWalkerAdapter
),
]
);
} elseif (
$AST->whereClause->conditionalExpression instanceof ConditionalExpression
|| $AST->whereClause->conditionalExpression instanceof ConditionalFactor
) {
} else {
$tmpPrimary = new ConditionalPrimary();
$tmpPrimary->conditionalExpression = $AST->whereClause->conditionalExpression;
$AST->whereClause->conditionalExpression = new ConditionalTerm(

View File

@@ -23,6 +23,7 @@ use Doctrine\ORM\Event\PreFlushEventArgs;
use Doctrine\ORM\Event\PrePersistEventArgs;
use Doctrine\ORM\Event\PreRemoveEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Exception\EntityIdentityCollisionException;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\Exception\UnexpectedAssociationValue;
use Doctrine\ORM\Id\AssignedGenerator;
@@ -692,6 +693,7 @@ class UnitOfWork implements PropertyChangedListener
if ($class->isCollectionValuedAssociation($name) && $value !== null) {
if ($value instanceof PersistentCollection) {
if ($value->getOwner() === $entity) {
$actualData[$name] = $value;
continue;
}
@@ -1162,13 +1164,13 @@ class UnitOfWork implements PropertyChangedListener
*/
private function executeInserts(): void
{
$entities = $this->computeInsertExecutionOrder();
$entities = $this->computeInsertExecutionOrder();
$eventsToDispatch = [];
foreach ($entities as $entity) {
$oid = spl_object_id($entity);
$class = $this->em->getClassMetadata(get_class($entity));
$persister = $this->getEntityPersister($class->name);
$invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
$persister->addInsert($entity);
@@ -1195,10 +1197,24 @@ class UnitOfWork implements PropertyChangedListener
$this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
}
$invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
if ($invoke !== ListenersInvoker::INVOKE_NONE) {
$this->listenersInvoker->invoke($class, Events::postPersist, $entity, new PostPersistEventArgs($entity, $this->em), $invoke);
$eventsToDispatch[] = ['class' => $class, 'entity' => $entity, 'invoke' => $invoke];
}
}
// Defer dispatching `postPersist` events to until all entities have been inserted and post-insert
// IDs have been assigned.
foreach ($eventsToDispatch as $event) {
$this->listenersInvoker->invoke(
$event['class'],
Events::postPersist,
$event['entity'],
new PostPersistEventArgs($event['entity'], $this->em),
$event['invoke']
);
}
}
/**
@@ -1268,7 +1284,8 @@ class UnitOfWork implements PropertyChangedListener
*/
private function executeDeletions(): void
{
$entities = $this->computeDeleteExecutionOrder();
$entities = $this->computeDeleteExecutionOrder();
$eventsToDispatch = [];
foreach ($entities as $entity) {
$oid = spl_object_id($entity);
@@ -1293,9 +1310,20 @@ class UnitOfWork implements PropertyChangedListener
}
if ($invoke !== ListenersInvoker::INVOKE_NONE) {
$this->listenersInvoker->invoke($class, Events::postRemove, $entity, new PostRemoveEventArgs($entity, $this->em), $invoke);
$eventsToDispatch[] = ['class' => $class, 'entity' => $entity, 'invoke' => $invoke];
}
}
// Defer dispatching `postRemove` events to until all entities have been removed.
foreach ($eventsToDispatch as $event) {
$this->listenersInvoker->invoke(
$event['class'],
Events::postRemove,
$event['entity'],
new PostRemoveEventArgs($event['entity'], $this->em),
$event['invoke']
);
}
}
/** @return list<object> */
@@ -1623,6 +1651,7 @@ class UnitOfWork implements PropertyChangedListener
* the entity in question is already managed.
*
* @throws ORMInvalidArgumentException
* @throws EntityIdentityCollisionException
*
* @ignore
*/
@@ -1634,27 +1663,38 @@ class UnitOfWork implements PropertyChangedListener
if (isset($this->identityMap[$className][$idHash])) {
if ($this->identityMap[$className][$idHash] !== $entity) {
throw new RuntimeException(sprintf(
if ($this->em->getConfiguration()->isRejectIdCollisionInIdentityMapEnabled()) {
throw EntityIdentityCollisionException::create($this->identityMap[$className][$idHash], $entity, $idHash);
}
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10785',
<<<'EXCEPTION'
While adding an entity of class %s with an ID hash of "%s" to the identity map,
another object of class %s was already present for the same ID. This exception
is a safeguard against an internal inconsistency - IDs should uniquely map to
entity object instances. This problem may occur if:
another object of class %s was already present for the same ID. This will trigger
an exception in ORM 3.0.
IDs should uniquely map to entity object instances. This problem may occur if:
- you use application-provided IDs and reuse ID values;
- database-provided IDs are reassigned after truncating the database without
clearing the EntityManager;
- you might have been using EntityManager#getReference() to create a reference
for a nonexistent ID that was subsequently (by the RDBMS) assigned to another
entity.
- database-provided IDs are reassigned after truncating the database without
clearing the EntityManager;
- you might have been using EntityManager#getReference() to create a reference
for a nonexistent ID that was subsequently (by the RDBMS) assigned to another
entity.
Otherwise, it might be an ORM-internal inconsistency, please report it.
Otherwise, it might be an ORM-internal inconsistency, please report it.
To opt-in to the new exception, call
\Doctrine\ORM\Configuration::setRejectIdCollisionInIdentityMap on the entity
manager's configuration.
EXCEPTION
,
get_class($entity),
$idHash,
get_class($this->identityMap[$className][$idHash])
));
);
}
return false;
@@ -3014,14 +3054,18 @@ EXCEPTION
// We are negating the condition here. Other cases will assume it is valid!
case $hints['fetchMode'][$class->name][$field] !== ClassMetadata::FETCH_EAGER:
$newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $normalizedAssociatedId);
$this->registerManaged($newValue, $associatedId, []);
break;
// Deferred eager load only works for single identifier classes
case isset($hints[self::HINT_DEFEREAGERLOAD]) && ! $targetClass->isIdentifierComposite:
case isset($hints[self::HINT_DEFEREAGERLOAD]) &&
$hints[self::HINT_DEFEREAGERLOAD] &&
! $targetClass->isIdentifierComposite:
// TODO: Is there a faster approach?
$this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($normalizedAssociatedId);
$newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $normalizedAssociatedId);
$this->registerManaged($newValue, $associatedId, []);
break;
default:
@@ -3029,13 +3073,6 @@ EXCEPTION
$newValue = $this->em->find($assoc['targetEntity'], $normalizedAssociatedId);
break;
}
if ($newValue === null) {
break;
}
$this->registerManaged($newValue, $associatedId, []);
break;
}
$this->originalEntityData[$oid][$field] = $newValue;

View File

@@ -440,6 +440,11 @@ parameters:
count: 1
path: lib/Doctrine/ORM/Query/SqlWalker.php
-
message: "#^Parameter \\#1 \\$condTerm of method Doctrine\\\\ORM\\\\Query\\\\SqlWalker\\:\\:walkConditionalTerm\\(\\) expects Doctrine\\\\ORM\\\\Query\\\\AST\\\\ConditionalFactor\\|Doctrine\\\\ORM\\\\Query\\\\AST\\\\ConditionalPrimary\\|Doctrine\\\\ORM\\\\Query\\\\AST\\\\ConditionalTerm, Doctrine\\\\ORM\\\\Query\\\\AST\\\\Phase2OptimizableConditional given\\.$#"
count: 1
path: lib/Doctrine/ORM/Query/SqlWalker.php
-
message: "#^Result of && is always false\\.$#"
count: 1
@@ -595,16 +600,6 @@ parameters:
count: 1
path: lib/Doctrine/ORM/Tools/Export/Driver/YamlExporter.php
-
message: "#^Instanceof between \\*NEVER\\* and Doctrine\\\\ORM\\\\Query\\\\AST\\\\ConditionalFactor will always evaluate to false\\.$#"
count: 1
path: lib/Doctrine/ORM/Tools/Pagination/WhereInWalker.php
-
message: "#^Instanceof between Doctrine\\\\ORM\\\\Query\\\\AST\\\\ConditionalExpression and Doctrine\\\\ORM\\\\Query\\\\AST\\\\ConditionalPrimary will always evaluate to false\\.$#"
count: 1
path: lib/Doctrine/ORM/Tools/Pagination/WhereInWalker.php
-
message: "#^Else branch is unreachable because ternary operator condition is always true\\.$#"
count: 1

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="5.13.1@086b94371304750d1c673315321a55d15fc59015">
<files psalm-version="5.15.0@5c774aca4746caf3d239d9c8cadb9f882ca29352">
<file src="lib/Doctrine/ORM/AbstractQuery.php">
<DeprecatedClass>
<code>IterableResult</code>
@@ -916,13 +916,12 @@
<code><![CDATA[$metadata->table]]></code>
</InvalidPropertyAssignmentValue>
<InvalidPropertyFetch>
<code><![CDATA[$indexXml->options]]></code>
<code><![CDATA[$uniqueXml->options]]></code>
<code><![CDATA[$xmlRoot->{'discriminator-column'}]]></code>
<code><![CDATA[$xmlRoot->{'discriminator-map'}]]></code>
</InvalidPropertyFetch>
<InvalidReturnStatement>
<code>$mapping</code>
<code>$result</code>
<code><![CDATA[[
'usage' => $usage,
'region' => $region,
@@ -946,6 +945,7 @@
* options?: array
* }</code>
<code>array{usage: int|null, region?: string}</code>
<code>loadMappingFile</code>
</InvalidReturnType>
<MissingParamType>
<code>$fileExtension</code>
@@ -955,18 +955,9 @@
<code>$metadata</code>
</MoreSpecificImplementedParamType>
<NoInterfaceProperties>
<code><![CDATA[$indexXml->options]]></code>
<code><![CDATA[$uniqueXml->options]]></code>
<code><![CDATA[$xmlRoot->{'discriminator-column'}]]></code>
<code><![CDATA[$xmlRoot->{'discriminator-map'}]]></code>
</NoInterfaceProperties>
<PossiblyNullArgument>
<code><![CDATA[$joinColumnElement['options']->children()]]></code>
<code><![CDATA[$option->children()]]></code>
</PossiblyNullArgument>
<PossiblyNullIterator>
<code><![CDATA[$cascadeElement->children()]]></code>
</PossiblyNullIterator>
<TypeDoesNotContainType>
<code><![CDATA[$xmlRoot->getName() === 'embeddable']]></code>
<code><![CDATA[$xmlRoot->getName() === 'entity']]></code>
@@ -2031,18 +2022,13 @@
</PossiblyFalseArgument>
<PossiblyInvalidArgument>
<code>$AST</code>
<code>$conditionalExpression</code>
<code>$expr</code>
<code>$pathExp</code>
<code><![CDATA[$this->ConditionalExpression()]]></code>
<code><![CDATA[$this->ConditionalExpression()]]></code>
<code><![CDATA[$this->lexer->getLiteral($token)]]></code>
<code><![CDATA[$this->lexer->getLiteral($token)]]></code>
<code><![CDATA[$this->lexer->getLiteral($token)]]></code>
</PossiblyInvalidArgument>
<PossiblyInvalidPropertyAssignmentValue>
<code><![CDATA[$this->ConditionalExpression()]]></code>
<code><![CDATA[$this->ConditionalExpression()]]></code>
<code><![CDATA[$this->SimpleArithmeticExpression()]]></code>
</PossiblyInvalidPropertyAssignmentValue>
<PossiblyNullArgument>
@@ -2154,11 +2140,6 @@
<ImplicitToStringCast>
<code>$expr</code>
</ImplicitToStringCast>
<InvalidArgument>
<code>$condExpr</code>
<code>$condTerm</code>
<code>$factor</code>
</InvalidArgument>
<InvalidNullableReturnType>
<code>string</code>
</InvalidNullableReturnType>
@@ -2167,7 +2148,6 @@
</MoreSpecificImplementedParamType>
<PossiblyInvalidArgument>
<code><![CDATA[$aggExpression->pathExpression]]></code>
<code><![CDATA[$whereClause->conditionalExpression]]></code>
</PossiblyInvalidArgument>
<PossiblyNullArgument>
<code><![CDATA[$AST->whereClause]]></code>
@@ -2203,7 +2183,6 @@
</PossiblyUndefinedArrayOffset>
<RedundantConditionGivenDocblockType>
<code>$whereClause !== null</code>
<code><![CDATA[($factor->not ? 'NOT ' : '') . $this->walkConditionalPrimary($factor->conditionalPrimary)]]></code>
</RedundantConditionGivenDocblockType>
</file>
<file src="lib/Doctrine/ORM/Query/TreeWalkerAdapter.php">
@@ -2560,9 +2539,6 @@
<NonInvariantDocblockPropertyType>
<code>$_extension</code>
</NonInvariantDocblockPropertyType>
<PossiblyFalseArgument>
<code><![CDATA[$simpleXml->asXML()]]></code>
</PossiblyFalseArgument>
<RedundantCondition>
<code><![CDATA[$field['associationKey']]]></code>
<code><![CDATA[isset($field['associationKey']) && $field['associationKey']]]></code>
@@ -2645,21 +2621,6 @@
<code>$orderByClause</code>
</PropertyNotSetInConstructor>
</file>
<file src="lib/Doctrine/ORM/Tools/Pagination/WhereInWalker.php">
<DocblockTypeContradiction>
<code><![CDATA[$AST->whereClause->conditionalExpression instanceof ConditionalExpression
|| $AST->whereClause->conditionalExpression instanceof ConditionalFactor]]></code>
<code><![CDATA[$AST->whereClause->conditionalExpression instanceof ConditionalFactor]]></code>
<code><![CDATA[$AST->whereClause->conditionalExpression instanceof ConditionalPrimary]]></code>
</DocblockTypeContradiction>
<PossiblyInvalidPropertyAssignmentValue>
<code><![CDATA[$AST->whereClause->conditionalExpression]]></code>
</PossiblyInvalidPropertyAssignmentValue>
<RedundantConditionGivenDocblockType>
<code><![CDATA[$AST->whereClause->conditionalExpression instanceof ConditionalExpression
|| $AST->whereClause->conditionalExpression instanceof ConditionalFactor]]></code>
</RedundantConditionGivenDocblockType>
</file>
<file src="lib/Doctrine/ORM/Tools/SchemaTool.php">
<ArgumentTypeCoercion>
<code>$classes</code>

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Issue9300;
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\ManyToMany;
/**
* @Entity
*/
class Issue9300Child
{
/**
* @var int
* @Id
* @Column(type="integer")
* @GeneratedValue
*/
public $id;
/**
* @var Collection<int, Issue9300Parent>
* @ManyToMany(targetEntity="Issue9300Parent")
*/
public $parents;
/**
* @var string
* @Column(type="string")
*/
public $name;
public function __construct()
{
$this->parents = new ArrayCollection();
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Issue9300;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
/**
* @Entity
*/
class Issue9300Parent
{
/**
* @var int
* @Id
* @Column(type="integer")
* @GeneratedValue
*/
public $id;
/**
* @var string
* @Column(type="string")
*/
public $name;
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\ORM\EntityNotFoundException;
use Doctrine\ORM\Exception\EntityIdentityCollisionException;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\ORMInvalidArgumentException;
use Doctrine\ORM\PersistentCollection;
@@ -20,7 +21,6 @@ use Doctrine\Tests\Models\CMS\CmsPhonenumber;
use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\Tests\OrmFunctionalTestCase;
use InvalidArgumentException;
use RuntimeException;
use function get_class;
@@ -1329,6 +1329,8 @@ class BasicFunctionalTest extends OrmFunctionalTestCase
public function testItThrowsWhenReferenceUsesIdAssignedByDatabase(): void
{
$this->_em->getConfiguration()->setRejectIdCollisionInIdentityMap(true);
$user = new CmsUser();
$user->name = 'test';
$user->username = 'test';
@@ -1345,7 +1347,7 @@ class BasicFunctionalTest extends OrmFunctionalTestCase
// Now the database will assign an ID to the $user2 entity, but that place
// in the identity map is already taken by user error.
$this->expectException(RuntimeException::class);
$this->expectException(EntityIdentityCollisionException::class);
$this->expectExceptionMessageMatches('/another object .* was already present for the same ID/');
// depending on ID generation strategy, the ID may be asssigned already here

View File

@@ -4,12 +4,18 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\ORM\Event\PostPersistEventArgs;
use Doctrine\ORM\Event\PostRemoveEventArgs;
use Doctrine\ORM\Event\PreFlushEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\UnitOfWork;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use Doctrine\Tests\Models\Company\CompanyContractListener;
use Doctrine\Tests\Models\Company\CompanyFixContract;
use Doctrine\Tests\Models\Company\CompanyPerson;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Assert;
/** @group DDC-1955 */
class EntityListenersTest extends OrmFunctionalTestCase
@@ -96,6 +102,45 @@ class EntityListenersTest extends OrmFunctionalTestCase
self::assertInstanceOf(LifecycleEventArgs::class, $this->listener->postPersistCalls[0][1]);
}
public function testPostPersistCalledAfterAllInsertsHaveBeenPerformedAndIdsHaveBeenAssigned(): void
{
$object1 = new CompanyFixContract();
$object1->setFixPrice(2000);
$object2 = new CompanyPerson();
$object2->setName('J. Doe');
$this->_em->persist($object1);
$this->_em->persist($object2);
$listener = new class ([$object1, $object2]) {
/** @var array<object> */
private $trackedObjects;
/** @var int */
public $invocationCount = 0;
public function __construct(array $trackedObjects)
{
$this->trackedObjects = $trackedObjects;
}
public function postPersist(PostPersistEventArgs $args): void
{
foreach ($this->trackedObjects as $object) {
Assert::assertNotNull($object->getId());
}
++$this->invocationCount;
}
};
$this->_em->getEventManager()->addEventListener(Events::postPersist, $listener);
$this->_em->flush();
self::assertSame(2, $listener->invocationCount);
}
public function testPreUpdateListeners(): void
{
$fix = new CompanyFixContract();
@@ -175,4 +220,50 @@ class EntityListenersTest extends OrmFunctionalTestCase
self::assertInstanceOf(CompanyFixContract::class, $this->listener->postRemoveCalls[0][0]);
self::assertInstanceOf(LifecycleEventArgs::class, $this->listener->postRemoveCalls[0][1]);
}
public function testPostRemoveCalledAfterAllRemovalsHaveBeenPerformed(): void
{
$object1 = new CompanyFixContract();
$object1->setFixPrice(2000);
$object2 = new CompanyPerson();
$object2->setName('J. Doe');
$this->_em->persist($object1);
$this->_em->persist($object2);
$this->_em->flush();
$listener = new class ($this->_em->getUnitOfWork(), [$object1, $object2]) {
/** @var UnitOfWork */
private $uow;
/** @var array<object> */
private $trackedObjects;
/** @var int */
public $invocationCount = 0;
public function __construct(UnitOfWork $uow, array $trackedObjects)
{
$this->uow = $uow;
$this->trackedObjects = $trackedObjects;
}
public function postRemove(PostRemoveEventArgs $args): void
{
foreach ($this->trackedObjects as $object) {
Assert::assertFalse($this->uow->isInIdentityMap($object));
}
++$this->invocationCount;
}
};
$this->_em->getEventManager()->addEventListener(Events::postRemove, $listener);
$this->_em->remove($object1);
$this->_em->remove($object2);
$this->_em->flush();
self::assertSame(2, $listener->invocationCount);
}
}

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type as DBALType;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
@@ -31,6 +33,8 @@ use Doctrine\Tests\Models\DDC3899\DDC3899User;
use Doctrine\Tests\OrmFunctionalTestCase;
use InvalidArgumentException;
use function class_exists;
class NativeQueryTest extends OrmFunctionalTestCase
{
use SQLResultCasing;
@@ -75,6 +79,43 @@ class NativeQueryTest extends OrmFunctionalTestCase
self::assertEquals('Roman', $users[0]->name);
}
public function testNativeQueryWithArrayParameter(): void
{
$user = new CmsUser();
$user->name = 'William Shatner';
$user->username = 'wshatner';
$user->status = 'dev';
$this->_em->persist($user);
$user = new CmsUser();
$user->name = 'Leonard Nimoy';
$user->username = 'lnimoy';
$user->status = 'dev';
$this->_em->persist($user);
$user = new CmsUser();
$user->name = 'DeForest Kelly';
$user->username = 'dkelly';
$user->status = 'dev';
$this->_em->persist($user);
$this->_em->flush();
$this->_em->clear();
$rsm = new ResultSetMapping();
$rsm->addEntityResult(CmsUser::class, 'u');
$rsm->addFieldResult('u', $this->getSQLResultCasing($this->platform, 'id'), 'id');
$rsm->addFieldResult('u', $this->getSQLResultCasing($this->platform, 'name'), 'name');
$query = $this->_em->createNativeQuery('SELECT id, name FROM cms_users WHERE username IN (?) ORDER BY username', $rsm);
$query->setParameter(1, ['wshatner', 'lnimoy'], class_exists(ArrayParameterType::class) ? ArrayParameterType::STRING : Connection::PARAM_STR_ARRAY);
$users = $query->getResult();
self::assertCount(2, $users);
self::assertInstanceOf(CmsUser::class, $users[0]);
self::assertEquals('Leonard Nimoy', $users[0]->name);
self::assertEquals('William Shatner', $users[1]->name);
}
public function testBasicNativeQueryWithMetaResult(): void
{
$user = new CmsUser();

View File

@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
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\OneToOne;
use Doctrine\ORM\Mapping\Table;
use Doctrine\ORM\UnitOfWork;
use Doctrine\Tests\OrmFunctionalTestCase;
use function get_class;
/** @group GH10808 */
class GH10808Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
GH10808Appointment::class,
GH10808AppointmentChild::class
);
}
public function testDQLDeferredEagerLoad(): void
{
$appointment = new GH10808Appointment();
$this->_em->persist($appointment);
$this->_em->flush();
$this->_em->clear();
$query = $this->_em->createQuery(
'SELECT appointment from Doctrine\Tests\ORM\Functional\Ticket\GH10808Appointment appointment
JOIN appointment.child appointment_child
WITH appointment_child.id = 1'
);
// By default, UnitOfWork::HINT_DEFEREAGERLOAD is set to 'true'
$deferredLoadResult = $query->getSingleResult();
// Clear the EM to prevent the recovery of the loaded instance, which would otherwise result in a proxy.
$this->_em->clear();
$eagerLoadResult = $query->setHint(UnitOfWork::HINT_DEFEREAGERLOAD, false)->getSingleResult();
self::assertNotEquals(
GH10808AppointmentChild::class,
get_class($deferredLoadResult->child),
'$deferredLoadResult->child should be a proxy'
);
self::assertEquals(
GH10808AppointmentChild::class,
get_class($eagerLoadResult->child),
'$eagerLoadResult->child should not be a proxy'
);
}
}
/**
* @Entity
* @Table(name="gh10808_appointment")
*/
class GH10808Appointment
{
/**
* @var int
* @Id
* @Column(type="integer")
* @GeneratedValue
*/
public $id;
/**
* @var GH10808AppointmentChild
* @OneToOne(targetEntity="GH10808AppointmentChild", cascade={"persist", "remove"}, fetch="EAGER")
* @JoinColumn(name="child_id", referencedColumnName="id")
*/
public $child;
public function __construct()
{
$this->child = new GH10808AppointmentChild();
}
}
/**
* @Entity
* @Table(name="gh10808_appointment_child")
*/
class GH10808AppointmentChild
{
/**
* @var int
* @Id
* @Column(type="integer")
* @GeneratedValue
*/
private $id;
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Event\PostPersistEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH10869Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
GH10869Entity::class,
]);
}
public function testPostPersistListenerUpdatingObjectFieldWhileOtherInsertPending(): void
{
$entity1 = new GH10869Entity();
$this->_em->persist($entity1);
$entity2 = new GH10869Entity();
$this->_em->persist($entity2);
$this->_em->getEventManager()->addEventListener(Events::postPersist, new class {
public function postPersist(PostPersistEventArgs $args): void
{
$object = $args->getObject();
$objectManager = $args->getObjectManager();
$object->field = 'test ' . $object->id;
$objectManager->flush();
}
});
$this->_em->flush();
$this->_em->clear();
self::assertSame('test ' . $entity1->id, $entity1->field);
self::assertSame('test ' . $entity2->id, $entity2->field);
$entity1Reloaded = $this->_em->find(GH10869Entity::class, $entity1->id);
self::assertSame($entity1->field, $entity1Reloaded->field);
$entity2Reloaded = $this->_em->find(GH10869Entity::class, $entity2->id);
self::assertSame($entity2->field, $entity2Reloaded->field);
}
}
/**
* @ORM\Entity
*/
class GH10869Entity
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*
* @var ?int
*/
public $id;
/**
* @ORM\Column(type="text", nullable=true)
*
* @var ?string
*/
public $field;
}

View File

@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
use function reset;
class GH10880Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
GH10880BaseProcess::class,
GH10880Process::class,
GH10880ProcessOwner::class,
]);
}
public function testProcessShouldBeUpdated(): void
{
$process = new GH10880Process();
$process->description = 'first value';
$owner = new GH10880ProcessOwner();
$owner->process = $process;
$this->_em->persist($process);
$this->_em->persist($owner);
$this->_em->flush();
$this->_em->clear();
$ownerLoaded = $this->_em->getRepository(GH10880ProcessOwner::class)->find($owner->id);
$ownerLoaded->process->description = 'other description';
$queryLog = $this->getQueryLog();
$queryLog->reset()->enable();
$this->_em->flush();
$this->removeTransactionCommandsFromQueryLog();
self::assertCount(1, $queryLog->queries);
$query = reset($queryLog->queries);
self::assertSame('UPDATE GH10880BaseProcess SET description = ? WHERE id = ?', $query['sql']);
}
private function removeTransactionCommandsFromQueryLog(): void
{
$log = $this->getQueryLog();
foreach ($log->queries as $key => $entry) {
if ($entry['sql'] === '"START TRANSACTION"' || $entry['sql'] === '"COMMIT"') {
unset($log->queries[$key]);
}
}
}
}
/**
* @ORM\Entity
*/
class GH10880ProcessOwner
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
/**
* fetch=EAGER is important to reach the part of \Doctrine\ORM\UnitOfWork::createEntity()
* that is important for this regression test
*
* @ORM\ManyToOne(targetEntity="GH10880Process", fetch="EAGER")
*
* @var GH10880Process
*/
public $process;
}
/**
* @ORM\Entity()
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name="type", type="string")
* @ORM\DiscriminatorMap({"process" = "GH10880Process"})
*/
abstract class GH10880BaseProcess
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
/**
* @ORM\Column(type="text")
*
* @var string
*/
public $description;
}
/**
* @ORM\Entity
*/
class GH10880Process extends GH10880BaseProcess
{
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11017;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class GH11017Entity
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*
* @var ?int
*/
public $id;
/**
* @ORM\Column(type="string", enumType=GH11017Enum::class)
*
* @var GH11017Enum
*/
public $field;
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11017;
enum GH11017Enum: string
{
case FIRST = 'first';
case SECOND = 'second';
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11017;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\Tests\OrmFunctionalTestCase;
use function sprintf;
/**
* @requires PHP 8.1
*/
class GH11017Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
GH11017Entity::class,
]);
}
public function testPostPersistListenerUpdatingObjectFieldWhileOtherInsertPending(): void
{
$entity1 = new GH11017Entity();
$entity1->field = GH11017Enum::FIRST;
$this->_em->persist($entity1);
$this->_em->flush();
$this->_em->clear();
$rsm = new ResultSetMappingBuilder($this->_em, ResultSetMappingBuilder::COLUMN_RENAMING_INCREMENT);
$rsm->addRootEntityFromClassMetadata(GH11017Entity::class, 'e');
$tableName = $this->_em->getClassMetadata(GH11017Entity::class)->getTableName();
$sql = sprintf('SELECT %s FROM %s e WHERE id = :id', $rsm->generateSelectClause(), $tableName);
$query = $this->_em->createNativeQuery($sql, $rsm)
->setParameter('id', $entity1->id);
$entity1Reloaded = $query->getSingleResult(AbstractQuery::HYDRATE_ARRAY);
self::assertNotNull($entity1Reloaded);
self::assertSame($entity1->field, $entity1Reloaded['field']);
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Tests\Models\Issue9300\Issue9300Child;
use Doctrine\Tests\Models\Issue9300\Issue9300Parent;
use Doctrine\Tests\OrmFunctionalTestCase;
/**
* @group GH-9300
*/
class Issue9300Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
$this->useModelSet('issue9300');
parent::setUp();
}
/**
* @group GH-9300
*/
public function testPersistedCollectionIsPresentInOriginalDataAfterFlush(): void
{
$parent = new Issue9300Parent();
$child = new Issue9300Child();
$child->parents->add($parent);
$parent->name = 'abc';
$child->name = 'abc';
$this->_em->persist($parent);
$this->_em->persist($child);
$this->_em->flush();
$parent->name = 'abcd';
$child->name = 'abcd';
$this->_em->flush();
self::assertArrayHasKey('parents', $this->_em->getUnitOfWork()->getOriginalEntityData($child));
}
/**
* @group GH-9300
*/
public function testPersistingCollectionAfterFlushWorksAsExpected(): void
{
$parentOne = new Issue9300Parent();
$parentTwo = new Issue9300Parent();
$childOne = new Issue9300Child();
$parentOne->name = 'abc';
$parentTwo->name = 'abc';
$childOne->name = 'abc';
$childOne->parents = new ArrayCollection([$parentOne]);
$this->_em->persist($parentOne);
$this->_em->persist($parentTwo);
$this->_em->persist($childOne);
$this->_em->flush();
// Recalculate change-set -> new original data
$childOne->name = 'abcd';
$this->_em->flush();
$childOne->parents = new ArrayCollection([$parentTwo]);
$this->_em->flush();
$this->_em->clear();
$childOneFresh = $this->_em->find(Issue9300Child::class, $childOne->id);
self::assertCount(1, $childOneFresh->parents);
self::assertEquals($parentTwo->id, $childOneFresh->parents[0]->id);
}
}

View File

@@ -95,6 +95,8 @@ class ORMSetupTest extends TestCase
/**
* @requires extension apcu
* @requires setting apc.enable_cli 1
* @requires setting apc.enabled 1
*/
public function testCacheNamespaceShouldBeGeneratedForApcu(): void
{

View File

@@ -10,7 +10,9 @@ use Doctrine\Common\Cache\Cache;
use Doctrine\Common\Cache\Psr6\CacheAdapter;
use Doctrine\Common\Cache\Psr6\DoctrineProvider;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Types\Types;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
@@ -36,6 +38,7 @@ use Symfony\Component\Cache\Adapter\ArrayAdapter;
use function array_map;
use function assert;
use function class_exists;
use function method_exists;
use const PHP_VERSION_ID;
@@ -251,6 +254,26 @@ class QueryTest extends OrmTestCase
self::assertEquals($cities, $parameter->getValue());
}
/** @group DDC-1697 */
public function testExplicitCollectionParameters(): void
{
$cities = [
0 => 'Paris',
3 => 'Cannes',
9 => 'St Julien',
];
$query = $this->entityManager
->createQuery('SELECT a FROM Doctrine\Tests\Models\CMS\CmsAddress a WHERE a.city IN (:cities)')
->setParameter('cities', $cities, class_exists(ArrayParameterType::class) ? ArrayParameterType::STRING : Connection::PARAM_STR_ARRAY);
$parameters = $query->getParameters();
$parameter = $parameters->first();
self::assertEquals('cities', $parameter->getName());
self::assertEquals($cities, $parameter->getValue());
}
/** @psalm-return Generator<string, array{iterable}> */
public static function provideProcessParameterValueIterable(): Generator
{

View File

@@ -13,6 +13,7 @@ use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\EntityNotFoundException;
use Doctrine\ORM\Events;
use Doctrine\ORM\Exception\EntityIdentityCollisionException;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
@@ -41,7 +42,6 @@ use Doctrine\Tests\Models\GeoNames\Country;
use Doctrine\Tests\OrmTestCase;
use Doctrine\Tests\PHPUnitCompatibility\MockBuilderCompatibilityTools;
use PHPUnit\Framework\MockObject\MockObject;
use RuntimeException;
use stdClass;
use function assert;
@@ -927,7 +927,7 @@ class UnitOfWorkTest extends OrmTestCase
self::assertEmpty($user->phonenumbers->getSnapshot());
}
public function testItThrowsWhenApplicationProvidedIdsCollide(): void
public function testItTriggersADeprecationNoticeWhenApplicationProvidedIdsCollide(): void
{
// We're using application-provided IDs and assign the same ID twice
// Note this is about colliding IDs in the identity map in memory.
@@ -940,7 +940,27 @@ class UnitOfWorkTest extends OrmTestCase
$phone2 = new CmsPhonenumber();
$phone2->phonenumber = '1234';
$this->expectException(RuntimeException::class);
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/10785');
$this->_unitOfWork->persist($phone2);
}
public function testItThrowsWhenApplicationProvidedIdsCollide(): void
{
$this->_emMock->getConfiguration()->setRejectIdCollisionInIdentityMap(true);
// We're using application-provided IDs and assign the same ID twice
// Note this is about colliding IDs in the identity map in memory.
// Duplicate database-level IDs would be spotted when the EM is flushed.
$phone1 = new CmsPhonenumber();
$phone1->phonenumber = '1234';
$this->_unitOfWork->persist($phone1);
$phone2 = new CmsPhonenumber();
$phone2->phonenumber = '1234';
$this->expectException(EntityIdentityCollisionException::class);
$this->expectExceptionMessageMatches('/another object .* was already present for the same ID/');
$this->_unitOfWork->persist($phone2);

View File

@@ -338,6 +338,10 @@ abstract class OrmFunctionalTestCase extends OrmTestCase
Models\Issue5989\Issue5989Employee::class,
Models\Issue5989\Issue5989Manager::class,
],
'issue9300' => [
Models\Issue9300\Issue9300Child::class,
Models\Issue9300\Issue9300Parent::class,
],
];
/** @param class-string ...$models */