mirror of
https://github.com/doctrine/orm.git
synced 2026-03-24 15:02:22 +01:00
Compare commits
326 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69958152e6 | ||
|
|
cf8f5f9f93 | ||
|
|
60c245413d | ||
|
|
7c9b74221f | ||
|
|
c7e5605d11 | ||
|
|
c19afa1529 | ||
|
|
516b593193 | ||
|
|
e2b971d7c5 | ||
|
|
19d9244a88 | ||
|
|
10a5a3ff73 | ||
|
|
f5fb400d0f | ||
|
|
a321331c89 | ||
|
|
522863116a | ||
|
|
5bfb744967 | ||
|
|
8ed6c2234a | ||
|
|
ff612b9678 | ||
|
|
ee0d7197dd | ||
|
|
39d2136f46 | ||
|
|
c223b8f635 | ||
|
|
bea454eefc | ||
|
|
14f2572e4e | ||
|
|
c2c500077b | ||
|
|
6281c2b79f | ||
|
|
bac1c17eab | ||
|
|
b6137c8911 | ||
|
|
da7854f586 | ||
|
|
5f4ecfd1d8 | ||
|
|
51be1b1d52 | ||
|
|
5f39343bfd | ||
|
|
7ef1f0a379 | ||
|
|
488a5dd3bf | ||
|
|
30795559dc | ||
|
|
f71725575c | ||
|
|
4a3c7f05bf | ||
|
|
896c65504d | ||
|
|
16a8f10fd2 | ||
|
|
498de4c564 | ||
|
|
d80a831157 | ||
|
|
52660297ab | ||
|
|
b44774285b | ||
|
|
58287bb731 | ||
|
|
bc37f75b41 | ||
|
|
8a25b264f7 | ||
|
|
0e48b19cd3 | ||
|
|
d2978303f0 | ||
|
|
109042e5af | ||
|
|
08328adc6c | ||
|
|
191a5366b1 | ||
|
|
65806884b0 | ||
|
|
ad80e8281a | ||
|
|
44dddb2eee | ||
|
|
0c0c61c51b | ||
|
|
cc28fed9f5 | ||
|
|
2245149588 | ||
|
|
b13564c6c0 | ||
|
|
91709c1275 | ||
|
|
434b7cee2a | ||
|
|
7f0a181e39 | ||
|
|
d18126aac5 | ||
|
|
b7fd8241cf | ||
|
|
2432939e4f | ||
|
|
93ce84fa6e | ||
|
|
1bf4603422 | ||
|
|
e6961bd968 | ||
|
|
25d5bc5b46 | ||
|
|
5724e6279e | ||
|
|
cfc0655a1c | ||
|
|
6cde337777 | ||
|
|
c6b2d89748 | ||
|
|
e1dc94d1c2 | ||
|
|
74ef28295a | ||
|
|
831a1eb7d2 | ||
|
|
3a82b153f3 | ||
|
|
168ac31084 | ||
|
|
6f93cebe6e | ||
|
|
8c582a49d3 | ||
|
|
5f1fe1587c | ||
|
|
fe4a2e83cf | ||
|
|
205b2f5f20 | ||
|
|
3f550c19e3 | ||
|
|
8ac6a13ca0 | ||
|
|
2707b09a07 | ||
|
|
121158f92c | ||
|
|
56cd688c4a | ||
|
|
96546caceb | ||
|
|
57247ed6ca | ||
|
|
12817076c3 | ||
|
|
4c2f104d42 | ||
|
|
ef64cf7c33 | ||
|
|
0983d3a4af | ||
|
|
51ad860a25 | ||
|
|
9bd51aaeb6 | ||
|
|
1fe1a6a048 | ||
|
|
c37b115450 | ||
|
|
19129e9f8a | ||
|
|
efe62e3f0b | ||
|
|
7d01f19667 | ||
|
|
722cea6536 | ||
|
|
c1bb2ccf4b | ||
|
|
e3d7c6076c | ||
|
|
ce7d93f14d | ||
|
|
a139a1b63c | ||
|
|
1153b9468c | ||
|
|
40f299f1eb | ||
|
|
d0e9177121 | ||
|
|
428032ca7c | ||
|
|
68af854f46 | ||
|
|
77467cd824 | ||
|
|
f666aa641e | ||
|
|
ca3319c2f6 | ||
|
|
c06f6b9376 | ||
|
|
802f20b8e7 | ||
|
|
96d13ac62a | ||
|
|
ed53defaa1 | ||
|
|
2ea6a1a5fb | ||
|
|
41cb5fbbbf | ||
|
|
83851a9716 | ||
|
|
066ec1ac81 | ||
|
|
68744489f0 | ||
|
|
cc2ad1993c | ||
|
|
e4d46c4276 | ||
|
|
858a1adc3b | ||
|
|
3b499132d9 | ||
|
|
39153fd88a | ||
|
|
bdc9679e37 | ||
|
|
87a8ee21c9 | ||
|
|
59c8bc09ab | ||
|
|
3a7d7c9f57 | ||
|
|
06eca40134 | ||
|
|
23b35e9554 | ||
|
|
e063926cbd | ||
|
|
4a01a76a17 | ||
|
|
93c2dd9d4b | ||
|
|
75bc22980e | ||
|
|
9696c3434d | ||
|
|
9d4f54b9a4 | ||
|
|
37946d3a21 | ||
|
|
baf96cdad4 | ||
|
|
ce09c96427 | ||
|
|
ae659fe650 | ||
|
|
0a177d5074 | ||
|
|
dbfe47b07b | ||
|
|
bf3e082c00 | ||
|
|
d31aabb40c | ||
|
|
22b1f52c1c | ||
|
|
d66884403f | ||
|
|
a90ee5c495 | ||
|
|
11270425e5 | ||
|
|
552eae37a3 | ||
|
|
ee4b03aa78 | ||
|
|
f1246d57c2 | ||
|
|
a14ef7c279 | ||
|
|
54c29140fa | ||
|
|
eb49f66926 | ||
|
|
daa99f197b | ||
|
|
2b04cc2e3f | ||
|
|
3d9af3187f | ||
|
|
e83d8a80ba | ||
|
|
c5291b4de8 | ||
|
|
029ca611f0 | ||
|
|
831d86548c | ||
|
|
f26b3b9cf9 | ||
|
|
9e7715f678 | ||
|
|
9ab84f7478 | ||
|
|
e6bb4ef20e | ||
|
|
0e26e3ed50 | ||
|
|
63315c8e4a | ||
|
|
8ca99fdfdc | ||
|
|
8b6a58fa0e | ||
|
|
2d8e466636 | ||
|
|
94986af284 | ||
|
|
ad5c8e4bdc | ||
|
|
c363f55ad1 | ||
|
|
c973a62272 | ||
|
|
8d3446015a | ||
|
|
4e335f4044 | ||
|
|
bb36d49b38 | ||
|
|
2b81a8e260 | ||
|
|
7d3b3f28e9 | ||
|
|
cbec236e8b | ||
|
|
306963fe79 | ||
|
|
fb4578406f | ||
|
|
bdc41e2b5e | ||
|
|
90376a6431 | ||
|
|
97634ae6a1 | ||
|
|
b725908c83 | ||
|
|
f79d166a4e | ||
|
|
9c22814cfa | ||
|
|
b274893486 | ||
|
|
e0e55dc9c5 | ||
|
|
010b1e0886 | ||
|
|
93eb8a1bcb | ||
|
|
1464827220 | ||
|
|
8709fb38b0 | ||
|
|
cbb6c897de | ||
|
|
e9e60f2fbc | ||
|
|
5f3c1dbab8 | ||
|
|
753bc16c0b | ||
|
|
6090141e0b | ||
|
|
e4a6c041b5 | ||
|
|
be307edba8 | ||
|
|
c54c557e02 | ||
|
|
46d0865339 | ||
|
|
4672d284ff | ||
|
|
9c56071392 | ||
|
|
0a1988b349 | ||
|
|
1a5a4c674a | ||
|
|
95795c87a8 | ||
|
|
083f642cfa | ||
|
|
db6e702088 | ||
|
|
4175edf311 | ||
|
|
716da7e538 | ||
|
|
67ac5a82da | ||
|
|
e384978e0b | ||
|
|
5ccbc201bf | ||
|
|
d15624f72f | ||
|
|
9d1a4973ae | ||
|
|
55c4845d57 | ||
|
|
a38f473a92 | ||
|
|
bcdc5bdaf4 | ||
|
|
40a0964f06 | ||
|
|
08a9e60ed0 | ||
|
|
3e3c023c95 | ||
|
|
5e6d5c06a9 | ||
|
|
1622b7877d | ||
|
|
80aae2796d | ||
|
|
528ef40fc4 | ||
|
|
4b4b9b7b6f | ||
|
|
ae842259f5 | ||
|
|
69f51cc794 | ||
|
|
7178b9d6b7 | ||
|
|
8a14eee67a | ||
|
|
f9331ee2b9 | ||
|
|
c5315f86fb | ||
|
|
5820bb8f49 | ||
|
|
80278c545e | ||
|
|
cb05f1aadf | ||
|
|
ab616f1a1d | ||
|
|
820a0da4c1 | ||
|
|
fcd02b1ee2 | ||
|
|
90962f060a | ||
|
|
758f0d7605 | ||
|
|
eb8510ff5c | ||
|
|
d5fdd676f4 | ||
|
|
b0d07ffaba | ||
|
|
196d3a6996 | ||
|
|
a3e3a3bbf3 | ||
|
|
abcad6fa45 | ||
|
|
1b6cf58a1a | ||
|
|
6501890ab5 | ||
|
|
e399d21fb3 | ||
|
|
16f355f0cc | ||
|
|
7d1444e5b6 | ||
|
|
25d5936337 | ||
|
|
68f9bf5dfa | ||
|
|
94d45a036f | ||
|
|
9acca2252f | ||
|
|
716fc97b70 | ||
|
|
a809a71aa6 | ||
|
|
4617a5e310 | ||
|
|
e77c5a3a5e | ||
|
|
c3cc0fdd8c | ||
|
|
bd4449c462 | ||
|
|
e3e96745cc | ||
|
|
12e0cefba1 | ||
|
|
21221f73cc | ||
|
|
ab5e9e393b | ||
|
|
507c73c073 | ||
|
|
ba0ea8953b | ||
|
|
e62571c8f4 | ||
|
|
53763d432b | ||
|
|
154920a0b3 | ||
|
|
98f9de2af6 | ||
|
|
cb497826be | ||
|
|
ba0d3842a9 | ||
|
|
29e1935c65 | ||
|
|
33e02b2796 | ||
|
|
26f7588479 | ||
|
|
83c81f6c41 | ||
|
|
791667a9e4 | ||
|
|
c02ddd692f | ||
|
|
151a3fba9d | ||
|
|
1e056842fe | ||
|
|
ebb0c67ecc | ||
|
|
abd9186d00 | ||
|
|
08d3f72755 | ||
|
|
ee5b2ce5b0 | ||
|
|
d54c9678d0 | ||
|
|
859e6af972 | ||
|
|
8c3c9f115d | ||
|
|
779781173a | ||
|
|
2df4d75565 | ||
|
|
dc21ab63ac | ||
|
|
c9c493b2fe | ||
|
|
8d4718f875 | ||
|
|
e4c27092cd | ||
|
|
adadf1fb90 | ||
|
|
380b5b62ef | ||
|
|
a0e7a59572 | ||
|
|
fb6c0c1d8b | ||
|
|
fcf1116e33 | ||
|
|
78dc63df27 | ||
|
|
c0dfba2ef3 | ||
|
|
b1f553eba3 | ||
|
|
0c4aac5a35 | ||
|
|
e0081b59be | ||
|
|
4bd574daee | ||
|
|
b59189ab48 | ||
|
|
e5e3166747 | ||
|
|
6290747bf9 | ||
|
|
b6f4220493 | ||
|
|
afbf293c94 | ||
|
|
b7860c782b | ||
|
|
7baef1e120 | ||
|
|
9a24ce5fad | ||
|
|
9fcb8f1305 | ||
|
|
5049b615c5 | ||
|
|
1051817d92 | ||
|
|
517d038e5b | ||
|
|
3db79ebbf3 | ||
|
|
a2faeb9a26 | ||
|
|
3764ebf7a3 | ||
|
|
a7d5adb3ce | ||
|
|
6f507c322a | ||
|
|
54013671a7 | ||
|
|
f5dea25b6c |
@@ -11,29 +11,53 @@
|
||||
"slug": "latest",
|
||||
"upcoming": true
|
||||
},
|
||||
{
|
||||
"name": "3.3",
|
||||
"branchName": "3.3.x",
|
||||
"slug": "3.3",
|
||||
"upcoming": true
|
||||
},
|
||||
{
|
||||
"name": "3.2",
|
||||
"branchName": "3.2.x",
|
||||
"slug": "3.2",
|
||||
"current": true
|
||||
},
|
||||
{
|
||||
"name": "3.1",
|
||||
"branchName": "3.1.x",
|
||||
"slug": "3.1",
|
||||
"upcoming": true
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "3.0",
|
||||
"branchName": "3.0.x",
|
||||
"slug": "3.0",
|
||||
"current": true
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.21",
|
||||
"branchName": "2.21.x",
|
||||
"slug": "2.21",
|
||||
"upcoming": true
|
||||
},
|
||||
{
|
||||
"name": "2.20",
|
||||
"branchName": "2.20.x",
|
||||
"slug": "2.20",
|
||||
"maintained": true
|
||||
},
|
||||
{
|
||||
"name": "2.19",
|
||||
"branchName": "2.19.x",
|
||||
"slug": "2.19",
|
||||
"upcoming": true
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.18",
|
||||
"branchName": "2.18.x",
|
||||
"slug": "2.18",
|
||||
"maintained": true
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.17",
|
||||
@@ -82,42 +106,6 @@
|
||||
"branchName": "2.10.x",
|
||||
"slug": "2.10",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.9",
|
||||
"branchName": "2.9.x",
|
||||
"slug": "2.9",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.8",
|
||||
"branchName": "2.8.x",
|
||||
"slug": "2.8",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.7",
|
||||
"branchName": "2.7",
|
||||
"slug": "2.7",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.6",
|
||||
"branchName": "2.6",
|
||||
"slug": "2.6",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.5",
|
||||
"branchName": "2.5",
|
||||
"slug": "2.5",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.4",
|
||||
"branchName": "2.4",
|
||||
"slug": "2.4",
|
||||
"maintained": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
9
.github/dependabot.yml
vendored
Normal file
9
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "CI"
|
||||
target-branch: "2.19.x"
|
||||
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@3.0.0"
|
||||
uses: "doctrine/.github/.github/workflows/coding-standards.yml@5.1.0"
|
||||
|
||||
38
.github/workflows/continuous-integration.yml
vendored
38
.github/workflows/continuous-integration.yml
vendored
@@ -36,6 +36,7 @@ jobs:
|
||||
- "8.1"
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
- "8.4"
|
||||
dbal-version:
|
||||
- "default"
|
||||
- "3.7"
|
||||
@@ -75,7 +76,7 @@ jobs:
|
||||
if: "${{ matrix.dbal-version != 'default' }}"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v2"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
with:
|
||||
composer-options: "--ignore-platform-req=php+"
|
||||
dependency-versions: "${{ matrix.deps }}"
|
||||
@@ -91,9 +92,9 @@ jobs:
|
||||
ENABLE_SECOND_LEVEL_CACHE: 1
|
||||
|
||||
- name: "Upload coverage file"
|
||||
uses: "actions/upload-artifact@v3"
|
||||
uses: "actions/upload-artifact@v4"
|
||||
with:
|
||||
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
|
||||
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-coverage"
|
||||
path: "coverage*.xml"
|
||||
|
||||
|
||||
@@ -107,6 +108,7 @@ jobs:
|
||||
php-version:
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
- "8.4"
|
||||
dbal-version:
|
||||
- "default"
|
||||
- "3.7"
|
||||
@@ -156,7 +158,7 @@ jobs:
|
||||
if: "${{ matrix.dbal-version != 'default' }}"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v2"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
with:
|
||||
composer-options: "--ignore-platform-req=php+"
|
||||
|
||||
@@ -164,9 +166,9 @@ jobs:
|
||||
run: "vendor/bin/phpunit -c ci/github/phpunit/pdo_pgsql.xml --coverage-clover=coverage.xml"
|
||||
|
||||
- name: "Upload coverage file"
|
||||
uses: "actions/upload-artifact@v3"
|
||||
uses: "actions/upload-artifact@v4"
|
||||
with:
|
||||
name: "${{ github.job }}-${{ matrix.postgres-version }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
|
||||
name: "${{ github.job }}-${{ matrix.postgres-version }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.extension }}-coverage"
|
||||
path: "coverage.xml"
|
||||
|
||||
|
||||
@@ -180,12 +182,13 @@ jobs:
|
||||
php-version:
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
- "8.4"
|
||||
dbal-version:
|
||||
- "default"
|
||||
- "3.7"
|
||||
- "4@dev"
|
||||
mariadb-version:
|
||||
- "10.9"
|
||||
- "11.4"
|
||||
extension:
|
||||
- "mysqli"
|
||||
- "pdo_mysql"
|
||||
@@ -194,11 +197,11 @@ jobs:
|
||||
mariadb:
|
||||
image: "mariadb:${{ matrix.mariadb-version }}"
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: yes
|
||||
MYSQL_DATABASE: "doctrine_tests"
|
||||
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: yes
|
||||
MARIADB_DATABASE: "doctrine_tests"
|
||||
|
||||
options: >-
|
||||
--health-cmd "mysqladmin ping --silent"
|
||||
--health-cmd "healthcheck.sh --connect --innodb_initialized"
|
||||
|
||||
ports:
|
||||
- "3306:3306"
|
||||
@@ -222,7 +225,7 @@ jobs:
|
||||
extensions: "${{ matrix.extension }}"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v2"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
with:
|
||||
composer-options: "--ignore-platform-req=php+"
|
||||
|
||||
@@ -230,7 +233,7 @@ jobs:
|
||||
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --coverage-clover=coverage.xml"
|
||||
|
||||
- name: "Upload coverage file"
|
||||
uses: "actions/upload-artifact@v3"
|
||||
uses: "actions/upload-artifact@v4"
|
||||
with:
|
||||
name: "${{ github.job }}-${{ matrix.mariadb-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
|
||||
path: "coverage.xml"
|
||||
@@ -246,6 +249,7 @@ jobs:
|
||||
php-version:
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
- "8.4"
|
||||
dbal-version:
|
||||
- "default"
|
||||
- "3.7"
|
||||
@@ -296,7 +300,7 @@ jobs:
|
||||
if: "${{ matrix.dbal-version != 'default' }}"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v2"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
with:
|
||||
composer-options: "--ignore-platform-req=php+"
|
||||
|
||||
@@ -311,7 +315,7 @@ jobs:
|
||||
ENABLE_SECOND_LEVEL_CACHE: 1
|
||||
|
||||
- name: "Upload coverage files"
|
||||
uses: "actions/upload-artifact@v3"
|
||||
uses: "actions/upload-artifact@v4"
|
||||
with:
|
||||
name: "${{ github.job }}-${{ matrix.mysql-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
|
||||
path: "coverage*.xml"
|
||||
@@ -332,11 +336,13 @@ jobs:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: "Download coverage files"
|
||||
uses: "actions/download-artifact@v3"
|
||||
uses: "actions/download-artifact@v4"
|
||||
with:
|
||||
path: "reports"
|
||||
|
||||
- name: "Upload to Codecov"
|
||||
uses: "codecov/codecov-action@v3"
|
||||
uses: "codecov/codecov-action@v4"
|
||||
with:
|
||||
directory: reports
|
||||
env:
|
||||
CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}"
|
||||
|
||||
38
.github/workflows/documentation.yml
vendored
38
.github/workflows/documentation.yml
vendored
@@ -5,40 +5,16 @@ on:
|
||||
branches:
|
||||
- "*.x"
|
||||
paths:
|
||||
- .github/workflows/documentation.yml
|
||||
- docs/**
|
||||
- ".github/workflows/documentation.yml"
|
||||
- "docs/**"
|
||||
push:
|
||||
branches:
|
||||
- "*.x"
|
||||
paths:
|
||||
- .github/workflows/documentation.yml
|
||||
- docs/**
|
||||
- ".github/workflows/documentation.yml"
|
||||
- "docs/**"
|
||||
|
||||
jobs:
|
||||
validate-with-guides:
|
||||
name: "Validate documentation with phpDocumentor/guides"
|
||||
runs-on: "ubuntu-22.04"
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: "actions/checkout@v4"
|
||||
|
||||
- name: "Install PHP"
|
||||
uses: "shivammathur/setup-php@v2"
|
||||
with:
|
||||
coverage: "none"
|
||||
php-version: "8.2"
|
||||
|
||||
- name: "Remove existing composer file"
|
||||
run: "rm composer.json"
|
||||
|
||||
- name: "Require phpdocumentor/guides-cli"
|
||||
run: "composer require --dev phpdocumentor/guides-cli --no-update"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v2"
|
||||
with:
|
||||
dependency-versions: "highest"
|
||||
|
||||
- name: "Run guides-cli"
|
||||
run: "vendor/bin/guides -vvv --no-progress docs/en 2>&1 | grep -v 'No template found for rendering directive' | ( ! grep WARNING )"
|
||||
documentation:
|
||||
name: "Documentation"
|
||||
uses: "doctrine/.github/.github/workflows/documentation.yml@5.1.0"
|
||||
|
||||
11
.github/workflows/phpbench.yml
vendored
11
.github/workflows/phpbench.yml
vendored
@@ -47,15 +47,8 @@ jobs:
|
||||
coverage: "pcov"
|
||||
ini-values: "zend.assertions=1, apc.enable_cli=1"
|
||||
|
||||
- name: "Cache dependencies installed with composer"
|
||||
uses: "actions/cache@v3"
|
||||
with:
|
||||
path: "~/.composer/cache"
|
||||
key: "php-${{ matrix.php-version }}-composer-locked-${{ hashFiles('composer.lock') }}"
|
||||
restore-keys: "php-${{ matrix.php-version }}-composer-locked-"
|
||||
|
||||
- name: "Install dependencies with composer"
|
||||
run: "composer update --no-interaction --no-progress"
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
|
||||
- name: "Run PHPBench"
|
||||
run: "vendor/bin/phpbench run --report=default"
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@3.0.0"
|
||||
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@5.1.0"
|
||||
secrets:
|
||||
GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
|
||||
GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}
|
||||
|
||||
6
.github/workflows/static-analysis.yml
vendored
6
.github/workflows/static-analysis.yml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
coverage: none
|
||||
php-version: "8.2"
|
||||
php-version: "8.3"
|
||||
tools: cs2pr
|
||||
|
||||
- name: Require specific DBAL version
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
coverage: none
|
||||
php-version: "8.2"
|
||||
php-version: "8.3"
|
||||
tools: cs2pr
|
||||
|
||||
- name: Require specific DBAL version
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
if: "${{ matrix.dbal-version != 'default' }}"
|
||||
|
||||
- name: Install dependencies with Composer
|
||||
uses: ramsey/composer-install@v2
|
||||
uses: ramsey/composer-install@v3
|
||||
|
||||
- name: Run static analysis with Vimeo Psalm
|
||||
run: vendor/bin/psalm --shepherd
|
||||
|
||||
32
README.md
32
README.md
@@ -1,7 +1,7 @@
|
||||
| [4.0.x][4.0] | [3.1.x][3.1] | [3.0.x][3.0] | [2.19.x][2.19] | [2.18.x][2.18] |
|
||||
|:------------------------------------------------------:|:------------------------------------------------------:|:-------------------------------------------------------:|:--------------------------------------------------------:|:---------------------------------------------------------:|
|
||||
| [![Build status][4.0 image]][4.0] | [![Build status][3.1 image]][3.1] | [![Build status][3.0 image]][3.0] | [![Build status][2.19 image]][2.19] | [![Build status][2.18 image]][2.18] |
|
||||
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.1 coverage image]][3.1 coverage] | [![Coverage Status][3.0 coverage image]][3.0 coverage] | [![Coverage Status][2.19 coverage image]][2.19 coverage] | [![Coverage Status][2.18 coverage image]][2.18 coverage] |
|
||||
| [4.0.x][4.0] | [3.3.x][3.3] | [3.2.x][3.2] | [2.20.x][2.20] | [2.19.x][2.19] |
|
||||
|:------------------------------------------------------:|:------------------------------------------------------:|:------------------------------------------------------:|:--------------------------------------------------------:|:--------------------------------------------------------:|
|
||||
| [![Build status][4.0 image]][4.0] | [![Build status][3.3 image]][3.3] | [![Build status][3.2 image]][3.2] | [![Build status][2.20 image]][2.20] | [![Build status][2.19 image]][2.19] |
|
||||
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.3 coverage image]][3.3 coverage] | [![Coverage Status][3.2 coverage image]][3.2 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] | [![Coverage Status][2.19 coverage image]][2.19 coverage] |
|
||||
|
||||
[<h1 align="center">🇺🇦 UKRAINE NEEDS YOUR HELP NOW!</h1>](https://www.doctrine-project.org/stop-war.html)
|
||||
|
||||
@@ -22,19 +22,19 @@ without requiring unnecessary code duplication.
|
||||
[4.0]: https://github.com/doctrine/orm/tree/4.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.1 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.1.x
|
||||
[3.1]: https://github.com/doctrine/orm/tree/3.1.x
|
||||
[3.1 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.1.x/graph/badge.svg
|
||||
[3.1 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.1.x
|
||||
[3.0 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.0.x
|
||||
[3.0]: https://github.com/doctrine/orm/tree/3.0.x
|
||||
[3.0 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.0.x/graph/badge.svg
|
||||
[3.0 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.0.x
|
||||
[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.2 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.2.x
|
||||
[3.2]: https://github.com/doctrine/orm/tree/3.2.x
|
||||
[3.2 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.2.x/graph/badge.svg
|
||||
[3.2 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.2.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 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
|
||||
[2.19 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.19.x
|
||||
[2.19]: https://github.com/doctrine/orm/tree/2.19.x
|
||||
[2.19 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.19.x/graph/badge.svg
|
||||
[2.19 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.19.x
|
||||
[2.18 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.18.x
|
||||
[2.18]: https://github.com/doctrine/orm/tree/2.18.x
|
||||
[2.18 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.18.x/graph/badge.svg
|
||||
[2.18 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.18.x
|
||||
|
||||
116
UPGRADE.md
116
UPGRADE.md
@@ -1,3 +1,58 @@
|
||||
# Upgrade to 3.3
|
||||
|
||||
## Deprecate `DatabaseDriver`
|
||||
|
||||
The class `Doctrine\ORM\Mapping\Driver\DatabaseDriver` is deprecated without replacement.
|
||||
|
||||
# Upgrade to 3.2
|
||||
|
||||
## Deprecate the `NotSupported` exception
|
||||
|
||||
The class `Doctrine\ORM\Exception\NotSupported` is deprecated without replacement.
|
||||
|
||||
## Deprecate remaining `Serializable` implementation
|
||||
|
||||
Relying on `SequenceGenerator` implementing the `Serializable` is deprecated
|
||||
because that interface won't be implemented in ORM 4 anymore.
|
||||
|
||||
The following methods are deprecated:
|
||||
|
||||
* `SequenceGenerator::serialize()`
|
||||
* `SequenceGenerator::unserialize()`
|
||||
|
||||
## `orm:schema-tool:update` option `--complete` is deprecated
|
||||
|
||||
That option behaves as a no-op, and is deprecated. It will be removed in 4.0.
|
||||
|
||||
## Deprecate properties `$indexes` and `$uniqueConstraints` of `Doctrine\ORM\Mapping\Table`
|
||||
|
||||
The properties `$indexes` and `$uniqueConstraints` have been deprecated since they had no effect at all.
|
||||
The preferred way of defining indices and unique constraints is by
|
||||
using the `\Doctrine\ORM\Mapping\UniqueConstraint` and `\Doctrine\ORM\Mapping\Index` attributes.
|
||||
|
||||
# Upgrade to 3.1
|
||||
|
||||
## Deprecate `Doctrine\ORM\Mapping\ReflectionEnumProperty`
|
||||
|
||||
This class is deprecated and will be removed in 4.0.
|
||||
Instead, use `Doctrine\Persistence\Reflection\EnumReflectionProperty` from
|
||||
`doctrine/persistence`.
|
||||
|
||||
## Deprecate passing null to `ClassMetadata::fullyQualifiedClassName()`
|
||||
|
||||
Passing `null` to `Doctrine\ORM\ClassMetadata::fullyQualifiedClassName()` is
|
||||
deprecated and will no longer be possible in 4.0.
|
||||
|
||||
## Deprecate array access
|
||||
|
||||
Using array access on instances of the following classes is deprecated:
|
||||
|
||||
- `Doctrine\ORM\Mapping\DiscriminatorColumnMapping`
|
||||
- `Doctrine\ORM\Mapping\EmbedClassMapping`
|
||||
- `Doctrine\ORM\Mapping\FieldMapping`
|
||||
- `Doctrine\ORM\Mapping\JoinColumnMapping`
|
||||
- `Doctrine\ORM\Mapping\JoinTableMapping`
|
||||
|
||||
# Upgrade to 3.0
|
||||
|
||||
## BC BREAK: Calling `ClassMetadata::getAssociationMappedByTargetField()` with the owning side of an association now throws an exception
|
||||
@@ -46,9 +101,11 @@ now they throw an exception.
|
||||
|
||||
## BC BREAK: Partial objects are removed
|
||||
|
||||
- The `PARTIAL` keyword in DQL no longer exists.
|
||||
- `Doctrine\ORM\Query\AST\PartialObjectExpression`is removed.
|
||||
- `Doctrine\ORM\Query\SqlWalker::HINT_PARTIAL` and
|
||||
WARNING: This was relaxed in ORM 3.2 when partial was re-allowed for array-hydration.
|
||||
|
||||
- The `PARTIAL` keyword in DQL no longer exists (reintroduced in ORM 3.2)
|
||||
- `Doctrine\ORM\Query\AST\PartialObjectExpression` is removed. (reintroduced in ORM 3.2)
|
||||
- `Doctrine\ORM\Query\SqlWalker::HINT_PARTIAL` (reintroduced in ORM 3.2) and
|
||||
`Doctrine\ORM\Query::HINT_FORCE_PARTIAL_LOAD` are removed.
|
||||
- `Doctrine\ORM\EntityManager*::getPartialReference()` is removed.
|
||||
|
||||
@@ -674,6 +731,59 @@ following classes and methods:
|
||||
|
||||
Use `toIterable()` instead.
|
||||
|
||||
# Upgrade to 2.20
|
||||
|
||||
## Add `Doctrine\ORM\Query\OutputWalker` interface, deprecate `Doctrine\ORM\Query\SqlWalker::getExecutor()`
|
||||
|
||||
Output walkers should implement the new `\Doctrine\ORM\Query\OutputWalker` interface and create
|
||||
`Doctrine\ORM\Query\Exec\SqlFinalizer` instances instead of `Doctrine\ORM\Query\Exec\AbstractSqlExecutor`s.
|
||||
The output walker must not base its workings on the query `firstResult`/`maxResult` values, so that the
|
||||
`SqlFinalizer` can be kept in the query cache and used regardless of the actual `firstResult`/`maxResult` values.
|
||||
Any operation dependent on `firstResult`/`maxResult` should take place within the `SqlFinalizer::createExecutor()`
|
||||
method. Details can be found at https://github.com/doctrine/orm/pull/11188.
|
||||
|
||||
## Explictly forbid property hooks
|
||||
|
||||
Property hooks are not supported yet by Doctrine ORM. Until support is added,
|
||||
they are explicitly forbidden because the support would result in a breaking
|
||||
change in behavior.
|
||||
|
||||
Progress on this is tracked at https://github.com/doctrine/orm/issues/11624 .
|
||||
|
||||
## PARTIAL DQL syntax is undeprecated
|
||||
|
||||
Use of the PARTIAL keyword is not deprecated anymore in DQL, because we will be
|
||||
able to support PARTIAL objects with PHP 8.4 Lazy Objects and
|
||||
Symfony/VarExporter in a better way. When we decided to remove this feature
|
||||
these two abstractions did not exist yet.
|
||||
|
||||
WARNING: If you want to upgrade to 3.x and still use PARTIAL keyword in DQL
|
||||
with array or object hydrators, then you have to directly migrate to ORM 3.3.x or higher.
|
||||
PARTIAL keyword in DQL is not available in 3.0, 3.1 and 3.2 of ORM.
|
||||
|
||||
## Deprecate `\Doctrine\ORM\Query\Parser::setCustomOutputTreeWalker()`
|
||||
|
||||
Use the `\Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER` query hint to set the output walker
|
||||
class instead of setting it through the `\Doctrine\ORM\Query\Parser::setCustomOutputTreeWalker()` method
|
||||
on the parser instance.
|
||||
|
||||
# Upgrade to 2.19
|
||||
|
||||
## Deprecate calling `ClassMetadata::getAssociationMappedByTargetField()` with the owning side of an association
|
||||
|
||||
Calling
|
||||
`Doctrine\ORM\Mapping\ClassMetadata::getAssociationMappedByTargetField()` with
|
||||
the owning side of an association returns `null`, which is undocumented, and
|
||||
wrong according to the phpdoc of the parent method.
|
||||
|
||||
If you do not know whether you are on the owning or inverse side of an association,
|
||||
you can use `Doctrine\ORM\Mapping\ClassMetadata::isAssociationInverseSide()`
|
||||
to find out.
|
||||
|
||||
## Deprecate `Doctrine\ORM\Query\Lexer::T_*` constants
|
||||
|
||||
Use `Doctrine\ORM\Query\TokenType::T_*` instead.
|
||||
|
||||
# Upgrade to 2.17
|
||||
|
||||
## Deprecate annotations classes for named queries
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"composer/package-versions-deprecated": true,
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true,
|
||||
"phpstan/extension-installer": true
|
||||
},
|
||||
"sort-packages": true
|
||||
},
|
||||
@@ -23,27 +24,30 @@
|
||||
"php": "^8.1",
|
||||
"composer-runtime-api": "^2",
|
||||
"ext-ctype": "*",
|
||||
"doctrine/collections": "^2.1",
|
||||
"doctrine/collections": "^2.2",
|
||||
"doctrine/dbal": "^3.8.2 || ^4",
|
||||
"doctrine/deprecations": "^0.5.3 || ^1",
|
||||
"doctrine/event-manager": "^1.2 || ^2",
|
||||
"doctrine/inflector": "^1.4 || ^2.0",
|
||||
"doctrine/instantiator": "^1.3 || ^2",
|
||||
"doctrine/lexer": "^3",
|
||||
"doctrine/persistence": "^3.1.1",
|
||||
"doctrine/persistence": "^3.3.1",
|
||||
"psr/cache": "^1 || ^2 || ^3",
|
||||
"symfony/console": "^5.4 || ^6.0 || ^7.0",
|
||||
"symfony/var-exporter": "~6.2.13 || ^6.3.2 || ^7.0"
|
||||
"symfony/var-exporter": "^6.3.9 || ^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^12.0",
|
||||
"phpbench/phpbench": "^1.0",
|
||||
"phpstan/phpstan": "1.10.59",
|
||||
"phpdocumentor/guides-cli": "^1.4",
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpstan/phpstan": "1.12.6",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.2",
|
||||
"phpunit/phpunit": "^10.4.0",
|
||||
"psr/log": "^1 || ^2 || ^3",
|
||||
"squizlabs/php_codesniffer": "3.7.2",
|
||||
"symfony/cache": "^5.4 || ^6.2 || ^7.0",
|
||||
"vimeo/psalm": "5.22.2"
|
||||
"vimeo/psalm": "5.24.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-dom": "Provides support for XSD validation for XML mapping files",
|
||||
|
||||
4
docs/.gitignore
vendored
4
docs/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
en/_exts/configurationblock.pyc
|
||||
build
|
||||
en/_build
|
||||
.idea
|
||||
3
docs/.gitmodules
vendored
3
docs/.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "en/_theme"]
|
||||
path = en/_theme
|
||||
url = https://github.com/doctrine/doctrine-sphinx-theme.git
|
||||
Submodule docs/en/_theme deleted from 6f1bc8bead
@@ -232,6 +232,33 @@ vendors SQL parser to show us further errors in the parsing
|
||||
process, for example if the Unit would not be one of the supported
|
||||
values by MySql.
|
||||
|
||||
Typed functions
|
||||
---------------
|
||||
By default, result of custom functions is fetched as-is from the database driver.
|
||||
If you want to be sure that the type is always the same, then your custom function needs to
|
||||
implement ``Doctrine\ORM\Query\AST\TypedExpression``. Then, the result is wired
|
||||
through ``Doctrine\DBAL\Types\Type::convertToPhpValue()`` of the ``Type`` returned in ``getReturnType()``.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
|
||||
use Doctrine\ORM\Query\AST\TypedExpression;
|
||||
|
||||
class DateDiff extends FunctionNode implements TypedExpression
|
||||
{
|
||||
// ...
|
||||
|
||||
public function getReturnType(): Type
|
||||
{
|
||||
return Type::getType(Types::INTEGER);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Conclusion
|
||||
----------
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ What we offer are hooks to execute any kind of validation.
|
||||
.. note::
|
||||
|
||||
You don't need to validate your entities in the lifecycle
|
||||
events. Its only one of many options. Of course you can also
|
||||
events. It is only one of many options. Of course you can also
|
||||
perform validations in value setters or any other method of your
|
||||
entities that are used in your code.
|
||||
|
||||
|
||||
@@ -73,6 +73,8 @@ Advanced Topics
|
||||
* :doc:`TypedFieldMapper <reference/typedfieldmapper>`
|
||||
* :doc:`Improving Performance <reference/improving-performance>`
|
||||
* :doc:`Caching <reference/caching>`
|
||||
* :doc:`Partial Hydration <reference/partial-hydration>`
|
||||
* :doc:`Partial Objects <reference/partial-objects>`
|
||||
* :doc:`Change Tracking Policies <reference/change-tracking-policies>`
|
||||
* :doc:`Best Practices <reference/best-practices>`
|
||||
* :doc:`Metadata Drivers <reference/metadata-drivers>`
|
||||
|
||||
@@ -18,7 +18,7 @@ well.
|
||||
Requirements
|
||||
------------
|
||||
|
||||
Doctrine ORM requires a minimum of PHP 7.1. For greatly improved
|
||||
Doctrine ORM requires a minimum of PHP 8.1. For greatly improved
|
||||
performance it is also recommended that you use APC with PHP.
|
||||
|
||||
Doctrine ORM Packages
|
||||
|
||||
@@ -870,8 +870,8 @@ This is essentially the same as the following, more verbose, mapping:
|
||||
* @var Collection<int, Group>
|
||||
*/
|
||||
#[JoinTable(name: 'User_Group')]
|
||||
#[JoinColumn(name: 'User_id', referencedColumnName: 'id')]
|
||||
#[InverseJoinColumn(name: 'Group_id', referencedColumnName: 'id')]
|
||||
#[JoinColumn(name: 'user_id', referencedColumnName: 'id')]
|
||||
#[InverseJoinColumn(name: 'group_id', referencedColumnName: 'id')]
|
||||
#[ManyToMany(targetEntity: Group::class)]
|
||||
private Collection $groups;
|
||||
// ...
|
||||
@@ -884,10 +884,10 @@ This is essentially the same as the following, more verbose, mapping:
|
||||
<many-to-many field="groups" target-entity="Group">
|
||||
<join-table name="User_Group">
|
||||
<join-columns>
|
||||
<join-column id="User_id" referenced-column-name="id" />
|
||||
<join-column id="user_id" referenced-column-name="id" />
|
||||
</join-columns>
|
||||
<inverse-join-columns>
|
||||
<join-column id="Group_id" referenced-column-name="id" />
|
||||
<join-column id="group_id" referenced-column-name="id" />
|
||||
</inverse-join-columns>
|
||||
</join-table>
|
||||
</many-to-many>
|
||||
|
||||
@@ -15,7 +15,7 @@ Index
|
||||
- :ref:`#[AttributeOverride] <attrref_attributeoverride>`
|
||||
- :ref:`#[Column] <attrref_column>`
|
||||
- :ref:`#[Cache] <attrref_cache>`
|
||||
- :ref:`#[ChangeTrackingPolicy <attrref_changetrackingpolicy>`
|
||||
- :ref:`#[ChangeTrackingPolicy] <attrref_changetrackingpolicy>`
|
||||
- :ref:`#[CustomIdGenerator] <attrref_customidgenerator>`
|
||||
- :ref:`#[DiscriminatorColumn] <attrref_discriminatorcolumn>`
|
||||
- :ref:`#[DiscriminatorMap] <attrref_discriminatormap>`
|
||||
|
||||
@@ -228,50 +228,12 @@ and a custom ``Doctrine\ORM\Mapping\TypedFieldMapper`` implementation.
|
||||
Doctrine Mapping Types
|
||||
----------------------
|
||||
|
||||
The ``type`` option used in the ``@Column`` accepts any of the existing
|
||||
Doctrine types or even your own custom types. A Doctrine type defines
|
||||
The ``type`` option used in the ``@Column`` accepts any of the
|
||||
`existing Doctrine DBAL types <https://docs.doctrine-project.org/projects/doctrine-dbal/en/stable/reference/types.html#reference>`_
|
||||
or :doc:`your own custom mapping types
|
||||
<../cookbook/custom-mapping-types>`. A Doctrine type defines
|
||||
the conversion between PHP and SQL types, independent from the database vendor
|
||||
you are using. All Mapping Types that ship with Doctrine are fully portable
|
||||
between the supported database systems.
|
||||
|
||||
As an example, the Doctrine Mapping Type ``string`` defines the
|
||||
mapping from a PHP string to a SQL VARCHAR (or VARCHAR2 etc.
|
||||
depending on the RDBMS brand). Here is a quick overview of the
|
||||
built-in mapping types:
|
||||
|
||||
- ``string``: Type that maps a SQL VARCHAR to a PHP string.
|
||||
- ``integer``: Type that maps a SQL INT to a PHP integer.
|
||||
- ``smallint``: Type that maps a database SMALLINT to a PHP
|
||||
integer.
|
||||
- ``bigint``: Type that maps a database BIGINT to a PHP string.
|
||||
- ``boolean``: Type that maps a SQL boolean or equivalent (TINYINT) to a PHP boolean.
|
||||
- ``decimal``: Type that maps a SQL DECIMAL to a PHP string.
|
||||
- ``date``: Type that maps a SQL DATETIME to a PHP DateTime
|
||||
object.
|
||||
- ``time``: Type that maps a SQL TIME to a PHP DateTime object.
|
||||
- ``datetime``: Type that maps a SQL DATETIME/TIMESTAMP to a PHP
|
||||
DateTime object.
|
||||
- ``datetimetz``: Type that maps a SQL DATETIME/TIMESTAMP to a PHP
|
||||
DateTime object with timezone.
|
||||
- ``text``: Type that maps a SQL CLOB to a PHP string.
|
||||
- ``object``: Type that maps a SQL CLOB to a PHP object using
|
||||
``serialize()`` and ``unserialize()``
|
||||
- ``array``: Type that maps a SQL CLOB to a PHP array using
|
||||
``serialize()`` and ``unserialize()``
|
||||
- ``simple_array``: Type that maps a SQL CLOB to a PHP array using
|
||||
``implode()`` and ``explode()``, with a comma as delimiter. *IMPORTANT*
|
||||
Only use this type if you are sure that your values cannot contain a ",".
|
||||
- ``json_array``: Type that maps a SQL CLOB to a PHP array using
|
||||
``json_encode()`` and ``json_decode()``
|
||||
- ``float``: Type that maps a SQL Float (Double Precision) to a
|
||||
PHP double. *IMPORTANT*: Works only with locale settings that use
|
||||
decimal points as separator.
|
||||
- ``guid``: Type that maps a database GUID/UUID to a PHP string. Defaults to
|
||||
varchar but uses a specific type if the platform supports it.
|
||||
- ``blob``: Type that maps a SQL BLOB to a PHP resource stream
|
||||
|
||||
A cookbook article shows how to define :doc:`your own custom mapping types
|
||||
<../cookbook/custom-mapping-types>`.
|
||||
you are using.
|
||||
|
||||
.. note::
|
||||
|
||||
|
||||
@@ -523,6 +523,34 @@ when the DQL is switched to an arbitrary join.
|
||||
- HAVING is applied to the results of a query after
|
||||
aggregation (GROUP BY)
|
||||
|
||||
|
||||
Partial Hydration Syntax
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
By default when you run a DQL query in Doctrine and select only a
|
||||
subset of the fields for a given entity, you do not receive objects
|
||||
back. Instead, you receive only arrays as a flat rectangular result
|
||||
set, similar to how you would if you were just using SQL directly
|
||||
and joining some data.
|
||||
|
||||
If you want to select partial objects or fields in array hydration you can use the ``partial``
|
||||
DQL keyword:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT partial u.{id, username} FROM CmsUser u');
|
||||
$users = $query->getResult(); // array of partially loaded CmsUser objects
|
||||
|
||||
You can use the partial syntax when joining as well:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT partial u.{id, username}, partial a.{id, name} FROM CmsUser u JOIN u.articles a');
|
||||
$usersArray = $query->getArrayResult(); // array of partially loaded CmsUser and CmsArticle fields
|
||||
$users = $query->getResult(); // array of partially loaded CmsUser objects
|
||||
|
||||
"NEW" Operator Syntax
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@@ -560,7 +588,91 @@ And then use the ``NEW`` DQL keyword :
|
||||
$query = $em->createQuery('SELECT NEW CustomerDTO(c.name, e.email, a.city, SUM(o.value)) FROM Customer c JOIN c.email e JOIN c.address a JOIN c.orders o GROUP BY c');
|
||||
$users = $query->getResult(); // array of CustomerDTO
|
||||
|
||||
Note that you can only pass scalar expressions to the constructor.
|
||||
You can also nest several DTO :
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
class CustomerDTO
|
||||
{
|
||||
public function __construct(string $name, string $email, AddressDTO $address, string|null $value = null)
|
||||
{
|
||||
// Bind values to the object properties.
|
||||
}
|
||||
}
|
||||
|
||||
class AddressDTO
|
||||
{
|
||||
public function __construct(string $street, string $city, string $zip)
|
||||
{
|
||||
// Bind values to the object properties.
|
||||
}
|
||||
}
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT NEW CustomerDTO(c.name, e.email, NEW AddressDTO(a.street, a.city, a.zip)) FROM Customer c JOIN c.email e JOIN c.address a');
|
||||
$users = $query->getResult(); // array of CustomerDTO
|
||||
|
||||
Note that you can only pass scalar expressions or other Data Transfer Objects to the constructor.
|
||||
|
||||
If you use your data transfer objects for multiple queries, and you would rather not have to
|
||||
specify arguments that precede the ones you are really interested in, you can use named arguments.
|
||||
|
||||
Consider the following DTO, which uses optional arguments:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
class CustomerDTO
|
||||
{
|
||||
public function __construct(
|
||||
public string|null $name = null,
|
||||
public string|null $email = null,
|
||||
public string|null $city = null,
|
||||
public mixed|null $value = null,
|
||||
public AddressDTO|null $address = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
You can specify arbitrary arguments in an arbitrary order by using the named argument syntax, and the ORM will try to match argument names with the selected column names.
|
||||
The syntax relies on the NAMED keyword, like so:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT NEW NAMED CustomerDTO(a.city, c.name) FROM Customer c JOIN c.address a');
|
||||
$users = $query->getResult(); // array of CustomerDTO
|
||||
|
||||
// CustomerDTO => {name : 'SMITH', email: null, city: 'London', value: null}
|
||||
|
||||
ORM will also give precedence to column aliases over column names :
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT NEW NAMED CustomerDTO(c.name, CONCAT(a.city, ' ' , a.zip) AS value) FROM Customer c JOIN c.address a');
|
||||
$users = $query->getResult(); // array of CustomerDTO
|
||||
|
||||
// CustomerDTO => {name : 'DOE', email: null, city: null, value: 'New York 10011'}
|
||||
|
||||
To define a custom name for a DTO constructor argument, you can either alias the column with the ``AS`` keyword.
|
||||
|
||||
The ``NAMED`` keyword must precede all DTO you want to instantiate :
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT NEW NAMED CustomerDTO(c.name, NEW NAMED AddressDTO(a.street, a.city, a.zip) AS address) FROM Customer c JOIN c.address a');
|
||||
$users = $query->getResult(); // array of CustomerDTO
|
||||
|
||||
// CustomerDTO => {name : 'DOE', email: null, city: null, value: 'New York 10011'}
|
||||
|
||||
If two arguments have the same name, a ``DuplicateFieldException`` is thrown.
|
||||
If a field cannot be matched with a property name, a ``NoMatchingPropertyException`` is thrown. This typically happens when using functions without aliasing them.
|
||||
|
||||
Using INDEX BY
|
||||
~~~~~~~~~~~~~~
|
||||
@@ -976,7 +1088,7 @@ The Query class
|
||||
---------------
|
||||
|
||||
An instance of the ``Doctrine\ORM\Query`` class represents a DQL
|
||||
query. You create a Query instance be calling
|
||||
query. You create a Query instance by calling
|
||||
``EntityManager#createQuery($dql)``, passing the DQL query string.
|
||||
Alternatively you can create an empty ``Query`` instance and invoke
|
||||
``Query#setDQL($dql)`` afterwards. Here are some examples:
|
||||
@@ -993,58 +1105,146 @@ Alternatively you can create an empty ``Query`` instance and invoke
|
||||
$q = $em->createQuery();
|
||||
$q->setDQL('select u from MyProject\Model\User u');
|
||||
|
||||
Query Result Formats
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
Query Result Formats (Hydration Modes)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The format in which the result of a DQL SELECT query is returned
|
||||
can be influenced by a so-called ``hydration mode``. A hydration
|
||||
mode specifies a particular way in which a SQL result set is
|
||||
transformed. Each hydration mode has its own dedicated method on
|
||||
the Query class. Here they are:
|
||||
The way in which the SQL result set of a DQL SELECT query is transformed
|
||||
to PHP is determined by the so-called "hydration mode".
|
||||
|
||||
``getResult()``
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
- ``Query#getResult()``: Retrieves a collection of objects. The
|
||||
result is either a plain collection of objects (pure) or an array
|
||||
where the objects are nested in the result rows (mixed).
|
||||
- ``Query#getSingleResult()``: Retrieves a single object. If the
|
||||
result contains more than one object, an ``NonUniqueResultException``
|
||||
is thrown. If the result contains no objects, an ``NoResultException``
|
||||
is thrown. The pure/mixed distinction does not apply.
|
||||
- ``Query#getOneOrNullResult()``: Retrieve a single object. If the
|
||||
result contains more than one object, a ``NonUniqueResultException``
|
||||
is thrown. If no object is found null will be returned.
|
||||
- ``Query#getArrayResult()``: Retrieves an array graph (a nested
|
||||
array) that is largely interchangeable with the object graph
|
||||
generated by ``Query#getResult()`` for read-only purposes.
|
||||
Retrieves a collection of objects. The result is either a plain collection of objects (pure) or an array
|
||||
where the objects are nested in the result rows (mixed):
|
||||
|
||||
.. note::
|
||||
.. code-block:: php
|
||||
|
||||
An array graph can differ from the corresponding object
|
||||
graph in certain scenarios due to the difference of the identity
|
||||
semantics between arrays and objects.
|
||||
<?php
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
|
||||
$query = $em->createQuery('SELECT u FROM User u');
|
||||
$users = $query->getResult();
|
||||
// same as:
|
||||
$users = $query->getResult(AbstractQuery::HYDRATE_OBJECT);
|
||||
|
||||
- Objects fetched in a FROM clause are returned as a Set, that means every
|
||||
object is only ever included in the resulting array once. This is the case
|
||||
even when using JOIN or GROUP BY in ways that return the same row for an
|
||||
object multiple times. If the hydrator sees the same object multiple times,
|
||||
then it makes sure it is only returned once.
|
||||
|
||||
- ``Query#getScalarResult()``: Retrieves a flat/rectangular result
|
||||
set of scalar values that can contain duplicate data. The
|
||||
pure/mixed distinction does not apply.
|
||||
- ``Query#getSingleScalarResult()``: Retrieves a single scalar
|
||||
value from the result returned by the dbms. If the result contains
|
||||
more than a single scalar value, an exception is thrown. The
|
||||
pure/mixed distinction does not apply.
|
||||
- If an object is already in memory from a previous query of any kind, then
|
||||
then the previous object is used, even if the database may contain more
|
||||
recent data. This even happens if the previous object is still an unloaded proxy.
|
||||
|
||||
Instead of using these methods, you can alternatively use the
|
||||
general-purpose method
|
||||
``Query#execute(array $params = [], $hydrationMode = Query::HYDRATE_OBJECT)``.
|
||||
Using this method you can directly supply the hydration mode as the
|
||||
second parameter via one of the Query constants. In fact, the
|
||||
methods mentioned earlier are just convenient shortcuts for the
|
||||
execute method. For example, the method ``Query#getResult()``
|
||||
internally invokes execute, passing in ``Query::HYDRATE_OBJECT`` as
|
||||
the hydration mode.
|
||||
``getArrayResult()``
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The use of the methods mentioned earlier is generally preferred as
|
||||
it leads to more concise code.
|
||||
Retrieves an array graph (a nested array) for read-only purposes.
|
||||
|
||||
.. note::
|
||||
|
||||
An array graph can differ from the corresponding object
|
||||
graph in certain scenarios due to the difference of the identity
|
||||
semantics between arrays and objects.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$users = $query->getArrayResult();
|
||||
// same as:
|
||||
$users = $query->getResult(AbstractQuery::HYDRATE_ARRAY);
|
||||
|
||||
``getScalarResult()``
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Retrieves a flat/rectangular result set of scalar values that can contain duplicate data. The
|
||||
pure/mixed distinction does not apply.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$users = $query->getScalarResult();
|
||||
// same as:
|
||||
$users = $query->getResult(AbstractQuery::HYDRATE_SCALAR);
|
||||
|
||||
Fields from classes are prefixed by the DQL alias in the result.
|
||||
A query of the kind `SELECT u.name ...` returns a key `u_name` in the result rows.
|
||||
|
||||
``getSingleScalarResult()``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Retrieves a single scalar value from the result returned by the database. If the result contains
|
||||
more than a single scalar value, a ``NonUniqueResultException`` is thrown. The pure/mixed distinction does not apply.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT COUNT(u.id) FROM User u');
|
||||
$numUsers = $query->getSingleScalarResult();
|
||||
// same as:
|
||||
$numUsers = $query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR);
|
||||
|
||||
``getSingleColumnResult()``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Retrieves an array from a one-dimensional array of scalar values:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT a.id FROM User u');
|
||||
$ids = $query->getSingleColumnResult();
|
||||
// same as:
|
||||
$ids = $query->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN);
|
||||
|
||||
``getSingleResult()``
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Retrieves a single object. If the result contains more than one object, a ``NonUniqueResultException``
|
||||
is thrown. If the result contains no objects, a ``NoResultException`` is thrown. The pure/mixed distinction does not apply.
|
||||
|
||||
``getOneOrNullResult()``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Retrieves a single object. If the result contains more than one object, a ``NonUniqueResultException``
|
||||
is thrown. If no object is found, ``null`` will be returned.
|
||||
|
||||
Custom Hydration Modes
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
You can easily add your own custom hydration modes by first
|
||||
creating a class which extends ``AbstractHydrator``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
namespace MyProject\Hydrators;
|
||||
|
||||
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
|
||||
|
||||
class CustomHydrator extends AbstractHydrator
|
||||
{
|
||||
protected function _hydrateAll()
|
||||
{
|
||||
return $this->_stmt->fetchAllAssociative();
|
||||
}
|
||||
}
|
||||
|
||||
Next you just need to add the class to the ORM configuration:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$em->getConfiguration()->addCustomHydrationMode('CustomHydrator', 'MyProject\Hydrators\CustomHydrator');
|
||||
|
||||
Now the hydrator is ready to be used in your queries:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT u FROM CmsUser u');
|
||||
$results = $query->getResult('CustomHydrator');
|
||||
|
||||
Pure and Mixed Results
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -1148,165 +1348,6 @@ will return the rows iterating the different top-level entities.
|
||||
[2] => Object (User)
|
||||
[3] => Object (Group)
|
||||
|
||||
|
||||
Hydration Modes
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Each of the Hydration Modes makes assumptions about how the result
|
||||
is returned to user land. You should know about all the details to
|
||||
make best use of the different result formats:
|
||||
|
||||
The constants for the different hydration modes are:
|
||||
|
||||
|
||||
- ``Query::HYDRATE_OBJECT``
|
||||
- ``Query::HYDRATE_ARRAY``
|
||||
- ``Query::HYDRATE_SCALAR``
|
||||
- ``Query::HYDRATE_SINGLE_SCALAR``
|
||||
- ``Query::HYDRATE_SCALAR_COLUMN``
|
||||
|
||||
Object Hydration
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
Object hydration hydrates the result set into the object graph:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT u FROM CmsUser u');
|
||||
$users = $query->getResult(Query::HYDRATE_OBJECT);
|
||||
|
||||
Sometimes the behavior in the object hydrator can be confusing, which is
|
||||
why we are listing as many of the assumptions here for reference:
|
||||
|
||||
- Objects fetched in a FROM clause are returned as a Set, that means every
|
||||
object is only ever included in the resulting array once. This is the case
|
||||
even when using JOIN or GROUP BY in ways that return the same row for an
|
||||
object multiple times. If the hydrator sees the same object multiple times,
|
||||
then it makes sure it is only returned once.
|
||||
|
||||
- If an object is already in memory from a previous query of any kind, then
|
||||
then the previous object is used, even if the database may contain more
|
||||
recent data. Data from the database is discarded. This even happens if the
|
||||
previous object is still an unloaded proxy.
|
||||
|
||||
This list might be incomplete.
|
||||
|
||||
Array Hydration
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
You can run the same query with array hydration and the result set
|
||||
is hydrated into an array that represents the object graph:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT u FROM CmsUser u');
|
||||
$users = $query->getResult(Query::HYDRATE_ARRAY);
|
||||
|
||||
You can use the ``getArrayResult()`` shortcut as well:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$users = $query->getArrayResult();
|
||||
|
||||
Scalar Hydration
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
If you want to return a flat rectangular result set instead of an
|
||||
object graph you can use scalar hydration:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT u FROM CmsUser u');
|
||||
$users = $query->getResult(Query::HYDRATE_SCALAR);
|
||||
echo $users[0]['u_id'];
|
||||
|
||||
The following assumptions are made about selected fields using
|
||||
Scalar Hydration:
|
||||
|
||||
|
||||
1. Fields from classes are prefixed by the DQL alias in the result.
|
||||
A query of the kind 'SELECT u.name ..' returns a key 'u_name' in
|
||||
the result rows.
|
||||
|
||||
Single Scalar Hydration
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If you have a query which returns just a single scalar value you can use
|
||||
single scalar hydration:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT COUNT(a.id) FROM CmsUser u LEFT JOIN u.articles a WHERE u.username = ?1 GROUP BY u.id');
|
||||
$query->setParameter(1, 'jwage');
|
||||
$numArticles = $query->getResult(Query::HYDRATE_SINGLE_SCALAR);
|
||||
|
||||
You can use the ``getSingleScalarResult()`` shortcut as well:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$numArticles = $query->getSingleScalarResult();
|
||||
|
||||
Scalar Column Hydration
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If you have a query which returns a one-dimensional array of scalar values
|
||||
you can use scalar column hydration:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT a.id FROM CmsUser u');
|
||||
$ids = $query->getResult(Query::HYDRATE_SCALAR_COLUMN);
|
||||
|
||||
You can use the ``getSingleColumnResult()`` shortcut as well:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$ids = $query->getSingleColumnResult();
|
||||
|
||||
Custom Hydration Modes
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
You can easily add your own custom hydration modes by first
|
||||
creating a class which extends ``AbstractHydrator``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
namespace MyProject\Hydrators;
|
||||
|
||||
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
|
||||
|
||||
class CustomHydrator extends AbstractHydrator
|
||||
{
|
||||
protected function _hydrateAll()
|
||||
{
|
||||
return $this->_stmt->fetchAllAssociative();
|
||||
}
|
||||
}
|
||||
|
||||
Next you just need to add the class to the ORM configuration:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$em->getConfiguration()->addCustomHydrationMode('CustomHydrator', 'MyProject\Hydrators\CustomHydrator');
|
||||
|
||||
Now the hydrator is ready to be used in your queries:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT u FROM CmsUser u');
|
||||
$results = $query->getResult('CustomHydrator');
|
||||
|
||||
Iterating Large Result Sets
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -1395,6 +1436,15 @@ exist mostly internal query hints that are not be consumed in
|
||||
userland. However the following few hints are to be used in
|
||||
userland:
|
||||
|
||||
|
||||
- ``Query::HINT_FORCE_PARTIAL_LOAD`` - Allows to hydrate objects
|
||||
although not all their columns are fetched. This query hint can be
|
||||
used to handle memory consumption problems with large result-sets
|
||||
that contain char or binary data. Doctrine has no way of implicitly
|
||||
reloading this data. Partially loaded objects have to be passed to
|
||||
``EntityManager::refresh()`` if they are to be reloaded fully from
|
||||
the database. This query hint is deprecated and will be removed
|
||||
in the future (\ `Details <https://github.com/doctrine/orm/issues/8471>`_)
|
||||
- ``Query::HINT_REFRESH`` - This query is used internally by
|
||||
``EntityManager::refresh()`` and can be used in userland as well.
|
||||
If you specify this hint and a query returns the data for an entity
|
||||
@@ -1647,10 +1697,12 @@ Select Expressions
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
SelectExpression ::= (IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | "(" Subselect ")" | CaseExpression | NewObjectExpression) [["AS"] ["HIDDEN"] AliasResultVariable]
|
||||
SelectExpression ::= (IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression) [["AS"] ["HIDDEN"] AliasResultVariable]
|
||||
SimpleSelectExpression ::= (StateFieldPathExpression | IdentificationVariable | FunctionDeclaration | AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] AliasResultVariable]
|
||||
PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
|
||||
PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
|
||||
NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
|
||||
NewObjectArg ::= ScalarExpression | "(" Subselect ")"
|
||||
NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]
|
||||
|
||||
Conditional Expressions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -70,8 +70,8 @@ implements the ``MappingDriver`` interface:
|
||||
/**
|
||||
* Loads the metadata for the specified class into the provided container.
|
||||
*
|
||||
* @psalm-param class-string<T> $className
|
||||
* @psalm-param ClassMetadata<T> $metadata
|
||||
* @param class-string<T> $className
|
||||
* @param ClassMetadata<T> $metadata
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
@@ -82,8 +82,7 @@ implements the ``MappingDriver`` interface:
|
||||
/**
|
||||
* Gets the names of all mapped classes known to this driver.
|
||||
*
|
||||
* @return array<int, string> The names of all mapped classes known to this driver.
|
||||
* @psalm-return list<class-string>
|
||||
* @return list<class-string> The names of all mapped classes known to this driver.
|
||||
*/
|
||||
public function getAllClassNames();
|
||||
|
||||
@@ -91,7 +90,7 @@ implements the ``MappingDriver`` interface:
|
||||
* Returns whether the class with the specified name should have its metadata loaded.
|
||||
* This is only the case if it is either mapped as an Entity or a MappedSuperclass.
|
||||
*
|
||||
* @psalm-param class-string $className
|
||||
* @param class-string $className
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
15
docs/en/reference/partial-hydration.rst
Normal file
15
docs/en/reference/partial-hydration.rst
Normal file
@@ -0,0 +1,15 @@
|
||||
Partial Hydration
|
||||
=================
|
||||
|
||||
Partial hydration of entities is allowed in the array hydrator, when
|
||||
only a subset of the fields of an entity are loaded from the database
|
||||
and the nested results are still created based on the entity relationship structure.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$users = $em->createQuery("SELECT PARTIAL u.{id,name}, partial a.{id,street} FROM MyApp\Domain\User u JOIN u.addresses a")
|
||||
->getArrayResult();
|
||||
|
||||
This is a useful optimization when you are not interested in all fields of an entity
|
||||
for performance reasons, for example in use-cases for exporting or rendering lots of data.
|
||||
88
docs/en/reference/partial-objects.rst
Normal file
88
docs/en/reference/partial-objects.rst
Normal file
@@ -0,0 +1,88 @@
|
||||
Partial Objects
|
||||
===============
|
||||
|
||||
A partial object is an object whose state is not fully initialized
|
||||
after being reconstituted from the database and that is
|
||||
disconnected from the rest of its data. The following section will
|
||||
describe why partial objects are problematic and what the approach
|
||||
of Doctrine to this problem is.
|
||||
|
||||
.. note::
|
||||
|
||||
The partial object problem in general does not apply to
|
||||
methods or queries where you do not retrieve the query result as
|
||||
objects. Examples are: ``Query#getArrayResult()``,
|
||||
``Query#getScalarResult()``, ``Query#getSingleScalarResult()``,
|
||||
etc.
|
||||
|
||||
.. warning::
|
||||
|
||||
Use of partial objects is tricky. Fields that are not retrieved
|
||||
from the database will not be updated by the UnitOfWork even if they
|
||||
get changed in your objects. You can only promote a partial object
|
||||
to a fully-loaded object by calling ``EntityManager#refresh()``
|
||||
or a DQL query with the refresh flag.
|
||||
|
||||
|
||||
What is the problem?
|
||||
--------------------
|
||||
|
||||
In short, partial objects are problematic because they are usually
|
||||
objects with broken invariants. As such, code that uses these
|
||||
partial objects tends to be very fragile and either needs to "know"
|
||||
which fields or methods can be safely accessed or add checks around
|
||||
every field access or method invocation. The same holds true for
|
||||
the internals, i.e. the method implementations, of such objects.
|
||||
You usually simply assume the state you need in the method is
|
||||
available, after all you properly constructed this object before
|
||||
you pushed it into the database, right? These blind assumptions can
|
||||
quickly lead to null reference errors when working with such
|
||||
partial objects.
|
||||
|
||||
It gets worse with the scenario of an optional association (0..1 to
|
||||
1). When the associated field is NULL, you don't know whether this
|
||||
object does not have an associated object or whether it was simply
|
||||
not loaded when the owning object was loaded from the database.
|
||||
|
||||
These are reasons why many ORMs do not allow partial objects at all
|
||||
and instead you always have to load an object with all its fields
|
||||
(associations being proxied). One secure way to allow partial
|
||||
objects is if the programming language/platform allows the ORM tool
|
||||
to hook deeply into the object and instrument it in such a way that
|
||||
individual fields (not only associations) can be loaded lazily on
|
||||
first access. This is possible in Java, for example, through
|
||||
bytecode instrumentation. In PHP though this is not possible, so
|
||||
there is no way to have "secure" partial objects in an ORM with
|
||||
transparent persistence.
|
||||
|
||||
Doctrine, by default, does not allow partial objects. That means,
|
||||
any query that only selects partial object data and wants to
|
||||
retrieve the result as objects (i.e. ``Query#getResult()``) will
|
||||
raise an exception telling you that partial objects are dangerous.
|
||||
If you want to force a query to return you partial objects,
|
||||
possibly as a performance tweak, you can use the ``partial``
|
||||
keyword as follows:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$q = $em->createQuery("select partial u.{id,name} from MyApp\Domain\User u");
|
||||
|
||||
You can also get a partial reference instead of a proxy reference by
|
||||
calling:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$reference = $em->getPartialReference('MyApp\Domain\User', 1);
|
||||
|
||||
Partial references are objects with only the identifiers set as they
|
||||
are passed to the second argument of the ``getPartialReference()`` method.
|
||||
All other fields are null.
|
||||
|
||||
When should I force partial objects?
|
||||
------------------------------------
|
||||
|
||||
Mainly for optimization purposes, but be careful of premature
|
||||
optimization as partial objects lead to potentially more fragile
|
||||
code.
|
||||
@@ -611,3 +611,21 @@ same query of example 6 written using
|
||||
->add('from', new Expr\From('User', 'u'))
|
||||
->add('where', new Expr\Comparison('u.id', '=', '?1'))
|
||||
->add('orderBy', new Expr\OrderBy('u.name', 'ASC'));
|
||||
|
||||
Binding Parameters to Placeholders
|
||||
----------------------------------
|
||||
|
||||
It is often not necessary to know about the exact placeholder names when
|
||||
building a query. You can use a helper method to bind a value to a placeholder
|
||||
and directly use that placeholder in your query as a return value:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $qb instanceof QueryBuilder
|
||||
|
||||
$qb->select('u')
|
||||
->from('User', 'u')
|
||||
->where('u.email = ' . $qb->createNamedParameter($userInputEmail))
|
||||
;
|
||||
// SELECT u FROM User u WHERE email = :dcValue1
|
||||
|
||||
@@ -88,7 +88,7 @@ requirement.
|
||||
|
||||
A more convenient alternative for explicit transaction demarcation is the use
|
||||
of provided control abstractions in the form of
|
||||
``Connection#transactional($func)`` and ``EntityManager#transactional($func)``.
|
||||
``Connection#transactional($func)`` and ``EntityManager#wrapInTransaction($func)``.
|
||||
When used, these control abstractions ensure that you never forget to rollback
|
||||
the transaction, in addition to the obvious code reduction. An example that is
|
||||
functionally equivalent to the previously shown code looks as follows:
|
||||
@@ -96,21 +96,23 @@ functionally equivalent to the previously shown code looks as follows:
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// transactional with Connection instance
|
||||
// $conn instanceof Connection
|
||||
$conn->transactional(function($conn) {
|
||||
// ... do some work
|
||||
$user = new User;
|
||||
$user->setName('George');
|
||||
});
|
||||
|
||||
// transactional with EntityManager instance
|
||||
// $em instanceof EntityManager
|
||||
$em->transactional(function($em) {
|
||||
$em->wrapInTransaction(function($em) {
|
||||
// ... do some work
|
||||
$user = new User;
|
||||
$user->setName('George');
|
||||
$em->persist($user);
|
||||
});
|
||||
|
||||
.. warning::
|
||||
|
||||
For historical reasons, ``EntityManager#transactional($func)`` will return
|
||||
``true`` whenever the return value of ``$func`` is loosely false.
|
||||
Some examples of this include ``array()``, ``"0"``, ``""``, ``0``, and
|
||||
``null``.
|
||||
|
||||
The difference between ``Connection#transactional($func)`` and
|
||||
``EntityManager#transactional($func)`` is that the latter
|
||||
abstraction flushes the ``EntityManager`` prior to transaction
|
||||
|
||||
@@ -338,10 +338,11 @@ Performance of different deletion strategies
|
||||
Deleting an object with all its associated objects can be achieved
|
||||
in multiple ways with very different performance impacts.
|
||||
|
||||
1. If an association is marked as ``CASCADE=REMOVE`` Doctrine ORM
|
||||
will fetch this association. If its a Single association it will
|
||||
pass this entity to
|
||||
``EntityManager#remove()``. If the association is a collection, Doctrine will loop over all its elements and pass them to``EntityManager#remove()``.
|
||||
1. If an association is marked as ``CASCADE=REMOVE`` Doctrine ORM will
|
||||
fetch this association. If it's a Single association it will pass
|
||||
this entity to ``EntityManager#remove()``. If the association is a
|
||||
collection, Doctrine will loop over all its elements and pass them to
|
||||
``EntityManager#remove()``.
|
||||
In both cases the cascade remove semantics are applied recursively.
|
||||
For large object graphs this removal strategy can be very costly.
|
||||
2. Using a DQL ``DELETE`` statement allows you to delete multiple
|
||||
|
||||
@@ -1,82 +1,75 @@
|
||||
:orphan:
|
||||
|
||||
.. toc::
|
||||
.. toctree::
|
||||
:caption: Tutorials
|
||||
:depth: 3
|
||||
|
||||
.. tocheader:: Tutorials
|
||||
tutorials/getting-started
|
||||
tutorials/getting-started-database
|
||||
tutorials/getting-started-models
|
||||
tutorials/working-with-indexed-associations
|
||||
tutorials/extra-lazy-associations
|
||||
tutorials/composite-primary-keys
|
||||
tutorials/ordered-associations
|
||||
tutorials/override-field-association-mappings-in-subclasses
|
||||
tutorials/pagination
|
||||
tutorials/embeddables
|
||||
|
||||
.. toctree::
|
||||
:depth: 3
|
||||
.. toctree::
|
||||
:caption: Reference
|
||||
:depth: 3
|
||||
|
||||
tutorials/getting-started
|
||||
tutorials/getting-started-database
|
||||
tutorials/getting-started-models
|
||||
tutorials/working-with-indexed-associations
|
||||
tutorials/extra-lazy-associations
|
||||
tutorials/composite-primary-keys
|
||||
tutorials/ordered-associations
|
||||
tutorials/override-field-association-mappings-in-subclasses
|
||||
tutorials/pagination
|
||||
tutorials/embeddables
|
||||
reference/architecture
|
||||
reference/configuration
|
||||
reference/faq
|
||||
reference/basic-mapping
|
||||
reference/association-mapping
|
||||
reference/inheritance-mapping
|
||||
reference/working-with-objects
|
||||
reference/working-with-associations
|
||||
reference/typedfieldmapper
|
||||
reference/events
|
||||
reference/unitofwork
|
||||
reference/unitofwork-associations
|
||||
reference/transactions-and-concurrency
|
||||
reference/batch-processing
|
||||
reference/dql-doctrine-query-language
|
||||
reference/query-builder
|
||||
reference/native-sql
|
||||
reference/change-tracking-policies
|
||||
reference/partial-hydration
|
||||
reference/partial-objects
|
||||
reference/attributes-reference
|
||||
reference/xml-mapping
|
||||
reference/php-mapping
|
||||
reference/caching
|
||||
reference/improving-performance
|
||||
reference/tools
|
||||
reference/metadata-drivers
|
||||
reference/best-practices
|
||||
reference/limitations-and-known-issues
|
||||
tutorials/pagination
|
||||
reference/filters
|
||||
reference/namingstrategy
|
||||
reference/advanced-configuration
|
||||
reference/second-level-cache
|
||||
reference/security
|
||||
|
||||
.. toc::
|
||||
.. toctree::
|
||||
:caption: Cookbook
|
||||
:depth: 3
|
||||
|
||||
.. tocheader:: Reference
|
||||
|
||||
.. toctree::
|
||||
:depth: 3
|
||||
|
||||
reference/architecture
|
||||
reference/configuration
|
||||
reference/faq
|
||||
reference/basic-mapping
|
||||
reference/association-mapping
|
||||
reference/inheritance-mapping
|
||||
reference/working-with-objects
|
||||
reference/working-with-associations
|
||||
reference/typedfieldmapper
|
||||
reference/events
|
||||
reference/unitofwork
|
||||
reference/unitofwork-associations
|
||||
reference/transactions-and-concurrency
|
||||
reference/batch-processing
|
||||
reference/dql-doctrine-query-language
|
||||
reference/query-builder
|
||||
reference/native-sql
|
||||
reference/change-tracking-policies
|
||||
reference/attributes-reference
|
||||
reference/xml-mapping
|
||||
reference/php-mapping
|
||||
reference/caching
|
||||
reference/improving-performance
|
||||
reference/tools
|
||||
reference/metadata-drivers
|
||||
reference/best-practices
|
||||
reference/limitations-and-known-issues
|
||||
tutorials/pagination
|
||||
reference/filters
|
||||
reference/namingstrategy
|
||||
reference/advanced-configuration
|
||||
reference/second-level-cache
|
||||
reference/security
|
||||
|
||||
.. toc::
|
||||
|
||||
.. tocheader:: Cookbook
|
||||
|
||||
.. toctree::
|
||||
:depth: 3
|
||||
|
||||
cookbook/aggregate-fields
|
||||
cookbook/custom-mapping-types
|
||||
cookbook/decorator-pattern
|
||||
cookbook/dql-custom-walkers
|
||||
cookbook/dql-user-defined-functions
|
||||
cookbook/implementing-arrayaccess-for-domain-objects
|
||||
cookbook/resolve-target-entity-listener
|
||||
cookbook/sql-table-prefixes
|
||||
cookbook/strategy-cookbook-introduction
|
||||
cookbook/validation-of-entities
|
||||
cookbook/working-with-datetime
|
||||
cookbook/mysql-enums
|
||||
cookbook/advanced-field-value-conversion-using-custom-mapping-types
|
||||
cookbook/entities-in-session
|
||||
cookbook/aggregate-fields
|
||||
cookbook/custom-mapping-types
|
||||
cookbook/decorator-pattern
|
||||
cookbook/dql-custom-walkers
|
||||
cookbook/dql-user-defined-functions
|
||||
cookbook/implementing-arrayaccess-for-domain-objects
|
||||
cookbook/resolve-target-entity-listener
|
||||
cookbook/sql-table-prefixes
|
||||
cookbook/strategy-cookbook-introduction
|
||||
cookbook/validation-of-entities
|
||||
cookbook/working-with-datetime
|
||||
cookbook/mysql-enums
|
||||
cookbook/advanced-field-value-conversion-using-custom-mapping-types
|
||||
cookbook/entities-in-session
|
||||
|
||||
@@ -145,7 +145,7 @@ We keep up the example of an Article with arbitrary attributes, the mapping look
|
||||
#[OneToMany(targetEntity: ArticleAttribute::class, mappedBy: 'article', cascade: ['ALL'], indexBy: 'attribute')]
|
||||
private Collection $attributes;
|
||||
|
||||
public function addAttribute(string $name, ArticleAttribute $value): void
|
||||
public function addAttribute(string $name, string $value): void
|
||||
{
|
||||
$this->attributes[$name] = new ArticleAttribute($name, $value, $this);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ What is Doctrine?
|
||||
-----------------
|
||||
|
||||
Doctrine ORM is an `object-relational mapper (ORM) <https://en.wikipedia.org/wiki/Object-relational_mapping>`_
|
||||
for PHP 7.1+ that provides transparent persistence for PHP objects. It uses the Data Mapper
|
||||
for PHP that provides transparent persistence for PHP objects. It uses the Data Mapper
|
||||
pattern at the heart, aiming for a complete separation of your domain/business
|
||||
logic from the persistence in a relational database management system.
|
||||
|
||||
@@ -139,12 +139,12 @@ step:
|
||||
|
||||
// Create a simple "default" Doctrine ORM configuration for Attributes
|
||||
$config = ORMSetup::createAttributeMetadataConfiguration(
|
||||
paths: array(__DIR__."/src"),
|
||||
paths: [__DIR__ . '/src'],
|
||||
isDevMode: true,
|
||||
);
|
||||
// or if you prefer XML
|
||||
// $config = ORMSetup::createXMLMetadataConfiguration(
|
||||
// paths: array(__DIR__."/config/xml"),
|
||||
// paths: [__DIR__ . '/config/xml'],
|
||||
// isDevMode: true,
|
||||
//);
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
<file>src</file>
|
||||
<file>tests</file>
|
||||
|
||||
<exclude-pattern>*/src/Mapping/InverseJoinColumn.php</exclude-pattern>
|
||||
<exclude-pattern>*/tests/Tests/Proxies/__CG__*</exclude-pattern>
|
||||
<exclude-pattern>*/tests/Tests/ORM/Tools/Export/export/*</exclude-pattern>
|
||||
|
||||
|
||||
@@ -115,11 +115,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/EntityManagerInterface.php
|
||||
|
||||
-
|
||||
message: "#^Template type T of method Doctrine\\\\ORM\\\\EntityManagerInterface\\:\\:getClassMetadata\\(\\) is not referenced in a parameter\\.$#"
|
||||
count: 1
|
||||
path: src/EntityManagerInterface.php
|
||||
|
||||
-
|
||||
message: "#^Method Doctrine\\\\ORM\\\\EntityRepository\\:\\:matching\\(\\) should return Doctrine\\\\Common\\\\Collections\\\\AbstractLazyCollection\\<int, T of object\\>&Doctrine\\\\Common\\\\Collections\\\\Selectable\\<int, T of object\\> but returns Doctrine\\\\ORM\\\\LazyCriteriaCollection\\<\\(int\\|string\\), object\\>\\.$#"
|
||||
count: 1
|
||||
|
||||
@@ -20,10 +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: '~^Unreachable statement \- code above always terminates\.$~'
|
||||
path: src/Mapping/AssociationMapping.php
|
||||
|
||||
- '~^Class Doctrine\\DBAL\\Platforms\\SQLitePlatform not found\.$~'
|
||||
|
||||
# To be removed in 4.0
|
||||
@@ -31,3 +27,10 @@ parameters:
|
||||
message: '#Negated boolean expression is always false\.#'
|
||||
paths:
|
||||
- src/Mapping/Driver/AttributeDriver.php
|
||||
|
||||
-
|
||||
message: '~^Call to deprecated method getEventManager\(\) of class Doctrine\\DBAL\\Connection\.$~'
|
||||
path: src/EntityManager.php
|
||||
-
|
||||
message: '~deprecated class Doctrine\\DBAL\\Tools\\Console\\Command\\ReservedWordsCommand\:~'
|
||||
path: src/Tools/Console/ConsoleRunner.php
|
||||
|
||||
@@ -20,10 +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: '~^Unreachable statement \- code above always terminates\.$~'
|
||||
path: src/Mapping/AssociationMapping.php
|
||||
|
||||
# Compatibility with DBAL 3
|
||||
# See https://github.com/doctrine/dbal/pull/3480
|
||||
-
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<files psalm-version="5.22.2@d768d914152dbbf3486c36398802f74e80cfde48">
|
||||
<files psalm-version="5.24.0@462c80e31c34e58cc4f750c656be3927e80e550e">
|
||||
<file src="src/AbstractQuery.php">
|
||||
<FalsableReturnStatement>
|
||||
<code><![CDATA[! $filteredParameters->isEmpty() ? $filteredParameters->first() : null]]></code>
|
||||
@@ -154,14 +154,6 @@
|
||||
<code><![CDATA[$className]]></code>
|
||||
</ArgumentTypeCoercion>
|
||||
</file>
|
||||
<file src="src/Decorator/EntityManagerDecorator.php">
|
||||
<InvalidReturnStatement>
|
||||
<code><![CDATA[$this->wrapped->getClassMetadata($className)]]></code>
|
||||
</InvalidReturnStatement>
|
||||
<InvalidReturnType>
|
||||
<code><![CDATA[ClassMetadata]]></code>
|
||||
</InvalidReturnType>
|
||||
</file>
|
||||
<file src="src/EntityManager.php">
|
||||
<ArgumentTypeCoercion>
|
||||
<code><![CDATA[$className]]></code>
|
||||
@@ -174,11 +166,9 @@
|
||||
<code><![CDATA[$persister->load($sortedId, null, null, [], $lockMode)]]></code>
|
||||
<code><![CDATA[$persister->loadById($sortedId)]]></code>
|
||||
<code><![CDATA[$this->metadataFactory]]></code>
|
||||
<code><![CDATA[$this->metadataFactory->getMetadataFor($className)]]></code>
|
||||
</InvalidReturnStatement>
|
||||
<InvalidReturnType>
|
||||
<code><![CDATA[ClassMetadataFactory]]></code>
|
||||
<code><![CDATA[Mapping\ClassMetadata]]></code>
|
||||
</InvalidReturnType>
|
||||
<PossiblyNullArgument>
|
||||
<code><![CDATA[$config->getProxyDir()]]></code>
|
||||
@@ -216,7 +206,16 @@
|
||||
<code><![CDATA[$entity]]></code>
|
||||
</PossiblyNullArgument>
|
||||
</file>
|
||||
<file src="src/Id/SequenceGenerator.php">
|
||||
<ParamNameMismatch>
|
||||
<code><![CDATA[$serialized]]></code>
|
||||
</ParamNameMismatch>
|
||||
</file>
|
||||
<file src="src/Internal/Hydration/AbstractHydrator.php">
|
||||
<PossiblyUndefinedArrayOffset>
|
||||
<code><![CDATA[$newObject['args']]]></code>
|
||||
<code><![CDATA[$newObject['args']]]></code>
|
||||
</PossiblyUndefinedArrayOffset>
|
||||
<ReferenceConstraintViolation>
|
||||
<code><![CDATA[return $rowData;]]></code>
|
||||
<code><![CDATA[return $rowData;]]></code>
|
||||
@@ -233,9 +232,6 @@
|
||||
<code><![CDATA[$result[$resultKey]]]></code>
|
||||
<code><![CDATA[$result[$resultKey]]]></code>
|
||||
</PossiblyNullArrayAssignment>
|
||||
<PossiblyUndefinedArrayOffset>
|
||||
<code><![CDATA[$newObject['args']]]></code>
|
||||
</PossiblyUndefinedArrayOffset>
|
||||
<ReferenceConstraintViolation>
|
||||
<code><![CDATA[$result]]></code>
|
||||
</ReferenceConstraintViolation>
|
||||
@@ -251,9 +247,6 @@
|
||||
</UnsupportedReferenceUsage>
|
||||
</file>
|
||||
<file src="src/Internal/Hydration/ObjectHydrator.php">
|
||||
<InvalidArgument>
|
||||
<code><![CDATA[$element]]></code>
|
||||
</InvalidArgument>
|
||||
<PossiblyFalseArgument>
|
||||
<code><![CDATA[$index]]></code>
|
||||
</PossiblyFalseArgument>
|
||||
@@ -273,9 +266,6 @@
|
||||
<code><![CDATA[setValue]]></code>
|
||||
<code><![CDATA[setValue]]></code>
|
||||
</PossiblyNullReference>
|
||||
<PossiblyUndefinedArrayOffset>
|
||||
<code><![CDATA[$newObject['args']]]></code>
|
||||
</PossiblyUndefinedArrayOffset>
|
||||
</file>
|
||||
<file src="src/Mapping/AssociationMapping.php">
|
||||
<LessSpecificReturnStatement>
|
||||
@@ -290,11 +280,6 @@
|
||||
<code><![CDATA[$repositoryClassName]]></code>
|
||||
</ArgumentTypeCoercion>
|
||||
</file>
|
||||
<file src="src/Mapping/Builder/EntityListenerBuilder.php">
|
||||
<PossiblyNullArgument>
|
||||
<code><![CDATA[$class]]></code>
|
||||
</PossiblyNullArgument>
|
||||
</file>
|
||||
<file src="src/Mapping/ClassMetadata.php">
|
||||
<DeprecatedProperty>
|
||||
<code><![CDATA[$this->columnNames]]></code>
|
||||
@@ -303,6 +288,10 @@
|
||||
<code><![CDATA[$this->columnNames]]></code>
|
||||
</DeprecatedProperty>
|
||||
<InvalidArgument>
|
||||
<code><![CDATA[$mapping]]></code>
|
||||
<code><![CDATA[$mapping]]></code>
|
||||
<code><![CDATA[$mapping]]></code>
|
||||
<code><![CDATA[$mapping]]></code>
|
||||
<code><![CDATA[$mapping]]></code>
|
||||
<code><![CDATA[$mapping]]></code>
|
||||
<code><![CDATA[$mapping]]></code>
|
||||
@@ -322,8 +311,6 @@
|
||||
<code><![CDATA[$entity]]></code>
|
||||
</ParamNameMismatch>
|
||||
<PossiblyNullArgument>
|
||||
<code><![CDATA[$class]]></code>
|
||||
<code><![CDATA[$className]]></code>
|
||||
<code><![CDATA[$mapping['targetEntity']]]></code>
|
||||
<code><![CDATA[$mapping['targetEntity']]]></code>
|
||||
<code><![CDATA[$parentReflFields[$embeddedClass->declaredField]]]></code>
|
||||
@@ -415,6 +402,7 @@
|
||||
<file src="src/Mapping/DefaultTypedFieldMapper.php">
|
||||
<LessSpecificReturnStatement>
|
||||
<code><![CDATA[$mapping]]></code>
|
||||
<code><![CDATA[$mapping]]></code>
|
||||
</LessSpecificReturnStatement>
|
||||
<MoreSpecificReturnType>
|
||||
<code><![CDATA[array]]></code>
|
||||
@@ -508,13 +496,8 @@
|
||||
<InvalidPropertyAssignmentValue>
|
||||
<code><![CDATA[$metadata->table]]></code>
|
||||
</InvalidPropertyAssignmentValue>
|
||||
<InvalidPropertyFetch>
|
||||
<code><![CDATA[$xmlRoot->{'discriminator-column'}]]></code>
|
||||
<code><![CDATA[$xmlRoot->{'discriminator-map'}]]></code>
|
||||
</InvalidPropertyFetch>
|
||||
<InvalidReturnStatement>
|
||||
<code><![CDATA[$mapping]]></code>
|
||||
<code><![CDATA[$result]]></code>
|
||||
<code><![CDATA[[
|
||||
'usage' => $usage,
|
||||
'region' => $region,
|
||||
@@ -538,20 +521,10 @@
|
||||
* options?: array
|
||||
* }]]></code>
|
||||
<code><![CDATA[array{usage: int|null, region?: string}]]></code>
|
||||
<code><![CDATA[loadMappingFile]]></code>
|
||||
</InvalidReturnType>
|
||||
<MoreSpecificImplementedParamType>
|
||||
<code><![CDATA[$metadata]]></code>
|
||||
</MoreSpecificImplementedParamType>
|
||||
<NoInterfaceProperties>
|
||||
<code><![CDATA[$xmlRoot->{'discriminator-column'}]]></code>
|
||||
<code><![CDATA[$xmlRoot->{'discriminator-map'}]]></code>
|
||||
</NoInterfaceProperties>
|
||||
<TypeDoesNotContainType>
|
||||
<code><![CDATA[$xmlRoot->getName() === 'embeddable']]></code>
|
||||
<code><![CDATA[$xmlRoot->getName() === 'entity']]></code>
|
||||
<code><![CDATA[$xmlRoot->getName() === 'mapped-superclass']]></code>
|
||||
</TypeDoesNotContainType>
|
||||
</file>
|
||||
<file src="src/Mapping/ManyToManyInverseSideMapping.php">
|
||||
<PropertyNotSetInConstructor>
|
||||
@@ -767,13 +740,10 @@
|
||||
<code><![CDATA[$autoGenerate > 4]]></code>
|
||||
</TypeDoesNotContainType>
|
||||
<UndefinedMethod>
|
||||
<code><![CDATA[self::createLazyGhost(static function (InternalProxy $object) use ($initializer, &$proxy): void {
|
||||
$initializer($object, $proxy);
|
||||
<code><![CDATA[self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void {
|
||||
$initializer($object, $identifier);
|
||||
}, $skippedProperties)]]></code>
|
||||
</UndefinedMethod>
|
||||
<UndefinedVariable>
|
||||
<code><![CDATA[$proxy]]></code>
|
||||
</UndefinedVariable>
|
||||
<UnresolvableInclude>
|
||||
<code><![CDATA[require $fileName]]></code>
|
||||
</UnresolvableInclude>
|
||||
@@ -954,6 +924,9 @@
|
||||
<ArgumentTypeCoercion>
|
||||
<code><![CDATA[$stringPattern]]></code>
|
||||
</ArgumentTypeCoercion>
|
||||
<DeprecatedMethod>
|
||||
<code><![CDATA[setSqlExecutor]]></code>
|
||||
</DeprecatedMethod>
|
||||
<InvalidNullableReturnType>
|
||||
<code><![CDATA[AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement]]></code>
|
||||
</InvalidNullableReturnType>
|
||||
@@ -1034,10 +1007,6 @@
|
||||
</file>
|
||||
<file src="src/QueryBuilder.php">
|
||||
<ArgumentTypeCoercion>
|
||||
<code><![CDATA[$having]]></code>
|
||||
<code><![CDATA[$having]]></code>
|
||||
<code><![CDATA[$where]]></code>
|
||||
<code><![CDATA[$where]]></code>
|
||||
<code><![CDATA[[$rootAlias => $join]]]></code>
|
||||
<code><![CDATA[[$rootAlias => $join]]]></code>
|
||||
</ArgumentTypeCoercion>
|
||||
@@ -1148,6 +1117,12 @@
|
||||
</RedundantConditionGivenDocblockType>
|
||||
</file>
|
||||
<file src="src/Tools/Pagination/LimitSubqueryOutputWalker.php">
|
||||
<InvalidReturnStatement>
|
||||
<code><![CDATA[$abstractSqlExecutor->getSqlStatements()]]></code>
|
||||
</InvalidReturnStatement>
|
||||
<InvalidReturnType>
|
||||
<code><![CDATA[string]]></code>
|
||||
</InvalidReturnType>
|
||||
<PossiblyFalseArgument>
|
||||
<code><![CDATA[strrpos($orderByItemString, ' ')]]></code>
|
||||
</PossiblyFalseArgument>
|
||||
|
||||
@@ -22,8 +22,8 @@ class CollectionCacheKey extends CacheKey
|
||||
public readonly array $ownerIdentifier;
|
||||
|
||||
/**
|
||||
* @param class-string $entityClass The owner entity class.
|
||||
* @param array<string, mixed> $ownerIdentifier The identifier of the owning entity.
|
||||
* @param class-string $entityClass The owner entity class
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $entityClass,
|
||||
|
||||
@@ -16,6 +16,7 @@ use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\ORM\Query\SqlWalker;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
|
||||
use function array_map;
|
||||
@@ -210,6 +211,10 @@ class DefaultQueryCache implements QueryCache
|
||||
throw FeatureNotImplemented::nonSelectStatements();
|
||||
}
|
||||
|
||||
if (($hints[SqlWalker::HINT_PARTIAL] ?? false) === true || ($hints[Query::HINT_FORCE_PARTIAL_LOAD] ?? false) === true) {
|
||||
throw FeatureNotImplemented::partialEntities();
|
||||
}
|
||||
|
||||
if (! ($key->cacheMode & Cache::MODE_PUT)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ use function array_map;
|
||||
class EntityCacheEntry implements CacheEntry
|
||||
{
|
||||
/**
|
||||
* @param array<string,mixed> $data The entity map data
|
||||
* @psalm-param class-string $class The entity class name
|
||||
* @param class-string $class The entity class name
|
||||
* @param array<string,mixed> $data The entity map data
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $class,
|
||||
|
||||
@@ -20,4 +20,9 @@ class FeatureNotImplemented extends CacheException
|
||||
{
|
||||
return new self('Second-level cache query supports only select statements.');
|
||||
}
|
||||
|
||||
public static function partialEntities(): self
|
||||
{
|
||||
return new self('Second level cache does not support partial entities.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Doctrine\ORM\Cache\Persister\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\DBAL\LockMode;
|
||||
use Doctrine\ORM\Cache;
|
||||
use Doctrine\ORM\Cache\CollectionCacheKey;
|
||||
@@ -17,7 +18,6 @@ use Doctrine\ORM\Cache\Region;
|
||||
use Doctrine\ORM\Cache\TimestampCacheKey;
|
||||
use Doctrine\ORM\Cache\TimestampRegion;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Internal\CriteriaOrderings;
|
||||
use Doctrine\ORM\Mapping\AssociationMapping;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\ClassMetadataFactory;
|
||||
@@ -34,8 +34,6 @@ use function sha1;
|
||||
|
||||
abstract class AbstractEntityPersister implements CachedEntityPersister
|
||||
{
|
||||
use CriteriaOrderings;
|
||||
|
||||
protected UnitOfWork $uow;
|
||||
protected ClassMetadataFactory $metadataFactory;
|
||||
|
||||
@@ -204,8 +202,8 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
|
||||
/**
|
||||
* Generates a string of currently query
|
||||
*
|
||||
* @param string[]|Criteria $criteria
|
||||
* @param string[]|null $orderBy
|
||||
* @param string[]|Criteria $criteria
|
||||
* @param array<string, Order>|null $orderBy
|
||||
*/
|
||||
protected function getHash(
|
||||
string $query,
|
||||
@@ -429,7 +427,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
|
||||
*/
|
||||
public function loadCriteria(Criteria $criteria): array
|
||||
{
|
||||
$orderBy = self::getCriteriaOrderings($criteria);
|
||||
$orderBy = $criteria->orderings();
|
||||
$limit = $criteria->getMaxResults();
|
||||
$offset = $criteria->getFirstResult();
|
||||
$query = $this->persister->getSelectSQL($criteria);
|
||||
|
||||
@@ -67,7 +67,7 @@ class FileLockRegion implements ConcurrentRegion
|
||||
$time = $this->getLockTime($filename);
|
||||
$content = $this->getLockContent($filename);
|
||||
|
||||
if (! $content || ! $time) {
|
||||
if ($content === false || $time === false) {
|
||||
@unlink($filename);
|
||||
|
||||
return false;
|
||||
@@ -156,12 +156,10 @@ class FileLockRegion implements ConcurrentRegion
|
||||
{
|
||||
// The check below is necessary because on some platforms glob returns false
|
||||
// when nothing matched (even though no errors occurred)
|
||||
$filenames = glob(sprintf('%s/*.%s', $this->directory, self::LOCK_EXTENSION));
|
||||
$filenames = glob(sprintf('%s/*.%s', $this->directory, self::LOCK_EXTENSION)) ?: [];
|
||||
|
||||
if ($filenames) {
|
||||
foreach ($filenames as $filename) {
|
||||
@unlink($filename);
|
||||
}
|
||||
foreach ($filenames as $filename) {
|
||||
@unlink($filename);
|
||||
}
|
||||
|
||||
return $this->region->evictAll();
|
||||
@@ -176,7 +174,7 @@ class FileLockRegion implements ConcurrentRegion
|
||||
$lock = Lock::createLockRead();
|
||||
$filename = $this->getLockFileName($key);
|
||||
|
||||
if (! @file_put_contents($filename, $lock->value, LOCK_EX)) {
|
||||
if (@file_put_contents($filename, $lock->value, LOCK_EX) === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -263,8 +263,8 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
*
|
||||
* Any previously added numeric functions are discarded.
|
||||
*
|
||||
* @psalm-param array<string, class-string> $functions The map of custom
|
||||
* DQL numeric functions.
|
||||
* @param array<string, class-string> $functions The map of custom
|
||||
* DQL numeric functions.
|
||||
*/
|
||||
public function setCustomNumericFunctions(array $functions): void
|
||||
{
|
||||
@@ -291,7 +291,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
/**
|
||||
* Gets the implementation class name of a registered custom date/time DQL function.
|
||||
*
|
||||
* @psalm-return class-string|callable|null
|
||||
* @return class-string|callable|null
|
||||
*/
|
||||
public function getCustomDatetimeFunction(string $name): string|callable|null
|
||||
{
|
||||
@@ -351,7 +351,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
/**
|
||||
* Gets the hydrator class for the given hydration mode name.
|
||||
*
|
||||
* @psalm-return class-string<AbstractHydrator>|null
|
||||
* @return class-string<AbstractHydrator>|null
|
||||
*/
|
||||
public function getCustomHydrationMode(string $modeName): string|null
|
||||
{
|
||||
@@ -361,7 +361,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
/**
|
||||
* Adds a custom hydration mode.
|
||||
*
|
||||
* @psalm-param class-string<AbstractHydrator> $hydrator
|
||||
* @param class-string<AbstractHydrator> $hydrator
|
||||
*/
|
||||
public function addCustomHydrationMode(string $modeName, string $hydrator): void
|
||||
{
|
||||
@@ -371,14 +371,14 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
/**
|
||||
* Sets a class metadata factory.
|
||||
*
|
||||
* @psalm-param class-string $cmfName
|
||||
* @param class-string $cmfName
|
||||
*/
|
||||
public function setClassMetadataFactoryName(string $cmfName): void
|
||||
{
|
||||
$this->attributes['classMetadataFactoryName'] = $cmfName;
|
||||
}
|
||||
|
||||
/** @psalm-return class-string */
|
||||
/** @return class-string */
|
||||
public function getClassMetadataFactoryName(): string
|
||||
{
|
||||
if (! isset($this->attributes['classMetadataFactoryName'])) {
|
||||
@@ -391,8 +391,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
/**
|
||||
* Adds a filter to the list of possible filters.
|
||||
*
|
||||
* @param string $className The class name of the filter.
|
||||
* @psalm-param class-string<SQLFilter> $className
|
||||
* @param class-string<SQLFilter> $className The class name of the filter.
|
||||
*/
|
||||
public function addFilter(string $name, string $className): void
|
||||
{
|
||||
@@ -402,9 +401,8 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
/**
|
||||
* Gets the class name for a given filter name.
|
||||
*
|
||||
* @return string|null The class name of the filter, or null if it is not
|
||||
* defined.
|
||||
* @psalm-return class-string<SQLFilter>|null
|
||||
* @return class-string<SQLFilter>|null The class name of the filter,
|
||||
* or null if it is not defined.
|
||||
*/
|
||||
public function getFilterClassName(string $name): string|null
|
||||
{
|
||||
@@ -414,7 +412,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
/**
|
||||
* Sets default repository class.
|
||||
*
|
||||
* @psalm-param class-string<EntityRepository> $className
|
||||
* @param class-string<EntityRepository> $className
|
||||
*
|
||||
* @throws InvalidEntityRepository If $classname is not an ObjectRepository.
|
||||
*/
|
||||
@@ -430,7 +428,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
|
||||
/**
|
||||
* Get default repository class.
|
||||
*
|
||||
* @psalm-return class-string<EntityRepository>
|
||||
* @return class-string<EntityRepository>
|
||||
*/
|
||||
public function getDefaultRepositoryClassName(): string
|
||||
{
|
||||
|
||||
@@ -24,7 +24,6 @@ use Doctrine\ORM\Query\Expr;
|
||||
use Doctrine\ORM\Query\FilterCollection;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\ORM\Repository\RepositoryFactory;
|
||||
use Throwable;
|
||||
|
||||
use function array_keys;
|
||||
use function is_array;
|
||||
@@ -63,27 +62,27 @@ class EntityManager implements EntityManagerInterface
|
||||
/**
|
||||
* The metadata factory, used to retrieve the ORM metadata of entity classes.
|
||||
*/
|
||||
private readonly ClassMetadataFactory $metadataFactory;
|
||||
private ClassMetadataFactory $metadataFactory;
|
||||
|
||||
/**
|
||||
* The UnitOfWork used to coordinate object-level transactions.
|
||||
*/
|
||||
private readonly UnitOfWork $unitOfWork;
|
||||
private UnitOfWork $unitOfWork;
|
||||
|
||||
/**
|
||||
* The event manager that is the central point of the event system.
|
||||
*/
|
||||
private readonly EventManager $eventManager;
|
||||
private EventManager $eventManager;
|
||||
|
||||
/**
|
||||
* The proxy factory used to create dynamic proxies.
|
||||
*/
|
||||
private readonly ProxyFactory $proxyFactory;
|
||||
private ProxyFactory $proxyFactory;
|
||||
|
||||
/**
|
||||
* The repository factory used to create dynamic repositories.
|
||||
*/
|
||||
private readonly RepositoryFactory $repositoryFactory;
|
||||
private RepositoryFactory $repositoryFactory;
|
||||
|
||||
/**
|
||||
* The expression builder instance used to generate query expressions.
|
||||
@@ -112,8 +111,8 @@ class EntityManager implements EntityManagerInterface
|
||||
* @param Connection $conn The database connection used by the EntityManager.
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly Connection $conn,
|
||||
private readonly Configuration $config,
|
||||
private Connection $conn,
|
||||
private Configuration $config,
|
||||
EventManager|null $eventManager = null,
|
||||
) {
|
||||
if (! $config->getMetadataDriverImpl()) {
|
||||
@@ -178,18 +177,24 @@ class EntityManager implements EntityManagerInterface
|
||||
{
|
||||
$this->conn->beginTransaction();
|
||||
|
||||
$successful = false;
|
||||
|
||||
try {
|
||||
$return = $func($this);
|
||||
|
||||
$this->flush();
|
||||
$this->conn->commit();
|
||||
|
||||
return $return;
|
||||
} catch (Throwable $e) {
|
||||
$this->close();
|
||||
$this->conn->rollBack();
|
||||
$successful = true;
|
||||
|
||||
throw $e;
|
||||
return $return;
|
||||
} finally {
|
||||
if (! $successful) {
|
||||
$this->close();
|
||||
if ($this->conn->isTransactionActive()) {
|
||||
$this->conn->rollBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,9 +484,9 @@ class EntityManager implements EntityManagerInterface
|
||||
/**
|
||||
* Gets the repository for an entity class.
|
||||
*
|
||||
* @psalm-param class-string<T> $className
|
||||
* @param class-string<T> $className The name of the entity.
|
||||
*
|
||||
* @psalm-return EntityRepository<T>
|
||||
* @return EntityRepository<T> The repository class.
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
|
||||
@@ -22,9 +22,9 @@ interface EntityManagerInterface extends ObjectManager
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-param class-string<T> $className
|
||||
* @param class-string<T> $className
|
||||
*
|
||||
* @psalm-return EntityRepository<T>
|
||||
* @return EntityRepository<T>
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
@@ -151,11 +151,10 @@ interface EntityManagerInterface extends ObjectManager
|
||||
* Gets a reference to the entity identified by the given type and identifier
|
||||
* without actually loading it, if the entity is not yet loaded.
|
||||
*
|
||||
* @param string $entityName The name of the entity type.
|
||||
* @param mixed $id The entity identifier.
|
||||
* @psalm-param class-string<T> $entityName
|
||||
* @param class-string<T> $entityName The name of the entity type.
|
||||
* @param mixed $id The entity identifier.
|
||||
*
|
||||
* @psalm-return T|null
|
||||
* @return T|null The entity reference.
|
||||
*
|
||||
* @throws ORMException
|
||||
*
|
||||
@@ -232,9 +231,9 @@ interface EntityManagerInterface extends ObjectManager
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-param string|class-string<T> $className
|
||||
* @param string|class-string<T> $className
|
||||
*
|
||||
* @psalm-return Mapping\ClassMetadata<T>
|
||||
* @psalm-return ($className is class-string<T> ? Mapping\ClassMetadata<T> : Mapping\ClassMetadata<object>)
|
||||
*
|
||||
* @psalm-template T of object
|
||||
*/
|
||||
|
||||
@@ -35,11 +35,11 @@ use function substr;
|
||||
*/
|
||||
class EntityRepository implements ObjectRepository, Selectable
|
||||
{
|
||||
/** @psalm-var class-string<T> */
|
||||
/** @var class-string<T> */
|
||||
private readonly string $entityName;
|
||||
private static Inflector|null $inflector = null;
|
||||
|
||||
/** @psalm-param ClassMetadata<T> $class */
|
||||
/** @param ClassMetadata<T> $class */
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly ClassMetadata $class,
|
||||
@@ -131,6 +131,7 @@ class EntityRepository implements ObjectRepository, Selectable
|
||||
* @psalm-param array<string, mixed> $criteria
|
||||
*
|
||||
* @return int The cardinality of the objects that match the given criteria.
|
||||
* @psalm-return 0|positive-int
|
||||
*
|
||||
* @todo Add this method to `ObjectRepository` interface in the next major release
|
||||
*/
|
||||
@@ -168,7 +169,7 @@ class EntityRepository implements ObjectRepository, Selectable
|
||||
));
|
||||
}
|
||||
|
||||
/** @psalm-return class-string<T> */
|
||||
/** @return class-string<T> */
|
||||
protected function getEntityName(): string
|
||||
{
|
||||
return $this->entityName;
|
||||
|
||||
17
src/Exception/DuplicateFieldException.php
Normal file
17
src/Exception/DuplicateFieldException.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Exception;
|
||||
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class DuplicateFieldException extends LogicException implements ORMException
|
||||
{
|
||||
public static function create(string $argName, string $columnName): self
|
||||
{
|
||||
return new self(sprintf('Name "%s" for "%s" already in use.', $argName, $columnName));
|
||||
}
|
||||
}
|
||||
17
src/Exception/NoMatchingPropertyException.php
Normal file
17
src/Exception/NoMatchingPropertyException.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Exception;
|
||||
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class NoMatchingPropertyException extends LogicException implements ORMException
|
||||
{
|
||||
public static function create(string $property): self
|
||||
{
|
||||
return new self(sprintf('Column name "%s" does not match any property name. Consider aliasing it to the name of an existing property.', $property));
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/** @deprecated */
|
||||
final class NotSupported extends LogicException implements ORMException
|
||||
{
|
||||
public static function create(): self
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Doctrine\ORM\Id;
|
||||
|
||||
use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Serializable;
|
||||
|
||||
@@ -65,8 +66,17 @@ class SequenceGenerator extends AbstractIdGenerator implements Serializable
|
||||
return $this->nextValue;
|
||||
}
|
||||
|
||||
/** @deprecated without replacement. */
|
||||
final public function serialize(): string
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11468',
|
||||
'%s() is deprecated, use __serialize() instead. %s won\'t implement the Serializable interface anymore in ORM 4.',
|
||||
__METHOD__,
|
||||
self::class,
|
||||
);
|
||||
|
||||
return serialize($this->__serialize());
|
||||
}
|
||||
|
||||
@@ -79,8 +89,17 @@ class SequenceGenerator extends AbstractIdGenerator implements Serializable
|
||||
];
|
||||
}
|
||||
|
||||
/** @deprecated without replacement. */
|
||||
final public function unserialize(string $serialized): void
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11468',
|
||||
'%s() is deprecated, use __unserialize() instead. %s won\'t implement the Serializable interface anymore in ORM 4.',
|
||||
__METHOD__,
|
||||
self::class,
|
||||
);
|
||||
|
||||
$this->__unserialize(unserialize($serialized));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Internal;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Common\Collections\Order;
|
||||
|
||||
use function array_map;
|
||||
use function enum_exists;
|
||||
use function method_exists;
|
||||
use function strtoupper;
|
||||
|
||||
trait CriteriaOrderings
|
||||
{
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*
|
||||
* @psalm-suppress DeprecatedMethod We need to call the deprecated API if the new one does not exist yet.
|
||||
*/
|
||||
private static function getCriteriaOrderings(Criteria $criteria): array
|
||||
{
|
||||
if (! method_exists(Criteria::class, 'orderings')) {
|
||||
return $criteria->getOrderings();
|
||||
}
|
||||
|
||||
return array_map(
|
||||
static fn (Order $order): string => $order->value,
|
||||
$criteria->orderings(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $orderings
|
||||
*
|
||||
* @return array<string, string>|array<string, Order>
|
||||
*/
|
||||
private static function mapToOrderEnumIfAvailable(array $orderings): array
|
||||
{
|
||||
if (! enum_exists(Order::class)) {
|
||||
return $orderings;
|
||||
}
|
||||
|
||||
return array_map(
|
||||
static fn (string $order): Order => Order::from(strtoupper($order)),
|
||||
$orderings,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -104,29 +104,31 @@ abstract class AbstractHydrator
|
||||
|
||||
$this->prepare();
|
||||
|
||||
while (true) {
|
||||
$row = $this->statement()->fetchAssociative();
|
||||
try {
|
||||
while (true) {
|
||||
$row = $this->statement()->fetchAssociative();
|
||||
|
||||
if ($row === false) {
|
||||
$this->cleanup();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
$this->hydrateRowData($row, $result);
|
||||
|
||||
$this->cleanupAfterRowIteration();
|
||||
if (count($result) === 1) {
|
||||
if (count($resultSetMapping->indexByMap) === 0) {
|
||||
yield end($result);
|
||||
} else {
|
||||
yield from $result;
|
||||
if ($row === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
$this->hydrateRowData($row, $result);
|
||||
|
||||
$this->cleanupAfterRowIteration();
|
||||
if (count($result) === 1) {
|
||||
if (count($resultSetMapping->indexByMap) === 0) {
|
||||
yield end($result);
|
||||
} else {
|
||||
yield from $result;
|
||||
}
|
||||
} else {
|
||||
yield $result;
|
||||
}
|
||||
} else {
|
||||
yield $result;
|
||||
}
|
||||
} finally {
|
||||
$this->cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,15 +252,16 @@ abstract class AbstractHydrator
|
||||
* @psalm-return array{
|
||||
* data: array<array-key, array>,
|
||||
* newObjects?: array<array-key, array{
|
||||
* class: mixed,
|
||||
* args?: array
|
||||
* class: ReflectionClass,
|
||||
* args: array,
|
||||
* obj: object
|
||||
* }>,
|
||||
* scalars?: array
|
||||
* }
|
||||
*/
|
||||
protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents): array
|
||||
{
|
||||
$rowData = ['data' => []];
|
||||
$rowData = ['data' => [], 'newObjects' => []];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$cacheKeyInfo = $this->hydrateColumnInfo($key);
|
||||
@@ -333,6 +336,25 @@ abstract class AbstractHydrator
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->resultSetMapping()->nestedNewObjectArguments as $objIndex => ['ownerIndex' => $ownerIndex, 'argIndex' => $argIndex]) {
|
||||
if (! isset($rowData['newObjects'][$ownerIndex . ':' . $argIndex])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$newObject = $rowData['newObjects'][$ownerIndex . ':' . $argIndex];
|
||||
unset($rowData['newObjects'][$ownerIndex . ':' . $argIndex]);
|
||||
|
||||
$obj = $newObject['class']->newInstanceArgs($newObject['args']);
|
||||
|
||||
$rowData['newObjects'][$ownerIndex]['args'][$argIndex] = $obj;
|
||||
}
|
||||
|
||||
foreach ($rowData['newObjects'] as $objIndex => $newObject) {
|
||||
$obj = $newObject['class']->newInstanceArgs($newObject['args']);
|
||||
|
||||
$rowData['newObjects'][$objIndex]['obj'] = $obj;
|
||||
}
|
||||
|
||||
return $rowData;
|
||||
}
|
||||
|
||||
|
||||
@@ -214,9 +214,8 @@ class ArrayHydrator extends AbstractHydrator
|
||||
$scalarCount = (isset($rowData['scalars']) ? count($rowData['scalars']) : 0);
|
||||
|
||||
foreach ($rowData['newObjects'] as $objIndex => $newObject) {
|
||||
$class = $newObject['class'];
|
||||
$args = $newObject['args'];
|
||||
$obj = $class->newInstanceArgs($args);
|
||||
$args = $newObject['args'];
|
||||
$obj = $newObject['obj'];
|
||||
|
||||
if (count($args) === $scalarCount || ($scalarCount === 0 && count($rowData['newObjects']) === 1)) {
|
||||
$result[$resultKey] = $obj;
|
||||
|
||||
@@ -64,4 +64,9 @@ class HydrationException extends Exception implements ORMException
|
||||
implode('", "', $discrValues),
|
||||
));
|
||||
}
|
||||
|
||||
public static function partialObjectHydrationDisallowed(): self
|
||||
{
|
||||
return new self('Hydration of entity objects is not allowed when DQL PARTIAL keyword is used.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ class ObjectHydrator extends AbstractHydrator
|
||||
}
|
||||
|
||||
// handle fetch-joined owning side bi-directional one-to-one associations
|
||||
if ($assoc->inversedBy) {
|
||||
if ($assoc->inversedBy !== null) {
|
||||
$class = $this->getClassMetadata($className);
|
||||
$inverseAssoc = $class->associationMappings[$assoc->inversedBy];
|
||||
|
||||
@@ -265,7 +265,7 @@ class ObjectHydrator extends AbstractHydrator
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $className
|
||||
* @param class-string $className
|
||||
* @psalm-param array<string, mixed> $data
|
||||
*/
|
||||
private function getEntityFromIdentityMap(string $className, array $data): object|bool
|
||||
@@ -356,11 +356,15 @@ class ObjectHydrator extends AbstractHydrator
|
||||
$parentObject = $this->resultPointers[$parentAlias];
|
||||
} else {
|
||||
// Parent object of relation not found, mark as not-fetched again
|
||||
$element = $this->getEntity($data, $dqlAlias);
|
||||
if (isset($nonemptyComponents[$dqlAlias])) {
|
||||
$element = $this->getEntity($data, $dqlAlias);
|
||||
|
||||
// Update result pointer and provide initial fetch data for parent
|
||||
$this->resultPointers[$dqlAlias] = $element;
|
||||
$rowData['data'][$parentAlias][$relationField] = $element;
|
||||
// Update result pointer and provide initial fetch data for parent
|
||||
$this->resultPointers[$dqlAlias] = $element;
|
||||
$rowData['data'][$parentAlias][$relationField] = $element;
|
||||
} else {
|
||||
$element = null;
|
||||
}
|
||||
|
||||
// Mark as not-fetched again
|
||||
unset($this->hints['fetched'][$parentAlias][$relationField]);
|
||||
@@ -439,7 +443,7 @@ class ObjectHydrator extends AbstractHydrator
|
||||
if ($relation->isOwningSide()) {
|
||||
// TODO: Just check hints['fetched'] here?
|
||||
// If there is an inverse mapping on the target class its bidirectional
|
||||
if ($relation->inversedBy) {
|
||||
if ($relation->inversedBy !== null) {
|
||||
$inverseAssoc = $targetClass->associationMappings[$relation->inversedBy];
|
||||
if ($inverseAssoc->isToOne()) {
|
||||
$targetClass->reflFields[$inverseAssoc->fieldName]->setValue($element, $parentObject);
|
||||
@@ -552,9 +556,7 @@ class ObjectHydrator extends AbstractHydrator
|
||||
$scalarCount = (isset($rowData['scalars']) ? count($rowData['scalars']) : 0);
|
||||
|
||||
foreach ($rowData['newObjects'] as $objIndex => $newObject) {
|
||||
$class = $newObject['class'];
|
||||
$args = $newObject['args'];
|
||||
$obj = $class->newInstanceArgs($args);
|
||||
$obj = $newObject['obj'];
|
||||
|
||||
if ($scalarCount === 0 && count($rowData['newObjects']) === 1) {
|
||||
$result[$resultKey] = $obj;
|
||||
|
||||
55
src/Internal/NoUnknownNamedArguments.php
Normal file
55
src/Internal/NoUnknownNamedArguments.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Internal;
|
||||
|
||||
use BadMethodCallException;
|
||||
|
||||
use function array_filter;
|
||||
use function array_is_list;
|
||||
use function array_keys;
|
||||
use function array_values;
|
||||
use function assert;
|
||||
use function debug_backtrace;
|
||||
use function implode;
|
||||
use function is_string;
|
||||
use function sprintf;
|
||||
|
||||
use const DEBUG_BACKTRACE_IGNORE_ARGS;
|
||||
|
||||
/**
|
||||
* Checks if a variadic parameter contains unexpected named arguments.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait NoUnknownNamedArguments
|
||||
{
|
||||
/**
|
||||
* @param TItem[] $parameter
|
||||
*
|
||||
* @template TItem
|
||||
* @psalm-assert list<TItem> $parameter
|
||||
*/
|
||||
private static function validateVariadicParameter(array $parameter): void
|
||||
{
|
||||
if (array_is_list($parameter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
[, $trace] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||
assert(isset($trace['class']));
|
||||
|
||||
$additionalArguments = array_values(array_filter(
|
||||
array_keys($parameter),
|
||||
is_string(...),
|
||||
));
|
||||
|
||||
throw new BadMethodCallException(sprintf(
|
||||
'Invalid call to %s::%s(), unknown named arguments: %s',
|
||||
$trace['class'],
|
||||
$trace['function'],
|
||||
implode(', ', $additionalArguments),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -64,11 +64,11 @@ class LazyCriteriaCollection extends AbstractLazyCollection implements Selectabl
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* Do an optimized search of an element
|
||||
*
|
||||
* @template TMaybeContained
|
||||
* @param mixed $element The element to search for.
|
||||
*
|
||||
* @return bool TRUE if the collection contains $element, FALSE otherwise.
|
||||
*/
|
||||
public function contains(mixed $element): bool
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Mapping;
|
||||
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use InvalidArgumentException;
|
||||
|
||||
use function property_exists;
|
||||
@@ -14,12 +15,26 @@ trait ArrayAccessImplementation
|
||||
/** @param string $offset */
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11211',
|
||||
'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.',
|
||||
static::class,
|
||||
);
|
||||
|
||||
return isset($this->$offset);
|
||||
}
|
||||
|
||||
/** @param string $offset */
|
||||
public function offsetGet(mixed $offset): mixed
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11211',
|
||||
'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.',
|
||||
static::class,
|
||||
);
|
||||
|
||||
if (! property_exists($this, $offset)) {
|
||||
throw new InvalidArgumentException('Undefined property: ' . $offset);
|
||||
}
|
||||
@@ -30,12 +45,26 @@ trait ArrayAccessImplementation
|
||||
/** @param string $offset */
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11211',
|
||||
'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.',
|
||||
static::class,
|
||||
);
|
||||
|
||||
$this->$offset = $value;
|
||||
}
|
||||
|
||||
/** @param string $offset */
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11211',
|
||||
'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.',
|
||||
static::class,
|
||||
);
|
||||
|
||||
$this->$offset = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ abstract class AssociationMapping implements ArrayAccess
|
||||
/**
|
||||
* The fetching strategy to use for the association, usually defaults to FETCH_LAZY.
|
||||
*
|
||||
* @var ClassMetadata::FETCH_*
|
||||
* @var ClassMetadata::FETCH_*|null
|
||||
*/
|
||||
public int|null $fetch = null;
|
||||
|
||||
@@ -96,13 +96,26 @@ abstract class AssociationMapping implements ArrayAccess
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $mappingArray
|
||||
* @psalm-param array{
|
||||
* fieldName: string,
|
||||
* sourceEntity: class-string,
|
||||
* targetEntity: class-string,
|
||||
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
|
||||
* fetch?: ClassMetadata::FETCH_*|null,
|
||||
* inherited?: class-string|null,
|
||||
* declared?: class-string|null,
|
||||
* cache?: array<mixed>|null,
|
||||
* id?: bool|null,
|
||||
* isOnDeleteCascade?: bool|null,
|
||||
* originalClass?: class-string|null,
|
||||
* originalField?: string|null,
|
||||
* orphanRemoval?: bool,
|
||||
* unique?: bool|null,
|
||||
* joinTable?: mixed[]|null,
|
||||
* type?: int,
|
||||
* isOwningSide: bool, ...} $mappingArray
|
||||
* isOwningSide: bool,
|
||||
* } $mappingArray
|
||||
*/
|
||||
public static function fromMappingArray(array $mappingArray): static
|
||||
{
|
||||
|
||||
@@ -169,8 +169,8 @@ class ClassMetadataBuilder
|
||||
/**
|
||||
* Sets the discriminator column details.
|
||||
*
|
||||
* @psalm-param class-string<BackedEnum>|null $enumType
|
||||
* @psalm-param array<string, mixed> $options
|
||||
* @param class-string<BackedEnum>|null $enumType
|
||||
* @param array<string, mixed> $options
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
@@ -288,7 +288,7 @@ class ClassMetadataBuilder
|
||||
): ClassMetadataBuilder {
|
||||
$builder = $this->createManyToOne($name, $targetEntity);
|
||||
|
||||
if ($inversedBy) {
|
||||
if ($inversedBy !== null) {
|
||||
$builder->inversedBy($inversedBy);
|
||||
}
|
||||
|
||||
@@ -348,7 +348,7 @@ class ClassMetadataBuilder
|
||||
): ClassMetadataBuilder {
|
||||
$builder = $this->createOneToOne($name, $targetEntity);
|
||||
|
||||
if ($inversedBy) {
|
||||
if ($inversedBy !== null) {
|
||||
$builder->inversedBy($inversedBy);
|
||||
}
|
||||
|
||||
@@ -380,7 +380,7 @@ class ClassMetadataBuilder
|
||||
): ClassMetadataBuilder {
|
||||
$builder = $this->createManyToMany($name, $targetEntity);
|
||||
|
||||
if ($inversedBy) {
|
||||
if ($inversedBy !== null) {
|
||||
$builder->inversedBy($inversedBy);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,18 +4,20 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Mapping;
|
||||
|
||||
use Doctrine\ORM\Internal\NoUnknownNamedArguments;
|
||||
use ReflectionProperty;
|
||||
|
||||
final class ChainTypedFieldMapper implements TypedFieldMapper
|
||||
{
|
||||
/**
|
||||
* @readonly
|
||||
* @var TypedFieldMapper[] $typedFieldMappers
|
||||
*/
|
||||
use NoUnknownNamedArguments;
|
||||
|
||||
/** @var list<TypedFieldMapper> $typedFieldMappers */
|
||||
private readonly array $typedFieldMappers;
|
||||
|
||||
public function __construct(TypedFieldMapper ...$typedFieldMappers)
|
||||
{
|
||||
self::validateVariadicParameter($typedFieldMappers);
|
||||
|
||||
$this->typedFieldMappers = $typedFieldMappers;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace Doctrine\ORM\Mapping;
|
||||
use BackedEnum;
|
||||
use BadMethodCallException;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\Instantiator\Instantiator;
|
||||
use Doctrine\Instantiator\InstantiatorInterface;
|
||||
use Doctrine\ORM\Cache\Exception\NonCacheableEntityAssociation;
|
||||
@@ -14,6 +16,7 @@ use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Id\AbstractIdGenerator;
|
||||
use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata;
|
||||
use Doctrine\Persistence\Mapping\ReflectionService;
|
||||
use Doctrine\Persistence\Reflection\EnumReflectionProperty;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use ReflectionClass;
|
||||
@@ -21,6 +24,7 @@ use ReflectionNamedType;
|
||||
use ReflectionProperty;
|
||||
use Stringable;
|
||||
|
||||
use function array_column;
|
||||
use function array_diff;
|
||||
use function array_intersect;
|
||||
use function array_key_exists;
|
||||
@@ -32,6 +36,7 @@ use function array_values;
|
||||
use function assert;
|
||||
use function class_exists;
|
||||
use function count;
|
||||
use function defined;
|
||||
use function enum_exists;
|
||||
use function explode;
|
||||
use function in_array;
|
||||
@@ -821,7 +826,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
assert($childProperty !== null);
|
||||
|
||||
if (isset($mapping->enumType)) {
|
||||
$childProperty = new ReflectionEnumProperty(
|
||||
$childProperty = new EnumReflectionProperty(
|
||||
$childProperty,
|
||||
$mapping->enumType,
|
||||
);
|
||||
@@ -840,7 +845,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
: $this->getAccessibleProperty($reflService, $this->name, $field);
|
||||
|
||||
if (isset($mapping->enumType) && $this->reflFields[$field] !== null) {
|
||||
$this->reflFields[$field] = new ReflectionEnumProperty(
|
||||
$this->reflFields[$field] = new EnumReflectionProperty(
|
||||
$this->reflFields[$field],
|
||||
$mapping->enumType,
|
||||
);
|
||||
@@ -1037,6 +1042,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
*/
|
||||
public function getColumnName(string $fieldName): string
|
||||
{
|
||||
// @phpstan-ignore property.deprecated
|
||||
return $this->columnNames[$fieldName] ?? $fieldName;
|
||||
}
|
||||
|
||||
@@ -1116,9 +1122,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
{
|
||||
$field = $this->reflClass->getProperty($mapping['fieldName']);
|
||||
|
||||
$mapping = $this->typedFieldMapper->validateAndComplete($mapping, $field);
|
||||
|
||||
return $mapping;
|
||||
return $this->typedFieldMapper->validateAndComplete($mapping, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1150,7 +1154,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
* fieldName?: string,
|
||||
* columnName?: string,
|
||||
* id?: bool,
|
||||
* generated?: int,
|
||||
* generated?: self::GENERATED_*,
|
||||
* enumType?: class-string,
|
||||
* } $mapping The field mapping to validate & complete.
|
||||
*
|
||||
@@ -1186,6 +1190,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
$mapping->quoted = true;
|
||||
}
|
||||
|
||||
// @phpstan-ignore property.deprecated
|
||||
$this->columnNames[$mapping->fieldName] = $mapping->columnName;
|
||||
|
||||
if (isset($this->fieldNames[$mapping->columnName]) || ($this->discriminatorColumn && $this->discriminatorColumn->name === $mapping->columnName)) {
|
||||
@@ -1228,6 +1233,14 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
if (! empty($mapping->id)) {
|
||||
$this->containsEnumIdentifier = true;
|
||||
}
|
||||
|
||||
if (
|
||||
defined('Doctrine\DBAL\Types\Types::ENUM')
|
||||
&& $mapping->type === Types::ENUM
|
||||
&& ! isset($mapping->options['values'])
|
||||
) {
|
||||
$mapping->options['values'] = array_column($mapping->enumType::cases(), 'value');
|
||||
}
|
||||
}
|
||||
|
||||
return $mapping;
|
||||
@@ -1766,6 +1779,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
|
||||
unset($this->fieldMappings[$fieldName]);
|
||||
unset($this->fieldNames[$mapping->columnName]);
|
||||
// @phpstan-ignore property.deprecated
|
||||
unset($this->columnNames[$mapping->fieldName]);
|
||||
|
||||
$overrideMapping = $this->validateAndCompleteFieldMapping($overrideMapping);
|
||||
@@ -1916,8 +1930,9 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
public function addInheritedFieldMapping(FieldMapping $fieldMapping): void
|
||||
{
|
||||
$this->fieldMappings[$fieldMapping->fieldName] = $fieldMapping;
|
||||
$this->columnNames[$fieldMapping->fieldName] = $fieldMapping->columnName;
|
||||
$this->fieldNames[$fieldMapping->columnName] = $fieldMapping->fieldName;
|
||||
// @phpstan-ignore property.deprecated
|
||||
$this->columnNames[$fieldMapping->fieldName] = $fieldMapping->columnName;
|
||||
$this->fieldNames[$fieldMapping->columnName] = $fieldMapping->fieldName;
|
||||
|
||||
if (isset($fieldMapping->generated)) {
|
||||
$this->requiresFetchAfterChange = true;
|
||||
@@ -2004,6 +2019,12 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
*/
|
||||
public function setCustomRepositoryClass(string|null $repositoryClassName): void
|
||||
{
|
||||
if ($repositoryClassName === null) {
|
||||
$this->customRepositoryClassName = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->customRepositoryClassName = $this->fullyQualifiedClassName($repositoryClassName);
|
||||
}
|
||||
|
||||
@@ -2476,11 +2497,25 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
return $assoc->mappedBy;
|
||||
}
|
||||
|
||||
/** @return string|null null if the input value is null */
|
||||
/**
|
||||
* @param C $className
|
||||
*
|
||||
* @return string|null null if and only if the input value is null
|
||||
* @psalm-return (C is class-string ? class-string : (C is string ? string : null))
|
||||
*
|
||||
* @template C of string|null
|
||||
*/
|
||||
public function fullyQualifiedClassName(string|null $className): string|null
|
||||
{
|
||||
if (empty($className)) {
|
||||
return $className;
|
||||
if ($className === null) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11294',
|
||||
'Passing null to %s is deprecated and will not be supported in Doctrine ORM 4.0',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! str_contains($className, '\\') && $this->namespace) {
|
||||
@@ -2523,12 +2558,8 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
throw MappingException::missingEmbeddedClass($mapping['fieldName']);
|
||||
}
|
||||
|
||||
$fqcn = $this->fullyQualifiedClassName($mapping['class']);
|
||||
|
||||
assert($fqcn !== null);
|
||||
|
||||
$this->embeddedClasses[$mapping['fieldName']] = EmbeddedClassMapping::fromMappingArray([
|
||||
'class' => $fqcn,
|
||||
'class' => $this->fullyQualifiedClassName($mapping['class']),
|
||||
'columnPrefix' => $mapping['columnPrefix'] ?? null,
|
||||
'declaredField' => $mapping['declaredField'] ?? null,
|
||||
'originalField' => $mapping['originalField'] ?? null,
|
||||
|
||||
@@ -399,7 +399,7 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
|
||||
/**
|
||||
* Gets the lower-case short name of a class.
|
||||
*
|
||||
* @psalm-param class-string $className
|
||||
* @param class-string $className
|
||||
*/
|
||||
private function getShortName(string $className): string
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ use function trim;
|
||||
*/
|
||||
class DefaultEntityListenerResolver implements EntityListenerResolver
|
||||
{
|
||||
/** @psalm-var array<class-string, object> Map to store entity listener instances. */
|
||||
/** @var array<class-string, object> Map to store entity listener instances. */
|
||||
private array $instances = [];
|
||||
|
||||
public function clear(string|null $className = null): void
|
||||
|
||||
@@ -16,6 +16,7 @@ use ReflectionProperty;
|
||||
|
||||
use function array_merge;
|
||||
use function assert;
|
||||
use function defined;
|
||||
use function enum_exists;
|
||||
use function is_a;
|
||||
|
||||
@@ -49,30 +50,40 @@ final class DefaultTypedFieldMapper implements TypedFieldMapper
|
||||
{
|
||||
$type = $field->getType();
|
||||
|
||||
if (! $type instanceof ReflectionNamedType) {
|
||||
return $mapping;
|
||||
}
|
||||
|
||||
if (
|
||||
! isset($mapping['type'])
|
||||
&& ($type instanceof ReflectionNamedType)
|
||||
! $type->isBuiltin()
|
||||
&& enum_exists($type->getName())
|
||||
&& (! isset($mapping['type']) || (
|
||||
defined('Doctrine\DBAL\Types\Types::ENUM')
|
||||
&& $mapping['type'] === Types::ENUM
|
||||
))
|
||||
) {
|
||||
if (! $type->isBuiltin() && enum_exists($type->getName())) {
|
||||
$reflection = new ReflectionEnum($type->getName());
|
||||
if (! $reflection->isBacked()) {
|
||||
throw MappingException::backedEnumTypeRequired(
|
||||
$field->class,
|
||||
$mapping['fieldName'],
|
||||
$type->getName(),
|
||||
);
|
||||
}
|
||||
|
||||
assert(is_a($type->getName(), BackedEnum::class, true));
|
||||
$mapping['enumType'] = $type->getName();
|
||||
$type = $reflection->getBackingType();
|
||||
|
||||
assert($type instanceof ReflectionNamedType);
|
||||
$reflection = new ReflectionEnum($type->getName());
|
||||
if (! $reflection->isBacked()) {
|
||||
throw MappingException::backedEnumTypeRequired(
|
||||
$field->class,
|
||||
$mapping['fieldName'],
|
||||
$type->getName(),
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($this->typedFieldMappings[$type->getName()])) {
|
||||
$mapping['type'] = $this->typedFieldMappings[$type->getName()];
|
||||
}
|
||||
assert(is_a($type->getName(), BackedEnum::class, true));
|
||||
$mapping['enumType'] = $type->getName();
|
||||
$type = $reflection->getBackingType();
|
||||
|
||||
assert($type instanceof ReflectionNamedType);
|
||||
}
|
||||
|
||||
if (isset($mapping['type'])) {
|
||||
return $mapping;
|
||||
}
|
||||
|
||||
if (isset($this->typedFieldMappings[$type->getName()])) {
|
||||
$mapping['type'] = $this->typedFieldMappings[$type->getName()];
|
||||
}
|
||||
|
||||
return $mapping;
|
||||
|
||||
@@ -69,8 +69,8 @@ class AttributeDriver implements MappingDriver
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-param class-string<T> $className
|
||||
* @psalm-param ClassMetadata<T> $metadata
|
||||
* @param class-string<T> $className
|
||||
* @param ClassMetadata<T> $metadata
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
@@ -390,7 +390,7 @@ class AttributeDriver implements MappingDriver
|
||||
$metadata->mapOneToMany($mapping);
|
||||
} elseif ($manyToOneAttribute !== null) {
|
||||
if ($metadata->isEmbeddedClass) {
|
||||
throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\OneToMany::class);
|
||||
throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\ManyToOne::class);
|
||||
}
|
||||
|
||||
$idAttribute = $this->reader->getPropertyAttribute($property, Mapping\Id::class);
|
||||
|
||||
@@ -35,6 +35,8 @@ use function strtolower;
|
||||
/**
|
||||
* The DatabaseDriver reverse engineers the mapping metadata from a database.
|
||||
*
|
||||
* @deprecated No replacement planned
|
||||
*
|
||||
* @link www.doctrine-project.org
|
||||
*/
|
||||
class DatabaseDriver implements MappingDriver
|
||||
@@ -151,8 +153,8 @@ class DatabaseDriver implements MappingDriver
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-param class-string<T> $className
|
||||
* @psalm-param ClassMetadata<T> $metadata
|
||||
* @param class-string<T> $className
|
||||
* @param ClassMetadata<T> $metadata
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
@@ -491,7 +493,7 @@ class DatabaseDriver implements MappingDriver
|
||||
/**
|
||||
* Returns the mapped class name for a table if it exists. Otherwise return "classified" version.
|
||||
*
|
||||
* @psalm-return class-string
|
||||
* @return class-string
|
||||
*/
|
||||
private function getClassNameForTable(string $tableName): string
|
||||
{
|
||||
|
||||
@@ -38,6 +38,8 @@ use function strtoupper;
|
||||
* XmlDriver is a metadata driver that enables mapping through XML files.
|
||||
*
|
||||
* @link www.doctrine-project.org
|
||||
*
|
||||
* @template-extends FileDriver<SimpleXMLElement>
|
||||
*/
|
||||
class XmlDriver extends FileDriver
|
||||
{
|
||||
@@ -70,15 +72,14 @@ class XmlDriver extends FileDriver
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-param class-string<T> $className
|
||||
* @psalm-param ClassMetadata<T> $metadata
|
||||
* @param class-string<T> $className
|
||||
* @param ClassMetadata<T> $metadata
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
public function loadMetadataForClass($className, PersistenceClassMetadata $metadata): void
|
||||
{
|
||||
$xmlRoot = $this->getElement($className);
|
||||
assert($xmlRoot instanceof SimpleXMLElement);
|
||||
|
||||
if ($xmlRoot->getName() === 'entity') {
|
||||
if (isset($xmlRoot['repository-class'])) {
|
||||
@@ -134,6 +135,7 @@ class XmlDriver extends FileDriver
|
||||
];
|
||||
|
||||
if (isset($discrColumn['options'])) {
|
||||
assert($discrColumn['options'] instanceof SimpleXMLElement);
|
||||
$columnDef['options'] = $this->parseOptions($discrColumn['options']->children());
|
||||
}
|
||||
|
||||
@@ -145,6 +147,7 @@ class XmlDriver extends FileDriver
|
||||
// Evaluate <discriminator-map...>
|
||||
if (isset($xmlRoot->{'discriminator-map'})) {
|
||||
$map = [];
|
||||
assert($xmlRoot->{'discriminator-map'}->{'discriminator-mapping'} instanceof SimpleXMLElement);
|
||||
foreach ($xmlRoot->{'discriminator-map'}->{'discriminator-mapping'} as $discrMapElement) {
|
||||
$map[(string) $discrMapElement['value']] = (string) $discrMapElement['class'];
|
||||
}
|
||||
@@ -408,6 +411,7 @@ class XmlDriver extends FileDriver
|
||||
/** @psalm-suppress DeprecatedConstant */
|
||||
$orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
|
||||
? (string) $orderByField['direction']
|
||||
// @phpstan-ignore classConstant.deprecated
|
||||
: (enum_exists(Order::class) ? Order::Ascending->value : Criteria::ASC);
|
||||
}
|
||||
|
||||
@@ -537,6 +541,7 @@ class XmlDriver extends FileDriver
|
||||
/** @psalm-suppress DeprecatedConstant */
|
||||
$orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
|
||||
? (string) $orderByField['direction']
|
||||
// @phpstan-ignore classConstant.deprecated
|
||||
: (enum_exists(Order::class) ? Order::Ascending->value : Criteria::ASC);
|
||||
}
|
||||
|
||||
@@ -886,19 +891,19 @@ class XmlDriver extends FileDriver
|
||||
|
||||
if (isset($xmlElement->entity)) {
|
||||
foreach ($xmlElement->entity as $entityElement) {
|
||||
/** @psalm-var class-string $entityName */
|
||||
/** @var class-string $entityName */
|
||||
$entityName = (string) $entityElement['name'];
|
||||
$result[$entityName] = $entityElement;
|
||||
}
|
||||
} elseif (isset($xmlElement->{'mapped-superclass'})) {
|
||||
foreach ($xmlElement->{'mapped-superclass'} as $mappedSuperClass) {
|
||||
/** @psalm-var class-string $className */
|
||||
/** @var class-string $className */
|
||||
$className = (string) $mappedSuperClass['name'];
|
||||
$result[$className] = $mappedSuperClass;
|
||||
}
|
||||
} elseif (isset($xmlElement->embeddable)) {
|
||||
foreach ($xmlElement->embeddable as $embeddableElement) {
|
||||
/** @psalm-var class-string $embeddableName */
|
||||
/** @var class-string $embeddableName */
|
||||
$embeddableName = (string) $embeddableElement['name'];
|
||||
$result[$embeddableName] = $embeddableElement;
|
||||
}
|
||||
|
||||
@@ -49,10 +49,12 @@ final class EmbeddedClassMapping implements ArrayAccess
|
||||
|
||||
/**
|
||||
* @psalm-param array{
|
||||
* class: class-string,
|
||||
* columnPrefix?: false|string|null,
|
||||
* declaredField?: string|null,
|
||||
* originalField?: string|null
|
||||
* class: class-string,
|
||||
* columnPrefix?: false|string|null,
|
||||
* declaredField?: string|null,
|
||||
* originalField?: string|null,
|
||||
* inherited?: class-string|null,
|
||||
* declared?: class-string|null,
|
||||
* } $mappingArray
|
||||
*/
|
||||
public static function fromMappingArray(array $mappingArray): self
|
||||
|
||||
@@ -26,7 +26,7 @@ final class FieldMapping implements ArrayAccess
|
||||
public bool|null $notInsertable = null;
|
||||
public bool|null $notUpdatable = null;
|
||||
public string|null $columnDefinition = null;
|
||||
/** @psalm-var ClassMetadata::GENERATED_* */
|
||||
/** @psalm-var ClassMetadata::GENERATED_*|null */
|
||||
public int|null $generated = null;
|
||||
/** @var class-string<BackedEnum>|null */
|
||||
public string|null $enumType = null;
|
||||
@@ -83,7 +83,34 @@ final class FieldMapping implements ArrayAccess
|
||||
) {
|
||||
}
|
||||
|
||||
/** @param array{type: string, fieldName: string, columnName: string} $mappingArray */
|
||||
/**
|
||||
* @param array<string, mixed> $mappingArray
|
||||
* @psalm-param array{
|
||||
* type: string,
|
||||
* fieldName: string,
|
||||
* columnName: string,
|
||||
* length?: int|null,
|
||||
* id?: bool|null,
|
||||
* nullable?: bool|null,
|
||||
* notInsertable?: bool|null,
|
||||
* notUpdatable?: bool|null,
|
||||
* columnDefinition?: string|null,
|
||||
* generated?: ClassMetadata::GENERATED_*|null,
|
||||
* enumType?: string|null,
|
||||
* precision?: int|null,
|
||||
* scale?: int|null,
|
||||
* unique?: bool|null,
|
||||
* inherited?: string|null,
|
||||
* originalClass?: string|null,
|
||||
* originalField?: string|null,
|
||||
* quoted?: bool|null,
|
||||
* declared?: string|null,
|
||||
* declaredField?: string|null,
|
||||
* options?: array<string, mixed>|null,
|
||||
* version?: bool|null,
|
||||
* default?: string|int|null,
|
||||
* } $mappingArray
|
||||
*/
|
||||
public static function fromMappingArray(array $mappingArray): self
|
||||
{
|
||||
$mapping = new self(
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace Doctrine\ORM\Mapping;
|
||||
|
||||
use Attribute;
|
||||
|
||||
@@ -31,7 +31,17 @@ final class JoinColumnMapping implements ArrayAccess
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $mappingArray
|
||||
* @psalm-param array{name: string, referencedColumnName: string, ...} $mappingArray
|
||||
* @psalm-param array{
|
||||
* name: string,
|
||||
* referencedColumnName: string,
|
||||
* unique?: bool|null,
|
||||
* quoted?: bool|null,
|
||||
* fieldName?: string|null,
|
||||
* onDelete?: string|null,
|
||||
* columnDefinition?: string|null,
|
||||
* nullable?: bool|null,
|
||||
* options?: array<string, mixed>|null,
|
||||
* } $mappingArray
|
||||
*/
|
||||
public static function fromMappingArray(array $mappingArray): self
|
||||
{
|
||||
|
||||
@@ -35,10 +35,10 @@ final class JoinTableMapping implements ArrayAccess
|
||||
* @param mixed[] $mappingArray
|
||||
* @psalm-param array{
|
||||
* name: string,
|
||||
* quoted?: bool,
|
||||
* quoted?: bool|null,
|
||||
* joinColumns?: mixed[],
|
||||
* inverseJoinColumns?: mixed[],
|
||||
* schema?: string,
|
||||
* schema?: string|null,
|
||||
* options?: array<string, mixed>
|
||||
* } $mappingArray
|
||||
*/
|
||||
|
||||
@@ -41,9 +41,21 @@ final class ManyToManyOwningSideMapping extends ToManyOwningSideMapping implemen
|
||||
* fieldName: string,
|
||||
* sourceEntity: class-string,
|
||||
* targetEntity: class-string,
|
||||
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
|
||||
* fetch?: ClassMetadata::FETCH_*|null,
|
||||
* inherited?: class-string|null,
|
||||
* declared?: class-string|null,
|
||||
* cache?: array<mixed>|null,
|
||||
* id?: bool|null,
|
||||
* isOnDeleteCascade?: bool|null,
|
||||
* originalClass?: class-string|null,
|
||||
* originalField?: string|null,
|
||||
* orphanRemoval?: bool,
|
||||
* unique?: bool|null,
|
||||
* joinTable?: mixed[]|null,
|
||||
* type?: int,
|
||||
* isOwningSide: bool, ...} $mappingArray
|
||||
* isOwningSide: bool,
|
||||
* } $mappingArray
|
||||
*/
|
||||
public static function fromMappingArrayAndNamingStrategy(array $mappingArray, NamingStrategy $namingStrategy): self
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ use Doctrine\ORM\EntityRepository;
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
final class MappedSuperclass implements MappingAttribute
|
||||
{
|
||||
/** @psalm-param class-string<EntityRepository>|null $repositoryClass */
|
||||
/** @param class-string<EntityRepository>|null $repositoryClass */
|
||||
public function __construct(
|
||||
public readonly string|null $repositoryClass = null,
|
||||
) {
|
||||
|
||||
@@ -12,9 +12,21 @@ final class OneToManyAssociationMapping extends ToManyInverseSideMapping
|
||||
* fieldName: string,
|
||||
* sourceEntity: class-string,
|
||||
* targetEntity: class-string,
|
||||
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
|
||||
* fetch?: ClassMetadata::FETCH_*|null,
|
||||
* inherited?: class-string|null,
|
||||
* declared?: class-string|null,
|
||||
* cache?: array<mixed>|null,
|
||||
* id?: bool|null,
|
||||
* isOnDeleteCascade?: bool|null,
|
||||
* originalClass?: class-string|null,
|
||||
* originalField?: string|null,
|
||||
* orphanRemoval?: bool,
|
||||
* unique?: bool|null,
|
||||
* joinTable?: mixed[]|null,
|
||||
* type?: int,
|
||||
* isOwningSide: bool, ...} $mappingArray
|
||||
* isOwningSide: bool,
|
||||
* } $mappingArray
|
||||
*/
|
||||
public static function fromMappingArray(array $mappingArray): static
|
||||
{
|
||||
@@ -33,9 +45,21 @@ final class OneToManyAssociationMapping extends ToManyInverseSideMapping
|
||||
* fieldName: string,
|
||||
* sourceEntity: class-string,
|
||||
* targetEntity: class-string,
|
||||
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
|
||||
* fetch?: ClassMetadata::FETCH_*|null,
|
||||
* inherited?: class-string|null,
|
||||
* declared?: class-string|null,
|
||||
* cache?: array<mixed>|null,
|
||||
* id?: bool|null,
|
||||
* isOnDeleteCascade?: bool|null,
|
||||
* originalClass?: class-string|null,
|
||||
* originalField?: string|null,
|
||||
* orphanRemoval?: bool,
|
||||
* unique?: bool|null,
|
||||
* joinTable?: mixed[]|null,
|
||||
* type?: int,
|
||||
* isOwningSide: bool, ...} $mappingArray
|
||||
* isOwningSide: bool,
|
||||
* } $mappingArray
|
||||
*/
|
||||
public static function fromMappingArrayAndName(array $mappingArray, string $name): static
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@ final class ReflectionEmbeddedProperty extends ReflectionProperty
|
||||
private readonly ReflectionProperty $childProperty,
|
||||
private readonly string $embeddedClass,
|
||||
) {
|
||||
parent::__construct($childProperty->class, $childProperty->name);
|
||||
parent::__construct($childProperty->getDeclaringClass()->name, $childProperty->getName());
|
||||
}
|
||||
|
||||
public function getValue(object|null $object = null): mixed
|
||||
|
||||
@@ -11,6 +11,7 @@ use ValueError;
|
||||
use function array_map;
|
||||
use function is_array;
|
||||
|
||||
/** @deprecated use Doctrine\Persistence\Reflection\EnumReflectionProperty instead */
|
||||
final class ReflectionEnumProperty extends ReflectionProperty
|
||||
{
|
||||
/** @param class-string<BackedEnum> $enumType */
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Doctrine\ORM\Mapping;
|
||||
|
||||
use Attribute;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
final class Table implements MappingAttribute
|
||||
@@ -21,5 +22,24 @@ final class Table implements MappingAttribute
|
||||
public readonly array|null $uniqueConstraints = null,
|
||||
public readonly array $options = [],
|
||||
) {
|
||||
if ($this->indexes !== null) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11357',
|
||||
'Providing the property $indexes on %s does not have any effect and will be removed in Doctrine ORM 4.0. Please use the %s attribute instead.',
|
||||
self::class,
|
||||
Index::class,
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->uniqueConstraints !== null) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11357',
|
||||
'Providing the property $uniqueConstraints on %s does not have any effect and will be removed in Doctrine ORM 4.0. Please use the %s attribute instead.',
|
||||
self::class,
|
||||
UniqueConstraint::class,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,21 @@ abstract class ToOneInverseSideMapping extends InverseSideMapping
|
||||
* fieldName: string,
|
||||
* sourceEntity: class-string,
|
||||
* targetEntity: class-string,
|
||||
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
|
||||
* fetch?: ClassMetadata::FETCH_*|null,
|
||||
* inherited?: class-string|null,
|
||||
* declared?: class-string|null,
|
||||
* cache?: array<mixed>|null,
|
||||
* id?: bool|null,
|
||||
* isOnDeleteCascade?: bool|null,
|
||||
* originalClass?: class-string|null,
|
||||
* originalField?: string|null,
|
||||
* orphanRemoval?: bool,
|
||||
* unique?: bool|null,
|
||||
* joinTable?: mixed[]|null,
|
||||
* type?: int,
|
||||
* isOwningSide: bool,
|
||||
* } $mappingArray
|
||||
* } $mappingArray
|
||||
*/
|
||||
public static function fromMappingArrayAndName(
|
||||
array $mappingArray,
|
||||
|
||||
@@ -31,8 +31,22 @@ abstract class ToOneOwningSideMapping extends OwningSideMapping implements ToOne
|
||||
* fieldName: string,
|
||||
* sourceEntity: class-string,
|
||||
* targetEntity: class-string,
|
||||
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
|
||||
* fetch?: ClassMetadata::FETCH_*|null,
|
||||
* inherited?: class-string|null,
|
||||
* declared?: class-string|null,
|
||||
* cache?: array<mixed>|null,
|
||||
* id?: bool|null,
|
||||
* isOnDeleteCascade?: bool|null,
|
||||
* originalClass?: class-string|null,
|
||||
* originalField?: string|null,
|
||||
* orphanRemoval?: bool,
|
||||
* unique?: bool|null,
|
||||
* joinTable?: mixed[]|null,
|
||||
* type?: int,
|
||||
* isOwningSide: bool,
|
||||
* joinColumns?: mixed[]|null,
|
||||
* isOwningSide: bool, ...} $mappingArray
|
||||
* } $mappingArray
|
||||
*/
|
||||
public static function fromMappingArray(array $mappingArray): static
|
||||
{
|
||||
@@ -64,8 +78,22 @@ abstract class ToOneOwningSideMapping extends OwningSideMapping implements ToOne
|
||||
* fieldName: string,
|
||||
* sourceEntity: class-string,
|
||||
* targetEntity: class-string,
|
||||
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
|
||||
* fetch?: ClassMetadata::FETCH_*|null,
|
||||
* inherited?: class-string|null,
|
||||
* declared?: class-string|null,
|
||||
* cache?: array<mixed>|null,
|
||||
* id?: bool|null,
|
||||
* isOnDeleteCascade?: bool|null,
|
||||
* originalClass?: class-string|null,
|
||||
* originalField?: string|null,
|
||||
* orphanRemoval?: bool,
|
||||
* unique?: bool|null,
|
||||
* joinTable?: mixed[]|null,
|
||||
* type?: int,
|
||||
* isOwningSide: bool,
|
||||
* joinColumns?: mixed[]|null,
|
||||
* isOwningSide: bool, ...} $mappingArray
|
||||
* } $mappingArray
|
||||
*/
|
||||
public static function fromMappingArrayAndName(
|
||||
array $mappingArray,
|
||||
|
||||
@@ -40,7 +40,15 @@ class NativeQuery extends AbstractQuery
|
||||
$types = [];
|
||||
|
||||
foreach ($this->getParameters() as $parameter) {
|
||||
$name = $parameter->getName();
|
||||
$name = $parameter->getName();
|
||||
|
||||
if ($parameter->typeWasSpecified()) {
|
||||
$parameters[$name] = $parameter->getValue();
|
||||
$types[$name] = $parameter->getType();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $this->processParameterValue($parameter->getValue());
|
||||
$type = $parameter->getValue() === $value
|
||||
? $parameter->getType()
|
||||
|
||||
@@ -8,8 +8,8 @@ use Doctrine\Common\Collections\AbstractLazyCollection;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\Common\Collections\Selectable;
|
||||
use Doctrine\ORM\Internal\CriteriaOrderings;
|
||||
use Doctrine\ORM\Mapping\AssociationMapping;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\ToManyAssociationMapping;
|
||||
@@ -24,6 +24,7 @@ use function array_walk;
|
||||
use function assert;
|
||||
use function is_object;
|
||||
use function spl_object_id;
|
||||
use function strtoupper;
|
||||
|
||||
/**
|
||||
* A PersistentCollection represents a collection of elements that have persistent state.
|
||||
@@ -41,8 +42,6 @@ use function spl_object_id;
|
||||
*/
|
||||
final class PersistentCollection extends AbstractLazyCollection implements Selectable
|
||||
{
|
||||
use CriteriaOrderings;
|
||||
|
||||
/**
|
||||
* A snapshot of the collection at the moment it was fetched from the database.
|
||||
* This is used to create a diff of the collection at commit time.
|
||||
@@ -351,11 +350,6 @@ final class PersistentCollection extends AbstractLazyCollection implements Selec
|
||||
return parent::containsKey($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @template TMaybeContained
|
||||
*/
|
||||
public function contains(mixed $element): bool
|
||||
{
|
||||
if (! $this->initialized && $this->getMapping()->fetch === ClassMetadata::FETCH_EXTRA_LAZY) {
|
||||
@@ -588,9 +582,12 @@ final class PersistentCollection extends AbstractLazyCollection implements Selec
|
||||
|
||||
$criteria = clone $criteria;
|
||||
$criteria->where($expression);
|
||||
$criteria->orderBy(self::mapToOrderEnumIfAvailable(
|
||||
self::getCriteriaOrderings($criteria) ?: $association->orderBy(),
|
||||
));
|
||||
$criteria->orderBy(
|
||||
$criteria->orderings() ?: array_map(
|
||||
static fn (string $order): Order => Order::from(strtoupper($order)),
|
||||
$association->orderBy(),
|
||||
),
|
||||
);
|
||||
|
||||
$persister = $this->getUnitOfWork()->getEntityPersister($association->targetEntity);
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Common\Collections\Expr\Comparison;
|
||||
use Doctrine\DBAL\Exception as DBALException;
|
||||
use Doctrine\DBAL\LockMode;
|
||||
use Doctrine\ORM\Internal\CriteriaOrderings;
|
||||
use Doctrine\ORM\Mapping\AssociationMapping;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\InverseSideMapping;
|
||||
@@ -33,8 +32,6 @@ use function sprintf;
|
||||
*/
|
||||
class ManyToManyPersister extends AbstractCollectionPersister
|
||||
{
|
||||
use CriteriaOrderings;
|
||||
|
||||
public function delete(PersistentCollection $collection): void
|
||||
{
|
||||
$mapping = $this->getMapping($collection);
|
||||
@@ -735,7 +732,7 @@ class ManyToManyPersister extends AbstractCollectionPersister
|
||||
|
||||
private function getOrderingSql(Criteria $criteria, ClassMetadata $targetClass): string
|
||||
{
|
||||
$orderings = self::getCriteriaOrderings($criteria);
|
||||
$orderings = $criteria->orderings();
|
||||
if ($orderings) {
|
||||
$orderBy = [];
|
||||
foreach ($orderings as $name => $direction) {
|
||||
@@ -744,7 +741,7 @@ class ManyToManyPersister extends AbstractCollectionPersister
|
||||
$targetClass,
|
||||
$this->platform,
|
||||
);
|
||||
$orderBy[] = $field . ' ' . $direction;
|
||||
$orderBy[] = $field . ' ' . $direction->value;
|
||||
}
|
||||
|
||||
return ' ORDER BY ' . implode(', ', $orderBy);
|
||||
|
||||
@@ -8,13 +8,18 @@ use BadMethodCallException;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\DBAL\Exception as DBALException;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\ORM\EntityNotFoundException;
|
||||
use Doctrine\ORM\Mapping\MappingException;
|
||||
use Doctrine\ORM\Mapping\OneToManyAssociationMapping;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\Utility\PersisterHelper;
|
||||
|
||||
use function array_fill;
|
||||
use function array_keys;
|
||||
use function array_reverse;
|
||||
use function array_values;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function implode;
|
||||
use function is_int;
|
||||
use function is_string;
|
||||
@@ -146,7 +151,11 @@ class OneToManyPersister extends AbstractCollectionPersister
|
||||
throw new BadMethodCallException('Filtering a collection by Criteria is not supported by this CollectionPersister.');
|
||||
}
|
||||
|
||||
/** @throws DBALException */
|
||||
/**
|
||||
* @throws DBALException
|
||||
* @throws EntityNotFoundException
|
||||
* @throws MappingException
|
||||
*/
|
||||
private function deleteEntityCollection(PersistentCollection $collection): int
|
||||
{
|
||||
$mapping = $this->getMapping($collection);
|
||||
@@ -166,6 +175,16 @@ class OneToManyPersister extends AbstractCollectionPersister
|
||||
$statement = 'DELETE FROM ' . $this->quoteStrategy->getTableName($targetClass, $this->platform)
|
||||
. ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
|
||||
|
||||
if ($targetClass->isInheritanceTypeSingleTable()) {
|
||||
$discriminatorColumn = $targetClass->getDiscriminatorColumn();
|
||||
$discriminatorValues = $targetClass->discriminatorValue ? [$targetClass->discriminatorValue] : array_keys($targetClass->discriminatorMap);
|
||||
$statement .= ' AND ' . $discriminatorColumn->name . ' IN (' . implode(', ', array_fill(0, count($discriminatorValues), '?')) . ')';
|
||||
foreach ($discriminatorValues as $discriminatorValue) {
|
||||
$parameters[] = $discriminatorValue;
|
||||
$types[] = $discriminatorColumn->type;
|
||||
}
|
||||
}
|
||||
|
||||
$numAffected = $this->conn->executeStatement($statement, $parameters, $types);
|
||||
|
||||
assert(is_int($numAffected));
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace Doctrine\ORM\Persisters\Entity;
|
||||
use BackedEnum;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Common\Collections\Expr\Comparison;
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\LockMode;
|
||||
@@ -16,7 +17,6 @@ use Doctrine\DBAL\Result;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Internal\CriteriaOrderings;
|
||||
use Doctrine\ORM\Mapping\AssociationMapping;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\JoinColumnMapping;
|
||||
@@ -98,7 +98,6 @@ use function trim;
|
||||
*/
|
||||
class BasicEntityPersister implements EntityPersister
|
||||
{
|
||||
use CriteriaOrderings;
|
||||
use LockSqlHelper;
|
||||
|
||||
/** @var array<string,string> */
|
||||
@@ -766,7 +765,7 @@ class BasicEntityPersister implements EntityPersister
|
||||
$targetClass = $this->em->getClassMetadata($assoc->targetEntity);
|
||||
|
||||
if ($assoc->isOwningSide()) {
|
||||
$isInverseSingleValued = $assoc->inversedBy && ! $targetClass->isCollectionValuedAssociation($assoc->inversedBy);
|
||||
$isInverseSingleValued = $assoc->inversedBy !== null && ! $targetClass->isCollectionValuedAssociation($assoc->inversedBy);
|
||||
|
||||
// Mark inverse side as fetched in the hints, otherwise the UoW would
|
||||
// try to load it in a separate query (remember: to-one inverse sides can not be lazy).
|
||||
@@ -793,17 +792,42 @@ class BasicEntityPersister implements EntityPersister
|
||||
|
||||
$computedIdentifier = [];
|
||||
|
||||
/** @var array<string,mixed>|null $sourceEntityData */
|
||||
$sourceEntityData = null;
|
||||
|
||||
// TRICKY: since the association is specular source and target are flipped
|
||||
foreach ($owningAssoc->targetToSourceKeyColumns as $sourceKeyColumn => $targetKeyColumn) {
|
||||
if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) {
|
||||
throw MappingException::joinColumnMustPointToMappedField(
|
||||
$sourceClass->name,
|
||||
$sourceKeyColumn,
|
||||
);
|
||||
}
|
||||
// The likely case here is that the column is a join column
|
||||
// in an association mapping. However, there is no guarantee
|
||||
// at this point that a corresponding (generally identifying)
|
||||
// association has been mapped in the source entity. To handle
|
||||
// this case we directly reference the column-keyed data used
|
||||
// to initialize the source entity before throwing an exception.
|
||||
$resolvedSourceData = false;
|
||||
if (! isset($sourceEntityData)) {
|
||||
$sourceEntityData = $this->em->getUnitOfWork()->getOriginalEntityData($sourceEntity);
|
||||
}
|
||||
|
||||
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
|
||||
$sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
|
||||
if (isset($sourceEntityData[$sourceKeyColumn])) {
|
||||
$dataValue = $sourceEntityData[$sourceKeyColumn];
|
||||
if ($dataValue !== null) {
|
||||
$resolvedSourceData = true;
|
||||
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
|
||||
$dataValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $resolvedSourceData) {
|
||||
throw MappingException::joinColumnMustPointToMappedField(
|
||||
$sourceClass->name,
|
||||
$sourceKeyColumn,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
|
||||
$sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
|
||||
}
|
||||
}
|
||||
|
||||
$targetEntity = $this->load($computedIdentifier, null, $assoc);
|
||||
@@ -836,7 +860,10 @@ class BasicEntityPersister implements EntityPersister
|
||||
? $this->expandCriteriaParameters($criteria)
|
||||
: $this->expandParameters($criteria);
|
||||
|
||||
return (int) $this->conn->executeQuery($sql, $params, $types)->fetchOne();
|
||||
$count = (int) $this->conn->executeQuery($sql, $params, $types)->fetchOne();
|
||||
assert($count >= 0);
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -844,7 +871,10 @@ class BasicEntityPersister implements EntityPersister
|
||||
*/
|
||||
public function loadCriteria(Criteria $criteria): array
|
||||
{
|
||||
$orderBy = self::getCriteriaOrderings($criteria);
|
||||
$orderBy = array_map(
|
||||
static fn (Order $order): string => $order->value,
|
||||
$criteria->orderings(),
|
||||
);
|
||||
$limit = $criteria->getMaxResults();
|
||||
$offset = $criteria->getFirstResult();
|
||||
$query = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
|
||||
@@ -1153,7 +1183,7 @@ class BasicEntityPersister implements EntityPersister
|
||||
|
||||
if (isset($this->class->fieldMappings[$fieldName])) {
|
||||
$tableAlias = isset($this->class->fieldMappings[$fieldName]->inherited)
|
||||
? $this->getSQLTableAlias($this->class->fieldMappings[$fieldName]['inherited'])
|
||||
? $this->getSQLTableAlias($this->class->fieldMappings[$fieldName]->inherited)
|
||||
: $baseTableAlias;
|
||||
|
||||
$columnName = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform);
|
||||
|
||||
@@ -125,6 +125,8 @@ interface EntityPersister
|
||||
* Count entities (optionally filtered by a criteria)
|
||||
*
|
||||
* @param mixed[]|Criteria $criteria
|
||||
*
|
||||
* @psalm-return 0|positive-int
|
||||
*/
|
||||
public function count(array|Criteria $criteria = []): int;
|
||||
|
||||
|
||||
@@ -466,7 +466,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
|
||||
|| isset($this->class->associationMappings[$name]->inherited)
|
||||
|| ($this->class->isVersioned && $this->class->versionField === $name)
|
||||
|| isset($this->class->embeddedClasses[$name])
|
||||
|| isset($this->class->fieldMappings[$name]['notInsertable'])
|
||||
|| isset($this->class->fieldMappings[$name]->notInsertable)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -519,9 +519,9 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
|
||||
$class = null;
|
||||
if ($this->class->isVersioned && $key === $versionedClass->versionField) {
|
||||
$class = $versionedClass;
|
||||
} elseif (isset($column['generated'])) {
|
||||
$class = isset($column['inherited'])
|
||||
? $this->em->getClassMetadata($column['inherited'])
|
||||
} elseif (isset($column->generated)) {
|
||||
$class = isset($column->inherited)
|
||||
? $this->em->getClassMetadata($column->inherited)
|
||||
: $this->class;
|
||||
} else {
|
||||
continue;
|
||||
|
||||
@@ -210,17 +210,16 @@ EOPHP;
|
||||
/**
|
||||
* Creates a closure capable of initializing a proxy
|
||||
*
|
||||
* @return Closure(InternalProxy, InternalProxy):void
|
||||
* @return Closure(InternalProxy, array):void
|
||||
*
|
||||
* @throws EntityNotFoundException
|
||||
*/
|
||||
private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure
|
||||
{
|
||||
return static function (InternalProxy $proxy, InternalProxy $original) use ($entityPersister, $classMetadata, $identifierFlattener): void {
|
||||
$identifier = $classMetadata->getIdentifierValues($original);
|
||||
$entity = $entityPersister->loadById($identifier, $original);
|
||||
return static function (InternalProxy $proxy, array $identifier) use ($entityPersister, $classMetadata, $identifierFlattener): void {
|
||||
$original = $entityPersister->loadById($identifier);
|
||||
|
||||
if ($entity === null) {
|
||||
if ($original === null) {
|
||||
throw EntityNotFoundException::fromClassNameAndIdentifier(
|
||||
$classMetadata->getName(),
|
||||
$identifierFlattener->flattenIdentifier($classMetadata, $identifier),
|
||||
@@ -234,11 +233,11 @@ EOPHP;
|
||||
$class = $entityPersister->getClassMetadata();
|
||||
|
||||
foreach ($class->getReflectionProperties() as $property) {
|
||||
if (! $property || ! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) {
|
||||
if (! $property || isset($identifier[$property->getName()]) || ! $class->hasField($property->getName()) && ! $class->hasAssociation($property->getName())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$property->setValue($proxy, $property->getValue($entity));
|
||||
$property->setValue($proxy, $property->getValue($original));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -283,8 +282,8 @@ EOPHP;
|
||||
$identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers);
|
||||
|
||||
$proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy {
|
||||
$proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, &$proxy): void {
|
||||
$initializer($object, $proxy);
|
||||
$proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void {
|
||||
$initializer($object, $identifier);
|
||||
}, $skippedProperties);
|
||||
|
||||
foreach ($identifierFields as $idField => $reflector) {
|
||||
@@ -388,12 +387,18 @@ EOPHP;
|
||||
$code = substr($code, 7 + (int) strpos($code, "\n{"));
|
||||
$code = substr($code, 0, (int) strpos($code, "\n}"));
|
||||
$code = str_replace('LazyGhostTrait;', str_replace("\n ", "\n", 'LazyGhostTrait {
|
||||
initializeLazyObject as __load;
|
||||
initializeLazyObject as private;
|
||||
setLazyObjectAsInitialized as public __setInitialized;
|
||||
isLazyObjectInitialized as private;
|
||||
createLazyGhost as private;
|
||||
resetLazyObject as private;
|
||||
}'), $code);
|
||||
}
|
||||
|
||||
public function __load(): void
|
||||
{
|
||||
$this->initializeLazyObject();
|
||||
}
|
||||
'), $code);
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@ namespace Doctrine\ORM;
|
||||
use Doctrine\DBAL\LockMode;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Query\AST\DeleteStatement;
|
||||
use Doctrine\ORM\Query\AST\SelectStatement;
|
||||
use Doctrine\ORM\Query\AST\UpdateStatement;
|
||||
use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
|
||||
use Doctrine\ORM\Query\Exec\SqlFinalizer;
|
||||
use Doctrine\ORM\Query\OutputWalker;
|
||||
use Doctrine\ORM\Query\Parameter;
|
||||
use Doctrine\ORM\Query\ParameterTypeInferer;
|
||||
use Doctrine\ORM\Query\Parser;
|
||||
@@ -27,6 +30,7 @@ use function assert;
|
||||
use function count;
|
||||
use function get_debug_type;
|
||||
use function in_array;
|
||||
use function is_a;
|
||||
use function ksort;
|
||||
use function md5;
|
||||
use function reset;
|
||||
@@ -70,6 +74,14 @@ class Query extends AbstractQuery
|
||||
*/
|
||||
public const HINT_REFRESH_ENTITY = 'doctrine.refresh.entity';
|
||||
|
||||
/**
|
||||
* The forcePartialLoad query hint forces a particular query to return
|
||||
* partial objects.
|
||||
*
|
||||
* @todo Rename: HINT_OPTIMIZE
|
||||
*/
|
||||
public const HINT_FORCE_PARTIAL_LOAD = 'doctrine.forcePartialLoad';
|
||||
|
||||
/**
|
||||
* The includeMetaColumns query hint causes meta columns like foreign keys and
|
||||
* discriminator columns to be selected and returned as part of the query result.
|
||||
@@ -163,7 +175,7 @@ class Query extends AbstractQuery
|
||||
*/
|
||||
public function getSQL(): string|array
|
||||
{
|
||||
return $this->parse()->getSqlExecutor()->getSqlStatements();
|
||||
return $this->getSqlExecutor()->getSqlStatements();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,7 +254,7 @@ class Query extends AbstractQuery
|
||||
|
||||
protected function _doExecute(): Result|int
|
||||
{
|
||||
$executor = $this->parse()->getSqlExecutor();
|
||||
$executor = $this->getSqlExecutor();
|
||||
|
||||
if ($this->queryCacheProfile) {
|
||||
$executor->setQueryCacheProfile($this->queryCacheProfile);
|
||||
@@ -635,9 +647,10 @@ class Query extends AbstractQuery
|
||||
/**
|
||||
* Get the current lock mode for this query.
|
||||
*
|
||||
* @return int|null The current lock mode of this query or NULL if no specific lock mode is set.
|
||||
* @return LockMode|int|null The current lock mode of this query or NULL if no specific lock mode is set.
|
||||
* @psalm-return LockMode::*|null
|
||||
*/
|
||||
public function getLockMode(): int|null
|
||||
public function getLockMode(): LockMode|int|null
|
||||
{
|
||||
$lockMode = $this->getHint(self::HINT_LOCK_MODE);
|
||||
|
||||
@@ -655,11 +668,31 @@ class Query extends AbstractQuery
|
||||
{
|
||||
ksort($this->hints);
|
||||
|
||||
if (! $this->hasHint(self::HINT_CUSTOM_OUTPUT_WALKER)) {
|
||||
// Assume Parser will create the SqlOutputWalker; save is_a call, which might trigger a class load
|
||||
$firstAndMaxResult = '';
|
||||
} else {
|
||||
$outputWalkerClass = $this->getHint(self::HINT_CUSTOM_OUTPUT_WALKER);
|
||||
if (is_a($outputWalkerClass, OutputWalker::class, true)) {
|
||||
$firstAndMaxResult = '';
|
||||
} else {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11188/',
|
||||
'Your output walker class %s should implement %s in order to provide a %s. This also means the output walker should not use the query firstResult/maxResult values, which should be read from the query by the SqlFinalizer only.',
|
||||
$outputWalkerClass,
|
||||
OutputWalker::class,
|
||||
SqlFinalizer::class,
|
||||
);
|
||||
$firstAndMaxResult = '&firstResult=' . $this->firstResult . '&maxResult=' . $this->maxResults;
|
||||
}
|
||||
}
|
||||
|
||||
return md5(
|
||||
$this->getDQL() . serialize($this->hints) .
|
||||
'&platform=' . get_debug_type($this->getEntityManager()->getConnection()->getDatabasePlatform()) .
|
||||
($this->em->hasFilters() ? $this->em->getFilters()->getHash() : '') .
|
||||
'&firstResult=' . $this->firstResult . '&maxResult=' . $this->maxResults .
|
||||
$firstAndMaxResult .
|
||||
'&hydrationMode=' . $this->hydrationMode . '&types=' . serialize($this->parsedTypes) . 'DOCTRINE_QUERY_CACHE_SALT',
|
||||
);
|
||||
}
|
||||
@@ -678,4 +711,9 @@ class Query extends AbstractQuery
|
||||
|
||||
$this->state = self::STATE_DIRTY;
|
||||
}
|
||||
|
||||
private function getSqlExecutor(): AbstractSqlExecutor
|
||||
{
|
||||
return $this->parse()->prepareSqlExecutor($this);
|
||||
}
|
||||
}
|
||||
|
||||
15
src/Query/AST/PartialObjectExpression.php
Normal file
15
src/Query/AST/PartialObjectExpression.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query\AST;
|
||||
|
||||
class PartialObjectExpression extends Node
|
||||
{
|
||||
/** @param mixed[] $partialFieldSet */
|
||||
public function __construct(
|
||||
public string $identificationVariable,
|
||||
public array $partialFieldSet,
|
||||
) {
|
||||
}
|
||||
}
|
||||
29
src/Query/Exec/FinalizedSelectExecutor.php
Normal file
29
src/Query/Exec/FinalizedSelectExecutor.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query\Exec;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Result;
|
||||
|
||||
/**
|
||||
* SQL executor for a given, final, single SELECT SQL query
|
||||
*
|
||||
* @method string getSqlStatements()
|
||||
*/
|
||||
class FinalizedSelectExecutor extends AbstractSqlExecutor
|
||||
{
|
||||
public function __construct(string $sql)
|
||||
{
|
||||
$this->sqlStatements = $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function execute(Connection $conn, array $params, array $types): Result
|
||||
{
|
||||
return $conn->executeQuery($this->getSqlStatements(), $params, $types, $this->queryCacheProfile);
|
||||
}
|
||||
}
|
||||
27
src/Query/Exec/PreparedExecutorFinalizer.php
Normal file
27
src/Query/Exec/PreparedExecutorFinalizer.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query\Exec;
|
||||
|
||||
use Doctrine\ORM\Query;
|
||||
|
||||
/**
|
||||
* PreparedExecutorFinalizer is a wrapper for the SQL finalization
|
||||
* phase that does nothing - it is constructed with the sql executor
|
||||
* already.
|
||||
*/
|
||||
final class PreparedExecutorFinalizer implements SqlFinalizer
|
||||
{
|
||||
private AbstractSqlExecutor $executor;
|
||||
|
||||
public function __construct(AbstractSqlExecutor $exeutor)
|
||||
{
|
||||
$this->executor = $exeutor;
|
||||
}
|
||||
|
||||
public function createExecutor(Query $query): AbstractSqlExecutor
|
||||
{
|
||||
return $this->executor;
|
||||
}
|
||||
}
|
||||
60
src/Query/Exec/SingleSelectSqlFinalizer.php
Normal file
60
src/Query/Exec/SingleSelectSqlFinalizer.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query\Exec;
|
||||
|
||||
use Doctrine\DBAL\LockMode;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\QueryException;
|
||||
use Doctrine\ORM\Utility\LockSqlHelper;
|
||||
|
||||
/**
|
||||
* SingleSelectSqlFinalizer finalizes a given SQL query by applying
|
||||
* the query's firstResult/maxResult values as well as extra read lock/write lock
|
||||
* statements, both through the platform-specific methods.
|
||||
*
|
||||
* The resulting, "finalized" SQL is passed to a FinalizedSelectExecutor.
|
||||
*/
|
||||
class SingleSelectSqlFinalizer implements SqlFinalizer
|
||||
{
|
||||
use LockSqlHelper;
|
||||
|
||||
public function __construct(private string $sql)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* This method exists temporarily to support old SqlWalker interfaces.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @psalm-internal Doctrine\ORM
|
||||
*/
|
||||
public function finalizeSql(Query $query): string
|
||||
{
|
||||
$platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();
|
||||
|
||||
$sql = $platform->modifyLimitQuery($this->sql, $query->getMaxResults(), $query->getFirstResult());
|
||||
|
||||
$lockMode = $query->getHint(Query::HINT_LOCK_MODE) ?: LockMode::NONE;
|
||||
|
||||
if ($lockMode !== LockMode::NONE && $lockMode !== LockMode::OPTIMISTIC && $lockMode !== LockMode::PESSIMISTIC_READ && $lockMode !== LockMode::PESSIMISTIC_WRITE) {
|
||||
throw QueryException::invalidLockMode();
|
||||
}
|
||||
|
||||
if ($lockMode === LockMode::PESSIMISTIC_READ) {
|
||||
$sql .= ' ' . $this->getReadLockSQL($platform);
|
||||
} elseif ($lockMode === LockMode::PESSIMISTIC_WRITE) {
|
||||
$sql .= ' ' . $this->getWriteLockSQL($platform);
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/** @return FinalizedSelectExecutor */
|
||||
public function createExecutor(Query $query): AbstractSqlExecutor
|
||||
{
|
||||
return new FinalizedSelectExecutor($this->finalizeSql($query));
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,6 @@ use Doctrine\ORM\Query\SqlWalker;
|
||||
* that are mapped to a single table.
|
||||
*
|
||||
* @link www.doctrine-project.org
|
||||
*
|
||||
* @todo This is exactly the same as SingleSelectExecutor. Unify in SingleStatementExecutor.
|
||||
*/
|
||||
class SingleTableDeleteUpdateExecutor extends AbstractSqlExecutor
|
||||
{
|
||||
|
||||
26
src/Query/Exec/SqlFinalizer.php
Normal file
26
src/Query/Exec/SqlFinalizer.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query\Exec;
|
||||
|
||||
use Doctrine\ORM\Query;
|
||||
|
||||
/**
|
||||
* SqlFinalizers are created by OutputWalkers that traversed the DQL AST.
|
||||
* The SqlFinalizer instance can be kept in the query cache and re-used
|
||||
* at a later time.
|
||||
*
|
||||
* Once the SqlFinalizer has been created or retrieved from the query cache,
|
||||
* it receives the Query object again in order to yield the AbstractSqlExecutor
|
||||
* that will then be used to execute the query.
|
||||
*
|
||||
* The SqlFinalizer may assume that the DQL that was used to build the AST
|
||||
* and run the OutputWalker (which created the SqlFinalizer) is equivalent to
|
||||
* the query that will be passed to the createExecutor() method. Potential differences
|
||||
* are the parameter values or firstResult/maxResult settings.
|
||||
*/
|
||||
interface SqlFinalizer
|
||||
{
|
||||
public function createExecutor(Query $query): AbstractSqlExecutor;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query;
|
||||
|
||||
use Doctrine\ORM\Internal\NoUnknownNamedArguments;
|
||||
use Traversable;
|
||||
|
||||
use function implode;
|
||||
@@ -23,6 +24,8 @@ use function str_replace;
|
||||
*/
|
||||
class Expr
|
||||
{
|
||||
use NoUnknownNamedArguments;
|
||||
|
||||
/**
|
||||
* Creates a conjunction of the given boolean expressions.
|
||||
*
|
||||
@@ -38,6 +41,8 @@ class Expr
|
||||
*/
|
||||
public function andX(Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x): Expr\Andx
|
||||
{
|
||||
self::validateVariadicParameter($x);
|
||||
|
||||
return new Expr\Andx($x);
|
||||
}
|
||||
|
||||
@@ -56,6 +61,8 @@ class Expr
|
||||
*/
|
||||
public function orX(Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x): Expr\Orx
|
||||
{
|
||||
self::validateVariadicParameter($x);
|
||||
|
||||
return new Expr\Orx($x);
|
||||
}
|
||||
|
||||
@@ -225,6 +232,8 @@ class Expr
|
||||
*/
|
||||
public function countDistinct(mixed ...$x): string
|
||||
{
|
||||
self::validateVariadicParameter($x);
|
||||
|
||||
return 'COUNT(DISTINCT ' . implode(', ', $x) . ')';
|
||||
}
|
||||
|
||||
@@ -470,6 +479,8 @@ class Expr
|
||||
*/
|
||||
public function concat(mixed ...$x): Expr\Func
|
||||
{
|
||||
self::validateVariadicParameter($x);
|
||||
|
||||
return new Expr\Func('CONCAT', $x);
|
||||
}
|
||||
|
||||
|
||||
27
src/Query/OutputWalker.php
Normal file
27
src/Query/OutputWalker.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query;
|
||||
|
||||
use Doctrine\ORM\Query\Exec\SqlFinalizer;
|
||||
|
||||
/**
|
||||
* Interface for output walkers
|
||||
*
|
||||
* Output walkers, like tree walkers, can traverse the DQL AST to perform
|
||||
* their purpose.
|
||||
*
|
||||
* The goal of an OutputWalker is to ultimately provide the SqlFinalizer
|
||||
* which produces the final, executable SQL statement in a "finalization" phase.
|
||||
*
|
||||
* It must be possible to use the same SqlFinalizer for Queries with different
|
||||
* firstResult/maxResult values. In other words, SQL produced by the
|
||||
* output walker should not depend on those values, and any SQL generation/modification
|
||||
* specific to them should happen in the finalizer's `\Doctrine\ORM\Query\Exec\SqlFinalizer::createExecutor()`
|
||||
* method instead.
|
||||
*/
|
||||
interface OutputWalker
|
||||
{
|
||||
public function getFinalizer(AST\DeleteStatement|AST\UpdateStatement|AST\SelectStatement $AST): SqlFinalizer;
|
||||
}
|
||||
@@ -5,14 +5,20 @@ declare(strict_types=1);
|
||||
namespace Doctrine\ORM\Query;
|
||||
|
||||
use Doctrine\Common\Lexer\Token;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Exception\DuplicateFieldException;
|
||||
use Doctrine\ORM\Exception\NoMatchingPropertyException;
|
||||
use Doctrine\ORM\Mapping\AssociationMapping;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\AST\Functions;
|
||||
use Doctrine\ORM\Query\Exec\SqlFinalizer;
|
||||
use LogicException;
|
||||
use ReflectionClass;
|
||||
|
||||
use function array_intersect;
|
||||
use function array_key_exists;
|
||||
use function array_search;
|
||||
use function assert;
|
||||
use function class_exists;
|
||||
@@ -28,6 +34,7 @@ use function strpos;
|
||||
use function strrpos;
|
||||
use function strtolower;
|
||||
use function substr;
|
||||
use function trim;
|
||||
|
||||
/**
|
||||
* An LL(*) recursive-descent parser for the context-free grammar of the Doctrine Query Language.
|
||||
@@ -48,7 +55,7 @@ final class Parser
|
||||
{
|
||||
/**
|
||||
* @readonly Maps BUILT-IN string function names to AST class names.
|
||||
* @psalm-var array<string, class-string<Functions\FunctionNode>>
|
||||
* @var array<string, class-string<Functions\FunctionNode>>
|
||||
*/
|
||||
private static array $stringFunctions = [
|
||||
'concat' => Functions\ConcatFunction::class,
|
||||
@@ -61,7 +68,7 @@ final class Parser
|
||||
|
||||
/**
|
||||
* @readonly Maps BUILT-IN numeric function names to AST class names.
|
||||
* @psalm-var array<string, class-string<Functions\FunctionNode>>
|
||||
* @var array<string, class-string<Functions\FunctionNode>>
|
||||
*/
|
||||
private static array $numericFunctions = [
|
||||
'length' => Functions\LengthFunction::class,
|
||||
@@ -84,7 +91,7 @@ final class Parser
|
||||
|
||||
/**
|
||||
* @readonly Maps BUILT-IN datetime function names to AST class names.
|
||||
* @psalm-var array<string, class-string<Functions\FunctionNode>>
|
||||
* @var array<string, class-string<Functions\FunctionNode>>
|
||||
*/
|
||||
private static array $datetimeFunctions = [
|
||||
'current_date' => Functions\CurrentDateFunction::class,
|
||||
@@ -102,6 +109,9 @@ final class Parser
|
||||
/** @psalm-var list<array{token: DqlToken|null, expression: mixed, nestingLevel: int}> */
|
||||
private array $deferredIdentificationVariables = [];
|
||||
|
||||
/** @psalm-var list<array{token: DqlToken|null, expression: AST\PartialObjectExpression, nestingLevel: int}> */
|
||||
private array $deferredPartialObjectExpressions = [];
|
||||
|
||||
/** @psalm-var list<array{token: DqlToken|null, expression: AST\PathExpression, nestingLevel: int}> */
|
||||
private array $deferredPathExpressions = [];
|
||||
|
||||
@@ -141,7 +151,7 @@ final class Parser
|
||||
/**
|
||||
* Any additional custom tree walkers that modify the AST.
|
||||
*
|
||||
* @psalm-var list<class-string<TreeWalker>>
|
||||
* @var list<class-string<TreeWalker>>
|
||||
*/
|
||||
private array $customTreeWalkers = [];
|
||||
|
||||
@@ -171,17 +181,24 @@ final class Parser
|
||||
* Sets a custom tree walker that produces output.
|
||||
* This tree walker will be run last over the AST, after any other walkers.
|
||||
*
|
||||
* @psalm-param class-string<SqlWalker> $className
|
||||
* @param class-string<SqlWalker> $className
|
||||
*/
|
||||
public function setCustomOutputTreeWalker(string $className): void
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11641',
|
||||
'%s is deprecated, set the output walker class with the \Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER query hint instead',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
$this->customOutputWalker = $className;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a custom tree walker for modifying the AST.
|
||||
*
|
||||
* @psalm-param class-string<TreeWalker> $className
|
||||
* @param class-string<TreeWalker> $className
|
||||
*/
|
||||
public function addCustomTreeWalker(string $className): void
|
||||
{
|
||||
@@ -224,6 +241,10 @@ final class Parser
|
||||
// This also allows post-processing of the AST for modification purposes.
|
||||
$this->processDeferredIdentificationVariables();
|
||||
|
||||
if ($this->deferredPartialObjectExpressions) {
|
||||
$this->processDeferredPartialObjectExpressions();
|
||||
}
|
||||
|
||||
if ($this->deferredPathExpressions) {
|
||||
$this->processDeferredPathExpressions();
|
||||
}
|
||||
@@ -335,11 +356,26 @@ final class Parser
|
||||
$this->queryComponents = $treeWalkerChain->getQueryComponents();
|
||||
}
|
||||
|
||||
$outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class;
|
||||
$outputWalkerClass = $this->customOutputWalker ?: SqlOutputWalker::class;
|
||||
$outputWalker = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);
|
||||
|
||||
// Assign an SQL executor to the parser result
|
||||
$this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST));
|
||||
if ($outputWalker instanceof OutputWalker) {
|
||||
$finalizer = $outputWalker->getFinalizer($AST);
|
||||
$this->parserResult->setSqlFinalizer($finalizer);
|
||||
} else {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11188/',
|
||||
'Your output walker class %s should implement %s in order to provide a %s. This also means the output walker should not use the query firstResult/maxResult values, which should be read from the query by the SqlFinalizer only.',
|
||||
$outputWalkerClass,
|
||||
OutputWalker::class,
|
||||
SqlFinalizer::class,
|
||||
);
|
||||
// @phpstan-ignore method.deprecated
|
||||
$executor = $outputWalker->getExecutor($AST);
|
||||
// @phpstan-ignore method.deprecated
|
||||
$this->parserResult->setSqlExecutor($executor);
|
||||
}
|
||||
|
||||
return $this->parserResult;
|
||||
}
|
||||
@@ -599,6 +635,44 @@ final class Parser
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the given <tt>PartialObjectExpression</tt> is semantically correct.
|
||||
* It must exist in query components list.
|
||||
*/
|
||||
private function processDeferredPartialObjectExpressions(): void
|
||||
{
|
||||
foreach ($this->deferredPartialObjectExpressions as $deferredItem) {
|
||||
$expr = $deferredItem['expression'];
|
||||
$class = $this->getMetadataForDqlAlias($expr->identificationVariable);
|
||||
|
||||
foreach ($expr->partialFieldSet as $field) {
|
||||
if (isset($class->fieldMappings[$field])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
isset($class->associationMappings[$field]) &&
|
||||
$class->associationMappings[$field]->isToOneOwningSide()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->semanticalError(sprintf(
|
||||
"There is no mapped field named '%s' on class %s.",
|
||||
$field,
|
||||
$class->name,
|
||||
), $deferredItem['token']);
|
||||
}
|
||||
|
||||
if (array_intersect($class->identifier, $expr->partialFieldSet) !== $class->identifier) {
|
||||
$this->semanticalError(
|
||||
'The partial field selection of class ' . $class->name . ' must contain the identifier.',
|
||||
$deferredItem['token'],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the given <tt>ResultVariable</tt> is semantically correct.
|
||||
* It must exist in query components list.
|
||||
@@ -1621,25 +1695,88 @@ final class Parser
|
||||
return new AST\JoinAssociationDeclaration($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
|
||||
* PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
|
||||
*/
|
||||
public function PartialObjectExpression(): AST\PartialObjectExpression
|
||||
{
|
||||
$this->match(TokenType::T_PARTIAL);
|
||||
|
||||
$partialFieldSet = [];
|
||||
|
||||
$identificationVariable = $this->IdentificationVariable();
|
||||
|
||||
$this->match(TokenType::T_DOT);
|
||||
$this->match(TokenType::T_OPEN_CURLY_BRACE);
|
||||
$this->match(TokenType::T_IDENTIFIER);
|
||||
|
||||
assert($this->lexer->token !== null);
|
||||
$field = $this->lexer->token->value;
|
||||
|
||||
// First field in partial expression might be embeddable property
|
||||
while ($this->lexer->isNextToken(TokenType::T_DOT)) {
|
||||
$this->match(TokenType::T_DOT);
|
||||
$this->match(TokenType::T_IDENTIFIER);
|
||||
$field .= '.' . $this->lexer->token->value;
|
||||
}
|
||||
|
||||
$partialFieldSet[] = $field;
|
||||
|
||||
while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
|
||||
$this->match(TokenType::T_COMMA);
|
||||
$this->match(TokenType::T_IDENTIFIER);
|
||||
|
||||
$field = $this->lexer->token->value;
|
||||
|
||||
while ($this->lexer->isNextToken(TokenType::T_DOT)) {
|
||||
$this->match(TokenType::T_DOT);
|
||||
$this->match(TokenType::T_IDENTIFIER);
|
||||
$field .= '.' . $this->lexer->token->value;
|
||||
}
|
||||
|
||||
$partialFieldSet[] = $field;
|
||||
}
|
||||
|
||||
$this->match(TokenType::T_CLOSE_CURLY_BRACE);
|
||||
|
||||
$partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet);
|
||||
|
||||
// Defer PartialObjectExpression validation
|
||||
$this->deferredPartialObjectExpressions[] = [
|
||||
'expression' => $partialObjectExpression,
|
||||
'nestingLevel' => $this->nestingLevel,
|
||||
'token' => $this->lexer->token,
|
||||
];
|
||||
|
||||
return $partialObjectExpression;
|
||||
}
|
||||
|
||||
/**
|
||||
* NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
|
||||
*/
|
||||
public function NewObjectExpression(): AST\NewObjectExpression
|
||||
{
|
||||
$args = [];
|
||||
$useNamedArguments = false;
|
||||
$args = [];
|
||||
$argFieldAlias = [];
|
||||
$this->match(TokenType::T_NEW);
|
||||
|
||||
if ($this->lexer->isNextToken(TokenType::T_NAMED)) {
|
||||
$this->match(TokenType::T_NAMED);
|
||||
$useNamedArguments = true;
|
||||
}
|
||||
|
||||
$className = $this->AbstractSchemaName(); // note that this is not yet validated
|
||||
$token = $this->lexer->token;
|
||||
|
||||
$this->match(TokenType::T_OPEN_PARENTHESIS);
|
||||
|
||||
$args[] = $this->NewObjectArg();
|
||||
$this->addArgument($args, $useNamedArguments);
|
||||
|
||||
while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
|
||||
$this->match(TokenType::T_COMMA);
|
||||
|
||||
$args[] = $this->NewObjectArg();
|
||||
$this->addArgument($args, $useNamedArguments);
|
||||
}
|
||||
|
||||
$this->match(TokenType::T_CLOSE_PARENTHESIS);
|
||||
@@ -1656,25 +1793,71 @@ final class Parser
|
||||
return $expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* NewObjectArg ::= ScalarExpression | "(" Subselect ")"
|
||||
*/
|
||||
public function NewObjectArg(): mixed
|
||||
/** @param array<mixed> $args */
|
||||
public function addArgument(array &$args, bool $useNamedArguments): void
|
||||
{
|
||||
$fieldAlias = null;
|
||||
|
||||
if ($useNamedArguments) {
|
||||
$startToken = $this->lexer->lookahead?->position ?? 0;
|
||||
|
||||
$newArg = $this->NewObjectArg($fieldAlias);
|
||||
|
||||
$key = $fieldAlias ?? $newArg->field ?? null;
|
||||
|
||||
if ($key === null) {
|
||||
throw NoMatchingPropertyException::create(trim(substr(
|
||||
($this->query->getDQL() ?? ''),
|
||||
$startToken,
|
||||
($this->lexer->lookahead->position ?? 0) - $startToken,
|
||||
)));
|
||||
}
|
||||
|
||||
if (array_key_exists($key, $args)) {
|
||||
throw DuplicateFieldException::create($key, trim(substr(
|
||||
($this->query->getDQL() ?? ''),
|
||||
$startToken,
|
||||
($this->lexer->lookahead->position ?? 0) - $startToken,
|
||||
)));
|
||||
}
|
||||
|
||||
$args[$key] = $newArg;
|
||||
} else {
|
||||
$args[] = $this->NewObjectArg($fieldAlias);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]
|
||||
*/
|
||||
public function NewObjectArg(string|null &$fieldAlias = null): mixed
|
||||
{
|
||||
$fieldAlias = null;
|
||||
|
||||
assert($this->lexer->lookahead !== null);
|
||||
$token = $this->lexer->lookahead;
|
||||
$peek = $this->lexer->glimpse();
|
||||
|
||||
assert($peek !== null);
|
||||
|
||||
$expression = null;
|
||||
|
||||
if ($token->type === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT) {
|
||||
$this->match(TokenType::T_OPEN_PARENTHESIS);
|
||||
$expression = $this->Subselect();
|
||||
$this->match(TokenType::T_CLOSE_PARENTHESIS);
|
||||
|
||||
return $expression;
|
||||
} elseif ($token->type === TokenType::T_NEW) {
|
||||
$expression = $this->NewObjectExpression();
|
||||
} else {
|
||||
$expression = $this->ScalarExpression();
|
||||
}
|
||||
|
||||
return $this->ScalarExpression();
|
||||
if ($this->lexer->isNextToken(TokenType::T_AS)) {
|
||||
$this->match(TokenType::T_AS);
|
||||
$fieldAlias = $this->AliasIdentificationVariable();
|
||||
}
|
||||
|
||||
return $expression;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1920,7 +2103,7 @@ final class Parser
|
||||
/**
|
||||
* SelectExpression ::= (
|
||||
* IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration |
|
||||
* "(" Subselect ")" | CaseExpression | NewObjectExpression
|
||||
* PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression
|
||||
* ) [["AS"] ["HIDDEN"] AliasResultVariable]
|
||||
*/
|
||||
public function SelectExpression(): AST\SelectExpression
|
||||
@@ -1961,6 +2144,12 @@ final class Parser
|
||||
|
||||
break;
|
||||
|
||||
// PartialObjectExpression (PARTIAL u.{id, name})
|
||||
case $lookaheadType === TokenType::T_PARTIAL:
|
||||
$expression = $this->PartialObjectExpression();
|
||||
$identVariable = $expression->identificationVariable;
|
||||
break;
|
||||
|
||||
// Subselect
|
||||
case $lookaheadType === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT:
|
||||
$this->match(TokenType::T_OPEN_PARENTHESIS);
|
||||
@@ -1986,7 +2175,7 @@ final class Parser
|
||||
|
||||
default:
|
||||
$this->syntaxError(
|
||||
'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | "(" Subselect ")" | CaseExpression',
|
||||
'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression',
|
||||
$this->lexer->lookahead,
|
||||
);
|
||||
}
|
||||
@@ -2563,7 +2752,10 @@ final class Parser
|
||||
return new AST\ParenthesisExpression($expr);
|
||||
}
|
||||
|
||||
assert($this->lexer->lookahead !== null);
|
||||
if ($this->lexer->lookahead === null) {
|
||||
$this->syntaxError('ArithmeticPrimary');
|
||||
}
|
||||
|
||||
switch ($this->lexer->lookahead->type) {
|
||||
case TokenType::T_COALESCE:
|
||||
case TokenType::T_NULLIF:
|
||||
|
||||
@@ -4,7 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query;
|
||||
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
|
||||
use Doctrine\ORM\Query\Exec\SqlFinalizer;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
@@ -22,6 +24,11 @@ class ParserResult
|
||||
*/
|
||||
private AbstractSqlExecutor|null $sqlExecutor = null;
|
||||
|
||||
/**
|
||||
* The SQL executor used for executing the SQL.
|
||||
*/
|
||||
private SqlFinalizer|null $sqlFinalizer = null;
|
||||
|
||||
/**
|
||||
* The ResultSetMapping that describes how to map the SQL result set.
|
||||
*/
|
||||
@@ -63,6 +70,8 @@ class ParserResult
|
||||
|
||||
/**
|
||||
* Sets the SQL executor that should be used for this ParserResult.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
public function setSqlExecutor(AbstractSqlExecutor $executor): void
|
||||
{
|
||||
@@ -71,6 +80,8 @@ class ParserResult
|
||||
|
||||
/**
|
||||
* Gets the SQL executor used by this ParserResult.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
public function getSqlExecutor(): AbstractSqlExecutor
|
||||
{
|
||||
@@ -84,6 +95,24 @@ class ParserResult
|
||||
return $this->sqlExecutor;
|
||||
}
|
||||
|
||||
public function setSqlFinalizer(SqlFinalizer $finalizer): void
|
||||
{
|
||||
$this->sqlFinalizer = $finalizer;
|
||||
}
|
||||
|
||||
public function prepareSqlExecutor(Query $query): AbstractSqlExecutor
|
||||
{
|
||||
if ($this->sqlFinalizer !== null) {
|
||||
return $this->sqlFinalizer->createExecutor($query);
|
||||
}
|
||||
|
||||
if ($this->sqlExecutor !== null) {
|
||||
return $this->sqlExecutor;
|
||||
}
|
||||
|
||||
throw new LogicException('This ParserResult lacks both the SqlFinalizer as well as the (legacy) SqlExecutor');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a DQL to SQL parameter mapping. One DQL parameter name/position can map to
|
||||
* several SQL parameter positions.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user