mirror of
https://github.com/doctrine/orm.git
synced 2026-03-24 06:52:09 +01:00
Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69958152e6 | ||
|
|
cf8f5f9f93 | ||
|
|
60c245413d | ||
|
|
7c9b74221f | ||
|
|
c7e5605d11 | ||
|
|
c19afa1529 | ||
|
|
516b593193 | ||
|
|
e2b971d7c5 | ||
|
|
19d9244a88 | ||
|
|
10a5a3ff73 | ||
|
|
f5fb400d0f | ||
|
|
a321331c89 | ||
|
|
522863116a | ||
|
|
5bfb744967 | ||
|
|
8ed6c2234a | ||
|
|
ff612b9678 | ||
|
|
ee0d7197dd | ||
|
|
39d2136f46 | ||
|
|
c223b8f635 | ||
|
|
bea454eefc | ||
|
|
14f2572e4e | ||
|
|
c2c500077b | ||
|
|
6281c2b79f | ||
|
|
bac1c17eab | ||
|
|
b6137c8911 | ||
|
|
da7854f586 | ||
|
|
5f4ecfd1d8 | ||
|
|
51be1b1d52 | ||
|
|
5f39343bfd | ||
|
|
7ef1f0a379 | ||
|
|
488a5dd3bf | ||
|
|
30795559dc | ||
|
|
f71725575c | ||
|
|
4a3c7f05bf | ||
|
|
896c65504d | ||
|
|
16a8f10fd2 | ||
|
|
498de4c564 | ||
|
|
d80a831157 | ||
|
|
52660297ab | ||
|
|
b44774285b | ||
|
|
58287bb731 | ||
|
|
bc37f75b41 | ||
|
|
8a25b264f7 | ||
|
|
0e48b19cd3 | ||
|
|
d2978303f0 | ||
|
|
109042e5af | ||
|
|
08328adc6c | ||
|
|
191a5366b1 | ||
|
|
65806884b0 | ||
|
|
ad80e8281a | ||
|
|
44dddb2eee | ||
|
|
0c0c61c51b | ||
|
|
cc28fed9f5 | ||
|
|
2245149588 | ||
|
|
b13564c6c0 | ||
|
|
91709c1275 | ||
|
|
434b7cee2a | ||
|
|
7f0a181e39 | ||
|
|
d18126aac5 | ||
|
|
b7fd8241cf | ||
|
|
2432939e4f | ||
|
|
93ce84fa6e | ||
|
|
1bf4603422 | ||
|
|
e6961bd968 | ||
|
|
25d5bc5b46 | ||
|
|
5724e6279e | ||
|
|
cfc0655a1c | ||
|
|
6cde337777 | ||
|
|
c6b2d89748 | ||
|
|
e1dc94d1c2 | ||
|
|
74ef28295a | ||
|
|
6f93cebe6e | ||
|
|
8c582a49d3 | ||
|
|
5f1fe1587c | ||
|
|
56cd688c4a | ||
|
|
96546caceb | ||
|
|
57247ed6ca | ||
|
|
12817076c3 | ||
|
|
4c2f104d42 | ||
|
|
ef64cf7c33 | ||
|
|
0983d3a4af | ||
|
|
efe62e3f0b | ||
|
|
7d01f19667 | ||
|
|
a139a1b63c | ||
|
|
d0e9177121 | ||
|
|
f666aa641e | ||
|
|
ed53defaa1 | ||
|
|
83851a9716 | ||
|
|
066ec1ac81 | ||
|
|
68744489f0 | ||
|
|
bf3e082c00 | ||
|
|
eb49f66926 | ||
|
|
8b6a58fa0e | ||
|
|
b725908c83 | ||
|
|
be307edba8 | ||
|
|
083f642cfa | ||
|
|
716da7e538 | ||
|
|
bcdc5bdaf4 | ||
|
|
80278c545e | ||
|
|
90962f060a | ||
|
|
758f0d7605 | ||
|
|
eb8510ff5c | ||
|
|
d5fdd676f4 | ||
|
|
a3e3a3bbf3 |
@@ -35,17 +35,23 @@
|
||||
"slug": "3.0",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.21",
|
||||
"branchName": "2.21.x",
|
||||
"slug": "2.21",
|
||||
"upcoming": true
|
||||
},
|
||||
{
|
||||
"name": "2.20",
|
||||
"branchName": "2.20.x",
|
||||
"slug": "2.20",
|
||||
"upcoming": true
|
||||
"maintained": true
|
||||
},
|
||||
{
|
||||
"name": "2.19",
|
||||
"branchName": "2.19.x",
|
||||
"slug": "2.19",
|
||||
"maintained": true
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.18",
|
||||
|
||||
2
.github/workflows/coding-standards.yml
vendored
2
.github/workflows/coding-standards.yml
vendored
@@ -24,4 +24,4 @@ on:
|
||||
|
||||
jobs:
|
||||
coding-standards:
|
||||
uses: "doctrine/.github/.github/workflows/coding-standards.yml@5.0.1"
|
||||
uses: "doctrine/.github/.github/workflows/coding-standards.yml@5.1.0"
|
||||
|
||||
4
.github/workflows/continuous-integration.yml
vendored
4
.github/workflows/continuous-integration.yml
vendored
@@ -36,6 +36,7 @@ jobs:
|
||||
- "8.1"
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
- "8.4"
|
||||
dbal-version:
|
||||
- "default"
|
||||
- "3.7"
|
||||
@@ -107,6 +108,7 @@ jobs:
|
||||
php-version:
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
- "8.4"
|
||||
dbal-version:
|
||||
- "default"
|
||||
- "3.7"
|
||||
@@ -180,6 +182,7 @@ jobs:
|
||||
php-version:
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
- "8.4"
|
||||
dbal-version:
|
||||
- "default"
|
||||
- "3.7"
|
||||
@@ -246,6 +249,7 @@ jobs:
|
||||
php-version:
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
- "8.4"
|
||||
dbal-version:
|
||||
- "default"
|
||||
- "3.7"
|
||||
|
||||
43
.github/workflows/documentation.yml
vendored
43
.github/workflows/documentation.yml
vendored
@@ -5,45 +5,16 @@ on:
|
||||
branches:
|
||||
- "*.x"
|
||||
paths:
|
||||
- .github/workflows/documentation.yml
|
||||
- docs/**
|
||||
- ".github/workflows/documentation.yml"
|
||||
- "docs/**"
|
||||
push:
|
||||
branches:
|
||||
- "*.x"
|
||||
paths:
|
||||
- .github/workflows/documentation.yml
|
||||
- docs/**
|
||||
- ".github/workflows/documentation.yml"
|
||||
- "docs/**"
|
||||
|
||||
jobs:
|
||||
validate-with-guides:
|
||||
name: "Validate documentation with phpDocumentor/guides"
|
||||
runs-on: "ubuntu-22.04"
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: "actions/checkout@v4"
|
||||
|
||||
- name: "Install PHP"
|
||||
uses: "shivammathur/setup-php@v2"
|
||||
with:
|
||||
coverage: "none"
|
||||
php-version: "8.3"
|
||||
|
||||
- name: "Remove existing composer file"
|
||||
run: "rm composer.json"
|
||||
|
||||
- name: "Require phpdocumentor/guides-cli"
|
||||
run: "composer require --dev phpdocumentor/guides-cli --no-update"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
with:
|
||||
dependency-versions: "highest"
|
||||
|
||||
- name: "Add orphan metadata where needed"
|
||||
run: |
|
||||
printf '%s\n\n%s\n' ":orphan:" "$(cat docs/en/sidebar.rst)" > docs/en/sidebar.rst
|
||||
printf '%s\n\n%s\n' ":orphan:" "$(cat docs/en/reference/installation.rst)" > docs/en/reference/installation.rst
|
||||
|
||||
- name: "Run guides-cli"
|
||||
run: "vendor/bin/guides -vvv --no-progress docs/en 2>&1 | grep -v 'No template found for rendering directive' | ( ! grep WARNING )"
|
||||
documentation:
|
||||
name: "Documentation"
|
||||
uses: "doctrine/.github/.github/workflows/documentation.yml@5.1.0"
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@5.0.1"
|
||||
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@5.1.0"
|
||||
secrets:
|
||||
GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
|
||||
GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}
|
||||
|
||||
50
UPGRADE.md
50
UPGRADE.md
@@ -1,3 +1,9 @@
|
||||
# Upgrade to 3.3
|
||||
|
||||
## Deprecate `DatabaseDriver`
|
||||
|
||||
The class `Doctrine\ORM\Mapping\Driver\DatabaseDriver` is deprecated without replacement.
|
||||
|
||||
# Upgrade to 3.2
|
||||
|
||||
## Deprecate the `NotSupported` exception
|
||||
@@ -95,9 +101,11 @@ now they throw an exception.
|
||||
|
||||
## BC BREAK: Partial objects are removed
|
||||
|
||||
- The `PARTIAL` keyword in DQL no longer exists.
|
||||
- `Doctrine\ORM\Query\AST\PartialObjectExpression`is removed.
|
||||
- `Doctrine\ORM\Query\SqlWalker::HINT_PARTIAL` and
|
||||
WARNING: This was relaxed in ORM 3.2 when partial was re-allowed for array-hydration.
|
||||
|
||||
- The `PARTIAL` keyword in DQL no longer exists (reintroduced in ORM 3.2)
|
||||
- `Doctrine\ORM\Query\AST\PartialObjectExpression` is removed. (reintroduced in ORM 3.2)
|
||||
- `Doctrine\ORM\Query\SqlWalker::HINT_PARTIAL` (reintroduced in ORM 3.2) and
|
||||
`Doctrine\ORM\Query::HINT_FORCE_PARTIAL_LOAD` are removed.
|
||||
- `Doctrine\ORM\EntityManager*::getPartialReference()` is removed.
|
||||
|
||||
@@ -723,6 +731,42 @@ following classes and methods:
|
||||
|
||||
Use `toIterable()` instead.
|
||||
|
||||
# Upgrade to 2.20
|
||||
|
||||
## Add `Doctrine\ORM\Query\OutputWalker` interface, deprecate `Doctrine\ORM\Query\SqlWalker::getExecutor()`
|
||||
|
||||
Output walkers should implement the new `\Doctrine\ORM\Query\OutputWalker` interface and create
|
||||
`Doctrine\ORM\Query\Exec\SqlFinalizer` instances instead of `Doctrine\ORM\Query\Exec\AbstractSqlExecutor`s.
|
||||
The output walker must not base its workings on the query `firstResult`/`maxResult` values, so that the
|
||||
`SqlFinalizer` can be kept in the query cache and used regardless of the actual `firstResult`/`maxResult` values.
|
||||
Any operation dependent on `firstResult`/`maxResult` should take place within the `SqlFinalizer::createExecutor()`
|
||||
method. Details can be found at https://github.com/doctrine/orm/pull/11188.
|
||||
|
||||
## Explictly forbid property hooks
|
||||
|
||||
Property hooks are not supported yet by Doctrine ORM. Until support is added,
|
||||
they are explicitly forbidden because the support would result in a breaking
|
||||
change in behavior.
|
||||
|
||||
Progress on this is tracked at https://github.com/doctrine/orm/issues/11624 .
|
||||
|
||||
## PARTIAL DQL syntax is undeprecated
|
||||
|
||||
Use of the PARTIAL keyword is not deprecated anymore in DQL, because we will be
|
||||
able to support PARTIAL objects with PHP 8.4 Lazy Objects and
|
||||
Symfony/VarExporter in a better way. When we decided to remove this feature
|
||||
these two abstractions did not exist yet.
|
||||
|
||||
WARNING: If you want to upgrade to 3.x and still use PARTIAL keyword in DQL
|
||||
with array or object hydrators, then you have to directly migrate to ORM 3.3.x or higher.
|
||||
PARTIAL keyword in DQL is not available in 3.0, 3.1 and 3.2 of ORM.
|
||||
|
||||
## Deprecate `\Doctrine\ORM\Query\Parser::setCustomOutputTreeWalker()`
|
||||
|
||||
Use the `\Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER` query hint to set the output walker
|
||||
class instead of setting it through the `\Doctrine\ORM\Query\Parser::setCustomOutputTreeWalker()` method
|
||||
on the parser instance.
|
||||
|
||||
# Upgrade to 2.19
|
||||
|
||||
## Deprecate calling `ClassMetadata::getAssociationMappedByTargetField()` with the owning side of an association
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"composer/package-versions-deprecated": true,
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true,
|
||||
"phpstan/extension-installer": true
|
||||
},
|
||||
"sort-packages": true
|
||||
},
|
||||
@@ -38,7 +39,10 @@
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^12.0",
|
||||
"phpbench/phpbench": "^1.0",
|
||||
"phpstan/phpstan": "1.11.1",
|
||||
"phpdocumentor/guides-cli": "^1.4",
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpstan/phpstan": "1.12.6",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.2",
|
||||
"phpunit/phpunit": "^10.4.0",
|
||||
"psr/log": "^1 || ^2 || ^3",
|
||||
"squizlabs/php_codesniffer": "3.7.2",
|
||||
|
||||
Submodule docs/en/_theme deleted from 6f1bc8bead
@@ -73,6 +73,8 @@ Advanced Topics
|
||||
* :doc:`TypedFieldMapper <reference/typedfieldmapper>`
|
||||
* :doc:`Improving Performance <reference/improving-performance>`
|
||||
* :doc:`Caching <reference/caching>`
|
||||
* :doc:`Partial Hydration <reference/partial-hydration>`
|
||||
* :doc:`Partial Objects <reference/partial-objects>`
|
||||
* :doc:`Change Tracking Policies <reference/change-tracking-policies>`
|
||||
* :doc:`Best Practices <reference/best-practices>`
|
||||
* :doc:`Metadata Drivers <reference/metadata-drivers>`
|
||||
|
||||
@@ -15,7 +15,7 @@ Index
|
||||
- :ref:`#[AttributeOverride] <attrref_attributeoverride>`
|
||||
- :ref:`#[Column] <attrref_column>`
|
||||
- :ref:`#[Cache] <attrref_cache>`
|
||||
- :ref:`#[ChangeTrackingPolicy <attrref_changetrackingpolicy>`
|
||||
- :ref:`#[ChangeTrackingPolicy] <attrref_changetrackingpolicy>`
|
||||
- :ref:`#[CustomIdGenerator] <attrref_customidgenerator>`
|
||||
- :ref:`#[DiscriminatorColumn] <attrref_discriminatorcolumn>`
|
||||
- :ref:`#[DiscriminatorMap] <attrref_discriminatormap>`
|
||||
|
||||
@@ -523,6 +523,34 @@ when the DQL is switched to an arbitrary join.
|
||||
- HAVING is applied to the results of a query after
|
||||
aggregation (GROUP BY)
|
||||
|
||||
|
||||
Partial Hydration Syntax
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
By default when you run a DQL query in Doctrine and select only a
|
||||
subset of the fields for a given entity, you do not receive objects
|
||||
back. Instead, you receive only arrays as a flat rectangular result
|
||||
set, similar to how you would if you were just using SQL directly
|
||||
and joining some data.
|
||||
|
||||
If you want to select partial objects or fields in array hydration you can use the ``partial``
|
||||
DQL keyword:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT partial u.{id, username} FROM CmsUser u');
|
||||
$users = $query->getResult(); // array of partially loaded CmsUser objects
|
||||
|
||||
You can use the partial syntax when joining as well:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT partial u.{id, username}, partial a.{id, name} FROM CmsUser u JOIN u.articles a');
|
||||
$usersArray = $query->getArrayResult(); // array of partially loaded CmsUser and CmsArticle fields
|
||||
$users = $query->getResult(); // array of partially loaded CmsUser objects
|
||||
|
||||
"NEW" Operator Syntax
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@@ -560,7 +588,91 @@ And then use the ``NEW`` DQL keyword :
|
||||
$query = $em->createQuery('SELECT NEW CustomerDTO(c.name, e.email, a.city, SUM(o.value)) FROM Customer c JOIN c.email e JOIN c.address a JOIN c.orders o GROUP BY c');
|
||||
$users = $query->getResult(); // array of CustomerDTO
|
||||
|
||||
Note that you can only pass scalar expressions to the constructor.
|
||||
You can also nest several DTO :
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
class CustomerDTO
|
||||
{
|
||||
public function __construct(string $name, string $email, AddressDTO $address, string|null $value = null)
|
||||
{
|
||||
// Bind values to the object properties.
|
||||
}
|
||||
}
|
||||
|
||||
class AddressDTO
|
||||
{
|
||||
public function __construct(string $street, string $city, string $zip)
|
||||
{
|
||||
// Bind values to the object properties.
|
||||
}
|
||||
}
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT NEW CustomerDTO(c.name, e.email, NEW AddressDTO(a.street, a.city, a.zip)) FROM Customer c JOIN c.email e JOIN c.address a');
|
||||
$users = $query->getResult(); // array of CustomerDTO
|
||||
|
||||
Note that you can only pass scalar expressions or other Data Transfer Objects to the constructor.
|
||||
|
||||
If you use your data transfer objects for multiple queries, and you would rather not have to
|
||||
specify arguments that precede the ones you are really interested in, you can use named arguments.
|
||||
|
||||
Consider the following DTO, which uses optional arguments:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
class CustomerDTO
|
||||
{
|
||||
public function __construct(
|
||||
public string|null $name = null,
|
||||
public string|null $email = null,
|
||||
public string|null $city = null,
|
||||
public mixed|null $value = null,
|
||||
public AddressDTO|null $address = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
You can specify arbitrary arguments in an arbitrary order by using the named argument syntax, and the ORM will try to match argument names with the selected column names.
|
||||
The syntax relies on the NAMED keyword, like so:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT NEW NAMED CustomerDTO(a.city, c.name) FROM Customer c JOIN c.address a');
|
||||
$users = $query->getResult(); // array of CustomerDTO
|
||||
|
||||
// CustomerDTO => {name : 'SMITH', email: null, city: 'London', value: null}
|
||||
|
||||
ORM will also give precedence to column aliases over column names :
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT NEW NAMED CustomerDTO(c.name, CONCAT(a.city, ' ' , a.zip) AS value) FROM Customer c JOIN c.address a');
|
||||
$users = $query->getResult(); // array of CustomerDTO
|
||||
|
||||
// CustomerDTO => {name : 'DOE', email: null, city: null, value: 'New York 10011'}
|
||||
|
||||
To define a custom name for a DTO constructor argument, you can either alias the column with the ``AS`` keyword.
|
||||
|
||||
The ``NAMED`` keyword must precede all DTO you want to instantiate :
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT NEW NAMED CustomerDTO(c.name, NEW NAMED AddressDTO(a.street, a.city, a.zip) AS address) FROM Customer c JOIN c.address a');
|
||||
$users = $query->getResult(); // array of CustomerDTO
|
||||
|
||||
// CustomerDTO => {name : 'DOE', email: null, city: null, value: 'New York 10011'}
|
||||
|
||||
If two arguments have the same name, a ``DuplicateFieldException`` is thrown.
|
||||
If a field cannot be matched with a property name, a ``NoMatchingPropertyException`` is thrown. This typically happens when using functions without aliasing them.
|
||||
|
||||
Using INDEX BY
|
||||
~~~~~~~~~~~~~~
|
||||
@@ -1324,6 +1436,15 @@ exist mostly internal query hints that are not be consumed in
|
||||
userland. However the following few hints are to be used in
|
||||
userland:
|
||||
|
||||
|
||||
- ``Query::HINT_FORCE_PARTIAL_LOAD`` - Allows to hydrate objects
|
||||
although not all their columns are fetched. This query hint can be
|
||||
used to handle memory consumption problems with large result-sets
|
||||
that contain char or binary data. Doctrine has no way of implicitly
|
||||
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>`_)
|
||||
- ``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
|
||||
@@ -1576,10 +1697,12 @@ Select Expressions
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
SelectExpression ::= (IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | "(" Subselect ")" | CaseExpression | NewObjectExpression) [["AS"] ["HIDDEN"] AliasResultVariable]
|
||||
SelectExpression ::= (IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression) [["AS"] ["HIDDEN"] AliasResultVariable]
|
||||
SimpleSelectExpression ::= (StateFieldPathExpression | IdentificationVariable | FunctionDeclaration | AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] AliasResultVariable]
|
||||
PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
|
||||
PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
|
||||
NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
|
||||
NewObjectArg ::= ScalarExpression | "(" Subselect ")"
|
||||
NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]
|
||||
|
||||
Conditional Expressions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
:orphan:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
|
||||
@@ -70,8 +70,8 @@ implements the ``MappingDriver`` interface:
|
||||
/**
|
||||
* Loads the metadata for the specified class into the provided container.
|
||||
*
|
||||
* @psalm-param class-string<T> $className
|
||||
* @psalm-param ClassMetadata<T> $metadata
|
||||
* @param class-string<T> $className
|
||||
* @param ClassMetadata<T> $metadata
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
@@ -82,8 +82,7 @@ implements the ``MappingDriver`` interface:
|
||||
/**
|
||||
* Gets the names of all mapped classes known to this driver.
|
||||
*
|
||||
* @return array<int, string> The names of all mapped classes known to this driver.
|
||||
* @psalm-return list<class-string>
|
||||
* @return list<class-string> The names of all mapped classes known to this driver.
|
||||
*/
|
||||
public function getAllClassNames();
|
||||
|
||||
@@ -91,7 +90,7 @@ implements the ``MappingDriver`` interface:
|
||||
* Returns whether the class with the specified name should have its metadata loaded.
|
||||
* This is only the case if it is either mapped as an Entity or a MappedSuperclass.
|
||||
*
|
||||
* @psalm-param class-string $className
|
||||
* @param class-string $className
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
15
docs/en/reference/partial-hydration.rst
Normal file
15
docs/en/reference/partial-hydration.rst
Normal file
@@ -0,0 +1,15 @@
|
||||
Partial Hydration
|
||||
=================
|
||||
|
||||
Partial hydration of entities is allowed in the array hydrator, when
|
||||
only a subset of the fields of an entity are loaded from the database
|
||||
and the nested results are still created based on the entity relationship structure.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$users = $em->createQuery("SELECT PARTIAL u.{id,name}, partial a.{id,street} FROM MyApp\Domain\User u JOIN u.addresses a")
|
||||
->getArrayResult();
|
||||
|
||||
This is a useful optimization when you are not interested in all fields of an entity
|
||||
for performance reasons, for example in use-cases for exporting or rendering lots of data.
|
||||
88
docs/en/reference/partial-objects.rst
Normal file
88
docs/en/reference/partial-objects.rst
Normal file
@@ -0,0 +1,88 @@
|
||||
Partial Objects
|
||||
===============
|
||||
|
||||
A partial object is an object whose state is not fully initialized
|
||||
after being reconstituted from the database and that is
|
||||
disconnected from the rest of its data. The following section will
|
||||
describe why partial objects are problematic and what the approach
|
||||
of Doctrine to this problem is.
|
||||
|
||||
.. note::
|
||||
|
||||
The partial object problem in general does not apply to
|
||||
methods or queries where you do not retrieve the query result as
|
||||
objects. Examples are: ``Query#getArrayResult()``,
|
||||
``Query#getScalarResult()``, ``Query#getSingleScalarResult()``,
|
||||
etc.
|
||||
|
||||
.. warning::
|
||||
|
||||
Use of partial objects is tricky. Fields that are not retrieved
|
||||
from the database will not be updated by the UnitOfWork even if they
|
||||
get changed in your objects. You can only promote a partial object
|
||||
to a fully-loaded object by calling ``EntityManager#refresh()``
|
||||
or a DQL query with the refresh flag.
|
||||
|
||||
|
||||
What is the problem?
|
||||
--------------------
|
||||
|
||||
In short, partial objects are problematic because they are usually
|
||||
objects with broken invariants. As such, code that uses these
|
||||
partial objects tends to be very fragile and either needs to "know"
|
||||
which fields or methods can be safely accessed or add checks around
|
||||
every field access or method invocation. The same holds true for
|
||||
the internals, i.e. the method implementations, of such objects.
|
||||
You usually simply assume the state you need in the method is
|
||||
available, after all you properly constructed this object before
|
||||
you pushed it into the database, right? These blind assumptions can
|
||||
quickly lead to null reference errors when working with such
|
||||
partial objects.
|
||||
|
||||
It gets worse with the scenario of an optional association (0..1 to
|
||||
1). When the associated field is NULL, you don't know whether this
|
||||
object does not have an associated object or whether it was simply
|
||||
not loaded when the owning object was loaded from the database.
|
||||
|
||||
These are reasons why many ORMs do not allow partial objects at all
|
||||
and instead you always have to load an object with all its fields
|
||||
(associations being proxied). One secure way to allow partial
|
||||
objects is if the programming language/platform allows the ORM tool
|
||||
to hook deeply into the object and instrument it in such a way that
|
||||
individual fields (not only associations) can be loaded lazily on
|
||||
first access. This is possible in Java, for example, through
|
||||
bytecode instrumentation. In PHP though this is not possible, so
|
||||
there is no way to have "secure" partial objects in an ORM with
|
||||
transparent persistence.
|
||||
|
||||
Doctrine, by default, does not allow partial objects. That means,
|
||||
any query that only selects partial object data and wants to
|
||||
retrieve the result as objects (i.e. ``Query#getResult()``) will
|
||||
raise an exception telling you that partial objects are dangerous.
|
||||
If you want to force a query to return you partial objects,
|
||||
possibly as a performance tweak, you can use the ``partial``
|
||||
keyword as follows:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$q = $em->createQuery("select partial u.{id,name} from MyApp\Domain\User u");
|
||||
|
||||
You can also get a partial reference instead of a proxy reference by
|
||||
calling:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$reference = $em->getPartialReference('MyApp\Domain\User', 1);
|
||||
|
||||
Partial references are objects with only the identifiers set as they
|
||||
are passed to the second argument of the ``getPartialReference()`` method.
|
||||
All other fields are null.
|
||||
|
||||
When should I force partial objects?
|
||||
------------------------------------
|
||||
|
||||
Mainly for optimization purposes, but be careful of premature
|
||||
optimization as partial objects lead to potentially more fragile
|
||||
code.
|
||||
@@ -611,3 +611,21 @@ same query of example 6 written using
|
||||
->add('from', new Expr\From('User', 'u'))
|
||||
->add('where', new Expr\Comparison('u.id', '=', '?1'))
|
||||
->add('orderBy', new Expr\OrderBy('u.name', 'ASC'));
|
||||
|
||||
Binding Parameters to Placeholders
|
||||
----------------------------------
|
||||
|
||||
It is often not necessary to know about the exact placeholder names when
|
||||
building a query. You can use a helper method to bind a value to a placeholder
|
||||
and directly use that placeholder in your query as a return value:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $qb instanceof QueryBuilder
|
||||
|
||||
$qb->select('u')
|
||||
->from('User', 'u')
|
||||
->where('u.email = ' . $qb->createNamedParameter($userInputEmail))
|
||||
;
|
||||
// SELECT u FROM User u WHERE email = :dcValue1
|
||||
|
||||
@@ -1,80 +1,75 @@
|
||||
.. toc::
|
||||
:orphan:
|
||||
|
||||
.. tocheader:: Tutorials
|
||||
.. toctree::
|
||||
:caption: Tutorials
|
||||
:depth: 3
|
||||
|
||||
.. toctree::
|
||||
:depth: 3
|
||||
tutorials/getting-started
|
||||
tutorials/getting-started-database
|
||||
tutorials/getting-started-models
|
||||
tutorials/working-with-indexed-associations
|
||||
tutorials/extra-lazy-associations
|
||||
tutorials/composite-primary-keys
|
||||
tutorials/ordered-associations
|
||||
tutorials/override-field-association-mappings-in-subclasses
|
||||
tutorials/pagination
|
||||
tutorials/embeddables
|
||||
|
||||
tutorials/getting-started
|
||||
tutorials/getting-started-database
|
||||
tutorials/getting-started-models
|
||||
tutorials/working-with-indexed-associations
|
||||
tutorials/extra-lazy-associations
|
||||
tutorials/composite-primary-keys
|
||||
tutorials/ordered-associations
|
||||
tutorials/override-field-association-mappings-in-subclasses
|
||||
tutorials/pagination
|
||||
tutorials/embeddables
|
||||
.. toctree::
|
||||
:caption: Reference
|
||||
:depth: 3
|
||||
|
||||
.. toc::
|
||||
reference/architecture
|
||||
reference/configuration
|
||||
reference/faq
|
||||
reference/basic-mapping
|
||||
reference/association-mapping
|
||||
reference/inheritance-mapping
|
||||
reference/working-with-objects
|
||||
reference/working-with-associations
|
||||
reference/typedfieldmapper
|
||||
reference/events
|
||||
reference/unitofwork
|
||||
reference/unitofwork-associations
|
||||
reference/transactions-and-concurrency
|
||||
reference/batch-processing
|
||||
reference/dql-doctrine-query-language
|
||||
reference/query-builder
|
||||
reference/native-sql
|
||||
reference/change-tracking-policies
|
||||
reference/partial-hydration
|
||||
reference/partial-objects
|
||||
reference/attributes-reference
|
||||
reference/xml-mapping
|
||||
reference/php-mapping
|
||||
reference/caching
|
||||
reference/improving-performance
|
||||
reference/tools
|
||||
reference/metadata-drivers
|
||||
reference/best-practices
|
||||
reference/limitations-and-known-issues
|
||||
tutorials/pagination
|
||||
reference/filters
|
||||
reference/namingstrategy
|
||||
reference/advanced-configuration
|
||||
reference/second-level-cache
|
||||
reference/security
|
||||
|
||||
.. tocheader:: Reference
|
||||
.. toctree::
|
||||
:caption: Cookbook
|
||||
:depth: 3
|
||||
|
||||
.. toctree::
|
||||
:depth: 3
|
||||
|
||||
reference/architecture
|
||||
reference/configuration
|
||||
reference/faq
|
||||
reference/basic-mapping
|
||||
reference/association-mapping
|
||||
reference/inheritance-mapping
|
||||
reference/working-with-objects
|
||||
reference/working-with-associations
|
||||
reference/typedfieldmapper
|
||||
reference/events
|
||||
reference/unitofwork
|
||||
reference/unitofwork-associations
|
||||
reference/transactions-and-concurrency
|
||||
reference/batch-processing
|
||||
reference/dql-doctrine-query-language
|
||||
reference/query-builder
|
||||
reference/native-sql
|
||||
reference/change-tracking-policies
|
||||
reference/attributes-reference
|
||||
reference/xml-mapping
|
||||
reference/php-mapping
|
||||
reference/caching
|
||||
reference/improving-performance
|
||||
reference/tools
|
||||
reference/metadata-drivers
|
||||
reference/best-practices
|
||||
reference/limitations-and-known-issues
|
||||
tutorials/pagination
|
||||
reference/filters
|
||||
reference/namingstrategy
|
||||
reference/advanced-configuration
|
||||
reference/second-level-cache
|
||||
reference/security
|
||||
|
||||
.. toc::
|
||||
|
||||
.. tocheader:: Cookbook
|
||||
|
||||
.. toctree::
|
||||
:depth: 3
|
||||
|
||||
cookbook/aggregate-fields
|
||||
cookbook/custom-mapping-types
|
||||
cookbook/decorator-pattern
|
||||
cookbook/dql-custom-walkers
|
||||
cookbook/dql-user-defined-functions
|
||||
cookbook/implementing-arrayaccess-for-domain-objects
|
||||
cookbook/resolve-target-entity-listener
|
||||
cookbook/sql-table-prefixes
|
||||
cookbook/strategy-cookbook-introduction
|
||||
cookbook/validation-of-entities
|
||||
cookbook/working-with-datetime
|
||||
cookbook/mysql-enums
|
||||
cookbook/advanced-field-value-conversion-using-custom-mapping-types
|
||||
cookbook/entities-in-session
|
||||
cookbook/aggregate-fields
|
||||
cookbook/custom-mapping-types
|
||||
cookbook/decorator-pattern
|
||||
cookbook/dql-custom-walkers
|
||||
cookbook/dql-user-defined-functions
|
||||
cookbook/implementing-arrayaccess-for-domain-objects
|
||||
cookbook/resolve-target-entity-listener
|
||||
cookbook/sql-table-prefixes
|
||||
cookbook/strategy-cookbook-introduction
|
||||
cookbook/validation-of-entities
|
||||
cookbook/working-with-datetime
|
||||
cookbook/mysql-enums
|
||||
cookbook/advanced-field-value-conversion-using-custom-mapping-types
|
||||
cookbook/entities-in-session
|
||||
|
||||
@@ -27,3 +27,10 @@ parameters:
|
||||
message: '#Negated boolean expression is always false\.#'
|
||||
paths:
|
||||
- src/Mapping/Driver/AttributeDriver.php
|
||||
|
||||
-
|
||||
message: '~^Call to deprecated method getEventManager\(\) of class Doctrine\\DBAL\\Connection\.$~'
|
||||
path: src/EntityManager.php
|
||||
-
|
||||
message: '~deprecated class Doctrine\\DBAL\\Tools\\Console\\Command\\ReservedWordsCommand\:~'
|
||||
path: src/Tools/Console/ConsoleRunner.php
|
||||
|
||||
@@ -212,6 +212,10 @@
|
||||
</ParamNameMismatch>
|
||||
</file>
|
||||
<file src="src/Internal/Hydration/AbstractHydrator.php">
|
||||
<PossiblyUndefinedArrayOffset>
|
||||
<code><![CDATA[$newObject['args']]]></code>
|
||||
<code><![CDATA[$newObject['args']]]></code>
|
||||
</PossiblyUndefinedArrayOffset>
|
||||
<ReferenceConstraintViolation>
|
||||
<code><![CDATA[return $rowData;]]></code>
|
||||
<code><![CDATA[return $rowData;]]></code>
|
||||
@@ -228,9 +232,6 @@
|
||||
<code><![CDATA[$result[$resultKey]]]></code>
|
||||
<code><![CDATA[$result[$resultKey]]]></code>
|
||||
</PossiblyNullArrayAssignment>
|
||||
<PossiblyUndefinedArrayOffset>
|
||||
<code><![CDATA[$newObject['args']]]></code>
|
||||
</PossiblyUndefinedArrayOffset>
|
||||
<ReferenceConstraintViolation>
|
||||
<code><![CDATA[$result]]></code>
|
||||
</ReferenceConstraintViolation>
|
||||
@@ -265,9 +266,6 @@
|
||||
<code><![CDATA[setValue]]></code>
|
||||
<code><![CDATA[setValue]]></code>
|
||||
</PossiblyNullReference>
|
||||
<PossiblyUndefinedArrayOffset>
|
||||
<code><![CDATA[$newObject['args']]]></code>
|
||||
</PossiblyUndefinedArrayOffset>
|
||||
</file>
|
||||
<file src="src/Mapping/AssociationMapping.php">
|
||||
<LessSpecificReturnStatement>
|
||||
@@ -404,6 +402,7 @@
|
||||
<file src="src/Mapping/DefaultTypedFieldMapper.php">
|
||||
<LessSpecificReturnStatement>
|
||||
<code><![CDATA[$mapping]]></code>
|
||||
<code><![CDATA[$mapping]]></code>
|
||||
</LessSpecificReturnStatement>
|
||||
<MoreSpecificReturnType>
|
||||
<code><![CDATA[array]]></code>
|
||||
@@ -925,6 +924,9 @@
|
||||
<ArgumentTypeCoercion>
|
||||
<code><![CDATA[$stringPattern]]></code>
|
||||
</ArgumentTypeCoercion>
|
||||
<DeprecatedMethod>
|
||||
<code><![CDATA[setSqlExecutor]]></code>
|
||||
</DeprecatedMethod>
|
||||
<InvalidNullableReturnType>
|
||||
<code><![CDATA[AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement]]></code>
|
||||
</InvalidNullableReturnType>
|
||||
@@ -1115,6 +1117,12 @@
|
||||
</RedundantConditionGivenDocblockType>
|
||||
</file>
|
||||
<file src="src/Tools/Pagination/LimitSubqueryOutputWalker.php">
|
||||
<InvalidReturnStatement>
|
||||
<code><![CDATA[$abstractSqlExecutor->getSqlStatements()]]></code>
|
||||
</InvalidReturnStatement>
|
||||
<InvalidReturnType>
|
||||
<code><![CDATA[string]]></code>
|
||||
</InvalidReturnType>
|
||||
<PossiblyFalseArgument>
|
||||
<code><![CDATA[strrpos($orderByItemString, ' ')]]></code>
|
||||
</PossiblyFalseArgument>
|
||||
|
||||
@@ -22,8 +22,8 @@ class CollectionCacheKey extends CacheKey
|
||||
public readonly array $ownerIdentifier;
|
||||
|
||||
/**
|
||||
* @param class-string $entityClass The owner entity class.
|
||||
* @param array<string, mixed> $ownerIdentifier The identifier of the owning entity.
|
||||
* @param class-string $entityClass The owner entity class
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $entityClass,
|
||||
|
||||
@@ -16,6 +16,7 @@ use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\ORM\Query\SqlWalker;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
|
||||
use function array_map;
|
||||
@@ -210,6 +211,10 @@ class DefaultQueryCache implements QueryCache
|
||||
throw FeatureNotImplemented::nonSelectStatements();
|
||||
}
|
||||
|
||||
if (($hints[SqlWalker::HINT_PARTIAL] ?? false) === true || ($hints[Query::HINT_FORCE_PARTIAL_LOAD] ?? false) === true) {
|
||||
throw FeatureNotImplemented::partialEntities();
|
||||
}
|
||||
|
||||
if (! ($key->cacheMode & Cache::MODE_PUT)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ use function array_map;
|
||||
class EntityCacheEntry implements CacheEntry
|
||||
{
|
||||
/**
|
||||
* @param array<string,mixed> $data The entity map data
|
||||
* @psalm-param class-string $class The entity class name
|
||||
* @param class-string $class The entity class name
|
||||
* @param array<string,mixed> $data The entity map data
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $class,
|
||||
|
||||
@@ -20,4 +20,9 @@ class FeatureNotImplemented extends CacheException
|
||||
{
|
||||
return new self('Second-level cache query supports only select statements.');
|
||||
}
|
||||
|
||||
public static function partialEntities(): self
|
||||
{
|
||||
return new self('Second level cache does not support partial entities.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,8 +263,8 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
*
|
||||
* Any previously added numeric functions are discarded.
|
||||
*
|
||||
* @psalm-param array<string, class-string> $functions The map of custom
|
||||
* DQL numeric functions.
|
||||
* @param array<string, class-string> $functions The map of custom
|
||||
* DQL numeric functions.
|
||||
*/
|
||||
public function setCustomNumericFunctions(array $functions): void
|
||||
{
|
||||
@@ -291,7 +291,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
/**
|
||||
* Gets the implementation class name of a registered custom date/time DQL function.
|
||||
*
|
||||
* @psalm-return class-string|callable|null
|
||||
* @return class-string|callable|null
|
||||
*/
|
||||
public function getCustomDatetimeFunction(string $name): string|callable|null
|
||||
{
|
||||
@@ -351,7 +351,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
/**
|
||||
* Gets the hydrator class for the given hydration mode name.
|
||||
*
|
||||
* @psalm-return class-string<AbstractHydrator>|null
|
||||
* @return class-string<AbstractHydrator>|null
|
||||
*/
|
||||
public function getCustomHydrationMode(string $modeName): string|null
|
||||
{
|
||||
@@ -361,7 +361,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
/**
|
||||
* Adds a custom hydration mode.
|
||||
*
|
||||
* @psalm-param class-string<AbstractHydrator> $hydrator
|
||||
* @param class-string<AbstractHydrator> $hydrator
|
||||
*/
|
||||
public function addCustomHydrationMode(string $modeName, string $hydrator): void
|
||||
{
|
||||
@@ -371,14 +371,14 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
/**
|
||||
* Sets a class metadata factory.
|
||||
*
|
||||
* @psalm-param class-string $cmfName
|
||||
* @param class-string $cmfName
|
||||
*/
|
||||
public function setClassMetadataFactoryName(string $cmfName): void
|
||||
{
|
||||
$this->attributes['classMetadataFactoryName'] = $cmfName;
|
||||
}
|
||||
|
||||
/** @psalm-return class-string */
|
||||
/** @return class-string */
|
||||
public function getClassMetadataFactoryName(): string
|
||||
{
|
||||
if (! isset($this->attributes['classMetadataFactoryName'])) {
|
||||
@@ -391,8 +391,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
/**
|
||||
* Adds a filter to the list of possible filters.
|
||||
*
|
||||
* @param string $className The class name of the filter.
|
||||
* @psalm-param class-string<SQLFilter> $className
|
||||
* @param class-string<SQLFilter> $className The class name of the filter.
|
||||
*/
|
||||
public function addFilter(string $name, string $className): void
|
||||
{
|
||||
@@ -402,9 +401,8 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
/**
|
||||
* Gets the class name for a given filter name.
|
||||
*
|
||||
* @return string|null The class name of the filter, or null if it is not
|
||||
* defined.
|
||||
* @psalm-return class-string<SQLFilter>|null
|
||||
* @return class-string<SQLFilter>|null The class name of the filter,
|
||||
* or null if it is not defined.
|
||||
*/
|
||||
public function getFilterClassName(string $name): string|null
|
||||
{
|
||||
@@ -414,7 +412,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
/**
|
||||
* Sets default repository class.
|
||||
*
|
||||
* @psalm-param class-string<EntityRepository> $className
|
||||
* @param class-string<EntityRepository> $className
|
||||
*
|
||||
* @throws InvalidEntityRepository If $classname is not an ObjectRepository.
|
||||
*/
|
||||
@@ -430,7 +428,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
/**
|
||||
* Get default repository class.
|
||||
*
|
||||
* @psalm-return class-string<EntityRepository>
|
||||
* @return class-string<EntityRepository>
|
||||
*/
|
||||
public function getDefaultRepositoryClassName(): string
|
||||
{
|
||||
|
||||
@@ -24,7 +24,6 @@ use Doctrine\ORM\Query\Expr;
|
||||
use Doctrine\ORM\Query\FilterCollection;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\ORM\Repository\RepositoryFactory;
|
||||
use Throwable;
|
||||
|
||||
use function array_keys;
|
||||
use function is_array;
|
||||
@@ -178,18 +177,24 @@ class EntityManager implements EntityManagerInterface
|
||||
{
|
||||
$this->conn->beginTransaction();
|
||||
|
||||
$successful = false;
|
||||
|
||||
try {
|
||||
$return = $func($this);
|
||||
|
||||
$this->flush();
|
||||
$this->conn->commit();
|
||||
|
||||
return $return;
|
||||
} catch (Throwable $e) {
|
||||
$this->close();
|
||||
$this->conn->rollBack();
|
||||
$successful = true;
|
||||
|
||||
throw $e;
|
||||
return $return;
|
||||
} finally {
|
||||
if (! $successful) {
|
||||
$this->close();
|
||||
if ($this->conn->isTransactionActive()) {
|
||||
$this->conn->rollBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,9 +484,9 @@ class EntityManager implements EntityManagerInterface
|
||||
/**
|
||||
* Gets the repository for an entity class.
|
||||
*
|
||||
* @psalm-param class-string<T> $className
|
||||
* @param class-string<T> $className The name of the entity.
|
||||
*
|
||||
* @psalm-return EntityRepository<T>
|
||||
* @return EntityRepository<T> The repository class.
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
|
||||
@@ -22,9 +22,9 @@ interface EntityManagerInterface extends ObjectManager
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-param class-string<T> $className
|
||||
* @param class-string<T> $className
|
||||
*
|
||||
* @psalm-return EntityRepository<T>
|
||||
* @return EntityRepository<T>
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
@@ -151,11 +151,10 @@ interface EntityManagerInterface extends ObjectManager
|
||||
* Gets a reference to the entity identified by the given type and identifier
|
||||
* without actually loading it, if the entity is not yet loaded.
|
||||
*
|
||||
* @param string $entityName The name of the entity type.
|
||||
* @param mixed $id The entity identifier.
|
||||
* @psalm-param class-string<T> $entityName
|
||||
* @param class-string<T> $entityName The name of the entity type.
|
||||
* @param mixed $id The entity identifier.
|
||||
*
|
||||
* @psalm-return T|null
|
||||
* @return T|null The entity reference.
|
||||
*
|
||||
* @throws ORMException
|
||||
*
|
||||
@@ -232,7 +231,7 @@ interface EntityManagerInterface extends ObjectManager
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-param string|class-string<T> $className
|
||||
* @param string|class-string<T> $className
|
||||
*
|
||||
* @psalm-return ($className is class-string<T> ? Mapping\ClassMetadata<T> : Mapping\ClassMetadata<object>)
|
||||
*
|
||||
|
||||
@@ -35,11 +35,11 @@ use function substr;
|
||||
*/
|
||||
class EntityRepository implements ObjectRepository, Selectable
|
||||
{
|
||||
/** @psalm-var class-string<T> */
|
||||
/** @var class-string<T> */
|
||||
private readonly string $entityName;
|
||||
private static Inflector|null $inflector = null;
|
||||
|
||||
/** @psalm-param ClassMetadata<T> $class */
|
||||
/** @param ClassMetadata<T> $class */
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly ClassMetadata $class,
|
||||
@@ -131,6 +131,7 @@ class EntityRepository implements ObjectRepository, Selectable
|
||||
* @psalm-param array<string, mixed> $criteria
|
||||
*
|
||||
* @return int The cardinality of the objects that match the given criteria.
|
||||
* @psalm-return 0|positive-int
|
||||
*
|
||||
* @todo Add this method to `ObjectRepository` interface in the next major release
|
||||
*/
|
||||
@@ -168,7 +169,7 @@ class EntityRepository implements ObjectRepository, Selectable
|
||||
));
|
||||
}
|
||||
|
||||
/** @psalm-return class-string<T> */
|
||||
/** @return class-string<T> */
|
||||
protected function getEntityName(): string
|
||||
{
|
||||
return $this->entityName;
|
||||
|
||||
17
src/Exception/DuplicateFieldException.php
Normal file
17
src/Exception/DuplicateFieldException.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Exception;
|
||||
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class DuplicateFieldException extends LogicException implements ORMException
|
||||
{
|
||||
public static function create(string $argName, string $columnName): self
|
||||
{
|
||||
return new self(sprintf('Name "%s" for "%s" already in use.', $argName, $columnName));
|
||||
}
|
||||
}
|
||||
17
src/Exception/NoMatchingPropertyException.php
Normal file
17
src/Exception/NoMatchingPropertyException.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Exception;
|
||||
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class NoMatchingPropertyException extends LogicException implements ORMException
|
||||
{
|
||||
public static function create(string $property): self
|
||||
{
|
||||
return new self(sprintf('Column name "%s" does not match any property name. Consider aliasing it to the name of an existing property.', $property));
|
||||
}
|
||||
}
|
||||
@@ -252,15 +252,16 @@ abstract class AbstractHydrator
|
||||
* @psalm-return array{
|
||||
* data: array<array-key, array>,
|
||||
* newObjects?: array<array-key, array{
|
||||
* class: mixed,
|
||||
* args?: array
|
||||
* class: ReflectionClass,
|
||||
* args: array,
|
||||
* obj: object
|
||||
* }>,
|
||||
* scalars?: array
|
||||
* }
|
||||
*/
|
||||
protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents): array
|
||||
{
|
||||
$rowData = ['data' => []];
|
||||
$rowData = ['data' => [], 'newObjects' => []];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$cacheKeyInfo = $this->hydrateColumnInfo($key);
|
||||
@@ -335,6 +336,25 @@ abstract class AbstractHydrator
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->resultSetMapping()->nestedNewObjectArguments as $objIndex => ['ownerIndex' => $ownerIndex, 'argIndex' => $argIndex]) {
|
||||
if (! isset($rowData['newObjects'][$ownerIndex . ':' . $argIndex])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$newObject = $rowData['newObjects'][$ownerIndex . ':' . $argIndex];
|
||||
unset($rowData['newObjects'][$ownerIndex . ':' . $argIndex]);
|
||||
|
||||
$obj = $newObject['class']->newInstanceArgs($newObject['args']);
|
||||
|
||||
$rowData['newObjects'][$ownerIndex]['args'][$argIndex] = $obj;
|
||||
}
|
||||
|
||||
foreach ($rowData['newObjects'] as $objIndex => $newObject) {
|
||||
$obj = $newObject['class']->newInstanceArgs($newObject['args']);
|
||||
|
||||
$rowData['newObjects'][$objIndex]['obj'] = $obj;
|
||||
}
|
||||
|
||||
return $rowData;
|
||||
}
|
||||
|
||||
|
||||
@@ -214,9 +214,8 @@ class ArrayHydrator extends AbstractHydrator
|
||||
$scalarCount = (isset($rowData['scalars']) ? count($rowData['scalars']) : 0);
|
||||
|
||||
foreach ($rowData['newObjects'] as $objIndex => $newObject) {
|
||||
$class = $newObject['class'];
|
||||
$args = $newObject['args'];
|
||||
$obj = $class->newInstanceArgs($args);
|
||||
$args = $newObject['args'];
|
||||
$obj = $newObject['obj'];
|
||||
|
||||
if (count($args) === $scalarCount || ($scalarCount === 0 && count($rowData['newObjects']) === 1)) {
|
||||
$result[$resultKey] = $obj;
|
||||
|
||||
@@ -64,4 +64,9 @@ class HydrationException extends Exception implements ORMException
|
||||
implode('", "', $discrValues),
|
||||
));
|
||||
}
|
||||
|
||||
public static function partialObjectHydrationDisallowed(): self
|
||||
{
|
||||
return new self('Hydration of entity objects is not allowed when DQL PARTIAL keyword is used.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ class ObjectHydrator extends AbstractHydrator
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $className
|
||||
* @param class-string $className
|
||||
* @psalm-param array<string, mixed> $data
|
||||
*/
|
||||
private function getEntityFromIdentityMap(string $className, array $data): object|bool
|
||||
@@ -556,9 +556,7 @@ class ObjectHydrator extends AbstractHydrator
|
||||
$scalarCount = (isset($rowData['scalars']) ? count($rowData['scalars']) : 0);
|
||||
|
||||
foreach ($rowData['newObjects'] as $objIndex => $newObject) {
|
||||
$class = $newObject['class'];
|
||||
$args = $newObject['args'];
|
||||
$obj = $class->newInstanceArgs($args);
|
||||
$obj = $newObject['obj'];
|
||||
|
||||
if ($scalarCount === 0 && count($rowData['newObjects']) === 1) {
|
||||
$result[$resultKey] = $obj;
|
||||
|
||||
@@ -169,8 +169,8 @@ class ClassMetadataBuilder
|
||||
/**
|
||||
* Sets the discriminator column details.
|
||||
*
|
||||
* @psalm-param class-string<BackedEnum>|null $enumType
|
||||
* @psalm-param array<string, mixed> $options
|
||||
* @param class-string<BackedEnum>|null $enumType
|
||||
* @param array<string, mixed> $options
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace Doctrine\ORM\Mapping;
|
||||
use BackedEnum;
|
||||
use BadMethodCallException;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\Instantiator\Instantiator;
|
||||
use Doctrine\Instantiator\InstantiatorInterface;
|
||||
@@ -23,6 +24,7 @@ use ReflectionNamedType;
|
||||
use ReflectionProperty;
|
||||
use Stringable;
|
||||
|
||||
use function array_column;
|
||||
use function array_diff;
|
||||
use function array_intersect;
|
||||
use function array_key_exists;
|
||||
@@ -34,6 +36,7 @@ use function array_values;
|
||||
use function assert;
|
||||
use function class_exists;
|
||||
use function count;
|
||||
use function defined;
|
||||
use function enum_exists;
|
||||
use function explode;
|
||||
use function in_array;
|
||||
@@ -1039,6 +1042,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
*/
|
||||
public function getColumnName(string $fieldName): string
|
||||
{
|
||||
// @phpstan-ignore property.deprecated
|
||||
return $this->columnNames[$fieldName] ?? $fieldName;
|
||||
}
|
||||
|
||||
@@ -1118,9 +1122,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
{
|
||||
$field = $this->reflClass->getProperty($mapping['fieldName']);
|
||||
|
||||
$mapping = $this->typedFieldMapper->validateAndComplete($mapping, $field);
|
||||
|
||||
return $mapping;
|
||||
return $this->typedFieldMapper->validateAndComplete($mapping, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1188,6 +1190,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
$mapping->quoted = true;
|
||||
}
|
||||
|
||||
// @phpstan-ignore property.deprecated
|
||||
$this->columnNames[$mapping->fieldName] = $mapping->columnName;
|
||||
|
||||
if (isset($this->fieldNames[$mapping->columnName]) || ($this->discriminatorColumn && $this->discriminatorColumn->name === $mapping->columnName)) {
|
||||
@@ -1230,6 +1233,14 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
if (! empty($mapping->id)) {
|
||||
$this->containsEnumIdentifier = true;
|
||||
}
|
||||
|
||||
if (
|
||||
defined('Doctrine\DBAL\Types\Types::ENUM')
|
||||
&& $mapping->type === Types::ENUM
|
||||
&& ! isset($mapping->options['values'])
|
||||
) {
|
||||
$mapping->options['values'] = array_column($mapping->enumType::cases(), 'value');
|
||||
}
|
||||
}
|
||||
|
||||
return $mapping;
|
||||
@@ -1768,6 +1779,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
|
||||
unset($this->fieldMappings[$fieldName]);
|
||||
unset($this->fieldNames[$mapping->columnName]);
|
||||
// @phpstan-ignore property.deprecated
|
||||
unset($this->columnNames[$mapping->fieldName]);
|
||||
|
||||
$overrideMapping = $this->validateAndCompleteFieldMapping($overrideMapping);
|
||||
@@ -1918,8 +1930,9 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
public function addInheritedFieldMapping(FieldMapping $fieldMapping): void
|
||||
{
|
||||
$this->fieldMappings[$fieldMapping->fieldName] = $fieldMapping;
|
||||
$this->columnNames[$fieldMapping->fieldName] = $fieldMapping->columnName;
|
||||
$this->fieldNames[$fieldMapping->columnName] = $fieldMapping->fieldName;
|
||||
// @phpstan-ignore property.deprecated
|
||||
$this->columnNames[$fieldMapping->fieldName] = $fieldMapping->columnName;
|
||||
$this->fieldNames[$fieldMapping->columnName] = $fieldMapping->fieldName;
|
||||
|
||||
if (isset($fieldMapping->generated)) {
|
||||
$this->requiresFetchAfterChange = true;
|
||||
|
||||
@@ -399,7 +399,7 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
|
||||
/**
|
||||
* Gets the lower-case short name of a class.
|
||||
*
|
||||
* @psalm-param class-string $className
|
||||
* @param class-string $className
|
||||
*/
|
||||
private function getShortName(string $className): string
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ use function trim;
|
||||
*/
|
||||
class DefaultEntityListenerResolver implements EntityListenerResolver
|
||||
{
|
||||
/** @psalm-var array<class-string, object> Map to store entity listener instances. */
|
||||
/** @var array<class-string, object> Map to store entity listener instances. */
|
||||
private array $instances = [];
|
||||
|
||||
public function clear(string|null $className = null): void
|
||||
|
||||
@@ -16,6 +16,7 @@ use ReflectionProperty;
|
||||
|
||||
use function array_merge;
|
||||
use function assert;
|
||||
use function defined;
|
||||
use function enum_exists;
|
||||
use function is_a;
|
||||
|
||||
@@ -49,30 +50,40 @@ final class DefaultTypedFieldMapper implements TypedFieldMapper
|
||||
{
|
||||
$type = $field->getType();
|
||||
|
||||
if (! $type instanceof ReflectionNamedType) {
|
||||
return $mapping;
|
||||
}
|
||||
|
||||
if (
|
||||
! isset($mapping['type'])
|
||||
&& ($type instanceof ReflectionNamedType)
|
||||
! $type->isBuiltin()
|
||||
&& enum_exists($type->getName())
|
||||
&& (! isset($mapping['type']) || (
|
||||
defined('Doctrine\DBAL\Types\Types::ENUM')
|
||||
&& $mapping['type'] === Types::ENUM
|
||||
))
|
||||
) {
|
||||
if (! $type->isBuiltin() && enum_exists($type->getName())) {
|
||||
$reflection = new ReflectionEnum($type->getName());
|
||||
if (! $reflection->isBacked()) {
|
||||
throw MappingException::backedEnumTypeRequired(
|
||||
$field->class,
|
||||
$mapping['fieldName'],
|
||||
$type->getName(),
|
||||
);
|
||||
}
|
||||
|
||||
assert(is_a($type->getName(), BackedEnum::class, true));
|
||||
$mapping['enumType'] = $type->getName();
|
||||
$type = $reflection->getBackingType();
|
||||
|
||||
assert($type instanceof ReflectionNamedType);
|
||||
$reflection = new ReflectionEnum($type->getName());
|
||||
if (! $reflection->isBacked()) {
|
||||
throw MappingException::backedEnumTypeRequired(
|
||||
$field->class,
|
||||
$mapping['fieldName'],
|
||||
$type->getName(),
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($this->typedFieldMappings[$type->getName()])) {
|
||||
$mapping['type'] = $this->typedFieldMappings[$type->getName()];
|
||||
}
|
||||
assert(is_a($type->getName(), BackedEnum::class, true));
|
||||
$mapping['enumType'] = $type->getName();
|
||||
$type = $reflection->getBackingType();
|
||||
|
||||
assert($type instanceof ReflectionNamedType);
|
||||
}
|
||||
|
||||
if (isset($mapping['type'])) {
|
||||
return $mapping;
|
||||
}
|
||||
|
||||
if (isset($this->typedFieldMappings[$type->getName()])) {
|
||||
$mapping['type'] = $this->typedFieldMappings[$type->getName()];
|
||||
}
|
||||
|
||||
return $mapping;
|
||||
|
||||
@@ -69,8 +69,8 @@ class AttributeDriver implements MappingDriver
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-param class-string<T> $className
|
||||
* @psalm-param ClassMetadata<T> $metadata
|
||||
* @param class-string<T> $className
|
||||
* @param ClassMetadata<T> $metadata
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
|
||||
@@ -35,6 +35,8 @@ use function strtolower;
|
||||
/**
|
||||
* The DatabaseDriver reverse engineers the mapping metadata from a database.
|
||||
*
|
||||
* @deprecated No replacement planned
|
||||
*
|
||||
* @link www.doctrine-project.org
|
||||
*/
|
||||
class DatabaseDriver implements MappingDriver
|
||||
@@ -151,8 +153,8 @@ class DatabaseDriver implements MappingDriver
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-param class-string<T> $className
|
||||
* @psalm-param ClassMetadata<T> $metadata
|
||||
* @param class-string<T> $className
|
||||
* @param ClassMetadata<T> $metadata
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
@@ -491,7 +493,7 @@ class DatabaseDriver implements MappingDriver
|
||||
/**
|
||||
* Returns the mapped class name for a table if it exists. Otherwise return "classified" version.
|
||||
*
|
||||
* @psalm-return class-string
|
||||
* @return class-string
|
||||
*/
|
||||
private function getClassNameForTable(string $tableName): string
|
||||
{
|
||||
|
||||
@@ -72,8 +72,8 @@ class XmlDriver extends FileDriver
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-param class-string<T> $className
|
||||
* @psalm-param ClassMetadata<T> $metadata
|
||||
* @param class-string<T> $className
|
||||
* @param ClassMetadata<T> $metadata
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
@@ -411,6 +411,7 @@ class XmlDriver extends FileDriver
|
||||
/** @psalm-suppress DeprecatedConstant */
|
||||
$orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
|
||||
? (string) $orderByField['direction']
|
||||
// @phpstan-ignore classConstant.deprecated
|
||||
: (enum_exists(Order::class) ? Order::Ascending->value : Criteria::ASC);
|
||||
}
|
||||
|
||||
@@ -540,6 +541,7 @@ class XmlDriver extends FileDriver
|
||||
/** @psalm-suppress DeprecatedConstant */
|
||||
$orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
|
||||
? (string) $orderByField['direction']
|
||||
// @phpstan-ignore classConstant.deprecated
|
||||
: (enum_exists(Order::class) ? Order::Ascending->value : Criteria::ASC);
|
||||
}
|
||||
|
||||
@@ -889,19 +891,19 @@ class XmlDriver extends FileDriver
|
||||
|
||||
if (isset($xmlElement->entity)) {
|
||||
foreach ($xmlElement->entity as $entityElement) {
|
||||
/** @psalm-var class-string $entityName */
|
||||
/** @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 $className */
|
||||
/** @var class-string $className */
|
||||
$className = (string) $mappedSuperClass['name'];
|
||||
$result[$className] = $mappedSuperClass;
|
||||
}
|
||||
} elseif (isset($xmlElement->embeddable)) {
|
||||
foreach ($xmlElement->embeddable as $embeddableElement) {
|
||||
/** @psalm-var class-string $embeddableName */
|
||||
/** @var class-string $embeddableName */
|
||||
$embeddableName = (string) $embeddableElement['name'];
|
||||
$result[$embeddableName] = $embeddableElement;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use Doctrine\ORM\EntityRepository;
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
final class MappedSuperclass implements MappingAttribute
|
||||
{
|
||||
/** @psalm-param class-string<EntityRepository>|null $repositoryClass */
|
||||
/** @param class-string<EntityRepository>|null $repositoryClass */
|
||||
public function __construct(
|
||||
public readonly string|null $repositoryClass = null,
|
||||
) {
|
||||
|
||||
@@ -860,7 +860,10 @@ class BasicEntityPersister implements EntityPersister
|
||||
? $this->expandCriteriaParameters($criteria)
|
||||
: $this->expandParameters($criteria);
|
||||
|
||||
return (int) $this->conn->executeQuery($sql, $params, $types)->fetchOne();
|
||||
$count = (int) $this->conn->executeQuery($sql, $params, $types)->fetchOne();
|
||||
assert($count >= 0);
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -125,6 +125,8 @@ interface EntityPersister
|
||||
* Count entities (optionally filtered by a criteria)
|
||||
*
|
||||
* @param mixed[]|Criteria $criteria
|
||||
*
|
||||
* @psalm-return 0|positive-int
|
||||
*/
|
||||
public function count(array|Criteria $criteria = []): int;
|
||||
|
||||
|
||||
@@ -7,11 +7,14 @@ namespace Doctrine\ORM;
|
||||
use Doctrine\DBAL\LockMode;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Query\AST\DeleteStatement;
|
||||
use Doctrine\ORM\Query\AST\SelectStatement;
|
||||
use Doctrine\ORM\Query\AST\UpdateStatement;
|
||||
use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
|
||||
use Doctrine\ORM\Query\Exec\SqlFinalizer;
|
||||
use Doctrine\ORM\Query\OutputWalker;
|
||||
use Doctrine\ORM\Query\Parameter;
|
||||
use Doctrine\ORM\Query\ParameterTypeInferer;
|
||||
use Doctrine\ORM\Query\Parser;
|
||||
@@ -27,6 +30,7 @@ use function assert;
|
||||
use function count;
|
||||
use function get_debug_type;
|
||||
use function in_array;
|
||||
use function is_a;
|
||||
use function ksort;
|
||||
use function md5;
|
||||
use function reset;
|
||||
@@ -70,6 +74,14 @@ class Query extends AbstractQuery
|
||||
*/
|
||||
public const HINT_REFRESH_ENTITY = 'doctrine.refresh.entity';
|
||||
|
||||
/**
|
||||
* The forcePartialLoad query hint forces a particular query to return
|
||||
* partial objects.
|
||||
*
|
||||
* @todo Rename: HINT_OPTIMIZE
|
||||
*/
|
||||
public const HINT_FORCE_PARTIAL_LOAD = 'doctrine.forcePartialLoad';
|
||||
|
||||
/**
|
||||
* The includeMetaColumns query hint causes meta columns like foreign keys and
|
||||
* discriminator columns to be selected and returned as part of the query result.
|
||||
@@ -163,7 +175,7 @@ class Query extends AbstractQuery
|
||||
*/
|
||||
public function getSQL(): string|array
|
||||
{
|
||||
return $this->parse()->getSqlExecutor()->getSqlStatements();
|
||||
return $this->getSqlExecutor()->getSqlStatements();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,7 +254,7 @@ class Query extends AbstractQuery
|
||||
|
||||
protected function _doExecute(): Result|int
|
||||
{
|
||||
$executor = $this->parse()->getSqlExecutor();
|
||||
$executor = $this->getSqlExecutor();
|
||||
|
||||
if ($this->queryCacheProfile) {
|
||||
$executor->setQueryCacheProfile($this->queryCacheProfile);
|
||||
@@ -656,11 +668,31 @@ class Query extends AbstractQuery
|
||||
{
|
||||
ksort($this->hints);
|
||||
|
||||
if (! $this->hasHint(self::HINT_CUSTOM_OUTPUT_WALKER)) {
|
||||
// Assume Parser will create the SqlOutputWalker; save is_a call, which might trigger a class load
|
||||
$firstAndMaxResult = '';
|
||||
} else {
|
||||
$outputWalkerClass = $this->getHint(self::HINT_CUSTOM_OUTPUT_WALKER);
|
||||
if (is_a($outputWalkerClass, OutputWalker::class, true)) {
|
||||
$firstAndMaxResult = '';
|
||||
} else {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11188/',
|
||||
'Your output walker class %s should implement %s in order to provide a %s. This also means the output walker should not use the query firstResult/maxResult values, which should be read from the query by the SqlFinalizer only.',
|
||||
$outputWalkerClass,
|
||||
OutputWalker::class,
|
||||
SqlFinalizer::class,
|
||||
);
|
||||
$firstAndMaxResult = '&firstResult=' . $this->firstResult . '&maxResult=' . $this->maxResults;
|
||||
}
|
||||
}
|
||||
|
||||
return md5(
|
||||
$this->getDQL() . serialize($this->hints) .
|
||||
'&platform=' . get_debug_type($this->getEntityManager()->getConnection()->getDatabasePlatform()) .
|
||||
($this->em->hasFilters() ? $this->em->getFilters()->getHash() : '') .
|
||||
'&firstResult=' . $this->firstResult . '&maxResult=' . $this->maxResults .
|
||||
$firstAndMaxResult .
|
||||
'&hydrationMode=' . $this->hydrationMode . '&types=' . serialize($this->parsedTypes) . 'DOCTRINE_QUERY_CACHE_SALT',
|
||||
);
|
||||
}
|
||||
@@ -679,4 +711,9 @@ class Query extends AbstractQuery
|
||||
|
||||
$this->state = self::STATE_DIRTY;
|
||||
}
|
||||
|
||||
private function getSqlExecutor(): AbstractSqlExecutor
|
||||
{
|
||||
return $this->parse()->prepareSqlExecutor($this);
|
||||
}
|
||||
}
|
||||
|
||||
15
src/Query/AST/PartialObjectExpression.php
Normal file
15
src/Query/AST/PartialObjectExpression.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query\AST;
|
||||
|
||||
class PartialObjectExpression extends Node
|
||||
{
|
||||
/** @param mixed[] $partialFieldSet */
|
||||
public function __construct(
|
||||
public string $identificationVariable,
|
||||
public array $partialFieldSet,
|
||||
) {
|
||||
}
|
||||
}
|
||||
29
src/Query/Exec/FinalizedSelectExecutor.php
Normal file
29
src/Query/Exec/FinalizedSelectExecutor.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query\Exec;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Result;
|
||||
|
||||
/**
|
||||
* SQL executor for a given, final, single SELECT SQL query
|
||||
*
|
||||
* @method string getSqlStatements()
|
||||
*/
|
||||
class FinalizedSelectExecutor extends AbstractSqlExecutor
|
||||
{
|
||||
public function __construct(string $sql)
|
||||
{
|
||||
$this->sqlStatements = $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function execute(Connection $conn, array $params, array $types): Result
|
||||
{
|
||||
return $conn->executeQuery($this->getSqlStatements(), $params, $types, $this->queryCacheProfile);
|
||||
}
|
||||
}
|
||||
27
src/Query/Exec/PreparedExecutorFinalizer.php
Normal file
27
src/Query/Exec/PreparedExecutorFinalizer.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query\Exec;
|
||||
|
||||
use Doctrine\ORM\Query;
|
||||
|
||||
/**
|
||||
* PreparedExecutorFinalizer is a wrapper for the SQL finalization
|
||||
* phase that does nothing - it is constructed with the sql executor
|
||||
* already.
|
||||
*/
|
||||
final class PreparedExecutorFinalizer implements SqlFinalizer
|
||||
{
|
||||
private AbstractSqlExecutor $executor;
|
||||
|
||||
public function __construct(AbstractSqlExecutor $exeutor)
|
||||
{
|
||||
$this->executor = $exeutor;
|
||||
}
|
||||
|
||||
public function createExecutor(Query $query): AbstractSqlExecutor
|
||||
{
|
||||
return $this->executor;
|
||||
}
|
||||
}
|
||||
60
src/Query/Exec/SingleSelectSqlFinalizer.php
Normal file
60
src/Query/Exec/SingleSelectSqlFinalizer.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query\Exec;
|
||||
|
||||
use Doctrine\DBAL\LockMode;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\QueryException;
|
||||
use Doctrine\ORM\Utility\LockSqlHelper;
|
||||
|
||||
/**
|
||||
* SingleSelectSqlFinalizer finalizes a given SQL query by applying
|
||||
* the query's firstResult/maxResult values as well as extra read lock/write lock
|
||||
* statements, both through the platform-specific methods.
|
||||
*
|
||||
* The resulting, "finalized" SQL is passed to a FinalizedSelectExecutor.
|
||||
*/
|
||||
class SingleSelectSqlFinalizer implements SqlFinalizer
|
||||
{
|
||||
use LockSqlHelper;
|
||||
|
||||
public function __construct(private string $sql)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* This method exists temporarily to support old SqlWalker interfaces.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @psalm-internal Doctrine\ORM
|
||||
*/
|
||||
public function finalizeSql(Query $query): string
|
||||
{
|
||||
$platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();
|
||||
|
||||
$sql = $platform->modifyLimitQuery($this->sql, $query->getMaxResults(), $query->getFirstResult());
|
||||
|
||||
$lockMode = $query->getHint(Query::HINT_LOCK_MODE) ?: LockMode::NONE;
|
||||
|
||||
if ($lockMode !== LockMode::NONE && $lockMode !== LockMode::OPTIMISTIC && $lockMode !== LockMode::PESSIMISTIC_READ && $lockMode !== LockMode::PESSIMISTIC_WRITE) {
|
||||
throw QueryException::invalidLockMode();
|
||||
}
|
||||
|
||||
if ($lockMode === LockMode::PESSIMISTIC_READ) {
|
||||
$sql .= ' ' . $this->getReadLockSQL($platform);
|
||||
} elseif ($lockMode === LockMode::PESSIMISTIC_WRITE) {
|
||||
$sql .= ' ' . $this->getWriteLockSQL($platform);
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/** @return FinalizedSelectExecutor */
|
||||
public function createExecutor(Query $query): AbstractSqlExecutor
|
||||
{
|
||||
return new FinalizedSelectExecutor($this->finalizeSql($query));
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,6 @@ use Doctrine\ORM\Query\SqlWalker;
|
||||
* that are mapped to a single table.
|
||||
*
|
||||
* @link www.doctrine-project.org
|
||||
*
|
||||
* @todo This is exactly the same as SingleSelectExecutor. Unify in SingleStatementExecutor.
|
||||
*/
|
||||
class SingleTableDeleteUpdateExecutor extends AbstractSqlExecutor
|
||||
{
|
||||
|
||||
26
src/Query/Exec/SqlFinalizer.php
Normal file
26
src/Query/Exec/SqlFinalizer.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query\Exec;
|
||||
|
||||
use Doctrine\ORM\Query;
|
||||
|
||||
/**
|
||||
* SqlFinalizers are created by OutputWalkers that traversed the DQL AST.
|
||||
* The SqlFinalizer instance can be kept in the query cache and re-used
|
||||
* at a later time.
|
||||
*
|
||||
* Once the SqlFinalizer has been created or retrieved from the query cache,
|
||||
* it receives the Query object again in order to yield the AbstractSqlExecutor
|
||||
* that will then be used to execute the query.
|
||||
*
|
||||
* The SqlFinalizer may assume that the DQL that was used to build the AST
|
||||
* and run the OutputWalker (which created the SqlFinalizer) is equivalent to
|
||||
* the query that will be passed to the createExecutor() method. Potential differences
|
||||
* are the parameter values or firstResult/maxResult settings.
|
||||
*/
|
||||
interface SqlFinalizer
|
||||
{
|
||||
public function createExecutor(Query $query): AbstractSqlExecutor;
|
||||
}
|
||||
27
src/Query/OutputWalker.php
Normal file
27
src/Query/OutputWalker.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query;
|
||||
|
||||
use Doctrine\ORM\Query\Exec\SqlFinalizer;
|
||||
|
||||
/**
|
||||
* Interface for output walkers
|
||||
*
|
||||
* Output walkers, like tree walkers, can traverse the DQL AST to perform
|
||||
* their purpose.
|
||||
*
|
||||
* The goal of an OutputWalker is to ultimately provide the SqlFinalizer
|
||||
* which produces the final, executable SQL statement in a "finalization" phase.
|
||||
*
|
||||
* It must be possible to use the same SqlFinalizer for Queries with different
|
||||
* firstResult/maxResult values. In other words, SQL produced by the
|
||||
* output walker should not depend on those values, and any SQL generation/modification
|
||||
* specific to them should happen in the finalizer's `\Doctrine\ORM\Query\Exec\SqlFinalizer::createExecutor()`
|
||||
* method instead.
|
||||
*/
|
||||
interface OutputWalker
|
||||
{
|
||||
public function getFinalizer(AST\DeleteStatement|AST\UpdateStatement|AST\SelectStatement $AST): SqlFinalizer;
|
||||
}
|
||||
@@ -5,14 +5,20 @@ declare(strict_types=1);
|
||||
namespace Doctrine\ORM\Query;
|
||||
|
||||
use Doctrine\Common\Lexer\Token;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Exception\DuplicateFieldException;
|
||||
use Doctrine\ORM\Exception\NoMatchingPropertyException;
|
||||
use Doctrine\ORM\Mapping\AssociationMapping;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\AST\Functions;
|
||||
use Doctrine\ORM\Query\Exec\SqlFinalizer;
|
||||
use LogicException;
|
||||
use ReflectionClass;
|
||||
|
||||
use function array_intersect;
|
||||
use function array_key_exists;
|
||||
use function array_search;
|
||||
use function assert;
|
||||
use function class_exists;
|
||||
@@ -28,6 +34,7 @@ use function strpos;
|
||||
use function strrpos;
|
||||
use function strtolower;
|
||||
use function substr;
|
||||
use function trim;
|
||||
|
||||
/**
|
||||
* An LL(*) recursive-descent parser for the context-free grammar of the Doctrine Query Language.
|
||||
@@ -48,7 +55,7 @@ final class Parser
|
||||
{
|
||||
/**
|
||||
* @readonly Maps BUILT-IN string function names to AST class names.
|
||||
* @psalm-var array<string, class-string<Functions\FunctionNode>>
|
||||
* @var array<string, class-string<Functions\FunctionNode>>
|
||||
*/
|
||||
private static array $stringFunctions = [
|
||||
'concat' => Functions\ConcatFunction::class,
|
||||
@@ -61,7 +68,7 @@ final class Parser
|
||||
|
||||
/**
|
||||
* @readonly Maps BUILT-IN numeric function names to AST class names.
|
||||
* @psalm-var array<string, class-string<Functions\FunctionNode>>
|
||||
* @var array<string, class-string<Functions\FunctionNode>>
|
||||
*/
|
||||
private static array $numericFunctions = [
|
||||
'length' => Functions\LengthFunction::class,
|
||||
@@ -84,7 +91,7 @@ final class Parser
|
||||
|
||||
/**
|
||||
* @readonly Maps BUILT-IN datetime function names to AST class names.
|
||||
* @psalm-var array<string, class-string<Functions\FunctionNode>>
|
||||
* @var array<string, class-string<Functions\FunctionNode>>
|
||||
*/
|
||||
private static array $datetimeFunctions = [
|
||||
'current_date' => Functions\CurrentDateFunction::class,
|
||||
@@ -102,6 +109,9 @@ final class Parser
|
||||
/** @psalm-var list<array{token: DqlToken|null, expression: mixed, nestingLevel: int}> */
|
||||
private array $deferredIdentificationVariables = [];
|
||||
|
||||
/** @psalm-var list<array{token: DqlToken|null, expression: AST\PartialObjectExpression, nestingLevel: int}> */
|
||||
private array $deferredPartialObjectExpressions = [];
|
||||
|
||||
/** @psalm-var list<array{token: DqlToken|null, expression: AST\PathExpression, nestingLevel: int}> */
|
||||
private array $deferredPathExpressions = [];
|
||||
|
||||
@@ -141,7 +151,7 @@ final class Parser
|
||||
/**
|
||||
* Any additional custom tree walkers that modify the AST.
|
||||
*
|
||||
* @psalm-var list<class-string<TreeWalker>>
|
||||
* @var list<class-string<TreeWalker>>
|
||||
*/
|
||||
private array $customTreeWalkers = [];
|
||||
|
||||
@@ -171,17 +181,24 @@ final class Parser
|
||||
* Sets a custom tree walker that produces output.
|
||||
* This tree walker will be run last over the AST, after any other walkers.
|
||||
*
|
||||
* @psalm-param class-string<SqlWalker> $className
|
||||
* @param class-string<SqlWalker> $className
|
||||
*/
|
||||
public function setCustomOutputTreeWalker(string $className): void
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11641',
|
||||
'%s is deprecated, set the output walker class with the \Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER query hint instead',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
$this->customOutputWalker = $className;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a custom tree walker for modifying the AST.
|
||||
*
|
||||
* @psalm-param class-string<TreeWalker> $className
|
||||
* @param class-string<TreeWalker> $className
|
||||
*/
|
||||
public function addCustomTreeWalker(string $className): void
|
||||
{
|
||||
@@ -224,6 +241,10 @@ final class Parser
|
||||
// This also allows post-processing of the AST for modification purposes.
|
||||
$this->processDeferredIdentificationVariables();
|
||||
|
||||
if ($this->deferredPartialObjectExpressions) {
|
||||
$this->processDeferredPartialObjectExpressions();
|
||||
}
|
||||
|
||||
if ($this->deferredPathExpressions) {
|
||||
$this->processDeferredPathExpressions();
|
||||
}
|
||||
@@ -335,11 +356,26 @@ final class Parser
|
||||
$this->queryComponents = $treeWalkerChain->getQueryComponents();
|
||||
}
|
||||
|
||||
$outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class;
|
||||
$outputWalkerClass = $this->customOutputWalker ?: SqlOutputWalker::class;
|
||||
$outputWalker = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);
|
||||
|
||||
// Assign an SQL executor to the parser result
|
||||
$this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST));
|
||||
if ($outputWalker instanceof OutputWalker) {
|
||||
$finalizer = $outputWalker->getFinalizer($AST);
|
||||
$this->parserResult->setSqlFinalizer($finalizer);
|
||||
} else {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11188/',
|
||||
'Your output walker class %s should implement %s in order to provide a %s. This also means the output walker should not use the query firstResult/maxResult values, which should be read from the query by the SqlFinalizer only.',
|
||||
$outputWalkerClass,
|
||||
OutputWalker::class,
|
||||
SqlFinalizer::class,
|
||||
);
|
||||
// @phpstan-ignore method.deprecated
|
||||
$executor = $outputWalker->getExecutor($AST);
|
||||
// @phpstan-ignore method.deprecated
|
||||
$this->parserResult->setSqlExecutor($executor);
|
||||
}
|
||||
|
||||
return $this->parserResult;
|
||||
}
|
||||
@@ -599,6 +635,44 @@ final class Parser
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the given <tt>PartialObjectExpression</tt> is semantically correct.
|
||||
* It must exist in query components list.
|
||||
*/
|
||||
private function processDeferredPartialObjectExpressions(): void
|
||||
{
|
||||
foreach ($this->deferredPartialObjectExpressions as $deferredItem) {
|
||||
$expr = $deferredItem['expression'];
|
||||
$class = $this->getMetadataForDqlAlias($expr->identificationVariable);
|
||||
|
||||
foreach ($expr->partialFieldSet as $field) {
|
||||
if (isset($class->fieldMappings[$field])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
isset($class->associationMappings[$field]) &&
|
||||
$class->associationMappings[$field]->isToOneOwningSide()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->semanticalError(sprintf(
|
||||
"There is no mapped field named '%s' on class %s.",
|
||||
$field,
|
||||
$class->name,
|
||||
), $deferredItem['token']);
|
||||
}
|
||||
|
||||
if (array_intersect($class->identifier, $expr->partialFieldSet) !== $class->identifier) {
|
||||
$this->semanticalError(
|
||||
'The partial field selection of class ' . $class->name . ' must contain the identifier.',
|
||||
$deferredItem['token'],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the given <tt>ResultVariable</tt> is semantically correct.
|
||||
* It must exist in query components list.
|
||||
@@ -1621,25 +1695,88 @@ final class Parser
|
||||
return new AST\JoinAssociationDeclaration($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
|
||||
* PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
|
||||
*/
|
||||
public function PartialObjectExpression(): AST\PartialObjectExpression
|
||||
{
|
||||
$this->match(TokenType::T_PARTIAL);
|
||||
|
||||
$partialFieldSet = [];
|
||||
|
||||
$identificationVariable = $this->IdentificationVariable();
|
||||
|
||||
$this->match(TokenType::T_DOT);
|
||||
$this->match(TokenType::T_OPEN_CURLY_BRACE);
|
||||
$this->match(TokenType::T_IDENTIFIER);
|
||||
|
||||
assert($this->lexer->token !== null);
|
||||
$field = $this->lexer->token->value;
|
||||
|
||||
// First field in partial expression might be embeddable property
|
||||
while ($this->lexer->isNextToken(TokenType::T_DOT)) {
|
||||
$this->match(TokenType::T_DOT);
|
||||
$this->match(TokenType::T_IDENTIFIER);
|
||||
$field .= '.' . $this->lexer->token->value;
|
||||
}
|
||||
|
||||
$partialFieldSet[] = $field;
|
||||
|
||||
while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
|
||||
$this->match(TokenType::T_COMMA);
|
||||
$this->match(TokenType::T_IDENTIFIER);
|
||||
|
||||
$field = $this->lexer->token->value;
|
||||
|
||||
while ($this->lexer->isNextToken(TokenType::T_DOT)) {
|
||||
$this->match(TokenType::T_DOT);
|
||||
$this->match(TokenType::T_IDENTIFIER);
|
||||
$field .= '.' . $this->lexer->token->value;
|
||||
}
|
||||
|
||||
$partialFieldSet[] = $field;
|
||||
}
|
||||
|
||||
$this->match(TokenType::T_CLOSE_CURLY_BRACE);
|
||||
|
||||
$partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet);
|
||||
|
||||
// Defer PartialObjectExpression validation
|
||||
$this->deferredPartialObjectExpressions[] = [
|
||||
'expression' => $partialObjectExpression,
|
||||
'nestingLevel' => $this->nestingLevel,
|
||||
'token' => $this->lexer->token,
|
||||
];
|
||||
|
||||
return $partialObjectExpression;
|
||||
}
|
||||
|
||||
/**
|
||||
* NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
|
||||
*/
|
||||
public function NewObjectExpression(): AST\NewObjectExpression
|
||||
{
|
||||
$args = [];
|
||||
$useNamedArguments = false;
|
||||
$args = [];
|
||||
$argFieldAlias = [];
|
||||
$this->match(TokenType::T_NEW);
|
||||
|
||||
if ($this->lexer->isNextToken(TokenType::T_NAMED)) {
|
||||
$this->match(TokenType::T_NAMED);
|
||||
$useNamedArguments = true;
|
||||
}
|
||||
|
||||
$className = $this->AbstractSchemaName(); // note that this is not yet validated
|
||||
$token = $this->lexer->token;
|
||||
|
||||
$this->match(TokenType::T_OPEN_PARENTHESIS);
|
||||
|
||||
$args[] = $this->NewObjectArg();
|
||||
$this->addArgument($args, $useNamedArguments);
|
||||
|
||||
while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
|
||||
$this->match(TokenType::T_COMMA);
|
||||
|
||||
$args[] = $this->NewObjectArg();
|
||||
$this->addArgument($args, $useNamedArguments);
|
||||
}
|
||||
|
||||
$this->match(TokenType::T_CLOSE_PARENTHESIS);
|
||||
@@ -1656,25 +1793,71 @@ final class Parser
|
||||
return $expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* NewObjectArg ::= ScalarExpression | "(" Subselect ")"
|
||||
*/
|
||||
public function NewObjectArg(): mixed
|
||||
/** @param array<mixed> $args */
|
||||
public function addArgument(array &$args, bool $useNamedArguments): void
|
||||
{
|
||||
$fieldAlias = null;
|
||||
|
||||
if ($useNamedArguments) {
|
||||
$startToken = $this->lexer->lookahead?->position ?? 0;
|
||||
|
||||
$newArg = $this->NewObjectArg($fieldAlias);
|
||||
|
||||
$key = $fieldAlias ?? $newArg->field ?? null;
|
||||
|
||||
if ($key === null) {
|
||||
throw NoMatchingPropertyException::create(trim(substr(
|
||||
($this->query->getDQL() ?? ''),
|
||||
$startToken,
|
||||
($this->lexer->lookahead->position ?? 0) - $startToken,
|
||||
)));
|
||||
}
|
||||
|
||||
if (array_key_exists($key, $args)) {
|
||||
throw DuplicateFieldException::create($key, trim(substr(
|
||||
($this->query->getDQL() ?? ''),
|
||||
$startToken,
|
||||
($this->lexer->lookahead->position ?? 0) - $startToken,
|
||||
)));
|
||||
}
|
||||
|
||||
$args[$key] = $newArg;
|
||||
} else {
|
||||
$args[] = $this->NewObjectArg($fieldAlias);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]
|
||||
*/
|
||||
public function NewObjectArg(string|null &$fieldAlias = null): mixed
|
||||
{
|
||||
$fieldAlias = null;
|
||||
|
||||
assert($this->lexer->lookahead !== null);
|
||||
$token = $this->lexer->lookahead;
|
||||
$peek = $this->lexer->glimpse();
|
||||
|
||||
assert($peek !== null);
|
||||
|
||||
$expression = null;
|
||||
|
||||
if ($token->type === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT) {
|
||||
$this->match(TokenType::T_OPEN_PARENTHESIS);
|
||||
$expression = $this->Subselect();
|
||||
$this->match(TokenType::T_CLOSE_PARENTHESIS);
|
||||
|
||||
return $expression;
|
||||
} elseif ($token->type === TokenType::T_NEW) {
|
||||
$expression = $this->NewObjectExpression();
|
||||
} else {
|
||||
$expression = $this->ScalarExpression();
|
||||
}
|
||||
|
||||
return $this->ScalarExpression();
|
||||
if ($this->lexer->isNextToken(TokenType::T_AS)) {
|
||||
$this->match(TokenType::T_AS);
|
||||
$fieldAlias = $this->AliasIdentificationVariable();
|
||||
}
|
||||
|
||||
return $expression;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1920,7 +2103,7 @@ final class Parser
|
||||
/**
|
||||
* SelectExpression ::= (
|
||||
* IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration |
|
||||
* "(" Subselect ")" | CaseExpression | NewObjectExpression
|
||||
* PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression
|
||||
* ) [["AS"] ["HIDDEN"] AliasResultVariable]
|
||||
*/
|
||||
public function SelectExpression(): AST\SelectExpression
|
||||
@@ -1961,6 +2144,12 @@ final class Parser
|
||||
|
||||
break;
|
||||
|
||||
// PartialObjectExpression (PARTIAL u.{id, name})
|
||||
case $lookaheadType === TokenType::T_PARTIAL:
|
||||
$expression = $this->PartialObjectExpression();
|
||||
$identVariable = $expression->identificationVariable;
|
||||
break;
|
||||
|
||||
// Subselect
|
||||
case $lookaheadType === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT:
|
||||
$this->match(TokenType::T_OPEN_PARENTHESIS);
|
||||
@@ -1986,7 +2175,7 @@ final class Parser
|
||||
|
||||
default:
|
||||
$this->syntaxError(
|
||||
'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | "(" Subselect ")" | CaseExpression',
|
||||
'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression',
|
||||
$this->lexer->lookahead,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query;
|
||||
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
|
||||
use Doctrine\ORM\Query\Exec\SqlFinalizer;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
@@ -22,6 +24,11 @@ class ParserResult
|
||||
*/
|
||||
private AbstractSqlExecutor|null $sqlExecutor = null;
|
||||
|
||||
/**
|
||||
* The SQL executor used for executing the SQL.
|
||||
*/
|
||||
private SqlFinalizer|null $sqlFinalizer = null;
|
||||
|
||||
/**
|
||||
* The ResultSetMapping that describes how to map the SQL result set.
|
||||
*/
|
||||
@@ -63,6 +70,8 @@ class ParserResult
|
||||
|
||||
/**
|
||||
* Sets the SQL executor that should be used for this ParserResult.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
public function setSqlExecutor(AbstractSqlExecutor $executor): void
|
||||
{
|
||||
@@ -71,6 +80,8 @@ class ParserResult
|
||||
|
||||
/**
|
||||
* Gets the SQL executor used by this ParserResult.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
public function getSqlExecutor(): AbstractSqlExecutor
|
||||
{
|
||||
@@ -84,6 +95,24 @@ class ParserResult
|
||||
return $this->sqlExecutor;
|
||||
}
|
||||
|
||||
public function setSqlFinalizer(SqlFinalizer $finalizer): void
|
||||
{
|
||||
$this->sqlFinalizer = $finalizer;
|
||||
}
|
||||
|
||||
public function prepareSqlExecutor(Query $query): AbstractSqlExecutor
|
||||
{
|
||||
if ($this->sqlFinalizer !== null) {
|
||||
return $this->sqlFinalizer->createExecutor($query);
|
||||
}
|
||||
|
||||
if ($this->sqlExecutor !== null) {
|
||||
return $this->sqlExecutor;
|
||||
}
|
||||
|
||||
throw new LogicException('This ParserResult lacks both the SqlFinalizer as well as the (legacy) SqlExecutor');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a DQL to SQL parameter mapping. One DQL parameter name/position can map to
|
||||
* several SQL parameter positions.
|
||||
|
||||
@@ -88,6 +88,15 @@ class QueryException extends Exception implements ORMException
|
||||
);
|
||||
}
|
||||
|
||||
public static function partialObjectsAreDangerous(): self
|
||||
{
|
||||
return new self(
|
||||
'Loading partial objects is dangerous. Fetch full objects or consider ' .
|
||||
'using a different fetch mode. If you really want partial objects, ' .
|
||||
'set the doctrine.forcePartialLoad query hint to TRUE.',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $assoc
|
||||
* @psalm-param array<string, string> $assoc
|
||||
|
||||
@@ -38,7 +38,7 @@ class ResultSetMapping
|
||||
* Maps alias names to class names.
|
||||
*
|
||||
* @ignore
|
||||
* @psalm-var array<string, class-string>
|
||||
* @var array<string, class-string>
|
||||
*/
|
||||
public array $aliasMap = [];
|
||||
|
||||
@@ -134,7 +134,7 @@ class ResultSetMapping
|
||||
* Map from column names to class names that declare the field the column is mapped to.
|
||||
*
|
||||
* @ignore
|
||||
* @psalm-var array<string, class-string>
|
||||
* @var array<string, class-string>
|
||||
*/
|
||||
public array $declaringClasses = [];
|
||||
|
||||
@@ -152,6 +152,13 @@ class ResultSetMapping
|
||||
*/
|
||||
public array $newObjectMappings = [];
|
||||
|
||||
/**
|
||||
* Maps last argument for new objects in order to initiate object construction
|
||||
*
|
||||
* @psalm-var array<int|string, array{ownerIndex: string|int, argIndex: int|string}>
|
||||
*/
|
||||
public array $nestedNewObjectArguments = [];
|
||||
|
||||
/**
|
||||
* Maps metadata parameter names to the metadata attribute.
|
||||
*
|
||||
@@ -169,12 +176,11 @@ class ResultSetMapping
|
||||
/**
|
||||
* Adds an entity result to this ResultSetMapping.
|
||||
*
|
||||
* @param string $class The class name of the entity.
|
||||
* @param string $alias The alias for the class. The alias must be unique among all entity
|
||||
* results or joined entity results within this ResultSetMapping.
|
||||
* @param string|null $resultAlias The result alias with which the entity result should be
|
||||
* placed in the result structure.
|
||||
* @psalm-param class-string $class
|
||||
* @param class-string $class The class name of the entity.
|
||||
* @param string $alias The alias for the class. The alias must be unique among all entity
|
||||
* results or joined entity results within this ResultSetMapping.
|
||||
* @param string|null $resultAlias The result alias with which the entity result should be
|
||||
* placed in the result structure.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
@@ -302,15 +308,14 @@ class ResultSetMapping
|
||||
/**
|
||||
* Adds a field to the result that belongs to an entity or joined entity.
|
||||
*
|
||||
* @param string $alias The alias of the root entity or joined entity to which the field belongs.
|
||||
* @param string $columnName The name of the column in the SQL result set.
|
||||
* @param string $fieldName The name of the field on the declaring class.
|
||||
* @param string|null $declaringClass The name of the class that declares/owns the specified field.
|
||||
* When $alias refers to a superclass in a mapped hierarchy but
|
||||
* the field $fieldName is defined on a subclass, specify that here.
|
||||
* If not specified, the field is assumed to belong to the class
|
||||
* designated by $alias.
|
||||
* @psalm-param class-string|null $declaringClass
|
||||
* @param string $alias The alias of the root entity or joined entity to which the field belongs.
|
||||
* @param string $columnName The name of the column in the SQL result set.
|
||||
* @param string $fieldName The name of the field on the declaring class.
|
||||
* @param class-string|null $declaringClass The name of the class that declares/owns the specified field.
|
||||
* When $alias refers to a superclass in a mapped hierarchy but
|
||||
* the field $fieldName is defined on a subclass, specify that here.
|
||||
* If not specified, the field is assumed to belong to the class
|
||||
* designated by $alias.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
@@ -335,12 +340,11 @@ class ResultSetMapping
|
||||
/**
|
||||
* Adds a joined entity result.
|
||||
*
|
||||
* @param string $class The class name of the joined entity.
|
||||
* @param string $alias The unique alias to use for the joined entity.
|
||||
* @param string $parentAlias The alias of the entity result that is the parent of this joined result.
|
||||
* @param string $relation The association field that connects the parent entity result
|
||||
* with the joined entity result.
|
||||
* @psalm-param class-string $class
|
||||
* @param class-string $class The class name of the joined entity.
|
||||
* @param string $alias The unique alias to use for the joined entity.
|
||||
* @param string $parentAlias The alias of the entity result that is the parent of this joined result.
|
||||
* @param string $relation The association field that connects the parent entity result
|
||||
* with the joined entity result.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
@@ -484,7 +488,7 @@ class ResultSetMapping
|
||||
return $this->fieldMappings[$columnName];
|
||||
}
|
||||
|
||||
/** @psalm-return array<string, class-string> */
|
||||
/** @return array<string, class-string> */
|
||||
public function getAliasMap(): array
|
||||
{
|
||||
return $this->aliasMap;
|
||||
|
||||
@@ -56,11 +56,9 @@ class ResultSetMappingBuilder extends ResultSetMapping implements Stringable
|
||||
/**
|
||||
* Adds a root entity and all of its fields to the result set.
|
||||
*
|
||||
* @param string $class The class name of the root entity.
|
||||
* @param string $alias The unique alias to use for the root entity.
|
||||
* @param string[] $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName).
|
||||
* @psalm-param class-string $class
|
||||
* @psalm-param array<string, string> $renamedColumns
|
||||
* @param class-string $class The class name of the root entity.
|
||||
* @param string $alias The unique alias to use for the root entity.
|
||||
* @param array<string, string> $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName).
|
||||
* @psalm-param self::COLUMN_RENAMING_*|null $renameMode
|
||||
*/
|
||||
public function addRootEntityFromClassMetadata(
|
||||
@@ -79,14 +77,12 @@ class ResultSetMappingBuilder extends ResultSetMapping implements Stringable
|
||||
/**
|
||||
* Adds a joined entity and all of its fields to the result set.
|
||||
*
|
||||
* @param string $class The class name of the joined entity.
|
||||
* @param string $alias The unique alias to use for the joined entity.
|
||||
* @param string $parentAlias The alias of the entity result that is the parent of this joined result.
|
||||
* @param string $relation The association field that connects the parent entity result
|
||||
* with the joined entity result.
|
||||
* @param string[] $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName).
|
||||
* @psalm-param class-string $class
|
||||
* @psalm-param array<string, string> $renamedColumns
|
||||
* @param class-string $class The class name of the joined entity.
|
||||
* @param string $alias The unique alias to use for the joined entity.
|
||||
* @param string $parentAlias The alias of the entity result that is the parent of this joined result.
|
||||
* @param string $relation The association field that connects the parent entity result
|
||||
* with the joined entity result.
|
||||
* @param array<string, string> $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName).
|
||||
* @psalm-param self::COLUMN_RENAMING_*|null $renameMode
|
||||
*/
|
||||
public function addJoinedEntityFromClassMetadata(
|
||||
@@ -197,12 +193,11 @@ class ResultSetMappingBuilder extends ResultSetMapping implements Stringable
|
||||
*
|
||||
* This depends on the renaming mode selected by the user.
|
||||
*
|
||||
* @psalm-param class-string $className
|
||||
* @param class-string $className
|
||||
* @psalm-param self::COLUMN_RENAMING_* $mode
|
||||
* @psalm-param array<string, string> $customRenameColumns
|
||||
*
|
||||
* @return string[]
|
||||
* @psalm-return array<array-key, string>
|
||||
*/
|
||||
private function getColumnAliasMap(
|
||||
string $className,
|
||||
|
||||
29
src/Query/SqlOutputWalker.php
Normal file
29
src/Query/SqlOutputWalker.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query;
|
||||
|
||||
use Doctrine\ORM\Query\Exec\PreparedExecutorFinalizer;
|
||||
use Doctrine\ORM\Query\Exec\SingleSelectSqlFinalizer;
|
||||
use Doctrine\ORM\Query\Exec\SqlFinalizer;
|
||||
use LogicException;
|
||||
|
||||
class SqlOutputWalker extends SqlWalker implements OutputWalker
|
||||
{
|
||||
public function getFinalizer(AST\DeleteStatement|AST\UpdateStatement|AST\SelectStatement $AST): SqlFinalizer
|
||||
{
|
||||
switch (true) {
|
||||
case $AST instanceof AST\SelectStatement:
|
||||
return new SingleSelectSqlFinalizer($this->createSqlForFinalizer($AST));
|
||||
|
||||
case $AST instanceof AST\UpdateStatement:
|
||||
return new PreparedExecutorFinalizer($this->createUpdateStatementExecutor($AST));
|
||||
|
||||
case $AST instanceof AST\DeleteStatement:
|
||||
return new PreparedExecutorFinalizer($this->createDeleteStatementExecutor($AST));
|
||||
}
|
||||
|
||||
throw new LogicException('Unexpected AST node type');
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ use Doctrine\ORM\Mapping\QuoteStrategy;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Utility\HierarchyDiscriminatorResolver;
|
||||
use Doctrine\ORM\Utility\LockSqlHelper;
|
||||
use Doctrine\ORM\Utility\PersisterHelper;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
@@ -25,9 +24,12 @@ use function array_filter;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_pop;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function end;
|
||||
use function implode;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_float;
|
||||
use function is_int;
|
||||
@@ -48,10 +50,13 @@ use function trim;
|
||||
*/
|
||||
class SqlWalker
|
||||
{
|
||||
use LockSqlHelper;
|
||||
|
||||
public const HINT_DISTINCT = 'doctrine.distinct';
|
||||
|
||||
/**
|
||||
* Used to mark a query as containing a PARTIAL expression, which needs to be known by SLC.
|
||||
*/
|
||||
public const HINT_PARTIAL = 'doctrine.partial';
|
||||
|
||||
private readonly ResultSetMapping $rsm;
|
||||
|
||||
/**
|
||||
@@ -79,6 +84,13 @@ class SqlWalker
|
||||
*/
|
||||
private int $newObjectCounter = 0;
|
||||
|
||||
/**
|
||||
* Contains nesting levels of new objects arguments
|
||||
*
|
||||
* @psalm-var array<int, array{0: string|int, 1: int}>
|
||||
*/
|
||||
private array $newObjectStack = [];
|
||||
|
||||
private readonly EntityManagerInterface $em;
|
||||
private readonly Connection $conn;
|
||||
|
||||
@@ -220,23 +232,40 @@ class SqlWalker
|
||||
|
||||
/**
|
||||
* Gets an executor that can be used to execute the result of this walker.
|
||||
*
|
||||
* @deprecated Output walkers should no longer create the executor directly, but instead provide
|
||||
* a SqlFinalizer by implementing the `OutputWalker` interface. Thus, this method is
|
||||
* no longer needed and will be removed in 4.0.
|
||||
*/
|
||||
public function getExecutor(AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement $statement): Exec\AbstractSqlExecutor
|
||||
{
|
||||
return match (true) {
|
||||
$statement instanceof AST\SelectStatement
|
||||
=> new Exec\SingleSelectExecutor($statement, $this),
|
||||
$statement instanceof AST\UpdateStatement
|
||||
=> $this->em->getClassMetadata($statement->updateClause->abstractSchemaName)->isInheritanceTypeJoined()
|
||||
? new Exec\MultiTableUpdateExecutor($statement, $this)
|
||||
: new Exec\SingleTableDeleteUpdateExecutor($statement, $this),
|
||||
$statement instanceof AST\DeleteStatement
|
||||
=> $this->em->getClassMetadata($statement->deleteClause->abstractSchemaName)->isInheritanceTypeJoined()
|
||||
? new Exec\MultiTableDeleteExecutor($statement, $this)
|
||||
: new Exec\SingleTableDeleteUpdateExecutor($statement, $this),
|
||||
$statement instanceof AST\UpdateStatement => $this->createUpdateStatementExecutor($statement),
|
||||
$statement instanceof AST\DeleteStatement => $this->createDeleteStatementExecutor($statement),
|
||||
default => new Exec\SingleSelectExecutor($statement, $this),
|
||||
};
|
||||
}
|
||||
|
||||
/** @psalm-internal Doctrine\ORM */
|
||||
protected function createUpdateStatementExecutor(AST\UpdateStatement $AST): Exec\AbstractSqlExecutor
|
||||
{
|
||||
$primaryClass = $this->em->getClassMetadata($AST->updateClause->abstractSchemaName);
|
||||
|
||||
return $primaryClass->isInheritanceTypeJoined()
|
||||
? new Exec\MultiTableUpdateExecutor($AST, $this)
|
||||
: new Exec\SingleTableDeleteUpdateExecutor($AST, $this);
|
||||
}
|
||||
|
||||
/** @psalm-internal Doctrine\ORM */
|
||||
protected function createDeleteStatementExecutor(AST\DeleteStatement $AST): Exec\AbstractSqlExecutor
|
||||
{
|
||||
$primaryClass = $this->em->getClassMetadata($AST->deleteClause->abstractSchemaName);
|
||||
|
||||
return $primaryClass->isInheritanceTypeJoined()
|
||||
? new Exec\MultiTableDeleteExecutor($AST, $this)
|
||||
: new Exec\SingleTableDeleteUpdateExecutor($AST, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique, short SQL table alias.
|
||||
*/
|
||||
@@ -306,6 +335,11 @@ class SqlWalker
|
||||
$sql .= implode(' AND ', array_filter($sqlParts));
|
||||
}
|
||||
|
||||
// Ignore subclassing inclusion if partial objects is disallowed
|
||||
if ($this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
// LEFT JOIN child class tables
|
||||
foreach ($class->subClasses as $subClassName) {
|
||||
$subClass = $this->em->getClassMetadata($subClassName);
|
||||
@@ -464,10 +498,15 @@ class SqlWalker
|
||||
*/
|
||||
public function walkSelectStatement(AST\SelectStatement $selectStatement): string
|
||||
{
|
||||
$limit = $this->query->getMaxResults();
|
||||
$offset = $this->query->getFirstResult();
|
||||
$lockMode = $this->query->getHint(Query::HINT_LOCK_MODE) ?: LockMode::NONE;
|
||||
$sql = $this->walkSelectClause($selectStatement->selectClause)
|
||||
$sql = $this->createSqlForFinalizer($selectStatement);
|
||||
$finalizer = new Exec\SingleSelectSqlFinalizer($sql);
|
||||
|
||||
return $finalizer->finalizeSql($this->query);
|
||||
}
|
||||
|
||||
protected function createSqlForFinalizer(AST\SelectStatement $selectStatement): string
|
||||
{
|
||||
$sql = $this->walkSelectClause($selectStatement->selectClause)
|
||||
. $this->walkFromClause($selectStatement->fromClause)
|
||||
. $this->walkWhereClause($selectStatement->whereClause);
|
||||
|
||||
@@ -488,33 +527,24 @@ class SqlWalker
|
||||
$sql .= ' ORDER BY ' . $orderBySql;
|
||||
}
|
||||
|
||||
$sql = $this->platform->modifyLimitQuery($sql, $limit, $offset);
|
||||
|
||||
if ($lockMode === LockMode::NONE) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
if ($lockMode === LockMode::PESSIMISTIC_READ) {
|
||||
return $sql . ' ' . $this->getReadLockSQL($this->platform);
|
||||
}
|
||||
|
||||
if ($lockMode === LockMode::PESSIMISTIC_WRITE) {
|
||||
return $sql . ' ' . $this->getWriteLockSQL($this->platform);
|
||||
}
|
||||
|
||||
if ($lockMode !== LockMode::OPTIMISTIC) {
|
||||
throw QueryException::invalidLockMode();
|
||||
}
|
||||
|
||||
foreach ($this->selectedClasses as $selectedClass) {
|
||||
if (! $selectedClass['class']->isVersioned) {
|
||||
throw OptimisticLockException::lockFailed($selectedClass['class']->name);
|
||||
}
|
||||
}
|
||||
$this->assertOptimisticLockingHasAllClassesVersioned();
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
private function assertOptimisticLockingHasAllClassesVersioned(): void
|
||||
{
|
||||
$lockMode = $this->query->getHint(Query::HINT_LOCK_MODE) ?: LockMode::NONE;
|
||||
|
||||
if ($lockMode === LockMode::OPTIMISTIC) {
|
||||
foreach ($this->selectedClasses as $selectedClass) {
|
||||
if (! $selectedClass['class']->isVersioned) {
|
||||
throw OptimisticLockException::lockFailed($selectedClass['class']->name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks down a UpdateStatement AST node, thereby generating the appropriate SQL.
|
||||
*/
|
||||
@@ -644,7 +674,8 @@ class SqlWalker
|
||||
$this->query->setHint(self::HINT_DISTINCT, true);
|
||||
}
|
||||
|
||||
$addMetaColumns = $this->query->getHydrationMode() === Query::HYDRATE_OBJECT
|
||||
$addMetaColumns = ! $this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD) &&
|
||||
$this->query->getHydrationMode() === Query::HYDRATE_OBJECT
|
||||
|| $this->query->getHint(Query::HINT_INCLUDE_META_COLUMNS);
|
||||
|
||||
foreach ($this->selectedClasses as $selectedClass) {
|
||||
@@ -1325,7 +1356,17 @@ class SqlWalker
|
||||
break;
|
||||
|
||||
default:
|
||||
$dqlAlias = $expr;
|
||||
// IdentificationVariable or PartialObjectExpression
|
||||
if ($expr instanceof AST\PartialObjectExpression) {
|
||||
$this->query->setHint(self::HINT_PARTIAL, true);
|
||||
|
||||
$dqlAlias = $expr->identificationVariable;
|
||||
$partialFieldSet = $expr->partialFieldSet;
|
||||
} else {
|
||||
$dqlAlias = $expr;
|
||||
$partialFieldSet = [];
|
||||
}
|
||||
|
||||
$class = $this->getMetadataForDqlAlias($dqlAlias);
|
||||
$resultAlias = $selectExpression->fieldIdentificationVariable ?: null;
|
||||
|
||||
@@ -1341,6 +1382,10 @@ class SqlWalker
|
||||
|
||||
// Select all fields from the queried class
|
||||
foreach ($class->fieldMappings as $fieldName => $mapping) {
|
||||
if ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tableName = isset($mapping->inherited)
|
||||
? $this->em->getClassMetadata($mapping->inherited)->getTableName()
|
||||
: $class->getTableName();
|
||||
@@ -1367,29 +1412,32 @@ class SqlWalker
|
||||
|
||||
// Add any additional fields of subclasses (excluding inherited fields)
|
||||
// 1) on Single Table Inheritance: always, since its marginal overhead
|
||||
// 2) on Class Table Inheritance
|
||||
foreach ($class->subClasses as $subClassName) {
|
||||
$subClass = $this->em->getClassMetadata($subClassName);
|
||||
$sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias);
|
||||
// 2) on Class Table Inheritance only if partial objects are disallowed,
|
||||
// since it requires outer joining subtables.
|
||||
if ($class->isInheritanceTypeSingleTable() || ! $this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) {
|
||||
foreach ($class->subClasses as $subClassName) {
|
||||
$subClass = $this->em->getClassMetadata($subClassName);
|
||||
$sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias);
|
||||
|
||||
foreach ($subClass->fieldMappings as $fieldName => $mapping) {
|
||||
if (isset($mapping->inherited)) {
|
||||
continue;
|
||||
foreach ($subClass->fieldMappings as $fieldName => $mapping) {
|
||||
if (isset($mapping->inherited) || ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$columnAlias = $this->getSQLColumnAlias($mapping->columnName);
|
||||
$quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform);
|
||||
|
||||
$col = $sqlTableAlias . '.' . $quotedColumnName;
|
||||
|
||||
$type = Type::getType($mapping->type);
|
||||
$col = $type->convertToPHPValueSQL($col, $this->platform);
|
||||
|
||||
$sqlParts[] = $col . ' AS ' . $columnAlias;
|
||||
|
||||
$this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
|
||||
|
||||
$this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName);
|
||||
}
|
||||
|
||||
$columnAlias = $this->getSQLColumnAlias($mapping->columnName);
|
||||
$quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform);
|
||||
|
||||
$col = $sqlTableAlias . '.' . $quotedColumnName;
|
||||
|
||||
$type = Type::getType($mapping->type);
|
||||
$col = $type->convertToPHPValueSQL($col, $this->platform);
|
||||
|
||||
$sqlParts[] = $col . ' AS ' . $columnAlias;
|
||||
|
||||
$this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
|
||||
|
||||
$this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1461,7 +1509,14 @@ class SqlWalker
|
||||
public function walkNewObject(AST\NewObjectExpression $newObjectExpression, string|null $newObjectResultAlias = null): string
|
||||
{
|
||||
$sqlSelectExpressions = [];
|
||||
$objIndex = $newObjectResultAlias ?: $this->newObjectCounter++;
|
||||
$objOwner = $objOwnerIdx = null;
|
||||
|
||||
if ($this->newObjectStack !== []) {
|
||||
[$objOwner, $objOwnerIdx] = end($this->newObjectStack);
|
||||
$objIndex = $objOwner . ':' . $objOwnerIdx;
|
||||
} else {
|
||||
$objIndex = $newObjectResultAlias ?: $this->newObjectCounter++;
|
||||
}
|
||||
|
||||
foreach ($newObjectExpression->args as $argIndex => $e) {
|
||||
$resultAlias = $this->scalarResultCounter++;
|
||||
@@ -1470,7 +1525,10 @@ class SqlWalker
|
||||
|
||||
switch (true) {
|
||||
case $e instanceof AST\NewObjectExpression:
|
||||
$this->newObjectStack[] = [$objIndex, $argIndex];
|
||||
$sqlSelectExpressions[] = $e->dispatch($this);
|
||||
array_pop($this->newObjectStack);
|
||||
$this->rsm->nestedNewObjectArguments[$columnAlias] = ['ownerIndex' => $objIndex, 'argIndex' => $argIndex];
|
||||
break;
|
||||
|
||||
case $e instanceof AST\Subselect:
|
||||
|
||||
@@ -77,6 +77,7 @@ enum TokenType: int
|
||||
case T_OR = 242;
|
||||
case T_ORDER = 243;
|
||||
case T_OUTER = 244;
|
||||
case T_PARTIAL = 245;
|
||||
case T_SELECT = 246;
|
||||
case T_SET = 247;
|
||||
case T_SOME = 248;
|
||||
@@ -88,4 +89,5 @@ enum TokenType: int
|
||||
case T_WHEN = 254;
|
||||
case T_WHERE = 255;
|
||||
case T_WITH = 256;
|
||||
case T_NAMED = 257;
|
||||
}
|
||||
|
||||
@@ -19,8 +19,7 @@ class TreeWalkerChain implements TreeWalker
|
||||
/**
|
||||
* The tree walkers.
|
||||
*
|
||||
* @var string[]
|
||||
* @psalm-var list<class-string<TreeWalker>>
|
||||
* @var list<class-string<TreeWalker>>
|
||||
*/
|
||||
private array $walkers = [];
|
||||
|
||||
@@ -47,8 +46,7 @@ class TreeWalkerChain implements TreeWalker
|
||||
/**
|
||||
* Adds a tree walker to the chain.
|
||||
*
|
||||
* @param string $walkerClass The class of the walker to instantiate.
|
||||
* @psalm-param class-string<TreeWalker> $walkerClass
|
||||
* @param class-string<TreeWalker> $walkerClass The class of the walker to instantiate.
|
||||
*/
|
||||
public function addTreeWalker(string $walkerClass): void
|
||||
{
|
||||
|
||||
@@ -110,6 +110,13 @@ class QueryBuilder implements Stringable
|
||||
|
||||
protected int $lifetime = 0;
|
||||
|
||||
/**
|
||||
* The counter of bound parameters.
|
||||
*
|
||||
* @var int<0, max>
|
||||
*/
|
||||
private int $boundCounter = 0;
|
||||
|
||||
/**
|
||||
* Initializes a new <tt>QueryBuilder</tt> that uses the given <tt>EntityManager</tt>.
|
||||
*
|
||||
@@ -294,6 +301,7 @@ class QueryBuilder implements Stringable
|
||||
} else {
|
||||
// Should never happen with correct joining order. Might be
|
||||
// thoughtful to throw exception instead.
|
||||
// @phpstan-ignore method.deprecated
|
||||
$rootAlias = $this->getRootAlias();
|
||||
}
|
||||
|
||||
@@ -576,6 +584,7 @@ class QueryBuilder implements Stringable
|
||||
$newDqlPart = [];
|
||||
|
||||
foreach ($dqlPart as $k => $v) {
|
||||
// @phpstan-ignore method.deprecated
|
||||
$k = is_numeric($k) ? $this->getRootAlias() : $k;
|
||||
|
||||
$newDqlPart[$k] = $v;
|
||||
@@ -1336,6 +1345,41 @@ class QueryBuilder implements Stringable
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new named parameter and bind the value $value to it.
|
||||
*
|
||||
* The parameter $value specifies the value that you want to bind. If
|
||||
* $placeholder is not provided createNamedParameter() will automatically
|
||||
* create a placeholder for you. An automatic placeholder will be of the
|
||||
* name ':dcValue1', ':dcValue2' etc.
|
||||
*
|
||||
* Example:
|
||||
* <code>
|
||||
* $qb = $em->createQueryBuilder();
|
||||
* $qb
|
||||
* ->select('u')
|
||||
* ->from('User', 'u')
|
||||
* ->where('u.username = ' . $qb->createNamedParameter('Foo', Types::STRING))
|
||||
* ->orWhere('u.username = ' . $qb->createNamedParameter('Bar', Types::STRING))
|
||||
* </code>
|
||||
*
|
||||
* @param ParameterType|ArrayParameterType|string|int|null $type ParameterType::*, ArrayParameterType::* or \Doctrine\DBAL\Types\Type::* constant
|
||||
* @param non-empty-string|null $placeholder The name to bind with. The string must start with a colon ':'.
|
||||
*
|
||||
* @return non-empty-string the placeholder name used.
|
||||
*/
|
||||
public function createNamedParameter(mixed $value, ParameterType|ArrayParameterType|string|int|null $type = null, string|null $placeholder = null): string
|
||||
{
|
||||
if ($placeholder === null) {
|
||||
$this->boundCounter++;
|
||||
$placeholder = ':dcValue' . $this->boundCounter;
|
||||
}
|
||||
|
||||
$this->setParameter(substr($placeholder, 1), $value, $type);
|
||||
|
||||
return $placeholder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a string representation of this QueryBuilder which corresponds to
|
||||
* the final DQL query being constructed.
|
||||
|
||||
@@ -127,8 +127,7 @@ EOT);
|
||||
/**
|
||||
* Return all mapped entity class names
|
||||
*
|
||||
* @return string[]
|
||||
* @psalm-return class-string[]
|
||||
* @return class-string[]
|
||||
*/
|
||||
private function getMappedEntities(EntityManagerInterface $entityManager): array
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ use Doctrine\ORM\Query\AST\SelectStatement;
|
||||
use Doctrine\ORM\Query\Parser;
|
||||
use Doctrine\ORM\Query\ParserResult;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\ORM\Query\SqlWalker;
|
||||
use Doctrine\ORM\Query\SqlOutputWalker;
|
||||
use RuntimeException;
|
||||
|
||||
use function array_diff;
|
||||
@@ -37,7 +37,7 @@ use function sprintf;
|
||||
*
|
||||
* @psalm-import-type QueryComponent from Parser
|
||||
*/
|
||||
class CountOutputWalker extends SqlWalker
|
||||
class CountOutputWalker extends SqlOutputWalker
|
||||
{
|
||||
private readonly AbstractPlatform $platform;
|
||||
private readonly ResultSetMapping $rsm;
|
||||
@@ -53,13 +53,13 @@ class CountOutputWalker extends SqlWalker
|
||||
parent::__construct($query, $parserResult, $queryComponents);
|
||||
}
|
||||
|
||||
public function walkSelectStatement(SelectStatement $selectStatement): string
|
||||
protected function createSqlForFinalizer(SelectStatement $selectStatement): string
|
||||
{
|
||||
if ($this->platform instanceof SQLServerPlatform) {
|
||||
$selectStatement->orderByClause = null;
|
||||
}
|
||||
|
||||
$sql = parent::walkSelectStatement($selectStatement);
|
||||
$sql = parent::createSqlForFinalizer($selectStatement);
|
||||
|
||||
if ($selectStatement->groupByClause) {
|
||||
return sprintf(
|
||||
|
||||
@@ -37,27 +37,31 @@ class CountWalker extends TreeWalkerAdapter
|
||||
throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction');
|
||||
}
|
||||
|
||||
$fromRoot = reset($from);
|
||||
$rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
|
||||
$rootClass = $this->getMetadataForDqlAlias($rootAlias);
|
||||
$identifierFieldName = $rootClass->getSingleIdentifierFieldName();
|
||||
$distinct = $this->_getQuery()->getHint(self::HINT_DISTINCT);
|
||||
|
||||
$pathType = PathExpression::TYPE_STATE_FIELD;
|
||||
if (isset($rootClass->associationMappings[$identifierFieldName])) {
|
||||
$pathType = PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION;
|
||||
$countPathExpressionOrLiteral = '*';
|
||||
if ($distinct) {
|
||||
$fromRoot = reset($from);
|
||||
$rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
|
||||
$rootClass = $this->getMetadataForDqlAlias($rootAlias);
|
||||
$identifierFieldName = $rootClass->getSingleIdentifierFieldName();
|
||||
|
||||
$pathType = PathExpression::TYPE_STATE_FIELD;
|
||||
if (isset($rootClass->associationMappings[$identifierFieldName])) {
|
||||
$pathType = PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION;
|
||||
}
|
||||
|
||||
$countPathExpressionOrLiteral = new PathExpression(
|
||||
PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION,
|
||||
$rootAlias,
|
||||
$identifierFieldName,
|
||||
);
|
||||
$countPathExpressionOrLiteral->type = $pathType;
|
||||
}
|
||||
|
||||
$pathExpression = new PathExpression(
|
||||
PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION,
|
||||
$rootAlias,
|
||||
$identifierFieldName,
|
||||
);
|
||||
$pathExpression->type = $pathType;
|
||||
|
||||
$distinct = $this->_getQuery()->getHint(self::HINT_DISTINCT);
|
||||
$selectStatement->selectClause->selectExpressions = [
|
||||
new SelectExpression(
|
||||
new AggregateExpression('count', $pathExpression, $distinct),
|
||||
new AggregateExpression('count', $countPathExpressionOrLiteral, $distinct),
|
||||
null,
|
||||
),
|
||||
];
|
||||
|
||||
@@ -13,16 +13,20 @@ use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\QuoteStrategy;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\AST;
|
||||
use Doctrine\ORM\Query\AST\OrderByClause;
|
||||
use Doctrine\ORM\Query\AST\PathExpression;
|
||||
use Doctrine\ORM\Query\AST\SelectExpression;
|
||||
use Doctrine\ORM\Query\AST\SelectStatement;
|
||||
use Doctrine\ORM\Query\AST\Subselect;
|
||||
use Doctrine\ORM\Query\Exec\SingleSelectSqlFinalizer;
|
||||
use Doctrine\ORM\Query\Exec\SqlFinalizer;
|
||||
use Doctrine\ORM\Query\Parser;
|
||||
use Doctrine\ORM\Query\ParserResult;
|
||||
use Doctrine\ORM\Query\QueryException;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\ORM\Query\SqlWalker;
|
||||
use Doctrine\ORM\Query\SqlOutputWalker;
|
||||
use LogicException;
|
||||
use RuntimeException;
|
||||
|
||||
use function array_diff;
|
||||
@@ -50,7 +54,7 @@ use function substr;
|
||||
*
|
||||
* @psalm-import-type QueryComponent from Parser
|
||||
*/
|
||||
class LimitSubqueryOutputWalker extends SqlWalker
|
||||
class LimitSubqueryOutputWalker extends SqlOutputWalker
|
||||
{
|
||||
private const ORDER_BY_PATH_EXPRESSION = '/(?<![a-z0-9_])%s\.%s(?![a-z0-9_])/i';
|
||||
|
||||
@@ -85,6 +89,8 @@ class LimitSubqueryOutputWalker extends SqlWalker
|
||||
$this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();
|
||||
$this->rsm = $parserResult->getResultSetMapping();
|
||||
|
||||
$query = clone $query;
|
||||
|
||||
// Reset limit and offset
|
||||
$this->firstResult = $query->getFirstResult();
|
||||
$this->maxResults = $query->getMaxResults();
|
||||
@@ -139,11 +145,28 @@ class LimitSubqueryOutputWalker extends SqlWalker
|
||||
|
||||
public function walkSelectStatement(SelectStatement $selectStatement): string
|
||||
{
|
||||
if ($this->platformSupportsRowNumber()) {
|
||||
return $this->walkSelectStatementWithRowNumber($selectStatement);
|
||||
$sqlFinalizer = $this->getFinalizer($selectStatement);
|
||||
|
||||
$query = $this->getQuery();
|
||||
|
||||
$abstractSqlExecutor = $sqlFinalizer->createExecutor($query);
|
||||
|
||||
return $abstractSqlExecutor->getSqlStatements();
|
||||
}
|
||||
|
||||
public function getFinalizer(AST\DeleteStatement|AST\UpdateStatement|AST\SelectStatement $AST): SqlFinalizer
|
||||
{
|
||||
if (! $AST instanceof SelectStatement) {
|
||||
throw new LogicException(self::class . ' is to be used on SelectStatements only');
|
||||
}
|
||||
|
||||
return $this->walkSelectStatementWithoutRowNumber($selectStatement);
|
||||
if ($this->platformSupportsRowNumber()) {
|
||||
$sql = $this->createSqlWithRowNumber($AST);
|
||||
} else {
|
||||
$sql = $this->createSqlWithoutRowNumber($AST);
|
||||
}
|
||||
|
||||
return new SingleSelectSqlFinalizer($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,6 +176,16 @@ class LimitSubqueryOutputWalker extends SqlWalker
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function walkSelectStatementWithRowNumber(SelectStatement $AST): string
|
||||
{
|
||||
// Apply the limit and offset.
|
||||
return $this->platform->modifyLimitQuery(
|
||||
$this->createSqlWithRowNumber($AST),
|
||||
$this->maxResults,
|
||||
$this->firstResult,
|
||||
);
|
||||
}
|
||||
|
||||
private function createSqlWithRowNumber(SelectStatement $AST): string
|
||||
{
|
||||
$hasOrderBy = false;
|
||||
$outerOrderBy = ' ORDER BY dctrn_minrownum ASC';
|
||||
@@ -182,13 +215,6 @@ class LimitSubqueryOutputWalker extends SqlWalker
|
||||
$sql .= $orderGroupBy . $outerOrderBy;
|
||||
}
|
||||
|
||||
// Apply the limit and offset.
|
||||
$sql = $this->platform->modifyLimitQuery(
|
||||
$sql,
|
||||
$this->maxResults,
|
||||
$this->firstResult,
|
||||
);
|
||||
|
||||
// Add the columns to the ResultSetMapping. It's not really nice but
|
||||
// it works. Preferably I'd clear the RSM or simply create a new one
|
||||
// but that is not possible from inside the output walker, so we dirty
|
||||
@@ -207,6 +233,16 @@ class LimitSubqueryOutputWalker extends SqlWalker
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function walkSelectStatementWithoutRowNumber(SelectStatement $AST, bool $addMissingItemsFromOrderByToSelect = true): string
|
||||
{
|
||||
// Apply the limit and offset.
|
||||
return $this->platform->modifyLimitQuery(
|
||||
$this->createSqlWithoutRowNumber($AST, $addMissingItemsFromOrderByToSelect),
|
||||
$this->maxResults,
|
||||
$this->firstResult,
|
||||
);
|
||||
}
|
||||
|
||||
private function createSqlWithoutRowNumber(SelectStatement $AST, bool $addMissingItemsFromOrderByToSelect = true): string
|
||||
{
|
||||
// We don't want to call this recursively!
|
||||
if ($AST->orderByClause instanceof OrderByClause && $addMissingItemsFromOrderByToSelect) {
|
||||
@@ -235,13 +271,6 @@ class LimitSubqueryOutputWalker extends SqlWalker
|
||||
// https://github.com/doctrine/orm/issues/2630
|
||||
$sql = $this->preserveSqlOrdering($sqlIdentifier, $innerSql, $sql, $orderByClause);
|
||||
|
||||
// Apply the limit and offset.
|
||||
$sql = $this->platform->modifyLimitQuery(
|
||||
$sql,
|
||||
$this->maxResults,
|
||||
$this->firstResult,
|
||||
);
|
||||
|
||||
// Add the columns to the ResultSetMapping. It's not really nice but
|
||||
// it works. Preferably I'd clear the RSM or simply create a new one
|
||||
// but that is not possible from inside the output walker, so we dirty
|
||||
|
||||
@@ -183,7 +183,7 @@ class Paginator implements Countable, IteratorAggregate
|
||||
/**
|
||||
* Appends a custom tree walker to the tree walkers hint.
|
||||
*
|
||||
* @psalm-param class-string $walkerClass
|
||||
* @param class-string $walkerClass
|
||||
*/
|
||||
private function appendTreeWalker(Query $query, string $walkerClass): void
|
||||
{
|
||||
|
||||
@@ -5,7 +5,10 @@ declare(strict_types=1);
|
||||
namespace Doctrine\ORM\Tools\Pagination;
|
||||
|
||||
use Doctrine\ORM\Query\AST;
|
||||
use Doctrine\ORM\Query\SqlWalker;
|
||||
use Doctrine\ORM\Query\Exec\FinalizedSelectExecutor;
|
||||
use Doctrine\ORM\Query\Exec\PreparedExecutorFinalizer;
|
||||
use Doctrine\ORM\Query\Exec\SqlFinalizer;
|
||||
use Doctrine\ORM\Query\SqlOutputWalker;
|
||||
use Doctrine\ORM\Utility\PersisterHelper;
|
||||
use RuntimeException;
|
||||
|
||||
@@ -22,7 +25,7 @@ use function reset;
|
||||
* Returning the type instead of a "real" SQL statement is a slight hack. However, it has the
|
||||
* benefit that the DQL -> root entity id type resolution can be cached in the query cache.
|
||||
*/
|
||||
final class RootTypeWalker extends SqlWalker
|
||||
final class RootTypeWalker extends SqlOutputWalker
|
||||
{
|
||||
public function walkSelectStatement(AST\SelectStatement $selectStatement): string
|
||||
{
|
||||
@@ -45,4 +48,13 @@ final class RootTypeWalker extends SqlWalker
|
||||
->getEntityManager(),
|
||||
)[0];
|
||||
}
|
||||
|
||||
public function getFinalizer(AST\DeleteStatement|AST\UpdateStatement|AST\SelectStatement $AST): SqlFinalizer
|
||||
{
|
||||
if (! $AST instanceof AST\SelectStatement) {
|
||||
throw new RuntimeException(self::class . ' is to be used on SelectStatements only');
|
||||
}
|
||||
|
||||
return new PreparedExecutorFinalizer(new FinalizedSelectExecutor($this->walkSelectStatement($AST)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ use function strtolower;
|
||||
*/
|
||||
class SchemaTool
|
||||
{
|
||||
private const KNOWN_COLUMN_OPTIONS = ['comment', 'unsigned', 'fixed', 'default'];
|
||||
private const KNOWN_COLUMN_OPTIONS = ['comment', 'unsigned', 'fixed', 'default', 'values'];
|
||||
|
||||
private readonly AbstractPlatform $platform;
|
||||
private readonly QuoteStrategy $quoteStrategy;
|
||||
@@ -421,18 +421,12 @@ class SchemaTool
|
||||
*/
|
||||
private function gatherColumns(ClassMetadata $class, Table $table): void
|
||||
{
|
||||
$pkColumns = [];
|
||||
|
||||
foreach ($class->fieldMappings as $mapping) {
|
||||
if ($class->isInheritanceTypeSingleTable() && isset($mapping->inherited)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->gatherColumn($class, $mapping, $table);
|
||||
|
||||
if ($class->isIdentifier($mapping->fieldName)) {
|
||||
$pkColumns[] = $this->quoteStrategy->getColumnName($mapping->fieldName, $class, $this->platform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ use Exception;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use Stringable;
|
||||
use Throwable;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function array_chunk;
|
||||
@@ -119,7 +118,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
* Since all classes in a hierarchy must share the same identifier set,
|
||||
* we always take the root class name of the hierarchy.
|
||||
*
|
||||
* @psalm-var array<class-string, array<string, object>>
|
||||
* @var array<class-string, array<string, object>>
|
||||
*/
|
||||
private array $identityMap = [];
|
||||
|
||||
@@ -165,7 +164,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
* This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
|
||||
* Keys are object ids (spl_object_id).
|
||||
*
|
||||
* @psalm-var array<class-string, array<int, mixed>>
|
||||
* @var array<class-string, array<int, mixed>>
|
||||
*/
|
||||
private array $scheduledForSynchronization = [];
|
||||
|
||||
@@ -290,7 +289,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
/**
|
||||
* Map of Entity Class-Names and corresponding IDs that should eager loaded when requested.
|
||||
*
|
||||
* @psalm-var array<class-string, array<string, mixed>>
|
||||
* @var array<class-string, array<string, mixed>>
|
||||
*/
|
||||
private array $eagerLoadingEntities = [];
|
||||
|
||||
@@ -379,6 +378,8 @@ class UnitOfWork implements PropertyChangedListener
|
||||
$conn = $this->em->getConnection();
|
||||
$conn->beginTransaction();
|
||||
|
||||
$successful = false;
|
||||
|
||||
try {
|
||||
// Collection deletions (deletions of complete collections)
|
||||
foreach ($this->collectionDeletions as $collectionToDelete) {
|
||||
@@ -436,16 +437,18 @@ class UnitOfWork implements PropertyChangedListener
|
||||
if ($commitFailed) {
|
||||
throw new OptimisticLockException('Commit failed', null, $e ?? null);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->em->close();
|
||||
|
||||
if ($conn->isTransactionActive()) {
|
||||
$conn->rollBack();
|
||||
$successful = true;
|
||||
} finally {
|
||||
if (! $successful) {
|
||||
$this->em->close();
|
||||
|
||||
if ($conn->isTransactionActive()) {
|
||||
$conn->rollBack();
|
||||
}
|
||||
|
||||
$this->afterTransactionRolledBack();
|
||||
}
|
||||
|
||||
$this->afterTransactionRolledBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->afterTransactionComplete();
|
||||
@@ -2340,11 +2343,9 @@ class UnitOfWork implements PropertyChangedListener
|
||||
*
|
||||
* Internal note: Highly performance-sensitive method.
|
||||
*
|
||||
* @param string $className The name of the entity class.
|
||||
* @param mixed[] $data The data for the entity.
|
||||
* @param mixed[] $hints Any hints to account for during reconstitution/lookup of the entity.
|
||||
* @psalm-param class-string $className
|
||||
* @psalm-param array<string, mixed> $hints
|
||||
* @param class-string $className The name of the entity class.
|
||||
* @param mixed[] $data The data for the entity.
|
||||
* @param array<string, mixed> $hints Any hints to account for during reconstitution/lookup of the entity.
|
||||
*
|
||||
* @return object The managed entity instance.
|
||||
*
|
||||
@@ -2743,7 +2744,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
/**
|
||||
* Gets the identity map of the UnitOfWork.
|
||||
*
|
||||
* @psalm-return array<class-string, array<string, object>>
|
||||
* @return array<class-string, array<string, object>>
|
||||
*/
|
||||
public function getIdentityMap(): array
|
||||
{
|
||||
@@ -2824,9 +2825,8 @@ class UnitOfWork implements PropertyChangedListener
|
||||
* Tries to find an entity with the given identifier in the identity map of
|
||||
* this UnitOfWork.
|
||||
*
|
||||
* @param mixed $id The entity identifier to look for.
|
||||
* @param string $rootClassName The name of the root class of the mapped entity hierarchy.
|
||||
* @psalm-param class-string $rootClassName
|
||||
* @param mixed $id The entity identifier to look for.
|
||||
* @param class-string $rootClassName The name of the root class of the mapped entity hierarchy.
|
||||
*
|
||||
* @return object|false Returns the entity with the specified identifier if it exists in
|
||||
* this UnitOfWork, FALSE otherwise.
|
||||
@@ -2870,7 +2870,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
/**
|
||||
* Gets the EntityPersister for an Entity.
|
||||
*
|
||||
* @psalm-param class-string $entityName
|
||||
* @param class-string $entityName The name of the Entity.
|
||||
*/
|
||||
public function getEntityPersister(string $entityName): EntityPersister
|
||||
{
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Performance;
|
||||
|
||||
use Doctrine\DBAL\Cache\ArrayResult;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Driver\PDO\SQLite\Driver;
|
||||
use Doctrine\DBAL\Result;
|
||||
|
||||
final class ArrayResultFactory
|
||||
{
|
||||
public static function createFromArray(array $resultSet): Result
|
||||
{
|
||||
return new Result(new ArrayResult($resultSet), new Connection([], new Driver()));
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace Doctrine\Performance;
|
||||
|
||||
use Doctrine\Common\EventManager;
|
||||
use Doctrine\DBAL\Cache\ArrayResult;
|
||||
use Doctrine\DBAL\Cache\QueryCacheProfile;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Driver\PDO\SQLite\Driver;
|
||||
@@ -17,6 +16,7 @@ use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
|
||||
use Doctrine\ORM\Proxy\ProxyFactory;
|
||||
use Doctrine\ORM\Tools\SchemaTool;
|
||||
use Doctrine\Tests\Mocks\ArrayResultFactory;
|
||||
use Doctrine\Tests\TestUtil;
|
||||
|
||||
use function array_map;
|
||||
@@ -67,7 +67,7 @@ final class EntityManagerFactory
|
||||
/** {@inheritDoc} */
|
||||
public function executeQuery(string $sql, array $params = [], $types = [], QueryCacheProfile|null $qcp = null): Result
|
||||
{
|
||||
return new Result(new ArrayResult([]), $this);
|
||||
return ArrayResultFactory::createWrapperResultFromArray([], $this);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ namespace Doctrine\Performance\Hydration;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\ORM\Internal\Hydration\ArrayHydrator;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\Performance\ArrayResultFactory;
|
||||
use Doctrine\Performance\EntityManagerFactory;
|
||||
use Doctrine\Tests\Mocks\ArrayResultFactory;
|
||||
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
|
||||
use Doctrine\Tests\Models\CMS\CmsUser;
|
||||
use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods;
|
||||
@@ -62,7 +62,7 @@ final class MixedQueryFetchJoinArrayHydrationPerformanceBench
|
||||
];
|
||||
}
|
||||
|
||||
$this->result = ArrayResultFactory::createFromArray($resultSet);
|
||||
$this->result = ArrayResultFactory::createWrapperResultFromArray($resultSet);
|
||||
$this->hydrator = new ArrayHydrator(EntityManagerFactory::getEntityManager([]));
|
||||
$this->rsm = new ResultSetMapping();
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ namespace Doctrine\Performance\Hydration;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\ORM\Internal\Hydration\ObjectHydrator;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\Performance\ArrayResultFactory;
|
||||
use Doctrine\Performance\EntityManagerFactory;
|
||||
use Doctrine\Tests\Mocks\ArrayResultFactory;
|
||||
use Doctrine\Tests\Models\CMS\CmsAddress;
|
||||
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
|
||||
use Doctrine\Tests\Models\CMS\CmsUser;
|
||||
@@ -49,7 +49,7 @@ final class MixedQueryFetchJoinFullObjectHydrationPerformanceBench
|
||||
];
|
||||
}
|
||||
|
||||
$this->result = ArrayResultFactory::createFromArray($resultSet);
|
||||
$this->result = ArrayResultFactory::createWrapperResultFromArray($resultSet);
|
||||
$this->hydrator = new ObjectHydrator(EntityManagerFactory::getEntityManager([]));
|
||||
$this->rsm = new ResultSetMapping();
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Performance\Hydration;
|
||||
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\ORM\Internal\Hydration\ObjectHydrator;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\Performance\EntityManagerFactory;
|
||||
use Doctrine\Tests\Mocks\ArrayResultFactory;
|
||||
use Doctrine\Tests\Models\CMS\CmsAddress;
|
||||
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
|
||||
use Doctrine\Tests\Models\CMS\CmsUser;
|
||||
use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods;
|
||||
|
||||
/** @BeforeMethods({"init"}) */
|
||||
final class MixedQueryFetchJoinPartialObjectHydrationPerformanceBench
|
||||
{
|
||||
private ObjectHydrator|null $hydrator = null;
|
||||
|
||||
private ResultSetMapping|null $rsm = null;
|
||||
|
||||
private Result|null $result = null;
|
||||
|
||||
public function init(): void
|
||||
{
|
||||
$resultSet = [
|
||||
[
|
||||
'u__id' => '1',
|
||||
'u__status' => 'developer',
|
||||
'u__username' => 'romanb',
|
||||
'u__name' => 'Roman',
|
||||
'sclr0' => 'ROMANB',
|
||||
'p__phonenumber' => '42',
|
||||
'a__id' => '1',
|
||||
],
|
||||
[
|
||||
'u__id' => '1',
|
||||
'u__status' => 'developer',
|
||||
'u__username' => 'romanb',
|
||||
'u__name' => 'Roman',
|
||||
'sclr0' => 'ROMANB',
|
||||
'p__phonenumber' => '43',
|
||||
'a__id' => '1',
|
||||
],
|
||||
[
|
||||
'u__id' => '2',
|
||||
'u__status' => 'developer',
|
||||
'u__username' => 'romanb',
|
||||
'u__name' => 'Roman',
|
||||
'sclr0' => 'JWAGE',
|
||||
'p__phonenumber' => '91',
|
||||
'a__id' => '1',
|
||||
],
|
||||
];
|
||||
|
||||
for ($i = 4; $i < 2000; ++$i) {
|
||||
$resultSet[] = [
|
||||
'u__id' => $i,
|
||||
'u__status' => 'developer',
|
||||
'u__username' => 'jwage',
|
||||
'u__name' => 'Jonathan',
|
||||
'sclr0' => 'JWAGE' . $i,
|
||||
'p__phonenumber' => '91',
|
||||
'a__id' => '1',
|
||||
];
|
||||
}
|
||||
|
||||
$this->result = ArrayResultFactory::createWrapperResultFromArray($resultSet);
|
||||
$this->hydrator = new ObjectHydrator(EntityManagerFactory::getEntityManager([]));
|
||||
$this->rsm = new ResultSetMapping();
|
||||
|
||||
$this->rsm->addEntityResult(CmsUser::class, 'u');
|
||||
$this->rsm->addJoinedEntityResult(CmsPhonenumber::class, 'p', 'u', 'phonenumbers');
|
||||
$this->rsm->addFieldResult('u', 'u__id', 'id');
|
||||
$this->rsm->addFieldResult('u', 'u__status', 'status');
|
||||
$this->rsm->addFieldResult('u', 'u__username', 'username');
|
||||
$this->rsm->addFieldResult('u', 'u__name', 'name');
|
||||
$this->rsm->addScalarResult('sclr0', 'nameUpper');
|
||||
$this->rsm->addFieldResult('p', 'p__phonenumber', 'phonenumber');
|
||||
$this->rsm->addJoinedEntityResult(CmsAddress::class, 'a', 'u', 'address');
|
||||
$this->rsm->addFieldResult('a', 'a__id', 'id');
|
||||
}
|
||||
|
||||
public function benchHydration(): void
|
||||
{
|
||||
$this->hydrator->hydrateAll($this->result, $this->rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]);
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@ namespace Doctrine\Performance\Hydration;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\ORM\Internal\Hydration\ArrayHydrator;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\Performance\ArrayResultFactory;
|
||||
use Doctrine\Performance\EntityManagerFactory;
|
||||
use Doctrine\Tests\Mocks\ArrayResultFactory;
|
||||
use Doctrine\Tests\Models\CMS\CmsUser;
|
||||
use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods;
|
||||
|
||||
@@ -53,7 +53,7 @@ final class SimpleQueryArrayHydrationPerformanceBench
|
||||
];
|
||||
}
|
||||
|
||||
$this->result = ArrayResultFactory::createFromArray($resultSet);
|
||||
$this->result = ArrayResultFactory::createWrapperResultFromArray($resultSet);
|
||||
$this->hydrator = new ArrayHydrator(EntityManagerFactory::getEntityManager([]));
|
||||
$this->rsm = new ResultSetMapping();
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ namespace Doctrine\Performance\Hydration;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\ORM\Internal\Hydration\ObjectHydrator;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\Performance\ArrayResultFactory;
|
||||
use Doctrine\Performance\EntityManagerFactory;
|
||||
use Doctrine\Tests\Mocks\ArrayResultFactory;
|
||||
use Doctrine\Tests\Models\CMS\CmsAddress;
|
||||
use Doctrine\Tests\Models\CMS\CmsUser;
|
||||
use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods;
|
||||
@@ -44,7 +44,7 @@ final class SimpleQueryFullObjectHydrationPerformanceBench
|
||||
];
|
||||
}
|
||||
|
||||
$this->result = ArrayResultFactory::createFromArray($resultSet);
|
||||
$this->result = ArrayResultFactory::createWrapperResultFromArray($resultSet);
|
||||
$this->hydrator = new ObjectHydrator(EntityManagerFactory::getEntityManager([]));
|
||||
$this->rsm = new ResultSetMapping();
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Performance\Hydration;
|
||||
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\ORM\Internal\Hydration\ObjectHydrator;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\Performance\EntityManagerFactory;
|
||||
use Doctrine\Tests\Mocks\ArrayResultFactory;
|
||||
use Doctrine\Tests\Models\CMS\CmsAddress;
|
||||
use Doctrine\Tests\Models\CMS\CmsUser;
|
||||
use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods;
|
||||
|
||||
/** @BeforeMethods({"init"}) */
|
||||
final class SimpleQueryPartialObjectHydrationPerformanceBench
|
||||
{
|
||||
private ObjectHydrator|null $hydrator = null;
|
||||
|
||||
private ResultSetMapping|null $rsm = null;
|
||||
|
||||
private Result|null $result = null;
|
||||
|
||||
public function init(): void
|
||||
{
|
||||
$resultSet = [
|
||||
[
|
||||
'u__id' => '1',
|
||||
'u__status' => 'developer',
|
||||
'u__username' => 'romanb',
|
||||
'u__name' => 'Roman',
|
||||
'a__id' => '1',
|
||||
],
|
||||
[
|
||||
'u__id' => '1',
|
||||
'u__status' => 'developer',
|
||||
'u__username' => 'romanb',
|
||||
'u__name' => 'Roman',
|
||||
'a__id' => '1',
|
||||
],
|
||||
[
|
||||
'u__id' => '2',
|
||||
'u__status' => 'developer',
|
||||
'u__username' => 'romanb',
|
||||
'u__name' => 'Roman',
|
||||
'a__id' => '1',
|
||||
],
|
||||
];
|
||||
|
||||
for ($i = 4; $i < 10000; ++$i) {
|
||||
$resultSet[] = [
|
||||
'u__id' => $i,
|
||||
'u__status' => 'developer',
|
||||
'u__username' => 'jwage',
|
||||
'u__name' => 'Jonathan',
|
||||
'a__id' => '1',
|
||||
];
|
||||
}
|
||||
|
||||
$this->result = ArrayResultFactory::createWrapperResultFromArray($resultSet);
|
||||
$this->hydrator = new ObjectHydrator(EntityManagerFactory::getEntityManager([]));
|
||||
$this->rsm = new ResultSetMapping();
|
||||
|
||||
$this->rsm->addEntityResult(CmsUser::class, 'u');
|
||||
$this->rsm->addFieldResult('u', 'u__id', 'id');
|
||||
$this->rsm->addFieldResult('u', 'u__status', 'status');
|
||||
$this->rsm->addFieldResult('u', 'u__username', 'username');
|
||||
$this->rsm->addFieldResult('u', 'u__name', 'name');
|
||||
$this->rsm->addJoinedEntityResult(CmsAddress::class, 'a', 'u', 'address');
|
||||
$this->rsm->addFieldResult('a', 'a__id', 'id');
|
||||
}
|
||||
|
||||
public function benchHydration(): void
|
||||
{
|
||||
$this->hydrator->hydrateAll($this->result, $this->rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]);
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@ namespace Doctrine\Performance\Hydration;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\ORM\Internal\Hydration\ScalarHydrator;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\Performance\ArrayResultFactory;
|
||||
use Doctrine\Performance\EntityManagerFactory;
|
||||
use Doctrine\Tests\Mocks\ArrayResultFactory;
|
||||
use Doctrine\Tests\Models\CMS\CmsUser;
|
||||
use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods;
|
||||
|
||||
@@ -53,7 +53,7 @@ final class SimpleQueryScalarHydrationPerformanceBench
|
||||
];
|
||||
}
|
||||
|
||||
$this->result = ArrayResultFactory::createFromArray($resultSet);
|
||||
$this->result = ArrayResultFactory::createWrapperResultFromArray($resultSet);
|
||||
$this->hydrator = new ScalarHydrator(EntityManagerFactory::getEntityManager([]));
|
||||
$this->rsm = new ResultSetMapping();
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
class MetadataGenerator
|
||||
{
|
||||
/**
|
||||
* @psalm-param class-string<T> $entityName
|
||||
* @param class-string<T> $entityName
|
||||
*
|
||||
* @psalm-return ClassMetadata<T>
|
||||
* @return ClassMetadata<T>
|
||||
*/
|
||||
public function createMetadata(string $entityName): ClassMetadata
|
||||
{
|
||||
|
||||
@@ -15,15 +15,15 @@ use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
*/
|
||||
abstract class GetMetadata
|
||||
{
|
||||
/** @psalm-param class-string|object $class */
|
||||
/** @param class-string|object $class */
|
||||
abstract public function getEntityManager(string|object $class): EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string<TObject> $class
|
||||
* @param class-string<TObject> $class
|
||||
*
|
||||
* @psalm-return ClassMetadata<TObject>
|
||||
* @return ClassMetadata<TObject>
|
||||
*
|
||||
* @psalm-template TObject of object
|
||||
* @template TObject of object
|
||||
*/
|
||||
public function __invoke(string $class): ClassMetadata
|
||||
{
|
||||
|
||||
42
tests/Tests/Mocks/ArrayResultFactory.php
Normal file
42
tests/Tests/Mocks/ArrayResultFactory.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Mocks;
|
||||
|
||||
use Doctrine\DBAL\Cache\ArrayResult;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Driver\PDO\SQLite\Driver;
|
||||
use Doctrine\DBAL\Result;
|
||||
use ReflectionMethod;
|
||||
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_values;
|
||||
|
||||
final class ArrayResultFactory
|
||||
{
|
||||
/** @param list<array<string, mixed>> $resultSet */
|
||||
public static function createDriverResultFromArray(array $resultSet): ArrayResult
|
||||
{
|
||||
if ((new ReflectionMethod(ArrayResult::class, '__construct'))->getNumberOfRequiredParameters() < 2) {
|
||||
// DBAL < 4.2
|
||||
return new ArrayResult($resultSet);
|
||||
}
|
||||
|
||||
// DBAL 4.2+
|
||||
return new ArrayResult(
|
||||
array_keys($resultSet[0] ?? []),
|
||||
array_map(array_values(...), $resultSet),
|
||||
);
|
||||
}
|
||||
|
||||
/** @param list<array<string, mixed>> $resultSet */
|
||||
public static function createWrapperResultFromArray(array $resultSet, Connection|null $connection = null): Result
|
||||
{
|
||||
return new Result(
|
||||
self::createDriverResultFromArray($resultSet),
|
||||
$connection ?? new Connection([], new Driver()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,14 @@ namespace Doctrine\Tests\Mocks;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\ORM\Query\AST;
|
||||
use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
|
||||
use Doctrine\ORM\Query\SqlWalker;
|
||||
use Doctrine\ORM\Query\Exec\PreparedExecutorFinalizer;
|
||||
use Doctrine\ORM\Query\Exec\SqlFinalizer;
|
||||
use Doctrine\ORM\Query\SqlOutputWalker;
|
||||
|
||||
/**
|
||||
* SqlWalker implementation that does not produce SQL.
|
||||
*/
|
||||
final class NullSqlWalker extends SqlWalker
|
||||
final class NullSqlWalker extends SqlOutputWalker
|
||||
{
|
||||
public function walkSelectStatement(AST\SelectStatement $selectStatement): string
|
||||
{
|
||||
@@ -29,13 +31,15 @@ final class NullSqlWalker extends SqlWalker
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getExecutor(AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement $statement): AbstractSqlExecutor
|
||||
public function getFinalizer(AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement $statement): SqlFinalizer
|
||||
{
|
||||
return new class extends AbstractSqlExecutor {
|
||||
public function execute(Connection $conn, array $params, array $types): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
return new PreparedExecutorFinalizer(
|
||||
new class extends AbstractSqlExecutor {
|
||||
public function execute(Connection $conn, array $params, array $types): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Doctrine\Tests\Models\CMS;
|
||||
|
||||
class CmsAddressDTO
|
||||
{
|
||||
public function __construct(public string|null $country = null, public string|null $city = null, public string|null $zip = null)
|
||||
public function __construct(public string|null $country = null, public string|null $city = null, public string|null $zip = null, public CmsAddressDTO|string|null $address = null)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
16
tests/Tests/Models/CMS/CmsAddressDTONamedArgs.php
Normal file
16
tests/Tests/Models/CMS/CmsAddressDTONamedArgs.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\CMS;
|
||||
|
||||
class CmsAddressDTONamedArgs
|
||||
{
|
||||
public function __construct(
|
||||
public string|null $country = null,
|
||||
public string|null $city = null,
|
||||
public string|null $zip = null,
|
||||
public CmsAddressDTO|string|null $address = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ namespace Doctrine\Tests\Models\CMS;
|
||||
|
||||
class CmsUserDTO
|
||||
{
|
||||
public function __construct(public string|null $name = null, public string|null $email = null, public string|null $address = null, public int|null $phonenumbers = null)
|
||||
public function __construct(public string|null $name = null, public string|null $email = null, public CmsAddressDTO|string|null $address = null, public int|null $phonenumbers = null)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
18
tests/Tests/Models/CMS/CmsUserDTONamedArgs.php
Normal file
18
tests/Tests/Models/CMS/CmsUserDTONamedArgs.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\CMS;
|
||||
|
||||
class CmsUserDTONamedArgs
|
||||
{
|
||||
public function __construct(
|
||||
public string|null $name = null,
|
||||
public string|null $email = null,
|
||||
public string|null $address = null,
|
||||
public int|null $phonenumbers = null,
|
||||
public CmsAddressDTO|null $addressDto = null,
|
||||
public CmsAddressDTONamedArgs|null $addressDtoNamedArgs = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
21
tests/Tests/Models/CMS/CmsUserDTOVariadicArg.php
Normal file
21
tests/Tests/Models/CMS/CmsUserDTOVariadicArg.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\CMS;
|
||||
|
||||
class CmsUserDTOVariadicArg
|
||||
{
|
||||
public string|null $name = null;
|
||||
public string|null $email = null;
|
||||
public string|null $address = null;
|
||||
public int|null $phonenumbers = null;
|
||||
|
||||
public function __construct(...$args)
|
||||
{
|
||||
$this->name = $args['name'] ?? null;
|
||||
$this->email = $args['email'] ?? null;
|
||||
$this->phonenumbers = $args['phonenumbers'] ?? null;
|
||||
$this->address = $args['address'] ?? null;
|
||||
}
|
||||
}
|
||||
17
tests/Tests/Models/DDC6573/DDC6573Currency.php
Normal file
17
tests/Tests/Models/DDC6573/DDC6573Currency.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\DDC6573;
|
||||
|
||||
final class DDC6573Currency
|
||||
{
|
||||
public function __construct(private readonly string $code)
|
||||
{
|
||||
}
|
||||
|
||||
public function getCode(): string
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
}
|
||||
44
tests/Tests/Models/DDC6573/DDC6573Item.php
Normal file
44
tests/Tests/Models/DDC6573/DDC6573Item.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\DDC6573;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\GeneratedValue;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
use Doctrine\ORM\Mapping\Table;
|
||||
|
||||
#[Entity]
|
||||
#[Table(name: 'ddc6573_items')]
|
||||
class DDC6573Item
|
||||
{
|
||||
/** @var int */
|
||||
#[Id]
|
||||
#[Column(type: Types::INTEGER)]
|
||||
#[GeneratedValue(strategy: 'AUTO')]
|
||||
public $id;
|
||||
|
||||
#[Column(type: Types::STRING)]
|
||||
public string $name;
|
||||
|
||||
#[Column(type: Types::INTEGER)]
|
||||
public int $priceAmount;
|
||||
|
||||
#[Column(type: Types::STRING, length: 3)]
|
||||
public string $priceCurrency;
|
||||
|
||||
public function __construct(string $name, DDC6573Money $price)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->priceAmount = $price->getAmount();
|
||||
$this->priceCurrency = $price->getCurrency()->getCode();
|
||||
}
|
||||
|
||||
public function getPrice(): DDC6573Money
|
||||
{
|
||||
return new DDC6573Money($this->priceAmount, new DDC6573Currency($this->priceCurrency));
|
||||
}
|
||||
}
|
||||
24
tests/Tests/Models/DDC6573/DDC6573Money.php
Normal file
24
tests/Tests/Models/DDC6573/DDC6573Money.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\DDC6573;
|
||||
|
||||
final class DDC6573Money
|
||||
{
|
||||
public function __construct(
|
||||
private readonly int $amount,
|
||||
private readonly DDC6573Currency $currency,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getAmount(): int
|
||||
{
|
||||
return $this->amount;
|
||||
}
|
||||
|
||||
public function getCurrency(): DDC6573Currency
|
||||
{
|
||||
return $this->currency;
|
||||
}
|
||||
}
|
||||
25
tests/Tests/Models/Enums/CardNativeEnum.php
Normal file
25
tests/Tests/Models/Enums/CardNativeEnum.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\Enums;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\GeneratedValue;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
|
||||
#[Entity]
|
||||
class CardNativeEnum
|
||||
{
|
||||
/** @var int|null */
|
||||
#[Id]
|
||||
#[GeneratedValue]
|
||||
#[Column(type: Types::INTEGER)]
|
||||
public $id;
|
||||
|
||||
/** @var Suit */
|
||||
#[Column(type: Types::ENUM, enumType: Suit::class, options: ['values' => ['H', 'D', 'C', 'S', 'Z']])]
|
||||
public $suit;
|
||||
}
|
||||
23
tests/Tests/Models/Enums/TypedCardNativeEnum.php
Normal file
23
tests/Tests/Models/Enums/TypedCardNativeEnum.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\Enums;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\GeneratedValue;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
|
||||
#[Entity]
|
||||
class TypedCardNativeEnum
|
||||
{
|
||||
#[Id]
|
||||
#[GeneratedValue]
|
||||
#[Column]
|
||||
public int $id;
|
||||
|
||||
#[Column(type: Types::ENUM)]
|
||||
public Suit $suit;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace Doctrine\Tests\ORM;
|
||||
|
||||
use Doctrine\Common\EventManager;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Driver;
|
||||
use Doctrine\ORM\Configuration;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
@@ -19,7 +20,9 @@ use Doctrine\ORM\UnitOfWork;
|
||||
use Doctrine\Tests\Mocks\EntityManagerMock;
|
||||
use Doctrine\Tests\Models\CMS\CmsUser;
|
||||
use Doctrine\Tests\OrmTestCase;
|
||||
use Exception;
|
||||
use Generator;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use ReflectionProperty;
|
||||
@@ -207,4 +210,59 @@ class EntityManagerTest extends OrmTestCase
|
||||
$em->resetLazyObject();
|
||||
$this->assertTrue($em->isOpen());
|
||||
}
|
||||
|
||||
public function testItPreservesTheOriginalExceptionOnRollbackFailure(): void
|
||||
{
|
||||
$driver = $this->createMock(Driver::class);
|
||||
$driver->method('connect')
|
||||
->willReturn($this->createMock(Driver\Connection::class));
|
||||
|
||||
$entityManager = new EntityManagerMock(new class ([], $driver) extends Connection {
|
||||
public function rollBack(): void
|
||||
{
|
||||
throw new Exception('Rollback exception');
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
$entityManager->wrapInTransaction(static function (): void {
|
||||
throw new Exception('Original exception');
|
||||
});
|
||||
self::fail('Exception expected');
|
||||
} catch (Exception $e) {
|
||||
self::assertSame('Rollback exception', $e->getMessage());
|
||||
self::assertNotNull($e->getPrevious());
|
||||
self::assertSame('Original exception', $e->getPrevious()->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function testItDoesNotAttemptToRollbackIfNoTransactionIsActive(): void
|
||||
{
|
||||
$driver = $this->createMock(Driver::class);
|
||||
$driver->method('connect')
|
||||
->willReturn($this->createMock(Driver\Connection::class));
|
||||
|
||||
$entityManager = new EntityManagerMock(
|
||||
new class ([], $driver) extends Connection {
|
||||
public function commit(): void
|
||||
{
|
||||
throw new Exception('Commit exception that happens after doing the actual commit');
|
||||
}
|
||||
|
||||
public function rollBack(): void
|
||||
{
|
||||
Assert::fail('Should not attempt to rollback if no transaction is active');
|
||||
}
|
||||
|
||||
public function isTransactionActive(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
$this->expectExceptionMessage('Commit exception');
|
||||
$entityManager->wrapInTransaction(static function (): void {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ abstract class DatabaseDriverTestCase extends OrmFunctionalTestCase
|
||||
/**
|
||||
* @param string[] $classNames
|
||||
*
|
||||
* @psalm-return array<class-string, ClassMetadata>
|
||||
* @return array<class-string, ClassMetadata>
|
||||
*/
|
||||
protected function extractClassMetadata(array $classNames): array
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional;
|
||||
|
||||
use Doctrine\DBAL\Types\EnumType;
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
|
||||
@@ -13,6 +14,7 @@ use Doctrine\ORM\Tools\SchemaTool;
|
||||
use Doctrine\Tests\Models\DataTransferObjects\DtoWithArrayOfEnums;
|
||||
use Doctrine\Tests\Models\DataTransferObjects\DtoWithEnum;
|
||||
use Doctrine\Tests\Models\Enums\Card;
|
||||
use Doctrine\Tests\Models\Enums\CardNativeEnum;
|
||||
use Doctrine\Tests\Models\Enums\CardWithDefault;
|
||||
use Doctrine\Tests\Models\Enums\CardWithNullable;
|
||||
use Doctrine\Tests\Models\Enums\Product;
|
||||
@@ -20,10 +22,12 @@ use Doctrine\Tests\Models\Enums\Quantity;
|
||||
use Doctrine\Tests\Models\Enums\Scale;
|
||||
use Doctrine\Tests\Models\Enums\Suit;
|
||||
use Doctrine\Tests\Models\Enums\TypedCard;
|
||||
use Doctrine\Tests\Models\Enums\TypedCardNativeEnum;
|
||||
use Doctrine\Tests\Models\Enums\Unit;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
|
||||
use function class_exists;
|
||||
use function dirname;
|
||||
use function sprintf;
|
||||
use function uniqid;
|
||||
@@ -55,7 +59,7 @@ class EnumTest extends OrmFunctionalTestCase
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
$fetchedCard = $this->_em->find(Card::class, $card->id);
|
||||
$fetchedCard = $this->_em->find($cardClass, $card->id);
|
||||
|
||||
$this->assertInstanceOf(Suit::class, $fetchedCard->suit);
|
||||
$this->assertEquals(Suit::Clubs, $fetchedCard->suit);
|
||||
@@ -417,6 +421,10 @@ class EnumTest extends OrmFunctionalTestCase
|
||||
#[DataProvider('provideCardClasses')]
|
||||
public function testEnumWithNonMatchingDatabaseValueThrowsException(string $cardClass): void
|
||||
{
|
||||
if ($cardClass === TypedCardNativeEnum::class) {
|
||||
self::markTestSkipped('MySQL won\'t allow us to insert invalid values in this case.');
|
||||
}
|
||||
|
||||
$this->setUpEntitySchema([$cardClass]);
|
||||
|
||||
$card = new $cardClass();
|
||||
@@ -429,7 +437,7 @@ class EnumTest extends OrmFunctionalTestCase
|
||||
$metadata = $this->_em->getClassMetadata($cardClass);
|
||||
$this->_em->getConnection()->update(
|
||||
$metadata->table['name'],
|
||||
[$metadata->fieldMappings['suit']->columnName => 'invalid'],
|
||||
[$metadata->fieldMappings['suit']->columnName => 'Z'],
|
||||
[$metadata->fieldMappings['id']->columnName => $card->id],
|
||||
);
|
||||
|
||||
@@ -437,7 +445,7 @@ class EnumTest extends OrmFunctionalTestCase
|
||||
$this->expectExceptionMessage(sprintf(
|
||||
<<<'EXCEPTION'
|
||||
Context: Trying to hydrate enum property "%s::$suit"
|
||||
Problem: Case "invalid" is not listed in enum "Doctrine\Tests\Models\Enums\Suit"
|
||||
Problem: Case "Z" is not listed in enum "Doctrine\Tests\Models\Enums\Suit"
|
||||
Solution: Either add the case to the enum type or migrate the database column to use another case of the enum
|
||||
EXCEPTION
|
||||
,
|
||||
@@ -447,13 +455,16 @@ EXCEPTION
|
||||
$this->_em->find($cardClass, $card->id);
|
||||
}
|
||||
|
||||
/** @return array<string, array{class-string}> */
|
||||
public static function provideCardClasses(): array
|
||||
/** @return iterable<string, array{class-string}> */
|
||||
public static function provideCardClasses(): iterable
|
||||
{
|
||||
return [
|
||||
Card::class => [Card::class],
|
||||
TypedCard::class => [TypedCard::class],
|
||||
];
|
||||
yield Card::class => [Card::class];
|
||||
yield TypedCard::class => [TypedCard::class];
|
||||
|
||||
if (class_exists(EnumType::class)) {
|
||||
yield CardNativeEnum::class => [CardNativeEnum::class];
|
||||
yield TypedCardNativeEnum::class => [TypedCardNativeEnum::class];
|
||||
}
|
||||
}
|
||||
|
||||
public function testItAllowsReadingAttributes(): void
|
||||
|
||||
@@ -4,19 +4,25 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional;
|
||||
|
||||
use Doctrine\ORM\Exception\DuplicateFieldException;
|
||||
use Doctrine\ORM\Exception\NoMatchingPropertyException;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\QueryException;
|
||||
use Doctrine\Tests\Models\CMS\CmsAddress;
|
||||
use Doctrine\Tests\Models\CMS\CmsAddressDTO;
|
||||
use Doctrine\Tests\Models\CMS\CmsAddressDTONamedArgs;
|
||||
use Doctrine\Tests\Models\CMS\CmsEmail;
|
||||
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
|
||||
use Doctrine\Tests\Models\CMS\CmsUser;
|
||||
use Doctrine\Tests\Models\CMS\CmsUserDTO;
|
||||
use Doctrine\Tests\Models\CMS\CmsUserDTONamedArgs;
|
||||
use Doctrine\Tests\Models\CMS\CmsUserDTOVariadicArg;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
use function count;
|
||||
use function sprintf;
|
||||
|
||||
#[Group('DDC-1574')]
|
||||
class NewOperatorTest extends OrmFunctionalTestCase
|
||||
@@ -1013,6 +1019,394 @@ class NewOperatorTest extends OrmFunctionalTestCase
|
||||
$dql = 'SELECT new Doctrine\Tests\ORM\Functional\ClassWithPrivateConstructor(u.name) FROM Doctrine\Tests\Models\CMS\CmsUser u';
|
||||
$this->_em->createQuery($dql)->getResult();
|
||||
}
|
||||
|
||||
public function testShouldSupportNestedNewOperators(): void
|
||||
{
|
||||
$dql = '
|
||||
SELECT
|
||||
new CmsUserDTO(
|
||||
u.name,
|
||||
e.email,
|
||||
new CmsAddressDTO(
|
||||
a.country,
|
||||
a.city,
|
||||
a.zip,
|
||||
new CmsAddressDTO(
|
||||
a.country,
|
||||
a.city,
|
||||
a.zip
|
||||
)
|
||||
)
|
||||
) as user,
|
||||
u.status,
|
||||
u.username as cmsUserUsername
|
||||
FROM
|
||||
Doctrine\Tests\Models\CMS\CmsUser u
|
||||
JOIN
|
||||
u.email e
|
||||
JOIN
|
||||
u.address a
|
||||
ORDER BY
|
||||
u.name';
|
||||
|
||||
$query = $this->getEntityManager()->createQuery($dql);
|
||||
$result = $query->getResult();
|
||||
|
||||
self::assertCount(3, $result);
|
||||
|
||||
self::assertInstanceOf(CmsUserDTO::class, $result[0]['user']);
|
||||
self::assertInstanceOf(CmsUserDTO::class, $result[1]['user']);
|
||||
self::assertInstanceOf(CmsUserDTO::class, $result[2]['user']);
|
||||
|
||||
self::assertInstanceOf(CmsAddressDTO::class, $result[0]['user']->address);
|
||||
self::assertInstanceOf(CmsAddressDTO::class, $result[1]['user']->address);
|
||||
self::assertInstanceOf(CmsAddressDTO::class, $result[2]['user']->address);
|
||||
|
||||
self::assertSame($this->fixtures[0]->name, $result[0]['user']->name);
|
||||
self::assertSame($this->fixtures[1]->name, $result[1]['user']->name);
|
||||
self::assertSame($this->fixtures[2]->name, $result[2]['user']->name);
|
||||
|
||||
self::assertSame($this->fixtures[0]->email->email, $result[0]['user']->email);
|
||||
self::assertSame($this->fixtures[1]->email->email, $result[1]['user']->email);
|
||||
self::assertSame($this->fixtures[2]->email->email, $result[2]['user']->email);
|
||||
|
||||
self::assertSame($this->fixtures[0]->address->city, $result[0]['user']->address->city);
|
||||
self::assertSame($this->fixtures[1]->address->city, $result[1]['user']->address->city);
|
||||
self::assertSame($this->fixtures[2]->address->city, $result[2]['user']->address->city);
|
||||
|
||||
self::assertSame($this->fixtures[0]->address->country, $result[0]['user']->address->country);
|
||||
self::assertSame($this->fixtures[1]->address->country, $result[1]['user']->address->country);
|
||||
self::assertSame($this->fixtures[2]->address->country, $result[2]['user']->address->country);
|
||||
|
||||
self::assertSame($this->fixtures[0]->status, $result[0]['status']);
|
||||
self::assertSame($this->fixtures[1]->status, $result[1]['status']);
|
||||
self::assertSame($this->fixtures[2]->status, $result[2]['status']);
|
||||
|
||||
self::assertSame($this->fixtures[0]->username, $result[0]['cmsUserUsername']);
|
||||
self::assertSame($this->fixtures[1]->username, $result[1]['cmsUserUsername']);
|
||||
self::assertSame($this->fixtures[2]->username, $result[2]['cmsUserUsername']);
|
||||
}
|
||||
|
||||
public function testNamedArguments(): void
|
||||
{
|
||||
$dql = <<<'SQL'
|
||||
SELECT
|
||||
new named CmsUserDTONamedArgs(
|
||||
e.email,
|
||||
u.name,
|
||||
CONCAT(a.country, ' ', a.city, ' ', a.zip) AS address
|
||||
) as user,
|
||||
u.status,
|
||||
u.username as cmsUserUsername
|
||||
FROM
|
||||
Doctrine\Tests\Models\CMS\CmsUser u
|
||||
JOIN
|
||||
u.email e
|
||||
JOIN
|
||||
u.address a
|
||||
ORDER BY
|
||||
u.name
|
||||
SQL;
|
||||
|
||||
$query = $this->getEntityManager()->createQuery($dql);
|
||||
$result = $query->getResult();
|
||||
|
||||
self::assertCount(3, $result);
|
||||
|
||||
self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[0]['user']);
|
||||
self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[1]['user']);
|
||||
self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[2]['user']);
|
||||
|
||||
self::assertSame($this->fixtures[0]->name, $result[0]['user']->name);
|
||||
self::assertSame($this->fixtures[1]->name, $result[1]['user']->name);
|
||||
self::assertSame($this->fixtures[2]->name, $result[2]['user']->name);
|
||||
|
||||
self::assertSame($this->fixtures[0]->email->email, $result[0]['user']->email);
|
||||
self::assertSame($this->fixtures[1]->email->email, $result[1]['user']->email);
|
||||
self::assertSame($this->fixtures[2]->email->email, $result[2]['user']->email);
|
||||
|
||||
self::assertSame(sprintf(
|
||||
'%s %s %s',
|
||||
$this->fixtures[0]->address->country,
|
||||
$this->fixtures[0]->address->city,
|
||||
$this->fixtures[0]->address->zip,
|
||||
), $result[0]['user']->address);
|
||||
self::assertSame(
|
||||
sprintf(
|
||||
'%s %s %s',
|
||||
$this->fixtures[1]->address->country,
|
||||
$this->fixtures[1]->address->city,
|
||||
$this->fixtures[1]->address->zip,
|
||||
),
|
||||
$result[1]['user']->address,
|
||||
);
|
||||
self::assertSame(
|
||||
sprintf(
|
||||
'%s %s %s',
|
||||
$this->fixtures[2]->address->country,
|
||||
$this->fixtures[2]->address->city,
|
||||
$this->fixtures[2]->address->zip,
|
||||
),
|
||||
$result[2]['user']->address,
|
||||
);
|
||||
|
||||
self::assertSame($this->fixtures[0]->status, $result[0]['status']);
|
||||
self::assertSame($this->fixtures[1]->status, $result[1]['status']);
|
||||
self::assertSame($this->fixtures[2]->status, $result[2]['status']);
|
||||
|
||||
self::assertSame($this->fixtures[0]->username, $result[0]['cmsUserUsername']);
|
||||
self::assertSame($this->fixtures[1]->username, $result[1]['cmsUserUsername']);
|
||||
self::assertSame($this->fixtures[2]->username, $result[2]['cmsUserUsername']);
|
||||
}
|
||||
|
||||
public function testVariadicArgument(): void
|
||||
{
|
||||
$dql = <<<'SQL'
|
||||
SELECT
|
||||
new named CmsUserDTOVariadicArg(
|
||||
CONCAT(a.country, ' ', a.city, ' ', a.zip) AS address,
|
||||
e.email,
|
||||
u.name
|
||||
) as user,
|
||||
u.status,
|
||||
u.username as cmsUserUsername
|
||||
FROM
|
||||
Doctrine\Tests\Models\CMS\CmsUser u
|
||||
JOIN
|
||||
u.email e
|
||||
JOIN
|
||||
u.address a
|
||||
ORDER BY
|
||||
u.name
|
||||
SQL;
|
||||
|
||||
$query = $this->getEntityManager()->createQuery($dql);
|
||||
$result = $query->getResult();
|
||||
|
||||
self::assertCount(3, $result);
|
||||
|
||||
self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[0]['user']);
|
||||
self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[1]['user']);
|
||||
self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[2]['user']);
|
||||
|
||||
self::assertSame($this->fixtures[0]->name, $result[0]['user']->name);
|
||||
self::assertSame($this->fixtures[1]->name, $result[1]['user']->name);
|
||||
self::assertSame($this->fixtures[2]->name, $result[2]['user']->name);
|
||||
|
||||
self::assertSame($this->fixtures[0]->email->email, $result[0]['user']->email);
|
||||
self::assertSame($this->fixtures[1]->email->email, $result[1]['user']->email);
|
||||
self::assertSame($this->fixtures[2]->email->email, $result[2]['user']->email);
|
||||
|
||||
self::assertSame(
|
||||
sprintf(
|
||||
'%s %s %s',
|
||||
$this->fixtures[0]->address->country,
|
||||
$this->fixtures[0]->address->city,
|
||||
$this->fixtures[0]->address->zip,
|
||||
),
|
||||
$result[0]['user']->address,
|
||||
);
|
||||
self::assertSame(
|
||||
sprintf(
|
||||
'%s %s %s',
|
||||
$this->fixtures[1]->address->country,
|
||||
$this->fixtures[1]->address->city,
|
||||
$this->fixtures[1]->address->zip,
|
||||
),
|
||||
$result[1]['user']->address,
|
||||
);
|
||||
self::assertSame(
|
||||
sprintf(
|
||||
'%s %s %s',
|
||||
$this->fixtures[2]->address->country,
|
||||
$this->fixtures[2]->address->city,
|
||||
$this->fixtures[2]->address->zip,
|
||||
),
|
||||
$result[2]['user']->address,
|
||||
);
|
||||
|
||||
self::assertSame($this->fixtures[0]->status, $result[0]['status']);
|
||||
self::assertSame($this->fixtures[1]->status, $result[1]['status']);
|
||||
self::assertSame($this->fixtures[2]->status, $result[2]['status']);
|
||||
|
||||
self::assertSame($this->fixtures[0]->username, $result[0]['cmsUserUsername']);
|
||||
self::assertSame($this->fixtures[1]->username, $result[1]['cmsUserUsername']);
|
||||
self::assertSame($this->fixtures[2]->username, $result[2]['cmsUserUsername']);
|
||||
}
|
||||
|
||||
public function testShouldSupportNestedNewOperatorsAndNamedArguments(): void
|
||||
{
|
||||
$dql = '
|
||||
SELECT
|
||||
new named CmsUserDTONamedArgs(
|
||||
e.email,
|
||||
u.name as name,
|
||||
new CmsAddressDTO(
|
||||
a.country,
|
||||
a.city,
|
||||
a.zip
|
||||
) as addressDto
|
||||
) as user,
|
||||
u.status,
|
||||
u.username as cmsUserUsername
|
||||
FROM
|
||||
Doctrine\Tests\Models\CMS\CmsUser u
|
||||
JOIN
|
||||
u.email e
|
||||
JOIN
|
||||
u.address a
|
||||
ORDER BY
|
||||
u.name';
|
||||
|
||||
$query = $this->getEntityManager()->createQuery($dql);
|
||||
$result = $query->getResult();
|
||||
|
||||
self::assertCount(3, $result);
|
||||
|
||||
self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[0]['user']);
|
||||
self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[1]['user']);
|
||||
self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[2]['user']);
|
||||
|
||||
self::assertNull($result[0]['user']->address);
|
||||
self::assertNull($result[1]['user']->address);
|
||||
self::assertNull($result[2]['user']->address);
|
||||
|
||||
self::assertInstanceOf(CmsAddressDTO::class, $result[0]['user']->addressDto);
|
||||
self::assertInstanceOf(CmsAddressDTO::class, $result[1]['user']->addressDto);
|
||||
self::assertInstanceOf(CmsAddressDTO::class, $result[2]['user']->addressDto);
|
||||
|
||||
self::assertSame($this->fixtures[0]->name, $result[0]['user']->name);
|
||||
self::assertSame($this->fixtures[1]->name, $result[1]['user']->name);
|
||||
self::assertSame($this->fixtures[2]->name, $result[2]['user']->name);
|
||||
|
||||
self::assertSame($this->fixtures[0]->email->email, $result[0]['user']->email);
|
||||
self::assertSame($this->fixtures[1]->email->email, $result[1]['user']->email);
|
||||
self::assertSame($this->fixtures[2]->email->email, $result[2]['user']->email);
|
||||
|
||||
self::assertSame($this->fixtures[0]->address->city, $result[0]['user']->addressDto->city);
|
||||
self::assertSame($this->fixtures[1]->address->city, $result[1]['user']->addressDto->city);
|
||||
self::assertSame($this->fixtures[2]->address->city, $result[2]['user']->addressDto->city);
|
||||
|
||||
self::assertSame($this->fixtures[0]->address->country, $result[0]['user']->addressDto->country);
|
||||
self::assertSame($this->fixtures[1]->address->country, $result[1]['user']->addressDto->country);
|
||||
self::assertSame($this->fixtures[2]->address->country, $result[2]['user']->addressDto->country);
|
||||
|
||||
self::assertSame($this->fixtures[0]->status, $result[0]['status']);
|
||||
self::assertSame($this->fixtures[1]->status, $result[1]['status']);
|
||||
self::assertSame($this->fixtures[2]->status, $result[2]['status']);
|
||||
|
||||
self::assertSame($this->fixtures[0]->username, $result[0]['cmsUserUsername']);
|
||||
self::assertSame($this->fixtures[1]->username, $result[1]['cmsUserUsername']);
|
||||
self::assertSame($this->fixtures[2]->username, $result[2]['cmsUserUsername']);
|
||||
}
|
||||
|
||||
public function testShouldSupportNestedNamedArguments(): void
|
||||
{
|
||||
$dql = '
|
||||
SELECT
|
||||
new named CmsUserDTONamedArgs(
|
||||
e.email,
|
||||
u.name as name,
|
||||
new named CmsAddressDTONamedArgs(
|
||||
a.zip,
|
||||
a.city,
|
||||
a.country
|
||||
) as addressDtoNamedArgs
|
||||
) as user,
|
||||
u.status,
|
||||
u.username as cmsUserUsername
|
||||
FROM
|
||||
Doctrine\Tests\Models\CMS\CmsUser u
|
||||
JOIN
|
||||
u.email e
|
||||
JOIN
|
||||
u.address a
|
||||
ORDER BY
|
||||
u.name';
|
||||
|
||||
$query = $this->getEntityManager()->createQuery($dql);
|
||||
$result = $query->getResult();
|
||||
|
||||
self::assertCount(3, $result);
|
||||
|
||||
self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[0]['user']);
|
||||
self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[1]['user']);
|
||||
self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[2]['user']);
|
||||
|
||||
self::assertNull($result[0]['user']->address);
|
||||
self::assertNull($result[1]['user']->address);
|
||||
self::assertNull($result[2]['user']->address);
|
||||
|
||||
self::assertNull($result[0]['user']->addressDto);
|
||||
self::assertNull($result[1]['user']->addressDto);
|
||||
self::assertNull($result[2]['user']->addressDto);
|
||||
|
||||
self::assertInstanceOf(CmsAddressDTONamedArgs::class, $result[0]['user']->addressDtoNamedArgs);
|
||||
self::assertInstanceOf(CmsAddressDTONamedArgs::class, $result[1]['user']->addressDtoNamedArgs);
|
||||
self::assertInstanceOf(CmsAddressDTONamedArgs::class, $result[2]['user']->addressDtoNamedArgs);
|
||||
|
||||
self::assertSame($this->fixtures[0]->name, $result[0]['user']->name);
|
||||
self::assertSame($this->fixtures[1]->name, $result[1]['user']->name);
|
||||
self::assertSame($this->fixtures[2]->name, $result[2]['user']->name);
|
||||
|
||||
self::assertSame($this->fixtures[0]->email->email, $result[0]['user']->email);
|
||||
self::assertSame($this->fixtures[1]->email->email, $result[1]['user']->email);
|
||||
self::assertSame($this->fixtures[2]->email->email, $result[2]['user']->email);
|
||||
|
||||
self::assertSame($this->fixtures[0]->address->city, $result[0]['user']->addressDtoNamedArgs->city);
|
||||
self::assertSame($this->fixtures[1]->address->city, $result[1]['user']->addressDtoNamedArgs->city);
|
||||
self::assertSame($this->fixtures[2]->address->city, $result[2]['user']->addressDtoNamedArgs->city);
|
||||
|
||||
self::assertSame($this->fixtures[0]->address->country, $result[0]['user']->addressDtoNamedArgs->country);
|
||||
self::assertSame($this->fixtures[1]->address->country, $result[1]['user']->addressDtoNamedArgs->country);
|
||||
self::assertSame($this->fixtures[2]->address->country, $result[2]['user']->addressDtoNamedArgs->country);
|
||||
|
||||
self::assertSame($this->fixtures[0]->status, $result[0]['status']);
|
||||
self::assertSame($this->fixtures[1]->status, $result[1]['status']);
|
||||
self::assertSame($this->fixtures[2]->status, $result[2]['status']);
|
||||
|
||||
self::assertSame($this->fixtures[0]->username, $result[0]['cmsUserUsername']);
|
||||
self::assertSame($this->fixtures[1]->username, $result[1]['cmsUserUsername']);
|
||||
self::assertSame($this->fixtures[2]->username, $result[2]['cmsUserUsername']);
|
||||
}
|
||||
|
||||
public function testExceptionIfTwoAliases(): void
|
||||
{
|
||||
$dql = '
|
||||
SELECT
|
||||
new named Doctrine\Tests\Models\CMS\CmsUserDTO(
|
||||
u.name,
|
||||
u.username AS name
|
||||
)
|
||||
FROM
|
||||
Doctrine\Tests\Models\CMS\CmsUser u';
|
||||
|
||||
$this->expectException(DuplicateFieldException::class);
|
||||
$this->expectExceptionMessage('Name "name" for "u.username AS name" already in use.');
|
||||
|
||||
$query = $this->_em->createQuery($dql);
|
||||
$result = $query->getResult();
|
||||
}
|
||||
|
||||
public function testExceptionIfFunctionHasNoAlias(): void
|
||||
{
|
||||
$dql = "
|
||||
SELECT
|
||||
new named Doctrine\Tests\Models\CMS\CmsUserDTO(
|
||||
u.name,
|
||||
CASE WHEN (e.email = 'email@test1.com') THEN 'TEST1' ELSE 'OTHER_TEST' END
|
||||
)
|
||||
FROM
|
||||
Doctrine\Tests\Models\CMS\CmsUser u
|
||||
JOIN
|
||||
u.email e";
|
||||
|
||||
$this->expectException(NoMatchingPropertyException::class);
|
||||
$this->expectExceptionMessage('Column name "CASE WHEN (e.email = \'email@test1.com\') THEN \'TEST1\' ELSE \'OTHER_TEST\' END" does not match any property name. Consider aliasing it to the name of an existing property.');
|
||||
|
||||
$query = $this->_em->createQuery($dql);
|
||||
$result = $query->getResult();
|
||||
}
|
||||
}
|
||||
|
||||
class ClassWithTooMuchArgs
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Doctrine\Tests\ORM\Functional;
|
||||
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\Tests\Models\ECommerce\ECommerceProduct;
|
||||
use Doctrine\Tests\Models\ECommerce\ECommerceShipping;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
@@ -78,6 +79,19 @@ class OneToOneUnidirectionalAssociationTest extends OrmFunctionalTestCase
|
||||
self::assertEquals(1, $product->getShipping()->getDays());
|
||||
}
|
||||
|
||||
public function testDoesNotLazyLoadObjectsIfConfigurationDoesNotAllowIt(): void
|
||||
{
|
||||
$this->createFixture();
|
||||
|
||||
$query = $this->_em->createQuery('select p from Doctrine\Tests\Models\ECommerce\ECommerceProduct p');
|
||||
$query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true);
|
||||
|
||||
$result = $query->getResult();
|
||||
$product = $result[0];
|
||||
|
||||
self::assertNull($product->getShipping());
|
||||
}
|
||||
|
||||
protected function createFixture(): void
|
||||
{
|
||||
$product = new ECommerceProduct();
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace Doctrine\Tests\ORM\Functional;
|
||||
|
||||
use Closure;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\Exec\FinalizedSelectExecutor;
|
||||
use Doctrine\ORM\Query\Exec\PreparedExecutorFinalizer;
|
||||
use Doctrine\ORM\Query\Exec\SingleSelectExecutor;
|
||||
use Doctrine\ORM\Query\ParserResult;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
@@ -32,7 +34,26 @@ class ParserResultSerializationTest extends OrmFunctionalTestCase
|
||||
|
||||
/** @param Closure(ParserResult): ParserResult $toSerializedAndBack */
|
||||
#[DataProvider('provideToSerializedAndBack')]
|
||||
public function testSerializeParserResult(Closure $toSerializedAndBack): void
|
||||
public function testSerializeParserResultForQueryWithSqlWalker(Closure $toSerializedAndBack): void
|
||||
{
|
||||
$query = $this->_em
|
||||
->createQuery('SELECT u FROM Doctrine\Tests\Models\Company\CompanyEmployee u WHERE u.name = :name');
|
||||
|
||||
// Use the (legacy) SqlWalker which directly puts an SqlExecutor instance into the parser result
|
||||
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, Query\SqlWalker::class);
|
||||
|
||||
$parserResult = self::parseQuery($query);
|
||||
$unserialized = $toSerializedAndBack($parserResult);
|
||||
|
||||
$this->assertInstanceOf(ParserResult::class, $unserialized);
|
||||
$this->assertInstanceOf(ResultSetMapping::class, $unserialized->getResultSetMapping());
|
||||
$this->assertEquals(['name' => [0]], $unserialized->getParameterMappings());
|
||||
$this->assertNotNull($unserialized->prepareSqlExecutor($query));
|
||||
}
|
||||
|
||||
/** @param Closure(ParserResult): ParserResult $toSerializedAndBack */
|
||||
#[DataProvider('provideToSerializedAndBack')]
|
||||
public function testSerializeParserResultForQueryWithSqlOutputWalker(Closure $toSerializedAndBack): void
|
||||
{
|
||||
$query = $this->_em
|
||||
->createQuery('SELECT u FROM Doctrine\Tests\Models\Company\CompanyEmployee u WHERE u.name = :name');
|
||||
@@ -43,7 +64,7 @@ class ParserResultSerializationTest extends OrmFunctionalTestCase
|
||||
$this->assertInstanceOf(ParserResult::class, $unserialized);
|
||||
$this->assertInstanceOf(ResultSetMapping::class, $unserialized->getResultSetMapping());
|
||||
$this->assertEquals(['name' => [0]], $unserialized->getParameterMappings());
|
||||
$this->assertInstanceOf(SingleSelectExecutor::class, $unserialized->getSqlExecutor());
|
||||
$this->assertNotNull($unserialized->prepareSqlExecutor($query));
|
||||
}
|
||||
|
||||
/** @return Generator<string, array{Closure(ParserResult): ParserResult}> */
|
||||
@@ -87,11 +108,12 @@ class ParserResultSerializationTest extends OrmFunctionalTestCase
|
||||
|
||||
public function testSymfony44ProvidedData(): void
|
||||
{
|
||||
$sqlExecutor = $this->createMock(SingleSelectExecutor::class);
|
||||
$sqlExecutor = new FinalizedSelectExecutor('test');
|
||||
$sqlFinalizer = new PreparedExecutorFinalizer($sqlExecutor);
|
||||
$resultSetMapping = $this->createMock(ResultSetMapping::class);
|
||||
|
||||
$parserResult = new ParserResult();
|
||||
$parserResult->setSqlExecutor($sqlExecutor);
|
||||
$parserResult->setSqlFinalizer($sqlFinalizer);
|
||||
$parserResult->setResultSetMapping($resultSetMapping);
|
||||
$parserResult->addParameterMapping('name', 0);
|
||||
|
||||
@@ -101,7 +123,7 @@ class ParserResultSerializationTest extends OrmFunctionalTestCase
|
||||
$this->assertInstanceOf(ParserResult::class, $unserialized);
|
||||
$this->assertInstanceOf(ResultSetMapping::class, $unserialized->getResultSetMapping());
|
||||
$this->assertEquals(['name' => [0]], $unserialized->getParameterMappings());
|
||||
$this->assertInstanceOf(SingleSelectExecutor::class, $unserialized->getSqlExecutor());
|
||||
$this->assertEquals($sqlExecutor, $unserialized->prepareSqlExecutor($this->createMock(Query::class)));
|
||||
}
|
||||
|
||||
private static function parseQuery(Query $query): ParserResult
|
||||
|
||||
@@ -136,6 +136,27 @@ class PostLoadEventTest extends OrmFunctionalTestCase
|
||||
$userProxy->getName();
|
||||
}
|
||||
|
||||
public function testLoadedProxyPartialShouldTriggerEvent(): void
|
||||
{
|
||||
$eventManager = $this->_em->getEventManager();
|
||||
|
||||
// Should not be invoked during getReference call
|
||||
$mockListener = $this->createMock(PostLoadListener::class);
|
||||
|
||||
// CmsUser (partially loaded), CmsAddress (inverse ToOne), 2 CmsPhonenumber
|
||||
$mockListener
|
||||
->expects(self::exactly(4))
|
||||
->method('postLoad')
|
||||
->will(self::returnValue(true));
|
||||
|
||||
$eventManager->addEventListener([Events::postLoad], $mockListener);
|
||||
|
||||
$query = $this->_em->createQuery('SELECT PARTIAL u.{id, name}, p FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN u.phonenumbers p WHERE u.id = :id');
|
||||
|
||||
$query->setParameter('id', $this->userId);
|
||||
$query->getResult();
|
||||
}
|
||||
|
||||
public function testLoadedProxyAssociationToOneShouldTriggerEvent(): void
|
||||
{
|
||||
$user = $this->_em->find(CmsUser::class, $this->userId);
|
||||
@@ -275,7 +296,7 @@ class PostLoadListenerCheckAssociationsArePopulated
|
||||
|
||||
class PostLoadListenerLoadEntityInEventHandler
|
||||
{
|
||||
/** @psalm-var array<class-string, int> */
|
||||
/** @var array<class-string, int> */
|
||||
private array $firedByClasses = [];
|
||||
|
||||
public function postLoad(PostLoadEventArgs $event): void
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user