mirror of
https://github.com/doctrine/orm.git
synced 2026-03-24 06:52:09 +01:00
Compare commits
239 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccfc97c32f | ||
|
|
d386b43be3 | ||
|
|
0970ce7072 | ||
|
|
624c56be72 | ||
|
|
fbc8e6741e | ||
|
|
7151db3cb8 | ||
|
|
ac24c11808 | ||
|
|
dd478d8662 | ||
|
|
0b3cd72609 | ||
|
|
85034699cb | ||
|
|
d98186e2c4 | ||
|
|
a0ed37954b | ||
|
|
4875f4c878 | ||
|
|
398ab0547a | ||
|
|
8f15337b03 | ||
|
|
a8632aca8f | ||
|
|
3dd3d38857 | ||
|
|
c6b3509aa9 | ||
|
|
a32578b7ea | ||
|
|
e585a92763 | ||
|
|
26f47cb8d3 | ||
|
|
ebb101009c | ||
|
|
f80ef66ffb | ||
|
|
85d78f8b0d | ||
|
|
c2886478e8 | ||
|
|
108fa30db2 | ||
|
|
393679a479 | ||
|
|
e50ae06fe7 | ||
|
|
05ef1f4f96 | ||
|
|
2b91edc525 | ||
|
|
6af7f9f7bf | ||
|
|
46cb9a980b | ||
|
|
ed1df148c2 | ||
|
|
44e943e100 | ||
|
|
23d36c0d52 | ||
|
|
212edaa80b | ||
|
|
e5ab18ff80 | ||
|
|
665ccf1376 | ||
|
|
1a4fe6e0bb | ||
|
|
0a7b939623 | ||
|
|
6be65ebc70 | ||
|
|
e8afa9f80c | ||
|
|
1267f482ef | ||
|
|
b41de2a39d | ||
|
|
6a48b0741b | ||
|
|
0e95838787 | ||
|
|
be62f72b38 | ||
|
|
7613f25d57 | ||
|
|
c075154e1e | ||
|
|
f08159eb87 | ||
|
|
300cffb942 | ||
|
|
2a9390d426 | ||
|
|
ec74c83845 | ||
|
|
4b2b4860fb | ||
|
|
609e10df36 | ||
|
|
d03aed1265 | ||
|
|
6993ad28ed | ||
|
|
16028e4fd3 | ||
|
|
609647a51a | ||
|
|
293299a314 | ||
|
|
0b7fe1862e | ||
|
|
866283d1a7 | ||
|
|
3676e3c571 | ||
|
|
d84f607487 | ||
|
|
edd962e385 | ||
|
|
a33a3813b2 | ||
|
|
bf69d0ac4e | ||
|
|
3f2fa309d4 | ||
|
|
8057b51f85 | ||
|
|
c09660ac50 | ||
|
|
8ec599bb17 | ||
|
|
bf74b434b8 | ||
|
|
cd54c56278 | ||
|
|
76fd34f766 | ||
|
|
1cec0b82bd | ||
|
|
0e74a180c4 | ||
|
|
fdfca0f0e7 | ||
|
|
42af7cabb7 | ||
|
|
c5137da90e | ||
|
|
e89b680a28 | ||
|
|
07b0917505 | ||
|
|
143ee25697 | ||
|
|
52853c2e9c | ||
|
|
40bfe07172 | ||
|
|
6983f48490 | ||
|
|
194f5062bb | ||
|
|
922365d2c5 | ||
|
|
a1e055b608 | ||
|
|
0e3489b240 | ||
|
|
ff28ba8080 | ||
|
|
41410e6be1 | ||
|
|
b9e55bad4d | ||
|
|
47952c3228 | ||
|
|
fdceb82454 | ||
|
|
dc899e26cf | ||
|
|
48edb33b3f | ||
|
|
9c2e49a665 | ||
|
|
8ef5c80148 | ||
|
|
083b1f98c1 | ||
|
|
3ff67c3e2f | ||
|
|
84a762e12e | ||
|
|
ef2123bd0f | ||
|
|
55699a9129 | ||
|
|
68fc3b6458 | ||
|
|
32192c7b01 | ||
|
|
77843e45f3 | ||
|
|
1919eea0a9 | ||
|
|
925631878f | ||
|
|
db2791001c | ||
|
|
62ed63bbbe | ||
|
|
081ec2ad26 | ||
|
|
633ce41460 | ||
|
|
e9537f4cde | ||
|
|
0f67ba2176 | ||
|
|
38ad3925e2 | ||
|
|
858b01f85e | ||
|
|
9f555ea8fb | ||
|
|
1f8c02f345 | ||
|
|
d81afdb6e3 | ||
|
|
0628204b43 | ||
|
|
816ecc6d6b | ||
|
|
f66263d859 | ||
|
|
8aa5aa2f57 | ||
|
|
96e31a3b30 | ||
|
|
a60a273423 | ||
|
|
a2d2e173c2 | ||
|
|
17500f56ea | ||
|
|
fc2f724e2d | ||
|
|
7986fc64dd | ||
|
|
2f9e98754b | ||
|
|
aa333e2f1d | ||
|
|
b6441b4f26 | ||
|
|
bb5524099c | ||
|
|
3a8cafe228 | ||
|
|
8259a16681 | ||
|
|
5577d51c44 | ||
|
|
d1922a3065 | ||
|
|
9647d0e2ae | ||
|
|
597a63a86c | ||
|
|
368eb01ac3 | ||
|
|
6b220e3c90 | ||
|
|
6de4b68705 | ||
|
|
16c0151831 | ||
|
|
440b244ebc | ||
|
|
47cf50bcd5 | ||
|
|
58df4078fc | ||
|
|
eda1909c75 | ||
|
|
a616914887 | ||
|
|
fd0bdc69b0 | ||
|
|
f50803ccb9 | ||
|
|
eeefc6bc0f | ||
|
|
710dde83aa | ||
|
|
ab542e97df | ||
|
|
495cd06b9a | ||
|
|
fd7a14ad22 | ||
|
|
0d3ce5d4f8 | ||
|
|
f01d107edc | ||
|
|
3cc30c4024 | ||
|
|
d2de4ec03c | ||
|
|
64ee76e94e | ||
|
|
8e20e1598e | ||
|
|
24df74d61d | ||
|
|
442f073d25 | ||
|
|
a5161e9485 | ||
|
|
ddc7d953b9 | ||
|
|
db51ed4f4c | ||
|
|
6d27797b2e | ||
|
|
89250b8ca2 | ||
|
|
bc61d7d21e | ||
|
|
dca7ddf969 | ||
|
|
e781639812 | ||
|
|
b5987ad29a | ||
|
|
8c513a6523 | ||
|
|
3b3056f910 | ||
|
|
fa5c37e972 | ||
|
|
f3e36debfe | ||
|
|
ca7abd04a2 | ||
|
|
a555626150 | ||
|
|
f26946b477 | ||
|
|
efc83bce8e | ||
|
|
81ddeb426c | ||
|
|
42e63bf358 | ||
|
|
bb21865cba | ||
|
|
606da9280d | ||
|
|
21708e43c4 | ||
|
|
8eb69922e6 | ||
|
|
e9b6fd89a4 | ||
|
|
01a14327d2 | ||
|
|
2df1071e7a | ||
|
|
7fc359c2bb | ||
|
|
853b9f75ae | ||
|
|
44d2a83e09 | ||
|
|
5f079c2061 | ||
|
|
7ef4afc688 | ||
|
|
70bcff7410 | ||
|
|
f778d8cf98 | ||
|
|
18b32ab9db | ||
|
|
aa3ff458c7 | ||
|
|
8bc74c624a | ||
|
|
6c0a5ecbf9 | ||
|
|
5f6501f842 | ||
|
|
41f704cd96 | ||
|
|
5c74795893 | ||
|
|
8961bfe90c | ||
|
|
15e3a7e861 | ||
|
|
1280e005b6 | ||
|
|
79f53d5dae | ||
|
|
bf2937e63a | ||
|
|
ed212ab924 | ||
|
|
dd0e02e912 | ||
|
|
aad875eea1 | ||
|
|
84ab535e56 | ||
|
|
a72a0c3597 | ||
|
|
6217285544 | ||
|
|
330c0bc67e | ||
|
|
b5595ca041 | ||
|
|
b779b112f3 | ||
|
|
04e08640fb | ||
|
|
aa27b3a35f | ||
|
|
d76fc4ebf6 | ||
|
|
ae60cf005f | ||
|
|
ddd3066bc4 | ||
|
|
6662195936 | ||
|
|
d951aa05b9 | ||
|
|
be297b9fd3 | ||
|
|
4e137f77a5 | ||
|
|
a33aa15c79 | ||
|
|
255ce51526 | ||
|
|
38e47fdeab | ||
|
|
9ac063d879 | ||
|
|
b52a8f8b9e | ||
|
|
1b2771f964 | ||
|
|
9766b6b03e | ||
|
|
37c8953015 | ||
|
|
a199ca3002 | ||
|
|
ed34327941 | ||
|
|
b42cf99402 | ||
|
|
33a19f1bf3 | ||
|
|
b6669746b7 |
@@ -11,21 +11,29 @@
|
||||
"slug": "latest",
|
||||
"upcoming": true
|
||||
},
|
||||
{
|
||||
"name": "2.18",
|
||||
"branchName": "2.18.x",
|
||||
"slug": "2.18",
|
||||
"upcoming": true
|
||||
},
|
||||
{
|
||||
"name": "2.17",
|
||||
"branchName": "2.17.x",
|
||||
"slug": "2.17",
|
||||
"current": true
|
||||
},
|
||||
{
|
||||
"name": "2.16",
|
||||
"branchName": "2.16.x",
|
||||
"slug": "2.16",
|
||||
"upcoming": true
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.15",
|
||||
"branchName": "2.15.x",
|
||||
"slug": "2.15",
|
||||
"current": true,
|
||||
"aliases": [
|
||||
"current",
|
||||
"stable"
|
||||
]
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.14",
|
||||
|
||||
16
.github/workflows/continuous-integration.yml
vendored
16
.github/workflows/continuous-integration.yml
vendored
@@ -39,6 +39,7 @@ jobs:
|
||||
- "8.0"
|
||||
- "8.1"
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
dbal-version:
|
||||
- "default"
|
||||
extension:
|
||||
@@ -62,7 +63,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
@@ -111,6 +112,7 @@ jobs:
|
||||
matrix:
|
||||
php-version:
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
dbal-version:
|
||||
- "default"
|
||||
- "3@dev"
|
||||
@@ -143,7 +145,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
@@ -183,6 +185,7 @@ jobs:
|
||||
matrix:
|
||||
php-version:
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
dbal-version:
|
||||
- "default"
|
||||
- "3@dev"
|
||||
@@ -212,7 +215,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
@@ -252,6 +255,7 @@ jobs:
|
||||
matrix:
|
||||
php-version:
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
dbal-version:
|
||||
- "default"
|
||||
- "3@dev"
|
||||
@@ -281,7 +285,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
@@ -333,7 +337,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
@@ -363,7 +367,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
|
||||
9
.github/workflows/documentation.yml
vendored
9
.github/workflows/documentation.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
|
||||
- name: "Install PHP"
|
||||
uses: "shivammathur/setup-php@v2"
|
||||
@@ -33,10 +33,7 @@ jobs:
|
||||
run: "rm composer.json"
|
||||
|
||||
- name: "Require phpdocumentor/guides-cli"
|
||||
run: "composer require --dev phpdocumentor/guides-cli dev-main@dev --no-update"
|
||||
|
||||
- name: "Configure minimum stability"
|
||||
run: "composer config minimum-stability dev"
|
||||
run: "composer require --dev phpdocumentor/guides-cli --no-update"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v2"
|
||||
@@ -48,4 +45,4 @@ jobs:
|
||||
printf '%s\n%s\n\n%s\n' "Dummy title" "===========" "$(cat docs/en/sidebar.rst)" > docs/en/sidebar.rst
|
||||
|
||||
- name: "Run guides-cli"
|
||||
run: "vendor/bin/guides -vvv --no-progress docs/en /tmp/test 2>&1 | ( ! grep WARNING )"
|
||||
run: "vendor/bin/guides -vvv --no-progress docs/en 2>&1 | grep -v 'Unknown directive' | ( ! grep WARNING )"
|
||||
|
||||
2
.github/workflows/phpbench.yml
vendored
2
.github/workflows/phpbench.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
|
||||
4
.github/workflows/static-analysis.yml
vendored
4
.github/workflows/static-analysis.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
|
||||
- name: "Install PHP"
|
||||
uses: "shivammathur/setup-php@v2"
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
|
||||
- name: "Install PHP"
|
||||
uses: "shivammathur/setup-php@v2"
|
||||
|
||||
22
README.md
22
README.md
@@ -1,7 +1,7 @@
|
||||
| [3.0.x][3.0] | [2.16.x][2.16] | [2.15.x][2.15] |
|
||||
| [3.0.x][3.0] | [2.18.x][2.18] | [2.17.x][2.17] |
|
||||
|:----------------:|:----------------:|:----------:|
|
||||
| [![Build status][3.0 image]][3.0] | [![Build status][2.16 image]][2.16] | [![Build status][2.15 image]][2.15] |
|
||||
| [![Coverage Status][3.0 coverage image]][3.0 coverage]| [![Coverage Status][2.16 coverage image]][2.16 coverage] | [![Coverage Status][2.15 coverage image]][2.15 coverage] |
|
||||
| [![Build status][3.0 image]][3.0] | [![Build status][2.18 image]][2.18] | [![Build status][2.17 image]][2.17] |
|
||||
| [![Coverage Status][3.0 coverage image]][3.0 coverage]| [![Coverage Status][2.18 coverage image]][2.18 coverage] | [![Coverage Status][2.17 coverage image]][2.17 coverage] |
|
||||
|
||||
[<h1 align="center">🇺🇦 UKRAINE NEEDS YOUR HELP NOW!</h1>](https://www.doctrine-project.org/stop-war.html)
|
||||
|
||||
@@ -22,11 +22,11 @@ without requiring unnecessary code duplication.
|
||||
[3.0]: https://github.com/doctrine/orm/tree/3.0.x
|
||||
[3.0 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.0.x/graph/badge.svg
|
||||
[3.0 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.0.x
|
||||
[2.16 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.16.x
|
||||
[2.16]: https://github.com/doctrine/orm/tree/2.16.x
|
||||
[2.16 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.16.x/graph/badge.svg
|
||||
[2.16 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.16.x
|
||||
[2.15 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.15.x
|
||||
[2.15]: https://github.com/doctrine/orm/tree/2.15.x
|
||||
[2.15 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.15.x/graph/badge.svg
|
||||
[2.15 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.15.x
|
||||
[2.18 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.18.x
|
||||
[2.18]: https://github.com/doctrine/orm/tree/2.18.x
|
||||
[2.18 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.18.x/graph/badge.svg
|
||||
[2.18 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.18.x
|
||||
[2.17 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.17.x
|
||||
[2.17]: https://github.com/doctrine/orm/tree/2.17.x
|
||||
[2.17 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.17.x/graph/badge.svg
|
||||
[2.17 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.17.x
|
||||
|
||||
@@ -13,6 +13,5 @@ understand the assumptions we make.
|
||||
- [DBAL Security Page](https://www.doctrine-project.org/projects/doctrine-dbal/en/stable/reference/security.html)
|
||||
- [ORM Security Page](https://www.doctrine-project.org/projects/doctrine-orm/en/stable/reference/security.html)
|
||||
|
||||
If you find a Security bug in Doctrine, please report it on Jira and change the
|
||||
Security Level to "Security Issues". It will be visible to Doctrine Core
|
||||
developers and you only.
|
||||
If you find a Security bug in Doctrine, please follow our
|
||||
[Security reporting guidelines](https://www.doctrine-project.org/policies/security.html#reporting).
|
||||
|
||||
107
UPGRADE.md
107
UPGRADE.md
@@ -1,3 +1,108 @@
|
||||
# Upgrade to 2.17
|
||||
|
||||
## Deprecate annotations classes for named queries
|
||||
|
||||
The following classes have been deprecated:
|
||||
|
||||
* `Doctrine\ORM\Mapping\NamedNativeQueries`
|
||||
* `Doctrine\ORM\Mapping\NamedNativeQuery`
|
||||
* `Doctrine\ORM\Mapping\NamedQueries`
|
||||
* `Doctrine\ORM\Mapping\NamedQuery`
|
||||
|
||||
## Deprecate `Doctrine\ORM\Query\Exec\AbstractSqlExecutor::_sqlStatements`
|
||||
|
||||
Use `Doctrine\ORM\Query\Exec\AbstractSqlExecutor::sqlStatements` instead.
|
||||
|
||||
## Undeprecate `Doctrine\ORM\Proxy\Autoloader`
|
||||
|
||||
It will be a full-fledged class, no longer extending
|
||||
`Doctrine\Common\Proxy\Autoloader` in 3.0.x.
|
||||
|
||||
## Deprecated: reliance on the non-optimal defaults that come with the `AUTO` identifier generation strategy
|
||||
|
||||
When the `AUTO` identifier generation strategy was introduced, the best
|
||||
strategy at the time was selected for each database platform.
|
||||
A lot of time has passed since then, and with ORM 3.0.0 and DBAL 4.0.0, support
|
||||
for better strategies will be added.
|
||||
|
||||
Because of that, it is now deprecated to rely on the historical defaults when
|
||||
they differ from what we will be recommended in the future.
|
||||
|
||||
Instead, you should pick a strategy for each database platform you use, and it
|
||||
will be used when using `AUTO`. As of now, only PostgreSQL is affected by this.
|
||||
|
||||
It is recommended that PostgreSQL users configure their existing and new
|
||||
applications to use `SEQUENCE` until `doctrine/dbal` 4.0.0 is released:
|
||||
|
||||
```php
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
use Doctrine\ORM\Configuration;
|
||||
|
||||
assert($configuration instanceof Configuration);
|
||||
$configuration->setIdentityGenerationPreferences([
|
||||
PostgreSQLPlatform::CLASS => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
|
||||
]);
|
||||
```
|
||||
|
||||
When DBAL 4 is released, `AUTO` will result in `IDENTITY`, and the above
|
||||
configuration should be removed to migrate to it.
|
||||
|
||||
## Deprecate `EntityManagerInterface::getPartialReference()`
|
||||
|
||||
This method does not have a replacement and will be removed in 3.0.
|
||||
|
||||
## Deprecate not-enabling lazy-ghosts
|
||||
|
||||
Not enabling lazy ghost objects is deprecated. In ORM 3.0, they will be always enabled.
|
||||
Ensure `Doctrine\ORM\Configuration::setLazyGhostObjectEnabled(true)` is called to enable them.
|
||||
|
||||
# Upgrade to 2.16
|
||||
|
||||
## Deprecated accepting duplicate IDs in the identity map
|
||||
|
||||
For any given entity class and ID value, there should be only one object instance
|
||||
representing the entity.
|
||||
|
||||
In https://github.com/doctrine/orm/pull/10785, a check was added that will guard this
|
||||
in the identity map. The most probable cause for violations of this rule are collisions
|
||||
of application-provided IDs.
|
||||
|
||||
In ORM 2.16.0, the check was added by throwing an exception. In ORM 2.16.1, this will be
|
||||
changed to a deprecation notice. ORM 3.0 will make it an exception again. Use
|
||||
`\Doctrine\ORM\Configuration::setRejectIdCollisionInIdentityMap()` if you want to opt-in
|
||||
to the new mode.
|
||||
|
||||
## Potential changes to the order in which `INSERT`s are executed
|
||||
|
||||
In https://github.com/doctrine/orm/pull/10547, the commit order computation was improved
|
||||
to fix a series of bugs where a correct (working) commit order was previously not found.
|
||||
Also, the new computation may get away with fewer queries being executed: By inserting
|
||||
referred-to entities first and using their ID values for foreign key fields in subsequent
|
||||
`INSERT` statements, additional `UPDATE` statements that were previously necessary can be
|
||||
avoided.
|
||||
|
||||
When using database-provided, auto-incrementing IDs, this may lead to IDs being assigned
|
||||
to entities in a different order than it was previously the case.
|
||||
|
||||
## Deprecated returning post insert IDs from `EntityPersister::executeInserts()`
|
||||
|
||||
Persisters implementing `\Doctrine\ORM\Persisters\Entity\EntityPersister` should no longer
|
||||
return an array of post insert IDs from their `::executeInserts()` method. Make the
|
||||
persister call `Doctrine\ORM\UnitOfWork::assignPostInsertId()` instead.
|
||||
|
||||
## Changing the way how reflection-based mapping drivers report fields, deprecated the "old" mode
|
||||
|
||||
In ORM 3.0, a change will be made regarding how the `AttributeDriver` reports field mappings.
|
||||
This change is necessary to be able to detect (and reject) some invalid mapping configurations.
|
||||
|
||||
To avoid surprises during 2.x upgrades, the new mode is opt-in. It can be activated on the
|
||||
`AttributeDriver` and `AnnotationDriver` by setting the `$reportFieldsWhereDeclared`
|
||||
constructor parameter to `true`. It will cause `MappingException`s to be thrown when invalid
|
||||
configurations are detected.
|
||||
|
||||
Not enabling the new mode will cause a deprecation notice to be raised. In ORM 3.0,
|
||||
only the new mode will be available.
|
||||
|
||||
# Upgrade to 2.15
|
||||
|
||||
## Deprecated configuring `JoinColumn` on the inverse side of one-to-one associations
|
||||
@@ -15,7 +120,7 @@ and will be an error in 3.0.
|
||||
|
||||
## Deprecated undeclared entity inheritance
|
||||
|
||||
As soon as an entity class inherits from another entity class, inheritance has to
|
||||
As soon as an entity class inherits from another entity class, inheritance has to
|
||||
be declared by adding the appropriate configuration for the root entity.
|
||||
|
||||
## Deprecated stubs for "concrete table inheritance"
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"doctrine/lexer": "^2",
|
||||
"doctrine/persistence": "^2.4 || ^3",
|
||||
"psr/cache": "^1 || ^2 || ^3",
|
||||
"symfony/console": "^4.2 || ^5.0 || ^6.0",
|
||||
"symfony/console": "^4.2 || ^5.0 || ^6.0 || ^7.0",
|
||||
"symfony/polyfill-php72": "^1.23",
|
||||
"symfony/polyfill-php80": "^1.16"
|
||||
},
|
||||
@@ -42,14 +42,14 @@
|
||||
"doctrine/annotations": "^1.13 || ^2",
|
||||
"doctrine/coding-standard": "^9.0.2 || ^12.0",
|
||||
"phpbench/phpbench": "^0.16.10 || ^1.0",
|
||||
"phpstan/phpstan": "~1.4.10 || 1.10.25",
|
||||
"phpstan/phpstan": "~1.4.10 || 1.10.35",
|
||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6",
|
||||
"psr/log": "^1 || ^2 || ^3",
|
||||
"squizlabs/php_codesniffer": "3.7.2",
|
||||
"symfony/cache": "^4.4 || ^5.4 || ^6.0",
|
||||
"symfony/var-exporter": "^4.4 || ^5.4 || ^6.2",
|
||||
"symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0",
|
||||
"vimeo/psalm": "4.30.0 || 5.13.1"
|
||||
"symfony/cache": "^4.4 || ^5.4 || ^6.4 || ^7.0",
|
||||
"symfony/var-exporter": "^4.4 || ^5.4 || ^6.2 || ^7.0",
|
||||
"symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0",
|
||||
"vimeo/psalm": "4.30.0 || 5.16.0"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/annotations": "<1.13 || >= 3.0"
|
||||
|
||||
@@ -36,71 +36,50 @@ Our entities look like:
|
||||
namespace Bank\Entities;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
*/
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
|
||||
#[ORM\Entity]
|
||||
class Account
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", unique=true)
|
||||
*/
|
||||
private string $no;
|
||||
|
||||
/**
|
||||
* @ORM\OneToMany(targetEntity="Entry", mappedBy="account", cascade={"persist"})
|
||||
*/
|
||||
private array $entries;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private int $maxCredit = 0;
|
||||
|
||||
public function __construct(string $no, int $maxCredit = 0)
|
||||
{
|
||||
$this->no = $no;
|
||||
$this->maxCredit = $maxCredit;
|
||||
$this->entries = new \Doctrine\Common\Collections\ArrayCollection();
|
||||
|
||||
#[ORM\OneToMany(targetEntity: Entry::class, mappedBy: 'account', cascade: ['persist'])]
|
||||
private Collection $entries;
|
||||
|
||||
|
||||
public function __construct(
|
||||
#[ORM\Column(type: 'string', unique: true)]
|
||||
private string $no,
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $maxCredit = 0,
|
||||
) {
|
||||
$this->entries = new ArrayCollection();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
*/
|
||||
|
||||
#[ORM\Entity]
|
||||
class Entry
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Account", inversedBy="entries")
|
||||
*/
|
||||
private Account $account;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private int $amount;
|
||||
|
||||
public function __construct(Account $account, int $amount)
|
||||
{
|
||||
$this->account = $account;
|
||||
$this->amount = $amount;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\ManyToOne(targetEntity: Account::class, inversedBy: 'entries')]
|
||||
private Account $account,
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $amount,
|
||||
) {
|
||||
// more stuff here, from/to whom, stated reason, execution date and such
|
||||
}
|
||||
|
||||
|
||||
public function getAmount(): Amount
|
||||
{
|
||||
return $this->amount;
|
||||
@@ -193,9 +172,8 @@ relation with this method:
|
||||
public function addEntry(int $amount): void
|
||||
{
|
||||
$this->assertAcceptEntryAllowed($amount);
|
||||
|
||||
$e = new Entry($this, $amount);
|
||||
$this->entries[] = $e;
|
||||
|
||||
$this->entries[] = new Entry($this, $amount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,20 +189,20 @@ Now look at the following test-code for our entities:
|
||||
{
|
||||
public function testAddEntry()
|
||||
{
|
||||
$account = new Account("123456", $maxCredit = 200);
|
||||
$account = new Account("123456", maxCredit: 200);
|
||||
$this->assertEquals(0, $account->getBalance());
|
||||
|
||||
|
||||
$account->addEntry(500);
|
||||
$this->assertEquals(500, $account->getBalance());
|
||||
|
||||
|
||||
$account->addEntry(-700);
|
||||
$this->assertEquals(-200, $account->getBalance());
|
||||
}
|
||||
|
||||
|
||||
public function testExceedMaxLimit()
|
||||
{
|
||||
$account = new Account("123456", $maxCredit = 200);
|
||||
|
||||
$account = new Account("123456", maxCredit: 200);
|
||||
|
||||
$this->expectException(Exception::class);
|
||||
$account->addEntry(-1000);
|
||||
}
|
||||
@@ -285,22 +263,19 @@ entries collection) we want to add an aggregate field called
|
||||
<?php
|
||||
class Account
|
||||
{
|
||||
/**
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $balance = 0;
|
||||
|
||||
|
||||
public function getBalance(): int
|
||||
{
|
||||
return $this->balance;
|
||||
}
|
||||
|
||||
|
||||
public function addEntry(int $amount): void
|
||||
{
|
||||
$this->assertAcceptEntryAllowed($amount);
|
||||
|
||||
$e = new Entry($this, $amount);
|
||||
$this->entries[] = $e;
|
||||
|
||||
$this->entries[] = new Entry($this, $amount);
|
||||
$this->balance += $amount;
|
||||
}
|
||||
}
|
||||
@@ -331,13 +306,13 @@ potentially lead to inconsistent state. See this example:
|
||||
// The Account $accId has a balance of 0 and a max credit limit of 200:
|
||||
// request 1 account
|
||||
$account1 = $em->find(Account::class, $accId);
|
||||
|
||||
|
||||
// request 2 account
|
||||
$account2 = $em->find(Account::class, $accId);
|
||||
|
||||
|
||||
$account1->addEntry(-200);
|
||||
$account2->addEntry(-200);
|
||||
|
||||
|
||||
// now request 1 and 2 both flush the changes.
|
||||
|
||||
The aggregate field ``Account::$balance`` is now -200, however the
|
||||
@@ -357,10 +332,8 @@ Optimistic locking is as easy as adding a version column:
|
||||
|
||||
class Account
|
||||
{
|
||||
/**
|
||||
* @ORM\Column(type="integer")
|
||||
* @ORM\Version
|
||||
*/
|
||||
#[ORM\Column(type: 'integer')]
|
||||
#[ORM\Version]
|
||||
private int $version;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ for all our domain objects.
|
||||
.. note::
|
||||
|
||||
The notify change tracking policy is deprecated and will be removed in ORM 3.0.
|
||||
(`Details <https://github.com/doctrine/orm/issues/8383>`_)
|
||||
(\ `Details <https://github.com/doctrine/orm/issues/8383>`_)
|
||||
|
||||
Implementing NotifyPropertyChanged
|
||||
----------------------------------
|
||||
|
||||
@@ -47,10 +47,8 @@ A Customer entity
|
||||
use Acme\CustomerModule\Entity\Customer as BaseCustomer;
|
||||
use Acme\InvoiceModule\Model\InvoiceSubjectInterface;
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(name="customer")
|
||||
*/
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'customer')]
|
||||
class Customer extends BaseCustomer implements InvoiceSubjectInterface
|
||||
{
|
||||
// In our example, any methods defined in the InvoiceSubjectInterface
|
||||
@@ -69,19 +67,12 @@ An Invoice entity
|
||||
use Doctrine\ORM\Mapping AS ORM;
|
||||
use Acme\InvoiceModule\Model\InvoiceSubjectInterface;
|
||||
|
||||
/**
|
||||
* Represents an Invoice.
|
||||
*
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(name="invoice")
|
||||
*/
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'invoice')]
|
||||
class Invoice
|
||||
{
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Acme\InvoiceModule\Model\InvoiceSubjectInterface")
|
||||
* @var InvoiceSubjectInterface
|
||||
*/
|
||||
protected $subject;
|
||||
#[ORM\ManyToOne(targetEntity: InvoiceSubjectInterface::class)]
|
||||
protected InvoiceSubjectInterface $subject;
|
||||
}
|
||||
|
||||
An InvoiceSubjectInterface
|
||||
@@ -127,7 +118,7 @@ the targetEntity resolution will occur reliably:
|
||||
// Add the ResolveTargetEntityListener
|
||||
$evm->addEventListener(Doctrine\ORM\Events::loadClassMetadata, $rtel);
|
||||
|
||||
$connection = \Doctrine\DBAL\DriverManager::createConnection($connectionOptions, $config, $evm);
|
||||
$connection = \Doctrine\DBAL\DriverManager::getConnection($connectionOptions, $config, $evm);
|
||||
$em = new \Doctrine\ORM\EntityManager($connection, $config, $evm);
|
||||
|
||||
Final Thoughts
|
||||
|
||||
@@ -58,6 +58,10 @@ First Attributes:
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\HasLifecycleCallbacks;
|
||||
use Doctrine\ORM\Mapping\PrePersist;
|
||||
use Doctrine\ORM\Mapping\PreUpdate;
|
||||
|
||||
#[Entity]
|
||||
#[HasLifecycleCallbacks]
|
||||
|
||||
@@ -29,7 +29,7 @@ steps of configuration.
|
||||
|
||||
$config = new Configuration;
|
||||
$config->setMetadataCache($metadataCache);
|
||||
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities']);
|
||||
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true);
|
||||
$config->setMetadataDriverImpl($driverImpl);
|
||||
$config->setQueryCache($queryCache);
|
||||
$config->setProxyDir('/path/to/myproject/lib/MyProject/Proxies');
|
||||
@@ -134,7 +134,7 @@ The attribute driver can be injected in the ``Doctrine\ORM\Configuration``:
|
||||
<?php
|
||||
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
|
||||
|
||||
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities']);
|
||||
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true);
|
||||
$config->setMetadataDriverImpl($driverImpl);
|
||||
|
||||
The path information to the entities is required for the attribute
|
||||
@@ -408,7 +408,7 @@ means that you have to register a special autoloader for these classes:
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Doctrine\Common\Proxy\Autoloader;
|
||||
use Doctrine\ORM\Proxy\Autoloader;
|
||||
|
||||
$proxyDir = "/path/to/proxies";
|
||||
$proxyNamespace = "MyProxies";
|
||||
|
||||
@@ -24,28 +24,34 @@ performance it is also recommended that you use APC with PHP.
|
||||
Doctrine ORM Packages
|
||||
-------------------
|
||||
|
||||
Doctrine ORM is divided into three main packages.
|
||||
Doctrine ORM is divided into four main packages.
|
||||
|
||||
- Common
|
||||
- DBAL (includes Common)
|
||||
- ORM (includes DBAL+Common)
|
||||
- `Collections <https://www.doctrine-project.org/projects/doctrine-collections/en/stable/index.html>`_
|
||||
- `Event Manager <https://www.doctrine-project.org/projects/doctrine-event-manager/en/stable/index.html>`_
|
||||
- `Persistence <https://www.doctrine-project.org/projects/doctrine-persistence/en/stable/index.html>`_
|
||||
- `DBAL <https://www.doctrine-project.org/projects/doctrine-dbal/en/stable/index.html>`_
|
||||
- ORM (depends on DBAL+Persistence+Collections)
|
||||
|
||||
This manual mainly covers the ORM package, sometimes touching parts
|
||||
of the underlying DBAL and Common packages. The Doctrine code base
|
||||
of the underlying DBAL and Persistence packages. The Doctrine code base
|
||||
is split in to these packages for a few reasons and they are to...
|
||||
|
||||
|
||||
- ...make things more maintainable and decoupled
|
||||
- ...allow you to use the code in Doctrine Common without the ORM
|
||||
or DBAL
|
||||
- ...allow you to use the code in Doctrine Persistence and Collections
|
||||
without the ORM or DBAL
|
||||
- ...allow you to use the DBAL without the ORM
|
||||
|
||||
The Common Package
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
Collection, Event Manager and Persistence
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Common package contains highly reusable components that have no
|
||||
dependencies beyond the package itself (and PHP, of course). The
|
||||
root namespace of the Common package is ``Doctrine\Common``.
|
||||
The Collection, Event Manager and Persistence packages contain highly
|
||||
reusable components that have no dependencies beyond the packages
|
||||
themselves (and PHP, of course). The root namespace of the Persistence
|
||||
package is ``Doctrine\Persistence``. The root namespace of the
|
||||
Collection package is ``Doctrine\Common\Collections``, for historical
|
||||
reasons. The root namespace of the Event Manager package is just
|
||||
``Doctrine\Common``, also for historical reasons.
|
||||
|
||||
The DBAL Package
|
||||
~~~~~~~~~~~~~~~~
|
||||
@@ -199,5 +205,3 @@ typical implementation of the
|
||||
to keep track of all the things that need to be done the next time
|
||||
``flush`` is invoked. You usually do not directly interact with a
|
||||
``UnitOfWork`` but with the ``EntityManager`` instead.
|
||||
|
||||
|
||||
|
||||
@@ -47,8 +47,7 @@ mapping metadata:
|
||||
- :doc:`Attributes <attributes-reference>`
|
||||
- :doc:`XML <xml-mapping>`
|
||||
- :doc:`PHP code <php-mapping>`
|
||||
- :doc:`Docblock Annotations <annotations-reference>` (deprecated and
|
||||
will be removed in ``doctrine/orm`` 3.0)
|
||||
- :doc:`Docblock Annotations <annotations-reference>` (deprecated and will be removed in ``doctrine/orm`` 3.0)
|
||||
- :doc:`YAML <yaml-mapping>` (deprecated and will be removed in ``doctrine/orm`` 3.0.)
|
||||
|
||||
This manual will usually show mapping metadata via attributes, though
|
||||
@@ -423,9 +422,11 @@ the field that serves as the identifier with the ``#[Id]`` attribute.
|
||||
# fields here
|
||||
|
||||
In most cases using the automatic generator strategy (``#[GeneratedValue]``) is
|
||||
what you want. It defaults to the identifier generation mechanism your current
|
||||
database vendor prefers: AUTO_INCREMENT with MySQL, sequences with PostgreSQL
|
||||
and Oracle and so on.
|
||||
what you want, but for backwards-compatibility reasons it might not. It
|
||||
defaults to the identifier generation mechanism your current database
|
||||
vendor preferred at the time that strategy was introduced:
|
||||
``AUTO_INCREMENT`` with MySQL, sequences with PostgreSQL and Oracle and
|
||||
so on.
|
||||
|
||||
.. _identifier-generation-strategies:
|
||||
|
||||
@@ -442,17 +443,18 @@ Here is the list of possible generation strategies:
|
||||
|
||||
- ``AUTO`` (default): Tells Doctrine to pick the strategy that is
|
||||
preferred by the used database platform. The preferred strategies
|
||||
are IDENTITY for MySQL, SQLite, MsSQL and SQL Anywhere and SEQUENCE
|
||||
for Oracle and PostgreSQL. This strategy provides full portability.
|
||||
- ``SEQUENCE``: Tells Doctrine to use a database sequence for ID
|
||||
generation. This strategy does currently not provide full
|
||||
portability. Sequences are supported by Oracle, PostgreSql and
|
||||
SQL Anywhere.
|
||||
are ``IDENTITY`` for MySQL, SQLite, MsSQL and SQL Anywhere and, for
|
||||
historical reasons, ``SEQUENCE`` for Oracle and PostgreSQL. This
|
||||
strategy provides full portability.
|
||||
- ``IDENTITY``: Tells Doctrine to use special identity columns in
|
||||
the database that generate a value on insertion of a row. This
|
||||
strategy does currently not provide full portability and is
|
||||
supported by the following platforms: MySQL/SQLite/SQL Anywhere
|
||||
(AUTO\_INCREMENT), MSSQL (IDENTITY) and PostgreSQL (SERIAL).
|
||||
(``AUTO_INCREMENT``), MSSQL (``IDENTITY``) and PostgreSQL (``SERIAL``).
|
||||
- ``SEQUENCE``: Tells Doctrine to use a database sequence for ID
|
||||
generation. This strategy does currently not provide full
|
||||
portability. Sequences are supported by Oracle, PostgreSql and
|
||||
SQL Anywhere.
|
||||
- ``UUID`` (deprecated): Tells Doctrine to use the built-in Universally
|
||||
Unique Identifier generator. This strategy provides full portability.
|
||||
- ``NONE``: Tells Doctrine that the identifiers are assigned (and
|
||||
|
||||
@@ -63,7 +63,7 @@ Notify
|
||||
.. note::
|
||||
|
||||
The notify change tracking policy is deprecated and will be removed in ORM 3.0.
|
||||
(`Details <https://github.com/doctrine/orm/issues/8383>`_)
|
||||
(\ `Details <https://github.com/doctrine/orm/issues/8383>`_)
|
||||
|
||||
This policy is based on the assumption that the entities notify
|
||||
interested listeners of changes to their properties. For that
|
||||
|
||||
@@ -464,6 +464,11 @@ hierarchies:
|
||||
$query = $em->createQuery('SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF Doctrine\Tests\Models\Company\CompanyEmployee');
|
||||
$query = $em->createQuery('SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF ?1');
|
||||
$query = $em->createQuery('SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u NOT INSTANCE OF ?1');
|
||||
$query->setParameter(0, $em->getClassMetadata(CompanyEmployee::class));
|
||||
|
||||
.. note::
|
||||
To use a class as parameter, you have to bind its class metadata:
|
||||
``$query->setParameter(0, $em->getClassMetadata(CompanyEmployee::class);``.
|
||||
|
||||
Get all users visible on a given website that have chosen certain gender:
|
||||
|
||||
@@ -1381,7 +1386,7 @@ Result Cache API:
|
||||
$query->setResultCacheDriver(new ApcCache());
|
||||
|
||||
$query->useResultCache(true)
|
||||
->setResultCacheLifeTime($seconds = 3600);
|
||||
->setResultCacheLifeTime(3600);
|
||||
|
||||
$result = $query->getResult(); // cache miss
|
||||
|
||||
@@ -1392,7 +1397,7 @@ Result Cache API:
|
||||
$result = $query->getResult(); // saved in given result cache id.
|
||||
|
||||
// or call useResultCache() with all parameters:
|
||||
$query->useResultCache(true, $seconds = 3600, 'my_query_result');
|
||||
$query->useResultCache(true, 3600, 'my_query_result');
|
||||
$result = $query->getResult(); // cache hit!
|
||||
|
||||
// Introspection
|
||||
@@ -1425,7 +1430,7 @@ userland:
|
||||
reloading this data. Partially loaded objects have to be passed to
|
||||
``EntityManager::refresh()`` if they are to be reloaded fully from
|
||||
the database. This query hint is deprecated and will be removed
|
||||
in the future (`Details <https://github.com/doctrine/orm/issues/8471>`_)
|
||||
in the future (\ `Details <https://github.com/doctrine/orm/issues/8471>`_)
|
||||
- ``Query::HINT_REFRESH`` - This query is used internally by
|
||||
``EntityManager::refresh()`` and can be used in userland as well.
|
||||
If you specify this hint and a query returns the data for an entity
|
||||
@@ -1457,7 +1462,7 @@ several methods to interact with it:
|
||||
|
||||
- ``Query::setQueryCacheDriver($driver)`` - Allows to set a Cache
|
||||
instance
|
||||
- ``Query::setQueryCacheLifeTime($seconds = 3600)`` - Set lifetime
|
||||
- ``Query::setQueryCacheLifeTime($seconds)`` - Set lifetime
|
||||
of the query caching.
|
||||
- ``Query::expireQueryCache($bool)`` - Enforce the expiring of the
|
||||
query cache if set to true.
|
||||
|
||||
@@ -173,6 +173,19 @@ Events Overview
|
||||
| :ref:`onClear<reference-events-on-clear>` | ``$em->clear()`` | No | `OnClearEventArgs`_ |
|
||||
+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
|
||||
|
||||
.. warning::
|
||||
|
||||
Making changes to entities and calling ``EntityManager::flush()`` from within
|
||||
event handlers dispatched by ``EntityManager::flush()`` itself is strongly
|
||||
discouraged, and might be deprecated and eventually prevented in the future.
|
||||
|
||||
The reason is that it causes re-entrance into ``UnitOfWork::commit()`` while a commit
|
||||
is currently being processed. The ``UnitOfWork`` was never designed to support this,
|
||||
and its behavior in this situation is not covered by any tests.
|
||||
|
||||
This may lead to entity or collection updates being missed, applied only in parts and
|
||||
changes being lost at the end of the commit phase.
|
||||
|
||||
Naming convention
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -215,6 +228,10 @@ specific to a particular entity class's lifecycle.
|
||||
<?php
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Event\PrePersistEventArgs;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\HasLifecycleCallbacks;
|
||||
use Doctrine\ORM\Mapping\PrePersist;
|
||||
use Doctrine\ORM\Mapping\PreUpdate;
|
||||
|
||||
#[Entity]
|
||||
#[HasLifecycleCallbacks]
|
||||
@@ -453,13 +470,12 @@ prePersist
|
||||
|
||||
There are two ways for the ``prePersist`` event to be triggered:
|
||||
|
||||
- One is obviously when you call ``EntityManager::persist()``. The
|
||||
event is also called for all :ref:`cascaded associations<transitive-persistence>`.
|
||||
- The other is inside the
|
||||
``flush()`` method when changes to associations are computed and
|
||||
this association is marked as :ref:`cascade: persist<transitive-persistence>`. Any new entity found
|
||||
during this operation is also persisted and ``prePersist`` called
|
||||
on it. This is called :ref:`persistence by reachability<persistence-by-reachability>`.
|
||||
- One is when you call ``EntityManager::persist()``. The
|
||||
event is also called for all :ref:`cascaded associations<transitive-persistence>`.
|
||||
- The other is inside the ``flush()`` method when changes to associations are computed and
|
||||
this association is marked as :ref:`cascade: persist<transitive-persistence>`. Any new entity found
|
||||
during this operation is also persisted and ``prePersist`` called
|
||||
on it. This is called :ref:`persistence by reachability<persistence-by-reachability>`.
|
||||
|
||||
In both cases you get passed a ``PrePersistEventArgs`` instance
|
||||
which has access to the entity and the entity manager.
|
||||
@@ -696,22 +712,33 @@ Restrictions for this event:
|
||||
postUpdate, postRemove, postPersist
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
These three post* events are called inside ``EntityManager::flush()``.
|
||||
These three ``post*`` events are called inside ``EntityManager::flush()``.
|
||||
Changes in here are not relevant to the persistence in the
|
||||
database, but you can use these events to alter non-persistable items,
|
||||
like non-mapped fields, logging or even associated classes that are
|
||||
not directly mapped by Doctrine.
|
||||
|
||||
- The ``postUpdate`` event occurs after the database
|
||||
update operations to entity data. It is not called for a DQL
|
||||
``UPDATE`` statement.
|
||||
- The ``postPersist`` event occurs for an entity after
|
||||
the entity has been made persistent. It will be invoked after the
|
||||
database insert operations. Generated primary key values are
|
||||
available in the postPersist event.
|
||||
update operations to entity data, but before the database transaction
|
||||
has been committed. It is not called for a DQL ``UPDATE`` statement.
|
||||
- The ``postPersist`` event occurs for an entity after the entity has
|
||||
been made persistent. It will be invoked after all database insert
|
||||
operations for new entities have been performed, but before the database
|
||||
transaction has been committed. Generated primary key values will be
|
||||
available for all entities at the time this event is triggered.
|
||||
- The ``postRemove`` event occurs for an entity after the
|
||||
entity has been deleted. It will be invoked after the database
|
||||
delete operations. It is not called for a DQL ``DELETE`` statement.
|
||||
entity has been deleted. It will be invoked after all database
|
||||
delete operations for entity rows have been executed, but before the
|
||||
database transaction has been committed. This event is not called for
|
||||
a DQL ``DELETE`` statement.
|
||||
|
||||
.. note::
|
||||
|
||||
At the time ``postPersist`` is called, there may still be collection and/or
|
||||
"extra" updates pending. The database may not yet be completely in
|
||||
sync with the entity states in memory, not even for the new entities. Similarly,
|
||||
also at the time ``postUpdate`` and ``postRemove`` are called, in-memory collections
|
||||
may still be in a "dirty" state or still contain removed entities.
|
||||
|
||||
.. warning::
|
||||
|
||||
|
||||
@@ -93,3 +93,34 @@ object.
|
||||
want to refresh or reload an object after having modified a filter or the
|
||||
FilterCollection, then you should clear the EntityManager and re-fetch your
|
||||
entities, having the new rules for filtering applied.
|
||||
|
||||
|
||||
Suspending/Restoring Filters
|
||||
----------------------------
|
||||
When a filter is disabled, the instance is fully deleted and all the filter
|
||||
parameters previously set are lost. Then, if you enable it again, a new filter
|
||||
is created without the previous filter parameters. If you want to keep a filter
|
||||
(in order to use it later) but temporary disable it, you'll need to use the
|
||||
``FilterCollection#suspend($name)`` and ``FilterCollection#restore($name)``
|
||||
methods instead.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$filter = $em->getFilters()->enable("locale");
|
||||
$filter->setParameter('locale', 'en');
|
||||
|
||||
// Temporary suspend the filter
|
||||
$filter = $em->getFilters()->suspend("locale");
|
||||
|
||||
// Do things
|
||||
|
||||
// Then restore it, the locale parameter will still be set
|
||||
$filter = $em->getFilters()->restore("locale");
|
||||
|
||||
.. warning::
|
||||
If you enable a previously disabled filter, doctrine will create a new
|
||||
one without keeping any of the previously parameter set with
|
||||
``SQLFilter#setParameter()`` or ``SQLFilter#getParameterList()``. If you
|
||||
want to restore the previously disabled filter instead, you must use the
|
||||
``FilterCollection#restore($name)`` method.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
Limitations and Known Issues
|
||||
============================
|
||||
|
||||
We try to make using Doctrine2 a very pleasant experience.
|
||||
We try to make using Doctrine ORM a very pleasant experience.
|
||||
Therefore we think it is very important to be honest about the
|
||||
current limitations to our users. Much like every other piece of
|
||||
software Doctrine2 is not perfect and far from feature complete.
|
||||
software the ORM is not perfect and far from feature complete.
|
||||
This section should give you an overview of current limitations of
|
||||
Doctrine ORM as well as critical known issues that you should know
|
||||
about.
|
||||
@@ -167,7 +167,7 @@ have produced, this is probably fine.
|
||||
|
||||
However, to mention known limitations, it is currently not possible to use "class"
|
||||
level `annotations <https://github.com/doctrine/orm/pull/1517>`_ or
|
||||
`attributes <https://github.com/doctrine/orm/issues/8868>` on traits, and attempts to
|
||||
`attributes <https://github.com/doctrine/orm/issues/8868>`_ on traits, and attempts to
|
||||
improve parser support for traits as `here <https://github.com/doctrine/annotations/pull/102>`_
|
||||
or `there <https://github.com/doctrine/annotations/pull/63>`_ have been abandoned
|
||||
due to complexity.
|
||||
@@ -175,6 +175,18 @@ due to complexity.
|
||||
XML mapping configuration probably needs to completely re-configure or otherwise
|
||||
copy-and-paste configuration for fields used from traits.
|
||||
|
||||
Mapping multiple private fields of the same name
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When two classes, say a mapped superclass and an entity inheriting from it,
|
||||
both contain a ``private`` field of the same name, this will lead to a ``MappingException``.
|
||||
|
||||
Since the fields are ``private``, both are technically separate and can contain
|
||||
different values at the same time. However, the ``ClassMetadata`` configuration used
|
||||
internally by the ORM currently refers to fields by their name only, without taking the
|
||||
class containing the field into consideration. This makes it impossible to keep separate
|
||||
mapping configuration for both fields.
|
||||
|
||||
Known Issues
|
||||
------------
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Partial Objects
|
||||
|
||||
Creating Partial Objects through DQL is deprecated and
|
||||
will be removed in the future, use data transfer object
|
||||
support in DQL instead. (`Details
|
||||
support in DQL instead. (\ `Details
|
||||
<https://github.com/doctrine/orm/issues/8471>`_)
|
||||
|
||||
A partial object is an object whose state is not fully initialized
|
||||
|
||||
@@ -253,7 +253,7 @@ Calling ``setParameter()`` automatically infers which type you are setting as
|
||||
value. This works for integers, arrays of strings/integers, DateTime instances
|
||||
and for managed entities. If you want to set a type explicitly you can call
|
||||
the third argument to ``setParameter()`` explicitly. It accepts either a DBAL
|
||||
Doctrine\DBAL\ParameterType::* or a DBAL Type name for conversion.
|
||||
``Doctrine\DBAL\ParameterType::*`` or a DBAL Type name for conversion.
|
||||
|
||||
.. note::
|
||||
|
||||
|
||||
@@ -12,9 +12,8 @@ page only handles Security issues in the ORM.
|
||||
|
||||
- `DBAL Security Page <https://www.doctrine-project.org/projects/doctrine-dbal/en/current/reference/security.html>`
|
||||
|
||||
If you find a Security bug in Doctrine, please report it on Jira and change the
|
||||
Security Level to "Security Issues". It will be visible to Doctrine Core
|
||||
developers and you only.
|
||||
If you find a Security bug in Doctrine, please follow our
|
||||
`Security reporting guidelines <https://www.doctrine-project.org/policies/security.html#reporting>`_.
|
||||
|
||||
User input and Doctrine ORM
|
||||
---------------------------
|
||||
|
||||
@@ -37,8 +37,8 @@ will still end up with the same reference:
|
||||
public function testIdentityMapReference(): void
|
||||
{
|
||||
$objectA = $this->entityManager->getReference('EntityName', 1);
|
||||
// check for proxyinterface
|
||||
$this->assertInstanceOf('Doctrine\Persistence\Proxy', $objectA);
|
||||
// check entity is not initialized
|
||||
$this->assertTrue($this->entityManager->isUninitializedObject($objectA));
|
||||
|
||||
$objectB = $this->entityManager->find('EntityName', 1);
|
||||
|
||||
@@ -137,7 +137,7 @@ optimize the performance of the Flush Operation:
|
||||
.. note::
|
||||
|
||||
Flush only a single entity with ``$entityManager->flush($entity)`` is deprecated and will be removed in ORM 3.0.
|
||||
(`Details <https://github.com/doctrine/orm/issues/8459>`_)
|
||||
(\ `Details <https://github.com/doctrine/orm/issues/8459>`_)
|
||||
|
||||
Query Internals
|
||||
---------------
|
||||
|
||||
@@ -192,6 +192,11 @@ be properly synchronized with the database when
|
||||
database in the most efficient way and a single, short transaction,
|
||||
taking care of maintaining referential integrity.
|
||||
|
||||
.. note::
|
||||
|
||||
Do not make any assumptions in your code about the number of queries
|
||||
it takes to flush changes, about the ordering of ``INSERT``, ``UPDATE``
|
||||
and ``DELETE`` queries or the order in which entities will be processed.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -777,6 +782,23 @@ and these associations are mapped as EAGER, they will automatically
|
||||
be loaded together with the entity being queried and is thus
|
||||
immediately available to your application.
|
||||
|
||||
Eager Loading can also be configured at runtime through
|
||||
``AbstractQuery::setFetchMode`` in DQL or Native Queries.
|
||||
|
||||
Eager loading for many-to-one and one-to-one associations is using either a
|
||||
LEFT JOIN or a second query for fetching the related entity eagerly.
|
||||
|
||||
Eager loading for many-to-one associations uses a second query to load
|
||||
the collections for several entities at the same time.
|
||||
|
||||
When many-to-many, one-to-one or one-to-many associations are eagerly loaded,
|
||||
then the global batch size configuration is used to avoid IN(?) queries with
|
||||
too many arguments. The default batch size is 100 and can be changed with
|
||||
``Configuration::setEagerFetchBatchSize()``.
|
||||
|
||||
For eagerly loaded Many-To-Many associations one query has to be made for each
|
||||
collection.
|
||||
|
||||
By Lazy Loading
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ before. There are some prerequisites for the tutorial that have to be
|
||||
installed:
|
||||
|
||||
- PHP (latest stable version)
|
||||
- Composer Package Manager (`Install Composer
|
||||
- Composer Package Manager (\ `Install Composer
|
||||
<https://getcomposer.org/doc/00-intro.md>`_)
|
||||
|
||||
The code of this tutorial is `available on Github <https://github.com/doctrine/doctrine2-orm-tutorial>`_.
|
||||
@@ -321,7 +321,7 @@ data in your storage, and later in your application when the data is loaded agai
|
||||
.. note::
|
||||
|
||||
This method, although very common, is inappropriate for Domain Driven
|
||||
Design (`DDD <https://en.wikipedia.org/wiki/Domain-driven_design>`_)
|
||||
Design (\ `DDD <https://en.wikipedia.org/wiki/Domain-driven_design>`_)
|
||||
where methods should represent real business operations and not simple
|
||||
property change, And business invariants should be maintained both in the
|
||||
application state (entities in this case) and in the database, with no
|
||||
@@ -735,7 +735,7 @@ classes. We'll store them in ``src/Bug.php`` and ``src/User.php``, respectively.
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
#[ORM\GeneratedValue]
|
||||
private int $id;
|
||||
private int|null $id;
|
||||
|
||||
#[ORM\Column(type: 'string')]
|
||||
private string $description;
|
||||
@@ -1199,21 +1199,21 @@ which translates the YYYY-mm-dd HH:mm:ss database format
|
||||
into a PHP DateTime instance and back.
|
||||
|
||||
After the field definitions, the two qualified references to the
|
||||
user entity are defined. They are created by the ``many-to-one``
|
||||
tag. The class name of the related entity has to be specified with
|
||||
the ``target-entity`` attribute, which is enough information for
|
||||
the database mapper to access the foreign-table. Since
|
||||
user entity are defined. They are created by the ``ManyToOne``
|
||||
attribute. The class name of the related entity has to be specified with
|
||||
the ``targetEntity`` parameter, which is enough information for
|
||||
the database mapper to access the foreign table. Since
|
||||
``reporter`` and ``engineer`` are on the owning side of a
|
||||
bi-directional relation, we also have to specify the ``inversed-by``
|
||||
attribute. They have to point to the field names on the inverse
|
||||
side of the relationship. We will see in the next example that the ``inversed-by``
|
||||
attribute has a counterpart ``mapped-by`` which makes that
|
||||
bi-directional relation, we also have to specify the ``inversedBy``
|
||||
parameter. They have to point to the field names on the inverse
|
||||
side of the relationship. We will see in the next example that the ``inversedBy``
|
||||
parameter has a counterpart ``mappedBy`` which makes that
|
||||
the inverse side.
|
||||
|
||||
The last definition is for the ``Bug#products`` collection. It
|
||||
holds all products where the specific bug occurs. Again
|
||||
you have to define the ``target-entity`` and ``field`` attributes
|
||||
on the ``many-to-many`` tag.
|
||||
you have to define the ``targetEntity`` and ``field`` parameters
|
||||
on the ``ManyToMany`` attribute.
|
||||
|
||||
Finally, we'll add metadata mappings for the ``User`` entity.
|
||||
|
||||
@@ -1336,7 +1336,7 @@ Finally, we'll add metadata mappings for the ``User`` entity.
|
||||
targetEntity: Bug
|
||||
mappedBy: engineer
|
||||
|
||||
Here are some new things to mention about the ``one-to-many`` tags.
|
||||
Here are some new things to mention about the ``OneToMany`` attribute.
|
||||
Remember that we discussed about the inverse and owning side. Now
|
||||
both reportedBugs and assignedBugs are inverse relations, which
|
||||
means the join details have already been defined on the owning
|
||||
|
||||
@@ -15,7 +15,7 @@ has a very simple API and implements the SPL interfaces ``Countable`` and
|
||||
->setFirstResult(0)
|
||||
->setMaxResults(100);
|
||||
|
||||
$paginator = new Paginator($query, $fetchJoinCollection = true);
|
||||
$paginator = new Paginator($query, fetchJoinCollection: true);
|
||||
|
||||
$c = count($paginator);
|
||||
foreach ($paginator as $post) {
|
||||
@@ -36,10 +36,25 @@ correct result:
|
||||
|
||||
This behavior is only necessary if you actually fetch join a to-many
|
||||
collection. You can disable this behavior by setting the
|
||||
``$fetchJoinCollection`` flag to ``false``; in that case only 2 instead of the 3 queries
|
||||
``fetchJoinCollection`` argument to ``false``; in that case only 2 instead of the 3 queries
|
||||
described are executed. We hope to automate the detection for this in
|
||||
the future.
|
||||
|
||||
.. note::
|
||||
|
||||
``$fetchJoinCollection`` flag set to ``true`` might affect results if you use aggregations in your query.
|
||||
``fetchJoinCollection`` argument set to ``true`` might affect results if you use aggregations in your query.
|
||||
|
||||
By using the ``Paginator::HINT_ENABLE_DISTINCT`` you can instruct doctrine that the query to be executed
|
||||
will not produce "duplicate" rows (only to-one relations are joined), thus the SQL limit will work as expected.
|
||||
In this way the `DISTINCT` keyword will be omitted and can bring important performance improvements.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
|
||||
$dql = "SELECT u, p FROM User u JOIN u.mainPicture p";
|
||||
$query = $entityManager->createQuery($dql)
|
||||
->setHint(Paginator::HINT_ENABLE_DISTINCT, false)
|
||||
->setFirstResult(0)
|
||||
->setMaxResults(100);
|
||||
|
||||
@@ -148,7 +148,6 @@
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="entity-result" type="orm:entity-result"/>
|
||||
<xs:element name="column-result" type="orm:column-result"/>
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
@@ -226,22 +225,13 @@
|
||||
|
||||
<xs:complexType name="mapped-superclass" >
|
||||
<xs:complexContent>
|
||||
<xs:extension base="orm:entity">
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
<xs:anyAttribute namespace="##other"/>
|
||||
</xs:extension>
|
||||
<xs:extension base="orm:entity"/>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="embeddable">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="orm:entity">
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
</xs:extension>
|
||||
<xs:extension base="orm:entity"/>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
@@ -565,7 +555,6 @@
|
||||
<xs:choice minOccurs="0" maxOccurs="1">
|
||||
<xs:element name="join-column" type="orm:join-column"/>
|
||||
<xs:element name="join-columns" type="orm:join-columns"/>
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
@@ -583,7 +572,6 @@
|
||||
<xs:choice minOccurs="0" maxOccurs="1">
|
||||
<xs:element name="join-column" type="orm:join-column"/>
|
||||
<xs:element name="join-columns" type="orm:join-columns"/>
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
|
||||
@@ -10,7 +10,6 @@ use Doctrine\Common\Cache\Psr6\CacheAdapter;
|
||||
use Doctrine\Common\Cache\Psr6\DoctrineProvider;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Util\ClassUtils;
|
||||
use Doctrine\DBAL\Cache\QueryCacheProfile;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
@@ -20,6 +19,7 @@ use Doctrine\ORM\Cache\QueryCacheKey;
|
||||
use Doctrine\ORM\Cache\TimestampCacheKey;
|
||||
use Doctrine\ORM\Internal\Hydration\IterableResult;
|
||||
use Doctrine\ORM\Mapping\MappingException as ORMMappingException;
|
||||
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
|
||||
use Doctrine\ORM\Query\Parameter;
|
||||
use Doctrine\ORM\Query\QueryException;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
@@ -430,7 +430,7 @@ abstract class AbstractQuery
|
||||
}
|
||||
|
||||
try {
|
||||
$class = ClassUtils::getClass($value);
|
||||
$class = DefaultProxyClassNameResolver::getClass($value);
|
||||
$value = $this->_em->getUnitOfWork()->getSingleIdentifierValue($value);
|
||||
|
||||
if ($value === null) {
|
||||
@@ -1010,7 +1010,7 @@ abstract class AbstractQuery
|
||||
*
|
||||
* Alias for getSingleResult(HYDRATE_SINGLE_SCALAR).
|
||||
*
|
||||
* @return mixed The scalar result.
|
||||
* @return bool|float|int|string|null The scalar result.
|
||||
*
|
||||
* @throws NoResultException If the query returned no result.
|
||||
* @throws NonUniqueResultException If the query result is not unique.
|
||||
|
||||
@@ -4,12 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use Doctrine\Common\Util\ClassUtils;
|
||||
use Doctrine\ORM\Cache;
|
||||
use Doctrine\ORM\Cache\Persister\CachedPersister;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\ORMInvalidArgumentException;
|
||||
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
|
||||
use function is_array;
|
||||
@@ -293,7 +293,7 @@ class DefaultCache implements Cache
|
||||
private function toIdentifierArray(ClassMetadata $metadata, $identifier): array
|
||||
{
|
||||
if (is_object($identifier)) {
|
||||
$class = ClassUtils::getClass($identifier);
|
||||
$class = DefaultProxyClassNameResolver::getClass($identifier);
|
||||
if ($this->em->getMetadataFactory()->hasMetadataFor($class)) {
|
||||
$identifier = $this->uow->getSingleIdentifierValue($identifier);
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use Doctrine\Common\Util\ClassUtils;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
use Doctrine\ORM\Utility\IdentifierFlattener;
|
||||
@@ -112,7 +112,7 @@ class DefaultEntityHydrator implements EntityHydrator
|
||||
}
|
||||
|
||||
if (! isset($assoc['id'])) {
|
||||
$targetClass = ClassUtils::getClass($data[$name]);
|
||||
$targetClass = DefaultProxyClassNameResolver::getClass($data[$name]);
|
||||
$targetId = $this->uow->getEntityIdentifier($data[$name]);
|
||||
$data[$name] = new AssociationCacheEntry($targetClass, $targetId);
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
use Doctrine\Persistence\Proxy;
|
||||
|
||||
use function array_map;
|
||||
use function array_shift;
|
||||
@@ -345,7 +344,7 @@ class DefaultQueryCache implements QueryCache
|
||||
$assocIdentifier = $this->uow->getEntityIdentifier($assocValue);
|
||||
$entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier);
|
||||
|
||||
if (! $assocValue instanceof Proxy && ($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
|
||||
if (! $this->uow->isUninitializedObject($assocValue) && ($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
|
||||
// Entity put fail
|
||||
if (! $assocPersister->storeEntityCache($assocValue, $entityKey)) {
|
||||
return null;
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace Doctrine\ORM\Cache\Persister\Collection;
|
||||
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Common\Util\ClassUtils;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\Cache\CollectionCacheKey;
|
||||
use Doctrine\ORM\Cache\CollectionHydrator;
|
||||
@@ -19,6 +18,7 @@ use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\ClassMetadataFactory;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
|
||||
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
|
||||
use function array_values;
|
||||
@@ -148,7 +148,7 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister
|
||||
}
|
||||
|
||||
$class = $this->targetEntity;
|
||||
$className = ClassUtils::getClass($elements[$index]);
|
||||
$className = DefaultProxyClassNameResolver::getClass($elements[$index]);
|
||||
|
||||
if ($className !== $this->targetEntity->name) {
|
||||
$class = $this->metadataFactory->getMetadataFor($className);
|
||||
|
||||
@@ -4,9 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Persister\Collection;
|
||||
|
||||
use Doctrine\Common\Util\ClassUtils;
|
||||
use Doctrine\ORM\Cache\Exception\CannotUpdateReadOnlyCollection;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
|
||||
|
||||
class ReadOnlyCachedCollectionPersister extends NonStrictReadWriteCachedCollectionPersister
|
||||
{
|
||||
@@ -17,7 +17,7 @@ class ReadOnlyCachedCollectionPersister extends NonStrictReadWriteCachedCollecti
|
||||
{
|
||||
if ($collection->isDirty() && $collection->getSnapshot()) {
|
||||
throw CannotUpdateReadOnlyCollection::fromEntityAndField(
|
||||
ClassUtils::getClass($collection->getOwner()),
|
||||
DefaultProxyClassNameResolver::getClass($collection->getOwner()),
|
||||
$this->association['fieldName']
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace Doctrine\ORM\Cache\Persister\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Common\Util\ClassUtils;
|
||||
use Doctrine\ORM\Cache;
|
||||
use Doctrine\ORM\Cache\CollectionCacheKey;
|
||||
use Doctrine\ORM\Cache\EntityCacheKey;
|
||||
@@ -21,8 +20,10 @@ use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\ClassMetadataFactory;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\Persisters\Entity\EntityPersister;
|
||||
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
|
||||
use function array_merge;
|
||||
use function assert;
|
||||
use function serialize;
|
||||
use function sha1;
|
||||
@@ -189,7 +190,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
|
||||
public function storeEntityCache($entity, EntityCacheKey $key)
|
||||
{
|
||||
$class = $this->class;
|
||||
$className = ClassUtils::getClass($entity);
|
||||
$className = DefaultProxyClassNameResolver::getClass($entity);
|
||||
|
||||
if ($className !== $this->class->name) {
|
||||
$class = $this->metadataFactory->getMetadataFor($className);
|
||||
@@ -314,7 +315,13 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
|
||||
*/
|
||||
public function executeInserts()
|
||||
{
|
||||
$this->queuedCache['insert'] = $this->persister->getInserts();
|
||||
// The commit order/foreign key relationships may make it necessary that multiple calls to executeInsert()
|
||||
// are performed, so collect all the new entities.
|
||||
$newInserts = $this->persister->getInserts();
|
||||
|
||||
if ($newInserts) {
|
||||
$this->queuedCache['insert'] = array_merge($this->queuedCache['insert'] ?? [], $newInserts);
|
||||
}
|
||||
|
||||
return $this->persister->executeInserts();
|
||||
}
|
||||
@@ -431,7 +438,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
|
||||
}
|
||||
|
||||
$class = $this->class;
|
||||
$className = ClassUtils::getClass($entity);
|
||||
$className = DefaultProxyClassNameResolver::getClass($entity);
|
||||
|
||||
if ($className !== $this->class->name) {
|
||||
$class = $this->metadataFactory->getMetadataFor($className);
|
||||
|
||||
@@ -4,8 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Persister\Entity;
|
||||
|
||||
use Doctrine\Common\Util\ClassUtils;
|
||||
use Doctrine\ORM\Cache\Exception\CannotUpdateReadOnlyEntity;
|
||||
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
|
||||
|
||||
/**
|
||||
* Specific read-only region entity persister
|
||||
@@ -17,6 +17,6 @@ class ReadOnlyCachedEntityPersister extends NonStrictReadWriteCachedEntityPersis
|
||||
*/
|
||||
public function update($entity)
|
||||
{
|
||||
throw CannotUpdateReadOnlyEntity::fromEntity(ClassUtils::getClass($entity));
|
||||
throw CannotUpdateReadOnlyEntity::fromEntity(DefaultProxyClassNameResolver::getClass($entity));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use Doctrine\Common\Cache\Cache as CacheDriver;
|
||||
use Doctrine\Common\Cache\Psr6\CacheAdapter;
|
||||
use Doctrine\Common\Cache\Psr6\DoctrineProvider;
|
||||
use Doctrine\Common\Persistence\PersistentObject;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\Cache\CacheConfiguration;
|
||||
use Doctrine\ORM\Cache\Exception\CacheException;
|
||||
@@ -27,6 +28,7 @@ use Doctrine\ORM\Exception\NotSupported;
|
||||
use Doctrine\ORM\Exception\ProxyClassesAlwaysRegenerating;
|
||||
use Doctrine\ORM\Exception\UnknownEntityNamespace;
|
||||
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\ClassMetadataFactory;
|
||||
use Doctrine\ORM\Mapping\DefaultEntityListenerResolver;
|
||||
use Doctrine\ORM\Mapping\DefaultNamingStrategy;
|
||||
@@ -62,14 +64,27 @@ use function trim;
|
||||
* It combines all configuration options from DBAL & ORM.
|
||||
*
|
||||
* Internal note: When adding a new configuration option just write a getter/setter pair.
|
||||
*
|
||||
* @psalm-import-type AutogenerateMode from ProxyFactory
|
||||
*/
|
||||
class Configuration extends \Doctrine\DBAL\Configuration
|
||||
{
|
||||
/** @var mixed[] */
|
||||
protected $_attributes = [];
|
||||
|
||||
/** @psalm-var array<class-string<AbstractPlatform>, ClassMetadata::GENERATOR_TYPE_*> */
|
||||
private $identityGenerationPreferences = [];
|
||||
|
||||
/** @psalm-param array<class-string<AbstractPlatform>, ClassMetadata::GENERATOR_TYPE_*> $value */
|
||||
public function setIdentityGenerationPreferences(array $value): void
|
||||
{
|
||||
$this->identityGenerationPreferences = $value;
|
||||
}
|
||||
|
||||
/** @psalm-return array<class-string<AbstractPlatform>, ClassMetadata::GENERATOR_TYPE_*> $value */
|
||||
public function getIdentityGenerationPreferences(): array
|
||||
{
|
||||
return $this->identityGenerationPreferences;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the directory where Doctrine generates any necessary proxy class files.
|
||||
*
|
||||
@@ -95,8 +110,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
/**
|
||||
* Gets the strategy for automatically generating proxy classes.
|
||||
*
|
||||
* @return int Possible values are constants of Doctrine\ORM\Proxy\ProxyFactory.
|
||||
* @psalm-return AutogenerateMode
|
||||
* @return ProxyFactory::AUTOGENERATE_*
|
||||
*/
|
||||
public function getAutoGenerateProxyClasses()
|
||||
{
|
||||
@@ -106,9 +120,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
/**
|
||||
* Sets the strategy for automatically generating proxy classes.
|
||||
*
|
||||
* @param bool|int $autoGenerate Possible values are constants of Doctrine\ORM\Proxy\ProxyFactory.
|
||||
* @psalm-param bool|AutogenerateMode $autoGenerate
|
||||
* True is converted to AUTOGENERATE_ALWAYS, false to AUTOGENERATE_NEVER.
|
||||
* @param bool|ProxyFactory::AUTOGENERATE_* $autoGenerate True is converted to AUTOGENERATE_ALWAYS, false to AUTOGENERATE_NEVER.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
@@ -164,7 +176,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
*
|
||||
* @return AnnotationDriver
|
||||
*/
|
||||
public function newDefaultAnnotationDriver($paths = [], $useSimpleAnnotationReader = true)
|
||||
public function newDefaultAnnotationDriver($paths = [], $useSimpleAnnotationReader = true, bool $reportFieldsWhereDeclared = false)
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
@@ -203,7 +215,8 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
|
||||
return new AnnotationDriver(
|
||||
$reader,
|
||||
(array) $paths
|
||||
(array) $paths,
|
||||
$reportFieldsWhereDeclared
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1121,4 +1134,24 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
|
||||
$this->_attributes['isLazyGhostObjectEnabled'] = $flag;
|
||||
}
|
||||
|
||||
public function setRejectIdCollisionInIdentityMap(bool $flag): void
|
||||
{
|
||||
$this->_attributes['rejectIdCollisionInIdentityMap'] = $flag;
|
||||
}
|
||||
|
||||
public function isRejectIdCollisionInIdentityMapEnabled(): bool
|
||||
{
|
||||
return $this->_attributes['rejectIdCollisionInIdentityMap'] ?? false;
|
||||
}
|
||||
|
||||
public function setEagerFetchBatchSize(int $batchSize = 100): void
|
||||
{
|
||||
$this->_attributes['fetchModeSubselectBatchSize'] = $batchSize;
|
||||
}
|
||||
|
||||
public function getEagerFetchBatchSize(): int
|
||||
{
|
||||
return $this->_attributes['fetchModeSubselectBatchSize'] ?? 100;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Decorator;
|
||||
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
@@ -170,6 +171,13 @@ abstract class EntityManagerDecorator extends ObjectManagerDecorator implements
|
||||
*/
|
||||
public function getPartialReference($entityName, $identifier)
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/10987',
|
||||
'Method %s is deprecated and will be removed in 3.0.',
|
||||
__METHOD__
|
||||
);
|
||||
|
||||
return $this->wrapped->getPartialReference($entityName, $identifier);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ use BadMethodCallException;
|
||||
use Doctrine\Common\Cache\Psr6\CacheAdapter;
|
||||
use Doctrine\Common\EventManager;
|
||||
use Doctrine\Common\Persistence\PersistentObject;
|
||||
use Doctrine\Common\Util\ClassUtils;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\DriverManager;
|
||||
use Doctrine\DBAL\LockMode;
|
||||
@@ -24,6 +23,7 @@ use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\Exception\UnrecognizedIdentifierFields;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\ClassMetadataFactory;
|
||||
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
|
||||
use Doctrine\ORM\Proxy\ProxyFactory;
|
||||
use Doctrine\ORM\Query\Expr;
|
||||
use Doctrine\ORM\Query\FilterCollection;
|
||||
@@ -444,7 +444,7 @@ class EntityManager implements EntityManagerInterface
|
||||
|
||||
foreach ($id as $i => $value) {
|
||||
if (is_object($value)) {
|
||||
$className = ClassUtils::getClass($value);
|
||||
$className = DefaultProxyClassNameResolver::getClass($value);
|
||||
if ($this->metadataFactory->hasMetadataFor($className)) {
|
||||
$id[$i] = $this->unitOfWork->getSingleIdentifierValue($value);
|
||||
|
||||
@@ -571,6 +571,12 @@ class EntityManager implements EntityManagerInterface
|
||||
*/
|
||||
public function getPartialReference($entityName, $identifier)
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/10987',
|
||||
'Method %s is deprecated and will be removed in 3.0.',
|
||||
__METHOD__
|
||||
);
|
||||
$class = $this->metadataFactory->getMetadataFor(ltrim($entityName, '\\'));
|
||||
|
||||
$entity = $this->unitOfWork->tryGetById($identifier, $class->rootEntityName);
|
||||
@@ -953,6 +959,14 @@ class EntityManager implements EntityManagerInterface
|
||||
$this->unitOfWork->initializeObject($obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isUninitializedObject($obj): bool
|
||||
{
|
||||
return $this->unitOfWork->isUninitializedObject($obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create EntityManager instances.
|
||||
*
|
||||
@@ -973,7 +987,7 @@ class EntityManager implements EntityManagerInterface
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/9961',
|
||||
'%s() is deprecated. To boostrap a DBAL connection, call %s::getConnection() instead. Use the constructor to create an instance of %s.',
|
||||
'%s() is deprecated. To bootstrap a DBAL connection, call %s::getConnection() instead. Use the constructor to create an instance of %s.',
|
||||
__METHOD__,
|
||||
DriverManager::class,
|
||||
self::class
|
||||
|
||||
@@ -200,6 +200,8 @@ interface EntityManagerInterface extends ObjectManager
|
||||
* never be visible to the application (especially not event listeners) as it will
|
||||
* never be loaded in the first place.
|
||||
*
|
||||
* @deprecated 2.7 This method is being removed from the ORM and won't have any replacement
|
||||
*
|
||||
* @param string $entityName The name of the entity type.
|
||||
* @param mixed $identifier The entity identifier.
|
||||
* @psalm-param class-string<T> $entityName
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Exception;
|
||||
|
||||
use function get_class;
|
||||
use function sprintf;
|
||||
|
||||
final class EntityIdentityCollisionException extends ORMException
|
||||
{
|
||||
/**
|
||||
* @param object $existingEntity
|
||||
* @param object $newEntity
|
||||
*/
|
||||
public static function create($existingEntity, $newEntity, string $idHash): self
|
||||
{
|
||||
return new self(
|
||||
sprintf(
|
||||
<<<'EXCEPTION'
|
||||
While adding an entity of class %s with an ID hash of "%s" to the identity map,
|
||||
another object of class %s was already present for the same ID. This exception
|
||||
is a safeguard against an internal inconsistency - IDs should uniquely map to
|
||||
entity object instances. This problem may occur if:
|
||||
|
||||
- you use application-provided IDs and reuse ID values;
|
||||
- database-provided IDs are reassigned after truncating the database without
|
||||
clearing the EntityManager;
|
||||
- you might have been using EntityManager#getReference() to create a reference
|
||||
for a nonexistent ID that was subsequently (by the RDBMS) assigned to another
|
||||
entity.
|
||||
|
||||
Otherwise, it might be an ORM-internal inconsistency, please report it.
|
||||
EXCEPTION
|
||||
,
|
||||
get_class($newEntity),
|
||||
$idHash,
|
||||
get_class($existingEntity)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Internal\CommitOrder;
|
||||
|
||||
/** @internal */
|
||||
final class Edge
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @readonly
|
||||
*/
|
||||
public $from;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @readonly
|
||||
*/
|
||||
public $to;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
* @readonly
|
||||
*/
|
||||
public $weight;
|
||||
|
||||
public function __construct(string $from, string $to, int $weight)
|
||||
{
|
||||
$this->from = $from;
|
||||
$this->to = $to;
|
||||
$this->weight = $weight;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Internal\CommitOrder;
|
||||
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
|
||||
/** @internal */
|
||||
final class Vertex
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @readonly
|
||||
*/
|
||||
public $hash;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
* @psalm-var VertexState::*
|
||||
*/
|
||||
public $state = VertexState::NOT_VISITED;
|
||||
|
||||
/**
|
||||
* @var ClassMetadata
|
||||
* @readonly
|
||||
*/
|
||||
public $value;
|
||||
|
||||
/** @var array<string, Edge> */
|
||||
public $dependencyList = [];
|
||||
|
||||
public function __construct(string $hash, ClassMetadata $value)
|
||||
{
|
||||
$this->hash = $hash;
|
||||
$this->value = $value;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Internal\CommitOrder;
|
||||
|
||||
/** @internal */
|
||||
final class VertexState
|
||||
{
|
||||
public const NOT_VISITED = 0;
|
||||
public const IN_PROGRESS = 1;
|
||||
public const VISITED = 2;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Internal;
|
||||
|
||||
use Doctrine\ORM\Internal\CommitOrder\Edge;
|
||||
use Doctrine\ORM\Internal\CommitOrder\Vertex;
|
||||
use Doctrine\ORM\Internal\CommitOrder\VertexState;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
|
||||
use function array_reverse;
|
||||
|
||||
/**
|
||||
* CommitOrderCalculator implements topological sorting, which is an ordering
|
||||
* algorithm for directed graphs (DG) and/or directed acyclic graphs (DAG) by
|
||||
* using a depth-first searching (DFS) to traverse the graph built in memory.
|
||||
* This algorithm have a linear running time based on nodes (V) and dependency
|
||||
* between the nodes (E), resulting in a computational complexity of O(V + E).
|
||||
*/
|
||||
class CommitOrderCalculator
|
||||
{
|
||||
/** @deprecated */
|
||||
public const NOT_VISITED = VertexState::NOT_VISITED;
|
||||
|
||||
/** @deprecated */
|
||||
public const IN_PROGRESS = VertexState::IN_PROGRESS;
|
||||
|
||||
/** @deprecated */
|
||||
public const VISITED = VertexState::VISITED;
|
||||
|
||||
/**
|
||||
* Matrix of nodes (aka. vertex).
|
||||
*
|
||||
* Keys are provided hashes and values are the node definition objects.
|
||||
*
|
||||
* @var array<string, Vertex>
|
||||
*/
|
||||
private $nodeList = [];
|
||||
|
||||
/**
|
||||
* Volatile variable holding calculated nodes during sorting process.
|
||||
*
|
||||
* @psalm-var list<ClassMetadata>
|
||||
*/
|
||||
private $sortedNodeList = [];
|
||||
|
||||
/**
|
||||
* Checks for node (vertex) existence in graph.
|
||||
*
|
||||
* @param string $hash
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasNode($hash)
|
||||
{
|
||||
return isset($this->nodeList[$hash]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new node (vertex) to the graph, assigning its hash and value.
|
||||
*
|
||||
* @param string $hash
|
||||
* @param ClassMetadata $node
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addNode($hash, $node)
|
||||
{
|
||||
$this->nodeList[$hash] = new Vertex($hash, $node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new dependency (edge) to the graph using their hashes.
|
||||
*
|
||||
* @param string $fromHash
|
||||
* @param string $toHash
|
||||
* @param int $weight
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addDependency($fromHash, $toHash, $weight)
|
||||
{
|
||||
$this->nodeList[$fromHash]->dependencyList[$toHash]
|
||||
= new Edge($fromHash, $toHash, $weight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a valid order list of all current nodes.
|
||||
* The desired topological sorting is the reverse post order of these searches.
|
||||
*
|
||||
* {@internal Highly performance-sensitive method.}
|
||||
*
|
||||
* @psalm-return list<ClassMetadata>
|
||||
*/
|
||||
public function sort()
|
||||
{
|
||||
foreach ($this->nodeList as $vertex) {
|
||||
if ($vertex->state !== VertexState::NOT_VISITED) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->visit($vertex);
|
||||
}
|
||||
|
||||
$sortedList = $this->sortedNodeList;
|
||||
|
||||
$this->nodeList = [];
|
||||
$this->sortedNodeList = [];
|
||||
|
||||
return array_reverse($sortedList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit a given node definition for reordering.
|
||||
*
|
||||
* {@internal Highly performance-sensitive method.}
|
||||
*/
|
||||
private function visit(Vertex $vertex): void
|
||||
{
|
||||
$vertex->state = VertexState::IN_PROGRESS;
|
||||
|
||||
foreach ($vertex->dependencyList as $edge) {
|
||||
$adjacentVertex = $this->nodeList[$edge->to];
|
||||
|
||||
switch ($adjacentVertex->state) {
|
||||
case VertexState::VISITED:
|
||||
// Do nothing, since node was already visited
|
||||
break;
|
||||
|
||||
case VertexState::IN_PROGRESS:
|
||||
if (
|
||||
isset($adjacentVertex->dependencyList[$vertex->hash]) &&
|
||||
$adjacentVertex->dependencyList[$vertex->hash]->weight < $edge->weight
|
||||
) {
|
||||
// If we have some non-visited dependencies in the in-progress dependency, we
|
||||
// need to visit them before adding the node.
|
||||
foreach ($adjacentVertex->dependencyList as $adjacentEdge) {
|
||||
$adjacentEdgeVertex = $this->nodeList[$adjacentEdge->to];
|
||||
|
||||
if ($adjacentEdgeVertex->state === VertexState::NOT_VISITED) {
|
||||
$this->visit($adjacentEdgeVertex);
|
||||
}
|
||||
}
|
||||
|
||||
$adjacentVertex->state = VertexState::VISITED;
|
||||
|
||||
$this->sortedNodeList[] = $adjacentVertex->value;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case VertexState::NOT_VISITED:
|
||||
$this->visit($adjacentVertex);
|
||||
}
|
||||
}
|
||||
|
||||
if ($vertex->state !== VertexState::VISITED) {
|
||||
$vertex->state = VertexState::VISITED;
|
||||
|
||||
$this->sortedNodeList[] = $vertex->value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
use Doctrine\Persistence\Proxy;
|
||||
|
||||
use function array_fill_keys;
|
||||
use function array_keys;
|
||||
@@ -439,7 +438,7 @@ class ObjectHydrator extends AbstractHydrator
|
||||
// PATH B: Single-valued association
|
||||
$reflFieldValue = $reflField->getValue($parentObject);
|
||||
|
||||
if (! $reflFieldValue || isset($this->_hints[Query::HINT_REFRESH]) || ($reflFieldValue instanceof Proxy && ! $reflFieldValue->__isInitialized())) {
|
||||
if (! $reflFieldValue || isset($this->_hints[Query::HINT_REFRESH]) || $this->_uow->isUninitializedObject($reflFieldValue)) {
|
||||
// we only need to take action if this value is null,
|
||||
// we refresh the entity or its an uninitialized proxy.
|
||||
if (isset($nonemptyComponents[$dqlAlias])) {
|
||||
@@ -457,9 +456,6 @@ class ObjectHydrator extends AbstractHydrator
|
||||
$targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($element, $parentObject);
|
||||
$this->_uow->setOriginalEntityProperty(spl_object_id($element), $inverseAssoc['fieldName'], $parentObject);
|
||||
}
|
||||
} elseif ($parentClass === $targetClass && $relation['mappedBy']) {
|
||||
// Special case: bi-directional self-referencing one-one on the same class
|
||||
$targetClass->reflFields[$relationField]->setValue($element, $parentObject);
|
||||
}
|
||||
} else {
|
||||
// For sure bidirectional, as there is no inverse side in unidirectional mappings
|
||||
|
||||
170
lib/Doctrine/ORM/Internal/StronglyConnectedComponents.php
Normal file
170
lib/Doctrine/ORM/Internal/StronglyConnectedComponents.php
Normal file
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Internal;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
use function array_keys;
|
||||
use function array_pop;
|
||||
use function array_push;
|
||||
use function min;
|
||||
use function spl_object_id;
|
||||
|
||||
/**
|
||||
* StronglyConnectedComponents implements Tarjan's algorithm to find strongly connected
|
||||
* components (SCC) in a directed graph. This algorithm has a linear running time based on
|
||||
* nodes (V) and edges between the nodes (E), resulting in a computational complexity
|
||||
* of O(V + E).
|
||||
*
|
||||
* See https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
|
||||
* for an explanation and the meaning of the DFS and lowlink numbers.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class StronglyConnectedComponents
|
||||
{
|
||||
private const NOT_VISITED = 1;
|
||||
private const IN_PROGRESS = 2;
|
||||
private const VISITED = 3;
|
||||
|
||||
/**
|
||||
* Array of all nodes, indexed by object ids.
|
||||
*
|
||||
* @var array<int, object>
|
||||
*/
|
||||
private $nodes = [];
|
||||
|
||||
/**
|
||||
* DFS state for the different nodes, indexed by node object id and using one of
|
||||
* this class' constants as value.
|
||||
*
|
||||
* @var array<int, self::*>
|
||||
*/
|
||||
private $states = [];
|
||||
|
||||
/**
|
||||
* Edges between the nodes. The first-level key is the object id of the outgoing
|
||||
* node; the second array maps the destination node by object id as key.
|
||||
*
|
||||
* @var array<int, array<int, bool>>
|
||||
*/
|
||||
private $edges = [];
|
||||
|
||||
/**
|
||||
* DFS numbers, by object ID
|
||||
*
|
||||
* @var array<int, int>
|
||||
*/
|
||||
private $dfs = [];
|
||||
|
||||
/**
|
||||
* lowlink numbers, by object ID
|
||||
*
|
||||
* @var array<int, int>
|
||||
*/
|
||||
private $lowlink = [];
|
||||
|
||||
/** @var int */
|
||||
private $maxdfs = 0;
|
||||
|
||||
/**
|
||||
* Nodes representing the SCC another node is in, indexed by lookup-node object ID
|
||||
*
|
||||
* @var array<int, object>
|
||||
*/
|
||||
private $representingNodes = [];
|
||||
|
||||
/**
|
||||
* Stack with OIDs of nodes visited in the current state of the DFS
|
||||
*
|
||||
* @var list<int>
|
||||
*/
|
||||
private $stack = [];
|
||||
|
||||
/** @param object $node */
|
||||
public function addNode($node): void
|
||||
{
|
||||
$id = spl_object_id($node);
|
||||
$this->nodes[$id] = $node;
|
||||
$this->states[$id] = self::NOT_VISITED;
|
||||
$this->edges[$id] = [];
|
||||
}
|
||||
|
||||
/** @param object $node */
|
||||
public function hasNode($node): bool
|
||||
{
|
||||
return isset($this->nodes[spl_object_id($node)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new edge between two nodes to the graph
|
||||
*
|
||||
* @param object $from
|
||||
* @param object $to
|
||||
*/
|
||||
public function addEdge($from, $to): void
|
||||
{
|
||||
$fromId = spl_object_id($from);
|
||||
$toId = spl_object_id($to);
|
||||
|
||||
$this->edges[$fromId][$toId] = true;
|
||||
}
|
||||
|
||||
public function findStronglyConnectedComponents(): void
|
||||
{
|
||||
foreach (array_keys($this->nodes) as $oid) {
|
||||
if ($this->states[$oid] === self::NOT_VISITED) {
|
||||
$this->tarjan($oid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function tarjan(int $oid): void
|
||||
{
|
||||
$this->dfs[$oid] = $this->lowlink[$oid] = $this->maxdfs++;
|
||||
$this->states[$oid] = self::IN_PROGRESS;
|
||||
array_push($this->stack, $oid);
|
||||
|
||||
foreach ($this->edges[$oid] as $adjacentId => $ignored) {
|
||||
if ($this->states[$adjacentId] === self::NOT_VISITED) {
|
||||
$this->tarjan($adjacentId);
|
||||
$this->lowlink[$oid] = min($this->lowlink[$oid], $this->lowlink[$adjacentId]);
|
||||
} elseif ($this->states[$adjacentId] === self::IN_PROGRESS) {
|
||||
$this->lowlink[$oid] = min($this->lowlink[$oid], $this->dfs[$adjacentId]);
|
||||
}
|
||||
}
|
||||
|
||||
$lowlink = $this->lowlink[$oid];
|
||||
if ($lowlink === $this->dfs[$oid]) {
|
||||
$representingNode = null;
|
||||
do {
|
||||
$unwindOid = array_pop($this->stack);
|
||||
|
||||
if (! $representingNode) {
|
||||
$representingNode = $this->nodes[$unwindOid];
|
||||
}
|
||||
|
||||
$this->representingNodes[$unwindOid] = $representingNode;
|
||||
$this->states[$unwindOid] = self::VISITED;
|
||||
} while ($unwindOid !== $oid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $node
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function getNodeRepresentingStronglyConnectedComponent($node)
|
||||
{
|
||||
$oid = spl_object_id($node);
|
||||
|
||||
if (! isset($this->representingNodes[$oid])) {
|
||||
throw new InvalidArgumentException('unknown node');
|
||||
}
|
||||
|
||||
return $this->representingNodes[$oid];
|
||||
}
|
||||
}
|
||||
159
lib/Doctrine/ORM/Internal/TopologicalSort.php
Normal file
159
lib/Doctrine/ORM/Internal/TopologicalSort.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Internal;
|
||||
|
||||
use Doctrine\ORM\Internal\TopologicalSort\CycleDetectedException;
|
||||
|
||||
use function array_keys;
|
||||
use function spl_object_id;
|
||||
|
||||
/**
|
||||
* TopologicalSort implements topological sorting, which is an ordering
|
||||
* algorithm for directed graphs (DG) using a depth-first searching (DFS)
|
||||
* to traverse the graph built in memory.
|
||||
* This algorithm has a linear running time based on nodes (V) and edges
|
||||
* between the nodes (E), resulting in a computational complexity of O(V + E).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class TopologicalSort
|
||||
{
|
||||
private const NOT_VISITED = 1;
|
||||
private const IN_PROGRESS = 2;
|
||||
private const VISITED = 3;
|
||||
|
||||
/**
|
||||
* Array of all nodes, indexed by object ids.
|
||||
*
|
||||
* @var array<int, object>
|
||||
*/
|
||||
private $nodes = [];
|
||||
|
||||
/**
|
||||
* DFS state for the different nodes, indexed by node object id and using one of
|
||||
* this class' constants as value.
|
||||
*
|
||||
* @var array<int, self::*>
|
||||
*/
|
||||
private $states = [];
|
||||
|
||||
/**
|
||||
* Edges between the nodes. The first-level key is the object id of the outgoing
|
||||
* node; the second array maps the destination node by object id as key. The final
|
||||
* boolean value indicates whether the edge is optional or not.
|
||||
*
|
||||
* @var array<int, array<int, bool>>
|
||||
*/
|
||||
private $edges = [];
|
||||
|
||||
/**
|
||||
* Builds up the result during the DFS.
|
||||
*
|
||||
* @var list<object>
|
||||
*/
|
||||
private $sortResult = [];
|
||||
|
||||
/** @param object $node */
|
||||
public function addNode($node): void
|
||||
{
|
||||
$id = spl_object_id($node);
|
||||
$this->nodes[$id] = $node;
|
||||
$this->states[$id] = self::NOT_VISITED;
|
||||
$this->edges[$id] = [];
|
||||
}
|
||||
|
||||
/** @param object $node */
|
||||
public function hasNode($node): bool
|
||||
{
|
||||
return isset($this->nodes[spl_object_id($node)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new edge between two nodes to the graph
|
||||
*
|
||||
* @param object $from
|
||||
* @param object $to
|
||||
* @param bool $optional This indicates whether the edge may be ignored during the topological sort if it is necessary to break cycles.
|
||||
*/
|
||||
public function addEdge($from, $to, bool $optional): void
|
||||
{
|
||||
$fromId = spl_object_id($from);
|
||||
$toId = spl_object_id($to);
|
||||
|
||||
if (isset($this->edges[$fromId][$toId]) && $this->edges[$fromId][$toId] === false) {
|
||||
return; // we already know about this dependency, and it is not optional
|
||||
}
|
||||
|
||||
$this->edges[$fromId][$toId] = $optional;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a topological sort of all nodes. When we have an edge A->B between two nodes
|
||||
* A and B, then B will be listed before A in the result. Visually speaking, when ordering
|
||||
* the nodes in the result order from left to right, all edges point to the left.
|
||||
*
|
||||
* @return list<object>
|
||||
*/
|
||||
public function sort(): array
|
||||
{
|
||||
foreach (array_keys($this->nodes) as $oid) {
|
||||
if ($this->states[$oid] === self::NOT_VISITED) {
|
||||
$this->visit($oid);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->sortResult;
|
||||
}
|
||||
|
||||
private function visit(int $oid): void
|
||||
{
|
||||
if ($this->states[$oid] === self::IN_PROGRESS) {
|
||||
// This node is already on the current DFS stack. We've found a cycle!
|
||||
throw new CycleDetectedException($this->nodes[$oid]);
|
||||
}
|
||||
|
||||
if ($this->states[$oid] === self::VISITED) {
|
||||
// We've reached a node that we've already seen, including all
|
||||
// other nodes that are reachable from here. We're done here, return.
|
||||
return;
|
||||
}
|
||||
|
||||
$this->states[$oid] = self::IN_PROGRESS;
|
||||
|
||||
// Continue the DFS downwards the edge list
|
||||
foreach ($this->edges[$oid] as $adjacentId => $optional) {
|
||||
try {
|
||||
$this->visit($adjacentId);
|
||||
} catch (CycleDetectedException $exception) {
|
||||
if ($exception->isCycleCollected()) {
|
||||
// There is a complete cycle downstream of the current node. We cannot
|
||||
// do anything about that anymore.
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
if ($optional) {
|
||||
// The current edge is part of a cycle, but it is optional and the closest
|
||||
// such edge while backtracking. Break the cycle here by skipping the edge
|
||||
// and continuing with the next one.
|
||||
continue;
|
||||
}
|
||||
|
||||
// We have found a cycle and cannot break it at $edge. Best we can do
|
||||
// is to backtrack from the current vertex, hoping that somewhere up the
|
||||
// stack this can be salvaged.
|
||||
$this->states[$oid] = self::NOT_VISITED;
|
||||
$exception->addToCycle($this->nodes[$oid]);
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
// We have traversed all edges and visited all other nodes reachable from here.
|
||||
// So we're done with this vertex as well.
|
||||
|
||||
$this->states[$oid] = self::VISITED;
|
||||
$this->sortResult[] = $this->nodes[$oid];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Internal\TopologicalSort;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
use function array_unshift;
|
||||
|
||||
class CycleDetectedException extends RuntimeException
|
||||
{
|
||||
/** @var list<object> */
|
||||
private $cycle;
|
||||
|
||||
/** @var object */
|
||||
private $startNode;
|
||||
|
||||
/**
|
||||
* Do we have the complete cycle collected?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $cycleCollected = false;
|
||||
|
||||
/** @param object $startNode */
|
||||
public function __construct($startNode)
|
||||
{
|
||||
parent::__construct('A cycle has been detected, so a topological sort is not possible. The getCycle() method provides the list of nodes that form the cycle.');
|
||||
|
||||
$this->startNode = $startNode;
|
||||
$this->cycle = [$startNode];
|
||||
}
|
||||
|
||||
/** @return list<object> */
|
||||
public function getCycle(): array
|
||||
{
|
||||
return $this->cycle;
|
||||
}
|
||||
|
||||
/** @param object $node */
|
||||
public function addToCycle($node): void
|
||||
{
|
||||
array_unshift($this->cycle, $node);
|
||||
|
||||
if ($node === $this->startNode) {
|
||||
$this->cycleCollected = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function isCycleCollected(): bool
|
||||
{
|
||||
return $this->cycleCollected;
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ use Doctrine\ORM\Id\UuidGenerator;
|
||||
use Doctrine\ORM\Mapping\Exception\CannotGenerateIds;
|
||||
use Doctrine\ORM\Mapping\Exception\InvalidCustomGenerator;
|
||||
use Doctrine\ORM\Mapping\Exception\UnknownGeneratorType;
|
||||
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
|
||||
use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory;
|
||||
use Doctrine\Persistence\Mapping\ClassMetadata as ClassMetadataInterface;
|
||||
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
|
||||
@@ -38,6 +39,7 @@ use function end;
|
||||
use function explode;
|
||||
use function get_class;
|
||||
use function in_array;
|
||||
use function is_a;
|
||||
use function is_subclass_of;
|
||||
use function str_contains;
|
||||
use function strlen;
|
||||
@@ -71,9 +73,17 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
|
||||
/** @var mixed[] */
|
||||
private $embeddablesActiveNesting = [];
|
||||
|
||||
private const NON_IDENTITY_DEFAULT_STRATEGY = [
|
||||
'Doctrine\DBAL\Platforms\PostgreSqlPlatform' => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
|
||||
Platforms\OraclePlatform::class => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
|
||||
Platforms\PostgreSQLPlatform::class => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
|
||||
];
|
||||
|
||||
/** @return void */
|
||||
public function setEntityManager(EntityManagerInterface $em)
|
||||
{
|
||||
parent::setProxyClassNameResolver(new DefaultProxyClassNameResolver());
|
||||
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
@@ -634,7 +644,7 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
|
||||
'https://github.com/doctrine/orm/issues/8850',
|
||||
<<<'DEPRECATION'
|
||||
Context: Loading metadata for class %s
|
||||
Problem: Using the IDENTITY generator strategy with platform "%s" is deprecated and will not be possible in Doctrine ORM 3.0.
|
||||
Problem: Using identity columns emulated with a sequence is deprecated and will not be possible in Doctrine ORM 3.0.
|
||||
Solution: Use the SEQUENCE generator strategy instead.
|
||||
DEPRECATION
|
||||
,
|
||||
@@ -729,14 +739,40 @@ DEPRECATION
|
||||
}
|
||||
}
|
||||
|
||||
/** @psalm-return ClassMetadata::GENERATOR_TYPE_SEQUENCE|ClassMetadata::GENERATOR_TYPE_IDENTITY */
|
||||
/** @psalm-return ClassMetadata::GENERATOR_TYPE_* */
|
||||
private function determineIdGeneratorStrategy(AbstractPlatform $platform): int
|
||||
{
|
||||
if (
|
||||
$platform instanceof Platforms\OraclePlatform
|
||||
|| $platform instanceof Platforms\PostgreSQLPlatform
|
||||
) {
|
||||
return ClassMetadata::GENERATOR_TYPE_SEQUENCE;
|
||||
assert($this->em !== null);
|
||||
foreach ($this->em->getConfiguration()->getIdentityGenerationPreferences() as $platformFamily => $strategy) {
|
||||
if (is_a($platform, $platformFamily)) {
|
||||
return $strategy;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (self::NON_IDENTITY_DEFAULT_STRATEGY as $platformFamily => $strategy) {
|
||||
if (is_a($platform, $platformFamily)) {
|
||||
if ($platform instanceof Platforms\PostgreSQLPlatform || is_a($platform, 'Doctrine\DBAL\Platforms\PostgreSqlPlatform')) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/issues/8893',
|
||||
<<<'DEPRECATION'
|
||||
Relying on non-optimal defaults for ID generation is deprecated, and IDENTITY
|
||||
results in SERIAL, which is not recommended.
|
||||
Instead, configure identifier generation strategies explicitly through
|
||||
configuration.
|
||||
We currently recommend "SEQUENCE" for "%s", so you should use
|
||||
$configuration->setIdentityGenerationPreferences([
|
||||
"%s" => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
|
||||
]);
|
||||
DEPRECATION
|
||||
,
|
||||
$platformFamily,
|
||||
$platformFamily
|
||||
);
|
||||
}
|
||||
|
||||
return $strategy;
|
||||
}
|
||||
}
|
||||
|
||||
if ($platform->supportsIdentityColumns()) {
|
||||
|
||||
@@ -1176,7 +1176,7 @@ class ClassMetadataInfo implements ClassMetadata
|
||||
$this->namespace = $reflService->getClassNamespace($this->name);
|
||||
|
||||
if ($this->reflClass) {
|
||||
$this->name = $this->rootEntityName = $this->reflClass->getName();
|
||||
$this->name = $this->rootEntityName = $this->reflClass->name;
|
||||
}
|
||||
|
||||
$this->table['name'] = $this->namingStrategy->classToTableName($this->name);
|
||||
@@ -2558,6 +2558,10 @@ class ClassMetadataInfo implements ClassMetadata
|
||||
$overrideMapping['id'] = $mapping['id'];
|
||||
}
|
||||
|
||||
if (isset($mapping['declared'])) {
|
||||
$overrideMapping['declared'] = $mapping['declared'];
|
||||
}
|
||||
|
||||
if (! isset($overrideMapping['type'])) {
|
||||
$overrideMapping['type'] = $mapping['type'];
|
||||
}
|
||||
@@ -2764,6 +2768,10 @@ class ClassMetadataInfo implements ClassMetadata
|
||||
$this->fieldMappings[$fieldMapping['fieldName']] = $fieldMapping;
|
||||
$this->columnNames[$fieldMapping['fieldName']] = $fieldMapping['columnName'];
|
||||
$this->fieldNames[$fieldMapping['columnName']] = $fieldMapping['fieldName'];
|
||||
|
||||
if (isset($fieldMapping['generated'])) {
|
||||
$this->requiresFetchAfterChange = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3862,7 +3870,7 @@ class ClassMetadataInfo implements ClassMetadata
|
||||
{
|
||||
$reflectionProperty = $reflService->getAccessibleProperty($class, $field);
|
||||
if ($reflectionProperty !== null && PHP_VERSION_ID >= 80100 && $reflectionProperty->isReadOnly()) {
|
||||
$declaringClass = $reflectionProperty->getDeclaringClass()->name;
|
||||
$declaringClass = $reflectionProperty->class;
|
||||
if ($declaringClass !== $class) {
|
||||
$reflectionProperty = $reflService->getAccessibleProperty($declaringClass, $field);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,15 @@ final class DefaultTypedFieldMapper implements TypedFieldMapper
|
||||
$mapping['enumType'] = $type->getName();
|
||||
|
||||
$reflection = new ReflectionEnum($type->getName());
|
||||
$type = $reflection->getBackingType();
|
||||
if (! $reflection->isBacked()) {
|
||||
throw MappingException::backedEnumTypeRequired(
|
||||
$field->class,
|
||||
$mapping['fieldName'],
|
||||
$mapping['enumType']
|
||||
);
|
||||
}
|
||||
|
||||
$type = $reflection->getBackingType();
|
||||
|
||||
assert($type instanceof ReflectionNamedType);
|
||||
}
|
||||
|
||||
@@ -30,10 +30,13 @@ use function is_numeric;
|
||||
|
||||
/**
|
||||
* The AnnotationDriver reads the mapping metadata from docblock annotations.
|
||||
*
|
||||
* @deprecated This class will be removed in 3.0 without replacement.
|
||||
*/
|
||||
class AnnotationDriver extends CompatibilityAnnotationDriver
|
||||
{
|
||||
use ColocatedMappingDriver;
|
||||
use ReflectionBasedDriver;
|
||||
|
||||
/**
|
||||
* The annotation reader.
|
||||
@@ -60,7 +63,7 @@ class AnnotationDriver extends CompatibilityAnnotationDriver
|
||||
* @param Reader $reader The AnnotationReader to use
|
||||
* @param string|string[]|null $paths One or multiple paths where mapping classes can be found.
|
||||
*/
|
||||
public function __construct($reader, $paths = null)
|
||||
public function __construct($reader, $paths = null, bool $reportFieldsWhereDeclared = false)
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
@@ -70,6 +73,17 @@ class AnnotationDriver extends CompatibilityAnnotationDriver
|
||||
$this->reader = $reader;
|
||||
|
||||
$this->addPaths((array) $paths);
|
||||
|
||||
if (! $reportFieldsWhereDeclared) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/10455',
|
||||
'In ORM 3.0, the AttributeDriver will report fields for the classes where they are declared. This may uncover invalid mapping configurations. To opt into the new mode also with the AnnotationDriver today, set the "reportFieldsWhereDeclared" constructor parameter to true.',
|
||||
self::class
|
||||
);
|
||||
}
|
||||
|
||||
$this->reportFieldsWhereDeclared = $reportFieldsWhereDeclared;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -348,20 +362,12 @@ class AnnotationDriver extends CompatibilityAnnotationDriver
|
||||
|
||||
// Evaluate annotations on properties/fields
|
||||
foreach ($class->getProperties() as $property) {
|
||||
if (
|
||||
$metadata->isMappedSuperclass && ! $property->isPrivate()
|
||||
||
|
||||
$metadata->isInheritedField($property->name)
|
||||
||
|
||||
$metadata->isInheritedAssociation($property->name)
|
||||
||
|
||||
$metadata->isInheritedEmbeddedClass($property->name)
|
||||
) {
|
||||
if ($this->isRepeatedPropertyDeclaration($property, $metadata)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mapping = [];
|
||||
$mapping['fieldName'] = $property->getName();
|
||||
$mapping['fieldName'] = $property->name;
|
||||
|
||||
// Evaluate @Cache annotation
|
||||
$cacheAnnot = $this->reader->getPropertyAnnotation($property, Mapping\Cache::class);
|
||||
@@ -394,7 +400,7 @@ class AnnotationDriver extends CompatibilityAnnotationDriver
|
||||
// @Column, @OneToOne, @OneToMany, @ManyToOne, @ManyToMany
|
||||
$columnAnnot = $this->reader->getPropertyAnnotation($property, Mapping\Column::class);
|
||||
if ($columnAnnot) {
|
||||
$mapping = $this->columnToArray($property->getName(), $columnAnnot);
|
||||
$mapping = $this->columnToArray($property->name, $columnAnnot);
|
||||
|
||||
$idAnnot = $this->reader->getPropertyAnnotation($property, Mapping\Id::class);
|
||||
if ($idAnnot) {
|
||||
|
||||
@@ -29,6 +29,7 @@ use const PHP_VERSION_ID;
|
||||
class AttributeDriver extends CompatibilityAnnotationDriver
|
||||
{
|
||||
use ColocatedMappingDriver;
|
||||
use ReflectionBasedDriver;
|
||||
|
||||
private const ENTITY_ATTRIBUTE_CLASSES = [
|
||||
Mapping\Entity::class => 1,
|
||||
@@ -52,7 +53,7 @@ class AttributeDriver extends CompatibilityAnnotationDriver
|
||||
protected $reader;
|
||||
|
||||
/** @param array<string> $paths */
|
||||
public function __construct(array $paths)
|
||||
public function __construct(array $paths, bool $reportFieldsWhereDeclared = false)
|
||||
{
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
throw new LogicException(
|
||||
@@ -72,6 +73,17 @@ class AttributeDriver extends CompatibilityAnnotationDriver
|
||||
self::class
|
||||
);
|
||||
}
|
||||
|
||||
if (! $reportFieldsWhereDeclared) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/10455',
|
||||
'In ORM 3.0, the AttributeDriver will report fields for the classes where they are declared. This may uncover invalid mapping configurations. To opt into the new mode today, set the "reportFieldsWhereDeclared" constructor parameter to true.',
|
||||
self::class
|
||||
);
|
||||
}
|
||||
|
||||
$this->reportFieldsWhereDeclared = $reportFieldsWhereDeclared;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -297,20 +309,13 @@ class AttributeDriver extends CompatibilityAnnotationDriver
|
||||
|
||||
foreach ($reflectionClass->getProperties() as $property) {
|
||||
assert($property instanceof ReflectionProperty);
|
||||
if (
|
||||
$metadata->isMappedSuperclass && ! $property->isPrivate()
|
||||
||
|
||||
$metadata->isInheritedField($property->name)
|
||||
||
|
||||
$metadata->isInheritedAssociation($property->name)
|
||||
||
|
||||
$metadata->isInheritedEmbeddedClass($property->name)
|
||||
) {
|
||||
|
||||
if ($this->isRepeatedPropertyDeclaration($property, $metadata)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mapping = [];
|
||||
$mapping['fieldName'] = $property->getName();
|
||||
$mapping['fieldName'] = $property->name;
|
||||
|
||||
// Evaluate #[Cache] attribute
|
||||
$cacheAttribute = $this->reader->getPropertyAttribute($property, Mapping\Cache::class);
|
||||
@@ -345,7 +350,7 @@ class AttributeDriver extends CompatibilityAnnotationDriver
|
||||
$embeddedAttribute = $this->reader->getPropertyAttribute($property, Mapping\Embedded::class);
|
||||
|
||||
if ($columnAttribute !== null) {
|
||||
$mapping = $this->columnToArray($property->getName(), $columnAttribute);
|
||||
$mapping = $this->columnToArray($property->name, $columnAttribute);
|
||||
|
||||
if ($this->reader->getPropertyAttribute($property, Mapping\Id::class)) {
|
||||
$mapping['id'] = true;
|
||||
|
||||
@@ -69,8 +69,7 @@ final class AttributeReader
|
||||
));
|
||||
}
|
||||
|
||||
return $this->getPropertyAttributes($property)[$attributeName]
|
||||
?? ($this->isRepeatable($attributeName) ? new RepeatableAttributeCollection() : null);
|
||||
return $this->getPropertyAttributes($property)[$attributeName] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
54
lib/Doctrine/ORM/Mapping/Driver/ReflectionBasedDriver.php
Normal file
54
lib/Doctrine/ORM/Mapping/Driver/ReflectionBasedDriver.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Mapping\Driver;
|
||||
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use ReflectionProperty;
|
||||
|
||||
/** @internal */
|
||||
trait ReflectionBasedDriver
|
||||
{
|
||||
/** @var bool */
|
||||
private $reportFieldsWhereDeclared = false;
|
||||
|
||||
/**
|
||||
* Helps to deal with the case that reflection may report properties inherited from parent classes.
|
||||
* When we know about the fields already (inheritance has been anticipated in ClassMetadataFactory),
|
||||
* the driver must skip them.
|
||||
*
|
||||
* The declaring classes may mismatch when there are private properties: The same property name may be
|
||||
* reported multiple times, but since it is private, it is in fact multiple (different) properties in
|
||||
* different classes. In that case, report the property as an individual field. (ClassMetadataFactory will
|
||||
* probably fail in that case, though.)
|
||||
*/
|
||||
private function isRepeatedPropertyDeclaration(ReflectionProperty $property, ClassMetadata $metadata): bool
|
||||
{
|
||||
if (! $this->reportFieldsWhereDeclared) {
|
||||
return $metadata->isMappedSuperclass && ! $property->isPrivate()
|
||||
|| $metadata->isInheritedField($property->name)
|
||||
|| $metadata->isInheritedAssociation($property->name)
|
||||
|| $metadata->isInheritedEmbeddedClass($property->name);
|
||||
}
|
||||
|
||||
$declaringClass = $property->class;
|
||||
|
||||
if (
|
||||
isset($metadata->fieldMappings[$property->name]['declared'])
|
||||
&& $metadata->fieldMappings[$property->name]['declared'] === $declaringClass
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
isset($metadata->associationMappings[$property->name]['declared'])
|
||||
&& $metadata->associationMappings[$property->name]['declared'] === $declaringClass
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isset($metadata->embeddedClasses[$property->name]['declared'])
|
||||
&& $metadata->embeddedClasses[$property->name]['declared'] === $declaringClass;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace Doctrine\ORM\Mapping\Driver;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\MappingException;
|
||||
@@ -56,15 +55,6 @@ class XmlDriver extends FileDriver
|
||||
);
|
||||
}
|
||||
|
||||
if (! $isXsdValidationEnabled) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/6728',
|
||||
'Using XML mapping driver with XSD validation disabled is deprecated'
|
||||
. ' and will not be supported in Doctrine ORM 3.0.'
|
||||
);
|
||||
}
|
||||
|
||||
if ($isXsdValidationEnabled && ! extension_loaded('dom')) {
|
||||
throw new LogicException(
|
||||
'XSD validation cannot be enabled because the DOM extension is missing.'
|
||||
@@ -128,7 +118,7 @@ class XmlDriver extends FileDriver
|
||||
|
||||
// Evaluate named queries
|
||||
if (isset($xmlRoot->{'named-queries'})) {
|
||||
foreach ($xmlRoot->{'named-queries'}->{'named-query'} as $namedQueryElement) {
|
||||
foreach ($xmlRoot->{'named-queries'}->{'named-query'} ?? [] as $namedQueryElement) {
|
||||
$metadata->addNamedQuery(
|
||||
[
|
||||
'name' => (string) $namedQueryElement['name'],
|
||||
@@ -140,7 +130,7 @@ class XmlDriver extends FileDriver
|
||||
|
||||
// Evaluate native named queries
|
||||
if (isset($xmlRoot->{'named-native-queries'})) {
|
||||
foreach ($xmlRoot->{'named-native-queries'}->{'named-native-query'} as $nativeQueryElement) {
|
||||
foreach ($xmlRoot->{'named-native-queries'}->{'named-native-query'} ?? [] as $nativeQueryElement) {
|
||||
$metadata->addNamedNativeQuery(
|
||||
[
|
||||
'name' => isset($nativeQueryElement['name']) ? (string) $nativeQueryElement['name'] : null,
|
||||
@@ -154,7 +144,7 @@ class XmlDriver extends FileDriver
|
||||
|
||||
// Evaluate sql result set mapping
|
||||
if (isset($xmlRoot->{'sql-result-set-mappings'})) {
|
||||
foreach ($xmlRoot->{'sql-result-set-mappings'}->{'sql-result-set-mapping'} as $rsmElement) {
|
||||
foreach ($xmlRoot->{'sql-result-set-mappings'}->{'sql-result-set-mapping'} ?? [] as $rsmElement) {
|
||||
$entities = [];
|
||||
$columns = [];
|
||||
foreach ($rsmElement as $entityElement) {
|
||||
@@ -240,7 +230,7 @@ class XmlDriver extends FileDriver
|
||||
// Evaluate <indexes...>
|
||||
if (isset($xmlRoot->indexes)) {
|
||||
$metadata->table['indexes'] = [];
|
||||
foreach ($xmlRoot->indexes->index as $indexXml) {
|
||||
foreach ($xmlRoot->indexes->index ?? [] as $indexXml) {
|
||||
$index = [];
|
||||
|
||||
if (isset($indexXml['columns']) && ! empty($indexXml['columns'])) {
|
||||
@@ -283,7 +273,7 @@ class XmlDriver extends FileDriver
|
||||
// Evaluate <unique-constraints..>
|
||||
if (isset($xmlRoot->{'unique-constraints'})) {
|
||||
$metadata->table['uniqueConstraints'] = [];
|
||||
foreach ($xmlRoot->{'unique-constraints'}->{'unique-constraint'} as $uniqueXml) {
|
||||
foreach ($xmlRoot->{'unique-constraints'}->{'unique-constraint'} ?? [] as $uniqueXml) {
|
||||
$unique = [];
|
||||
|
||||
if (isset($uniqueXml['columns']) && ! empty($uniqueXml['columns'])) {
|
||||
@@ -370,7 +360,7 @@ class XmlDriver extends FileDriver
|
||||
|
||||
// Evaluate <id ...> mappings
|
||||
$associationIds = [];
|
||||
foreach ($xmlRoot->id as $idElement) {
|
||||
foreach ($xmlRoot->id ?? [] as $idElement) {
|
||||
if (isset($idElement['association-key']) && $this->evaluateBoolean($idElement['association-key'])) {
|
||||
$associationIds[(string) $idElement['name']] = true;
|
||||
continue;
|
||||
@@ -439,7 +429,7 @@ class XmlDriver extends FileDriver
|
||||
if (isset($oneToOneElement->{'join-column'})) {
|
||||
$joinColumns[] = $this->joinColumnToArray($oneToOneElement->{'join-column'});
|
||||
} elseif (isset($oneToOneElement->{'join-columns'})) {
|
||||
foreach ($oneToOneElement->{'join-columns'}->{'join-column'} as $joinColumnElement) {
|
||||
foreach ($oneToOneElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
|
||||
$joinColumns[] = $this->joinColumnToArray($joinColumnElement);
|
||||
}
|
||||
}
|
||||
@@ -490,7 +480,7 @@ class XmlDriver extends FileDriver
|
||||
|
||||
if (isset($oneToManyElement->{'order-by'})) {
|
||||
$orderBy = [];
|
||||
foreach ($oneToManyElement->{'order-by'}->{'order-by-field'} as $orderByField) {
|
||||
foreach ($oneToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) {
|
||||
$orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
|
||||
? (string) $orderByField['direction']
|
||||
: Criteria::ASC;
|
||||
@@ -542,7 +532,7 @@ class XmlDriver extends FileDriver
|
||||
if (isset($manyToOneElement->{'join-column'})) {
|
||||
$joinColumns[] = $this->joinColumnToArray($manyToOneElement->{'join-column'});
|
||||
} elseif (isset($manyToOneElement->{'join-columns'})) {
|
||||
foreach ($manyToOneElement->{'join-columns'}->{'join-column'} as $joinColumnElement) {
|
||||
foreach ($manyToOneElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
|
||||
$joinColumns[] = $this->joinColumnToArray($joinColumnElement);
|
||||
}
|
||||
}
|
||||
@@ -601,11 +591,11 @@ class XmlDriver extends FileDriver
|
||||
$joinTable['options'] = $this->parseOptions($joinTableElement->options->children());
|
||||
}
|
||||
|
||||
foreach ($joinTableElement->{'join-columns'}->{'join-column'} as $joinColumnElement) {
|
||||
foreach ($joinTableElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
|
||||
$joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumnElement);
|
||||
}
|
||||
|
||||
foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} as $joinColumnElement) {
|
||||
foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
|
||||
$joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumnElement);
|
||||
}
|
||||
|
||||
@@ -618,7 +608,7 @@ class XmlDriver extends FileDriver
|
||||
|
||||
if (isset($manyToManyElement->{'order-by'})) {
|
||||
$orderBy = [];
|
||||
foreach ($manyToManyElement->{'order-by'}->{'order-by-field'} as $orderByField) {
|
||||
foreach ($manyToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) {
|
||||
$orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
|
||||
? (string) $orderByField['direction']
|
||||
: Criteria::ASC;
|
||||
@@ -644,9 +634,9 @@ class XmlDriver extends FileDriver
|
||||
|
||||
// Evaluate association-overrides
|
||||
if (isset($xmlRoot->{'attribute-overrides'})) {
|
||||
foreach ($xmlRoot->{'attribute-overrides'}->{'attribute-override'} as $overrideElement) {
|
||||
foreach ($xmlRoot->{'attribute-overrides'}->{'attribute-override'} ?? [] as $overrideElement) {
|
||||
$fieldName = (string) $overrideElement['name'];
|
||||
foreach ($overrideElement->field as $field) {
|
||||
foreach ($overrideElement->field ?? [] as $field) {
|
||||
$mapping = $this->columnToArray($field);
|
||||
$mapping['fieldName'] = $fieldName;
|
||||
$metadata->setAttributeOverride($fieldName, $mapping);
|
||||
@@ -656,14 +646,14 @@ class XmlDriver extends FileDriver
|
||||
|
||||
// Evaluate association-overrides
|
||||
if (isset($xmlRoot->{'association-overrides'})) {
|
||||
foreach ($xmlRoot->{'association-overrides'}->{'association-override'} as $overrideElement) {
|
||||
foreach ($xmlRoot->{'association-overrides'}->{'association-override'} ?? [] as $overrideElement) {
|
||||
$fieldName = (string) $overrideElement['name'];
|
||||
$override = [];
|
||||
|
||||
// Check for join-columns
|
||||
if (isset($overrideElement->{'join-columns'})) {
|
||||
$joinColumns = [];
|
||||
foreach ($overrideElement->{'join-columns'}->{'join-column'} as $joinColumnElement) {
|
||||
foreach ($overrideElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
|
||||
$joinColumns[] = $this->joinColumnToArray($joinColumnElement);
|
||||
}
|
||||
|
||||
@@ -685,13 +675,13 @@ class XmlDriver extends FileDriver
|
||||
}
|
||||
|
||||
if (isset($joinTableElement->{'join-columns'})) {
|
||||
foreach ($joinTableElement->{'join-columns'}->{'join-column'} as $joinColumnElement) {
|
||||
foreach ($joinTableElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
|
||||
$joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumnElement);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($joinTableElement->{'inverse-join-columns'})) {
|
||||
foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} as $joinColumnElement) {
|
||||
foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
|
||||
$joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumnElement);
|
||||
}
|
||||
}
|
||||
@@ -715,14 +705,14 @@ class XmlDriver extends FileDriver
|
||||
|
||||
// Evaluate <lifecycle-callbacks...>
|
||||
if (isset($xmlRoot->{'lifecycle-callbacks'})) {
|
||||
foreach ($xmlRoot->{'lifecycle-callbacks'}->{'lifecycle-callback'} as $lifecycleCallback) {
|
||||
foreach ($xmlRoot->{'lifecycle-callbacks'}->{'lifecycle-callback'} ?? [] as $lifecycleCallback) {
|
||||
$metadata->addLifecycleCallback((string) $lifecycleCallback['method'], constant('Doctrine\ORM\Events::' . (string) $lifecycleCallback['type']));
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate entity listener
|
||||
if (isset($xmlRoot->{'entity-listeners'})) {
|
||||
foreach ($xmlRoot->{'entity-listeners'}->{'entity-listener'} as $listenerElement) {
|
||||
foreach ($xmlRoot->{'entity-listeners'}->{'entity-listener'} ?? [] as $listenerElement) {
|
||||
$className = (string) $listenerElement['class'];
|
||||
// Evaluate the listener using naming convention.
|
||||
if ($listenerElement->count() === 0) {
|
||||
@@ -744,16 +734,14 @@ class XmlDriver extends FileDriver
|
||||
/**
|
||||
* Parses (nested) option elements.
|
||||
*
|
||||
* @param SimpleXMLElement $options The XML element.
|
||||
*
|
||||
* @return mixed[] The options array.
|
||||
* @psalm-return array<int|string, array<int|string, mixed|string>|bool|string>
|
||||
*/
|
||||
private function parseOptions(SimpleXMLElement $options): array
|
||||
private function parseOptions(?SimpleXMLElement $options): array
|
||||
{
|
||||
$array = [];
|
||||
|
||||
foreach ($options as $option) {
|
||||
foreach ($options ?? [] as $option) {
|
||||
if ($option->count()) {
|
||||
$value = $this->parseOptions($option->children());
|
||||
} else {
|
||||
@@ -816,7 +804,7 @@ class XmlDriver extends FileDriver
|
||||
}
|
||||
|
||||
if (isset($joinColumnElement['options'])) {
|
||||
$joinColumn['options'] = $this->parseOptions($joinColumnElement['options']->children());
|
||||
$joinColumn['options'] = $this->parseOptions($joinColumnElement['options'] ? $joinColumnElement['options']->children() : null);
|
||||
}
|
||||
|
||||
return $joinColumn;
|
||||
@@ -944,7 +932,10 @@ class XmlDriver extends FileDriver
|
||||
private function getCascadeMappings(SimpleXMLElement $cascadeElement): array
|
||||
{
|
||||
$cascades = [];
|
||||
foreach ($cascadeElement->children() as $action) {
|
||||
$children = $cascadeElement->children();
|
||||
assert($children !== null);
|
||||
|
||||
foreach ($children as $action) {
|
||||
// According to the JPA specifications, XML uses "cascade-persist"
|
||||
// instead of "persist". Here, both variations
|
||||
// are supported because YAML, Annotation and Attribute use "persist"
|
||||
@@ -969,19 +960,19 @@ class XmlDriver extends FileDriver
|
||||
|
||||
if (isset($xmlElement->entity)) {
|
||||
foreach ($xmlElement->entity as $entityElement) {
|
||||
/** @psalm-var class-string */
|
||||
/** @psalm-var class-string $entityName */
|
||||
$entityName = (string) $entityElement['name'];
|
||||
$result[$entityName] = $entityElement;
|
||||
}
|
||||
} elseif (isset($xmlElement->{'mapped-superclass'})) {
|
||||
foreach ($xmlElement->{'mapped-superclass'} as $mappedSuperClass) {
|
||||
/** @psalm-var class-string */
|
||||
/** @psalm-var class-string $className */
|
||||
$className = (string) $mappedSuperClass['name'];
|
||||
$result[$className] = $mappedSuperClass;
|
||||
}
|
||||
} elseif (isset($xmlElement->embeddable)) {
|
||||
foreach ($xmlElement->embeddable as $embeddableElement) {
|
||||
/** @psalm-var class-string */
|
||||
/** @psalm-var class-string $embeddableName */
|
||||
$embeddableName = (string) $embeddableElement['name'];
|
||||
$result[$embeddableName] = $embeddableElement;
|
||||
}
|
||||
|
||||
@@ -954,6 +954,16 @@ class MappingException extends ORMException
|
||||
return new self(sprintf('Enum types require PHP 8.1 in %s::$%s', $className, $fieldName));
|
||||
}
|
||||
|
||||
public static function backedEnumTypeRequired(string $className, string $fieldName, string $enumType): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Attempting to map a non-backed enum type %s in entity %s::$%s. Please use backed enums only',
|
||||
$enumType,
|
||||
$className,
|
||||
$fieldName
|
||||
));
|
||||
}
|
||||
|
||||
public static function nonEnumTypeMapped(string $className, string $fieldName, string $enumType): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace Doctrine\ORM\Mapping;
|
||||
* Is used to specify an array of native SQL named queries.
|
||||
* The NamedNativeQueries annotation can be applied to an entity or mapped superclass.
|
||||
*
|
||||
* @deprecated Named queries won't be supported in ORM 3.
|
||||
*
|
||||
* @Annotation
|
||||
* @Target("CLASS")
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace Doctrine\ORM\Mapping;
|
||||
* Is used to specify a native SQL named query.
|
||||
* The NamedNativeQuery annotation can be applied to an entity or mapped superclass.
|
||||
*
|
||||
* @deprecated Named queries won't be supported in ORM 3.
|
||||
*
|
||||
* @Annotation
|
||||
* @Target("ANNOTATION")
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
||||
namespace Doctrine\ORM\Mapping;
|
||||
|
||||
/**
|
||||
* @deprecated Named queries won't be supported in ORM 3.
|
||||
*
|
||||
* @Annotation
|
||||
* @Target("CLASS")
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
||||
namespace Doctrine\ORM\Mapping;
|
||||
|
||||
/**
|
||||
* @deprecated Named queries won't be supported in ORM 3.
|
||||
*
|
||||
* @Annotation
|
||||
* @Target("ANNOTATION")
|
||||
*/
|
||||
|
||||
@@ -73,7 +73,7 @@ final class ReflectionPropertiesGetter
|
||||
|
||||
$parentClass = $currentClass->getParentClass();
|
||||
if ($parentClass) {
|
||||
$parentClassName = $parentClass->getName();
|
||||
$parentClassName = $parentClass->name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,14 +111,14 @@ final class ReflectionPropertiesGetter
|
||||
private function getAccessibleProperty(ReflectionProperty $property): ?ReflectionProperty
|
||||
{
|
||||
return $this->reflectionService->getAccessibleProperty(
|
||||
$property->getDeclaringClass()->getName(),
|
||||
$property->getName()
|
||||
$property->class,
|
||||
$property->name
|
||||
);
|
||||
}
|
||||
|
||||
private function getLogicalName(ReflectionProperty $property): string
|
||||
{
|
||||
$propertyName = $property->getName();
|
||||
$propertyName = $property->name;
|
||||
|
||||
if ($property->isPublic()) {
|
||||
return $propertyName;
|
||||
@@ -128,6 +128,6 @@ final class ReflectionPropertiesGetter
|
||||
return "\0*\0" . $propertyName;
|
||||
}
|
||||
|
||||
return "\0" . $property->getDeclaringClass()->getName() . "\0" . $propertyName;
|
||||
return "\0" . $property->class . "\0" . $propertyName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class ReflectionEmbeddedProperty extends ReflectionProperty
|
||||
$this->childProperty = $childProperty;
|
||||
$this->embeddedClass = (string) $embeddedClass;
|
||||
|
||||
parent::__construct($childProperty->getDeclaringClass()->getName(), $childProperty->getName());
|
||||
parent::__construct($childProperty->class, $childProperty->name);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,8 +28,8 @@ class ReflectionEnumProperty extends ReflectionProperty
|
||||
$this->enumType = $enumType;
|
||||
|
||||
parent::__construct(
|
||||
$originalReflectionProperty->getDeclaringClass()->getName(),
|
||||
$originalReflectionProperty->getName()
|
||||
$originalReflectionProperty->class,
|
||||
$originalReflectionProperty->name
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ class ReflectionEnumProperty extends ReflectionProperty
|
||||
} catch (ValueError $e) {
|
||||
throw MappingException::invalidEnumValue(
|
||||
get_class($object),
|
||||
$this->originalReflectionProperty->getName(),
|
||||
$this->originalReflectionProperty->name,
|
||||
(string) $value,
|
||||
$enumType,
|
||||
$e
|
||||
|
||||
@@ -30,7 +30,10 @@ class UnderscoreNamingStrategy implements NamingStrategy
|
||||
/** @var int */
|
||||
private $case;
|
||||
|
||||
/** @var string */
|
||||
/**
|
||||
* @var string
|
||||
* @psalm-var non-empty-string
|
||||
*/
|
||||
private $pattern;
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,8 +11,10 @@ use function ksort;
|
||||
|
||||
/**
|
||||
* Represents a native SQL query.
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
final class NativeQuery extends AbstractQuery
|
||||
class NativeQuery extends AbstractQuery
|
||||
{
|
||||
/** @var string */
|
||||
private $sql;
|
||||
|
||||
@@ -15,6 +15,7 @@ use function func_num_args;
|
||||
use function get_debug_type;
|
||||
use function gettype;
|
||||
use function implode;
|
||||
use function is_scalar;
|
||||
use function method_exists;
|
||||
use function reset;
|
||||
use function spl_object_id;
|
||||
@@ -261,6 +262,32 @@ EXCEPTION
|
||||
return new self(sprintf('Entity name must be a string, %s given', get_debug_type($entityName)));
|
||||
}
|
||||
|
||||
/** @param mixed $value */
|
||||
public static function invalidAutoGenerateMode($value): self
|
||||
{
|
||||
return new self(sprintf('Invalid auto generate mode "%s" given.', is_scalar($value) ? (string) $value : get_debug_type($value)));
|
||||
}
|
||||
|
||||
public static function missingPrimaryKeyValue(string $className, string $idField): self
|
||||
{
|
||||
return new self(sprintf('Missing value for primary key %s on %s', $idField, $className));
|
||||
}
|
||||
|
||||
public static function proxyDirectoryRequired(): self
|
||||
{
|
||||
return new self('You must configure a proxy directory. See docs for details');
|
||||
}
|
||||
|
||||
public static function proxyNamespaceRequired(): self
|
||||
{
|
||||
return new self('You must configure a proxy namespace');
|
||||
}
|
||||
|
||||
public static function proxyDirectoryNotWritable(string $proxyDirectory): self
|
||||
{
|
||||
return new self(sprintf('Your proxy directory "%s" must be writable', $proxyDirectory));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to show an object as string.
|
||||
*
|
||||
|
||||
@@ -62,7 +62,8 @@ final class ORMSetup
|
||||
*/
|
||||
public static function createDefaultAnnotationDriver(
|
||||
array $paths = [],
|
||||
?CacheItemPoolInterface $cache = null
|
||||
?CacheItemPoolInterface $cache = null,
|
||||
bool $reportFieldsWhereDeclared = false
|
||||
): AnnotationDriver {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
@@ -88,7 +89,7 @@ final class ORMSetup
|
||||
$reader = new PsrCachedReader($reader, $cache);
|
||||
}
|
||||
|
||||
return new AnnotationDriver($reader, $paths);
|
||||
return new AnnotationDriver($reader, $paths, $reportFieldsWhereDeclared);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,10 +101,11 @@ final class ORMSetup
|
||||
array $paths,
|
||||
bool $isDevMode = false,
|
||||
?string $proxyDir = null,
|
||||
?CacheItemPoolInterface $cache = null
|
||||
?CacheItemPoolInterface $cache = null,
|
||||
bool $reportFieldsWhereDeclared = false
|
||||
): Configuration {
|
||||
$config = self::createConfiguration($isDevMode, $proxyDir, $cache);
|
||||
$config->setMetadataDriverImpl(new AttributeDriver($paths));
|
||||
$config->setMetadataDriverImpl(new AttributeDriver($paths, $reportFieldsWhereDeclared));
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ namespace Doctrine\ORM\Persisters\Entity;
|
||||
use BackedEnum;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Common\Collections\Expr\Comparison;
|
||||
use Doctrine\Common\Util\ClassUtils;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\LockMode;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
@@ -26,6 +25,7 @@ use Doctrine\ORM\Persisters\Exception\InvalidOrientation;
|
||||
use Doctrine\ORM\Persisters\Exception\UnrecognizedField;
|
||||
use Doctrine\ORM\Persisters\SqlExpressionVisitor;
|
||||
use Doctrine\ORM\Persisters\SqlValueVisitor;
|
||||
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\QueryException;
|
||||
use Doctrine\ORM\Repository\Exception\InvalidFindByCall;
|
||||
@@ -183,7 +183,7 @@ class BasicEntityPersister implements EntityPersister
|
||||
*
|
||||
* @var IdentifierFlattener
|
||||
*/
|
||||
private $identifierFlattener;
|
||||
protected $identifierFlattener;
|
||||
|
||||
/** @var CachedPersisterContext */
|
||||
protected $currentPersisterContext;
|
||||
@@ -256,17 +256,17 @@ class BasicEntityPersister implements EntityPersister
|
||||
public function executeInserts()
|
||||
{
|
||||
if (! $this->queuedInserts) {
|
||||
return [];
|
||||
return;
|
||||
}
|
||||
|
||||
$postInsertIds = [];
|
||||
$uow = $this->em->getUnitOfWork();
|
||||
$idGenerator = $this->class->idGenerator;
|
||||
$isPostInsertId = $idGenerator->isPostInsertGenerator();
|
||||
|
||||
$stmt = $this->conn->prepare($this->getInsertSQL());
|
||||
$tableName = $this->class->getTableName();
|
||||
|
||||
foreach ($this->queuedInserts as $entity) {
|
||||
foreach ($this->queuedInserts as $key => $entity) {
|
||||
$insertData = $this->prepareInsertData($entity);
|
||||
|
||||
if (isset($insertData[$tableName])) {
|
||||
@@ -280,12 +280,10 @@ class BasicEntityPersister implements EntityPersister
|
||||
$stmt->executeStatement();
|
||||
|
||||
if ($isPostInsertId) {
|
||||
$generatedId = $idGenerator->generateId($this->em, $entity);
|
||||
$id = [$this->class->identifier[0] => $generatedId];
|
||||
$postInsertIds[] = [
|
||||
'generatedId' => $generatedId,
|
||||
'entity' => $entity,
|
||||
];
|
||||
$generatedId = $idGenerator->generateId($this->em, $entity);
|
||||
$id = [$this->class->identifier[0] => $generatedId];
|
||||
|
||||
$uow->assignPostInsertId($entity, $generatedId);
|
||||
} else {
|
||||
$id = $this->class->getIdentifierValues($entity);
|
||||
}
|
||||
@@ -293,11 +291,16 @@ class BasicEntityPersister implements EntityPersister
|
||||
if ($this->class->requiresFetchAfterChange) {
|
||||
$this->assignDefaultVersionAndUpsertableValues($entity, $id);
|
||||
}
|
||||
|
||||
// Unset this queued insert, so that the prepareUpdateData() method knows right away
|
||||
// (for the next entity already) that the current entity has been written to the database
|
||||
// and no extra updates need to be scheduled to refer to it.
|
||||
//
|
||||
// In \Doctrine\ORM\UnitOfWork::executeInserts(), the UoW already removed entities
|
||||
// from its own list (\Doctrine\ORM\UnitOfWork::$entityInsertions) right after they
|
||||
// were given to our addInsert() method.
|
||||
unset($this->queuedInserts[$key]);
|
||||
}
|
||||
|
||||
$this->queuedInserts = [];
|
||||
|
||||
return $postInsertIds;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -376,7 +379,7 @@ class BasicEntityPersister implements EntityPersister
|
||||
* @return int[]|null[]|string[]
|
||||
* @psalm-return list<int|string|null>
|
||||
*/
|
||||
private function extractIdentifierTypes(array $id, ClassMetadata $versionedClass): array
|
||||
final protected function extractIdentifierTypes(array $id, ClassMetadata $versionedClass): array
|
||||
{
|
||||
$types = [];
|
||||
|
||||
@@ -675,10 +678,30 @@ class BasicEntityPersister implements EntityPersister
|
||||
if ($newVal !== null) {
|
||||
$oid = spl_object_id($newVal);
|
||||
|
||||
if (isset($this->queuedInserts[$oid]) || $uow->isScheduledForInsert($newVal)) {
|
||||
// The associated entity $newVal is not yet persisted, so we must
|
||||
// set $newVal = null, in order to insert a null value and schedule an
|
||||
// extra update on the UnitOfWork.
|
||||
// If the associated entity $newVal is not yet persisted and/or does not yet have
|
||||
// an ID assigned, we must set $newVal = null. This will insert a null value and
|
||||
// schedule an extra update on the UnitOfWork.
|
||||
//
|
||||
// This gives us extra time to a) possibly obtain a database-generated identifier
|
||||
// value for $newVal, and b) insert $newVal into the database before the foreign
|
||||
// key reference is being made.
|
||||
//
|
||||
// When looking at $this->queuedInserts and $uow->isScheduledForInsert, be aware
|
||||
// of the implementation details that our own executeInserts() method will remove
|
||||
// entities from the former as soon as the insert statement has been executed and
|
||||
// a post-insert ID has been assigned (if necessary), and that the UnitOfWork has
|
||||
// already removed entities from its own list at the time they were passed to our
|
||||
// addInsert() method.
|
||||
//
|
||||
// Then, there is one extra exception we can make: An entity that references back to itself
|
||||
// _and_ uses an application-provided ID (the "NONE" generator strategy) also does not
|
||||
// need the extra update, although it is still in the list of insertions itself.
|
||||
// This looks like a minor optimization at first, but is the capstone for being able to
|
||||
// use non-NULLable, self-referencing associations in applications that provide IDs (like UUIDs).
|
||||
if (
|
||||
(isset($this->queuedInserts[$oid]) || $uow->isScheduledForInsert($newVal))
|
||||
&& ! ($newVal === $entity && $this->class->isIdentifierNatural())
|
||||
) {
|
||||
$uow->scheduleExtraUpdate($entity, [$field => [null, $newVal]]);
|
||||
|
||||
$newVal = null;
|
||||
@@ -1241,7 +1264,7 @@ class BasicEntityPersister implements EntityPersister
|
||||
}
|
||||
|
||||
$isAssocToOneInverseSide = $assoc['type'] & ClassMetadata::TO_ONE && ! $assoc['isOwningSide'];
|
||||
$isAssocFromOneEager = $assoc['type'] !== ClassMetadata::MANY_TO_MANY && $assoc['fetch'] === ClassMetadata::FETCH_EAGER;
|
||||
$isAssocFromOneEager = $assoc['type'] & ClassMetadata::TO_ONE && $assoc['fetch'] === ClassMetadata::FETCH_EAGER;
|
||||
|
||||
if (! ($isAssocFromOneEager || $isAssocToOneInverseSide)) {
|
||||
continue;
|
||||
@@ -2005,7 +2028,7 @@ class BasicEntityPersister implements EntityPersister
|
||||
return [$value->value];
|
||||
}
|
||||
|
||||
$valueClass = ClassUtils::getClass($value);
|
||||
$valueClass = DefaultProxyClassNameResolver::getClass($value);
|
||||
|
||||
if ($this->em->getMetadataFactory()->isTransient($valueClass)) {
|
||||
return [$value];
|
||||
|
||||
@@ -109,17 +109,15 @@ interface EntityPersister
|
||||
public function addInsert($entity);
|
||||
|
||||
/**
|
||||
* Executes all queued entity insertions and returns any generated post-insert
|
||||
* identifiers that were created as a result of the insertions.
|
||||
* Executes all queued entity insertions.
|
||||
*
|
||||
* If no inserts are queued, invoking this method is a NOOP.
|
||||
*
|
||||
* @psalm-return list<array{
|
||||
* @psalm-return void|list<array{
|
||||
* generatedId: int,
|
||||
* entity: object
|
||||
* }> An array of any generated post-insert IDs. This will be
|
||||
* an empty array if the entity class does not use the
|
||||
* IDENTITY generation strategy.
|
||||
* }> Returning an array of generated post-insert IDs is deprecated, implementations
|
||||
* should call UnitOfWork::assignPostInsertId() and return void.
|
||||
*/
|
||||
public function executeInserts();
|
||||
|
||||
|
||||
@@ -11,8 +11,11 @@ use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Internal\SQLResultCasing;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Utility\PersisterHelper;
|
||||
use LengthException;
|
||||
|
||||
use function array_combine;
|
||||
use function array_keys;
|
||||
use function array_values;
|
||||
use function implode;
|
||||
|
||||
/**
|
||||
@@ -109,10 +112,10 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
|
||||
public function executeInserts()
|
||||
{
|
||||
if (! $this->queuedInserts) {
|
||||
return [];
|
||||
return;
|
||||
}
|
||||
|
||||
$postInsertIds = [];
|
||||
$uow = $this->em->getUnitOfWork();
|
||||
$idGenerator = $this->class->idGenerator;
|
||||
$isPostInsertId = $idGenerator->isPostInsertGenerator();
|
||||
$rootClass = $this->class->name !== $this->class->rootEntityName
|
||||
@@ -157,20 +160,14 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
|
||||
$rootTableStmt->executeStatement();
|
||||
|
||||
if ($isPostInsertId) {
|
||||
$generatedId = $idGenerator->generateId($this->em, $entity);
|
||||
$id = [$this->class->identifier[0] => $generatedId];
|
||||
$postInsertIds[] = [
|
||||
'generatedId' => $generatedId,
|
||||
'entity' => $entity,
|
||||
];
|
||||
$generatedId = $idGenerator->generateId($this->em, $entity);
|
||||
$id = [$this->class->identifier[0] => $generatedId];
|
||||
|
||||
$uow->assignPostInsertId($entity, $generatedId);
|
||||
} else {
|
||||
$id = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
|
||||
}
|
||||
|
||||
if ($this->class->requiresFetchAfterChange) {
|
||||
$this->assignDefaultVersionAndUpsertableValues($entity, $id);
|
||||
}
|
||||
|
||||
// Execute inserts on subtables.
|
||||
// The order doesn't matter because all child tables link to the root table via FK.
|
||||
foreach ($subTableStmts as $tableName => $stmt) {
|
||||
@@ -191,11 +188,13 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
|
||||
|
||||
$stmt->executeStatement();
|
||||
}
|
||||
|
||||
if ($this->class->requiresFetchAfterChange) {
|
||||
$this->assignDefaultVersionAndUpsertableValues($entity, $id);
|
||||
}
|
||||
}
|
||||
|
||||
$this->queuedInserts = [];
|
||||
|
||||
return $postInsertIds;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -514,6 +513,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
|
||||
|| isset($this->class->associationMappings[$name]['inherited'])
|
||||
|| ($this->class->isVersioned && $this->class->versionField === $name)
|
||||
|| isset($this->class->embeddedClasses[$name])
|
||||
|| isset($this->class->fieldMappings[$name]['notInsertable'])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -556,6 +556,60 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function fetchVersionAndNotUpsertableValues($versionedClass, array $id)
|
||||
{
|
||||
$columnNames = [];
|
||||
foreach ($this->class->fieldMappings as $key => $column) {
|
||||
$class = null;
|
||||
if ($this->class->isVersioned && $key === $versionedClass->versionField) {
|
||||
$class = $versionedClass;
|
||||
} elseif (isset($column['generated'])) {
|
||||
$class = isset($column['inherited'])
|
||||
? $this->em->getClassMetadata($column['inherited'])
|
||||
: $this->class;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
$columnNames[$key] = $this->getSelectColumnSQL($key, $class);
|
||||
}
|
||||
|
||||
$tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform);
|
||||
$baseTableAlias = $this->getSQLTableAlias($this->class->name);
|
||||
$joinSql = $this->getJoinSql($baseTableAlias);
|
||||
$identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform);
|
||||
foreach ($identifier as $i => $idValue) {
|
||||
$identifier[$i] = $baseTableAlias . '.' . $idValue;
|
||||
}
|
||||
|
||||
$sql = 'SELECT ' . implode(', ', $columnNames)
|
||||
. ' FROM ' . $tableName . ' ' . $baseTableAlias
|
||||
. $joinSql
|
||||
. ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';
|
||||
|
||||
$flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id);
|
||||
$values = $this->conn->fetchNumeric(
|
||||
$sql,
|
||||
array_values($flatId),
|
||||
$this->extractIdentifierTypes($id, $versionedClass)
|
||||
);
|
||||
|
||||
if ($values === false) {
|
||||
throw new LengthException('Unexpected empty result for database query.');
|
||||
}
|
||||
|
||||
$values = array_combine(array_keys($columnNames), $values);
|
||||
|
||||
if (! $values) {
|
||||
throw new LengthException('Unexpected number of database columns.');
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
private function getJoinSql(string $baseTableAlias): string
|
||||
{
|
||||
$joinSql = '';
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace Doctrine\ORM\Proxy;
|
||||
|
||||
use Doctrine\Common\Proxy\Autoloader as BaseAutoloader;
|
||||
|
||||
/** @deprecated use \Doctrine\Common\Proxy\Autoloader instead */
|
||||
class Autoloader extends BaseAutoloader
|
||||
{
|
||||
}
|
||||
|
||||
40
lib/Doctrine/ORM/Proxy/DefaultProxyClassNameResolver.php
Normal file
40
lib/Doctrine/ORM/Proxy/DefaultProxyClassNameResolver.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Proxy;
|
||||
|
||||
use Doctrine\Persistence\Mapping\ProxyClassNameResolver;
|
||||
use Doctrine\Persistence\Proxy;
|
||||
|
||||
use function get_class;
|
||||
use function strrpos;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* Class-related functionality for objects that might or not be proxy objects
|
||||
* at the moment.
|
||||
*/
|
||||
final class DefaultProxyClassNameResolver implements ProxyClassNameResolver
|
||||
{
|
||||
public function resolveClassName(string $className): string
|
||||
{
|
||||
$pos = strrpos($className, '\\' . Proxy::MARKER . '\\');
|
||||
|
||||
if ($pos === false) {
|
||||
return $className;
|
||||
}
|
||||
|
||||
return substr($className, $pos + Proxy::MARKER_LENGTH + 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $object
|
||||
*
|
||||
* @return class-string
|
||||
*/
|
||||
public static function getClass($object): string
|
||||
{
|
||||
return (new self())->resolveClassName(get_class($object));
|
||||
}
|
||||
}
|
||||
19
lib/Doctrine/ORM/Proxy/InternalProxy.php
Normal file
19
lib/Doctrine/ORM/Proxy/InternalProxy.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Proxy;
|
||||
|
||||
use Doctrine\Persistence\Proxy;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @template T of object
|
||||
* @template-extends Proxy<T>
|
||||
*
|
||||
* @method void __setInitialized(bool $initialized)
|
||||
*/
|
||||
interface InternalProxy extends Proxy
|
||||
{
|
||||
}
|
||||
@@ -10,7 +10,11 @@ use Doctrine\Common\Proxy\Proxy as BaseProxy;
|
||||
* Interface for proxy classes.
|
||||
*
|
||||
* @deprecated 2.14. Use \Doctrine\Persistence\Proxy instead
|
||||
*
|
||||
* @template T of object
|
||||
* @template-extends BaseProxy<T>
|
||||
* @template-extends InternalProxy<T>
|
||||
*/
|
||||
interface Proxy extends BaseProxy
|
||||
interface Proxy extends BaseProxy, InternalProxy
|
||||
{
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ use Doctrine\Common\Proxy\AbstractProxyFactory;
|
||||
use Doctrine\Common\Proxy\Proxy as CommonProxy;
|
||||
use Doctrine\Common\Proxy\ProxyDefinition;
|
||||
use Doctrine\Common\Proxy\ProxyGenerator;
|
||||
use Doctrine\Common\Util\ClassUtils;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityNotFoundException;
|
||||
use Doctrine\ORM\ORMInvalidArgumentException;
|
||||
use Doctrine\ORM\Persisters\Entity\EntityPersister;
|
||||
use Doctrine\ORM\Proxy\Proxy as LegacyProxy;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
@@ -20,22 +21,82 @@ use Doctrine\Persistence\Mapping\ClassMetadata;
|
||||
use Doctrine\Persistence\Proxy;
|
||||
use ReflectionProperty;
|
||||
use Symfony\Component\VarExporter\ProxyHelper;
|
||||
use Symfony\Component\VarExporter\VarExporter;
|
||||
use Throwable;
|
||||
|
||||
use function array_combine;
|
||||
use function array_flip;
|
||||
use function array_intersect_key;
|
||||
use function bin2hex;
|
||||
use function chmod;
|
||||
use function class_exists;
|
||||
use function dirname;
|
||||
use function file_exists;
|
||||
use function file_put_contents;
|
||||
use function filemtime;
|
||||
use function is_bool;
|
||||
use function is_dir;
|
||||
use function is_int;
|
||||
use function is_writable;
|
||||
use function ltrim;
|
||||
use function mkdir;
|
||||
use function preg_match_all;
|
||||
use function random_bytes;
|
||||
use function rename;
|
||||
use function rtrim;
|
||||
use function str_replace;
|
||||
use function strpos;
|
||||
use function strrpos;
|
||||
use function strtr;
|
||||
use function substr;
|
||||
use function uksort;
|
||||
use function ucfirst;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use const PHP_VERSION_ID;
|
||||
|
||||
/**
|
||||
* This factory is used to create proxy objects for entities at runtime.
|
||||
*
|
||||
* @psalm-type AutogenerateMode = ProxyFactory::AUTOGENERATE_NEVER|ProxyFactory::AUTOGENERATE_ALWAYS|ProxyFactory::AUTOGENERATE_FILE_NOT_EXISTS|ProxyFactory::AUTOGENERATE_EVAL|ProxyFactory::AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED
|
||||
*/
|
||||
class ProxyFactory extends AbstractProxyFactory
|
||||
{
|
||||
/**
|
||||
* Never autogenerate a proxy and rely that it was generated by some
|
||||
* process before deployment.
|
||||
*/
|
||||
public const AUTOGENERATE_NEVER = 0;
|
||||
|
||||
/**
|
||||
* Always generates a new proxy in every request.
|
||||
*
|
||||
* This is only sane during development.
|
||||
*/
|
||||
public const AUTOGENERATE_ALWAYS = 1;
|
||||
|
||||
/**
|
||||
* Autogenerate the proxy class when the proxy file does not exist.
|
||||
*
|
||||
* This strategy causes a file_exists() call whenever any proxy is used the
|
||||
* first time in a request.
|
||||
*/
|
||||
public const AUTOGENERATE_FILE_NOT_EXISTS = 2;
|
||||
|
||||
/**
|
||||
* Generate the proxy classes using eval().
|
||||
*
|
||||
* This strategy is only sane for development, and even then it gives me
|
||||
* the creeps a little.
|
||||
*/
|
||||
public const AUTOGENERATE_EVAL = 3;
|
||||
|
||||
/**
|
||||
* Autogenerate the proxy class when the proxy file does not exist or
|
||||
* when the proxied file changed.
|
||||
*
|
||||
* This strategy causes a file_exists() call whenever any proxy is used the
|
||||
* first time in a request. When the proxied file is changed, the proxy will
|
||||
* be updated.
|
||||
*/
|
||||
public const AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED = 4;
|
||||
|
||||
private const PROXY_CLASS_TEMPLATE = <<<'EOPHP'
|
||||
<?php
|
||||
|
||||
@@ -48,15 +109,6 @@ class <proxyShortClassName> extends \<className> implements \<baseProxyInterface
|
||||
{
|
||||
<useLazyGhostTrait>
|
||||
|
||||
public function __construct(?\Closure $initializer = null, ?\Closure $cloner = null)
|
||||
{
|
||||
if ($cloner !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::createLazyGhost($initializer, <skippedProperties>, $this);
|
||||
}
|
||||
|
||||
public function __isInitialized(): bool
|
||||
{
|
||||
return isset($this->lazyObjectState) && $this->isLazyObjectInitialized();
|
||||
@@ -76,9 +128,15 @@ EOPHP;
|
||||
/** @var UnitOfWork The UnitOfWork this factory uses to retrieve persisters */
|
||||
private $uow;
|
||||
|
||||
/** @var string */
|
||||
private $proxyDir;
|
||||
|
||||
/** @var string */
|
||||
private $proxyNs;
|
||||
|
||||
/** @var self::AUTOGENERATE_* */
|
||||
private $autoGenerate;
|
||||
|
||||
/**
|
||||
* The IdentifierFlattener used for manipulating identifiers
|
||||
*
|
||||
@@ -86,39 +144,57 @@ EOPHP;
|
||||
*/
|
||||
private $identifierFlattener;
|
||||
|
||||
/** @var ProxyDefinition[] */
|
||||
private $definitions = [];
|
||||
/** @var array<class-string, Closure> */
|
||||
private $proxyFactories = [];
|
||||
|
||||
/** @var bool */
|
||||
private $isLazyGhostObjectEnabled = true;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the <tt>ProxyFactory</tt> class that is
|
||||
* connected to the given <tt>EntityManager</tt>.
|
||||
*
|
||||
* @param EntityManagerInterface $em The EntityManager the new factory works for.
|
||||
* @param string $proxyDir The directory to use for the proxy classes. It must exist.
|
||||
* @param string $proxyNs The namespace to use for the proxy classes.
|
||||
* @param bool|int $autoGenerate The strategy for automatically generating proxy classes. Possible
|
||||
* values are constants of {@see ProxyFactory::AUTOGENERATE_*}.
|
||||
* @psalm-param bool|AutogenerateMode $autoGenerate
|
||||
* @param EntityManagerInterface $em The EntityManager the new factory works for.
|
||||
* @param string $proxyDir The directory to use for the proxy classes. It must exist.
|
||||
* @param string $proxyNs The namespace to use for the proxy classes.
|
||||
* @param bool|self::AUTOGENERATE_* $autoGenerate The strategy for automatically generating proxy classes.
|
||||
*/
|
||||
public function __construct(EntityManagerInterface $em, $proxyDir, $proxyNs, $autoGenerate = self::AUTOGENERATE_NEVER)
|
||||
{
|
||||
$proxyGenerator = new ProxyGenerator($proxyDir, $proxyNs);
|
||||
if (! $em->getConfiguration()->isLazyGhostObjectEnabled()) {
|
||||
if (PHP_VERSION_ID >= 80100) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/10837/',
|
||||
'Not enabling lazy ghost objects is deprecated and will not be supported in Doctrine ORM 3.0. Ensure Doctrine\ORM\Configuration::setLazyGhostObjectEnabled(true) is called to enable them.'
|
||||
);
|
||||
}
|
||||
|
||||
if ($em->getConfiguration()->isLazyGhostObjectEnabled()) {
|
||||
$proxyGenerator->setPlaceholder('baseProxyInterface', Proxy::class);
|
||||
$proxyGenerator->setPlaceholder('useLazyGhostTrait', Closure::fromCallable([$this, 'generateUseLazyGhostTrait']));
|
||||
$proxyGenerator->setPlaceholder('skippedProperties', Closure::fromCallable([$this, 'generateSkippedProperties']));
|
||||
$proxyGenerator->setPlaceholder('serializeImpl', Closure::fromCallable([$this, 'generateSerializeImpl']));
|
||||
$proxyGenerator->setProxyClassTemplate(self::PROXY_CLASS_TEMPLATE);
|
||||
} else {
|
||||
$this->isLazyGhostObjectEnabled = false;
|
||||
|
||||
$proxyGenerator = new ProxyGenerator($proxyDir, $proxyNs);
|
||||
$proxyGenerator->setPlaceholder('baseProxyInterface', LegacyProxy::class);
|
||||
|
||||
parent::__construct($proxyGenerator, $em->getMetadataFactory(), $autoGenerate);
|
||||
}
|
||||
|
||||
parent::__construct($proxyGenerator, $em->getMetadataFactory(), $autoGenerate);
|
||||
if (! $proxyDir) {
|
||||
throw ORMInvalidArgumentException::proxyDirectoryRequired();
|
||||
}
|
||||
|
||||
if (! $proxyNs) {
|
||||
throw ORMInvalidArgumentException::proxyNamespaceRequired();
|
||||
}
|
||||
|
||||
if (is_int($autoGenerate) ? $autoGenerate < 0 || $autoGenerate > 4 : ! is_bool($autoGenerate)) {
|
||||
throw ORMInvalidArgumentException::invalidAutoGenerateMode($autoGenerate);
|
||||
}
|
||||
|
||||
$this->em = $em;
|
||||
$this->uow = $em->getUnitOfWork();
|
||||
$this->proxyDir = $proxyDir;
|
||||
$this->proxyNs = $proxyNs;
|
||||
$this->autoGenerate = (int) $autoGenerate;
|
||||
$this->identifierFlattener = new IdentifierFlattener($this->uow, $em->getMetadataFactory());
|
||||
}
|
||||
|
||||
@@ -127,19 +203,57 @@ EOPHP;
|
||||
*/
|
||||
public function getProxy($className, array $identifier)
|
||||
{
|
||||
$proxy = parent::getProxy($className, $identifier);
|
||||
|
||||
if (! $this->em->getConfiguration()->isLazyGhostObjectEnabled()) {
|
||||
return $proxy;
|
||||
if (! $this->isLazyGhostObjectEnabled) {
|
||||
return parent::getProxy($className, $identifier);
|
||||
}
|
||||
|
||||
$initializer = $this->definitions[$className]->initializer;
|
||||
$proxyFactory = $this->proxyFactories[$className] ?? $this->getProxyFactory($className);
|
||||
|
||||
$proxy->__construct(static function (Proxy $object) use ($initializer, $proxy): void {
|
||||
$initializer($object, $proxy);
|
||||
});
|
||||
return $proxyFactory($identifier);
|
||||
}
|
||||
|
||||
return $proxy;
|
||||
/**
|
||||
* Generates proxy classes for all given classes.
|
||||
*
|
||||
* @param ClassMetadata[] $classes The classes (ClassMetadata instances) for which to generate proxies.
|
||||
* @param string|null $proxyDir The target directory of the proxy classes. If not specified, the
|
||||
* directory configured on the Configuration of the EntityManager used
|
||||
* by this factory is used.
|
||||
*
|
||||
* @return int Number of generated proxies.
|
||||
*/
|
||||
public function generateProxyClasses(array $classes, $proxyDir = null)
|
||||
{
|
||||
if (! $this->isLazyGhostObjectEnabled) {
|
||||
return parent::generateProxyClasses($classes, $proxyDir);
|
||||
}
|
||||
|
||||
$generated = 0;
|
||||
|
||||
foreach ($classes as $class) {
|
||||
if ($this->skipClass($class)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$proxyFileName = $this->getProxyFileName($class->getName(), $proxyDir ?: $this->proxyDir);
|
||||
$proxyClassName = self::generateProxyClassName($class->getName(), $this->proxyNs);
|
||||
|
||||
$this->generateProxyClass($class, $proxyFileName, $proxyClassName);
|
||||
|
||||
++$generated;
|
||||
}
|
||||
|
||||
return $generated;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @deprecated ProxyFactory::resetUninitializedProxy() is deprecated and will be removed in version 3.0 of doctrine/orm.
|
||||
*/
|
||||
public function resetUninitializedProxy(CommonProxy $proxy)
|
||||
{
|
||||
return parent::resetUninitializedProxy($proxy);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,23 +268,19 @@ EOPHP;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @deprecated ProxyFactory::createProxyDefinition() is deprecated and will be removed in version 3.0 of doctrine/orm.
|
||||
*/
|
||||
protected function createProxyDefinition($className)
|
||||
{
|
||||
$classMetadata = $this->em->getClassMetadata($className);
|
||||
$entityPersister = $this->uow->getEntityPersister($className);
|
||||
|
||||
if ($this->em->getConfiguration()->isLazyGhostObjectEnabled()) {
|
||||
$initializer = $this->createLazyInitializer($classMetadata, $entityPersister);
|
||||
$cloner = static function (): void {
|
||||
};
|
||||
} else {
|
||||
$initializer = $this->createInitializer($classMetadata, $entityPersister);
|
||||
$cloner = $this->createCloner($classMetadata, $entityPersister);
|
||||
}
|
||||
$initializer = $this->createInitializer($classMetadata, $entityPersister);
|
||||
$cloner = $this->createCloner($classMetadata, $entityPersister);
|
||||
|
||||
return $this->definitions[$className] = new ProxyDefinition(
|
||||
ClassUtils::generateProxyClassName($className, $this->proxyNs),
|
||||
return new ProxyDefinition(
|
||||
self::generateProxyClassName($className, $this->proxyNs),
|
||||
$classMetadata->getIdentifierFieldNames(),
|
||||
$classMetadata->getReflectionProperties(),
|
||||
$initializer,
|
||||
@@ -181,6 +291,8 @@ EOPHP;
|
||||
/**
|
||||
* Creates a closure capable of initializing a proxy
|
||||
*
|
||||
* @deprecated ProxyFactory::createInitializer() is deprecated and will be removed in version 3.0 of doctrine/orm.
|
||||
*
|
||||
* @psalm-return Closure(CommonProxy):void
|
||||
*
|
||||
* @throws EntityNotFoundException
|
||||
@@ -242,20 +354,20 @@ EOPHP;
|
||||
/**
|
||||
* Creates a closure capable of initializing a proxy
|
||||
*
|
||||
* @return Closure(Proxy, Proxy):void
|
||||
* @return Closure(InternalProxy, InternalProxy):void
|
||||
*
|
||||
* @throws EntityNotFoundException
|
||||
*/
|
||||
private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister): Closure
|
||||
private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure
|
||||
{
|
||||
return function (Proxy $proxy, Proxy $original) use ($entityPersister, $classMetadata): void {
|
||||
return static function (InternalProxy $proxy, InternalProxy $original) use ($entityPersister, $classMetadata, $identifierFlattener): void {
|
||||
$identifier = $classMetadata->getIdentifierValues($original);
|
||||
$entity = $entityPersister->loadById($identifier, $original);
|
||||
|
||||
if ($entity === null) {
|
||||
throw EntityNotFoundException::fromClassNameAndIdentifier(
|
||||
$classMetadata->getName(),
|
||||
$this->identifierFlattener->flattenIdentifier($classMetadata, $identifier)
|
||||
$identifierFlattener->flattenIdentifier($classMetadata, $identifier)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -270,7 +382,6 @@ EOPHP;
|
||||
continue;
|
||||
}
|
||||
|
||||
$property->setAccessible(true);
|
||||
$property->setValue($proxy, $property->getValue($entity));
|
||||
}
|
||||
};
|
||||
@@ -279,6 +390,8 @@ EOPHP;
|
||||
/**
|
||||
* Creates a closure capable of finalizing state a cloned proxy
|
||||
*
|
||||
* @deprecated ProxyFactory::createCloner() is deprecated and will be removed in version 3.0 of doctrine/orm.
|
||||
*
|
||||
* @psalm-return Closure(CommonProxy):void
|
||||
*
|
||||
* @throws EntityNotFoundException
|
||||
@@ -315,6 +428,144 @@ EOPHP;
|
||||
};
|
||||
}
|
||||
|
||||
private function getProxyFileName(string $className, string $baseDirectory): string
|
||||
{
|
||||
$baseDirectory = $baseDirectory ?: $this->proxyDir;
|
||||
|
||||
return rtrim($baseDirectory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . InternalProxy::MARKER
|
||||
. str_replace('\\', '', $className) . '.php';
|
||||
}
|
||||
|
||||
private function getProxyFactory(string $className): Closure
|
||||
{
|
||||
$skippedProperties = [];
|
||||
$class = $this->em->getClassMetadata($className);
|
||||
$identifiers = array_flip($class->getIdentifierFieldNames());
|
||||
$filter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE;
|
||||
$reflector = $class->getReflectionClass();
|
||||
|
||||
while ($reflector) {
|
||||
foreach ($reflector->getProperties($filter) as $property) {
|
||||
$name = $property->name;
|
||||
|
||||
if ($property->isStatic() || (($class->hasField($name) || $class->hasAssociation($name)) && ! isset($identifiers[$name]))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$prefix = $property->isPrivate() ? "\0" . $property->class . "\0" : ($property->isProtected() ? "\0*\0" : '');
|
||||
|
||||
$skippedProperties[$prefix . $name] = true;
|
||||
}
|
||||
|
||||
$filter = ReflectionProperty::IS_PRIVATE;
|
||||
$reflector = $reflector->getParentClass();
|
||||
}
|
||||
|
||||
$className = $class->getName(); // aliases and case sensitivity
|
||||
$entityPersister = $this->uow->getEntityPersister($className);
|
||||
$initializer = $this->createLazyInitializer($class, $entityPersister, $this->identifierFlattener);
|
||||
$proxyClassName = $this->loadProxyClass($class);
|
||||
$identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers);
|
||||
|
||||
$proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy {
|
||||
$proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, &$proxy): void {
|
||||
$initializer($object, $proxy);
|
||||
}, $skippedProperties);
|
||||
|
||||
foreach ($identifierFields as $idField => $reflector) {
|
||||
if (! isset($identifier[$idField])) {
|
||||
throw ORMInvalidArgumentException::missingPrimaryKeyValue($className, $idField);
|
||||
}
|
||||
|
||||
$reflector->setValue($proxy, $identifier[$idField]);
|
||||
}
|
||||
|
||||
return $proxy;
|
||||
}, null, $proxyClassName);
|
||||
|
||||
return $this->proxyFactories[$className] = $proxyFactory;
|
||||
}
|
||||
|
||||
private function loadProxyClass(ClassMetadata $class): string
|
||||
{
|
||||
$proxyClassName = self::generateProxyClassName($class->getName(), $this->proxyNs);
|
||||
|
||||
if (class_exists($proxyClassName, false)) {
|
||||
return $proxyClassName;
|
||||
}
|
||||
|
||||
if ($this->autoGenerate === self::AUTOGENERATE_EVAL) {
|
||||
$this->generateProxyClass($class, null, $proxyClassName);
|
||||
|
||||
return $proxyClassName;
|
||||
}
|
||||
|
||||
$fileName = $this->getProxyFileName($class->getName(), $this->proxyDir);
|
||||
|
||||
switch ($this->autoGenerate) {
|
||||
case self::AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED:
|
||||
if (file_exists($fileName) && filemtime($fileName) >= filemtime($class->getReflectionClass()->getFileName())) {
|
||||
break;
|
||||
}
|
||||
// no break
|
||||
case self::AUTOGENERATE_FILE_NOT_EXISTS:
|
||||
if (file_exists($fileName)) {
|
||||
break;
|
||||
}
|
||||
// no break
|
||||
case self::AUTOGENERATE_ALWAYS:
|
||||
$this->generateProxyClass($class, $fileName, $proxyClassName);
|
||||
break;
|
||||
}
|
||||
|
||||
require $fileName;
|
||||
|
||||
return $proxyClassName;
|
||||
}
|
||||
|
||||
private function generateProxyClass(ClassMetadata $class, ?string $fileName, string $proxyClassName): void
|
||||
{
|
||||
$i = strrpos($proxyClassName, '\\');
|
||||
$placeholders = [
|
||||
'<className>' => $class->getName(),
|
||||
'<namespace>' => substr($proxyClassName, 0, $i),
|
||||
'<proxyShortClassName>' => substr($proxyClassName, 1 + $i),
|
||||
'<baseProxyInterface>' => InternalProxy::class,
|
||||
];
|
||||
|
||||
preg_match_all('(<([a-zA-Z]+)>)', self::PROXY_CLASS_TEMPLATE, $placeholderMatches);
|
||||
|
||||
foreach (array_combine($placeholderMatches[0], $placeholderMatches[1]) as $placeholder => $name) {
|
||||
$placeholders[$placeholder] ?? $placeholders[$placeholder] = $this->{'generate' . ucfirst($name)}($class);
|
||||
}
|
||||
|
||||
$proxyCode = strtr(self::PROXY_CLASS_TEMPLATE, $placeholders);
|
||||
|
||||
if (! $fileName) {
|
||||
if (! class_exists($proxyClassName)) {
|
||||
eval(substr($proxyCode, 5));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$parentDirectory = dirname($fileName);
|
||||
|
||||
if (! is_dir($parentDirectory) && ! @mkdir($parentDirectory, 0775, true)) {
|
||||
throw ORMInvalidArgumentException::proxyDirectoryNotWritable($this->proxyDir);
|
||||
}
|
||||
|
||||
if (! is_writable($parentDirectory)) {
|
||||
throw ORMInvalidArgumentException::proxyDirectoryNotWritable($this->proxyDir);
|
||||
}
|
||||
|
||||
$tmpFileName = $fileName . '.' . bin2hex(random_bytes(12));
|
||||
|
||||
file_put_contents($tmpFileName, $proxyCode);
|
||||
@chmod($tmpFileName, 0664);
|
||||
rename($tmpFileName, $fileName);
|
||||
}
|
||||
|
||||
private function generateUseLazyGhostTrait(ClassMetadata $class): string
|
||||
{
|
||||
$code = ProxyHelper::generateLazyGhost($class->getReflectionClass());
|
||||
@@ -331,46 +582,13 @@ EOPHP;
|
||||
return $code;
|
||||
}
|
||||
|
||||
private function generateSkippedProperties(ClassMetadata $class): string
|
||||
{
|
||||
$skippedProperties = [];
|
||||
$identifiers = array_flip($class->getIdentifierFieldNames());
|
||||
$filter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE;
|
||||
$reflector = $class->getReflectionClass();
|
||||
|
||||
while ($reflector) {
|
||||
foreach ($reflector->getProperties($filter) as $property) {
|
||||
$name = $property->getName();
|
||||
|
||||
if ($property->isStatic() || (($class->hasField($name) || $class->hasAssociation($name)) && ! isset($identifiers[$name]))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$prefix = $property->isPrivate() ? "\0" . $property->getDeclaringClass()->getName() . "\0" : ($property->isProtected() ? "\0*\0" : '');
|
||||
|
||||
$skippedProperties[$prefix . $name] = true;
|
||||
}
|
||||
|
||||
$filter = ReflectionProperty::IS_PRIVATE;
|
||||
$reflector = $reflector->getParentClass();
|
||||
}
|
||||
|
||||
uksort($skippedProperties, 'strnatcmp');
|
||||
|
||||
$code = VarExporter::export($skippedProperties);
|
||||
$code = str_replace(VarExporter::export($class->getName()), 'parent::class', $code);
|
||||
$code = str_replace("\n", "\n ", $code);
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
private function generateSerializeImpl(ClassMetadata $class): string
|
||||
{
|
||||
$reflector = $class->getReflectionClass();
|
||||
$properties = $reflector->hasMethod('__serialize') ? 'parent::__serialize()' : '(array) $this';
|
||||
|
||||
$code = '$properties = ' . $properties . ';
|
||||
unset($properties["\0" . self::class . "\0lazyObjectState"], $properties[\'__isCloning\']);
|
||||
unset($properties["\0" . self::class . "\0lazyObjectState"]);
|
||||
|
||||
';
|
||||
|
||||
@@ -381,7 +599,7 @@ EOPHP;
|
||||
return $code . '$data = [];
|
||||
|
||||
foreach (parent::__sleep() as $name) {
|
||||
$value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0' . $reflector->getName() . '\0$name"] ?? $k = null;
|
||||
$value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0' . $reflector->name . '\0$name"] ?? $k = null;
|
||||
|
||||
if (null === $k) {
|
||||
trigger_error(sprintf(\'serialize(): "%s" returned as member variable from __sleep() but does not exist\', $name), \E_USER_NOTICE);
|
||||
@@ -392,4 +610,9 @@ EOPHP;
|
||||
|
||||
return $data;';
|
||||
}
|
||||
|
||||
private static function generateProxyClassName(string $className, string $proxyNamespace): string
|
||||
{
|
||||
return rtrim($proxyNamespace, '\\') . '\\' . Proxy::MARKER . '\\' . ltrim($className, '\\');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,10 @@ use function stripos;
|
||||
|
||||
/**
|
||||
* A Query object represents a DQL query.
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
final class Query extends AbstractQuery
|
||||
class Query extends AbstractQuery
|
||||
{
|
||||
/**
|
||||
* A query object is in CLEAN state when it has NO unparsed/unprocessed DQL parts.
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Doctrine\ORM\Query\AST;
|
||||
*
|
||||
* @link www.doctrine-project.org
|
||||
*/
|
||||
class ConditionalFactor extends Node
|
||||
class ConditionalFactor extends Node implements Phase2OptimizableConditional
|
||||
{
|
||||
/** @var bool */
|
||||
public $not = false;
|
||||
|
||||
@@ -9,12 +9,12 @@ namespace Doctrine\ORM\Query\AST;
|
||||
*
|
||||
* @link www.doctrine-project.org
|
||||
*/
|
||||
class ConditionalPrimary extends Node
|
||||
class ConditionalPrimary extends Node implements Phase2OptimizableConditional
|
||||
{
|
||||
/** @var Node|null */
|
||||
public $simpleConditionalExpression;
|
||||
|
||||
/** @var ConditionalExpression|null */
|
||||
/** @var ConditionalExpression|Phase2OptimizableConditional|null */
|
||||
public $conditionalExpression;
|
||||
|
||||
/** @return bool */
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Doctrine\ORM\Query\AST;
|
||||
*
|
||||
* @link www.doctrine-project.org
|
||||
*/
|
||||
class ConditionalTerm extends Node
|
||||
class ConditionalTerm extends Node implements Phase2OptimizableConditional
|
||||
{
|
||||
/** @var mixed[] */
|
||||
public $conditionalFactors = [];
|
||||
|
||||
@@ -6,10 +6,10 @@ namespace Doctrine\ORM\Query\AST;
|
||||
|
||||
class HavingClause extends Node
|
||||
{
|
||||
/** @var ConditionalExpression */
|
||||
/** @var ConditionalExpression|Phase2OptimizableConditional */
|
||||
public $conditionalExpression;
|
||||
|
||||
/** @param ConditionalExpression $conditionalExpression */
|
||||
/** @param ConditionalExpression|Phase2OptimizableConditional $conditionalExpression */
|
||||
public function __construct($conditionalExpression)
|
||||
{
|
||||
$this->conditionalExpression = $conditionalExpression;
|
||||
|
||||
@@ -25,7 +25,7 @@ class Join extends Node
|
||||
/** @var Node|null */
|
||||
public $joinAssociationDeclaration = null;
|
||||
|
||||
/** @var ConditionalExpression|null */
|
||||
/** @var ConditionalExpression|Phase2OptimizableConditional|null */
|
||||
public $conditionalExpression = null;
|
||||
|
||||
/**
|
||||
|
||||
17
lib/Doctrine/ORM/Query/AST/Phase2OptimizableConditional.php
Normal file
17
lib/Doctrine/ORM/Query/AST/Phase2OptimizableConditional.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query\AST;
|
||||
|
||||
/**
|
||||
* Marks types that can be used in place of a ConditionalExpression as a phase
|
||||
* 2 optimization.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @psalm-inheritors ConditionalPrimary|ConditionalFactor|ConditionalTerm
|
||||
*/
|
||||
interface Phase2OptimizableConditional
|
||||
{
|
||||
}
|
||||
@@ -11,15 +11,15 @@ namespace Doctrine\ORM\Query\AST;
|
||||
*/
|
||||
class WhenClause extends Node
|
||||
{
|
||||
/** @var ConditionalExpression */
|
||||
/** @var ConditionalExpression|Phase2OptimizableConditional */
|
||||
public $caseConditionExpression;
|
||||
|
||||
/** @var mixed */
|
||||
public $thenScalarExpression = null;
|
||||
|
||||
/**
|
||||
* @param ConditionalExpression $caseConditionExpression
|
||||
* @param mixed $thenScalarExpression
|
||||
* @param ConditionalExpression|Phase2OptimizableConditional $caseConditionExpression
|
||||
* @param mixed $thenScalarExpression
|
||||
*/
|
||||
public function __construct($caseConditionExpression, $thenScalarExpression)
|
||||
{
|
||||
|
||||
@@ -11,10 +11,10 @@ namespace Doctrine\ORM\Query\AST;
|
||||
*/
|
||||
class WhereClause extends Node
|
||||
{
|
||||
/** @var ConditionalExpression|ConditionalTerm */
|
||||
/** @var ConditionalExpression|Phase2OptimizableConditional */
|
||||
public $conditionalExpression;
|
||||
|
||||
/** @param ConditionalExpression $conditionalExpression */
|
||||
/** @param ConditionalExpression|Phase2OptimizableConditional $conditionalExpression */
|
||||
public function __construct($conditionalExpression)
|
||||
{
|
||||
$this->conditionalExpression = $conditionalExpression;
|
||||
|
||||
@@ -9,6 +9,12 @@ use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
|
||||
use function array_diff;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_values;
|
||||
use function str_replace;
|
||||
|
||||
/**
|
||||
* Base class for SQL statement executors.
|
||||
*
|
||||
@@ -18,12 +24,24 @@ use Doctrine\DBAL\Types\Type;
|
||||
*/
|
||||
abstract class AbstractSqlExecutor
|
||||
{
|
||||
/** @var list<string>|string */
|
||||
/**
|
||||
* @deprecated use $sqlStatements instead
|
||||
*
|
||||
* @var list<string>|string
|
||||
*/
|
||||
protected $_sqlStatements;
|
||||
|
||||
/** @var list<string>|string */
|
||||
protected $sqlStatements;
|
||||
|
||||
/** @var QueryCacheProfile */
|
||||
protected $queryCacheProfile;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->_sqlStatements = &$this->sqlStatements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the SQL statements that are executed by the executor.
|
||||
*
|
||||
@@ -31,21 +49,18 @@ abstract class AbstractSqlExecutor
|
||||
*/
|
||||
public function getSqlStatements()
|
||||
{
|
||||
return $this->_sqlStatements;
|
||||
return $this->sqlStatements;
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function setQueryCacheProfile(QueryCacheProfile $qcp)
|
||||
public function setQueryCacheProfile(QueryCacheProfile $qcp): void
|
||||
{
|
||||
$this->queryCacheProfile = $qcp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not use query cache
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function removeQueryCacheProfile()
|
||||
public function removeQueryCacheProfile(): void
|
||||
{
|
||||
$this->queryCacheProfile = null;
|
||||
}
|
||||
@@ -60,4 +75,28 @@ abstract class AbstractSqlExecutor
|
||||
* @return Result|int
|
||||
*/
|
||||
abstract public function execute(Connection $conn, array $params, array $types);
|
||||
|
||||
/** @return list<string> */
|
||||
public function __sleep(): array
|
||||
{
|
||||
/* Two reasons for this:
|
||||
- we do not need to serialize the deprecated property, we can
|
||||
rebuild the reference to the new property in __wakeup()
|
||||
- not having the legacy property in the serialized data means the
|
||||
serialized representation becomes compatible with 3.0.x, meaning
|
||||
there will not be a deprecation warning about a missing property
|
||||
when unserializing data */
|
||||
return array_values(array_diff(array_map(static function (string $prop): string {
|
||||
return str_replace("\0*\0", '', $prop);
|
||||
}, array_keys((array) $this)), ['_sqlStatements']));
|
||||
}
|
||||
|
||||
public function __wakeup(): void
|
||||
{
|
||||
if ($this->_sqlStatements !== null && $this->sqlStatements === null) {
|
||||
$this->sqlStatements = $this->_sqlStatements;
|
||||
}
|
||||
|
||||
$this->_sqlStatements = &$this->sqlStatements;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ class MultiTableDeleteExecutor extends AbstractSqlExecutor
|
||||
*/
|
||||
public function __construct(AST\Node $AST, $sqlWalker)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$em = $sqlWalker->getEntityManager();
|
||||
$conn = $em->getConnection();
|
||||
$platform = $conn->getDatabasePlatform();
|
||||
@@ -83,8 +85,8 @@ class MultiTableDeleteExecutor extends AbstractSqlExecutor
|
||||
// 3. Create and store DELETE statements
|
||||
$classNames = array_merge($primaryClass->parentClasses, [$primaryClass->name], $primaryClass->subClasses);
|
||||
foreach (array_reverse($classNames) as $className) {
|
||||
$tableName = $quoteStrategy->getTableName($em->getClassMetadata($className), $platform);
|
||||
$this->_sqlStatements[] = 'DELETE FROM ' . $tableName
|
||||
$tableName = $quoteStrategy->getTableName($em->getClassMetadata($className), $platform);
|
||||
$this->sqlStatements[] = 'DELETE FROM ' . $tableName
|
||||
. ' WHERE (' . $idColumnList . ') IN (' . $idSubselect . ')';
|
||||
}
|
||||
|
||||
@@ -117,7 +119,7 @@ class MultiTableDeleteExecutor extends AbstractSqlExecutor
|
||||
$numDeleted = $conn->executeStatement($this->insertSql, $params, $types);
|
||||
|
||||
// Execute DELETE statements
|
||||
foreach ($this->_sqlStatements as $sql) {
|
||||
foreach ($this->sqlStatements as $sql) {
|
||||
$conn->executeStatement($sql);
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
|
||||
@@ -50,6 +50,8 @@ class MultiTableUpdateExecutor extends AbstractSqlExecutor
|
||||
*/
|
||||
public function __construct(AST\Node $AST, $sqlWalker)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$em = $sqlWalker->getEntityManager();
|
||||
$conn = $em->getConnection();
|
||||
$platform = $conn->getDatabasePlatform();
|
||||
@@ -119,7 +121,7 @@ class MultiTableUpdateExecutor extends AbstractSqlExecutor
|
||||
}
|
||||
|
||||
if ($affected) {
|
||||
$this->_sqlStatements[$i] = $updateSql . ' WHERE (' . $idColumnList . ') IN (' . $idSubselect . ')';
|
||||
$this->sqlStatements[$i] = $updateSql . ' WHERE (' . $idColumnList . ') IN (' . $idSubselect . ')';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +165,7 @@ class MultiTableUpdateExecutor extends AbstractSqlExecutor
|
||||
);
|
||||
|
||||
// Execute UPDATE statements
|
||||
foreach ($this->_sqlStatements as $key => $statement) {
|
||||
foreach ($this->sqlStatements as $key => $statement) {
|
||||
$paramValues = [];
|
||||
$paramTypes = [];
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@ class SingleSelectExecutor extends AbstractSqlExecutor
|
||||
{
|
||||
public function __construct(SelectStatement $AST, SqlWalker $sqlWalker)
|
||||
{
|
||||
$this->_sqlStatements = $sqlWalker->walkSelectStatement($AST);
|
||||
parent::__construct();
|
||||
|
||||
$this->sqlStatements = $sqlWalker->walkSelectStatement($AST);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,6 +30,6 @@ class SingleSelectExecutor extends AbstractSqlExecutor
|
||||
*/
|
||||
public function execute(Connection $conn, array $params, array $types)
|
||||
{
|
||||
return $conn->executeQuery($this->_sqlStatements, $params, $types, $this->queryCacheProfile);
|
||||
return $conn->executeQuery($this->sqlStatements, $params, $types, $this->queryCacheProfile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +22,12 @@ class SingleTableDeleteUpdateExecutor extends AbstractSqlExecutor
|
||||
/** @param SqlWalker $sqlWalker */
|
||||
public function __construct(AST\Node $AST, $sqlWalker)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
if ($AST instanceof AST\UpdateStatement) {
|
||||
$this->_sqlStatements = $sqlWalker->walkUpdateStatement($AST);
|
||||
$this->sqlStatements = $sqlWalker->walkUpdateStatement($AST);
|
||||
} elseif ($AST instanceof AST\DeleteStatement) {
|
||||
$this->_sqlStatements = $sqlWalker->walkDeleteStatement($AST);
|
||||
$this->sqlStatements = $sqlWalker->walkDeleteStatement($AST);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +42,6 @@ class SingleTableDeleteUpdateExecutor extends AbstractSqlExecutor
|
||||
$conn->ensureConnectedToPrimary();
|
||||
}
|
||||
|
||||
return $conn->executeStatement($this->_sqlStatements, $params, $types);
|
||||
return $conn->executeStatement($this->sqlStatements, $params, $types);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,14 @@ class FilterCollection
|
||||
*/
|
||||
private $enabledFilters = [];
|
||||
|
||||
/**
|
||||
* Instances of suspended filters.
|
||||
*
|
||||
* @var SQLFilter[]
|
||||
* @psalm-var array<string, SQLFilter>
|
||||
*/
|
||||
private $suspendedFilters = [];
|
||||
|
||||
/**
|
||||
* The filter hash from the last time the query was parsed.
|
||||
*
|
||||
@@ -83,6 +91,17 @@ class FilterCollection
|
||||
return $this->enabledFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the suspended filters.
|
||||
*
|
||||
* @return SQLFilter[] The suspended filters.
|
||||
* @psalm-return array<string, SQLFilter>
|
||||
*/
|
||||
public function getSuspendedFilters(): array
|
||||
{
|
||||
return $this->suspendedFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables a filter from the collection.
|
||||
*
|
||||
@@ -105,6 +124,9 @@ class FilterCollection
|
||||
|
||||
$this->enabledFilters[$name] = new $filterClass($this->em);
|
||||
|
||||
// In case a suspended filter with the same name was forgotten
|
||||
unset($this->suspendedFilters[$name]);
|
||||
|
||||
// Keep the enabled filters sorted for the hash
|
||||
ksort($this->enabledFilters);
|
||||
|
||||
@@ -135,6 +157,54 @@ class FilterCollection
|
||||
return $filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspend a filter.
|
||||
*
|
||||
* @param string $name Name of the filter.
|
||||
*
|
||||
* @return SQLFilter The suspended filter.
|
||||
*
|
||||
* @throws InvalidArgumentException If the filter does not exist.
|
||||
*/
|
||||
public function suspend(string $name): SQLFilter
|
||||
{
|
||||
// Get the filter to return it
|
||||
$filter = $this->getFilter($name);
|
||||
|
||||
$this->suspendedFilters[$name] = $filter;
|
||||
unset($this->enabledFilters[$name]);
|
||||
|
||||
$this->setFiltersStateDirty();
|
||||
|
||||
return $filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a disabled filter from the collection.
|
||||
*
|
||||
* @param string $name Name of the filter.
|
||||
*
|
||||
* @return SQLFilter The restored filter.
|
||||
*
|
||||
* @throws InvalidArgumentException If the filter does not exist.
|
||||
*/
|
||||
public function restore(string $name): SQLFilter
|
||||
{
|
||||
if (! $this->isSuspended($name)) {
|
||||
throw new InvalidArgumentException("Filter '" . $name . "' is not suspended.");
|
||||
}
|
||||
|
||||
$this->enabledFilters[$name] = $this->suspendedFilters[$name];
|
||||
unset($this->suspendedFilters[$name]);
|
||||
|
||||
// Keep the enabled filters sorted for the hash
|
||||
ksort($this->enabledFilters);
|
||||
|
||||
$this->setFiltersStateDirty();
|
||||
|
||||
return $this->enabledFilters[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an enabled filter from the collection.
|
||||
*
|
||||
@@ -177,6 +247,18 @@ class FilterCollection
|
||||
return isset($this->enabledFilters[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a filter is suspended.
|
||||
*
|
||||
* @param string $name Name of the filter.
|
||||
*
|
||||
* @return bool True if the filter is suspended, false otherwise.
|
||||
*/
|
||||
public function isSuspended(string $name): bool
|
||||
{
|
||||
return isset($this->suspendedFilters[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the filter collection is clean.
|
||||
*
|
||||
|
||||
@@ -141,7 +141,9 @@ class ParserResult
|
||||
{
|
||||
foreach (self::LEGACY_PROPERTY_MAPPING as $property => $legacyProperty) {
|
||||
$this->$property = $data[sprintf("\0%s\0%s", self::class, $legacyProperty)]
|
||||
?? $data[self::class][$legacyProperty]
|
||||
?? $data[sprintf("\0%s\0%s", self::class, $property)]
|
||||
?? $data[self::class][$property]
|
||||
?? $this->$property
|
||||
?? null;
|
||||
}
|
||||
|
||||
@@ -204,6 +204,14 @@ class QueryException extends ORMException
|
||||
);
|
||||
}
|
||||
|
||||
public static function eagerFetchJoinWithNotAllowed(string $sourceEntity, string $fieldName): QueryException
|
||||
{
|
||||
return new self(
|
||||
'Associations with fetch-mode=EAGER may not be using WITH conditions in
|
||||
"' . $sourceEntity . '#' . $fieldName . '".'
|
||||
);
|
||||
}
|
||||
|
||||
public static function iterateWithMixedResultNotAllowed(): QueryException
|
||||
{
|
||||
return new self('Iterating a query with mixed results (using scalars) is not supported.');
|
||||
|
||||
@@ -154,6 +154,11 @@ class ResultSetMappingBuilder extends ResultSetMapping
|
||||
}
|
||||
|
||||
$this->addFieldResult($alias, $columnAlias, $propertyName);
|
||||
|
||||
$enumType = $classMetadata->getFieldMapping($propertyName)['enumType'] ?? null;
|
||||
if (! empty($enumType)) {
|
||||
$this->addEnumResult($columnAlias, $enumType);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($classMetadata->associationMappings as $associationMapping) {
|
||||
|
||||
@@ -1010,9 +1010,9 @@ class SqlWalker implements TreeWalker
|
||||
/**
|
||||
* Walks down a JoinAssociationDeclaration AST node, thereby generating the appropriate SQL.
|
||||
*
|
||||
* @param AST\JoinAssociationDeclaration $joinAssociationDeclaration
|
||||
* @param int $joinType
|
||||
* @param AST\ConditionalExpression $condExpr
|
||||
* @param AST\JoinAssociationDeclaration $joinAssociationDeclaration
|
||||
* @param int $joinType
|
||||
* @param AST\ConditionalExpression|AST\Phase2OptimizableConditional $condExpr
|
||||
* @psalm-param AST\Join::JOIN_TYPE_* $joinType
|
||||
*
|
||||
* @return string
|
||||
@@ -1047,7 +1047,9 @@ class SqlWalker implements TreeWalker
|
||||
}
|
||||
}
|
||||
|
||||
$targetTableJoin = null;
|
||||
if ($relation['fetch'] === ClassMetadata::FETCH_EAGER && $condExpr !== null) {
|
||||
throw QueryException::eagerFetchJoinWithNotAllowed($assoc['sourceEntity'], $assoc['fieldName']);
|
||||
}
|
||||
|
||||
// This condition is not checking ClassMetadata::MANY_TO_ONE, because by definition it cannot
|
||||
// be the owning side and previously we ensured that $assoc is always the owning side of the associations.
|
||||
@@ -2048,7 +2050,7 @@ class SqlWalker implements TreeWalker
|
||||
/**
|
||||
* Walk down a ConditionalExpression AST node, thereby generating the appropriate SQL.
|
||||
*
|
||||
* @param AST\ConditionalExpression $condExpr
|
||||
* @param AST\ConditionalExpression|AST\Phase2OptimizableConditional $condExpr
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
@@ -2068,7 +2070,7 @@ class SqlWalker implements TreeWalker
|
||||
/**
|
||||
* Walks down a ConditionalTerm AST node, thereby generating the appropriate SQL.
|
||||
*
|
||||
* @param AST\ConditionalTerm $condTerm
|
||||
* @param AST\ConditionalTerm|AST\ConditionalFactor|AST\ConditionalPrimary $condTerm
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
@@ -2088,7 +2090,7 @@ class SqlWalker implements TreeWalker
|
||||
/**
|
||||
* Walks down a ConditionalFactor AST node, thereby generating the appropriate SQL.
|
||||
*
|
||||
* @param AST\ConditionalFactor $factor
|
||||
* @param AST\ConditionalFactor|AST\ConditionalPrimary $factor
|
||||
*
|
||||
* @return string The SQL.
|
||||
*
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
|
||||
|
||||
use Doctrine\ORM\Cache;
|
||||
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
|
||||
use Doctrine\ORM\Tools\Console\CommandCompatibility;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
@@ -20,6 +21,8 @@ use function sprintf;
|
||||
*/
|
||||
class CollectionRegionCommand extends AbstractEntityManagerCommand
|
||||
{
|
||||
use CommandCompatibility;
|
||||
|
||||
/** @return void */
|
||||
protected function configure()
|
||||
{
|
||||
@@ -58,12 +61,7 @@ EOT
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
private function doExecute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
|
||||
|
||||
use Doctrine\ORM\Cache;
|
||||
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
|
||||
use Doctrine\ORM\Tools\Console\CommandCompatibility;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
@@ -20,6 +21,8 @@ use function sprintf;
|
||||
*/
|
||||
class EntityRegionCommand extends AbstractEntityManagerCommand
|
||||
{
|
||||
use CommandCompatibility;
|
||||
|
||||
/** @return void */
|
||||
protected function configure()
|
||||
{
|
||||
@@ -57,12 +60,7 @@ EOT
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
private function doExecute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
|
||||
|
||||
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
|
||||
use Doctrine\ORM\Tools\Console\CommandCompatibility;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
@@ -18,6 +19,8 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
*/
|
||||
class MetadataCommand extends AbstractEntityManagerCommand
|
||||
{
|
||||
use CommandCompatibility;
|
||||
|
||||
/** @return void */
|
||||
protected function configure()
|
||||
{
|
||||
@@ -31,12 +34,7 @@ EOT
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
private function doExecute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user