mirror of
https://github.com/doctrine/orm.git
synced 2026-03-24 15:02:22 +01:00
Compare commits
240 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a541b8b3a | ||
|
|
9fb9cc46e4 | ||
|
|
c322c71cd4 | ||
|
|
3c733a2fee | ||
|
|
5984ad586a | ||
|
|
ee2c3a506b | ||
|
|
781ed30926 | ||
|
|
04694a9f7b | ||
|
|
257c5094c4 | ||
|
|
66e0e92816 | ||
|
|
5b2060e25f | ||
|
|
64444dcfd5 | ||
|
|
eb2cd5375c | ||
|
|
de7140e105 | ||
|
|
39e35fc06c | ||
|
|
7f061c3870 | ||
|
|
74495711fb | ||
|
|
97a7cb8d2f | ||
|
|
85d66de9df | ||
|
|
1b98be31ce | ||
|
|
61f2752a80 | ||
|
|
f3371e1773 | ||
|
|
6476894dc4 | ||
|
|
e0052390e1 | ||
|
|
8c6419e0e0 | ||
|
|
6f5ce1aca2 | ||
|
|
98e7a53b42 | ||
|
|
3aaaf37dfb | ||
|
|
154a4652ee | ||
|
|
ae7489ff19 | ||
|
|
0f32569a7a | ||
|
|
d99f74c704 | ||
|
|
62ca8424d8 | ||
|
|
3f2209a571 | ||
|
|
1ee01f4473 | ||
|
|
8a9ed138a8 | ||
|
|
e714b1a2fc | ||
|
|
ec0bf05853 | ||
|
|
dc58aa3ea1 | ||
|
|
23b74e4f8b | ||
|
|
d2b699e6f5 | ||
|
|
0338d69324 | ||
|
|
2c01dac173 | ||
|
|
137ecb491a | ||
|
|
f38ee09082 | ||
|
|
6ab858a5c5 | ||
|
|
3dca27ce0d | ||
|
|
e19704e1f8 | ||
|
|
41ea59ac66 | ||
|
|
e605e6d569 | ||
|
|
9437675d3b | ||
|
|
63409d638c | ||
|
|
9b9160b206 | ||
|
|
6deec3655b | ||
|
|
7f40422d21 | ||
|
|
e67fa5388b | ||
|
|
80053336c9 | ||
|
|
dddcc507ef | ||
|
|
b41d9da88d | ||
|
|
c04bfb78b7 | ||
|
|
8a5dfc86d4 | ||
|
|
79e103c07e | ||
|
|
5afadf163a | ||
|
|
edfaa37228 | ||
|
|
ea056e98ba | ||
|
|
bab5771e98 | ||
|
|
ee919d6231 | ||
|
|
04c390693a | ||
|
|
49293c4d48 | ||
|
|
8d9e2e7d4e | ||
|
|
ef607f26c2 | ||
|
|
ed543a205c | ||
|
|
de1c28bb16 | ||
|
|
60ff966d54 | ||
|
|
33684253c3 | ||
|
|
b4ca0cd5fb | ||
|
|
a49c1beb93 | ||
|
|
76852cfef3 | ||
|
|
3bd89caf36 | ||
|
|
eb2e7d959c | ||
|
|
a4b20356f4 | ||
|
|
2550b2d1de | ||
|
|
e94e1ab126 | ||
|
|
6307b4fa7d | ||
|
|
19e1a64a91 | ||
|
|
082e776e91 | ||
|
|
92e2f6db83 | ||
|
|
aa624f64c1 | ||
|
|
e1675eb371 | ||
|
|
cc2b6385a1 | ||
|
|
a64bed9bbb | ||
|
|
3272e1c0af | ||
|
|
69da22d517 | ||
|
|
06109f360f | ||
|
|
06a9ef1127 | ||
|
|
5d21bb158b | ||
|
|
bbde41f712 | ||
|
|
8c0994f35f | ||
|
|
3d390bc053 | ||
|
|
16f1be7f10 | ||
|
|
c74df3fab3 | ||
|
|
f2c902ee03 | ||
|
|
4e5e3c5e50 | ||
|
|
da697f218f | ||
|
|
4f47a80deb | ||
|
|
1334162a56 | ||
|
|
ab89517093 | ||
|
|
48a51d8470 | ||
|
|
ab11244f08 | ||
|
|
68ec3ebaa3 | ||
|
|
4f4ed2f242 | ||
|
|
a1c2be140d | ||
|
|
4664373bd0 | ||
|
|
5ab6b74f14 | ||
|
|
79cc70a62f | ||
|
|
4e6b5a1b0b | ||
|
|
21b144fff9 | ||
|
|
658940de38 | ||
|
|
ad487370f5 | ||
|
|
259f83b549 | ||
|
|
4a24860dcf | ||
|
|
116cdf8661 | ||
|
|
71550106d4 | ||
|
|
36011f0d0f | ||
|
|
c97d775370 | ||
|
|
97b29bb063 | ||
|
|
e9f0345a97 | ||
|
|
b7fff508a4 | ||
|
|
c6fa14ed52 | ||
|
|
05c8c5f114 | ||
|
|
1dbdb0e895 | ||
|
|
0feb09d0d6 | ||
|
|
fe5f8bbaa1 | ||
|
|
ecf3cec376 | ||
|
|
0a714db4d9 | ||
|
|
471fda8d0b | ||
|
|
dfe32c2f74 | ||
|
|
c51ba3ce6b | ||
|
|
fe025e8d23 | ||
|
|
0a43e4af8f | ||
|
|
7111cc09f3 | ||
|
|
777504b9c4 | ||
|
|
9d9985076a | ||
|
|
35d301b052 | ||
|
|
083b241c81 | ||
|
|
ae2957cf7e | ||
|
|
e172b3bf9c | ||
|
|
c9c6e8da2e | ||
|
|
528b8837e1 | ||
|
|
b9989555fd | ||
|
|
80a79f6d2d | ||
|
|
9a3f5579f1 | ||
|
|
12c721f528 | ||
|
|
9a9c3e8aba | ||
|
|
46a020108d | ||
|
|
b286d6cd2c | ||
|
|
443cf92242 | ||
|
|
eb3b984132 | ||
|
|
04395f98f9 | ||
|
|
0c10010f9f | ||
|
|
be8da83aca | ||
|
|
f5ab687226 | ||
|
|
742eead849 | ||
|
|
f98e871913 | ||
|
|
4b0c11978e | ||
|
|
067ad51b3f | ||
|
|
0ef5610a6c | ||
|
|
e29d0e977d | ||
|
|
00c77213fb | ||
|
|
c68b8f90b3 | ||
|
|
aa4f9ce9e9 | ||
|
|
d540f73778 | ||
|
|
d96fc23327 | ||
|
|
201d751a26 | ||
|
|
6308b2fd86 | ||
|
|
8f99e84438 | ||
|
|
e36b7755e9 | ||
|
|
7b4d869b31 | ||
|
|
4fb044d5f6 | ||
|
|
8873109b4f | ||
|
|
8ce7b310c5 | ||
|
|
2a953c5e2b | ||
|
|
5077ae41e5 | ||
|
|
8e1a27b8cc | ||
|
|
e7db1b005f | ||
|
|
72ce662e45 | ||
|
|
673cf0d4d8 | ||
|
|
1cae0534a0 | ||
|
|
6fb3083f63 | ||
|
|
68c17ca1bd | ||
|
|
82cf29407c | ||
|
|
abc6a40ccb | ||
|
|
ae74be5e9d | ||
|
|
73e68f3c7d | ||
|
|
4163efd2f2 | ||
|
|
d7ac6123ad | ||
|
|
73777d0bd4 | ||
|
|
bd260d1be8 | ||
|
|
cd1a52c7e4 | ||
|
|
0d2cb6acd1 | ||
|
|
ec6d1b9f72 | ||
|
|
d809fed52a | ||
|
|
327418a4b7 | ||
|
|
0e4786dfa8 | ||
|
|
c429262f02 | ||
|
|
f4fdcbcdcb | ||
|
|
b0806469d5 | ||
|
|
9f2b367081 | ||
|
|
a9873c86bb | ||
|
|
8ebd98ee92 | ||
|
|
e89b58a13f | ||
|
|
5a220078e9 | ||
|
|
2b94ec18b9 | ||
|
|
a15543a2ce | ||
|
|
238fb74028 | ||
|
|
6ff2b130d3 | ||
|
|
8c9bfca255 | ||
|
|
c2a2386df9 | ||
|
|
2f98e11562 | ||
|
|
073809cf5c | ||
|
|
e82690d256 | ||
|
|
23c31aec51 | ||
|
|
622ba2dcc7 | ||
|
|
0c1cf853fc | ||
|
|
79d1f07fa2 | ||
|
|
eba01f8d0e | ||
|
|
bd292481bd | ||
|
|
fcc53b260f | ||
|
|
7d61a1e73f | ||
|
|
b3cffe2d12 | ||
|
|
052c7d7698 | ||
|
|
c2713adebc | ||
|
|
51a984be3d | ||
|
|
2a662149f4 | ||
|
|
6007154484 | ||
|
|
22ce0aff37 | ||
|
|
37051d57ce | ||
|
|
4563f2f9a7 | ||
|
|
91201c094a | ||
|
|
a4a15ad243 |
@@ -12,32 +12,39 @@
|
||||
"upcoming": true
|
||||
},
|
||||
{
|
||||
"name": "3.4",
|
||||
"branchName": "3.4.x",
|
||||
"slug": "3.4",
|
||||
"name": "3.6",
|
||||
"branchName": "3.6.x",
|
||||
"slug": "3.6",
|
||||
"upcoming": true
|
||||
},
|
||||
{
|
||||
"name": "3.3",
|
||||
"branchName": "3.3.x",
|
||||
"slug": "3.3",
|
||||
"name": "3.5",
|
||||
"branchName": "3.5.x",
|
||||
"slug": "3.5",
|
||||
"current": true
|
||||
},
|
||||
{
|
||||
"name": "3.4",
|
||||
"slug": "3.4",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "3.3",
|
||||
"slug": "3.3",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "3.2",
|
||||
"branchName": "3.2.x",
|
||||
"slug": "3.2",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "3.1",
|
||||
"branchName": "3.1.x",
|
||||
"slug": "3.1",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "3.0",
|
||||
"branchName": "3.0.x",
|
||||
"slug": "3.0",
|
||||
"maintained": false
|
||||
},
|
||||
@@ -55,61 +62,51 @@
|
||||
},
|
||||
{
|
||||
"name": "2.19",
|
||||
"branchName": "2.19.x",
|
||||
"slug": "2.19",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.18",
|
||||
"branchName": "2.18.x",
|
||||
"slug": "2.18",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.17",
|
||||
"branchName": "2.17.x",
|
||||
"slug": "2.17",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.16",
|
||||
"branchName": "2.16.x",
|
||||
"slug": "2.16",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.15",
|
||||
"branchName": "2.15.x",
|
||||
"slug": "2.15",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.14",
|
||||
"branchName": "2.14.x",
|
||||
"slug": "2.14",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.13",
|
||||
"branchName": "2.13.x",
|
||||
"slug": "2.13",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.12",
|
||||
"branchName": "2.12.x",
|
||||
"slug": "2.12",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.11",
|
||||
"branchName": "2.11.x",
|
||||
"slug": "2.11",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.10",
|
||||
"branchName": "2.10.x",
|
||||
"slug": "2.10",
|
||||
"maintained": false
|
||||
}
|
||||
|
||||
2
.github/workflows/coding-standards.yml
vendored
2
.github/workflows/coding-standards.yml
vendored
@@ -24,4 +24,4 @@ on:
|
||||
|
||||
jobs:
|
||||
coding-standards:
|
||||
uses: "doctrine/.github/.github/workflows/coding-standards.yml@7.2.2"
|
||||
uses: "doctrine/.github/.github/workflows/coding-standards.yml@7.3.0"
|
||||
|
||||
16
.github/workflows/continuous-integration.yml
vendored
16
.github/workflows/continuous-integration.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: "Continuous Integration"
|
||||
name: "CI"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -43,17 +43,27 @@ jobs:
|
||||
- "pdo_sqlite"
|
||||
deps:
|
||||
- "highest"
|
||||
native_lazy:
|
||||
- "0"
|
||||
include:
|
||||
- php-version: "8.2"
|
||||
dbal-version: "4@dev"
|
||||
extension: "pdo_sqlite"
|
||||
native_lazy: "0"
|
||||
- php-version: "8.2"
|
||||
dbal-version: "4@dev"
|
||||
extension: "sqlite3"
|
||||
native_lazy: "0"
|
||||
- php-version: "8.1"
|
||||
dbal-version: "default"
|
||||
deps: "lowest"
|
||||
extension: "pdo_sqlite"
|
||||
native_lazy: "0"
|
||||
- php-version: "8.4"
|
||||
dbal-version: "default"
|
||||
deps: "highest"
|
||||
extension: "pdo_sqlite"
|
||||
native_lazy: "1"
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
@@ -83,16 +93,18 @@ jobs:
|
||||
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --coverage-clover=coverage-no-cache.xml"
|
||||
env:
|
||||
ENABLE_SECOND_LEVEL_CACHE: 0
|
||||
ENABLE_NATIVE_LAZY_OBJECTS: ${{ matrix.native_lazy }}
|
||||
|
||||
- name: "Run PHPUnit with Second Level Cache"
|
||||
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --exclude-group performance,non-cacheable,locking_functional --coverage-clover=coverage-cache.xml"
|
||||
env:
|
||||
ENABLE_SECOND_LEVEL_CACHE: 1
|
||||
ENABLE_NATIVE_LAZY_OBJECTS: ${{ matrix.native_lazy }}
|
||||
|
||||
- name: "Upload coverage file"
|
||||
uses: "actions/upload-artifact@v4"
|
||||
with:
|
||||
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-coverage"
|
||||
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-${{ matrix.native_lazy }}-coverage"
|
||||
path: "coverage*.xml"
|
||||
|
||||
|
||||
|
||||
2
.github/workflows/documentation.yml
vendored
2
.github/workflows/documentation.yml
vendored
@@ -17,4 +17,4 @@ on:
|
||||
jobs:
|
||||
documentation:
|
||||
name: "Documentation"
|
||||
uses: "doctrine/.github/.github/workflows/documentation.yml@7.2.2"
|
||||
uses: "doctrine/.github/.github/workflows/documentation.yml@7.3.0"
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@7.2.2"
|
||||
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@7.3.0"
|
||||
secrets:
|
||||
GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
|
||||
GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}
|
||||
|
||||
27
README.md
27
README.md
@@ -1,7 +1,7 @@
|
||||
| [4.0.x][4.0] | [3.4.x][3.4] | [3.3.x][3.3] | [2.21.x][2.21] | [2.20.x][2.20] |
|
||||
| [4.0.x][4.0] | [3.6.x][3.6] | [3.5.x][3.5] | [2.21.x][2.21] | [2.20.x][2.20] |
|
||||
|:------------------------------------------------------:|:------------------------------------------------------:|:------------------------------------------------------:|:--------------------------------------------------------:|:--------------------------------------------------------:|
|
||||
| [![Build status][4.0 image]][4.0] | [![Build status][3.4 image]][3.4] | [![Build status][3.3 image]][3.3] | [![Build status][2.21 image]][2.21] | [![Build status][2.20 image]][2.20] |
|
||||
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.4 coverage image]][3.4 coverage] | [![Coverage Status][3.3 coverage image]][3.3 coverage] | [![Coverage Status][2.21 coverage image]][2.21 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] |
|
||||
| [![Build status][4.0 image]][4.0 workflow] | [![Build status][3.6 image]][3.6 workflow] | [![Build status][3.5 image]][3.5 workflow] | [![Build status][2.21 image]][2.21 workflow] | [![Build status][2.20 image]][2.20 workflow] |
|
||||
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.6 coverage image]][3.6 coverage] | [![Coverage Status][3.5 coverage image]][3.5 coverage] | [![Coverage Status][2.21 coverage image]][2.21 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] |
|
||||
|
||||
Doctrine ORM is an object-relational mapper for PHP 8.1+ that provides transparent persistence
|
||||
for PHP objects. It sits on top of a powerful database abstraction layer (DBAL). One of its key features
|
||||
@@ -18,21 +18,26 @@ without requiring unnecessary code duplication.
|
||||
|
||||
[4.0 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=4.0.x
|
||||
[4.0]: https://github.com/doctrine/orm/tree/4.0.x
|
||||
[4.0 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A4.0.x
|
||||
[4.0 coverage image]: https://codecov.io/gh/doctrine/orm/branch/4.0.x/graph/badge.svg
|
||||
[4.0 coverage]: https://codecov.io/gh/doctrine/orm/branch/4.0.x
|
||||
[3.4 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.4.x
|
||||
[3.4]: https://github.com/doctrine/orm/tree/3.4.x
|
||||
[3.4 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.4.x/graph/badge.svg
|
||||
[3.4 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.4.x
|
||||
[3.3 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.3.x
|
||||
[3.3]: https://github.com/doctrine/orm/tree/3.3.x
|
||||
[3.3 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.3.x/graph/badge.svg
|
||||
[3.3 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.3.x
|
||||
[3.6 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.6.x
|
||||
[3.6]: https://github.com/doctrine/orm/tree/3.6.x
|
||||
[3.6 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A3.6.x
|
||||
[3.6 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.6.x/graph/badge.svg
|
||||
[3.6 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.6.x
|
||||
[3.5 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.5.x
|
||||
[3.5]: https://github.com/doctrine/orm/tree/3.5.x
|
||||
[3.5 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A3.5.x
|
||||
[3.5 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.5.x/graph/badge.svg
|
||||
[3.5 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.5.x
|
||||
[2.21 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.21.x
|
||||
[2.21]: https://github.com/doctrine/orm/tree/2.21.x
|
||||
[2.21 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A2.21.x
|
||||
[2.21 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.21.x/graph/badge.svg
|
||||
[2.21 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.21.x
|
||||
[2.20 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.20.x
|
||||
[2.20]: https://github.com/doctrine/orm/tree/2.20.x
|
||||
[2.20 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A2.20.x
|
||||
[2.20 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.20.x/graph/badge.svg
|
||||
[2.20 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.20.x
|
||||
|
||||
72
UPGRADE.md
72
UPGRADE.md
@@ -1,8 +1,76 @@
|
||||
# Upgrade to 3.5
|
||||
|
||||
## Deprecate not using native lazy objects on PHP 8.4+
|
||||
|
||||
Having native lazy objects disabled on PHP 8.4+ is deprecated and will not be
|
||||
possible in 4.0.
|
||||
|
||||
You can enable them through configuration:
|
||||
|
||||
```php
|
||||
$config->enableNativeLazyObjects(true);
|
||||
```
|
||||
|
||||
As a consequence, methods, parameters and commands related to userland lazy
|
||||
objects have been deprecated on PHP 8.4+:
|
||||
|
||||
- `Doctrine\ORM\Tools\Console\Command\GenerateProxiesCommand`
|
||||
- `Doctrine\ORM\Configuration::getAutoGenerateProxyClasses()`
|
||||
- `Doctrine\ORM\Configuration::getProxyDir()`
|
||||
- `Doctrine\ORM\Configuration::getProxyNamespace()`
|
||||
- `Doctrine\ORM\Configuration::setAutoGenerateProxyClasses()`
|
||||
- `Doctrine\ORM\Configuration::setProxyDir()`
|
||||
- `Doctrine\ORM\Configuration::setProxyNamespace()`
|
||||
- Passing more than one argument to `Doctrine\ORM\Proxy\ProxyFactory::__construct()`
|
||||
|
||||
Additionally, some methods of ORMSetup have been deprecated in favor of a new
|
||||
counterpart.
|
||||
|
||||
- `Doctrine\ORM\ORMSetup::createAttributeMetadataConfiguration()` is deprecated in favor of
|
||||
`Doctrine\ORM\ORMSetup::createAttributeMetadataConfig()`
|
||||
- `Doctrine\ORM\ORMSetup::createXMLMetadataConfiguration()` is deprecated in favor of
|
||||
`Doctrine\ORM\ORMSetup::createXMLMetadataConfig()`
|
||||
- `Doctrine\ORM\ORMSetup::createConfiguration()` is deprecated in favor of
|
||||
`Doctrine\ORM\ORMSetup::createConfig()`
|
||||
|
||||
## Deprecate methods for configuring no longer configurable features
|
||||
|
||||
Since 3.0, lazy ghosts are enabled unconditionally, and so is rejecting ID
|
||||
collisions in the identity map.
|
||||
|
||||
As a consequence, the following methods are deprecated and will be removed in 4.0:
|
||||
* `Doctrine\ORM\Configuration::setLazyGhostObjectEnabled()`
|
||||
* `Doctrine\ORM\Configuration::isLazyGhostObjectEnabled()`
|
||||
* `Doctrine\ORM\Configuration::setRejectIdCollisionInIdentityMap()`
|
||||
* `Doctrine\ORM\Configuration::isRejectIdCollisionInIdentityMapEnabled()`
|
||||
|
||||
# Upgrade to 3.4.1
|
||||
|
||||
## BC BREAK: You can no longer use the `.*` notation to get all fields of an entity in a DTO
|
||||
|
||||
This feature was introduced in 3.4.0, and introduces several issues, so we
|
||||
decide to remove it before it is used too widely.
|
||||
|
||||
# Upgrade to 3.4
|
||||
|
||||
## Discriminator Map class duplicates
|
||||
|
||||
Using the same class several times in a discriminator map is deprecated.
|
||||
In 4.0, this will be an error.
|
||||
|
||||
## `Doctrine\ORM\Mapping\ClassMetadata::$reflFields` deprecated
|
||||
|
||||
To better support property hooks and lazy proxies in the future, `$reflFields` had to
|
||||
be deprecated because we cannot use the PHP internal reflection API directly anymore.
|
||||
|
||||
The property was changed from an array to an object of type `LegacyReflectionFields`
|
||||
that implements `ArrayAccess`.
|
||||
|
||||
Use the new `Doctrine\ORM\Mapping\PropertyAccessors\PropertyAccessor` API and access
|
||||
through `Doctrine\ORM\Mapping\ClassMetadata::$propertyAccessors` instead.
|
||||
|
||||
Companion accessor methods are deprecated as well.
|
||||
|
||||
# Upgrade to 3.3
|
||||
|
||||
## Deprecate `DatabaseDriver`
|
||||
@@ -13,7 +81,7 @@ The class `Doctrine\ORM\Mapping\Driver\DatabaseDriver` is deprecated without rep
|
||||
|
||||
Output walkers should implement the new `\Doctrine\ORM\Query\OutputWalker` interface and create
|
||||
`Doctrine\ORM\Query\Exec\SqlFinalizer` instances instead of `Doctrine\ORM\Query\Exec\AbstractSqlExecutor`s.
|
||||
The output walker must not base its workings on the query `firstResult`/`maxResult` values, so that the
|
||||
The output walker must not base its workings on the query `firstResult`/`maxResult` values, so that the
|
||||
`SqlFinalizer` can be kept in the query cache and used regardless of the actual `firstResult`/`maxResult` values.
|
||||
Any operation dependent on `firstResult`/`maxResult` should take place within the `SqlFinalizer::createExecutor()`
|
||||
method. Details can be found at https://github.com/doctrine/orm/pull/11188.
|
||||
@@ -124,7 +192,7 @@ WARNING: This was relaxed in ORM 3.2 when partial was re-allowed for array-hydra
|
||||
`Doctrine\ORM\Query::HINT_FORCE_PARTIAL_LOAD` are removed.
|
||||
- `Doctrine\ORM\EntityManager*::getPartialReference()` is removed.
|
||||
|
||||
## BC BREAK: Enforce ArrayCollection Type on `\Doctrine\ORM\QueryBuilder::setParameters(ArrayCollection $parameters)`
|
||||
## BC BREAK: Enforce ArrayCollection Type on `\Doctrine\ORM\QueryBuilder::setParameters(ArrayCollection $parameters)`
|
||||
|
||||
The argument $parameters can no longer be a key=>value array. Only ArrayCollection types are allowed.
|
||||
|
||||
|
||||
3
docs/.gitignore
vendored
Normal file
3
docs/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
composer.lock
|
||||
vendor/
|
||||
build/
|
||||
24
docs/Makefile
Normal file
24
docs/Makefile
Normal file
@@ -0,0 +1,24 @@
|
||||
# Makefile for Doctrine ORM documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
DOCOPTS =
|
||||
BUILD = vendor/bin/guides
|
||||
BUILDDIR = build
|
||||
|
||||
# Internal variables.
|
||||
ALLGUIDESOPTS = $(DOCOPTS) en/
|
||||
|
||||
.PHONY: help clean html
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
|
||||
clean:
|
||||
-rm -rf ./$(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
$(BUILD) $(ALLGUIDESOPTS) --output=$(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
@@ -1,18 +1,22 @@
|
||||
# Doctrine ORM Documentation
|
||||
|
||||
The documentation is written in [ReStructured Text](https://docutils.sourceforge.io/rst.html).
|
||||
|
||||
## How to Generate:
|
||||
Using Ubuntu 14.04 LTS:
|
||||
|
||||
1. Run ./bin/install-dependencies.sh
|
||||
2. Run ./bin/generate-docs.sh
|
||||
In the `docs/` folder, run
|
||||
|
||||
It will generate the documentation into the build directory of the checkout.
|
||||
composer update
|
||||
|
||||
Then compile the documentation with:
|
||||
|
||||
## Theme issues
|
||||
make html
|
||||
|
||||
If you get a "Theme error", check if the `en/_theme` subdirectory is empty,
|
||||
in which case you will need to run:
|
||||
This will generate the documentation into the `build` subdirectory.
|
||||
|
||||
1. git submodule init
|
||||
2. git submodule update
|
||||
To browse the documentation, you need to run a webserver:
|
||||
|
||||
cd build/html
|
||||
php -S localhost:8000
|
||||
|
||||
Now the documentation is available at [http://localhost:8000](http://localhost:8000).
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
EXECPATH=`dirname $0`
|
||||
cd $EXECPATH
|
||||
cd ..
|
||||
|
||||
rm build -Rf
|
||||
sphinx-build en build
|
||||
|
||||
sphinx-build -b latex en build/pdf
|
||||
rubber --into build/pdf --pdf build/pdf/Doctrine2ORM.tex
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/bash
|
||||
sudo apt-get update && sudo apt-get install -y python2.7 python-sphinx python-pygments
|
||||
10
docs/composer.json
Normal file
10
docs/composer.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "doctrine/orm-docs",
|
||||
"description": "Documentation for the Object-Relational Mapper\"",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"phpdocumentor/guides-cli": "1.7.1",
|
||||
"phpdocumentor/filesystem": "1.7.1"
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Doctrine2ORM.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Doctrine2ORM.qhc"
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
|
||||
"run these through (pdf)latex."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
44
docs/en/cookbook/generated-columns.rst
Normal file
44
docs/en/cookbook/generated-columns.rst
Normal file
@@ -0,0 +1,44 @@
|
||||
Generated Columns
|
||||
=================
|
||||
|
||||
Generated columns, sometimes also called virtual columns, are populated by
|
||||
the database engine itself. They are a tool for performance optimization, to
|
||||
avoid calculating a value on each query.
|
||||
|
||||
You can define generated columns on entities and have Doctrine map the values
|
||||
to your entity.
|
||||
|
||||
Declaring a generated column
|
||||
----------------------------
|
||||
|
||||
There is no explicit mapping instruction for generated columns. Instead, you
|
||||
specify that the column should not be written to, and define a custom column
|
||||
definition.
|
||||
|
||||
.. literalinclude:: generated-columns/Person.php
|
||||
:language: php
|
||||
|
||||
* ``insertable``, ``updatable``: Setting these to false tells Doctrine to never
|
||||
write this column - writing to a generated column would result in an error
|
||||
from the database.
|
||||
* ``columnDefinition``: We specify the full DDL to create the column. To allow
|
||||
to use database specific features, this attribute does not use Doctrine Query
|
||||
Language but native SQL. Note that you need to reference columns by their
|
||||
database name (either explicitly set in the mapping or per the current
|
||||
:doc:`naming strategy <../reference/namingstrategy>`).
|
||||
Be aware that specifying a column definition makes the ``SchemaTool``
|
||||
completely ignore all other configuration for this column. See also
|
||||
:ref:`#[Column] <attrref_column>`
|
||||
* ``generated``: Specifying that this column is always generated tells Doctrine
|
||||
to update the field on the entity with the value from the database after
|
||||
every write operation.
|
||||
|
||||
Advanced example: Extracting a value from a JSON structure
|
||||
----------------------------------------------------------
|
||||
|
||||
Lets assume we have an entity that stores a blogpost as structured JSON.
|
||||
To avoid extracting all titles on the fly when listing the posts, we create a
|
||||
generated column with the field.
|
||||
|
||||
.. literalinclude:: generated-columns/Article.php
|
||||
:language: php
|
||||
33
docs/en/cookbook/generated-columns/Article.php
Normal file
33
docs/en/cookbook/generated-columns/Article.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
class Article
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private int $id;
|
||||
|
||||
/**
|
||||
* When working with Postgres, it is recommended to use the jsonb
|
||||
* format for better performance.
|
||||
*/
|
||||
#[ORM\Column(options: ['jsonb' => true])]
|
||||
private array $content;
|
||||
|
||||
/**
|
||||
* Because we specify NOT NULL, inserting will fail if the content does
|
||||
* not have a string in the title field.
|
||||
*/
|
||||
#[ORM\Column(
|
||||
insertable: false,
|
||||
updatable: false,
|
||||
columnDefinition: "VARCHAR(255) generated always as (content->>'title') stored NOT NULL",
|
||||
generated: 'ALWAYS',
|
||||
)]
|
||||
private string $title;
|
||||
}
|
||||
24
docs/en/cookbook/generated-columns/Person.php
Normal file
24
docs/en/cookbook/generated-columns/Person.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
class Person
|
||||
{
|
||||
#[ORM\Column(type: 'string')]
|
||||
private string $firstName;
|
||||
|
||||
#[ORM\Column(type: 'string', name: 'name')]
|
||||
private string $lastName;
|
||||
|
||||
#[ORM\Column(
|
||||
type: 'string',
|
||||
insertable: false,
|
||||
updatable: false,
|
||||
columnDefinition: "VARCHAR(255) GENERATED ALWAYS AS (concat(firstName, ' ', name) stored NOT NULL",
|
||||
generated: 'ALWAYS',
|
||||
)]
|
||||
private string $fullName;
|
||||
}
|
||||
@@ -46,17 +46,18 @@ entities:
|
||||
#[Entity]
|
||||
class Article
|
||||
{
|
||||
const STATUS_VISIBLE = 'visible';
|
||||
const STATUS_INVISIBLE = 'invisible';
|
||||
public const STATUS_VISIBLE = 'visible';
|
||||
public const STATUS_INVISIBLE = 'invisible';
|
||||
|
||||
#[Column(type: "string")]
|
||||
private $status;
|
||||
|
||||
public function setStatus($status)
|
||||
public function setStatus(string $status): void
|
||||
{
|
||||
if (!in_array($status, array(self::STATUS_VISIBLE, self::STATUS_INVISIBLE))) {
|
||||
if (!in_array($status, [self::STATUS_VISIBLE, self::STATUS_INVISIBLE], true)) {
|
||||
throw new \InvalidArgumentException("Invalid status");
|
||||
}
|
||||
|
||||
$this->status = $status;
|
||||
}
|
||||
}
|
||||
@@ -92,37 +93,33 @@ For example for the previous enum type:
|
||||
|
||||
class EnumVisibilityType extends Type
|
||||
{
|
||||
const ENUM_VISIBILITY = 'enumvisibility';
|
||||
const STATUS_VISIBLE = 'visible';
|
||||
const STATUS_INVISIBLE = 'invisible';
|
||||
private const ENUM_VISIBILITY = 'enumvisibility';
|
||||
private const STATUS_VISIBLE = 'visible';
|
||||
private const STATUS_INVISIBLE = 'invisible';
|
||||
|
||||
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
|
||||
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
|
||||
{
|
||||
return "ENUM('visible', 'invisible')";
|
||||
}
|
||||
|
||||
public function convertToPHPValue($value, AbstractPlatform $platform)
|
||||
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function convertToDatabaseValue($value, AbstractPlatform $platform)
|
||||
public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): string
|
||||
{
|
||||
if (!in_array($value, array(self::STATUS_VISIBLE, self::STATUS_INVISIBLE))) {
|
||||
if (!in_array($value, [self::STATUS_VISIBLE, self::STATUS_INVISIBLE], true)) {
|
||||
throw new \InvalidArgumentException("Invalid status");
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
public function getName(): string
|
||||
{
|
||||
return self::ENUM_VISIBILITY;
|
||||
}
|
||||
|
||||
public function requiresSQLCommentHint(AbstractPlatform $platform)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
You can register this type with ``Type::addType('enumvisibility', 'MyProject\DBAL\EnumVisibilityType');``.
|
||||
@@ -151,37 +148,33 @@ You can generalize this approach easily to create a base class for enums:
|
||||
abstract class EnumType extends Type
|
||||
{
|
||||
protected $name;
|
||||
protected $values = array();
|
||||
protected $values = [];
|
||||
|
||||
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
|
||||
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
|
||||
{
|
||||
$values = array_map(function($val) { return "'".$val."'"; }, $this->values);
|
||||
$values = array_map(fn($val) => "'".$val."'", $this->values);
|
||||
|
||||
return "ENUM(".implode(", ", $values).")";
|
||||
}
|
||||
|
||||
public function convertToPHPValue($value, AbstractPlatform $platform)
|
||||
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function convertToDatabaseValue($value, AbstractPlatform $platform)
|
||||
public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): mixed
|
||||
{
|
||||
if (!in_array($value, $this->values)) {
|
||||
if (!in_array($value, $this->values, true)) {
|
||||
throw new \InvalidArgumentException("Invalid '".$this->name."' value.");
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function requiresSQLCommentHint(AbstractPlatform $platform)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
With this base class you can define an enum as easily as:
|
||||
@@ -194,5 +187,5 @@ With this base class you can define an enum as easily as:
|
||||
class EnumVisibilityType extends EnumType
|
||||
{
|
||||
protected $name = 'enumvisibility';
|
||||
protected $values = array('visible', 'invisible');
|
||||
protected $values = ['visible', 'invisible'];
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ Cookbook
|
||||
|
||||
* **Patterns**:
|
||||
:doc:`Aggregate Fields <cookbook/aggregate-fields>` \|
|
||||
:doc:`Generated/Virtual Columns <cookbook/generated-columns>` \|
|
||||
:doc:`Decorator Pattern <cookbook/decorator-pattern>` \|
|
||||
:doc:`Strategy Pattern <cookbook/strategy-cookbook-introduction>`
|
||||
|
||||
@@ -121,4 +122,5 @@ Cookbook
|
||||
|
||||
* **Custom Datatypes**
|
||||
:doc:`MySQL Enums <cookbook/mysql-enums>`
|
||||
:doc:`Custom Mapping Types <cookbook/custom-mapping-types>`
|
||||
:doc:`Advanced Field Value Conversion <cookbook/advanced-field-value-conversion-using-custom-mapping-types>`
|
||||
|
||||
@@ -19,7 +19,7 @@ steps of configuration.
|
||||
|
||||
// ...
|
||||
|
||||
if ($applicationMode == "development") {
|
||||
if ($applicationMode === "development") {
|
||||
$queryCache = new ArrayAdapter();
|
||||
$metadataCache = new ArrayAdapter();
|
||||
} else {
|
||||
@@ -32,13 +32,18 @@ steps of configuration.
|
||||
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true);
|
||||
$config->setMetadataDriverImpl($driverImpl);
|
||||
$config->setQueryCache($queryCache);
|
||||
$config->setProxyDir('/path/to/myproject/lib/MyProject/Proxies');
|
||||
$config->setProxyNamespace('MyProject\Proxies');
|
||||
|
||||
if ($applicationMode == "development") {
|
||||
$config->setAutoGenerateProxyClasses(true);
|
||||
if (PHP_VERSION_ID > 80400) {
|
||||
$config->enableNativeLazyObjects(true);
|
||||
} else {
|
||||
$config->setAutoGenerateProxyClasses(false);
|
||||
$config->setProxyDir('/path/to/myproject/lib/MyProject/Proxies');
|
||||
$config->setProxyNamespace('MyProject\Proxies');
|
||||
|
||||
if ($applicationMode === "development") {
|
||||
$config->setAutoGenerateProxyClasses(true);
|
||||
} else {
|
||||
$config->setAutoGenerateProxyClasses(false);
|
||||
}
|
||||
}
|
||||
|
||||
$connection = DriverManager::getConnection([
|
||||
@@ -71,8 +76,25 @@ Configuration Options
|
||||
The following sections describe all the configuration options
|
||||
available on a ``Doctrine\ORM\Configuration`` instance.
|
||||
|
||||
Proxy Directory (**REQUIRED**)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Native Lazy Objects (**OPTIONAL**)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
With PHP 8.4 we recommend that you use native lazy objects instead of
|
||||
the code generation approach using the ``symfony/var-exporter`` Ghost trait.
|
||||
|
||||
With Doctrine 4, the minimal requirement will become PHP 8.4 and native lazy objects
|
||||
will become the only approach to lazy loading.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$config->enableNativeLazyObjects(true);
|
||||
|
||||
Proxy Directory
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Required except if you use native lazy objects with PHP 8.4.
|
||||
This setting will be removed in the future.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
@@ -85,8 +107,11 @@ classes. For a detailed explanation on proxy classes and how they
|
||||
are used in Doctrine, refer to the "Proxy Objects" section further
|
||||
down.
|
||||
|
||||
Proxy Namespace (**REQUIRED**)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Proxy Namespace
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Required except if you use native lazy objects with PHP 8.4.
|
||||
This setting will be removed in the future.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
@@ -200,6 +225,9 @@ deprecated ``Doctrine\DBAL\Logging\SQLLogger`` interface.
|
||||
Auto-generating Proxy Classes (**OPTIONAL**)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This setting is not required if you use native lazy objects with PHP 8.4
|
||||
and will be removed in the future.
|
||||
|
||||
Proxy classes can either be generated manually through the Doctrine
|
||||
Console or automatically at runtime by Doctrine. The configuration
|
||||
option that controls this behavior is:
|
||||
|
||||
@@ -175,6 +175,10 @@ Optional parameters:
|
||||
- **unique**: Boolean value to determine if the value of the column
|
||||
should be unique across all rows of the underlying entities table.
|
||||
|
||||
- **index**: Boolean value to generate an index for this column.
|
||||
For more advanced usages, take a look at :ref:`#[Index] <attrref_index>`.
|
||||
If not specified, default value is ``false``.
|
||||
|
||||
- **nullable**: Determines if NULL values allowed for this column.
|
||||
If not specified, default value is ``false``.
|
||||
|
||||
@@ -214,12 +218,15 @@ Optional parameters:
|
||||
- ``check``: Adds a check constraint type to the column (might not
|
||||
be supported by all vendors).
|
||||
|
||||
- **columnDefinition**: DDL SQL snippet that starts after the column
|
||||
- **columnDefinition**: Specify the DDL SQL snippet that starts after the column
|
||||
name and specifies the complete (non-portable!) column definition.
|
||||
This attribute allows to make use of advanced RMDBS features.
|
||||
However you should make careful use of this feature and the
|
||||
consequences. ``SchemaTool`` will not detect changes on the column correctly
|
||||
anymore if you use ``columnDefinition``.
|
||||
However, as this needs to be specified in the DDL native to the database,
|
||||
the resulting schema changes are no longer portable. If you specify a
|
||||
``columnDefinition``, the ``SchemaTool`` ignores all other attributes
|
||||
that are normally used to build the definition DDL. Changes to the
|
||||
``columnDefinition`` are not detected, you will need to manually create a
|
||||
migration to apply changes.
|
||||
|
||||
Additionally you should remember that the ``type``
|
||||
attribute still handles the conversion between PHP and Database
|
||||
@@ -242,6 +249,9 @@ Examples:
|
||||
#[Column(type: "string", length: 32, unique: true, nullable: false)]
|
||||
protected $username;
|
||||
|
||||
#[Column(type: "string", index: true)]
|
||||
protected $firstName;
|
||||
|
||||
#[Column(type: "string", columnDefinition: "CHAR(2) NOT NULL")]
|
||||
protected $country;
|
||||
|
||||
@@ -262,10 +272,11 @@ Examples:
|
||||
)]
|
||||
protected $loginCount;
|
||||
|
||||
// MySQL example: full_name char(41) GENERATED ALWAYS AS (concat(firstname,' ',lastname)),
|
||||
// columnDefinition is raw SQL, not DQL. This example works for MySQL:
|
||||
#[Column(
|
||||
type: "string",
|
||||
name: "user_fullname",
|
||||
columnDefinition: "VARCHAR(255) GENERATED ALWAYS AS (concat(firstname,' ',lastname))",
|
||||
insertable: false,
|
||||
updatable: false
|
||||
)]
|
||||
@@ -366,7 +377,7 @@ Optional parameters:
|
||||
|
||||
- **type**: By default this is string.
|
||||
- **length**: By default this is 255.
|
||||
- **columnDefinition**: By default this is null the definition according to the type will be used. This option allows to override it.
|
||||
- **columnDefinition**: Allows to override how the column is generated. See the "columnDefinition" attribute on :ref:`#[Column] <attrref_column>`
|
||||
- **enumType**: By default this is `null`. Allows to map discriminatorColumn value to PHP enum
|
||||
- **options**: See "options" attribute on :ref:`#[Column] <attrref_column>`.
|
||||
|
||||
@@ -672,13 +683,16 @@ Optional parameters:
|
||||
- **unique**: Determines whether this relation is exclusive between the
|
||||
affected entities and should be enforced as such on the database
|
||||
constraint level. Defaults to false.
|
||||
- **deferrable**: Determines whether this relation constraint can be deferred. Defaults to false.
|
||||
- **nullable**: Determine whether the related entity is required, or if
|
||||
null is an allowed state for the relation. Defaults to true.
|
||||
- **onDelete**: Cascade Action (Database-level)
|
||||
- **columnDefinition**: DDL SQL snippet that starts after the column
|
||||
name and specifies the complete (non-portable!) column definition.
|
||||
This attribute enables the use of advanced RMDBS features. Using
|
||||
this attribute on ``#[JoinColumn]`` is necessary if you need slightly
|
||||
This attribute enables the use of advanced RMDBS features. Note that you
|
||||
need to reference columns by their database name (either explicitly set in
|
||||
the mapping or per the current :doc:`naming strategy <namingstrategy>`).
|
||||
Using this attribute on ``#[JoinColumn]`` is necessary if you need
|
||||
different column definitions for joining columns, for example
|
||||
regarding NULL/NOT NULL defaults. However by default a
|
||||
"columnDefinition" attribute on :ref:`#[Column] <attrref_column>` also sets
|
||||
@@ -1133,7 +1147,7 @@ Marker attribute that defines a specified column as version attribute used in
|
||||
an :ref:`optimistic locking <transactions-and-concurrency_optimistic-locking>`
|
||||
scenario. It only works on :ref:`#[Column] <attrref_column>` attributes that have
|
||||
the type ``integer`` or ``datetime``. Setting ``#[Version]`` on a property with
|
||||
:ref:`#[Id <attrref_id>` is not supported.
|
||||
:ref:`#[Id] <attrref_id>` is not supported.
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
@@ -214,6 +214,8 @@ These are the "automatic" mapping rules:
|
||||
| Any other type | ``Types::STRING`` |
|
||||
+-----------------------+-------------------------------+
|
||||
|
||||
.. versionadded:: 2.11
|
||||
|
||||
As of version 2.11 Doctrine can also automatically map typed properties using a
|
||||
PHP 8.1 enum to set the right ``type`` and ``enumType``.
|
||||
|
||||
@@ -224,6 +226,70 @@ and a custom ``Doctrine\ORM\Mapping\TypedFieldMapper`` implementation.
|
||||
|
||||
:doc:`Read more about TypedFieldMapper <typedfieldmapper>`.
|
||||
|
||||
Property Hooks
|
||||
--------------
|
||||
|
||||
.. versionadded:: 3.4
|
||||
|
||||
Doctrine supports mapping hooked properties as long as they have a backed property
|
||||
and are not virtual.
|
||||
|
||||
|
||||
.. configuration-block::
|
||||
|
||||
.. code-block:: attribute
|
||||
|
||||
<?php
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
|
||||
#[Entity]
|
||||
class Message
|
||||
{
|
||||
#[Column(type: Types::INTEGER)]
|
||||
private $id;
|
||||
#[Column(type: Types::STRING)]
|
||||
public string $language = 'de' {
|
||||
// Override the "read" action with arbitrary logic.
|
||||
get => strtoupper($this->language);
|
||||
|
||||
// Override the "write" action with arbitrary logic.
|
||||
set {
|
||||
$this->language = strtolower($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<doctrine-mapping>
|
||||
<entity name="Message">
|
||||
<field name="id" type="integer" />
|
||||
<field name="language" />
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
|
||||
If you attempt to map a virtual property with ``#[Column]`` an exception will be thrown.
|
||||
|
||||
Some caveats apply to the use of property hooks, as they behave differently when accessing the property through
|
||||
the entity or directly through DQL/EntityRepository. Because the property hook can modify the value of the property in a way
|
||||
that value and raw value are different, you have to use the raw value representation when querying for the property.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$queryBuilder = $entityManager->createQueryBuilder();
|
||||
$queryBuilder->select('m')
|
||||
->from(Message::class, 'm')
|
||||
->where('m.language = :language')
|
||||
->setParameter('language', 'de'); // Use lower case here for raw value representation
|
||||
|
||||
$query = $queryBuilder->getQuery();
|
||||
$result = $query->getResult();
|
||||
|
||||
$messageRepository = $entityManager->getRepository(Message::class);
|
||||
$deMessages = $messageRepository->findBy(['language' => 'de']); // Use lower case here for raw value representation
|
||||
|
||||
.. _reference-mapping-types:
|
||||
|
||||
Doctrine Mapping Types
|
||||
|
||||
@@ -56,7 +56,8 @@ access point to ORM functionality provided by Doctrine.
|
||||
'dbname' => 'foo',
|
||||
];
|
||||
|
||||
$config = ORMSetup::createAttributeMetadataConfiguration($paths, $isDevMode);
|
||||
$config = ORMSetup::createAttributeMetadataConfig($paths, $isDevMode);
|
||||
// on PHP < 8.4, use ORMSetup::createAttributeMetadataConfiguration() instead
|
||||
$connection = DriverManager::getConnection($dbParams, $config);
|
||||
$entityManager = new EntityManager($connection, $config);
|
||||
|
||||
@@ -66,7 +67,8 @@ Or if you prefer XML:
|
||||
|
||||
<?php
|
||||
$paths = ['/path/to/xml-mappings'];
|
||||
$config = ORMSetup::createXMLMetadataConfiguration($paths, $isDevMode);
|
||||
$config = ORMSetup::createXMLMetadataConfig($paths, $isDevMode);
|
||||
// on PHP < 8.4, use ORMSetup::createXMLMetadataConfiguration() instead
|
||||
$connection = DriverManager::getConnection($dbParams, $config);
|
||||
$entityManager = new EntityManager($connection, $config);
|
||||
|
||||
|
||||
@@ -588,7 +588,7 @@ And then use the ``NEW`` DQL keyword :
|
||||
$query = $em->createQuery('SELECT NEW CustomerDTO(c.name, e.email, a.city, SUM(o.value)) FROM Customer c JOIN c.email e JOIN c.address a JOIN c.orders o GROUP BY c');
|
||||
$users = $query->getResult(); // array of CustomerDTO
|
||||
|
||||
You can also nest several DTO :
|
||||
You can also nest several DTO :
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
@@ -674,6 +674,16 @@ The ``NAMED`` keyword must precede all DTO you want to instantiate :
|
||||
If two arguments have the same name, a ``DuplicateFieldException`` is thrown.
|
||||
If a field cannot be matched with a property name, a ``NoMatchingPropertyException`` is thrown. This typically happens when using functions without aliasing them.
|
||||
|
||||
You can hydrate an entity nested in a DTO :
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT NEW CustomerDTO(c.name, a AS address) FROM Customer c JOIN c.address a');
|
||||
$users = $query->getResult(); // array of CustomerDTO
|
||||
|
||||
// CustomerDTO => {name : 'DOE', email: null, address : {city: 'New York', zip: '10011', address: 'Abbey Road'}
|
||||
|
||||
Using INDEX BY
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
@@ -1697,12 +1707,13 @@ Select Expressions
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
SelectExpression ::= (IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression) [["AS"] ["HIDDEN"] AliasResultVariable]
|
||||
SimpleSelectExpression ::= (StateFieldPathExpression | IdentificationVariable | FunctionDeclaration | AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] AliasResultVariable]
|
||||
PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
|
||||
PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
|
||||
NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
|
||||
NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]
|
||||
SelectExpression ::= (IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression) [["AS"] ["HIDDEN"] AliasResultVariable]
|
||||
SimpleSelectExpression ::= (StateFieldPathExpression | IdentificationVariable | FunctionDeclaration | AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] AliasResultVariable]
|
||||
PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
|
||||
PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
|
||||
NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
|
||||
NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression | EntityAsDtoArgumentExpression) ["AS" AliasResultVariable]
|
||||
EntityAsDtoArgumentExpression ::= IdentificationVariable
|
||||
|
||||
Conditional Expressions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -209,6 +209,7 @@ Field & Association Getters
|
||||
|
||||
- ``isUniqueField($fieldName)``
|
||||
- ``isNullable($fieldName)``
|
||||
- ``isIndexed($fieldName)``
|
||||
- ``getColumnName($fieldName)``
|
||||
- ``getFieldMapping($fieldName)``
|
||||
- ``getAssociationMapping($fieldName)``
|
||||
|
||||
@@ -84,7 +84,7 @@ The following Commands are currently available:
|
||||
- ``orm:clear-cache:result`` Clear result cache of the various
|
||||
cache drivers.
|
||||
- ``orm:generate-proxies`` Generates proxy classes for entity
|
||||
classes.
|
||||
classes. Deprecated in favor of using native lazy objects.
|
||||
- ``orm:run-dql`` Executes arbitrary DQL directly from the command
|
||||
line.
|
||||
- ``orm:schema-tool:create`` Processes the schema and either
|
||||
|
||||
@@ -112,7 +112,6 @@ of several common elements:
|
||||
|
||||
<indexes>
|
||||
<index name="name_idx" columns="name"/>
|
||||
<index columns="user_email"/>
|
||||
</indexes>
|
||||
|
||||
<unique-constraints>
|
||||
@@ -131,7 +130,7 @@ of several common elements:
|
||||
</id>
|
||||
|
||||
<field name="name" column="name" type="string" length="50" nullable="true" unique="true" />
|
||||
<field name="email" column="user_email" type="string" column-definition="CHAR(32) NOT NULL" />
|
||||
<field name="email" column="user_email" type="string" index="true" column-definition="CHAR(32) NOT NULL" />
|
||||
|
||||
<one-to-one field="address" target-entity="Address" inversed-by="user">
|
||||
<cascade><cascade-remove /></cascade>
|
||||
@@ -255,6 +254,8 @@ Optional attributes:
|
||||
only.
|
||||
- unique - Should this field contain a unique value across the
|
||||
table? Defaults to false.
|
||||
- index - Should an index be created for this column? Defaults to
|
||||
false.
|
||||
- nullable - Should this field allow NULL as a value? Defaults to
|
||||
false.
|
||||
- insertable - Should this field be inserted? Defaults to true.
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
cookbook/decorator-pattern
|
||||
cookbook/dql-custom-walkers
|
||||
cookbook/dql-user-defined-functions
|
||||
cookbook/generated-columns
|
||||
cookbook/implementing-arrayaccess-for-domain-objects
|
||||
cookbook/resolve-target-entity-listener
|
||||
cookbook/sql-table-prefixes
|
||||
|
||||
@@ -138,12 +138,12 @@ step:
|
||||
require_once "vendor/autoload.php";
|
||||
|
||||
// Create a simple "default" Doctrine ORM configuration for Attributes
|
||||
$config = ORMSetup::createAttributeMetadataConfiguration(
|
||||
$config = ORMSetup::createAttributeMetadataConfig( // on PHP < 8.4, use ORMSetup::createAttributeMetadataConfiguration()
|
||||
paths: [__DIR__ . '/src'],
|
||||
isDevMode: true,
|
||||
);
|
||||
// or if you prefer XML
|
||||
// $config = ORMSetup::createXMLMetadataConfiguration(
|
||||
// $config = ORMSetup::createXMLMetadataConfig( // on PHP < 8.4, use ORMSetup::createXMLMetadataConfiguration()
|
||||
// paths: [__DIR__ . '/config/xml'],
|
||||
// isDevMode: true,
|
||||
//);
|
||||
|
||||
@@ -243,6 +243,7 @@
|
||||
<xs:attribute name="length" type="xs:NMTOKEN" />
|
||||
<xs:attribute name="unique" type="xs:boolean" default="false" />
|
||||
<xs:attribute name="nullable" type="xs:boolean" default="false" />
|
||||
<xs:attribute name="index" type="xs:boolean" default="false" />
|
||||
<xs:attribute name="insertable" type="xs:boolean" default="true" />
|
||||
<xs:attribute name="updatable" type="xs:boolean" default="true" />
|
||||
<xs:attribute name="generated" type="orm:generated-type" default="NEVER" />
|
||||
|
||||
@@ -583,7 +583,7 @@ parameters:
|
||||
path: src/EntityManager.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\EntityManager\:\:getReference\(\) should return \(T of object\)\|null but returns Doctrine\\ORM\\Proxy\\InternalProxy\.$#'
|
||||
message: '#^Method Doctrine\\ORM\\EntityManager\:\:getReference\(\) should return \(T of object\)\|null but returns object\.$#'
|
||||
identifier: return.type
|
||||
count: 1
|
||||
path: src/EntityManager.php
|
||||
@@ -984,12 +984,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Mapping/ClassMetadata.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$class of method Doctrine\\Persistence\\Mapping\\ReflectionService\:\:getAccessibleProperty\(\) expects class\-string, string given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/Mapping/ClassMetadata.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$mapping of method Doctrine\\ORM\\Mapping\\ClassMetadata\<T of object\>\:\:validateAndCompleteTypedAssociationMapping\(\) expects array\{type\: 1\|2\|4\|8, fieldName\: string, targetEntity\?\: class\-string\}, non\-empty\-array\<string, mixed\> given\.$#'
|
||||
identifier: argument.type
|
||||
@@ -1032,18 +1026,6 @@ parameters:
|
||||
count: 2
|
||||
path: src/Mapping/ClassMetadata.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#2 \$class of method Doctrine\\ORM\\Mapping\\ClassMetadata\<T of object\>\:\:getAccessibleProperty\(\) expects class\-string, string given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/Mapping/ClassMetadata.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#3 \$embeddedClass of class Doctrine\\ORM\\Mapping\\ReflectionEmbeddedProperty constructor expects class\-string, string given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/Mapping/ClassMetadata.php
|
||||
|
||||
-
|
||||
message: '#^Property Doctrine\\ORM\\Mapping\\ClassMetadata\:\:\$customRepositoryClassName with generic class Doctrine\\ORM\\EntityRepository does not specify its types\: T$#'
|
||||
identifier: missingType.generics
|
||||
@@ -1351,51 +1333,45 @@ parameters:
|
||||
path: src/Mapping/Driver/AttributeDriver.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\:\:getReferencedColumnNames\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
message: '#^Call to function method_exists\(\) with ''Doctrine\\\\DBAL\\\\Schema\\\\ForeignKeyConstraint'' and ''getReferencedColumn…'' will always evaluate to true\.$#'
|
||||
identifier: function.alreadyNarrowedType
|
||||
count: 1
|
||||
path: src/Mapping/Driver/DatabaseDriver.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\:\:getReferencedTableName\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
message: '#^Call to function method_exists\(\) with ''Doctrine\\\\DBAL\\\\Schema\\\\ForeignKeyConstraint'' and ''getReferencedTableN…'' will always evaluate to true\.$#'
|
||||
identifier: function.alreadyNarrowedType
|
||||
count: 1
|
||||
path: src/Mapping/Driver/DatabaseDriver.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\:\:getReferencingColumnNames\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
message: '#^Call to function method_exists\(\) with ''Doctrine\\\\DBAL\\\\Schema\\\\ForeignKeyConstraint'' and ''getReferencingColum…'' will always evaluate to true\.$#'
|
||||
identifier: function.alreadyNarrowedType
|
||||
count: 1
|
||||
path: src/Mapping/Driver/DatabaseDriver.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Doctrine\\DBAL\\Schema\\Index\:\:getIndexedColumns\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
message: '#^Call to function method_exists\(\) with ''Doctrine\\\\DBAL\\\\Schema\\\\Index'' and ''getIndexedColumns'' will always evaluate to true\.$#'
|
||||
identifier: function.alreadyNarrowedType
|
||||
count: 1
|
||||
path: src/Mapping/Driver/DatabaseDriver.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Doctrine\\DBAL\\Schema\\Table\:\:getPrimaryKeyConstraint\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
message: '#^Call to function method_exists\(\) with ''Doctrine\\\\DBAL\\\\Schema\\\\Table'' and ''getPrimaryKeyConstr…'' will always evaluate to true\.$#'
|
||||
identifier: function.alreadyNarrowedType
|
||||
count: 1
|
||||
path: src/Mapping/Driver/DatabaseDriver.php
|
||||
|
||||
-
|
||||
message: '#^Call to method getColumnName\(\) on an unknown class Doctrine\\DBAL\\Schema\\Index\\IndexedColumn\.$#'
|
||||
identifier: class.notFound
|
||||
message: '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Schema\\Index and ''getType'' will always evaluate to true\.$#'
|
||||
identifier: function.alreadyNarrowedType
|
||||
count: 1
|
||||
path: src/Mapping/Driver/DatabaseDriver.php
|
||||
|
||||
-
|
||||
message: '#^Call to method getColumnNames\(\) on an unknown class Doctrine\\DBAL\\Schema\\PrimaryKeyConstraint\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: src/Mapping/Driver/DatabaseDriver.php
|
||||
|
||||
-
|
||||
message: '#^Call to method toString\(\) on an unknown class Doctrine\\DBAL\\Schema\\Name\\UnqualifiedName\.$#'
|
||||
identifier: class.notFound
|
||||
count: 5
|
||||
message: '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Schema\\Table and ''getPrimaryKeyConstr…'' will always evaluate to true\.$#'
|
||||
identifier: function.alreadyNarrowedType
|
||||
count: 2
|
||||
path: src/Mapping/Driver/DatabaseDriver.php
|
||||
|
||||
-
|
||||
@@ -1404,18 +1380,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Mapping/Driver/DatabaseDriver.php
|
||||
|
||||
-
|
||||
message: '#^Class Doctrine\\DBAL\\Schema\\Index\\IndexType not found\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: src/Mapping/Driver/DatabaseDriver.php
|
||||
|
||||
-
|
||||
message: '#^Class Doctrine\\DBAL\\Schema\\PrimaryKeyConstraint not found\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: src/Mapping/Driver/DatabaseDriver.php
|
||||
|
||||
-
|
||||
message: '#^Instanceof between Doctrine\\ORM\\Mapping\\ClassMetadata\<T of object\> and Doctrine\\ORM\\Mapping\\ClassMetadata will always evaluate to true\.$#'
|
||||
identifier: instanceof.alwaysTrue
|
||||
@@ -1452,30 +1416,12 @@ parameters:
|
||||
count: 2
|
||||
path: src/Mapping/Driver/DatabaseDriver.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$array of function sort contains unresolvable type\.$#'
|
||||
identifier: argument.unresolvableType
|
||||
count: 1
|
||||
path: src/Mapping/Driver/DatabaseDriver.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#2 \$columnName of method Doctrine\\ORM\\Mapping\\Driver\\DatabaseDriver\:\:getFieldNameForColumn\(\) expects string, string\|false given\.$#'
|
||||
identifier: argument.type
|
||||
count: 4
|
||||
path: src/Mapping/Driver/DatabaseDriver.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \$indexedColumn of anonymous function has invalid type Doctrine\\DBAL\\Schema\\Index\\IndexedColumn\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: src/Mapping/Driver/DatabaseDriver.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \$name of anonymous function has invalid type Doctrine\\DBAL\\Schema\\Name\\UnqualifiedName\.$#'
|
||||
identifier: class.notFound
|
||||
count: 5
|
||||
path: src/Mapping/Driver/DatabaseDriver.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\Mapping\\Driver\\SimplifiedXmlDriver\:\:__construct\(\) has parameter \$fileExtension with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
@@ -1578,12 +1524,36 @@ parameters:
|
||||
count: 1
|
||||
path: src/Mapping/JoinTableMapping.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\Mapping\\LegacyReflectionFields\:\:__construct\(\) has parameter \$classMetadata with generic class Doctrine\\ORM\\Mapping\\ClassMetadata but does not specify its types\: T$#'
|
||||
identifier: missingType.generics
|
||||
count: 1
|
||||
path: src/Mapping/LegacyReflectionFields.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$class of method Doctrine\\Persistence\\Mapping\\ReflectionService\:\:getAccessibleProperty\(\) expects class\-string, string given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/Mapping/LegacyReflectionFields.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\Mapping\\MappedSuperclass\:\:__construct\(\) has parameter \$repositoryClass with generic class Doctrine\\ORM\\EntityRepository but does not specify its types\: T$#'
|
||||
identifier: missingType.generics
|
||||
count: 1
|
||||
path: src/Mapping/MappedSuperclass.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\Mapping\\PropertyAccessors\\EnumPropertyAccessor\:\:toEnum\(\) should return array\<BackedEnum\>\|BackedEnum but returns array\<BackedEnum\|int\|string\>\.$#'
|
||||
identifier: return.type
|
||||
count: 1
|
||||
path: src/Mapping/PropertyAccessors/EnumPropertyAccessor.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$callback of function array_map expects \(callable\(BackedEnum\|int\|string\)\: mixed\)\|null, array\{class\-string\<BackedEnum\>, ''from''\} given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/Mapping/PropertyAccessors/EnumPropertyAccessor.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\Mapping\\QuoteStrategy\:\:getColumnAlias\(\) has parameter \$class with generic class Doctrine\\ORM\\Mapping\\ClassMetadata but does not specify its types\: T$#'
|
||||
identifier: missingType.generics
|
||||
@@ -2016,6 +1986,12 @@ parameters:
|
||||
count: 1
|
||||
path: src/Persisters/Collection/OneToManyPersister.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$columns of method Doctrine\\DBAL\\Platforms\\AbstractPlatform\:\:getColumnDeclarationListSQL\(\) expects list\<array\{name\: string, type\: Doctrine\\DBAL\\Types\\Type, default\: mixed, notnull\?\: bool, autoincrement\: bool, columnDefinition\: non\-empty\-string\|null, comment\: string, charset\?\: non\-empty\-string\|null, \.\.\.\}\>, array\<string, array\{name\: string, notnull\: true, type\: Doctrine\\DBAL\\Types\\Type\}\> given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/Persisters/Collection/OneToManyPersister.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\AbstractEntityInheritancePersister\:\:getSelectColumnSQL\(\) has parameter \$class with generic class Doctrine\\ORM\\Mapping\\ClassMetadata but does not specify its types\: T$#'
|
||||
identifier: missingType.generics
|
||||
@@ -2394,12 +2370,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Proxy/ProxyFactory.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\Proxy\\ProxyFactory\:\:getProxy\(\) return type with generic interface Doctrine\\ORM\\Proxy\\InternalProxy does not specify its types\: T$#'
|
||||
identifier: missingType.generics
|
||||
count: 1
|
||||
path: src/Proxy/ProxyFactory.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\Proxy\\ProxyFactory\:\:loadProxyClass\(\) has parameter \$class with generic interface Doctrine\\Persistence\\Mapping\\ClassMetadata but does not specify its types\: T$#'
|
||||
identifier: missingType.generics
|
||||
@@ -2610,6 +2580,12 @@ parameters:
|
||||
count: 1
|
||||
path: src/Query/Exec/MultiTableDeleteExecutor.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$columns of method Doctrine\\DBAL\\Platforms\\AbstractPlatform\:\:getColumnDeclarationListSQL\(\) expects list\<array\{name\: string, type\: Doctrine\\DBAL\\Types\\Type, default\: mixed, notnull\?\: bool, autoincrement\: bool, columnDefinition\: non\-empty\-string\|null, comment\: string, charset\?\: non\-empty\-string\|null, \.\.\.\}\>, array\<string, array\{name\: string, notnull\: true, type\: Doctrine\\DBAL\\Types\\Type\}\> given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/Query/Exec/MultiTableDeleteExecutor.php
|
||||
|
||||
-
|
||||
message: '#^Argument of an invalid type list\<string\>\|string supplied for foreach, only iterables are supported\.$#'
|
||||
identifier: foreach.nonIterable
|
||||
@@ -2622,6 +2598,12 @@ parameters:
|
||||
count: 1
|
||||
path: src/Query/Exec/MultiTableUpdateExecutor.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$columns of method Doctrine\\DBAL\\Platforms\\AbstractPlatform\:\:getColumnDeclarationListSQL\(\) expects list\<array\{name\: string, type\: Doctrine\\DBAL\\Types\\Type, default\: mixed, notnull\?\: bool, autoincrement\: bool, columnDefinition\: non\-empty\-string\|null, comment\: string, charset\?\: non\-empty\-string\|null, \.\.\.\}\>, array\<string, array\{name\: string, notnull\: true, type\: Doctrine\\DBAL\\Types\\Type\}\> given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/Query/Exec/MultiTableUpdateExecutor.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#3 \$types of method Doctrine\\DBAL\\Connection\:\:executeStatement\(\) expects array\<int\<0, max\>\|string, Doctrine\\DBAL\\ArrayParameterType\|Doctrine\\DBAL\\ParameterType\|Doctrine\\DBAL\\Types\\Type\|string\>, list\<Doctrine\\DBAL\\ArrayParameterType\|Doctrine\\DBAL\\ParameterType\|Doctrine\\DBAL\\Types\\Type\|int\|string\> given\.$#'
|
||||
identifier: argument.type
|
||||
@@ -2868,12 +2850,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Query/SqlWalker.php
|
||||
|
||||
-
|
||||
message: '#^Property Doctrine\\ORM\\Query\\SqlWalker\:\:\$selectedClasses \(array\<string, array\{class\: Doctrine\\ORM\\Mapping\\ClassMetadata, dqlAlias\: string, resultAlias\: string\|null\}\>\) does not accept non\-empty\-array\<int\|string, array\{class\: Doctrine\\ORM\\Mapping\\ClassMetadata, dqlAlias\: mixed, resultAlias\: string\|null\}\>\.$#'
|
||||
identifier: assign.propertyType
|
||||
count: 1
|
||||
path: src/Query/SqlWalker.php
|
||||
|
||||
-
|
||||
message: '#^Property Doctrine\\ORM\\Query\\SqlWalker\:\:\$selectedClasses with generic class Doctrine\\ORM\\Mapping\\ClassMetadata does not specify its types\: T$#'
|
||||
identifier: missingType.generics
|
||||
@@ -3127,32 +3103,71 @@ parameters:
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\:\:getReferencedColumnNames\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
message: '''
|
||||
#^Call to deprecated method getColumns\(\) of class Doctrine\\DBAL\\Schema\\Index\:
|
||||
Use \{@see getIndexedColumns\(\)\} instead\.$#
|
||||
'''
|
||||
identifier: method.deprecated
|
||||
count: 1
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\:\:getReferencedTableName\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
message: '''
|
||||
#^Call to deprecated method getForeignColumns\(\) of class Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\:
|
||||
Use \{@see getReferencedColumnNames\(\)\} instead\.
|
||||
|
||||
Returns the names of the referenced table columns
|
||||
the foreign key constraint is associated with\.$#
|
||||
'''
|
||||
identifier: method.deprecated
|
||||
count: 1
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\:\:getReferencingColumnNames\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
message: '''
|
||||
#^Call to deprecated method getForeignTableName\(\) of class Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\:
|
||||
Use \{@see getReferencedTableName\(\)\} instead\.
|
||||
|
||||
Returns the name of the referenced table
|
||||
the foreign key constraint is associated with\.$#
|
||||
'''
|
||||
identifier: method.deprecated
|
||||
count: 1
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Doctrine\\DBAL\\Schema\\Index\:\:getIndexedColumns\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
message: '''
|
||||
#^Call to deprecated method getLocalColumns\(\) of class Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\:
|
||||
Use \{@see getReferencingColumnNames\(\)\} instead\.$#
|
||||
'''
|
||||
identifier: method.deprecated
|
||||
count: 1
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Doctrine\\DBAL\\Schema\\Table\:\:dropForeignKey\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
message: '''
|
||||
#^Call to deprecated method getPrimaryKey\(\) of class Doctrine\\DBAL\\Schema\\Table\:
|
||||
Use \{@see getPrimaryKeyConstraint\(\)\} instead\.$#
|
||||
'''
|
||||
identifier: method.deprecated
|
||||
count: 1
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '''
|
||||
#^Call to deprecated method removeForeignKey\(\) of class Doctrine\\DBAL\\Schema\\Table\:
|
||||
Use \{@link dropForeignKey\(\)\} instead\.$#
|
||||
'''
|
||||
identifier: method.deprecated
|
||||
count: 1
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '''
|
||||
#^Call to deprecated method setPrimaryKey\(\) of class Doctrine\\DBAL\\Schema\\Table\:
|
||||
Use \{@see addPrimaryKeyConstraint\(\)\} instead\.$#
|
||||
'''
|
||||
identifier: method.deprecated
|
||||
count: 1
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
@@ -3163,32 +3178,14 @@ parameters:
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '#^Call to method getColumnName\(\) on an unknown class Doctrine\\DBAL\\Schema\\Index\\IndexedColumn\.$#'
|
||||
identifier: class.notFound
|
||||
message: '#^Call to function method_exists\(\) with ''Doctrine\\\\DBAL\\\\Schema\\\\Index'' and ''getIndexedColumns'' will always evaluate to true\.$#'
|
||||
identifier: function.alreadyNarrowedType
|
||||
count: 1
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '#^Call to method getColumnNames\(\) on an unknown class Doctrine\\DBAL\\Schema\\PrimaryKeyConstraint\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '#^Call to method toString\(\) on an unknown class Doctrine\\DBAL\\Schema\\Name\\UnqualifiedName\.$#'
|
||||
identifier: class.notFound
|
||||
count: 3
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '#^Class Doctrine\\DBAL\\Schema\\PrimaryKeyConstraint not found\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\DBAL\\Schema\\AbstractSchemaManager\<Doctrine\\DBAL\\Platforms\\AbstractPlatform\>\:\:createComparator\(\) invoked with 1 parameter, 0 required\.$#'
|
||||
identifier: arguments.count
|
||||
message: '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Schema\\Table and ''getPrimaryKeyConstr…'' will always evaluate to true\.$#'
|
||||
identifier: function.alreadyNarrowedType
|
||||
count: 1
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
@@ -3295,15 +3292,51 @@ parameters:
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \$indexedColumn of anonymous function has invalid type Doctrine\\DBAL\\Schema\\Index\\IndexedColumn\.$#'
|
||||
identifier: class.notFound
|
||||
message: '#^Parameter \#1 \$columnNames of method Doctrine\\DBAL\\Schema\\Table\:\:addIndex\(\) expects non\-empty\-list\<string\>, list\<string\> given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \$name of anonymous function has invalid type Doctrine\\DBAL\\Schema\\Name\\UnqualifiedName\.$#'
|
||||
identifier: class.notFound
|
||||
count: 3
|
||||
message: '#^Parameter \#1 \$columnNames of method Doctrine\\DBAL\\Schema\\Table\:\:addUniqueIndex\(\) expects non\-empty\-list\<string\>, array\<string\> given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$columnNames of method Doctrine\\DBAL\\Schema\\Table\:\:setPrimaryKey\(\) expects non\-empty\-list\<string\>, array\<string\> given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$value of static method Doctrine\\DBAL\\Schema\\Name\\Identifier\:\:unquoted\(\) expects non\-empty\-string, string given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#2 \$columnNames of class Doctrine\\DBAL\\Schema\\PrimaryKeyConstraint constructor expects non\-empty\-list\<Doctrine\\DBAL\\Schema\\Name\\UnqualifiedName\>, list\<Doctrine\\DBAL\\Schema\\Name\\UnqualifiedName\> given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#2 \$columns of class Doctrine\\DBAL\\Schema\\Index constructor expects non\-empty\-list\<string\>, list\<string\> given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#2 \$localColumnNames of method Doctrine\\DBAL\\Schema\\Table\:\:addForeignKeyConstraint\(\) expects non\-empty\-list\<string\>, list\<string\> given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#3 \$foreignColumnNames of method Doctrine\\DBAL\\Schema\\Table\:\:addForeignKeyConstraint\(\) expects non\-empty\-list\<string\>, list\<string\> given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
@@ -3456,12 +3489,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/UnitOfWork.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @phpstan\-assert\-if\-true for \$obj contains generic interface Doctrine\\ORM\\Proxy\\InternalProxy but does not specify its types\: T$#'
|
||||
identifier: missingType.generics
|
||||
count: 1
|
||||
path: src/UnitOfWork.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#2 \$assoc of method Doctrine\\ORM\\PersistentCollection\<\(int\|string\),mixed\>\:\:setOwner\(\) expects Doctrine\\ORM\\Mapping\\AssociationMapping&Doctrine\\ORM\\Mapping\\ToManyAssociationMapping, Doctrine\\ORM\\Mapping\\ManyToManyInverseSideMapping\|Doctrine\\ORM\\Mapping\\ManyToManyOwningSideMapping\|Doctrine\\ORM\\Mapping\\ManyToOneAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToManyAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToOneInverseSideMapping\|Doctrine\\ORM\\Mapping\\OneToOneOwningSideMapping given\.$#'
|
||||
identifier: argument.type
|
||||
@@ -3504,6 +3531,12 @@ parameters:
|
||||
count: 1
|
||||
path: src/UnitOfWork.php
|
||||
|
||||
-
|
||||
message: '#^Unable to resolve the template type T in call to method static method Symfony\\Component\\VarExporter\\Hydrator\:\:hydrate\(\)$#'
|
||||
identifier: argument.templateType
|
||||
count: 1
|
||||
path: src/UnitOfWork.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property Doctrine\\Persistence\\Mapping\\ClassMetadata\:\:\$name\.$#'
|
||||
identifier: property.notFound
|
||||
|
||||
@@ -34,8 +34,58 @@ parameters:
|
||||
message: '~^Call to an undefined method Doctrine\\DBAL\\Schema\\Table::addPrimaryKeyConstraint\(\)\.$~'
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '~^Call to an undefined method Doctrine\\DBAL\\Schema\\ForeignKeyConstraint::get.*\.$~'
|
||||
identifier: method.notFound
|
||||
|
||||
-
|
||||
message: '~createComparator~'
|
||||
identifier: arguments.count
|
||||
|
||||
-
|
||||
message: '~UnqualifiedName~'
|
||||
identifier: class.notFound
|
||||
|
||||
-
|
||||
message: '~IndexedColumn~'
|
||||
identifier: class.notFound
|
||||
|
||||
-
|
||||
message: '~PrimaryKeyConstraint~'
|
||||
identifier: class.notFound
|
||||
|
||||
-
|
||||
message: '~IndexType~'
|
||||
identifier: class.notFound
|
||||
|
||||
-
|
||||
message: '~dropForeignKey~'
|
||||
identifier: method.notFound
|
||||
|
||||
-
|
||||
message: '~getIndexedColumns~'
|
||||
identifier: method.notFound
|
||||
|
||||
-
|
||||
message: '~getPrimaryKeyConstraint~'
|
||||
identifier: method.notFound
|
||||
|
||||
-
|
||||
message: '~PrimaryKeyConstraint~'
|
||||
identifier: class.notFound
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '~^Call to method toString.*UnqualifiedName\.$~'
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
- '~^Class Doctrine\\DBAL\\Platforms\\SQLitePlatform not found\.$~'
|
||||
|
||||
-
|
||||
message: '~sort~'
|
||||
identifier: argument.unresolvableType
|
||||
path: src/Mapping/Driver/DatabaseDriver.php
|
||||
|
||||
# To be removed in 4.0
|
||||
-
|
||||
message: '#Negated boolean expression is always false\.#'
|
||||
|
||||
14
phpstan.neon
14
phpstan.neon
@@ -20,18 +20,6 @@ parameters:
|
||||
message: '~^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:getArrayBindingType\(\) never returns .* so it can be removed from the return type\.$~'
|
||||
path: src/Persisters/Entity/BasicEntityPersister.php
|
||||
|
||||
-
|
||||
message: '~^Call to static method unquoted\(\) on an unknown class Doctrine\\DBAL\\Schema\\Name\\Identifier\.$~'
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '~^Instantiated class Doctrine\\DBAL\\Schema\\Name\\UnqualifiedName not found\.$~'
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
-
|
||||
message: '~^Call to an undefined method Doctrine\\DBAL\\Schema\\Table::addPrimaryKeyConstraint\(\)\.$~'
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
# Compatibility with DBAL 3
|
||||
# See https://github.com/doctrine/dbal/pull/3480
|
||||
-
|
||||
@@ -45,7 +33,7 @@ parameters:
|
||||
path: src/UnitOfWork.php
|
||||
|
||||
-
|
||||
message: '~^Parameter #1 \$command of method Symfony\\Component\\Console\\Application::add\(\) expects Symfony\\Component\\Console\\Command\\Command, Doctrine\\DBAL\\Tools\\Console\\Command\\ReservedWordsCommand given\.$~'
|
||||
message: '~^Parameter #2 \$command of static method Doctrine\\ORM\\Tools\\Console\\ConsoleRunner::addCommandToApplication\(\) expects Symfony\\Component\\Console\\Command\\Command, Doctrine\\DBAL\\Tools\\Console\\Command\\ReservedWordsCommand given\.$~'
|
||||
path: src/Tools/Console/ConsoleRunner.php
|
||||
|
||||
-
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Doctrine\ORM;
|
||||
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\Cache\CacheConfiguration;
|
||||
use Doctrine\ORM\Exception\InvalidEntityRepository;
|
||||
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
|
||||
@@ -30,6 +31,8 @@ use function class_exists;
|
||||
use function is_a;
|
||||
use function strtolower;
|
||||
|
||||
use const PHP_VERSION_ID;
|
||||
|
||||
/**
|
||||
* Configuration container for all configuration options of Doctrine.
|
||||
* It combines all configuration options from DBAL & ORM.
|
||||
@@ -61,6 +64,15 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
*/
|
||||
public function setProxyDir(string $dir): void
|
||||
{
|
||||
if (PHP_VERSION_ID >= 80400) {
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/12005',
|
||||
'Calling %s is deprecated and will not be possible in Doctrine ORM 4.0.',
|
||||
__METHOD__,
|
||||
);
|
||||
}
|
||||
|
||||
$this->attributes['proxyDir'] = $dir;
|
||||
}
|
||||
|
||||
@@ -69,6 +81,15 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
*/
|
||||
public function getProxyDir(): string|null
|
||||
{
|
||||
if (PHP_VERSION_ID >= 80400) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/12005',
|
||||
'Calling %s is deprecated and will not be possible in Doctrine ORM 4.0.',
|
||||
__METHOD__,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->attributes['proxyDir'] ?? null;
|
||||
}
|
||||
|
||||
@@ -79,6 +100,15 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
*/
|
||||
public function getAutoGenerateProxyClasses(): int
|
||||
{
|
||||
if (PHP_VERSION_ID >= 80400) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/12005',
|
||||
'Calling %s is deprecated and will not be possible in Doctrine ORM 4.0.',
|
||||
__METHOD__,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->attributes['autoGenerateProxyClasses'] ?? ProxyFactory::AUTOGENERATE_ALWAYS;
|
||||
}
|
||||
|
||||
@@ -89,6 +119,15 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
*/
|
||||
public function setAutoGenerateProxyClasses(bool|int $autoGenerate): void
|
||||
{
|
||||
if (PHP_VERSION_ID >= 80400) {
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/12005',
|
||||
'Calling %s is deprecated and will not be possible in Doctrine ORM 4.0.',
|
||||
__METHOD__,
|
||||
);
|
||||
}
|
||||
|
||||
$this->attributes['autoGenerateProxyClasses'] = (int) $autoGenerate;
|
||||
}
|
||||
|
||||
@@ -97,6 +136,15 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
*/
|
||||
public function getProxyNamespace(): string|null
|
||||
{
|
||||
if (PHP_VERSION_ID >= 80400) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/12005',
|
||||
'Calling %s is deprecated and will not be possible in Doctrine ORM 4.0.',
|
||||
__METHOD__,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->attributes['proxyNamespace'] ?? null;
|
||||
}
|
||||
|
||||
@@ -105,6 +153,15 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
*/
|
||||
public function setProxyNamespace(string $ns): void
|
||||
{
|
||||
if (PHP_VERSION_ID >= 80400) {
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/12005',
|
||||
'Calling %s is deprecated and will not be possible in Doctrine ORM 4.0.',
|
||||
__METHOD__,
|
||||
);
|
||||
}
|
||||
|
||||
$this->attributes['proxyNamespace'] = $ns;
|
||||
}
|
||||
|
||||
@@ -593,8 +650,30 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
$this->attributes['schemaIgnoreClasses'] = $schemaIgnoreClasses;
|
||||
}
|
||||
|
||||
public function isNativeLazyObjectsEnabled(): bool
|
||||
{
|
||||
return $this->attributes['nativeLazyObjects'] ?? false;
|
||||
}
|
||||
|
||||
public function enableNativeLazyObjects(bool $nativeLazyObjects): void
|
||||
{
|
||||
if (PHP_VERSION_ID >= 80400 && ! $nativeLazyObjects) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/12005',
|
||||
'Disabling native lazy objects is deprecated and will be impossible in Doctrine ORM 4.0.',
|
||||
);
|
||||
}
|
||||
|
||||
if (PHP_VERSION_ID < 80400 && $nativeLazyObjects) {
|
||||
throw new LogicException('Lazy loading proxies require PHP 8.4 or higher.');
|
||||
}
|
||||
|
||||
$this->attributes['nativeLazyObjects'] = $nativeLazyObjects;
|
||||
}
|
||||
|
||||
/**
|
||||
* To be deprecated in 3.1.0
|
||||
* @deprecated lazy ghost objects are always enabled
|
||||
*
|
||||
* @return true
|
||||
*/
|
||||
@@ -603,7 +682,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
return true;
|
||||
}
|
||||
|
||||
/** To be deprecated in 3.1.0 */
|
||||
/** @deprecated lazy ghost objects cannot be disabled */
|
||||
public function setLazyGhostObjectEnabled(bool $flag): void
|
||||
{
|
||||
if (! $flag) {
|
||||
@@ -614,7 +693,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
}
|
||||
}
|
||||
|
||||
/** To be deprecated in 3.1.0 */
|
||||
/** @deprecated rejecting ID collisions in the identity map cannot be disabled */
|
||||
public function setRejectIdCollisionInIdentityMap(bool $flag): void
|
||||
{
|
||||
if (! $flag) {
|
||||
@@ -626,7 +705,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
}
|
||||
|
||||
/**
|
||||
* To be deprecated in 3.1.0
|
||||
* @deprecated rejecting ID collisions in the identity map is always enabled
|
||||
*
|
||||
* @return true
|
||||
*/
|
||||
|
||||
@@ -43,7 +43,7 @@ use function method_exists;
|
||||
*
|
||||
* $paths = ['/path/to/entity/mapping/files'];
|
||||
*
|
||||
* $config = ORMSetup::createAttributeMetadataConfiguration($paths);
|
||||
* $config = ORMSetup::createAttributeMetadataConfig($paths);
|
||||
* $connection = DriverManager::getConnection(['driver' => 'pdo_sqlite', 'memory' => true], $config);
|
||||
* $entityManager = new EntityManager($connection, $config);
|
||||
*
|
||||
@@ -134,12 +134,16 @@ class EntityManager implements EntityManagerInterface
|
||||
|
||||
$this->repositoryFactory = $config->getRepositoryFactory();
|
||||
$this->unitOfWork = new UnitOfWork($this);
|
||||
$this->proxyFactory = new ProxyFactory(
|
||||
$this,
|
||||
$config->getProxyDir(),
|
||||
$config->getProxyNamespace(),
|
||||
$config->getAutoGenerateProxyClasses(),
|
||||
);
|
||||
if ($config->isNativeLazyObjectsEnabled()) {
|
||||
$this->proxyFactory = new ProxyFactory($this);
|
||||
} else {
|
||||
$this->proxyFactory = new ProxyFactory(
|
||||
$this,
|
||||
$config->getProxyDir(),
|
||||
$config->getProxyNamespace(),
|
||||
$config->getAutoGenerateProxyClasses(),
|
||||
);
|
||||
}
|
||||
|
||||
if ($config->isSecondLevelCacheEnabled()) {
|
||||
$cacheConfig = $config->getSecondLevelCacheConfiguration();
|
||||
|
||||
@@ -19,6 +19,7 @@ use LogicException;
|
||||
use ReflectionClass;
|
||||
|
||||
use function array_key_exists;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function count;
|
||||
@@ -348,14 +349,29 @@ abstract class AbstractHydrator
|
||||
}
|
||||
}
|
||||
|
||||
$nestedEntities = [];
|
||||
/**@var string $argAlias */
|
||||
foreach ($this->resultSetMapping()->nestedNewObjectArguments as ['ownerIndex' => $ownerIndex, 'argIndex' => $argIndex, 'argAlias' => $argAlias]) {
|
||||
if (array_key_exists($argAlias, $rowData['newObjects'])) {
|
||||
ksort($rowData['newObjects'][$argAlias]['args']);
|
||||
$rowData['newObjects'][$ownerIndex]['args'][$argIndex] = $rowData['newObjects'][$argAlias]['class']->newInstanceArgs($rowData['newObjects'][$argAlias]['args']);
|
||||
unset($rowData['newObjects'][$argAlias]);
|
||||
} elseif (array_key_exists($argAlias, $rowData['data'])) {
|
||||
if (! array_key_exists($argAlias, $nestedEntities)) {
|
||||
$nestedEntities[$argAlias] = '';
|
||||
$rowData['data'][$argAlias] = $this->hydrateNestedEntity($rowData['data'][$argAlias], $argAlias);
|
||||
}
|
||||
|
||||
$rowData['newObjects'][$ownerIndex]['args'][$argIndex] = $rowData['data'][$argAlias];
|
||||
} else {
|
||||
throw new LogicException($argAlias . ' does not exist');
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_keys($nestedEntities) as $entity) {
|
||||
unset($rowData['data'][$entity]);
|
||||
}
|
||||
|
||||
foreach ($rowData['newObjects'] as $objIndex => $newObject) {
|
||||
ksort($rowData['newObjects'][$objIndex]['args']);
|
||||
$obj = $rowData['newObjects'][$objIndex]['class']->newInstanceArgs($rowData['newObjects'][$objIndex]['args']);
|
||||
@@ -366,6 +382,12 @@ abstract class AbstractHydrator
|
||||
return $rowData;
|
||||
}
|
||||
|
||||
/** @param mixed[] $data pre-hydrated SQL Result Row. */
|
||||
protected function hydrateNestedEntity(array $data, string $dqlAlias): mixed
|
||||
{
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a row of the result set.
|
||||
*
|
||||
|
||||
@@ -70,6 +70,10 @@ class ObjectHydrator extends AbstractHydrator
|
||||
$parent = $this->resultSetMapping()->parentAliasMap[$dqlAlias];
|
||||
|
||||
if (! isset($this->resultSetMapping()->aliasMap[$parent])) {
|
||||
if (isset($this->resultSetMapping()->nestedEntities[$dqlAlias])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw HydrationException::parentObjectOfRelationNotFound($dqlAlias, $parent);
|
||||
}
|
||||
|
||||
@@ -171,7 +175,7 @@ class ObjectHydrator extends AbstractHydrator
|
||||
): PersistentCollection {
|
||||
$oid = spl_object_id($entity);
|
||||
$relation = $class->associationMappings[$fieldName];
|
||||
$value = $class->reflFields[$fieldName]->getValue($entity);
|
||||
$value = $class->propertyAccessors[$fieldName]->getValue($entity);
|
||||
|
||||
if ($value === null || is_array($value)) {
|
||||
$value = new ArrayCollection((array) $value);
|
||||
@@ -186,7 +190,7 @@ class ObjectHydrator extends AbstractHydrator
|
||||
);
|
||||
$value->setOwner($entity, $relation);
|
||||
|
||||
$class->reflFields[$fieldName]->setValue($entity, $value);
|
||||
$class->propertyAccessors[$fieldName]->setValue($entity, $value);
|
||||
$this->uow->setOriginalEntityProperty($oid, $fieldName, $value);
|
||||
|
||||
$this->initializedCollections[$oid . $fieldName] = $value;
|
||||
@@ -346,7 +350,7 @@ class ObjectHydrator extends AbstractHydrator
|
||||
$parentClass = $this->metadataCache[$this->resultSetMapping()->aliasMap[$parentAlias]];
|
||||
$relationField = $this->resultSetMapping()->relationMap[$dqlAlias];
|
||||
$relation = $parentClass->associationMappings[$relationField];
|
||||
$reflField = $parentClass->reflFields[$relationField];
|
||||
$reflField = $parentClass->propertyAccessors[$relationField];
|
||||
|
||||
// Get a reference to the parent object to which the joined element belongs.
|
||||
if ($this->resultSetMapping()->isMixed && isset($this->rootAliases[$parentAlias])) {
|
||||
@@ -446,13 +450,13 @@ class ObjectHydrator extends AbstractHydrator
|
||||
if ($relation->inversedBy !== null) {
|
||||
$inverseAssoc = $targetClass->associationMappings[$relation->inversedBy];
|
||||
if ($inverseAssoc->isToOne()) {
|
||||
$targetClass->reflFields[$inverseAssoc->fieldName]->setValue($element, $parentObject);
|
||||
$targetClass->propertyAccessors[$inverseAssoc->fieldName]->setValue($element, $parentObject);
|
||||
$this->uow->setOriginalEntityProperty(spl_object_id($element), $inverseAssoc->fieldName, $parentObject);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For sure bidirectional, as there is no inverse side in unidirectional mappings
|
||||
$targetClass->reflFields[$relation->mappedBy]->setValue($element, $parentObject);
|
||||
$targetClass->propertyAccessors[$relation->mappedBy]->setValue($element, $parentObject);
|
||||
$this->uow->setOriginalEntityProperty(spl_object_id($element), $relation->mappedBy, $parentObject);
|
||||
}
|
||||
|
||||
@@ -569,6 +573,16 @@ class ObjectHydrator extends AbstractHydrator
|
||||
}
|
||||
}
|
||||
|
||||
/** @param mixed[] $data pre-hydrated SQL Result Row. */
|
||||
protected function hydrateNestedEntity(array $data, string $dqlAlias): mixed
|
||||
{
|
||||
if (isset($this->resultSetMapping()->nestedEntities[$dqlAlias])) {
|
||||
return $this->getEntity($data, $dqlAlias);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* When executed in a hydrate() loop we may have to clear internal state to
|
||||
* decrease memory consumption.
|
||||
|
||||
79
src/Internal/UnitOfWork/InsertBatch.php
Normal file
79
src/Internal/UnitOfWork/InsertBatch.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Internal\UnitOfWork;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Id\AssignedGenerator;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
|
||||
/**
|
||||
* An {@see InsertBatch} represents a set of entities that are safe to be batched
|
||||
* together in a single query.
|
||||
*
|
||||
* These entities are only those that have all fields already assigned, including the
|
||||
* identifier field(s).
|
||||
*
|
||||
* This data structure only exists for internal {@see UnitOfWork} optimisations, and
|
||||
* should not be relied upon outside the ORM.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @template TEntity of object
|
||||
*/
|
||||
final class InsertBatch
|
||||
{
|
||||
/**
|
||||
* @param ClassMetadata<TEntity> $class
|
||||
* @param non-empty-list<TEntity> $entities
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly ClassMetadata $class,
|
||||
public array $entities,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: Code in here is procedural/ugly due to it being in a hot path of the {@see UnitOfWork}
|
||||
*
|
||||
* This method will batch the given entity set by type, preserving their order. For example,
|
||||
* given an input [A1, A2, A3, B1, B2, A4, A5], it will create an [[A1, A2, A3], [B1, B2], [A4, A5]] batch.
|
||||
*
|
||||
* Entities for which the identifier needs to be generated or fetched by a sequence are put as single
|
||||
* items in a batch of their own, since it is unsafe to batch-insert them.
|
||||
*
|
||||
* @param list<TEntities> $entities
|
||||
*
|
||||
* @return list<self<TEntities>>
|
||||
*
|
||||
* @template TEntities of object
|
||||
*/
|
||||
public static function batchByEntityType(
|
||||
EntityManagerInterface $entityManager,
|
||||
array $entities,
|
||||
): array {
|
||||
$currentClass = null;
|
||||
$batches = [];
|
||||
$batchIndex = -1;
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
$entityClass = $entityManager->getClassMetadata($entity::class);
|
||||
|
||||
if (
|
||||
$currentClass?->name !== $entityClass->name
|
||||
|| ! $entityClass->idGenerator instanceof AssignedGenerator
|
||||
) {
|
||||
$currentClass = $entityClass;
|
||||
$batches[] = new InsertBatch($entityClass, [$entity]);
|
||||
$batchIndex += 1;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$batches[$batchIndex]->entities[] = $entity;
|
||||
}
|
||||
|
||||
return $batches;
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,13 @@ abstract class AssociationMapping implements ArrayAccess
|
||||
continue;
|
||||
}
|
||||
|
||||
assert($mapping instanceof ManyToManyOwningSideMapping);
|
||||
if (! $mapping instanceof ManyToManyOwningSideMapping) {
|
||||
throw new MappingException(
|
||||
"Mapping error on field '" .
|
||||
$mapping->fieldName . "' in " . $mapping->sourceEntity .
|
||||
" : 'joinTable' can only be set on many-to-many owning side.",
|
||||
);
|
||||
}
|
||||
|
||||
$mapping->joinTable = JoinTableMapping::fromMappingArray($value);
|
||||
|
||||
|
||||
@@ -64,6 +64,18 @@ class FieldBuilder
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets indexed.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function index(bool $flag = true): static
|
||||
{
|
||||
$this->mapping['index'] = $flag;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets column name.
|
||||
*
|
||||
|
||||
@@ -14,9 +14,12 @@ use Doctrine\Instantiator\InstantiatorInterface;
|
||||
use Doctrine\ORM\Cache\Exception\NonCacheableEntityAssociation;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Id\AbstractIdGenerator;
|
||||
use Doctrine\ORM\Mapping\PropertyAccessors\EmbeddablePropertyAccessor;
|
||||
use Doctrine\ORM\Mapping\PropertyAccessors\EnumPropertyAccessor;
|
||||
use Doctrine\ORM\Mapping\PropertyAccessors\PropertyAccessor;
|
||||
use Doctrine\ORM\Mapping\PropertyAccessors\PropertyAccessorFactory;
|
||||
use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata;
|
||||
use Doctrine\Persistence\Mapping\ReflectionService;
|
||||
use Doctrine\Persistence\Reflection\EnumReflectionProperty;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use ReflectionClass;
|
||||
@@ -57,8 +60,6 @@ use function strtolower;
|
||||
use function trait_exists;
|
||||
use function trim;
|
||||
|
||||
use const PHP_VERSION_ID;
|
||||
|
||||
/**
|
||||
* A <tt>ClassMetadata</tt> instance holds all the object-relational mapping metadata
|
||||
* of an entity and its associations.
|
||||
@@ -399,13 +400,9 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
public DiscriminatorColumnMapping|null $discriminatorColumn = null;
|
||||
|
||||
/**
|
||||
* READ-ONLY: The primary table definition. The definition is an array with the
|
||||
* following entries:
|
||||
* READ-ONLY: The primary table definition.
|
||||
*
|
||||
* name => <tableName>
|
||||
* schema => <schemaName>
|
||||
* indexes => array
|
||||
* uniqueConstraints => array
|
||||
* "quoted" indicates whether the table name is quoted (with backticks) or not
|
||||
*
|
||||
* @var mixed[]
|
||||
* @phpstan-var array{
|
||||
@@ -543,9 +540,14 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
/**
|
||||
* The ReflectionProperty instances of the mapped class.
|
||||
*
|
||||
* @var array<string, ReflectionProperty|null>
|
||||
* @deprecated Use $propertyAccessors instead.
|
||||
*
|
||||
* @var LegacyReflectionFields|array<string, ReflectionProperty>
|
||||
*/
|
||||
public array $reflFields = [];
|
||||
public LegacyReflectionFields|array $reflFields = [];
|
||||
|
||||
/** @var array<string, PropertyAccessors\PropertyAccessor> */
|
||||
public array $propertyAccessors = [];
|
||||
|
||||
private InstantiatorInterface|null $instantiator = null;
|
||||
|
||||
@@ -569,24 +571,43 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
/**
|
||||
* Gets the ReflectionProperties of the mapped class.
|
||||
*
|
||||
* @return ReflectionProperty[]|null[] An array of ReflectionProperty instances.
|
||||
* @phpstan-return array<ReflectionProperty|null>
|
||||
* @deprecated Use getPropertyAccessors() instead.
|
||||
*
|
||||
* @return LegacyReflectionFields|ReflectionProperty[] An array of ReflectionProperty instances.
|
||||
* @phpstan-return LegacyReflectionFields|array<string, ReflectionProperty>
|
||||
*/
|
||||
public function getReflectionProperties(): array
|
||||
public function getReflectionProperties(): array|LegacyReflectionFields
|
||||
{
|
||||
return $this->reflFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ReflectionProperties of the mapped class.
|
||||
*
|
||||
* @return PropertyAccessor[] An array of PropertyAccessor instances.
|
||||
*/
|
||||
public function getPropertyAccessors(): array
|
||||
{
|
||||
return $this->propertyAccessors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a ReflectionProperty for a specific field of the mapped class.
|
||||
*
|
||||
* @deprecated Use getPropertyAccessor() instead.
|
||||
*/
|
||||
public function getReflectionProperty(string $name): ReflectionProperty|null
|
||||
{
|
||||
return $this->reflFields[$name];
|
||||
}
|
||||
|
||||
public function getPropertyAccessor(string $name): PropertyAccessor|null
|
||||
{
|
||||
return $this->propertyAccessors[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ReflectionProperty for the single identifier field.
|
||||
* @deprecated Use getPropertyAccessor() instead.
|
||||
*
|
||||
* @throws BadMethodCallException If the class has a composite identifier.
|
||||
*/
|
||||
@@ -599,6 +620,16 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
return $this->reflFields[$this->identifier[0]];
|
||||
}
|
||||
|
||||
/** @throws BadMethodCallException If the class has a composite identifier. */
|
||||
public function getSingleIdPropertyAccessor(): PropertyAccessor|null
|
||||
{
|
||||
if ($this->isIdentifierComposite) {
|
||||
throw new BadMethodCallException('Class ' . $this->name . ' has a composite identifier.');
|
||||
}
|
||||
|
||||
return $this->propertyAccessors[$this->identifier[0]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the identifier values of an entity of this class.
|
||||
*
|
||||
@@ -613,7 +644,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
$id = [];
|
||||
|
||||
foreach ($this->identifier as $idField) {
|
||||
$value = $this->reflFields[$idField]->getValue($entity);
|
||||
$value = $this->propertyAccessors[$idField]->getValue($entity);
|
||||
|
||||
if ($value !== null) {
|
||||
$id[$idField] = $value;
|
||||
@@ -624,7 +655,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
}
|
||||
|
||||
$id = $this->identifier[0];
|
||||
$value = $this->reflFields[$id]->getValue($entity);
|
||||
$value = $this->propertyAccessors[$id]->getValue($entity);
|
||||
|
||||
if ($value === null) {
|
||||
return [];
|
||||
@@ -643,7 +674,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
public function setIdentifierValues(object $entity, array $id): void
|
||||
{
|
||||
foreach ($id as $idField => $idValue) {
|
||||
$this->reflFields[$idField]->setValue($entity, $idValue);
|
||||
$this->propertyAccessors[$idField]->setValue($entity, $idValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -652,7 +683,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
*/
|
||||
public function setFieldValue(object $entity, string $field, mixed $value): void
|
||||
{
|
||||
$this->reflFields[$field]->setValue($entity, $value);
|
||||
$this->propertyAccessors[$field]->setValue($entity, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -660,7 +691,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
*/
|
||||
public function getFieldValue(object $entity, string $field): mixed
|
||||
{
|
||||
return $this->reflFields[$field]->getValue($entity);
|
||||
return $this->propertyAccessors[$field]->getValue($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -793,77 +824,76 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
public function wakeupReflection(ReflectionService $reflService): void
|
||||
{
|
||||
// Restore ReflectionClass and properties
|
||||
$this->reflClass = $reflService->getClass($this->name);
|
||||
$this->reflClass = $reflService->getClass($this->name);
|
||||
/** @phpstan-ignore property.deprecated */
|
||||
$this->reflFields = new LegacyReflectionFields($this, $reflService);
|
||||
$this->instantiator = $this->instantiator ?: new Instantiator();
|
||||
|
||||
$parentReflFields = [];
|
||||
$parentAccessors = [];
|
||||
|
||||
foreach ($this->embeddedClasses as $property => $embeddedClass) {
|
||||
if (isset($embeddedClass->declaredField)) {
|
||||
assert($embeddedClass->originalField !== null);
|
||||
$childProperty = $this->getAccessibleProperty(
|
||||
$reflService,
|
||||
$childAccessor = PropertyAccessorFactory::createPropertyAccessor(
|
||||
$this->embeddedClasses[$embeddedClass->declaredField]->class,
|
||||
$embeddedClass->originalField,
|
||||
);
|
||||
assert($childProperty !== null);
|
||||
$parentReflFields[$property] = new ReflectionEmbeddedProperty(
|
||||
$parentReflFields[$embeddedClass->declaredField],
|
||||
$childProperty,
|
||||
|
||||
$parentAccessors[$property] = new EmbeddablePropertyAccessor(
|
||||
$parentAccessors[$embeddedClass->declaredField],
|
||||
$childAccessor,
|
||||
$this->embeddedClasses[$embeddedClass->declaredField]->class,
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldRefl = $this->getAccessibleProperty(
|
||||
$reflService,
|
||||
$accessor = PropertyAccessorFactory::createPropertyAccessor(
|
||||
$embeddedClass->declared ?? $this->name,
|
||||
$property,
|
||||
);
|
||||
|
||||
$parentReflFields[$property] = $fieldRefl;
|
||||
$this->reflFields[$property] = $fieldRefl;
|
||||
$parentAccessors[$property] = $accessor;
|
||||
$this->propertyAccessors[$property] = $accessor;
|
||||
}
|
||||
|
||||
foreach ($this->fieldMappings as $field => $mapping) {
|
||||
if (isset($mapping->declaredField) && isset($parentReflFields[$mapping->declaredField])) {
|
||||
if (isset($mapping->declaredField) && isset($parentAccessors[$mapping->declaredField])) {
|
||||
assert($mapping->originalField !== null);
|
||||
assert($mapping->originalClass !== null);
|
||||
$childProperty = $this->getAccessibleProperty($reflService, $mapping->originalClass, $mapping->originalField);
|
||||
assert($childProperty !== null);
|
||||
$accessor = PropertyAccessorFactory::createPropertyAccessor($mapping->originalClass, $mapping->originalField);
|
||||
|
||||
if (isset($mapping->enumType)) {
|
||||
$childProperty = new EnumReflectionProperty(
|
||||
$childProperty,
|
||||
if ($mapping->enumType !== null) {
|
||||
$accessor = new EnumPropertyAccessor(
|
||||
$accessor,
|
||||
$mapping->enumType,
|
||||
);
|
||||
}
|
||||
|
||||
$this->reflFields[$field] = new ReflectionEmbeddedProperty(
|
||||
$parentReflFields[$mapping->declaredField],
|
||||
$childProperty,
|
||||
$this->propertyAccessors[$field] = new EmbeddablePropertyAccessor(
|
||||
$parentAccessors[$mapping->declaredField],
|
||||
$accessor,
|
||||
$mapping->originalClass,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->reflFields[$field] = isset($mapping->declared)
|
||||
? $this->getAccessibleProperty($reflService, $mapping->declared, $field)
|
||||
: $this->getAccessibleProperty($reflService, $this->name, $field);
|
||||
$this->propertyAccessors[$field] = isset($mapping->declared)
|
||||
? PropertyAccessorFactory::createPropertyAccessor($mapping->declared, $field)
|
||||
: PropertyAccessorFactory::createPropertyAccessor($this->name, $field);
|
||||
|
||||
if (isset($mapping->enumType) && $this->reflFields[$field] !== null) {
|
||||
$this->reflFields[$field] = new EnumReflectionProperty(
|
||||
$this->reflFields[$field],
|
||||
if ($mapping->enumType !== null) {
|
||||
$this->propertyAccessors[$field] = new EnumPropertyAccessor(
|
||||
$this->propertyAccessors[$field],
|
||||
$mapping->enumType,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->associationMappings as $field => $mapping) {
|
||||
$this->reflFields[$field] = isset($mapping->declared)
|
||||
? $this->getAccessibleProperty($reflService, $mapping->declared, $field)
|
||||
: $this->getAccessibleProperty($reflService, $this->name, $field);
|
||||
$this->propertyAccessors[$field] = isset($mapping->declared)
|
||||
? PropertyAccessorFactory::createPropertyAccessor($mapping->declared, $field)
|
||||
: PropertyAccessorFactory::createPropertyAccessor($this->name, $field);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1033,6 +1063,13 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
return $mapping !== false && isset($mapping->nullable) && $mapping->nullable;
|
||||
}
|
||||
|
||||
public function isIndexed(string $fieldName): bool
|
||||
{
|
||||
$mapping = $this->getFieldMapping($fieldName);
|
||||
|
||||
return isset($mapping->index) && $mapping->index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a column name for a field name.
|
||||
* If the column name for the field cannot be found, the given field name
|
||||
@@ -2659,26 +2696,4 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
|
||||
return $sequencePrefix;
|
||||
}
|
||||
|
||||
/** @phpstan-param class-string $class */
|
||||
private function getAccessibleProperty(ReflectionService $reflService, string $class, string $field): ReflectionProperty|null
|
||||
{
|
||||
$reflectionProperty = $reflService->getAccessibleProperty($class, $field);
|
||||
if ($reflectionProperty?->isReadOnly()) {
|
||||
$declaringClass = $reflectionProperty->class;
|
||||
if ($declaringClass !== $class) {
|
||||
$reflectionProperty = $reflService->getAccessibleProperty($declaringClass, $field);
|
||||
}
|
||||
|
||||
if ($reflectionProperty !== null) {
|
||||
$reflectionProperty = new ReflectionReadonlyProperty($reflectionProperty);
|
||||
}
|
||||
}
|
||||
|
||||
if (PHP_VERSION_ID >= 80400 && $reflectionProperty !== null && count($reflectionProperty->getHooks()) > 0) {
|
||||
throw new LogicException('Doctrine ORM does not support property hooks in this version. Check https://github.com/doctrine/orm/issues/11624 for details of versions that support property hooks.');
|
||||
}
|
||||
|
||||
return $reflectionProperty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,8 @@ use function strlen;
|
||||
use function strtolower;
|
||||
use function substr;
|
||||
|
||||
use const PHP_VERSION_ID;
|
||||
|
||||
/**
|
||||
* The ClassMetadataFactory is used to create ClassMetadata objects that contain all the
|
||||
* metadata mapping information of a class which describes how a class should be mapped
|
||||
@@ -64,7 +66,9 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
|
||||
|
||||
public function setEntityManager(EntityManagerInterface $em): void
|
||||
{
|
||||
parent::setProxyClassNameResolver(new DefaultProxyClassNameResolver());
|
||||
if (! $em->getConfiguration()->isNativeLazyObjectsEnabled()) {
|
||||
parent::setProxyClassNameResolver(new DefaultProxyClassNameResolver());
|
||||
}
|
||||
|
||||
$this->em = $em;
|
||||
}
|
||||
@@ -440,8 +444,8 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
|
||||
$subClass->addInheritedFieldMapping($subClassMapping);
|
||||
}
|
||||
|
||||
foreach ($parentClass->reflFields as $name => $field) {
|
||||
$subClass->reflFields[$name] = $field;
|
||||
foreach ($parentClass->propertyAccessors as $name => $field) {
|
||||
$subClass->propertyAccessors[$name] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -699,6 +703,18 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
|
||||
protected function wakeupReflection(ClassMetadataInterface $class, ReflectionService $reflService): void
|
||||
{
|
||||
$class->wakeupReflection($reflService);
|
||||
|
||||
if (PHP_VERSION_ID < 80400) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($class->propertyAccessors as $propertyAccessor) {
|
||||
$property = $propertyAccessor->getUnderlyingReflector();
|
||||
|
||||
if ($property->isVirtual()) {
|
||||
throw MappingException::mappingVirtualPropertyNotAllowed($class->name, $property->getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function initializeReflection(ClassMetadataInterface $class, ReflectionService $reflService): void
|
||||
|
||||
@@ -31,6 +31,7 @@ final class Column implements MappingAttribute
|
||||
public readonly array $options = [],
|
||||
public readonly string|null $columnDefinition = null,
|
||||
public readonly string|null $generated = null,
|
||||
public readonly bool $index = false,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,11 @@ use Doctrine\ORM\Internal\SQLResultCasing;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function assert;
|
||||
use function explode;
|
||||
use function implode;
|
||||
use function is_numeric;
|
||||
use function preg_replace;
|
||||
use function sprintf;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
@@ -38,7 +41,13 @@ class DefaultQuoteStrategy implements QuoteStrategy
|
||||
$tableName = $class->table['name'];
|
||||
|
||||
if (! empty($class->table['schema'])) {
|
||||
$tableName = $class->table['schema'] . '.' . $class->table['name'];
|
||||
return isset($class->table['quoted'])
|
||||
? sprintf(
|
||||
'%s.%s',
|
||||
$platform->quoteSingleIdentifier($class->table['schema']),
|
||||
$platform->quoteSingleIdentifier($tableName),
|
||||
)
|
||||
: $class->table['schema'] . '.' . $class->table['name'];
|
||||
}
|
||||
|
||||
return isset($class->table['quoted'])
|
||||
@@ -52,7 +61,10 @@ class DefaultQuoteStrategy implements QuoteStrategy
|
||||
public function getSequenceName(array $definition, ClassMetadata $class, AbstractPlatform $platform): string
|
||||
{
|
||||
return isset($definition['quoted'])
|
||||
? $platform->quoteSingleIdentifier($definition['sequenceName'])
|
||||
? implode('.', array_map(
|
||||
static fn (string $part) => $platform->quoteSingleIdentifier($part),
|
||||
explode('.', $definition['sequenceName']),
|
||||
))
|
||||
: $definition['sequenceName'];
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Doctrine\ORM\Mapping;
|
||||
|
||||
use BackedEnum;
|
||||
use BcMath\Number;
|
||||
use DateInterval;
|
||||
use DateTime;
|
||||
use DateTimeImmutable;
|
||||
@@ -40,7 +41,12 @@ final class DefaultTypedFieldMapper implements TypedFieldMapper
|
||||
/** @param array<class-string|ScalarName, class-string<Type>|string> $typedFieldMappings */
|
||||
public function __construct(array $typedFieldMappings = [])
|
||||
{
|
||||
$this->typedFieldMappings = array_merge(self::DEFAULT_TYPED_FIELD_MAPPINGS, $typedFieldMappings);
|
||||
$defaultMappings = self::DEFAULT_TYPED_FIELD_MAPPINGS;
|
||||
if (defined(Types::class . '::NUMBER')) { // DBAL 4.3+
|
||||
$defaultMappings[Number::class] = Types::NUMBER;
|
||||
}
|
||||
|
||||
$this->typedFieldMappings = array_merge($defaultMappings, $typedFieldMappings);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -684,6 +684,7 @@ class AttributeDriver implements MappingDriver
|
||||
{
|
||||
$mapping = [
|
||||
'name' => $joinColumn->name,
|
||||
'deferrable' => $joinColumn->deferrable,
|
||||
'unique' => $joinColumn->unique,
|
||||
'nullable' => $joinColumn->nullable,
|
||||
'onDelete' => $joinColumn->onDelete,
|
||||
@@ -709,6 +710,7 @@ class AttributeDriver implements MappingDriver
|
||||
* length: int,
|
||||
* unique: bool,
|
||||
* nullable: bool,
|
||||
* index: bool,
|
||||
* precision: int,
|
||||
* enumType?: class-string,
|
||||
* options?: mixed[],
|
||||
@@ -725,6 +727,7 @@ class AttributeDriver implements MappingDriver
|
||||
'length' => $column->length,
|
||||
'unique' => $column->unique,
|
||||
'nullable' => $column->nullable,
|
||||
'index' => $column->index,
|
||||
'precision' => $column->precision,
|
||||
];
|
||||
|
||||
|
||||
@@ -754,6 +754,7 @@ class XmlDriver extends FileDriver
|
||||
* scale?: int,
|
||||
* unique?: bool,
|
||||
* nullable?: bool,
|
||||
* index?: bool,
|
||||
* notInsertable?: bool,
|
||||
* notUpdatable?: bool,
|
||||
* enumType?: string,
|
||||
@@ -792,6 +793,10 @@ class XmlDriver extends FileDriver
|
||||
$mapping['unique'] = $this->evaluateBoolean($fieldMapping['unique']);
|
||||
}
|
||||
|
||||
if (isset($fieldMapping['index'])) {
|
||||
$mapping['index'] = $this->evaluateBoolean($fieldMapping['index']);
|
||||
}
|
||||
|
||||
if (isset($fieldMapping['nullable'])) {
|
||||
$mapping['nullable'] = $this->evaluateBoolean($fieldMapping['nullable']);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ final class FieldMapping implements ArrayAccess
|
||||
public int|null $scale = null;
|
||||
/** Whether a unique constraint should be generated for the column. */
|
||||
public bool|null $unique = null;
|
||||
/** Whether an index should be generated for the column. */
|
||||
public bool|null $index = null;
|
||||
/**
|
||||
* @var class-string|null This is set when the field is inherited by this
|
||||
* class from another (inheritance) parent <em>entity</em> class. The value
|
||||
@@ -54,6 +56,7 @@ final class FieldMapping implements ArrayAccess
|
||||
*/
|
||||
public string|null $inherited = null;
|
||||
|
||||
/** @var class-string|null */
|
||||
public string|null $originalClass = null;
|
||||
public string|null $originalField = null;
|
||||
public bool|null $quoted = null;
|
||||
@@ -92,6 +95,7 @@ final class FieldMapping implements ArrayAccess
|
||||
* length?: int|null,
|
||||
* id?: bool|null,
|
||||
* nullable?: bool|null,
|
||||
* index?: bool|null,
|
||||
* notInsertable?: bool|null,
|
||||
* notUpdatable?: bool|null,
|
||||
* columnDefinition?: string|null,
|
||||
@@ -101,7 +105,7 @@ final class FieldMapping implements ArrayAccess
|
||||
* scale?: int|null,
|
||||
* unique?: bool|null,
|
||||
* inherited?: string|null,
|
||||
* originalClass?: string|null,
|
||||
* originalClass?: class-string|null,
|
||||
* originalField?: string|null,
|
||||
* quoted?: bool|null,
|
||||
* declared?: string|null,
|
||||
|
||||
@@ -13,6 +13,7 @@ final class JoinColumnMapping implements ArrayAccess
|
||||
{
|
||||
use ArrayAccessImplementation;
|
||||
|
||||
public bool|null $deferrable = null;
|
||||
public bool|null $unique = null;
|
||||
public bool|null $quoted = null;
|
||||
public string|null $fieldName = null;
|
||||
@@ -33,7 +34,7 @@ final class JoinColumnMapping implements ArrayAccess
|
||||
* @param array<string, mixed> $mappingArray
|
||||
* @phpstan-param array{
|
||||
* name: string,
|
||||
* referencedColumnName: string,
|
||||
* referencedColumnName: string|null,
|
||||
* unique?: bool|null,
|
||||
* quoted?: bool|null,
|
||||
* fieldName?: string|null,
|
||||
@@ -66,7 +67,7 @@ final class JoinColumnMapping implements ArrayAccess
|
||||
}
|
||||
}
|
||||
|
||||
foreach (['unique', 'quoted', 'nullable'] as $boolKey) {
|
||||
foreach (['deferrable', 'unique', 'quoted', 'nullable'] as $boolKey) {
|
||||
if ($this->$boolKey !== null) {
|
||||
$serialized[] = $boolKey;
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ trait JoinColumnProperties
|
||||
/** @param array<string, mixed> $options */
|
||||
public function __construct(
|
||||
public readonly string|null $name = null,
|
||||
public readonly string $referencedColumnName = 'id',
|
||||
public readonly string|null $referencedColumnName = null,
|
||||
public readonly bool $deferrable = false,
|
||||
public readonly bool $unique = false,
|
||||
public readonly bool $nullable = true,
|
||||
public readonly bool|null $nullable = null,
|
||||
public readonly mixed $onDelete = null,
|
||||
public readonly string|null $columnDefinition = null,
|
||||
public readonly string|null $fieldName = null,
|
||||
|
||||
170
src/Mapping/LegacyReflectionFields.php
Normal file
170
src/Mapping/LegacyReflectionFields.php
Normal file
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Mapping;
|
||||
|
||||
use ArrayAccess;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\Persistence\Mapping\ReflectionService;
|
||||
use Doctrine\Persistence\Reflection\EnumReflectionProperty;
|
||||
use Generator;
|
||||
use IteratorAggregate;
|
||||
use OutOfBoundsException;
|
||||
use ReflectionProperty;
|
||||
use Traversable;
|
||||
|
||||
use function array_keys;
|
||||
use function assert;
|
||||
use function is_string;
|
||||
use function str_contains;
|
||||
use function str_replace;
|
||||
|
||||
/**
|
||||
* @template-implements ArrayAccess<string, ReflectionProperty|null>
|
||||
* @template-implements IteratorAggregate<string, ReflectionProperty|null>
|
||||
*/
|
||||
class LegacyReflectionFields implements ArrayAccess, IteratorAggregate
|
||||
{
|
||||
/** @var array<string, ReflectionProperty|null> */
|
||||
private array $reflFields = [];
|
||||
|
||||
public function __construct(private ClassMetadata $classMetadata, private ReflectionService $reflectionService)
|
||||
{
|
||||
}
|
||||
|
||||
/** @param string $offset */
|
||||
public function offsetExists($offset): bool // phpcs:ignore
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11659',
|
||||
'Access to ClassMetadata::$reflFields is deprecated and will be removed in Doctrine ORM 4.0.',
|
||||
);
|
||||
|
||||
return isset($this->classMetadata->propertyAccessors[$offset]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
*
|
||||
* @psalm-suppress LessSpecificImplementedReturnType
|
||||
*/
|
||||
public function offsetGet($field): mixed // phpcs:ignore
|
||||
{
|
||||
if (isset($this->reflFields[$field])) {
|
||||
return $this->reflFields[$field];
|
||||
}
|
||||
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11659',
|
||||
'Access to ClassMetadata::$reflFields is deprecated and will be removed in Doctrine ORM 4.0.',
|
||||
);
|
||||
|
||||
if (isset($this->classMetadata->propertyAccessors[$field])) {
|
||||
$fieldName = str_contains($field, '.') ? $this->classMetadata->fieldMappings[$field]->originalField : $field;
|
||||
$className = $this->classMetadata->name;
|
||||
|
||||
assert(is_string($fieldName));
|
||||
|
||||
if (isset($this->classMetadata->fieldMappings[$field]) && $this->classMetadata->fieldMappings[$field]->originalClass !== null) {
|
||||
$className = $this->classMetadata->fieldMappings[$field]->originalClass;
|
||||
} elseif (isset($this->classMetadata->fieldMappings[$field]) && $this->classMetadata->fieldMappings[$field]->declared !== null) {
|
||||
$className = $this->classMetadata->fieldMappings[$field]->declared;
|
||||
} elseif (isset($this->classMetadata->associationMappings[$field]) && $this->classMetadata->associationMappings[$field]->declared !== null) {
|
||||
$className = $this->classMetadata->associationMappings[$field]->declared;
|
||||
} elseif (isset($this->classMetadata->embeddedClasses[$field]) && $this->classMetadata->embeddedClasses[$field]->declared !== null) {
|
||||
$className = $this->classMetadata->embeddedClasses[$field]->declared;
|
||||
}
|
||||
|
||||
/** @psalm-suppress ArgumentTypeCoercion */
|
||||
$this->reflFields[$field] = $this->getAccessibleProperty($className, $fieldName);
|
||||
|
||||
if (isset($this->classMetadata->fieldMappings[$field])) {
|
||||
if ($this->classMetadata->fieldMappings[$field]->enumType !== null) {
|
||||
$this->reflFields[$field] = new EnumReflectionProperty(
|
||||
$this->reflFields[$field],
|
||||
$this->classMetadata->fieldMappings[$field]->enumType,
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->classMetadata->fieldMappings[$field]->originalField !== null) {
|
||||
$parentField = str_replace('.' . $fieldName, '', $field);
|
||||
$originalClass = $this->classMetadata->fieldMappings[$field]->originalClass;
|
||||
|
||||
if (! str_contains($parentField, '.')) {
|
||||
$parentClass = $this->classMetadata->name;
|
||||
} else {
|
||||
$parentClass = $this->classMetadata->fieldMappings[$parentField]->originalClass;
|
||||
}
|
||||
|
||||
/** @psalm-var class-string $parentClass */
|
||||
/** @psalm-var class-string $originalClass */
|
||||
|
||||
$this->reflFields[$field] = new ReflectionEmbeddedProperty(
|
||||
$this->getAccessibleProperty($parentClass, $parentField),
|
||||
$this->reflFields[$field],
|
||||
$originalClass,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->reflFields[$field];
|
||||
}
|
||||
|
||||
throw new OutOfBoundsException('Unknown field: ' . $this->classMetadata->name . ' ::$' . $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $offset
|
||||
* @param ReflectionProperty $value
|
||||
*/
|
||||
public function offsetSet($offset, $value): void // phpcs:ignore
|
||||
{
|
||||
$this->reflFields[$offset] = $value;
|
||||
}
|
||||
|
||||
/** @param string $offset */
|
||||
public function offsetUnset($offset): void // phpcs:ignore
|
||||
{
|
||||
unset($this->reflFields[$offset]);
|
||||
}
|
||||
|
||||
/** @psalm-param class-string $class */
|
||||
private function getAccessibleProperty(string $class, string $field): ReflectionProperty
|
||||
{
|
||||
$reflectionProperty = $this->reflectionService->getAccessibleProperty($class, $field);
|
||||
|
||||
assert($reflectionProperty !== null);
|
||||
|
||||
if ($reflectionProperty->isReadOnly()) {
|
||||
$declaringClass = $reflectionProperty->class;
|
||||
if ($declaringClass !== $class) {
|
||||
$reflectionProperty = $this->reflectionService->getAccessibleProperty($declaringClass, $field);
|
||||
|
||||
assert($reflectionProperty !== null);
|
||||
}
|
||||
|
||||
$reflectionProperty = new ReflectionReadonlyProperty($reflectionProperty);
|
||||
}
|
||||
|
||||
return $reflectionProperty;
|
||||
}
|
||||
|
||||
/** @return Generator<string, ReflectionProperty> */
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11659',
|
||||
'Access to ClassMetadata::$reflFields is deprecated and will be removed in Doctrine ORM 4.0.',
|
||||
);
|
||||
|
||||
$keys = array_keys($this->classMetadata->propertyAccessors);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
yield $key => $this->offsetGet($key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,10 +61,14 @@ final class ManyToManyOwningSideMapping extends ToManyOwningSideMapping implemen
|
||||
{
|
||||
if (isset($mappingArray['joinTable']['joinColumns'])) {
|
||||
foreach ($mappingArray['joinTable']['joinColumns'] as $key => $joinColumn) {
|
||||
if (empty($joinColumn['referencedColumnName'])) {
|
||||
$mappingArray['joinTable']['joinColumns'][$key]['referencedColumnName'] = $namingStrategy->referenceColumnName();
|
||||
}
|
||||
|
||||
if (empty($joinColumn['name'])) {
|
||||
$mappingArray['joinTable']['joinColumns'][$key]['name'] = $namingStrategy->joinKeyColumnName(
|
||||
$mappingArray['sourceEntity'],
|
||||
$joinColumn['referencedColumnName'] ?? null,
|
||||
$joinColumn['referencedColumnName'] ?? $namingStrategy->referenceColumnName(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -72,10 +76,14 @@ final class ManyToManyOwningSideMapping extends ToManyOwningSideMapping implemen
|
||||
|
||||
if (isset($mappingArray['joinTable']['inverseJoinColumns'])) {
|
||||
foreach ($mappingArray['joinTable']['inverseJoinColumns'] as $key => $joinColumn) {
|
||||
if (empty($joinColumn['referencedColumnName'])) {
|
||||
$mappingArray['joinTable']['inverseJoinColumns'][$key]['referencedColumnName'] = $namingStrategy->referenceColumnName();
|
||||
}
|
||||
|
||||
if (empty($joinColumn['name'])) {
|
||||
$mappingArray['joinTable']['inverseJoinColumns'][$key]['name'] = $namingStrategy->joinKeyColumnName(
|
||||
$mappingArray['targetEntity'],
|
||||
$joinColumn['referencedColumnName'] ?? null,
|
||||
$joinColumn['referencedColumnName'] ?? $namingStrategy->referenceColumnName(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -119,6 +127,8 @@ final class ManyToManyOwningSideMapping extends ToManyOwningSideMapping implemen
|
||||
$mapping->joinTableColumns = [];
|
||||
|
||||
foreach ($mapping->joinTable->joinColumns as $joinColumn) {
|
||||
$joinColumn->nullable = false;
|
||||
|
||||
if (empty($joinColumn->referencedColumnName)) {
|
||||
$joinColumn->referencedColumnName = $namingStrategy->referenceColumnName();
|
||||
}
|
||||
@@ -142,6 +152,8 @@ final class ManyToManyOwningSideMapping extends ToManyOwningSideMapping implemen
|
||||
}
|
||||
|
||||
foreach ($mapping->joinTable->inverseJoinColumns as $inverseJoinColumn) {
|
||||
$inverseJoinColumn->nullable = false;
|
||||
|
||||
if (empty($inverseJoinColumn->referencedColumnName)) {
|
||||
$inverseJoinColumn->referencedColumnName = $namingStrategy->referenceColumnName();
|
||||
}
|
||||
|
||||
@@ -688,4 +688,13 @@ EXCEPTION
|
||||
$entityName,
|
||||
));
|
||||
}
|
||||
|
||||
public static function mappingVirtualPropertyNotAllowed(string $entityName, string $propertyName): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Mapping virtual property "%s" on entity "%s" is not allowed.',
|
||||
$propertyName,
|
||||
$entityName,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
53
src/Mapping/PropertyAccessors/EmbeddablePropertyAccessor.php
Normal file
53
src/Mapping/PropertyAccessors/EmbeddablePropertyAccessor.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Mapping\PropertyAccessors;
|
||||
|
||||
use Doctrine\Instantiator\Instantiator;
|
||||
use ReflectionProperty;
|
||||
|
||||
/** @internal */
|
||||
class EmbeddablePropertyAccessor implements PropertyAccessor
|
||||
{
|
||||
private static Instantiator|null $instantiator = null;
|
||||
|
||||
public function __construct(
|
||||
private PropertyAccessor $parent,
|
||||
private PropertyAccessor $child,
|
||||
/** @var class-string */
|
||||
private string $embeddedClass,
|
||||
) {
|
||||
}
|
||||
|
||||
public function setValue(object $object, mixed $value): void
|
||||
{
|
||||
$embeddedObject = $this->parent->getValue($object);
|
||||
|
||||
if ($embeddedObject === null) {
|
||||
self::$instantiator ??= new Instantiator();
|
||||
|
||||
$embeddedObject = self::$instantiator->instantiate($this->embeddedClass);
|
||||
|
||||
$this->parent->setValue($object, $embeddedObject);
|
||||
}
|
||||
|
||||
$this->child->setValue($embeddedObject, $value);
|
||||
}
|
||||
|
||||
public function getValue(object $object): mixed
|
||||
{
|
||||
$embeddedObject = $this->parent->getValue($object);
|
||||
|
||||
if ($embeddedObject === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->child->getValue($embeddedObject);
|
||||
}
|
||||
|
||||
public function getUnderlyingReflector(): ReflectionProperty
|
||||
{
|
||||
return $this->child->getUnderlyingReflector();
|
||||
}
|
||||
}
|
||||
85
src/Mapping/PropertyAccessors/EnumPropertyAccessor.php
Normal file
85
src/Mapping/PropertyAccessors/EnumPropertyAccessor.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Mapping\PropertyAccessors;
|
||||
|
||||
use BackedEnum;
|
||||
use ReflectionProperty;
|
||||
|
||||
use function array_map;
|
||||
use function is_array;
|
||||
use function reset;
|
||||
|
||||
/** @internal */
|
||||
class EnumPropertyAccessor implements PropertyAccessor
|
||||
{
|
||||
/** @param class-string<BackedEnum> $enumType */
|
||||
public function __construct(private PropertyAccessor $parent, private string $enumType)
|
||||
{
|
||||
}
|
||||
|
||||
public function setValue(object $object, mixed $value): void
|
||||
{
|
||||
if ($value !== null) {
|
||||
$value = $this->toEnum($value);
|
||||
}
|
||||
|
||||
$this->parent->setValue($object, $value);
|
||||
}
|
||||
|
||||
public function getValue(object $object): mixed
|
||||
{
|
||||
$enum = $this->parent->getValue($object);
|
||||
|
||||
if ($enum === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->fromEnum($enum);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param BackedEnum|BackedEnum[] $enum
|
||||
*
|
||||
* @return ($enum is BackedEnum ? (string|int) : (string[]|int[]))
|
||||
*/
|
||||
private function fromEnum($enum) // phpcs:ignore
|
||||
{
|
||||
if (is_array($enum)) {
|
||||
return array_map(static function (BackedEnum $enum) {
|
||||
return $enum->value;
|
||||
}, $enum);
|
||||
}
|
||||
|
||||
return $enum->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param BackedEnum|BackedEnum[]|int|string|int[]|string[] $value
|
||||
*
|
||||
* @return ($value is int|string|BackedEnum ? BackedEnum : BackedEnum[])
|
||||
*/
|
||||
private function toEnum($value): BackedEnum|array
|
||||
{
|
||||
if ($value instanceof BackedEnum) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$v = reset($value);
|
||||
if ($v instanceof BackedEnum) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return array_map([$this->enumType, 'from'], $value);
|
||||
}
|
||||
|
||||
return $this->enumType::from($value);
|
||||
}
|
||||
|
||||
public function getUnderlyingReflector(): ReflectionProperty
|
||||
{
|
||||
return $this->parent->getUnderlyingReflector();
|
||||
}
|
||||
}
|
||||
61
src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php
Normal file
61
src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Mapping\PropertyAccessors;
|
||||
|
||||
use Doctrine\ORM\Proxy\InternalProxy;
|
||||
use ReflectionProperty;
|
||||
|
||||
use function ltrim;
|
||||
|
||||
/** @internal */
|
||||
class ObjectCastPropertyAccessor implements PropertyAccessor
|
||||
{
|
||||
/** @param class-string $class */
|
||||
public static function fromNames(string $class, string $name): self
|
||||
{
|
||||
$reflectionProperty = new ReflectionProperty($class, $name);
|
||||
|
||||
$key = $reflectionProperty->isPrivate() ? "\0" . ltrim($class, '\\') . "\0" . $name : ($reflectionProperty->isProtected() ? "\0*\0" . $name : $name);
|
||||
|
||||
return new self($reflectionProperty, $key);
|
||||
}
|
||||
|
||||
public static function fromReflectionProperty(ReflectionProperty $reflectionProperty): self
|
||||
{
|
||||
$name = $reflectionProperty->getName();
|
||||
$key = $reflectionProperty->isPrivate() ? "\0" . ltrim($reflectionProperty->getDeclaringClass()->getName(), '\\') . "\0" . $name : ($reflectionProperty->isProtected() ? "\0*\0" . $name : $name);
|
||||
|
||||
return new self($reflectionProperty, $key);
|
||||
}
|
||||
|
||||
private function __construct(private ReflectionProperty $reflectionProperty, private string $key)
|
||||
{
|
||||
}
|
||||
|
||||
public function setValue(object $object, mixed $value): void
|
||||
{
|
||||
if (! ($object instanceof InternalProxy && ! $object->__isInitialized())) {
|
||||
$this->reflectionProperty->setValue($object, $value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$object->__setInitialized(true);
|
||||
|
||||
$this->reflectionProperty->setValue($object, $value);
|
||||
|
||||
$object->__setInitialized(false);
|
||||
}
|
||||
|
||||
public function getValue(object $object): mixed
|
||||
{
|
||||
return ((array) $object)[$this->key] ?? null;
|
||||
}
|
||||
|
||||
public function getUnderlyingReflector(): ReflectionProperty
|
||||
{
|
||||
return $this->reflectionProperty;
|
||||
}
|
||||
}
|
||||
27
src/Mapping/PropertyAccessors/PropertyAccessor.php
Normal file
27
src/Mapping/PropertyAccessors/PropertyAccessor.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Mapping\PropertyAccessors;
|
||||
|
||||
use ReflectionProperty;
|
||||
|
||||
/**
|
||||
* A property accessor is a class that allows to read and write properties on objects regardless of visibility.
|
||||
*
|
||||
* We use them while creating objects from database rows in {@link UnitOfWork::createEntity()} or when
|
||||
* computing changesets from objects that are about to be written back to the database in {@link UnitOfWork::computeChangeSet()}.
|
||||
*
|
||||
* This abstraction over ReflectionProperty is necessary, because for several features of either Doctrine or PHP, we
|
||||
* need to handle edge cases in reflection at a central location in the code.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface PropertyAccessor
|
||||
{
|
||||
public function setValue(object $object, mixed $value): void;
|
||||
|
||||
public function getValue(object $object): mixed;
|
||||
|
||||
public function getUnderlyingReflector(): ReflectionProperty;
|
||||
}
|
||||
32
src/Mapping/PropertyAccessors/PropertyAccessorFactory.php
Normal file
32
src/Mapping/PropertyAccessors/PropertyAccessorFactory.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Mapping\PropertyAccessors;
|
||||
|
||||
use ReflectionProperty;
|
||||
|
||||
use const PHP_VERSION_ID;
|
||||
|
||||
class PropertyAccessorFactory
|
||||
{
|
||||
/** @phpstan-param class-string $className */
|
||||
public static function createPropertyAccessor(string $className, string $propertyName): PropertyAccessor
|
||||
{
|
||||
$reflectionProperty = new ReflectionProperty($className, $propertyName);
|
||||
|
||||
$accessor = PHP_VERSION_ID >= 80400
|
||||
? RawValuePropertyAccessor::fromReflectionProperty($reflectionProperty)
|
||||
: ObjectCastPropertyAccessor::fromReflectionProperty($reflectionProperty);
|
||||
|
||||
if ($reflectionProperty->hasType() && ! $reflectionProperty->getType()->allowsNull()) {
|
||||
$accessor = new TypedNoDefaultPropertyAccessor($accessor, $reflectionProperty);
|
||||
}
|
||||
|
||||
if ($reflectionProperty->isReadOnly()) {
|
||||
$accessor = new ReadonlyAccessor($accessor, $reflectionProperty);
|
||||
}
|
||||
|
||||
return $accessor;
|
||||
}
|
||||
}
|
||||
63
src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php
Normal file
63
src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Mapping\PropertyAccessors;
|
||||
|
||||
use Doctrine\ORM\Proxy\InternalProxy;
|
||||
use LogicException;
|
||||
use ReflectionProperty;
|
||||
|
||||
use function ltrim;
|
||||
|
||||
use const PHP_VERSION_ID;
|
||||
|
||||
/**
|
||||
* This is a PHP 8.4 and up only class and replaces ObjectCastPropertyAccessor.
|
||||
*
|
||||
* It works based on the raw values of a property, which for a case of property hooks
|
||||
* is the backed value. If we kept using setValue/getValue, this would go through the hooks,
|
||||
* which potentially change the data.
|
||||
*/
|
||||
class RawValuePropertyAccessor implements PropertyAccessor
|
||||
{
|
||||
public static function fromReflectionProperty(ReflectionProperty $reflectionProperty): self
|
||||
{
|
||||
$name = $reflectionProperty->getName();
|
||||
$key = $reflectionProperty->isPrivate() ? "\0" . ltrim($reflectionProperty->getDeclaringClass()->getName(), '\\') . "\0" . $name : ($reflectionProperty->isProtected() ? "\0*\0" . $name : $name);
|
||||
|
||||
return new self($reflectionProperty, $key);
|
||||
}
|
||||
|
||||
private function __construct(private ReflectionProperty $reflectionProperty, private string $key)
|
||||
{
|
||||
if (PHP_VERSION_ID < 80400) {
|
||||
throw new LogicException('This class requires PHP 8.4 or higher.');
|
||||
}
|
||||
}
|
||||
|
||||
public function setValue(object $object, mixed $value): void
|
||||
{
|
||||
if (! ($object instanceof InternalProxy && ! $object->__isInitialized())) {
|
||||
$this->reflectionProperty->setRawValueWithoutLazyInitialization($object, $value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$object->__setInitialized(true);
|
||||
|
||||
$this->reflectionProperty->setRawValue($object, $value);
|
||||
|
||||
$object->__setInitialized(false);
|
||||
}
|
||||
|
||||
public function getValue(object $object): mixed
|
||||
{
|
||||
return ((array) $object)[$this->key] ?? null;
|
||||
}
|
||||
|
||||
public function getUnderlyingReflector(): ReflectionProperty
|
||||
{
|
||||
return $this->reflectionProperty;
|
||||
}
|
||||
}
|
||||
53
src/Mapping/PropertyAccessors/ReadonlyAccessor.php
Normal file
53
src/Mapping/PropertyAccessors/ReadonlyAccessor.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Mapping\PropertyAccessors;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use ReflectionProperty;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/** @internal */
|
||||
class ReadonlyAccessor implements PropertyAccessor
|
||||
{
|
||||
public function __construct(private PropertyAccessor $parent, private ReflectionProperty $reflectionProperty)
|
||||
{
|
||||
if (! $this->reflectionProperty->isReadOnly()) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'%s::$%s must be readonly property',
|
||||
$this->reflectionProperty->getDeclaringClass()->getName(),
|
||||
$this->reflectionProperty->getName(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public function setValue(object $object, mixed $value): void
|
||||
{
|
||||
if (! $this->reflectionProperty->isInitialized($object)) {
|
||||
$this->parent->setValue($object, $value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->parent->getValue($object) !== $value) {
|
||||
throw new LogicException(sprintf(
|
||||
'Attempting to change readonly property %s::$%s.',
|
||||
$this->reflectionProperty->getDeclaringClass()->getName(),
|
||||
$this->reflectionProperty->getName(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public function getValue(object $object): mixed
|
||||
{
|
||||
return $this->parent->getValue($object);
|
||||
}
|
||||
|
||||
public function getUnderlyingReflector(): ReflectionProperty
|
||||
{
|
||||
return $this->reflectionProperty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Mapping\PropertyAccessors;
|
||||
|
||||
use Closure;
|
||||
use InvalidArgumentException;
|
||||
use ReflectionProperty;
|
||||
|
||||
use function assert;
|
||||
use function sprintf;
|
||||
|
||||
/** @internal */
|
||||
class TypedNoDefaultPropertyAccessor implements PropertyAccessor
|
||||
{
|
||||
private Closure|null $unsetter = null;
|
||||
|
||||
public function __construct(private PropertyAccessor $parent, private ReflectionProperty $reflectionProperty)
|
||||
{
|
||||
if (! $this->reflectionProperty->hasType()) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'%s::$%s must have a type when used with TypedNoDefaultPropertyAccessor',
|
||||
$this->reflectionProperty->getDeclaringClass()->getName(),
|
||||
$this->reflectionProperty->getName(),
|
||||
));
|
||||
}
|
||||
|
||||
if ($this->reflectionProperty->getType()->allowsNull()) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'%s::$%s must not be nullable when used with TypedNoDefaultPropertyAccessor',
|
||||
$this->reflectionProperty->getDeclaringClass()->getName(),
|
||||
$this->reflectionProperty->getName(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public function setValue(object $object, mixed $value): void
|
||||
{
|
||||
if ($value === null) {
|
||||
if ($this->unsetter === null) {
|
||||
$propertyName = $this->reflectionProperty->getName();
|
||||
$this->unsetter = function () use ($propertyName): void {
|
||||
unset($this->$propertyName);
|
||||
};
|
||||
}
|
||||
|
||||
$unsetter = $this->unsetter->bindTo($object, $this->reflectionProperty->getDeclaringClass()->getName());
|
||||
|
||||
assert($unsetter instanceof Closure);
|
||||
|
||||
$unsetter();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->parent->setValue($object, $value);
|
||||
}
|
||||
|
||||
public function getValue(object $object): mixed
|
||||
{
|
||||
return $this->reflectionProperty->isInitialized($object) ? $this->parent->getValue($object) : null;
|
||||
}
|
||||
|
||||
public function getUnderlyingReflector(): ReflectionProperty
|
||||
{
|
||||
return $this->reflectionProperty;
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,10 @@ abstract class ToOneOwningSideMapping extends OwningSideMapping implements ToOne
|
||||
if (empty($joinColumn['name'])) {
|
||||
$mappingArray['joinColumns'][$index]['name'] = $namingStrategy->joinColumnName($mappingArray['fieldName'], $name);
|
||||
}
|
||||
|
||||
if (empty($joinColumn['referencedColumnName'])) {
|
||||
$mappingArray['joinColumns'][$index]['referencedColumnName'] = $namingStrategy->referenceColumnName();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +130,12 @@ abstract class ToOneOwningSideMapping extends OwningSideMapping implements ToOne
|
||||
$uniqueConstraintColumns = [];
|
||||
|
||||
foreach ($mapping->joinColumns as $joinColumn) {
|
||||
if ($mapping->id) {
|
||||
$joinColumn->nullable = false;
|
||||
} elseif ($joinColumn->nullable === null) {
|
||||
$joinColumn->nullable = true;
|
||||
}
|
||||
|
||||
if ($mapping->isOneToOne() && ! $isInheritanceTypeSingleTable) {
|
||||
if (count($mapping->joinColumns) === 1) {
|
||||
if (empty($mapping->id)) {
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM;
|
||||
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
|
||||
use Doctrine\ORM\Mapping\Driver\XmlDriver;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
@@ -20,6 +21,8 @@ use function extension_loaded;
|
||||
use function md5;
|
||||
use function sys_get_temp_dir;
|
||||
|
||||
use const PHP_VERSION_ID;
|
||||
|
||||
final class ORMSetup
|
||||
{
|
||||
/**
|
||||
@@ -33,12 +36,39 @@ final class ORMSetup
|
||||
string|null $proxyDir = null,
|
||||
CacheItemPoolInterface|null $cache = null,
|
||||
): Configuration {
|
||||
if (PHP_VERSION_ID >= 80400) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/12005',
|
||||
'%s is deprecated in favor of %s, and will be removed in 4.0.',
|
||||
__METHOD__,
|
||||
self::class . '::createAttributeMetadataConfig()',
|
||||
);
|
||||
}
|
||||
|
||||
$config = self::createConfiguration($isDevMode, $proxyDir, $cache);
|
||||
$config->setMetadataDriverImpl(new AttributeDriver($paths));
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a configuration with an attribute metadata driver.
|
||||
*
|
||||
* @param string[] $paths
|
||||
*/
|
||||
public static function createAttributeMetadataConfig(
|
||||
array $paths,
|
||||
bool $isDevMode = false,
|
||||
string|null $cacheNamespaceSeed = null,
|
||||
CacheItemPoolInterface|null $cache = null,
|
||||
): Configuration {
|
||||
$config = self::createConfig($isDevMode, $cacheNamespaceSeed, $cache);
|
||||
$config->setMetadataDriverImpl(new AttributeDriver($paths));
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a configuration with an XML metadata driver.
|
||||
*
|
||||
@@ -51,12 +81,44 @@ final class ORMSetup
|
||||
CacheItemPoolInterface|null $cache = null,
|
||||
bool $isXsdValidationEnabled = true,
|
||||
): Configuration {
|
||||
if (PHP_VERSION_ID >= 80400) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/12005',
|
||||
'%s is deprecated in favor of %s, and will be removed in 4.0.',
|
||||
__METHOD__,
|
||||
self::class . '::createXMLMetadataConfig()',
|
||||
);
|
||||
}
|
||||
|
||||
$config = self::createConfiguration($isDevMode, $proxyDir, $cache);
|
||||
$config->setMetadataDriverImpl(new XmlDriver($paths, XmlDriver::DEFAULT_FILE_EXTENSION, $isXsdValidationEnabled));
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a configuration with an XML metadata driver.
|
||||
*
|
||||
* @param string[] $paths
|
||||
*/
|
||||
public static function createXMLMetadataConfig(
|
||||
array $paths,
|
||||
bool $isDevMode = false,
|
||||
string|null $cacheNamespaceSeed = null,
|
||||
CacheItemPoolInterface|null $cache = null,
|
||||
bool $isXsdValidationEnabled = true,
|
||||
): Configuration {
|
||||
$config = self::createConfig($isDevMode, $cacheNamespaceSeed, $cache);
|
||||
$config->setMetadataDriverImpl(new XmlDriver(
|
||||
$paths,
|
||||
XmlDriver::DEFAULT_FILE_EXTENSION,
|
||||
$isXsdValidationEnabled,
|
||||
));
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a configuration without a metadata driver.
|
||||
*/
|
||||
@@ -65,6 +127,16 @@ final class ORMSetup
|
||||
string|null $proxyDir = null,
|
||||
CacheItemPoolInterface|null $cache = null,
|
||||
): Configuration {
|
||||
if (PHP_VERSION_ID >= 80400 && $proxyDir !== null) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/12005',
|
||||
'%s is deprecated in favor of %s, and will be removed in 4.0.',
|
||||
__METHOD__,
|
||||
self::class . '::createConfig()',
|
||||
);
|
||||
}
|
||||
|
||||
$proxyDir = $proxyDir ?: sys_get_temp_dir();
|
||||
|
||||
$cache = self::createCacheInstance($isDevMode, $proxyDir, $cache);
|
||||
@@ -81,9 +153,23 @@ final class ORMSetup
|
||||
return $config;
|
||||
}
|
||||
|
||||
public static function createConfig(
|
||||
bool $isDevMode = false,
|
||||
string|null $cacheNamespaceSeed = null,
|
||||
CacheItemPoolInterface|null $cache = null,
|
||||
): Configuration {
|
||||
$cache = self::createCacheInstance($isDevMode, $cacheNamespaceSeed, $cache);
|
||||
$config = new Configuration();
|
||||
$config->setMetadataCache($cache);
|
||||
$config->setQueryCache($cache);
|
||||
$config->setResultCache($cache);
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
private static function createCacheInstance(
|
||||
bool $isDevMode,
|
||||
string $proxyDir,
|
||||
string|null $cacheNamespaceSeed,
|
||||
CacheItemPoolInterface|null $cache,
|
||||
): CacheItemPoolInterface {
|
||||
if ($cache !== null) {
|
||||
@@ -101,7 +187,7 @@ final class ORMSetup
|
||||
return new ArrayAdapter();
|
||||
}
|
||||
|
||||
$namespace = 'dc2_' . md5($proxyDir);
|
||||
$namespace = 'dc2_' . md5($cacheNamespaceSeed ?? 'default');
|
||||
|
||||
if (extension_loaded('apcu') && apcu_enabled()) {
|
||||
return new ApcuAdapter($namespace);
|
||||
|
||||
@@ -140,7 +140,7 @@ final class PersistentCollection extends AbstractLazyCollection implements Selec
|
||||
if ($this->backRefFieldName && $this->getMapping()->isOneToMany()) {
|
||||
assert($this->typeClass !== null);
|
||||
// Set back reference to owner
|
||||
$this->typeClass->reflFields[$this->backRefFieldName]->setValue(
|
||||
$this->typeClass->propertyAccessors[$this->backRefFieldName]->setValue(
|
||||
$element,
|
||||
$this->owner,
|
||||
);
|
||||
@@ -166,7 +166,7 @@ final class PersistentCollection extends AbstractLazyCollection implements Selec
|
||||
if ($this->backRefFieldName && $this->getMapping()->isOneToMany()) {
|
||||
assert($this->typeClass !== null);
|
||||
// Set back reference to owner
|
||||
$this->typeClass->reflFields[$this->backRefFieldName]->setValue(
|
||||
$this->typeClass->propertyAccessors[$this->backRefFieldName]->setValue(
|
||||
$element,
|
||||
$this->owner,
|
||||
);
|
||||
|
||||
@@ -153,12 +153,6 @@ class BasicEntityPersister implements EntityPersister
|
||||
*/
|
||||
protected array $quotedColumns = [];
|
||||
|
||||
/**
|
||||
* The INSERT SQL statement used for entities handled by this persister.
|
||||
* This SQL is only generated once per request, if at all.
|
||||
*/
|
||||
private string|null $insertSql = null;
|
||||
|
||||
/**
|
||||
* The quote strategy.
|
||||
*/
|
||||
@@ -201,6 +195,16 @@ class BasicEntityPersister implements EntityPersister
|
||||
);
|
||||
}
|
||||
|
||||
final protected function isFilterHashUpToDate(): bool
|
||||
{
|
||||
return $this->filterHash === $this->em->getFilters()->getHash();
|
||||
}
|
||||
|
||||
final protected function updateFilterHash(): void
|
||||
{
|
||||
$this->filterHash = $this->em->getFilters()->getHash();
|
||||
}
|
||||
|
||||
public function getClassMetadata(): ClassMetadata
|
||||
{
|
||||
return $this->class;
|
||||
@@ -263,8 +267,8 @@ class BasicEntityPersister implements EntityPersister
|
||||
$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
|
||||
// Unset this queued insert, so that the prepareUpdateData() method (called via prepareInsertData() 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
|
||||
@@ -470,7 +474,7 @@ class BasicEntityPersister implements EntityPersister
|
||||
|
||||
$where[] = $versionColumn;
|
||||
$types[] = $this->class->fieldMappings[$versionField]->type;
|
||||
$params[] = $this->class->reflFields[$versionField]->getValue($entity);
|
||||
$params[] = $this->class->propertyAccessors[$versionField]->getValue($entity);
|
||||
|
||||
switch ($versionFieldType) {
|
||||
case Types::SMALLINT:
|
||||
@@ -683,11 +687,11 @@ class BasicEntityPersister implements EntityPersister
|
||||
$targetColumn = $joinColumn->referencedColumnName;
|
||||
$quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
|
||||
|
||||
$this->quotedColumns[$sourceColumn] = $quotedColumn;
|
||||
$this->columnTypes[$sourceColumn] = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em);
|
||||
$result[$owningTable][$sourceColumn] = $newValId
|
||||
? $newValId[$targetClass->getFieldForColumn($targetColumn)]
|
||||
: null;
|
||||
$this->quotedColumns[$sourceColumn] = $quotedColumn;
|
||||
$this->columnTypes[$sourceColumn] = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em);
|
||||
|
||||
$newValue = $newValId ? $newValId[$targetClass->getFieldForColumn($targetColumn)] : null;
|
||||
$result[$owningTable][$sourceColumn] = $newValue instanceof BackedEnum ? $newValue->value : $newValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -781,7 +785,7 @@ class BasicEntityPersister implements EntityPersister
|
||||
|
||||
// Complete bidirectional association, if necessary
|
||||
if ($targetEntity !== null && $isInverseSingleValued) {
|
||||
$targetClass->reflFields[$assoc->inversedBy]->setValue($targetEntity, $sourceEntity);
|
||||
$targetClass->propertyAccessors[$assoc->inversedBy]->setValue($targetEntity, $sourceEntity);
|
||||
}
|
||||
|
||||
return $targetEntity;
|
||||
@@ -828,7 +832,7 @@ class BasicEntityPersister implements EntityPersister
|
||||
}
|
||||
} else {
|
||||
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
|
||||
$sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
|
||||
$sourceClass->propertyAccessors[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1045,7 +1049,7 @@ class BasicEntityPersister implements EntityPersister
|
||||
switch (true) {
|
||||
case $sourceClass->containsForeignIdentifier:
|
||||
$field = $sourceClass->getFieldForColumn($sourceKeyColumn);
|
||||
$value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
|
||||
$value = $sourceClass->propertyAccessors[$field]->getValue($sourceEntity);
|
||||
|
||||
if (isset($sourceClass->associationMappings[$field])) {
|
||||
$value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
|
||||
@@ -1056,7 +1060,7 @@ class BasicEntityPersister implements EntityPersister
|
||||
|
||||
case isset($sourceClass->fieldNames[$sourceKeyColumn]):
|
||||
$field = $sourceClass->fieldNames[$sourceKeyColumn];
|
||||
$value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
|
||||
$value = $sourceClass->propertyAccessors[$field]->getValue($sourceEntity);
|
||||
|
||||
break;
|
||||
|
||||
@@ -1231,7 +1235,7 @@ class BasicEntityPersister implements EntityPersister
|
||||
*/
|
||||
protected function getSelectColumnsSQL(): string
|
||||
{
|
||||
if ($this->currentPersisterContext->selectColumnListSql !== null && $this->filterHash === $this->em->getFilters()->getHash()) {
|
||||
if ($this->currentPersisterContext->selectColumnListSql !== null && $this->isFilterHashUpToDate()) {
|
||||
return $this->currentPersisterContext->selectColumnListSql;
|
||||
}
|
||||
|
||||
@@ -1347,7 +1351,7 @@ class BasicEntityPersister implements EntityPersister
|
||||
}
|
||||
|
||||
$this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
|
||||
$this->filterHash = $this->em->getFilters()->getHash();
|
||||
$this->updateFilterHash();
|
||||
|
||||
return $this->currentPersisterContext->selectColumnListSql;
|
||||
}
|
||||
@@ -1408,22 +1412,17 @@ class BasicEntityPersister implements EntityPersister
|
||||
|
||||
public function getInsertSQL(): string
|
||||
{
|
||||
if ($this->insertSql !== null) {
|
||||
return $this->insertSql;
|
||||
}
|
||||
|
||||
$columns = $this->getInsertColumnList();
|
||||
$tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
|
||||
|
||||
if (empty($columns)) {
|
||||
$identityColumn = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform);
|
||||
$this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
|
||||
if ($columns === []) {
|
||||
$identityColumn = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform);
|
||||
|
||||
return $this->insertSql;
|
||||
return $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
|
||||
}
|
||||
|
||||
$values = [];
|
||||
$columns = array_unique($columns);
|
||||
$placeholders = [];
|
||||
$columns = array_unique($columns);
|
||||
|
||||
foreach ($columns as $column) {
|
||||
$placeholder = '?';
|
||||
@@ -1437,15 +1436,13 @@ class BasicEntityPersister implements EntityPersister
|
||||
$placeholder = $type->convertToDatabaseValueSQL('?', $this->platform);
|
||||
}
|
||||
|
||||
$values[] = $placeholder;
|
||||
$placeholders[] = $placeholder;
|
||||
}
|
||||
|
||||
$columns = implode(', ', $columns);
|
||||
$values = implode(', ', $values);
|
||||
$columns = implode(', ', $columns);
|
||||
$placeholders = implode(', ', $placeholders);
|
||||
|
||||
$this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $values);
|
||||
|
||||
return $this->insertSql;
|
||||
return sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $placeholders);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1460,7 +1457,7 @@ class BasicEntityPersister implements EntityPersister
|
||||
{
|
||||
$columns = [];
|
||||
|
||||
foreach ($this->class->reflFields as $name => $field) {
|
||||
foreach ($this->class->propertyAccessors as $name => $field) {
|
||||
if ($this->class->isVersioned && $this->class->versionField === $name) {
|
||||
continue;
|
||||
}
|
||||
@@ -1814,7 +1811,7 @@ class BasicEntityPersister implements EntityPersister
|
||||
foreach ($owningAssoc->targetToSourceKeyColumns as $sourceKeyColumn => $targetKeyColumn) {
|
||||
if ($sourceClass->containsForeignIdentifier) {
|
||||
$field = $sourceClass->getFieldForColumn($sourceKeyColumn);
|
||||
$value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
|
||||
$value = $sourceClass->propertyAccessors[$field]->getValue($sourceEntity);
|
||||
|
||||
if (isset($sourceClass->associationMappings[$field])) {
|
||||
$value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
|
||||
@@ -1832,7 +1829,7 @@ class BasicEntityPersister implements EntityPersister
|
||||
}
|
||||
|
||||
$field = $sourceClass->fieldNames[$sourceKeyColumn];
|
||||
$value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
|
||||
$value = $sourceClass->propertyAccessors[$field]->getValue($sourceEntity);
|
||||
|
||||
$criteria[$tableAlias . '.' . $targetKeyColumn] = $value;
|
||||
$parameters[] = [
|
||||
@@ -1954,6 +1951,7 @@ class BasicEntityPersister implements EntityPersister
|
||||
ParameterType::STRING => ArrayParameterType::STRING,
|
||||
ParameterType::INTEGER => ArrayParameterType::INTEGER,
|
||||
ParameterType::ASCII => ArrayParameterType::ASCII,
|
||||
ParameterType::BINARY => ArrayParameterType::BINARY,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
|
||||
// Execute all inserts. For each entity:
|
||||
// 1) Insert on root table
|
||||
// 2) Insert on sub tables
|
||||
foreach ($this->queuedInserts as $entity) {
|
||||
foreach ($this->queuedInserts as $key => $entity) {
|
||||
$insertData = $this->prepareInsertData($entity);
|
||||
|
||||
// Execute insert on root table
|
||||
@@ -179,9 +179,16 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
|
||||
if ($this->class->requiresFetchAfterChange) {
|
||||
$this->assignDefaultVersionAndUpsertableValues($entity, $id);
|
||||
}
|
||||
}
|
||||
|
||||
$this->queuedInserts = [];
|
||||
// Unset this queued insert, so that the prepareUpdateData() method (called via prepareInsertData() 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]);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(object $entity): void
|
||||
@@ -358,7 +365,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
|
||||
protected function getSelectColumnsSQL(): string
|
||||
{
|
||||
// Create the column list fragment only once
|
||||
if ($this->currentPersisterContext->selectColumnListSql !== null) {
|
||||
if ($this->currentPersisterContext->selectColumnListSql !== null && $this->isFilterHashUpToDate()) {
|
||||
return $this->currentPersisterContext->selectColumnListSql;
|
||||
}
|
||||
|
||||
@@ -445,6 +452,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
|
||||
}
|
||||
|
||||
$this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
|
||||
$this->updateFilterHash();
|
||||
|
||||
return $this->currentPersisterContext->selectColumnListSql;
|
||||
}
|
||||
@@ -459,7 +467,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
|
||||
? $this->class->getIdentifierColumnNames()
|
||||
: [];
|
||||
|
||||
foreach ($this->class->reflFields as $name => $field) {
|
||||
foreach ($this->class->propertyAccessors as $name => $field) {
|
||||
if (
|
||||
isset($this->class->fieldMappings[$name]->inherited)
|
||||
&& ! isset($this->class->fieldMappings[$name]->id)
|
||||
|
||||
@@ -35,7 +35,7 @@ class SingleTablePersister extends AbstractEntityInheritancePersister
|
||||
protected function getSelectColumnsSQL(): string
|
||||
{
|
||||
$columnList = [];
|
||||
if ($this->currentPersisterContext->selectColumnListSql !== null) {
|
||||
if ($this->currentPersisterContext->selectColumnListSql !== null && $this->isFilterHashUpToDate()) {
|
||||
return $this->currentPersisterContext->selectColumnListSql;
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ class SingleTablePersister extends AbstractEntityInheritancePersister
|
||||
}
|
||||
|
||||
$this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
|
||||
$this->updateFilterHash();
|
||||
|
||||
return $this->currentPersisterContext->selectColumnListSql;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Doctrine\ORM\Proxy;
|
||||
|
||||
use Closure;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
|
||||
use function file_exists;
|
||||
use function ltrim;
|
||||
@@ -15,6 +16,7 @@ use function strlen;
|
||||
use function substr;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use const PHP_VERSION_ID;
|
||||
|
||||
/**
|
||||
* Special Autoloader for Proxy classes, which are not PSR-0 compliant.
|
||||
@@ -34,6 +36,15 @@ final class Autoloader
|
||||
*/
|
||||
public static function resolveFile(string $proxyDir, string $proxyNamespace, string $className): string
|
||||
{
|
||||
if (PHP_VERSION_ID >= 80400) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/12005',
|
||||
'Class "%s" is deprecated. Use native lazy objects instead.',
|
||||
self::class,
|
||||
);
|
||||
}
|
||||
|
||||
if (! str_starts_with($className, $proxyNamespace)) {
|
||||
throw new NotAProxyClass($className, $proxyNamespace);
|
||||
}
|
||||
@@ -59,6 +70,15 @@ final class Autoloader
|
||||
string $proxyNamespace,
|
||||
Closure|null $notFoundCallback = null,
|
||||
): Closure {
|
||||
if (PHP_VERSION_ID >= 80400) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/12005',
|
||||
'Class "%s" is deprecated. Use native lazy objects instead.',
|
||||
self::class,
|
||||
);
|
||||
}
|
||||
|
||||
$proxyNamespace = ltrim($proxyNamespace, '\\');
|
||||
|
||||
$autoloader = /** @param class-string $className */ static function (string $className) use ($proxyDir, $proxyNamespace, $notFoundCallback): void {
|
||||
|
||||
@@ -4,12 +4,15 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Proxy;
|
||||
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\Persistence\Mapping\ProxyClassNameResolver;
|
||||
use Doctrine\Persistence\Proxy;
|
||||
|
||||
use function strrpos;
|
||||
use function substr;
|
||||
|
||||
use const PHP_VERSION_ID;
|
||||
|
||||
/**
|
||||
* Class-related functionality for objects that might or not be proxy objects
|
||||
* at the moment.
|
||||
@@ -18,6 +21,15 @@ final class DefaultProxyClassNameResolver implements ProxyClassNameResolver
|
||||
{
|
||||
public function resolveClassName(string $className): string
|
||||
{
|
||||
if (PHP_VERSION_ID >= 80400) {
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/12005',
|
||||
'Class "%s" is deprecated. Use native lazy objects instead.',
|
||||
self::class,
|
||||
);
|
||||
}
|
||||
|
||||
$pos = strrpos($className, '\\' . Proxy::MARKER . '\\');
|
||||
|
||||
if ($pos === false) {
|
||||
@@ -30,6 +42,15 @@ final class DefaultProxyClassNameResolver implements ProxyClassNameResolver
|
||||
/** @return class-string */
|
||||
public static function getClass(object $object): string
|
||||
{
|
||||
if (PHP_VERSION_ID >= 80400) {
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/12005',
|
||||
'Class "%s" is deprecated. Use native lazy objects instead.',
|
||||
self::class,
|
||||
);
|
||||
}
|
||||
|
||||
return (new self())->resolveClassName($object::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Doctrine\ORM\Proxy;
|
||||
|
||||
use Closure;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityNotFoundException;
|
||||
use Doctrine\ORM\ORMInvalidArgumentException;
|
||||
@@ -13,20 +14,24 @@ use Doctrine\ORM\UnitOfWork;
|
||||
use Doctrine\ORM\Utility\IdentifierFlattener;
|
||||
use Doctrine\Persistence\Mapping\ClassMetadata;
|
||||
use Doctrine\Persistence\Proxy;
|
||||
use LogicException;
|
||||
use ReflectionClass;
|
||||
use ReflectionProperty;
|
||||
use Symfony\Component\VarExporter\ProxyHelper;
|
||||
|
||||
use function array_combine;
|
||||
use function array_flip;
|
||||
use function array_intersect_key;
|
||||
use function array_keys;
|
||||
use function assert;
|
||||
use function bin2hex;
|
||||
use function chmod;
|
||||
use function class_exists;
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function file_exists;
|
||||
use function file_put_contents;
|
||||
use function filemtime;
|
||||
use function func_num_args;
|
||||
use function is_bool;
|
||||
use function is_dir;
|
||||
use function is_int;
|
||||
@@ -37,6 +42,7 @@ use function preg_match_all;
|
||||
use function random_bytes;
|
||||
use function rename;
|
||||
use function rtrim;
|
||||
use function sprintf;
|
||||
use function str_replace;
|
||||
use function strpos;
|
||||
use function strrpos;
|
||||
@@ -45,6 +51,7 @@ use function substr;
|
||||
use function ucfirst;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use const PHP_VERSION_ID;
|
||||
|
||||
/**
|
||||
* This factory is used to create proxy objects for entities at runtime.
|
||||
@@ -127,6 +134,9 @@ EOPHP;
|
||||
/** @var array<class-string, Closure> */
|
||||
private array $proxyFactories = [];
|
||||
|
||||
private readonly string $proxyDir;
|
||||
private readonly string $proxyNs;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the <tt>ProxyFactory</tt> class that is
|
||||
* connected to the given <tt>EntityManager</tt>.
|
||||
@@ -138,22 +148,50 @@ EOPHP;
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly string $proxyDir,
|
||||
private readonly string $proxyNs,
|
||||
string|null $proxyDir = null,
|
||||
string|null $proxyNs = null,
|
||||
bool|int $autoGenerate = self::AUTOGENERATE_NEVER,
|
||||
) {
|
||||
if (! $proxyDir) {
|
||||
throw ORMInvalidArgumentException::proxyDirectoryRequired();
|
||||
}
|
||||
if (! $em->getConfiguration()->isNativeLazyObjectsEnabled()) {
|
||||
if (PHP_VERSION_ID >= 80400) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/12005',
|
||||
'Not enabling native lazy objects is deprecated and will be impossible in Doctrine ORM 4.0.',
|
||||
);
|
||||
}
|
||||
|
||||
if (! $proxyNs) {
|
||||
throw ORMInvalidArgumentException::proxyNamespaceRequired();
|
||||
if (! $proxyDir) {
|
||||
throw ORMInvalidArgumentException::proxyDirectoryRequired();
|
||||
}
|
||||
|
||||
if (! $proxyNs) {
|
||||
throw ORMInvalidArgumentException::proxyNamespaceRequired();
|
||||
}
|
||||
} elseif (PHP_VERSION_ID >= 80400 && func_num_args() > 1) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/12005',
|
||||
'Passing more than just the EntityManager to the %s is deprecated and will not be possible in Doctrine ORM 4.0.',
|
||||
__METHOD__,
|
||||
);
|
||||
}
|
||||
|
||||
if (is_int($autoGenerate) ? $autoGenerate < 0 || $autoGenerate > 4 : ! is_bool($autoGenerate)) {
|
||||
throw ORMInvalidArgumentException::invalidAutoGenerateMode($autoGenerate);
|
||||
}
|
||||
|
||||
if ($proxyDir === null && $em->getConfiguration()->isNativeLazyObjectsEnabled()) {
|
||||
$proxyDir = '';
|
||||
}
|
||||
|
||||
if ($proxyNs === null && $em->getConfiguration()->isNativeLazyObjectsEnabled()) {
|
||||
$proxyNs = '';
|
||||
}
|
||||
|
||||
$this->proxyDir = $proxyDir;
|
||||
$this->proxyNs = $proxyNs;
|
||||
|
||||
$this->uow = $em->getUnitOfWork();
|
||||
$this->autoGenerate = (int) $autoGenerate;
|
||||
$this->identifierFlattener = new IdentifierFlattener($this->uow, $em->getMetadataFactory());
|
||||
@@ -163,8 +201,35 @@ EOPHP;
|
||||
* @param class-string $className
|
||||
* @param array<mixed> $identifier
|
||||
*/
|
||||
public function getProxy(string $className, array $identifier): InternalProxy
|
||||
public function getProxy(string $className, array $identifier): object
|
||||
{
|
||||
if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled()) {
|
||||
$classMetadata = $this->em->getClassMetadata($className);
|
||||
$entityPersister = $this->uow->getEntityPersister($className);
|
||||
$identifierFlattener = $this->identifierFlattener;
|
||||
|
||||
$proxy = $classMetadata->reflClass->newLazyGhost(static function (object $object) use (
|
||||
$identifier,
|
||||
$entityPersister,
|
||||
$identifierFlattener,
|
||||
$classMetadata,
|
||||
): void {
|
||||
$original = $entityPersister->loadById($identifier, $object);
|
||||
if ($original === null) {
|
||||
throw EntityNotFoundException::fromClassNameAndIdentifier(
|
||||
$classMetadata->getName(),
|
||||
$identifierFlattener->flattenIdentifier($classMetadata, $identifier),
|
||||
);
|
||||
}
|
||||
}, ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE);
|
||||
|
||||
foreach ($identifier as $idField => $value) {
|
||||
$classMetadata->propertyAccessors[$idField]->setValue($proxy, $value);
|
||||
}
|
||||
|
||||
return $proxy;
|
||||
}
|
||||
|
||||
$proxyFactory = $this->proxyFactories[$className] ?? $this->getProxyFactory($className);
|
||||
|
||||
return $proxyFactory($identifier);
|
||||
@@ -182,6 +247,10 @@ EOPHP;
|
||||
*/
|
||||
public function generateProxyClasses(array $classes, string|null $proxyDir = null): int
|
||||
{
|
||||
if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$generated = 0;
|
||||
|
||||
foreach ($classes as $class) {
|
||||
@@ -232,8 +301,8 @@ EOPHP;
|
||||
|
||||
$class = $entityPersister->getClassMetadata();
|
||||
|
||||
foreach ($class->getReflectionProperties() as $property) {
|
||||
if (! $property || isset($identifier[$property->getName()])) {
|
||||
foreach ($class->getPropertyAccessors() as $name => $property) {
|
||||
if (isset($identifier[$name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -262,6 +331,14 @@ EOPHP;
|
||||
foreach ($reflector->getProperties($filter) as $property) {
|
||||
$name = $property->name;
|
||||
|
||||
if (PHP_VERSION_ID >= 80400 && count($property->getHooks()) > 0) {
|
||||
throw new LogicException(sprintf(
|
||||
'Doctrine ORM does not support property hook on %s::%s without using native lazy objects. Check https://github.com/doctrine/orm/issues/11624 for details of versions that support property hooks.',
|
||||
$property->getDeclaringClass()->getName(),
|
||||
$property->getName(),
|
||||
));
|
||||
}
|
||||
|
||||
if ($property->isStatic() || ! isset($identifiers[$name])) {
|
||||
continue;
|
||||
}
|
||||
@@ -279,7 +356,11 @@ EOPHP;
|
||||
$entityPersister = $this->uow->getEntityPersister($className);
|
||||
$initializer = $this->createLazyInitializer($class, $entityPersister, $this->identifierFlattener);
|
||||
$proxyClassName = $this->loadProxyClass($class);
|
||||
$identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers);
|
||||
$identifierFields = [];
|
||||
|
||||
foreach (array_keys($identifiers) as $identifier) {
|
||||
$identifierFields[$identifier] = $class->getPropertyAccessor($identifier);
|
||||
}
|
||||
|
||||
$proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy {
|
||||
$proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void {
|
||||
@@ -383,6 +464,7 @@ EOPHP;
|
||||
|
||||
private function generateUseLazyGhostTrait(ClassMetadata $class): string
|
||||
{
|
||||
// @phpstan-ignore staticMethod.deprecated (Because we support Symfony < 7.3)
|
||||
$code = ProxyHelper::generateLazyGhost($class->getReflectionClass());
|
||||
$code = substr($code, 7 + (int) strpos($code, "\n{"));
|
||||
$code = substr($code, 0, (int) strpos($code, "\n}"));
|
||||
|
||||
26
src/Query/AST/EntityAsDtoArgumentExpression.php
Normal file
26
src/Query/AST/EntityAsDtoArgumentExpression.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query\AST;
|
||||
|
||||
use Doctrine\ORM\Query\SqlWalker;
|
||||
|
||||
/**
|
||||
* EntityAsDtoArgumentExpression ::= IdentificationVariable
|
||||
*
|
||||
* @link www.doctrine-project.org
|
||||
*/
|
||||
class EntityAsDtoArgumentExpression extends Node
|
||||
{
|
||||
public function __construct(
|
||||
public mixed $expression,
|
||||
public string|null $identificationVariable,
|
||||
) {
|
||||
}
|
||||
|
||||
public function dispatch(SqlWalker $walker): string
|
||||
{
|
||||
return $walker->walkEntityAsDtoArgumentExpression($this);
|
||||
}
|
||||
}
|
||||
@@ -1106,6 +1106,50 @@ final class Parser
|
||||
return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* EntityAsDtoArgumentExpression ::= IdentificationVariable
|
||||
*/
|
||||
public function EntityAsDtoArgumentExpression(): AST\EntityAsDtoArgumentExpression
|
||||
{
|
||||
assert($this->lexer->lookahead !== null);
|
||||
$expression = null;
|
||||
$identVariable = null;
|
||||
$peek = $this->lexer->glimpse();
|
||||
$lookaheadType = $this->lexer->lookahead->type;
|
||||
assert($peek !== null);
|
||||
|
||||
assert($lookaheadType === TokenType::T_IDENTIFIER);
|
||||
assert($peek->type !== TokenType::T_DOT);
|
||||
assert($peek->type !== TokenType::T_OPEN_PARENTHESIS);
|
||||
|
||||
$expression = $identVariable = $this->IdentificationVariable();
|
||||
|
||||
// [["AS"] AliasResultVariable]
|
||||
$mustHaveAliasResultVariable = false;
|
||||
|
||||
if ($this->lexer->isNextToken(TokenType::T_AS)) {
|
||||
$this->match(TokenType::T_AS);
|
||||
|
||||
$mustHaveAliasResultVariable = true;
|
||||
}
|
||||
|
||||
$aliasResultVariable = null;
|
||||
|
||||
if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(TokenType::T_IDENTIFIER)) {
|
||||
$token = $this->lexer->lookahead;
|
||||
$aliasResultVariable = $this->AliasResultVariable();
|
||||
|
||||
// Include AliasResultVariable in query components.
|
||||
$this->queryComponents[$aliasResultVariable] = [
|
||||
'resultVariable' => $expression,
|
||||
'nestingLevel' => $this->nestingLevel,
|
||||
'token' => $token,
|
||||
];
|
||||
}
|
||||
|
||||
return new AST\EntityAsDtoArgumentExpression($expression, $identVariable);
|
||||
}
|
||||
|
||||
/**
|
||||
* SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression}
|
||||
*/
|
||||
@@ -1420,7 +1464,7 @@ final class Parser
|
||||
|
||||
assert($this->lexer->lookahead !== null);
|
||||
$expr = match (true) {
|
||||
$this->isMathOperator($peek) => $this->SimpleArithmeticExpression(),
|
||||
$this->isMathOperator($peek) || $this->isMathOperator($glimpse) => $this->SimpleArithmeticExpression(),
|
||||
$glimpse !== null && $glimpse->type === TokenType::T_DOT => $this->SingleValuedPathExpression(),
|
||||
$this->lexer->peek() && $this->isMathOperator($this->peekBeyondClosingParenthesis()) => $this->ScalarExpression(),
|
||||
$this->lexer->lookahead->type === TokenType::T_CASE => $this->CaseExpression(),
|
||||
@@ -1849,6 +1893,8 @@ final class Parser
|
||||
$this->match(TokenType::T_CLOSE_PARENTHESIS);
|
||||
} elseif ($token->type === TokenType::T_NEW) {
|
||||
$expression = $this->NewObjectExpression();
|
||||
} elseif ($token->type === TokenType::T_IDENTIFIER && $peek->type !== TokenType::T_DOT && $peek->type !== TokenType::T_OPEN_PARENTHESIS) {
|
||||
$expression = $this->EntityAsDtoArgumentExpression();
|
||||
} else {
|
||||
$expression = $this->ScalarExpression();
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ class ResultSetMapping
|
||||
/**
|
||||
* Maps last argument for new objects in order to initiate object construction
|
||||
*
|
||||
* @phpstan-var array<int|string, array{ownerIndex: string|int, argIndex: int|string}>
|
||||
* @phpstan-var array<int|string, array{ownerIndex: string|int, argIndex: int|string, argAlias: string}>
|
||||
*/
|
||||
public array $nestedNewObjectArguments = [];
|
||||
|
||||
@@ -187,6 +187,13 @@ class ResultSetMapping
|
||||
*/
|
||||
public array $discriminatorParameters = [];
|
||||
|
||||
/**
|
||||
* Entities nested in Dto's
|
||||
*
|
||||
* @phpstan-var array<string, array<string, (int|string)>>
|
||||
*/
|
||||
public array $nestedEntities = [];
|
||||
|
||||
/**
|
||||
* Adds an entity result to this ResultSetMapping.
|
||||
*
|
||||
|
||||
@@ -575,6 +575,14 @@ class SqlWalker
|
||||
return implode(', ', $sqlParts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks down an EntityAsDtoArgumentExpression AST node, thereby generating the appropriate SQL.
|
||||
*/
|
||||
public function walkEntityAsDtoArgumentExpression(AST\EntityAsDtoArgumentExpression $expr): string
|
||||
{
|
||||
return implode(', ', $this->walkObjectExpression($expr->expression, [], $expr->identificationVariable ?: null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks down an IdentificationVariable (no AST node associated), thereby generating the SQL.
|
||||
*/
|
||||
@@ -1356,32 +1364,79 @@ class SqlWalker
|
||||
$partialFieldSet = [];
|
||||
}
|
||||
|
||||
$class = $this->getMetadataForDqlAlias($dqlAlias);
|
||||
$resultAlias = $selectExpression->fieldIdentificationVariable ?: null;
|
||||
$sql .= implode(', ', $this->walkObjectExpression($dqlAlias, $partialFieldSet, $selectExpression->fieldIdentificationVariable ?: null));
|
||||
}
|
||||
|
||||
if (! isset($this->selectedClasses[$dqlAlias])) {
|
||||
$this->selectedClasses[$dqlAlias] = [
|
||||
'class' => $class,
|
||||
'dqlAlias' => $dqlAlias,
|
||||
'resultAlias' => $resultAlias,
|
||||
];
|
||||
}
|
||||
return $sql;
|
||||
}
|
||||
|
||||
$sqlParts = [];
|
||||
/**
|
||||
* Walks down an Object Expression AST node and return Sql Parts
|
||||
*
|
||||
* @param mixed[] $partialFieldSet
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function walkObjectExpression(string $dqlAlias, array $partialFieldSet, string|null $resultAlias): array
|
||||
{
|
||||
$class = $this->getMetadataForDqlAlias($dqlAlias);
|
||||
|
||||
// Select all fields from the queried class
|
||||
foreach ($class->fieldMappings as $fieldName => $mapping) {
|
||||
if ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true)) {
|
||||
if (! isset($this->selectedClasses[$dqlAlias])) {
|
||||
$this->selectedClasses[$dqlAlias] = [
|
||||
'class' => $class,
|
||||
'dqlAlias' => $dqlAlias,
|
||||
'resultAlias' => $resultAlias,
|
||||
];
|
||||
}
|
||||
|
||||
$sqlParts = [];
|
||||
|
||||
// Select all fields from the queried class
|
||||
foreach ($class->fieldMappings as $fieldName => $mapping) {
|
||||
if ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tableName = isset($mapping->inherited)
|
||||
? $this->em->getClassMetadata($mapping->inherited)->getTableName()
|
||||
: $class->getTableName();
|
||||
|
||||
$sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias);
|
||||
$columnAlias = $this->getSQLColumnAlias($mapping->columnName);
|
||||
$quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform);
|
||||
|
||||
$col = $sqlTableAlias . '.' . $quotedColumnName;
|
||||
|
||||
$type = Type::getType($mapping->type);
|
||||
$col = $type->convertToPHPValueSQL($col, $this->platform);
|
||||
|
||||
$sqlParts[] = $col . ' AS ' . $columnAlias;
|
||||
|
||||
$this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
|
||||
|
||||
$this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name);
|
||||
|
||||
if (! empty($mapping->enumType)) {
|
||||
$this->rsm->addEnumResult($columnAlias, $mapping->enumType);
|
||||
}
|
||||
}
|
||||
|
||||
// Add any additional fields of subclasses (excluding inherited fields)
|
||||
// 1) on Single Table Inheritance: always, since its marginal overhead
|
||||
// 2) on Class Table Inheritance only if partial objects are disallowed,
|
||||
// since it requires outer joining subtables.
|
||||
if ($class->isInheritanceTypeSingleTable() || ! $this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) {
|
||||
foreach ($class->subClasses as $subClassName) {
|
||||
$subClass = $this->em->getClassMetadata($subClassName);
|
||||
$sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias);
|
||||
|
||||
foreach ($subClass->fieldMappings as $fieldName => $mapping) {
|
||||
if (isset($mapping->inherited) || ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tableName = isset($mapping->inherited)
|
||||
? $this->em->getClassMetadata($mapping->inherited)->getTableName()
|
||||
: $class->getTableName();
|
||||
|
||||
$sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias);
|
||||
$columnAlias = $this->getSQLColumnAlias($mapping->columnName);
|
||||
$quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform);
|
||||
$quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform);
|
||||
|
||||
$col = $sqlTableAlias . '.' . $quotedColumnName;
|
||||
|
||||
@@ -1392,48 +1447,12 @@ class SqlWalker
|
||||
|
||||
$this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
|
||||
|
||||
$this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name);
|
||||
|
||||
if (! empty($mapping->enumType)) {
|
||||
$this->rsm->addEnumResult($columnAlias, $mapping->enumType);
|
||||
}
|
||||
$this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName);
|
||||
}
|
||||
|
||||
// Add any additional fields of subclasses (excluding inherited fields)
|
||||
// 1) on Single Table Inheritance: always, since its marginal overhead
|
||||
// 2) on Class Table Inheritance only if partial objects are disallowed,
|
||||
// since it requires outer joining subtables.
|
||||
if ($class->isInheritanceTypeSingleTable() || ! $this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) {
|
||||
foreach ($class->subClasses as $subClassName) {
|
||||
$subClass = $this->em->getClassMetadata($subClassName);
|
||||
$sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias);
|
||||
|
||||
foreach ($subClass->fieldMappings as $fieldName => $mapping) {
|
||||
if (isset($mapping->inherited) || ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$columnAlias = $this->getSQLColumnAlias($mapping->columnName);
|
||||
$quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform);
|
||||
|
||||
$col = $sqlTableAlias . '.' . $quotedColumnName;
|
||||
|
||||
$type = Type::getType($mapping->type);
|
||||
$col = $type->convertToPHPValueSQL($col, $this->platform);
|
||||
|
||||
$sqlParts[] = $col . ' AS ' . $columnAlias;
|
||||
|
||||
$this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
|
||||
|
||||
$this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sql .= implode(', ', $sqlParts);
|
||||
}
|
||||
}
|
||||
|
||||
return $sql;
|
||||
return $sqlParts;
|
||||
}
|
||||
|
||||
public function walkQuantifiedExpression(AST\QuantifiedExpression $qExpr): string
|
||||
@@ -1549,6 +1568,14 @@ class SqlWalker
|
||||
$sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias;
|
||||
break;
|
||||
|
||||
case $e instanceof AST\EntityAsDtoArgumentExpression:
|
||||
$alias = $e->identificationVariable ?: $columnAlias;
|
||||
$this->rsm->nestedNewObjectArguments[$columnAlias] = ['ownerIndex' => $objIndex, 'argIndex' => $argIndex, 'argAlias' => $alias];
|
||||
$this->rsm->nestedEntities[$alias] = ['parent' => $objIndex, 'argIndex' => $argIndex, 'type' => 'entity'];
|
||||
|
||||
$sqlSelectExpressions[] = trim($e->dispatch($this));
|
||||
break;
|
||||
|
||||
default:
|
||||
$sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias;
|
||||
break;
|
||||
|
||||
@@ -9,7 +9,6 @@ use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Doctrine\ORM\Internal\NoUnknownNamedArguments;
|
||||
use Doctrine\ORM\Internal\QueryType;
|
||||
use Doctrine\ORM\Query\Expr;
|
||||
use Doctrine\ORM\Query\Parameter;
|
||||
use Doctrine\ORM\Query\QueryExpressionVisitor;
|
||||
@@ -128,6 +127,11 @@ class QueryBuilder implements Stringable
|
||||
$this->parameters = new ArrayCollection();
|
||||
}
|
||||
|
||||
final protected function getType(): QueryType
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an ExpressionBuilder used for object-oriented construction of query expressions.
|
||||
* This producer method is intended for convenient inline usage. Example:
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Internal;
|
||||
namespace Doctrine\ORM;
|
||||
|
||||
/** @internal To be used inside the QueryBuilder only. */
|
||||
enum QueryType
|
||||
{
|
||||
case Select;
|
||||
28
src/Tools/Console/ApplicationCompatibility.php
Normal file
28
src/Tools/Console/ApplicationCompatibility.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Tools\Console;
|
||||
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
|
||||
use function method_exists;
|
||||
|
||||
/**
|
||||
* Forward compatibility with Symfony Console 7.4
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait ApplicationCompatibility
|
||||
{
|
||||
private static function addCommandToApplication(Application $application, Command $command): Command|null
|
||||
{
|
||||
if (method_exists(Application::class, 'addCommand')) {
|
||||
// @phpstan-ignore method.notFound (This method will be added in Symfony 7.4)
|
||||
return $application->addCommand($command);
|
||||
}
|
||||
|
||||
return $application->add($command);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Tools\Console\Command;
|
||||
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\Tools\Console\MetadataFilter;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
@@ -19,6 +20,8 @@ use function mkdir;
|
||||
use function realpath;
|
||||
use function sprintf;
|
||||
|
||||
use const PHP_VERSION_ID;
|
||||
|
||||
/**
|
||||
* Command to (re)generate the proxy classes used by doctrine.
|
||||
*
|
||||
@@ -39,6 +42,14 @@ class GenerateProxiesCommand extends AbstractEntityManagerCommand
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
if (PHP_VERSION_ID >= 80400) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/12005',
|
||||
'Generating proxies is deprecated and will be impossible in Doctrine ORM 4.0.',
|
||||
);
|
||||
}
|
||||
|
||||
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
|
||||
|
||||
$em = $this->getEntityManager($input);
|
||||
|
||||
@@ -34,7 +34,7 @@ EOT);
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
|
||||
$ui = new SymfonyStyle($input, $output);
|
||||
|
||||
$entityManager = $this->getEntityManager($input);
|
||||
|
||||
@@ -43,7 +43,7 @@ EOT);
|
||||
->getAllClassNames();
|
||||
|
||||
if (! $entityClassNames) {
|
||||
$ui->caution(
|
||||
$ui->getErrorStyle()->caution(
|
||||
[
|
||||
'You do not have any mapped Doctrine ORM entities according to the current configuration.',
|
||||
'If you have entities or mapping files you should check your mapping configuration for errors.',
|
||||
|
||||
@@ -10,6 +10,8 @@ use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\FieldMapping;
|
||||
use Doctrine\Persistence\Mapping\MappingException;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
@@ -19,6 +21,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function current;
|
||||
use function get_debug_type;
|
||||
@@ -32,6 +35,7 @@ use function preg_match;
|
||||
use function preg_quote;
|
||||
use function print_r;
|
||||
use function sprintf;
|
||||
use function str_replace;
|
||||
|
||||
use const JSON_PRETTY_PRINT;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
@@ -64,7 +68,7 @@ EOT);
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
|
||||
$ui = new SymfonyStyle($input, $output);
|
||||
|
||||
$entityManager = $this->getEntityManager($input);
|
||||
|
||||
@@ -73,6 +77,20 @@ EOT);
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
|
||||
{
|
||||
if ($input->mustSuggestArgumentValuesFor('entityName')) {
|
||||
$entityManager = $this->getEntityManager($input);
|
||||
|
||||
$entities = array_map(
|
||||
static fn (string $fqcn) => str_replace('\\', '\\\\', $fqcn),
|
||||
$this->getMappedEntities($entityManager),
|
||||
);
|
||||
|
||||
$suggestions->suggestValues(array_values($entities));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display all the mapping information for a single Entity.
|
||||
*
|
||||
@@ -97,7 +115,7 @@ EOT);
|
||||
$this->formatField('Embedded class?', $metadata->isEmbeddedClass),
|
||||
$this->formatField('Parent classes', $metadata->parentClasses),
|
||||
$this->formatField('Sub classes', $metadata->subClasses),
|
||||
$this->formatField('Embedded classes', $metadata->subClasses),
|
||||
$this->formatField('Embedded classes', $metadata->embeddedClasses),
|
||||
$this->formatField('Identifier', $metadata->identifier),
|
||||
$this->formatField('Inheritance type', $metadata->inheritanceType),
|
||||
$this->formatField('Discriminator column', $metadata->discriminatorColumn),
|
||||
|
||||
@@ -19,6 +19,8 @@ use function class_exists;
|
||||
*/
|
||||
final class ConsoleRunner
|
||||
{
|
||||
use ApplicationCompatibility;
|
||||
|
||||
/**
|
||||
* Runs console with the given helper set.
|
||||
*
|
||||
@@ -59,7 +61,10 @@ final class ConsoleRunner
|
||||
$connectionProvider = new ConnectionFromManagerProvider($entityManagerProvider);
|
||||
|
||||
if (class_exists(DBALConsole\Command\ReservedWordsCommand::class)) {
|
||||
$cli->add(new DBALConsole\Command\ReservedWordsCommand($connectionProvider));
|
||||
self::addCommandToApplication(
|
||||
$cli,
|
||||
new DBALConsole\Command\ReservedWordsCommand($connectionProvider),
|
||||
);
|
||||
}
|
||||
|
||||
$cli->addCommands(
|
||||
|
||||
@@ -505,6 +505,11 @@ class SchemaTool
|
||||
if ($isUnique) {
|
||||
$table->addUniqueIndex([$columnName]);
|
||||
}
|
||||
|
||||
$isIndex = $mapping->index ?? false;
|
||||
if ($isIndex) {
|
||||
$table->addIndex([$columnName]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -718,6 +723,10 @@ class SchemaTool
|
||||
if (isset($joinColumn->onDelete)) {
|
||||
$fkOptions['onDelete'] = $joinColumn->onDelete;
|
||||
}
|
||||
|
||||
if (isset($joinColumn->deferrable)) {
|
||||
$fkOptions['deferrable'] = $joinColumn->deferrable;
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer unique constraints over implicit simple indexes created for foreign keys.
|
||||
|
||||
@@ -31,7 +31,6 @@ use function array_map;
|
||||
use function array_push;
|
||||
use function array_search;
|
||||
use function array_values;
|
||||
use function assert;
|
||||
use function class_exists;
|
||||
use function class_parents;
|
||||
use function count;
|
||||
@@ -154,7 +153,7 @@ class SchemaValidator
|
||||
$ce[] = 'The field ' . $class->name . '#' . $fieldName . ' is on the inverse side of a ' .
|
||||
'bi-directional relationship, but the specified mappedBy association on the target-entity ' .
|
||||
$assoc->targetEntity . '#' . $assoc->mappedBy . ' does not contain the required ' .
|
||||
"'inversedBy=\"" . $fieldName . "\"' attribute.";
|
||||
"'inversedBy: \"" . $fieldName . "\"' attribute.";
|
||||
} elseif ($targetMetadata->associationMappings[$assoc->mappedBy]->inversedBy !== $fieldName) {
|
||||
$ce[] = 'The mappings ' . $class->name . '#' . $fieldName . ' and ' .
|
||||
$assoc->targetEntity . '#' . $assoc->mappedBy . ' are ' .
|
||||
@@ -175,7 +174,7 @@ class SchemaValidator
|
||||
$ce[] = 'The field ' . $class->name . '#' . $fieldName . ' is on the owning side of a ' .
|
||||
'bi-directional relationship, but the specified inversedBy association on the target-entity ' .
|
||||
$assoc->targetEntity . '#' . $assoc->inversedBy . ' does not contain the required ' .
|
||||
"'mappedBy=\"" . $fieldName . "\"' attribute.";
|
||||
"'mappedBy: \"" . $fieldName . "\"' attribute.";
|
||||
} elseif ($targetMetadata->associationMappings[$assoc->inversedBy]->mappedBy !== $fieldName) {
|
||||
$ce[] = 'The mappings ' . $class->name . '#' . $fieldName . ' and ' .
|
||||
$assoc->targetEntity . '#' . $assoc->inversedBy . ' are ' .
|
||||
@@ -329,9 +328,8 @@ class SchemaValidator
|
||||
array_filter(
|
||||
array_map(
|
||||
function (FieldMapping $fieldMapping) use ($class): string|null {
|
||||
$fieldName = $fieldMapping->fieldName;
|
||||
assert(isset($class->reflFields[$fieldName]));
|
||||
$propertyType = $class->reflFields[$fieldName]->getType();
|
||||
$fieldName = $fieldMapping->fieldName;
|
||||
$propertyType = $class->propertyAccessors[$fieldName]->getUnderlyingReflector()->getType();
|
||||
|
||||
// If the field type is not a built-in type, we cannot check it
|
||||
if (! Type::hasType($fieldMapping->type)) {
|
||||
|
||||
@@ -31,6 +31,7 @@ use Doctrine\ORM\Id\AssignedGenerator;
|
||||
use Doctrine\ORM\Internal\HydrationCompleteHandler;
|
||||
use Doctrine\ORM\Internal\StronglyConnectedComponents;
|
||||
use Doctrine\ORM\Internal\TopologicalSort;
|
||||
use Doctrine\ORM\Internal\UnitOfWork\InsertBatch;
|
||||
use Doctrine\ORM\Mapping\AssociationMapping;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\MappingException;
|
||||
@@ -586,7 +587,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
|
||||
$actualData = [];
|
||||
|
||||
foreach ($class->reflFields as $name => $refProp) {
|
||||
foreach ($class->propertyAccessors as $name => $refProp) {
|
||||
$value = $refProp->getValue($entity);
|
||||
|
||||
if ($class->isCollectionValuedAssociation($name) && $value !== null) {
|
||||
@@ -706,7 +707,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
|
||||
$newValue = clone $actualValue;
|
||||
$newValue->setOwner($entity, $assoc);
|
||||
$class->reflFields[$propName]->setValue($entity, $newValue);
|
||||
$class->propertyAccessors[$propName]->setValue($entity, $newValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -745,7 +746,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
|
||||
// Look for changes in associations of the entity
|
||||
foreach ($class->associationMappings as $field => $assoc) {
|
||||
$val = $class->reflFields[$field]->getValue($entity);
|
||||
$val = $class->propertyAccessors[$field]->getValue($entity);
|
||||
if ($val === null) {
|
||||
continue;
|
||||
}
|
||||
@@ -933,7 +934,9 @@ class UnitOfWork implements PropertyChangedListener
|
||||
|
||||
$this->entityStates[$oid] = self::STATE_MANAGED;
|
||||
|
||||
$this->scheduleForInsert($entity);
|
||||
if (! isset($this->entityInsertions[$oid])) {
|
||||
$this->scheduleForInsert($entity);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param mixed[] $idValue */
|
||||
@@ -981,7 +984,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
|
||||
$actualData = [];
|
||||
|
||||
foreach ($class->reflFields as $name => $refProp) {
|
||||
foreach ($class->propertyAccessors as $name => $refProp) {
|
||||
if (
|
||||
( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity())
|
||||
&& ($name !== $class->versionField)
|
||||
@@ -1037,30 +1040,36 @@ class UnitOfWork implements PropertyChangedListener
|
||||
*/
|
||||
private function executeInserts(): void
|
||||
{
|
||||
$entities = $this->computeInsertExecutionOrder();
|
||||
$batchedByType = InsertBatch::batchByEntityType($this->em, $this->computeInsertExecutionOrder());
|
||||
$eventsToDispatch = [];
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
$oid = spl_object_id($entity);
|
||||
$class = $this->em->getClassMetadata($entity::class);
|
||||
foreach ($batchedByType as $batch) {
|
||||
$class = $batch->class;
|
||||
$invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
|
||||
$persister = $this->getEntityPersister($class->name);
|
||||
|
||||
$persister->addInsert($entity);
|
||||
foreach ($batch->entities as $entity) {
|
||||
$oid = spl_object_id($entity);
|
||||
|
||||
unset($this->entityInsertions[$oid]);
|
||||
$persister->addInsert($entity);
|
||||
|
||||
unset($this->entityInsertions[$oid]);
|
||||
}
|
||||
|
||||
$persister->executeInserts();
|
||||
|
||||
if (! isset($this->entityIdentifiers[$oid])) {
|
||||
//entity was not added to identity map because some identifiers are foreign keys to new entities.
|
||||
//add it now
|
||||
$this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
|
||||
}
|
||||
foreach ($batch->entities as $entity) {
|
||||
$oid = spl_object_id($entity);
|
||||
|
||||
$invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
|
||||
if (! isset($this->entityIdentifiers[$oid])) {
|
||||
//entity was not added to identity map because some identifiers are foreign keys to new entities.
|
||||
//add it now
|
||||
$this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
|
||||
}
|
||||
|
||||
if ($invoke !== ListenersInvoker::INVOKE_NONE) {
|
||||
$eventsToDispatch[] = ['class' => $class, 'entity' => $entity, 'invoke' => $invoke];
|
||||
if ($invoke !== ListenersInvoker::INVOKE_NONE) {
|
||||
$eventsToDispatch[] = ['class' => $class, 'entity' => $entity, 'invoke' => $invoke];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1167,7 +1176,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
// is obtained by a new entity because the old one went out of scope.
|
||||
//$this->entityStates[$oid] = self::STATE_NEW;
|
||||
if (! $class->isIdentifierNatural()) {
|
||||
$class->reflFields[$class->identifier[0]]->setValue($entity, null);
|
||||
$class->propertyAccessors[$class->identifier[0]]->setValue($entity, null);
|
||||
}
|
||||
|
||||
if ($invoke !== ListenersInvoker::INVOKE_NONE) {
|
||||
@@ -2029,7 +2038,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
);
|
||||
|
||||
foreach ($associationMappings as $assoc) {
|
||||
$relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity);
|
||||
$relatedEntities = $class->propertyAccessors[$assoc->fieldName]->getValue($entity);
|
||||
|
||||
switch (true) {
|
||||
case $relatedEntities instanceof PersistentCollection:
|
||||
@@ -2070,7 +2079,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
);
|
||||
|
||||
foreach ($associationMappings as $assoc) {
|
||||
$relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity);
|
||||
$relatedEntities = $class->propertyAccessors[$assoc->fieldName]->getValue($entity);
|
||||
|
||||
switch (true) {
|
||||
case $relatedEntities instanceof PersistentCollection:
|
||||
@@ -2116,7 +2125,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
);
|
||||
|
||||
foreach ($associationMappings as $assoc) {
|
||||
$relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity);
|
||||
$relatedEntities = $class->propertyAccessors[$assoc->fieldName]->getValue($entity);
|
||||
|
||||
switch (true) {
|
||||
case $relatedEntities instanceof PersistentCollection:
|
||||
@@ -2179,7 +2188,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
$entitiesToCascade = [];
|
||||
|
||||
foreach ($associationMappings as $assoc) {
|
||||
$relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity);
|
||||
$relatedEntities = $class->propertyAccessors[$assoc->fieldName]->getValue($entity);
|
||||
|
||||
switch (true) {
|
||||
case $relatedEntities instanceof Collection:
|
||||
@@ -2235,7 +2244,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
$this->initializeObject($entity);
|
||||
|
||||
assert($class->versionField !== null);
|
||||
$entityVersion = $class->reflFields[$class->versionField]->getValue($entity);
|
||||
$entityVersion = $class->propertyAccessors[$class->versionField]->getValue($entity);
|
||||
|
||||
// phpcs:ignore SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedNotEqualOperator
|
||||
if ($entityVersion != $lockVersion) {
|
||||
@@ -2379,9 +2388,13 @@ class UnitOfWork implements PropertyChangedListener
|
||||
}
|
||||
|
||||
if ($this->isUninitializedObject($entity)) {
|
||||
$entity->__setInitialized(true);
|
||||
if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled()) {
|
||||
$class->reflClass->markLazyObjectAsInitialized($entity);
|
||||
} else {
|
||||
$entity->__setInitialized(true);
|
||||
|
||||
Hydrator::hydrate($entity, (array) $class->reflClass->newInstanceWithoutConstructor());
|
||||
Hydrator::hydrate($entity, (array) $class->reflClass->newInstanceWithoutConstructor());
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
! isset($hints[Query::HINT_REFRESH])
|
||||
@@ -2404,7 +2417,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
|
||||
foreach ($data as $field => $value) {
|
||||
if (isset($class->fieldMappings[$field])) {
|
||||
$class->reflFields[$field]->setValue($entity, $value);
|
||||
$class->propertyAccessors[$field]->setValue($entity, $value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2434,21 +2447,21 @@ class UnitOfWork implements PropertyChangedListener
|
||||
if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_id($data[$field])])) {
|
||||
$this->originalEntityData[$oid][$field] = $data[$field];
|
||||
|
||||
$class->reflFields[$field]->setValue($entity, $data[$field]);
|
||||
$targetClass->reflFields[$assoc->mappedBy]->setValue($data[$field], $entity);
|
||||
$class->propertyAccessors[$field]->setValue($entity, $data[$field]);
|
||||
$targetClass->propertyAccessors[$assoc->mappedBy]->setValue($data[$field], $entity);
|
||||
|
||||
continue 2;
|
||||
}
|
||||
|
||||
// Inverse side of x-to-one can never be lazy
|
||||
$class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc->targetEntity)->loadOneToOneEntity($assoc, $entity));
|
||||
$class->propertyAccessors[$field]->setValue($entity, $this->getEntityPersister($assoc->targetEntity)->loadOneToOneEntity($assoc, $entity));
|
||||
|
||||
continue 2;
|
||||
}
|
||||
|
||||
// use the entity association
|
||||
if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_id($data[$field])])) {
|
||||
$class->reflFields[$field]->setValue($entity, $data[$field]);
|
||||
$class->propertyAccessors[$field]->setValue($entity, $data[$field]);
|
||||
$this->originalEntityData[$oid][$field] = $data[$field];
|
||||
|
||||
break;
|
||||
@@ -2480,7 +2493,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
|
||||
if (! $associatedId) {
|
||||
// Foreign key is NULL
|
||||
$class->reflFields[$field]->setValue($entity, null);
|
||||
$class->propertyAccessors[$field]->setValue($entity, null);
|
||||
$this->originalEntityData[$oid][$field] = null;
|
||||
|
||||
break;
|
||||
@@ -2546,11 +2559,11 @@ class UnitOfWork implements PropertyChangedListener
|
||||
}
|
||||
|
||||
$this->originalEntityData[$oid][$field] = $newValue;
|
||||
$class->reflFields[$field]->setValue($entity, $newValue);
|
||||
$class->propertyAccessors[$field]->setValue($entity, $newValue);
|
||||
|
||||
if ($assoc->inversedBy !== null && $assoc->isOneToOne() && $newValue !== null) {
|
||||
$inverseAssoc = $targetClass->associationMappings[$assoc->inversedBy];
|
||||
$targetClass->reflFields[$inverseAssoc->fieldName]->setValue($newValue, $entity);
|
||||
$targetClass->propertyAccessors[$inverseAssoc->fieldName]->setValue($newValue, $entity);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -2566,7 +2579,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) {
|
||||
$data[$field]->setOwner($entity, $assoc);
|
||||
|
||||
$class->reflFields[$field]->setValue($entity, $data[$field]);
|
||||
$class->propertyAccessors[$field]->setValue($entity, $data[$field]);
|
||||
$this->originalEntityData[$oid][$field] = $data[$field];
|
||||
|
||||
break;
|
||||
@@ -2577,7 +2590,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
$pColl->setOwner($entity, $assoc);
|
||||
$pColl->setInitialized(false);
|
||||
|
||||
$reflField = $class->reflFields[$field];
|
||||
$reflField = $class->propertyAccessors[$field];
|
||||
$reflField->setValue($entity, $pColl);
|
||||
|
||||
if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER) {
|
||||
@@ -2657,7 +2670,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
$found = $this->getEntityPersister($targetEntity)->loadAll([$mappedBy => $entities], $mapping->orderBy);
|
||||
|
||||
$targetClass = $this->em->getClassMetadata($targetEntity);
|
||||
$targetProperty = $targetClass->getReflectionProperty($mappedBy);
|
||||
$targetProperty = $targetClass->getPropertyAccessor($mappedBy);
|
||||
assert($targetProperty !== null);
|
||||
|
||||
foreach ($found as $targetValue) {
|
||||
@@ -2679,7 +2692,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
$idHash = implode(' ', $id);
|
||||
|
||||
if ($mapping->indexBy !== null) {
|
||||
$indexByProperty = $targetClass->getReflectionProperty($mapping->indexBy);
|
||||
$indexByProperty = $targetClass->getPropertyAccessor($mapping->indexBy);
|
||||
assert($indexByProperty !== null);
|
||||
$collectionBatch[$idHash]->hydrateSet($indexByProperty->getValue($targetValue), $targetValue);
|
||||
} else {
|
||||
@@ -3036,16 +3049,23 @@ class UnitOfWork implements PropertyChangedListener
|
||||
|
||||
if ($obj instanceof PersistentCollection) {
|
||||
$obj->initialize();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled()) {
|
||||
$reflection = $this->em->getClassMetadata($obj::class)->getReflectionClass();
|
||||
$reflection->initializeLazyObject($obj);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if a value is an uninitialized entity.
|
||||
*
|
||||
* @phpstan-assert-if-true InternalProxy $obj
|
||||
*/
|
||||
/** Tests if a value is an uninitialized entity. */
|
||||
public function isUninitializedObject(mixed $obj): bool
|
||||
{
|
||||
if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled() && ! ($obj instanceof Collection) && is_object($obj)) {
|
||||
return $this->em->getClassMetadata($obj::class)->reflClass->isUninitializedLazyObject($obj);
|
||||
}
|
||||
|
||||
return $obj instanceof InternalProxy && ! $obj->__isInitialized();
|
||||
}
|
||||
|
||||
@@ -3244,7 +3264,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
$idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $generatedId);
|
||||
$oid = spl_object_id($entity);
|
||||
|
||||
$class->reflFields[$idField]->setValue($entity, $idValue);
|
||||
$class->propertyAccessors[$idField]->setValue($entity, $idValue);
|
||||
|
||||
$this->entityIdentifiers[$oid] = [$idField => $idValue];
|
||||
$this->entityStates[$oid] = self::STATE_MANAGED;
|
||||
|
||||
238
tests/Doctrine/Tests/ORM/Functional/GH8011Test.php
Normal file
238
tests/Doctrine/Tests/ORM/Functional/GH8011Test.php
Normal file
@@ -0,0 +1,238 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional;
|
||||
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
use Doctrine\Tests\Models\Company\CompanyEmployee;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* Functional tests for ordering with arithmetic expression.
|
||||
*/
|
||||
#[Group('GH8011')]
|
||||
class GH8011Test extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->useModelSet('company');
|
||||
|
||||
parent::setUp();
|
||||
|
||||
$this->generateFixture();
|
||||
}
|
||||
|
||||
private function skipIfPostgres(string $test): void
|
||||
{
|
||||
$platform = $this->_em->getConnection()->getDatabasePlatform();
|
||||
if ($platform instanceof PostgreSQLPlatform) {
|
||||
self::markTestSkipped(
|
||||
'The ' . $test . ' test does not work on postgresql (see https://github.com/doctrine/orm/pull/8012).',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function testOrderWithArithmeticExpressionWithSingleValuedPathExpression(): void
|
||||
{
|
||||
$dql = 'SELECT p ' .
|
||||
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
|
||||
'ORDER BY p.id + p.id ASC';
|
||||
|
||||
/** @var CompanyEmployee[] $result */
|
||||
$result = $this->_em->createQuery($dql)->getResult();
|
||||
|
||||
$this->assertEquals(2, count($result));
|
||||
$this->assertEquals('Benjamin E.', $result[0]->getName());
|
||||
$this->assertEquals('Guilherme B.', $result[1]->getName());
|
||||
}
|
||||
|
||||
public function testOrderWithArithmeticExpressionWithLiteralAndSingleValuedPathExpression(): void
|
||||
{
|
||||
$dql = 'SELECT p ' .
|
||||
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
|
||||
'ORDER BY 1 + p.id ASC';
|
||||
|
||||
/** @var CompanyEmployee[] $result */
|
||||
$result = $this->_em->createQuery($dql)->getResult();
|
||||
|
||||
$this->assertEquals(2, count($result));
|
||||
$this->assertEquals('Benjamin E.', $result[0]->getName());
|
||||
$this->assertEquals('Guilherme B.', $result[1]->getName());
|
||||
}
|
||||
|
||||
public function testOrderWithArithmeticExpressionWithLiteralAndSingleValuedPathExpression2(): void
|
||||
{
|
||||
$dql = 'SELECT p ' .
|
||||
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
|
||||
'ORDER BY ((1 + p.id)) ASC';
|
||||
|
||||
/** @var CompanyEmployee[] $result */
|
||||
$result = $this->_em->createQuery($dql)->getResult();
|
||||
|
||||
$this->assertEquals(2, count($result));
|
||||
$this->assertEquals('Benjamin E.', $result[0]->getName());
|
||||
$this->assertEquals('Guilherme B.', $result[1]->getName());
|
||||
}
|
||||
|
||||
public function testOrderWithArithmeticExpressionWithSingleValuedPathExpressionAndLiteral(): void
|
||||
{
|
||||
$dql = 'SELECT p ' .
|
||||
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
|
||||
'ORDER BY p.id + 1 ASC';
|
||||
|
||||
/** @var CompanyEmployee[] $result */
|
||||
$result = $this->_em->createQuery($dql)->getResult();
|
||||
|
||||
$this->assertEquals(2, count($result));
|
||||
$this->assertEquals('Benjamin E.', $result[0]->getName());
|
||||
$this->assertEquals('Guilherme B.', $result[1]->getName());
|
||||
}
|
||||
|
||||
public function testOrderWithArithmeticExpressionWithResultVariableAndLiteral(): void
|
||||
{
|
||||
$this->skipIfPostgres(__FUNCTION__);
|
||||
|
||||
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
|
||||
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
|
||||
'ORDER BY s + 1 DESC';
|
||||
|
||||
/** @var CompanyEmployee[] $result */
|
||||
$result = $this->_em->createQuery($dql)->getResult();
|
||||
|
||||
$this->assertEquals(2, count($result));
|
||||
$this->assertEquals('Guilherme B.', $result[0]->getName());
|
||||
$this->assertEquals('Benjamin E.', $result[1]->getName());
|
||||
}
|
||||
|
||||
public function testOrderWithArithmeticExpressionWithResultVariableAndLiteral2(): void
|
||||
{
|
||||
$this->skipIfPostgres(__FUNCTION__);
|
||||
|
||||
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
|
||||
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
|
||||
'ORDER BY ((s + 1)) DESC';
|
||||
|
||||
/** @var CompanyEmployee[] $result */
|
||||
$result = $this->_em->createQuery($dql)->getResult();
|
||||
|
||||
$this->assertEquals(2, count($result));
|
||||
$this->assertEquals('Guilherme B.', $result[0]->getName());
|
||||
$this->assertEquals('Benjamin E.', $result[1]->getName());
|
||||
}
|
||||
|
||||
public function testOrderWithArithmeticExpressionWithLiteralAndResultVariable(): void
|
||||
{
|
||||
$this->skipIfPostgres(__FUNCTION__);
|
||||
|
||||
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
|
||||
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
|
||||
'ORDER BY 1 + s DESC';
|
||||
|
||||
/** @var CompanyEmployee[] $result */
|
||||
$result = $this->_em->createQuery($dql)->getResult();
|
||||
|
||||
$this->assertEquals(2, count($result));
|
||||
$this->assertEquals('Guilherme B.', $result[0]->getName());
|
||||
$this->assertEquals('Benjamin E.', $result[1]->getName());
|
||||
}
|
||||
|
||||
public function testOrderWithArithmeticExpressionWithLiteralAndResultVariable2(): void
|
||||
{
|
||||
$this->skipIfPostgres(__FUNCTION__);
|
||||
|
||||
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
|
||||
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
|
||||
'ORDER BY ((1 + s)) DESC';
|
||||
|
||||
/** @var CompanyEmployee[] $result */
|
||||
$result = $this->_em->createQuery($dql)->getResult();
|
||||
|
||||
$this->assertEquals(2, count($result));
|
||||
$this->assertEquals('Guilherme B.', $result[0]->getName());
|
||||
$this->assertEquals('Benjamin E.', $result[1]->getName());
|
||||
}
|
||||
|
||||
public function testOrderWithArithmeticExpressionWithResultVariableAndSingleValuedPathExpression(): void
|
||||
{
|
||||
$this->skipIfPostgres(__FUNCTION__);
|
||||
|
||||
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
|
||||
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
|
||||
'ORDER BY s + p.id DESC';
|
||||
|
||||
/** @var CompanyEmployee[] $result */
|
||||
$result = $this->_em->createQuery($dql)->getResult();
|
||||
|
||||
$this->assertEquals(2, count($result));
|
||||
$this->assertEquals('Guilherme B.', $result[0]->getName());
|
||||
$this->assertEquals('Benjamin E.', $result[1]->getName());
|
||||
}
|
||||
|
||||
public function testOrderWithArithmeticExpressionWithResultVariableAndSingleValuedPathExpression2(): void
|
||||
{
|
||||
$this->skipIfPostgres(__FUNCTION__);
|
||||
|
||||
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
|
||||
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
|
||||
'ORDER BY ((s + p.id)) DESC';
|
||||
|
||||
/** @var CompanyEmployee[] $result */
|
||||
$result = $this->_em->createQuery($dql)->getResult();
|
||||
|
||||
$this->assertEquals(2, count($result));
|
||||
$this->assertEquals('Guilherme B.', $result[0]->getName());
|
||||
$this->assertEquals('Benjamin E.', $result[1]->getName());
|
||||
}
|
||||
|
||||
public function testOrderWithArithmeticExpressionWithSingleValuedPathExpressionAndResultVariable(): void
|
||||
{
|
||||
$this->skipIfPostgres(__FUNCTION__);
|
||||
|
||||
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
|
||||
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
|
||||
'ORDER BY p.id + s DESC';
|
||||
|
||||
/** @var CompanyEmployee[] $result */
|
||||
$result = $this->_em->createQuery($dql)->getResult();
|
||||
|
||||
$this->assertEquals(2, count($result));
|
||||
$this->assertEquals('Guilherme B.', $result[0]->getName());
|
||||
$this->assertEquals('Benjamin E.', $result[1]->getName());
|
||||
}
|
||||
|
||||
public function testOrderWithArithmeticExpressionWithLiteralAndResultVariableUsingHiddenResultVariable(): void
|
||||
{
|
||||
$dql = 'SELECT p, 1 + p.salary AS HIDDEN _order ' .
|
||||
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
|
||||
'ORDER BY _order DESC';
|
||||
|
||||
/** @var CompanyEmployee[] $result */
|
||||
$result = $this->_em->createQuery($dql)->getResult();
|
||||
|
||||
$this->assertEquals(2, count($result));
|
||||
$this->assertEquals('Guilherme B.', $result[0]->getName());
|
||||
$this->assertEquals('Benjamin E.', $result[1]->getName());
|
||||
}
|
||||
|
||||
public function generateFixture(): void
|
||||
{
|
||||
$person1 = new CompanyEmployee();
|
||||
$person1->setName('Benjamin E.');
|
||||
$person1->setDepartment('IT');
|
||||
$person1->setSalary(200000);
|
||||
|
||||
$person2 = new CompanyEmployee();
|
||||
$person2->setName('Guilherme B.');
|
||||
$person2->setDepartment('IT2');
|
||||
$person2->setSalary(400000);
|
||||
|
||||
$this->_em->persist($person1);
|
||||
$this->_em->persist($person2);
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
}
|
||||
}
|
||||
85
tests/Doctrine/Tests/ORM/Functional/Ticket/GH12063.php
Normal file
85
tests/Doctrine/Tests/ORM/Functional/Ticket/GH12063.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\GeneratedValue;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
class GH12063 extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->createSchemaForModels(GH12063Association::class, GH12063Entity::class);
|
||||
}
|
||||
|
||||
public function testLoadedAssociationWithBackedEnum(): void
|
||||
{
|
||||
$association = new GH12063Association();
|
||||
$association->code = GH12063Code::One;
|
||||
|
||||
$this->_em->persist($association);
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
$entity = new GH12063Entity();
|
||||
$entity->association = $this->_em->find(GH12063Association::class, GH12063Code::One);
|
||||
|
||||
$this->_em->persist($entity);
|
||||
$this->_em->flush();
|
||||
|
||||
$this->assertNotNull($entity->id);
|
||||
}
|
||||
|
||||
public function testProxyAssociationWithBackedEnum(): void
|
||||
{
|
||||
$association = new GH12063Association();
|
||||
$association->code = GH12063Code::Two;
|
||||
|
||||
$this->_em->persist($association);
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
$entity = new GH12063Entity();
|
||||
$entity->association = $this->_em->getReference(GH12063Association::class, GH12063Code::Two);
|
||||
|
||||
$this->_em->persist($entity);
|
||||
$this->_em->flush();
|
||||
|
||||
$this->assertNotNull($entity->id);
|
||||
}
|
||||
}
|
||||
|
||||
enum GH12063Code: string
|
||||
{
|
||||
case One = 'one';
|
||||
case Two = 'two';
|
||||
}
|
||||
|
||||
#[Entity]
|
||||
class GH12063Association
|
||||
{
|
||||
#[Id]
|
||||
#[Column]
|
||||
public GH12063Code $code;
|
||||
}
|
||||
|
||||
#[Entity]
|
||||
class GH12063Entity
|
||||
{
|
||||
#[Id]
|
||||
#[Column]
|
||||
#[GeneratedValue]
|
||||
public int|null $id = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(referencedColumnName: 'code')]
|
||||
public GH12063Association $association;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Performance\LazyLoading;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\Proxy\InternalProxy as Proxy;
|
||||
use Doctrine\Performance\EntityManagerFactory;
|
||||
use Doctrine\Performance\Mock\NonProxyLoadingEntityManager;
|
||||
@@ -25,9 +26,11 @@ final class ProxyInitializationTimeBench
|
||||
/** @var Proxy[] */
|
||||
private array|null $initializedEmployees = null;
|
||||
|
||||
private EntityManager $em;
|
||||
|
||||
public function init(): void
|
||||
{
|
||||
$proxyFactory = (new NonProxyLoadingEntityManager(EntityManagerFactory::getEntityManager([])))
|
||||
$proxyFactory = (new NonProxyLoadingEntityManager($this->em = EntityManagerFactory::getEntityManager([])))
|
||||
->getProxyFactory();
|
||||
|
||||
for ($i = 0; $i < 10000; ++$i) {
|
||||
@@ -36,36 +39,36 @@ final class ProxyInitializationTimeBench
|
||||
$this->initializedUsers[$i] = $proxyFactory->getProxy(CmsUser::class, ['id' => $i]);
|
||||
$this->initializedEmployees[$i] = $proxyFactory->getProxy(CmsEmployee::class, ['id' => $i]);
|
||||
|
||||
$this->initializedUsers[$i]->__load();
|
||||
$this->initializedEmployees[$i]->__load();
|
||||
$this->em->getUnitOfWork()->initializeObject($this->initializedUsers[$i]);
|
||||
$this->em->getUnitOfWork()->initializeObject($this->initializedEmployees[$i]);
|
||||
}
|
||||
}
|
||||
|
||||
public function benchCmsUserInitialization(): void
|
||||
{
|
||||
foreach ($this->cmsUsers as $proxy) {
|
||||
$proxy->__load();
|
||||
$this->em->getUnitOfWork()->initializeObject($proxy);
|
||||
}
|
||||
}
|
||||
|
||||
public function benchCmsEmployeeInitialization(): void
|
||||
{
|
||||
foreach ($this->cmsEmployees as $proxy) {
|
||||
$proxy->__load();
|
||||
$this->em->getUnitOfWork()->initializeObject($proxy);
|
||||
}
|
||||
}
|
||||
|
||||
public function benchInitializationOfAlreadyInitializedCmsUsers(): void
|
||||
{
|
||||
foreach ($this->initializedUsers as $proxy) {
|
||||
$proxy->__load();
|
||||
$this->em->getUnitOfWork()->initializeObject($proxy);
|
||||
}
|
||||
}
|
||||
|
||||
public function benchInitializationOfAlreadyInitializedCmsEmployees(): void
|
||||
{
|
||||
foreach ($this->initializedEmployees as $proxy) {
|
||||
$proxy->__load();
|
||||
$this->em->getUnitOfWork()->initializeObject($proxy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
|
||||
*/
|
||||
class EntityPersisterMock extends BasicEntityPersister
|
||||
{
|
||||
/** @var int<0, max> */
|
||||
private int $countOfExecuteInsertCalls = 0;
|
||||
private array $inserts = [];
|
||||
private array $updates = [];
|
||||
private array $deletes = [];
|
||||
@@ -40,6 +42,8 @@ class EntityPersisterMock extends BasicEntityPersister
|
||||
|
||||
public function executeInserts(): void
|
||||
{
|
||||
$this->countOfExecuteInsertCalls += 1;
|
||||
|
||||
foreach ($this->postInsertIds as $item) {
|
||||
$this->em->getUnitOfWork()->assignPostInsertId($item['entity'], $item['generatedId']);
|
||||
}
|
||||
@@ -86,6 +90,7 @@ class EntityPersisterMock extends BasicEntityPersister
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->countOfExecuteInsertCalls = 0;
|
||||
$this->existsCalled = false;
|
||||
$this->identityColumnValueCounter = 0;
|
||||
$this->inserts = [];
|
||||
@@ -97,4 +102,10 @@ class EntityPersisterMock extends BasicEntityPersister
|
||||
{
|
||||
return $this->existsCalled;
|
||||
}
|
||||
|
||||
/** @return int<0, max> */
|
||||
public function countOfExecuteInsertCalls(): int
|
||||
{
|
||||
return $this->countOfExecuteInsertCalls;
|
||||
}
|
||||
}
|
||||
|
||||
48
tests/Tests/Models/BinaryPrimaryKey/BinaryId.php
Normal file
48
tests/Tests/Models/BinaryPrimaryKey/BinaryId.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\BinaryPrimaryKey;
|
||||
|
||||
use LogicException;
|
||||
|
||||
use function bin2hex;
|
||||
use function hex2bin;
|
||||
use function random_bytes;
|
||||
|
||||
class BinaryId
|
||||
{
|
||||
public const LENGTH = 6;
|
||||
|
||||
private string $hexId;
|
||||
|
||||
private function __construct(string $data)
|
||||
{
|
||||
$this->hexId = $data;
|
||||
}
|
||||
|
||||
public static function new(): self
|
||||
{
|
||||
return new self(bin2hex(random_bytes(self::LENGTH)));
|
||||
}
|
||||
|
||||
public static function fromBytes(string $value): self
|
||||
{
|
||||
return new self(bin2hex($value));
|
||||
}
|
||||
|
||||
public function getBytes(): string
|
||||
{
|
||||
$binary = hex2bin($this->hexId);
|
||||
if ($binary === false) {
|
||||
throw new LogicException('Cannot convert hex to binary: ' . $this->hexId);
|
||||
}
|
||||
|
||||
return $binary;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getBytes();
|
||||
}
|
||||
}
|
||||
66
tests/Tests/Models/BinaryPrimaryKey/BinaryIdType.php
Normal file
66
tests/Tests/Models/BinaryPrimaryKey/BinaryIdType.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\BinaryPrimaryKey;
|
||||
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\Tests\Mocks\CompatibilityType;
|
||||
use LogicException;
|
||||
|
||||
final class BinaryIdType extends Type
|
||||
{
|
||||
use CompatibilityType;
|
||||
|
||||
public const NAME = 'binary_id';
|
||||
|
||||
public function convertToPHPValue(
|
||||
mixed $value,
|
||||
AbstractPlatform $platform,
|
||||
): BinaryId|null {
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value instanceof BinaryId) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return BinaryId::fromBytes($value);
|
||||
}
|
||||
|
||||
public function convertToDatabaseValue(
|
||||
mixed $value,
|
||||
AbstractPlatform $platform,
|
||||
): string|null {
|
||||
if ($value === null) {
|
||||
return null;
|
||||
} elseif ($value instanceof BinaryId) {
|
||||
return $value->getBytes();
|
||||
} else {
|
||||
throw new LogicException('Unexpected value: ' . $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function getSQLDeclaration(
|
||||
array $column,
|
||||
AbstractPlatform $platform,
|
||||
): string {
|
||||
return $platform->getBinaryTypeDeclarationSQL([
|
||||
'length' => BinaryId::LENGTH,
|
||||
'fixed' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
private function doGetBindingType(): ParameterType|int
|
||||
{
|
||||
return ParameterType::BINARY;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
}
|
||||
73
tests/Tests/Models/BinaryPrimaryKey/Category.php
Normal file
73
tests/Tests/Models/BinaryPrimaryKey/Category.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\BinaryPrimaryKey;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\ReadableCollection;
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
use Doctrine\ORM\Mapping\ManyToOne;
|
||||
use Doctrine\ORM\Mapping\OneToMany;
|
||||
|
||||
#[Entity]
|
||||
class Category
|
||||
{
|
||||
#[Id]
|
||||
#[Column(type: BinaryIdType::NAME, nullable: false)]
|
||||
private BinaryId $id;
|
||||
|
||||
#[Column]
|
||||
private string $name;
|
||||
|
||||
#[ManyToOne(targetEntity: self::class, inversedBy: 'children')]
|
||||
private self|null $parent;
|
||||
|
||||
/** @var Collection<int, Category> */
|
||||
#[OneToMany(targetEntity: self::class, mappedBy: 'parent')]
|
||||
private Collection $children;
|
||||
|
||||
public function __construct(
|
||||
string $name,
|
||||
self|null $parent = null,
|
||||
) {
|
||||
$this->id = BinaryId::new();
|
||||
$this->name = $name;
|
||||
$this->parent = $parent;
|
||||
$this->children = new ArrayCollection();
|
||||
|
||||
$parent?->addChild($this);
|
||||
}
|
||||
|
||||
public function getId(): BinaryId
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getParent(): self|null
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/** @return ReadableCollection<int, Category> */
|
||||
public function getChildren(): ReadableCollection
|
||||
{
|
||||
return $this->children;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function addChild(self $category): void
|
||||
{
|
||||
if (! $this->children->contains($category)) {
|
||||
$this->children->add($category);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
tests/Tests/Models/PropertyHooks/MappingVirtualProperty.php
Normal file
32
tests/Tests/Models/PropertyHooks/MappingVirtualProperty.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
// phpcs:ignoreFile
|
||||
namespace Doctrine\Tests\Models\PropertyHooks;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\GeneratedValue;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
use Doctrine\ORM\Mapping\Table;
|
||||
|
||||
#[Entity]
|
||||
#[Table(name: 'property_hooks_user')]
|
||||
class MappingVirtualProperty
|
||||
{
|
||||
#[Id, GeneratedValue, Column(type: Types::INTEGER)]
|
||||
public ?int $id;
|
||||
|
||||
#[Column(type: Types::STRING)]
|
||||
public string $first;
|
||||
|
||||
#[Column(type: Types::STRING)]
|
||||
public string $last;
|
||||
|
||||
#[Column(type: Types::STRING)]
|
||||
public string $fullName {
|
||||
get => $this->first . " " . $this->last;
|
||||
set {
|
||||
[$this->first, $this->last] = explode(' ', $value, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
tests/Tests/Models/PropertyHooks/User.php
Normal file
56
tests/Tests/Models/PropertyHooks/User.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
// phpcs:ignoreFile
|
||||
namespace Doctrine\Tests\Models\PropertyHooks;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\GeneratedValue;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
use Doctrine\ORM\Mapping\Table;
|
||||
|
||||
#[Entity]
|
||||
#[Table(name: 'property_hooks_user')]
|
||||
class User
|
||||
{
|
||||
#[Id, GeneratedValue, Column(type: Types::INTEGER)]
|
||||
public ?int $id;
|
||||
|
||||
#[Column(type: Types::STRING)]
|
||||
public string $first {
|
||||
set {
|
||||
if (strlen($value) === 0) {
|
||||
throw new ValueError("Name must be non-empty");
|
||||
}
|
||||
$this->first = $value;
|
||||
}
|
||||
}
|
||||
|
||||
#[Column(type: Types::STRING)]
|
||||
public string $last {
|
||||
set {
|
||||
if (strlen($value) === 0) {
|
||||
throw new ValueError("Name must be non-empty");
|
||||
}
|
||||
$this->last = $value;
|
||||
}
|
||||
}
|
||||
|
||||
public string $fullName {
|
||||
get => $this->first . " " . $this->last;
|
||||
set {
|
||||
[$this->first, $this->last] = explode(' ', $value, 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[Column(type: Types::STRING)]
|
||||
public string $language = 'de' {
|
||||
// Override the "read" action with arbitrary logic.
|
||||
get => strtoupper($this->language);
|
||||
|
||||
// Override the "write" action with arbitrary logic.
|
||||
set {
|
||||
$this->language = strtolower($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\TypedProperties;
|
||||
|
||||
use BcMath\Number;
|
||||
use DateInterval;
|
||||
use DateTime;
|
||||
use DateTimeImmutable;
|
||||
@@ -54,6 +55,9 @@ class UserTyped
|
||||
#[ORM\Embedded]
|
||||
public Contact|null $contact = null;
|
||||
|
||||
#[ORM\Column(precision: 5, scale: 2)]
|
||||
public Number|null $bodyHeight = null;
|
||||
|
||||
public static function loadMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->setInheritanceType(ClassMetadata::INHERITANCE_TYPE_NONE);
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM;
|
||||
|
||||
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
|
||||
use Doctrine\ORM\Cache\CacheConfiguration;
|
||||
use Doctrine\ORM\Configuration;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
@@ -17,6 +18,8 @@ use Doctrine\ORM\Proxy\ProxyFactory;
|
||||
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
|
||||
use Doctrine\Tests\Models\DDC753\DDC753CustomRepository;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use PHPUnit\Framework\Attributes\RequiresPhp;
|
||||
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
|
||||
@@ -25,6 +28,8 @@ use Psr\Cache\CacheItemPoolInterface;
|
||||
*/
|
||||
class ConfigurationTest extends TestCase
|
||||
{
|
||||
use VerifyDeprecations;
|
||||
|
||||
private Configuration $configuration;
|
||||
|
||||
protected function setUp(): void
|
||||
@@ -34,6 +39,7 @@ class ConfigurationTest extends TestCase
|
||||
$this->configuration = new Configuration();
|
||||
}
|
||||
|
||||
#[WithoutErrorHandler]
|
||||
public function testSetGetProxyDir(): void
|
||||
{
|
||||
self::assertNull($this->configuration->getProxyDir()); // defaults
|
||||
@@ -42,6 +48,7 @@ class ConfigurationTest extends TestCase
|
||||
self::assertSame(__DIR__, $this->configuration->getProxyDir());
|
||||
}
|
||||
|
||||
#[WithoutErrorHandler]
|
||||
public function testSetGetAutoGenerateProxyClasses(): void
|
||||
{
|
||||
self::assertSame(ProxyFactory::AUTOGENERATE_ALWAYS, $this->configuration->getAutoGenerateProxyClasses()); // defaults
|
||||
@@ -56,6 +63,7 @@ class ConfigurationTest extends TestCase
|
||||
self::assertSame(ProxyFactory::AUTOGENERATE_FILE_NOT_EXISTS, $this->configuration->getAutoGenerateProxyClasses());
|
||||
}
|
||||
|
||||
#[WithoutErrorHandler]
|
||||
public function testSetGetProxyNamespace(): void
|
||||
{
|
||||
self::assertNull($this->configuration->getProxyNamespace()); // defaults
|
||||
@@ -212,4 +220,13 @@ class ConfigurationTest extends TestCase
|
||||
$this->configuration->setTypedFieldMapper($defaultTypedFieldMapper);
|
||||
self::assertSame($defaultTypedFieldMapper, $this->configuration->getTypedFieldMapper());
|
||||
}
|
||||
|
||||
#[RequiresPhp('8.4')]
|
||||
#[WithoutErrorHandler]
|
||||
public function testDisablingNativeLazyObjectsIsDeprecated(): void
|
||||
{
|
||||
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/12005');
|
||||
|
||||
$this->configuration->enableNativeLazyObjects(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,10 @@ use Generator;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use PHPUnit\Framework\Attributes\RequiresPhp;
|
||||
use ReflectionClass;
|
||||
use ReflectionProperty;
|
||||
use stdClass;
|
||||
use Symfony\Component\VarExporter\LazyGhostTrait;
|
||||
use TypeError;
|
||||
|
||||
class EntityManagerTest extends OrmTestCase
|
||||
@@ -180,17 +181,12 @@ class EntityManagerTest extends OrmTestCase
|
||||
}
|
||||
|
||||
/** Resetting the EntityManager relies on lazy objects until https://github.com/doctrine/orm/issues/5933 is resolved */
|
||||
#[RequiresPhp('8.4')]
|
||||
public function testLazyGhostEntityManager(): void
|
||||
{
|
||||
$em = new class () extends EntityManager {
|
||||
use LazyGhostTrait;
|
||||
$reflector = new ReflectionClass(EntityManager::class);
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
$em = $em::createLazyGhost(static function ($em): void {
|
||||
$em = $reflector->newLazyGhost($initializer = static function (EntityManager $em): void {
|
||||
$r = new ReflectionProperty(EntityManager::class, 'unitOfWork');
|
||||
$r->setValue($em, new class () extends UnitOfWork {
|
||||
public function __construct()
|
||||
@@ -207,7 +203,7 @@ class EntityManagerTest extends OrmTestCase
|
||||
$em->close();
|
||||
$this->assertFalse($em->isOpen());
|
||||
|
||||
$em->resetLazyObject();
|
||||
$reflector->resetAsLazyGhost($em, $initializer);
|
||||
$this->assertTrue($em->isOpen());
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ use Doctrine\ORM\Exception\EntityIdentityCollisionException;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\ORMInvalidArgumentException;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\Proxy\InternalProxy;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
use Doctrine\Tests\IterableTester;
|
||||
@@ -557,7 +556,7 @@ class BasicFunctionalTest extends OrmFunctionalTestCase
|
||||
$this->_em->persist($article);
|
||||
$this->_em->flush();
|
||||
|
||||
self::assertFalse($userRef->__isInitialized());
|
||||
self::assertTrue($this->isUninitializedObject($userRef));
|
||||
|
||||
$this->_em->clear();
|
||||
|
||||
@@ -592,7 +591,7 @@ class BasicFunctionalTest extends OrmFunctionalTestCase
|
||||
$this->_em->persist($user);
|
||||
$this->_em->flush();
|
||||
|
||||
self::assertFalse($groupRef->__isInitialized());
|
||||
self::assertTrue($this->isUninitializedObject($groupRef));
|
||||
|
||||
$this->_em->clear();
|
||||
|
||||
@@ -940,8 +939,7 @@ class BasicFunctionalTest extends OrmFunctionalTestCase
|
||||
->setParameter(1, $article->id)
|
||||
->setFetchMode(CmsArticle::class, 'user', ClassMetadata::FETCH_EAGER)
|
||||
->getSingleResult();
|
||||
self::assertInstanceOf(InternalProxy::class, $article->user, 'It IS a proxy, ...');
|
||||
self::assertFalse($this->isUninitializedObject($article->user), '...but its initialized!');
|
||||
self::assertFalse($this->isUninitializedObject($article->user));
|
||||
$this->assertQueryCount(2);
|
||||
}
|
||||
|
||||
|
||||
@@ -470,7 +470,7 @@ EXCEPTION
|
||||
public function testItAllowsReadingAttributes(): void
|
||||
{
|
||||
$metadata = $this->_em->getClassMetadata(Card::class);
|
||||
$property = $metadata->getReflectionProperty('suit');
|
||||
$property = $metadata->propertyAccessors['suit']->getUnderlyingReflector();
|
||||
|
||||
$attributes = $property->getAttributes();
|
||||
|
||||
|
||||
133
tests/Tests/ORM/Functional/InvalidMappingDefinitionTest.php
Normal file
133
tests/Tests/ORM/Functional/InvalidMappingDefinitionTest.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\MappingException;
|
||||
use Doctrine\ORM\Mapping\Table;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
/**
|
||||
* Functional tests for the Class Table Inheritance mapping strategy.
|
||||
*/
|
||||
class InvalidMappingDefinitionTest extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testManyToManyRelationWithJoinTableOnTheWrongSide(): void
|
||||
{
|
||||
$this->expectException(MappingException::class);
|
||||
$this->expectExceptionMessage("Mapping error on field 'owners' in Doctrine\Tests\ORM\Functional\OwnedSideEntity : 'joinTable' can only be set on many-to-many owning side.");
|
||||
|
||||
$this->createSchemaForModels(
|
||||
OwningSideEntity::class,
|
||||
OwnedSideEntity::class,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[Table(name: 'owning_side_entities1')]
|
||||
#[Entity]
|
||||
class OwningSideEntity
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $id;
|
||||
|
||||
#[ORM\ManyToMany(targetEntity: OwnedSideEntity::class, inversedBy: 'owners')]
|
||||
#[ORM\JoinTable(name: 'owning_owned')]
|
||||
private Collection $relations;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->relations = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getRelations(): Collection
|
||||
{
|
||||
return $this->relations;
|
||||
}
|
||||
|
||||
public function addRelation(OwnedSideEntity $ownedSide): void
|
||||
{
|
||||
if (! $this->relations->contains($ownedSide)) {
|
||||
$this->relations->add($ownedSide);
|
||||
$ownedSide->addOwner($this);
|
||||
}
|
||||
}
|
||||
|
||||
public function removeRelation(OwnedSideEntity $ownedSide): void
|
||||
{
|
||||
if ($this->relations->removeElement($ownedSide)) {
|
||||
$ownedSide->removeOwner($this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[Table(name: 'owned_side_entities1')]
|
||||
#[Entity]
|
||||
class OwnedSideEntity
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $id;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255)]
|
||||
private string $data;
|
||||
|
||||
#[ORM\ManyToMany(targetEntity: OwningSideEntity::class, mappedBy: 'relations')]
|
||||
#[ORM\JoinTable(name: 'owning_owned')]
|
||||
private Collection $owners;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->owners = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getData(): string
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function setData(string $data): void
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function getOwners(): Collection
|
||||
{
|
||||
return $this->owners;
|
||||
}
|
||||
|
||||
public function addOwner(OwningSideEntity $owningSide): void
|
||||
{
|
||||
if (! $this->owners->contains($owningSide)) {
|
||||
$this->owners->add($owningSide);
|
||||
}
|
||||
}
|
||||
|
||||
public function removeOwner(OwningSideEntity $owningSide): void
|
||||
{
|
||||
$this->owners->removeElement($owningSide);
|
||||
}
|
||||
}
|
||||
@@ -134,7 +134,7 @@ class LifecycleCallbackTest extends OrmFunctionalTestCase
|
||||
LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation
|
||||
|
||||
$reference = $this->_em->getReference(LifecycleCallbackTestEntity::class, $id);
|
||||
self::assertFalse($reference::$postLoadCallbackInvoked);
|
||||
self::assertArrayNotHasKey('postLoadCallbackInvoked', (array) $reference);
|
||||
$this->assertTrue($this->isUninitializedObject($reference));
|
||||
|
||||
$reference->getValue(); // trigger proxy load
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user