mirror of
https://github.com/doctrine/orm.git
synced 2026-03-24 06:52:09 +01:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
609647a51a | ||
|
|
293299a314 | ||
|
|
0b7fe1862e | ||
|
|
866283d1a7 | ||
|
|
3676e3c571 | ||
|
|
d84f607487 | ||
|
|
42af7cabb7 | ||
|
|
32192c7b01 | ||
|
|
77843e45f3 | ||
|
|
1919eea0a9 | ||
|
|
62ed63bbbe | ||
|
|
081ec2ad26 | ||
|
|
e9537f4cde | ||
|
|
38ad3925e2 | ||
|
|
858b01f85e | ||
|
|
9f555ea8fb | ||
|
|
1f8c02f345 | ||
|
|
d81afdb6e3 | ||
|
|
0628204b43 | ||
|
|
816ecc6d6b | ||
|
|
f66263d859 | ||
|
|
8aa5aa2f57 | ||
|
|
96e31a3b30 | ||
|
|
a60a273423 | ||
|
|
17500f56ea | ||
|
|
fc2f724e2d | ||
|
|
7986fc64dd | ||
|
|
2f9e98754b | ||
|
|
bb5524099c | ||
|
|
3a8cafe228 | ||
|
|
8259a16681 | ||
|
|
5577d51c44 | ||
|
|
d1922a3065 | ||
|
|
597a63a86c | ||
|
|
6b220e3c90 | ||
|
|
6de4b68705 | ||
|
|
16c0151831 | ||
|
|
440b244ebc | ||
|
|
a616914887 | ||
|
|
fd0bdc69b0 | ||
|
|
f50803ccb9 | ||
|
|
eeefc6bc0f | ||
|
|
710dde83aa |
@@ -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",
|
||||
|
||||
16
.github/workflows/continuous-integration.yml
vendored
16
.github/workflows/continuous-integration.yml
vendored
@@ -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
|
||||
|
||||
|
||||
9
.github/workflows/documentation.yml
vendored
9
.github/workflows/documentation.yml
vendored
@@ -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 )"
|
||||
|
||||
2
.github/workflows/phpbench.yml
vendored
2
.github/workflows/phpbench.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
|
||||
4
.github/workflows/static-analysis.yml
vendored
4
.github/workflows/static-analysis.yml
vendored
@@ -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"
|
||||
|
||||
26
UPGRADE.md
26
UPGRADE.md
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
----------------------------------
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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::
|
||||
|
||||
|
||||
@@ -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
|
||||
---------------
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,10 @@ class UnderscoreNamingStrategy implements NamingStrategy
|
||||
/** @var int */
|
||||
private $case;
|
||||
|
||||
/** @var string */
|
||||
/**
|
||||
* @var string
|
||||
* @psalm-var non-empty-string
|
||||
*/
|
||||
private $pattern;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -25,7 +25,7 @@ class Join extends Node
|
||||
/** @var Node|null */
|
||||
public $joinAssociationDeclaration = null;
|
||||
|
||||
/** @var ConditionalExpression|null */
|
||||
/** @var ConditionalExpression|Phase2OptimizableConditional|null */
|
||||
public $conditionalExpression = null;
|
||||
|
||||
/**
|
||||
|
||||
17
lib/Doctrine/ORM/Query/AST/Phase2OptimizableConditional.php
Normal file
17
lib/Doctrine/ORM/Query/AST/Phase2OptimizableConditional.php
Normal 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
|
||||
{
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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']) {
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
44
tests/Doctrine/Tests/Models/Issue9300/Issue9300Child.php
Normal file
44
tests/Doctrine/Tests/Models/Issue9300/Issue9300Child.php
Normal 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();
|
||||
}
|
||||
}
|
||||
30
tests/Doctrine/Tests/Models/Issue9300/Issue9300Parent.php
Normal file
30
tests/Doctrine/Tests/Models/Issue9300/Issue9300Parent.php
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
107
tests/Doctrine/Tests/ORM/Functional/Ticket/GH10808Test.php
Normal file
107
tests/Doctrine/Tests/ORM/Functional/Ticket/GH10808Test.php
Normal 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;
|
||||
}
|
||||
76
tests/Doctrine/Tests/ORM/Functional/Ticket/GH10869Test.php
Normal file
76
tests/Doctrine/Tests/ORM/Functional/Ticket/GH10869Test.php
Normal 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;
|
||||
}
|
||||
119
tests/Doctrine/Tests/ORM/Functional/Ticket/GH10880Test.php
Normal file
119
tests/Doctrine/Tests/ORM/Functional/Ticket/GH10880Test.php
Normal 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
|
||||
{
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
80
tests/Doctrine/Tests/ORM/Functional/Ticket/Issue9300Test.php
Normal file
80
tests/Doctrine/Tests/ORM/Functional/Ticket/Issue9300Test.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user