Compare commits

...

69 Commits
3.5.4 ... 3.6.0

Author SHA1 Message Date
Grégoire Paris
d4e9276e79 Merge pull request #12325 from doctrine/3.5.x
Merge 3.5.x up into 3.6.x
2025-12-19 21:36:14 +01:00
Grégoire Paris
cee74faa97 Merge remote-tracking branch 'origin/2.20.x' into 3.5.x 2025-12-19 20:59:05 +01:00
Grégoire Paris
9ae2181185 Merge pull request #12165 from HypeMC/debug-events-commands
Add commands for inspecting configured listeners
2025-12-19 08:31:44 +01:00
HypeMC
3e25efd72b One table 2025-12-17 23:17:36 +01:00
HypeMC
47496ed882 Fixes 2025-12-17 18:52:17 +01:00
Sadetdin EYILI
492745d710 docs: add xml example for Single Table Inheritance mapping (#12169) 2025-12-17 10:40:06 +01:00
dependabot[bot]
67419cf951 Bump actions/download-artifact from 6 to 7 (#12321) 2025-12-15 07:56:29 +01:00
dependabot[bot]
1237f5c909 Bump actions/upload-artifact from 5 to 6 (#12322) 2025-12-15 07:55:38 +01:00
Grégoire Paris
609e616f2d Merge pull request #12279 from greg0ire/deprecate-conversion
Deprecate string default expressions
2025-12-10 15:05:54 +01:00
Grégoire Paris
4016d6ba4b Deprecate string default expressions
Right now, the ORM handles the conversion of strings that happen to be
default expressions for date, time and datetime columns into the
corresponding value objects.

Let us allow users to specify these value objects directly, and
deprecate relying on the aforementioned conversion.
2025-12-10 12:08:30 +01:00
dependabot[bot]
dcdd46251e Bump doctrine/.github/.github/workflows/release-on-milestone-closed.yml (#12315)
Bumps [doctrine/.github/.github/workflows/release-on-milestone-closed.yml](https://github.com/doctrine/.github) from 13.0.0 to 13.1.0.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/13.0.0...13.1.0)

---
updated-dependencies:
- dependency-name: doctrine/.github/.github/workflows/release-on-milestone-closed.yml
  dependency-version: 13.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 10:23:27 +01:00
dependabot[bot]
3d98b43561 Bump doctrine/.github/.github/workflows/composer-lint.yml (#12317)
Bumps [doctrine/.github/.github/workflows/composer-lint.yml](https://github.com/doctrine/.github) from 13.0.0 to 13.1.0.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/13.0.0...13.1.0)

---
updated-dependencies:
- dependency-name: doctrine/.github/.github/workflows/composer-lint.yml
  dependency-version: 13.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 10:22:42 +01:00
dependabot[bot]
9f3f70944a Bump doctrine/.github/.github/workflows/coding-standards.yml (#12316)
Bumps [doctrine/.github/.github/workflows/coding-standards.yml](https://github.com/doctrine/.github) from 13.0.0 to 13.1.0.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/13.0.0...13.1.0)

---
updated-dependencies:
- dependency-name: doctrine/.github/.github/workflows/coding-standards.yml
  dependency-version: 13.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 10:22:15 +01:00
dependabot[bot]
05e07c0ae0 Bump doctrine/.github/.github/workflows/documentation.yml (#12318)
Bumps [doctrine/.github/.github/workflows/documentation.yml](https://github.com/doctrine/.github) from 13.0.0 to 13.1.0.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/13.0.0...13.1.0)

---
updated-dependencies:
- dependency-name: doctrine/.github/.github/workflows/documentation.yml
  dependency-version: 13.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 10:21:12 +01:00
Grégoire Paris
fea42ab984 Merge pull request #12299 from alexislefebvre/chore-show-parameters-in-name-of-CI-jobs
chore: show parameters in name of CI jobs
2025-11-30 11:14:14 +01:00
Alexis Lefebvre
7c347b85c1 doc: do not mention InverseJoinColumn since it’s only with PHP 8.0 (#12313) 2025-11-30 00:49:09 +01:00
Alexander M. Turek
458b040d93 Remove obsolete PHPStan ignore rules 2025-11-30 00:34:10 +01:00
Alexander M. Turek
396636a2c2 Merge branch '3.5.x' into 3.6.x
* 3.5.x:
  Remove obsolete VarExporter feature detection (#12309)
  Allow Symfony 8 (#12308)
  Explicitly set a cache in testDisablingXmlValidationIsPossible (#12307)
  Removes Guides from our dependencies (#12303)
  Fix PHPStan and test errors after DBAL 4.4 and Symfony 7.4 releases (#12301)
  Support Symfony Console 8 (#12300)
  Bump doctrine/.github/.github/workflows/composer-lint.yml (#12288)
  Bump doctrine/.github/.github/workflows/documentation.yml (#12289)
  Bump doctrine/.github/.github/workflows/coding-standards.yml (#12290)
  Bump doctrine/.github/.github/workflows/release-on-milestone-closed.yml (#12291)
  Bump actions/checkout from 5 to 6 (#12292)
2025-11-30 00:29:59 +01:00
Alexander M. Turek
78dd074266 Remove obsolete VarExporter feature detection (#12309) 2025-11-30 00:11:02 +01:00
Alexander M. Turek
ff22a00fcf Allow Symfony 8 (#12308) 2025-11-30 00:10:09 +01:00
Alexander M. Turek
02e8ff9663 Explicitly set a cache in testDisablingXmlValidationIsPossible (#12307) 2025-11-29 23:16:54 +01:00
Alexis Lefebvre
01fd55e9ea chore: show parameters in name of CI jobs 2025-11-29 23:12:27 +01:00
Alexander M. Turek
2e75a7f1c1 Merge branch '2.20.x' into 3.5.x
* 2.20.x:
  Support Symfony Console 8 (#12300)
  Bump doctrine/.github/.github/workflows/composer-lint.yml (#12288)
  Bump doctrine/.github/.github/workflows/documentation.yml (#12289)
  Bump doctrine/.github/.github/workflows/coding-standards.yml (#12290)
  Bump doctrine/.github/.github/workflows/release-on-milestone-closed.yml (#12291)
  Bump actions/checkout from 5 to 6 (#12292)
2025-11-29 22:07:13 +01:00
Alexander M. Turek
152b0e3d65 Removes Guides from our dependencies (#12303) 2025-11-29 21:55:58 +01:00
Alexander M. Turek
9d11fdd3da Fix PHPStan and test errors after DBAL 4.4 and Symfony 7.4 releases (#12301)
* Fix PHPStan errors after DBAL 4.4 and Symfony 7.4 releases

* Fix PHPStan and test errors after DBAL 4.4 and Symfony 7.4 releases
2025-11-29 18:04:54 +01:00
Alexander M. Turek
87f1ba74e0 Support Symfony Console 8 (#12300) 2025-11-29 15:03:56 +01:00
Grégoire Paris
f357a33d23 Merge pull request #12293 from doctrine/3.5.x
Merge 3.5.x. up into 3.6.x
2025-11-24 20:25:07 +01:00
Grégoire Paris
ee70178314 Merge pull request #12287 from greg0ire/more-order-by
Add ORDER BY clause to more test cases
2025-11-24 09:40:48 +01:00
dependabot[bot]
ab148d3d9d Bump doctrine/.github/.github/workflows/composer-lint.yml (#12288)
Bumps [doctrine/.github/.github/workflows/composer-lint.yml](https://github.com/doctrine/.github) from 12.2.0 to 13.0.0.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/v12.2.0...13.0.0)

---
updated-dependencies:
- dependency-name: doctrine/.github/.github/workflows/composer-lint.yml
  dependency-version: 13.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 09:25:08 +01:00
dependabot[bot]
3924c38fab Bump doctrine/.github/.github/workflows/documentation.yml (#12289)
Bumps [doctrine/.github/.github/workflows/documentation.yml](https://github.com/doctrine/.github) from 12.2.0 to 13.0.0.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/v12.2.0...13.0.0)

---
updated-dependencies:
- dependency-name: doctrine/.github/.github/workflows/documentation.yml
  dependency-version: 13.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 09:24:42 +01:00
dependabot[bot]
9814078a2c Bump doctrine/.github/.github/workflows/coding-standards.yml (#12290)
Bumps [doctrine/.github/.github/workflows/coding-standards.yml](https://github.com/doctrine/.github) from 12.2.0 to 13.0.0.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/v12.2.0...13.0.0)

---
updated-dependencies:
- dependency-name: doctrine/.github/.github/workflows/coding-standards.yml
  dependency-version: 13.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 09:24:14 +01:00
dependabot[bot]
6de5684fd9 Bump doctrine/.github/.github/workflows/release-on-milestone-closed.yml (#12291)
Bumps [doctrine/.github/.github/workflows/release-on-milestone-closed.yml](https://github.com/doctrine/.github) from 12.2.0 to 13.0.0.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/v12.2.0...13.0.0)

---
updated-dependencies:
- dependency-name: doctrine/.github/.github/workflows/release-on-milestone-closed.yml
  dependency-version: 13.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 09:23:46 +01:00
dependabot[bot]
c142503a52 Bump actions/checkout from 5 to 6 (#12292) 2025-11-24 07:27:27 +01:00
Grégoire Paris
01c178b297 Add ORDER BY clause to more test cases
In https://github.com/doctrine/orm/pull/12222, I thought wrongly thought
the issue I was fixing only affected one test.
2025-11-23 19:18:20 +01:00
Grégoire Paris
ffa50a777f Merge pull request #12286 from greg0ire/3.5.x
Merge 2.20.x up into 3.5.x
2025-11-21 20:24:51 +01:00
Grégoire Paris
649048f745 Merge remote-tracking branch 'origin/2.20.x' into 3.5.x 2025-11-21 19:46:45 +01:00
Grégoire Paris
15537bc218 Merge pull request #12285 from HypeMC/fix-is-foreign-key-composite
Fix check for composite foreign key
2025-11-21 19:29:05 +01:00
HypeMC
bc95c7c08d Fix check for composite foreign key 2025-11-21 07:27:53 +01:00
Grégoire Paris
6982c8ab9d Merge pull request #12284 from doctrine/3.5.x
Merge 3.5.x up into 3.6.x
2025-11-20 22:49:20 +01:00
Grégoire Paris
3df11d518c Merge pull request #12283 from doctrine/2.20.x
Merge 2.20.x up into 3.5.x
2025-11-20 21:31:19 +01:00
Grégoire Paris
c1becd54e6 Merge pull request #12281 from greg0ire/document-default-expressions
Fix documentation about default values
2025-11-20 17:58:48 +01:00
Grégoire Paris
e4d7df29c2 Fix documentation about default values
Saying it is not possible to get Doctrine to use the `DEFAULT` SQL
keyword is wrong.
2025-11-19 23:17:53 +01:00
Grégoire Paris
608705427e Merge pull request #12277 from greg0ire/3.5.x
Merge 2.20.x up into 3.5.x
2025-11-19 07:13:23 +01:00
Grégoire Paris
f0562f4120 Merge pull request #12273 from greg0ire/deprecate-default
Deprecate FieldMapping::$default
2025-11-19 06:50:43 +01:00
Grégoire Paris
9f19310f27 Merge remote-tracking branch 'origin/2.20.x' into 3.5.x 2025-11-19 00:01:30 +01:00
Adrian Brajkovic
e38278bfca Fix eager fetch composite foreign key (#11397)
I think #11289 did not completely fix problem for eager fetch.
Change in that PR checked if primary key of target class is composite but that does not matter when loading collection by foreign key.
It should check if foreign key on target class is composite.

Fix from that PR did not work for me because i had entity with regular autogenerated id (single column), but foreign key referenced entity with composite primary key, like SecondLevelWithoutCompositePrimaryKey in this PR.

Checking if foreign key is composite fixed the problem for me.
2025-11-18 21:40:50 +01:00
Grégoire Paris
62f2cff218 Merge pull request #12268 from pmaasz/querybuilder-hints
Add hints to QueryBuilder
2025-11-15 09:32:24 +01:00
pmaasz
cdd774906b add member variable hints to the querybuilder for hints to be added to the query
This adds the membervariable hints to the QueryBuilder to enable setting hints
that will be applied to the query when it is created. This can help trigger
custom walker classes when the query is not adressable driectly e.g. in
Symfony Form Extensions where the quer_builder normalizer is handed the querybuilder
directly. Also see #11849
The feature mirrors the hint feature from the Query class.
This also adds tests for the hints in the QueryBuilder to ensure that those are added
correctly and applied to the query itself on creation
2025-11-14 09:11:48 +01:00
Grégoire Paris
96776e091d Deprecate FieldMapping::$default
Its purpose is unclear since there is FieldMapping::$options['default']
already.
2025-11-14 08:46:08 +01:00
Grégoire Paris
f7470d8a3f Merge pull request #12271 from greg0ire/3.6.x
Merge 3.5.x up into 3.6.x
2025-11-11 19:31:00 +01:00
Grégoire Paris
2c41cc7f1c Merge remote-tracking branch 'origin/3.5.x' into 3.6.x 2025-11-11 19:28:47 +01:00
Grégoire Paris
f18de9d569 Merge pull request #12269 from greg0ire/3.5.x
Undo merge from 3.6.x into 3.5.x
2025-11-11 19:27:40 +01:00
Grégoire Paris
37f76a8381 Undo merge from 3.6.x into 3.5.x 2025-11-11 19:11:32 +01:00
Grégoire Paris
a6c1e63a60 Merge pull request #12266 from doctrine/3.5.x-merge-up-into-3.6.x_hSSiOXm0
Merge release 3.5.6 into 3.6.x
2025-11-10 22:27:19 +01:00
Grégoire Paris
b62292256a Merge pull request #12265 from greg0ire/3.5.x
Merge 2.20.x up into 3.5.x
2025-11-10 22:11:37 +01:00
Grégoire Paris
b138395194 Merge remote-tracking branch 'origin/2.20.x' into 3.5.x 2025-11-10 21:32:47 +01:00
Grégoire Paris
6881cdff4c Merge pull request #12264 from doctrine/3.5.x-merge-up-into-3.6.x_9GolPzTd
Merge release 3.5.5 into 3.6.x
2025-11-10 21:27:36 +01:00
Grégoire Paris
5bff0919a7 Merge pull request #12254 from elliotbruneel/fix/empty-array-query
fix: handling of empty array in SQL condition generation
2025-11-10 14:35:45 +01:00
Elliot Bruneel
9ef0f5301b fix: update SQL condition for empty array to 1=0 instead of IN (NULL) 2025-11-10 10:44:48 +01:00
Elliot Bruneel
4989ca6f15 test: add test for finding by nullable field with empty array 2025-11-05 10:03:09 +01:00
Elliot Bruneel
32d1e97ce7 chore: improve empty array check in SQL condition generation 2025-11-05 09:51:33 +01:00
Grégoire Paris
ca8147b148 Merge pull request #12257 from doctrine/dependabot/github_actions/2.20.x/doctrine/dot-github-12.2.0
Bump doctrine/.github from 12.1.0 to 12.2.0
2025-11-03 09:32:58 +01:00
dependabot[bot]
c8ebea77f0 Bump doctrine/.github from 12.1.0 to 12.2.0
Bumps [doctrine/.github](https://github.com/doctrine/.github) from 12.1.0 to 12.2.0.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/12.1.0...v12.2.0)

---
updated-dependencies:
- dependency-name: doctrine/.github
  dependency-version: 12.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-03 06:11:36 +00:00
Elliot Bruneel
23f22860f1 chore: update phpstan version and regenerate baseline 2025-10-31 09:04:03 +01:00
Elliot Bruneel
b24586b1b5 fix: handling of empty array in SQL condition generation 2025-10-30 17:31:04 +01:00
Grégoire Paris
9e5442a892 Merge pull request #12251 from doctrine/3.5.x
Merge 3.5.x up into 3.6.x
2025-10-29 20:41:29 +01:00
Grégoire Paris
01774c035c Merge pull request #12065 from whataboutpereira/fix-enum-discriminator-column
Use enum values from enumType in DiscriminatorColumn and check DiscriminatorMap values against it
2025-10-29 07:28:16 +01:00
Reio Remma
6f83166266 Extract enum cases from enumType in DiscriminatorColumn
Check DiscriminatorMap keys match enum cases.
Test values are populated from enum cases and mismatched values throw an exception.
Fixes #11794
2025-10-28 20:36:11 +02:00
HypeMC
cb8a76ba3a Add commands for inspecting configured listeners 2025-09-15 15:03:52 +02:00
55 changed files with 1703 additions and 119 deletions

View File

@@ -24,4 +24,4 @@ on:
jobs:
coding-standards:
uses: "doctrine/.github/.github/workflows/coding-standards.yml@12.1.0"
uses: "doctrine/.github/.github/workflows/coding-standards.yml@13.1.0"

View File

@@ -17,4 +17,4 @@ on:
jobs:
composer-lint:
name: "Composer Lint"
uses: "doctrine/.github/.github/workflows/composer-lint.yml@12.1.0"
uses: "doctrine/.github/.github/workflows/composer-lint.yml@13.1.0"

View File

@@ -1,4 +1,4 @@
name: "CI"
name: "CI: PHPUnit"
on:
pull_request:
@@ -25,7 +25,14 @@ env:
jobs:
phpunit-smoke-check:
name: "PHPUnit with SQLite"
name: >
SQLite -
${{ format('PHP {0} - DBAL {1} - ext. {2} - proxy {3}',
matrix.php-version || 'Ø',
matrix.dbal-version || 'Ø',
matrix.extension || 'Ø',
matrix.proxy || 'Ø'
) }}
runs-on: "ubuntu-22.04"
strategy:
@@ -80,7 +87,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v5"
uses: "actions/checkout@v6"
with:
fetch-depth: 2
@@ -104,6 +111,10 @@ jobs:
run: "composer require doctrine/dbal ^${{ matrix.dbal-version }} --no-update"
if: "${{ matrix.dbal-version != 'default' }}"
- name: "Downgrade VarExporter"
run: 'composer require --no-update "symfony/var-exporter:^6.4 || ^7.4"'
if: "${{ matrix.native_lazy == '0' }}"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v3"
with:
@@ -139,7 +150,7 @@ jobs:
ENABLE_NATIVE_LAZY_OBJECTS: ${{ matrix.native_lazy }}
- name: "Upload coverage file"
uses: "actions/upload-artifact@v5"
uses: "actions/upload-artifact@v6"
with:
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-${{ matrix.stability }}-${{ matrix.native_lazy }}-coverage"
path: "coverage*.xml"
@@ -180,7 +191,13 @@ jobs:
phpunit-postgres:
name: "PHPUnit with PostgreSQL"
name: >
${{ format('PostgreSQL {0} - PHP {1} - DBAL {2} - ext. {3}',
matrix.postgres-version || 'Ø',
matrix.php-version || 'Ø',
matrix.dbal-version || 'Ø',
matrix.extension || 'Ø'
) }}
runs-on: "ubuntu-22.04"
needs: "phpunit-smoke-check"
@@ -223,7 +240,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v5"
uses: "actions/checkout@v6"
with:
fetch-depth: 2
@@ -248,14 +265,20 @@ jobs:
run: "vendor/bin/phpunit -c ci/github/phpunit/pdo_pgsql.xml --coverage-clover=coverage.xml"
- name: "Upload coverage file"
uses: "actions/upload-artifact@v5"
uses: "actions/upload-artifact@v6"
with:
name: "${{ github.job }}-${{ matrix.postgres-version }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.extension }}-coverage"
path: "coverage.xml"
phpunit-mariadb:
name: "PHPUnit with MariaDB"
name: >
${{ format('MariaDB {0} - PHP {1} - DBAL {2} - ext. {3}',
matrix.mariadb-version || 'Ø',
matrix.php-version || 'Ø',
matrix.dbal-version || 'Ø',
matrix.extension || 'Ø'
) }}
runs-on: "ubuntu-22.04"
needs: "phpunit-smoke-check"
@@ -291,7 +314,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v5"
uses: "actions/checkout@v6"
with:
fetch-depth: 2
@@ -316,14 +339,20 @@ jobs:
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --coverage-clover=coverage.xml"
- name: "Upload coverage file"
uses: "actions/upload-artifact@v5"
uses: "actions/upload-artifact@v6"
with:
name: "${{ github.job }}-${{ matrix.mariadb-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
path: "coverage.xml"
phpunit-mysql:
name: "PHPUnit with MySQL"
name: >
${{ format('MySQL {0} - PHP {1} - DBAL {2} - ext. {3}',
matrix.mysql-version || 'Ø',
matrix.php-version || 'Ø',
matrix.dbal-version || 'Ø',
matrix.extension || 'Ø'
) }}
runs-on: "ubuntu-22.04"
needs: "phpunit-smoke-check"
@@ -367,7 +396,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v5"
uses: "actions/checkout@v6"
with:
fetch-depth: 2
@@ -413,7 +442,7 @@ jobs:
ENABLE_SECOND_LEVEL_CACHE: 1
- name: "Upload coverage files"
uses: "actions/upload-artifact@v5"
uses: "actions/upload-artifact@v6"
with:
name: "${{ github.job }}-${{ matrix.mysql-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
path: "coverage*.xml"
@@ -431,12 +460,12 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v5"
uses: "actions/checkout@v6"
with:
fetch-depth: 2
- name: "Download coverage files"
uses: "actions/download-artifact@v6"
uses: "actions/download-artifact@v7"
with:
path: "reports"

View File

@@ -17,4 +17,4 @@ on:
jobs:
documentation:
name: "Documentation"
uses: "doctrine/.github/.github/workflows/documentation.yml@12.1.0"
uses: "doctrine/.github/.github/workflows/documentation.yml@13.1.0"

View File

@@ -36,7 +36,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v5"
uses: "actions/checkout@v6"
with:
fetch-depth: 2

View File

@@ -7,7 +7,7 @@ on:
jobs:
release:
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@12.1.0"
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@13.1.0"
secrets:
GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}

View File

@@ -35,7 +35,7 @@ jobs:
steps:
- name: "Checkout code"
uses: "actions/checkout@v5"
uses: "actions/checkout@v6"
- name: Install PHP
uses: shivammathur/setup-php@v2

View File

@@ -29,6 +29,100 @@ and directly start using native lazy objects.
# Upgrade to 3.6
## Deprecate using string expression for default values in mappings
Using a string expression for default values in field mappings is deprecated.
Use `Doctrine\DBAL\Schema\DefaultExpression` instances instead.
Here is how to address this deprecation when mapping entities using PHP attributes:
```diff
use DateTime;
+use Doctrine\DBAL\Schema\DefaultExpression\CurrentDate;
+use Doctrine\DBAL\Schema\DefaultExpression\CurrentTime;
+use Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
final class TimeEntity
{
#[ORM\Id]
#[ORM\Column]
public int $id;
- #[ORM\Column(options: ['default' => 'CURRENT_TIMESTAMP'], insertable: false, updatable: false)]
+ #[ORM\Column(options: ['default' => new CurrentTimestamp()], insertable: false, updatable: false)]
public DateTime $createdAt;
- #[ORM\Column(options: ['default' => 'CURRENT_TIME'], insertable: false, updatable: false)]
+ #[ORM\Column(options: ['default' => new CurrentTime()], insertable: false, updatable: false)]
public DateTime $createdTime;
- #[ORM\Column(options: ['default' => 'CURRENT_DATE'], insertable: false, updatable: false)]
+ #[ORM\Column(options: ['default' => new CurrentDate()], insertable: false, updatable: false)]
public DateTime $createdDate;
}
```
Here is how to do the same when mapping entities using XML:
```diff
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="Doctrine\Tests\ORM\Functional\XmlTimeEntity">
<id name="id" type="integer" column="id">
<generator strategy="AUTO"/>
</id>
<field name="createdAt" type="datetime" insertable="false" updatable="false">
<options>
- <option name="default">CURRENT_TIMESTAMP</option>
+ <option name="default">
+ <object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp"/>
+ </option>
</options>
</field>
<field name="createdAtImmutable" type="datetime_immutable" insertable="false" updatable="false">
<options>
- <option name="default">CURRENT_TIMESTAMP</option>
+ <option name="default">
+ <object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp"/>
+ </option>
</options>
</field>
<field name="createdTime" type="time" insertable="false" updatable="false">
<options>
- <option name="default">CURRENT_TIME</option>
+ <option name="default">
+ <object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentTime"/>
+ </option>
</options>
</field>
<field name="createdDate" type="date" insertable="false" updatable="false">
<options>
- <option name="default">CURRENT_DATE</option>
+ <option name="default">
+ <object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentDate"/>
+ </option>
</options>
</field>
</entity>
</doctrine-mapping>
```
## Deprecate `FieldMapping::$default`
The `default` property of `Doctrine\ORM\Mapping\FieldMapping` is deprecated and
will be removed in 4.0. Instead, use `FieldMapping::$options['default']`.
## Deprecate specifying `nullable` on columns that end up being used in a primary key
Specifying `nullable` on join columns that are part of a primary key is

View File

@@ -49,9 +49,8 @@
"require-dev": {
"doctrine/coding-standard": "^14.0",
"phpbench/phpbench": "^1.0",
"phpdocumentor/guides-cli": "^1.4",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "2.1.22",
"phpstan/phpstan": "2.1.23",
"phpstan/phpstan-deprecation-rules": "^2",
"phpunit/phpunit": "^10.5.0 || ^11.5",
"psr/log": "^1 || ^2 || ^3",

View File

@@ -668,11 +668,6 @@ and in the Context of a :ref:`#[ManyToMany] <attrref_manytomany>`. If this attri
are missing they will be computed considering the field's name and the current
:doc:`naming strategy <namingstrategy>`.
The ``#[InverseJoinColumn]`` is the same as ``#[JoinColumn]`` and is used in the context
of a ``#[ManyToMany]`` attribute declaration to specifiy the details of the join table's
column information used for the join to the inverse entity. This is only required
on PHP 8.0, where nested attributes are not yet supported.
Optional parameters:
- **name**: Column name that holds the foreign key identifier for

View File

@@ -182,6 +182,37 @@ Here is a complete list of ``Column``s attributes (all optional):
- ``options``: Key-value pairs of options that get passed
to the underlying database platform when generating DDL statements.
Specifying default values
~~~~~~~~~~~~~~~~~~~~~~~~~
While it is possible to specify default values for properties in your
PHP class, Doctrine also allows you to specify default values for
database columns using the ``default`` key in the ``options`` array of
the ``Column`` attribute.
When using XML, you can specify object instances using the ``<object>``
element:
.. code-block:: xml
<field name="createdAt" type="datetime" insertable="false" updatable="false">
<options>
<option name="default">
<object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp"/>
</option>
</options>
</field>
The ``<object>`` element requires a ``class`` attribute specifying the
fully qualified class name to instantiate.
.. configuration-block::
.. literalinclude:: basic-mapping/DefaultValues.php
:language: attribute
.. literalinclude:: basic-mapping/default-values.xml
:language: xml
.. _reference-php-mapping-types:
PHP Types Mapping

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use DateTime;
use Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
#[Entity]
class Message
{
#[Column(options: ['default' => 'Hello World!'])]
private string $text;
#[Column(options: ['default' => new CurrentTimestamp()], insertable: false, updatable: false)]
private DateTime $createdAt;
}

View File

@@ -0,0 +1,16 @@
<doctrine-mapping>
<entity name="Message">
<field name="text">
<options>
<option name="default">Hello World!</option>
</options>
</field>
<field name="createdAt" insertable="false" updatable="false">
<options>
<option name="default">
<object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp"/>
</option>
</options>
</field>
</entity>
</doctrine-mapping>

View File

@@ -18,30 +18,6 @@ In your mapping configuration, the column definition (for example, the
the ``charset`` and ``collation``. The default values are ``utf8`` and
``utf8_unicode_ci``, respectively.
Entity Classes
--------------
How can I add default values to a column?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Doctrine does not support to set the default values in columns through the "DEFAULT" keyword in SQL.
This is not necessary however, you can just use your class properties as default values. These are then used
upon insert:
.. code-block:: php
class User
{
private const STATUS_DISABLED = 0;
private const STATUS_ENABLED = 1;
private string $algorithm = "sha1";
/** @var self::STATUS_* */
private int $status = self::STATUS_DISABLED;
}
.
Mapping
-------

View File

@@ -208,6 +208,22 @@ Example:
// ...
}
.. code-block:: xml
<doctrine-mapping>
<entity name="MyProject\Model\Person" inheritance-type="SINGLE_TABLE">
<discriminator-column name="discr" type="string" />
<discriminator-map>
<discriminator-mapping value="person" class="MyProject\Model\Person"/>
<discriminator-mapping value="employee" class="MyProject\Model\Employee"/>
</discriminator-map>
</entity>
</doctrine-mapping>
<doctrine-mapping>
<entity name="MyProject\Model\Employee">
</entity>
</doctrine-mapping>
In this example, the ``#[DiscriminatorMap]`` specifies that in the
discriminator column, a value of "person" identifies a row as being of type

View File

@@ -555,6 +555,24 @@ using ``addCriteria``:
$qb->addCriteria($criteria);
// then execute your query like normal
Adding hints to a Query
^^^^^^^^^^^^^^^^^^^^^^^
You can also set query hints to a QueryBuilder by using ``setHint``:
.. code-block:: php
<?php
// ...
// $qb instanceof QueryBuilder
$qb->setHint('hintName', 'hintValue');
// then execute your query like normal
The query hint can hold anything the usual query hints can hold
except null. Those hints will be applied to the query when the
query is created.
Low Level API
^^^^^^^^^^^^^

View File

@@ -96,6 +96,10 @@ The following Commands are currently available:
- ``orm:schema-tool:update`` Processes the schema and either
update the database schema of EntityManager Storage Connection or
generate the SQL output.
- ``orm:debug:event-manager`` Lists event listeners for an entity
manager, optionally filtered by event name.
- ``orm:debug:entity-listeners`` Lists entity listeners for a given
entity, optionally filtered by event name.
The following alias is defined:

View File

@@ -155,10 +155,20 @@
</xs:restriction>
</xs:simpleType>
<xs:complexType name="object">
<xs:attribute name="class" type="xs:string" use="required"/>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="option" mixed="true">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="option" type="orm:option"/>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
<xs:choice minOccurs="0" maxOccurs="1">
<xs:element name="object" type="orm:object"/>
<xs:sequence>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="option" type="orm:option"/>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
</xs:sequence>
</xs:choice>
<xs:attribute name="name" type="xs:NMTOKEN" use="required"/>
<xs:anyAttribute namespace="##other"/>

View File

@@ -696,12 +696,6 @@ parameters:
count: 1
path: src/Internal/Hydration/AbstractHydrator.php
-
message: '#^Method Doctrine\\ORM\\Internal\\Hydration\\AbstractHydrator\:\:gatherRowData\(\) should return array\{data\: array\<array\>, newObjects\?\: array\<array\{class\: ReflectionClass, args\: array, obj\: object\}\>, scalars\?\: array\} but returns array\{data\: array, newObjects\: array\<array\{class\: ReflectionClass\<object\>, args\: array, obj\?\: object\}\>, scalars\?\: non\-empty\-array\}\.$#'
identifier: return.type
count: 1
path: src/Internal/Hydration/AbstractHydrator.php
-
message: '#^Method Doctrine\\ORM\\Internal\\Hydration\\AbstractHydrator\:\:getClassMetadata\(\) return type with generic class Doctrine\\ORM\\Mapping\\ClassMetadata does not specify its types\: T$#'
identifier: missingType.generics
@@ -822,6 +816,12 @@ parameters:
count: 1
path: src/Internal/HydrationCompleteHandler.php
-
message: '#^Offset int\|null might not exist on array\<int, object\>\.$#'
identifier: offsetAccess.notFound
count: 1
path: src/Internal/StronglyConnectedComponents.php
-
message: '#^Property Doctrine\\ORM\\Internal\\StronglyConnectedComponents\:\:\$representingNodes \(array\<int, object\>\) does not accept array\<int\|string, object\>\.$#'
identifier: assign.propertyType
@@ -1410,12 +1410,6 @@ parameters:
count: 2
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Parameter \#1 \$asset of static method Doctrine\\ORM\\Mapping\\Driver\\DatabaseDriver\:\:getAssetName\(\) expects Doctrine\\DBAL\\Schema\\AbstractAsset, Doctrine\\DBAL\\Schema\\Column\|false given\.$#'
identifier: argument.type
count: 1
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Parameter \#2 \$columnName of method Doctrine\\ORM\\Mapping\\Driver\\DatabaseDriver\:\:getFieldNameForColumn\(\) expects string, string\|false given\.$#'
identifier: argument.type
@@ -1447,7 +1441,7 @@ parameters:
path: src/Mapping/Driver/XmlDriver.php
-
message: '#^Parameter \#1 \$columnDef of method Doctrine\\ORM\\Mapping\\ClassMetadata\<T of object\>\:\:setDiscriminatorColumn\(\) expects array\{name\: string\|null, fieldName\?\: string\|null, type\?\: string\|null, length\?\: int\|null, columnDefinition\?\: string\|null, enumType\?\: class\-string\<BackedEnum\>\|null, options\?\: array\<string, mixed\>\|null\}\|Doctrine\\ORM\\Mapping\\DiscriminatorColumnMapping\|null, array\{name\: string\|null, type\: string, length\: int, columnDefinition\: string\|null, enumType\: string\|null, options\?\: array\<int\|string, array\<int\|string, mixed\>\|bool\|string\>\} given\.$#'
message: '#^Parameter \#1 \$columnDef of method Doctrine\\ORM\\Mapping\\ClassMetadata\<T of object\>\:\:setDiscriminatorColumn\(\) expects array\{name\: string\|null, fieldName\?\: string\|null, type\?\: string\|null, length\?\: int\|null, columnDefinition\?\: string\|null, enumType\?\: class\-string\<BackedEnum\>\|null, options\?\: array\<string, mixed\>\|null\}\|Doctrine\\ORM\\Mapping\\DiscriminatorColumnMapping\|null, array\{name\: string\|null, type\: string, length\: int, columnDefinition\: string\|null, enumType\: string\|null, options\?\: array\<int\|string, array\<int\|string, mixed\>\|bool\|object\|string\>\} given\.$#'
identifier: argument.type
count: 1
path: src/Mapping/Driver/XmlDriver.php
@@ -1477,7 +1471,7 @@ parameters:
path: src/Mapping/Driver/XmlDriver.php
-
message: '#^Property Doctrine\\ORM\\Mapping\\ClassMetadata\<T of object\>\:\:\$table \(array\{name\: string, schema\?\: string, indexes\?\: array, uniqueConstraints\?\: array, options\?\: array\<string, mixed\>, quoted\?\: bool\}\) does not accept array\{name\: string, schema\?\: string, indexes\?\: array, uniqueConstraints\?\: array, options\: array\<int\|string, array\<int\|string, mixed\>\|bool\|string\>, quoted\?\: bool\}\.$#'
message: '#^Property Doctrine\\ORM\\Mapping\\ClassMetadata\<T of object\>\:\:\$table \(array\{name\: string, schema\?\: string, indexes\?\: array, uniqueConstraints\?\: array, options\?\: array\<string, mixed\>, quoted\?\: bool\}\) does not accept array\{name\: string, schema\?\: string, indexes\?\: array, uniqueConstraints\?\: array, options\: array\<int\|string, array\<int\|string, mixed\>\|bool\|object\|string\>, quoted\?\: bool\}\.$#'
identifier: assign.propertyType
count: 1
path: src/Mapping/Driver/XmlDriver.php
@@ -1536,6 +1530,12 @@ parameters:
count: 1
path: src/Mapping/LegacyReflectionFields.php
-
message: '#^Strict comparison using \!\=\= between array\<string, string\> and null will always evaluate to true\.$#'
identifier: notIdentical.alwaysTrue
count: 1
path: src/Mapping/ManyToManyOwningSideMapping.php
-
message: '#^Method Doctrine\\ORM\\Mapping\\MappedSuperclass\:\:__construct\(\) has parameter \$repositoryClass with generic class Doctrine\\ORM\\EntityRepository but does not specify its types\: T$#'
identifier: missingType.generics
@@ -2874,12 +2874,6 @@ parameters:
count: 1
path: src/Tools/Console/Command/GenerateProxiesCommand.php
-
message: '#^Parameter \#2 \$proxyDir of method Doctrine\\ORM\\Proxy\\ProxyFactory\:\:generateProxyClasses\(\) expects string\|null, string\|false given\.$#'
identifier: argument.type
count: 1
path: src/Tools/Console/Command/GenerateProxiesCommand.php
-
message: '#^Method Doctrine\\ORM\\Tools\\Console\\Command\\MappingDescribeCommand\:\:getClassMetadata\(\) return type with generic class Doctrine\\ORM\\Mapping\\ClassMetadata does not specify its types\: T$#'
identifier: missingType.generics
@@ -3345,6 +3339,12 @@ parameters:
count: 1
path: src/UnitOfWork.php
-
message: '#^Access to an undefined property Doctrine\\ORM\\Mapping\\AssociationMapping\:\:\$joinColumns\.$#'
identifier: property.notFound
count: 1
path: src/UnitOfWork.php
-
message: '#^Access to an undefined property Doctrine\\ORM\\Mapping\\ManyToManyInverseSideMapping\|Doctrine\\ORM\\Mapping\\ManyToManyOwningSideMapping\|Doctrine\\ORM\\Mapping\\ManyToOneAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToManyAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToOneInverseSideMapping\|Doctrine\\ORM\\Mapping\\OneToOneOwningSideMapping\:\:\$inversedBy\.$#'
identifier: property.notFound
@@ -3360,7 +3360,7 @@ parameters:
-
message: '#^Access to an undefined property Doctrine\\ORM\\Mapping\\ManyToManyInverseSideMapping\|Doctrine\\ORM\\Mapping\\ManyToManyOwningSideMapping\|Doctrine\\ORM\\Mapping\\ManyToOneAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToManyAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToOneInverseSideMapping\|Doctrine\\ORM\\Mapping\\OneToOneOwningSideMapping\:\:\$mappedBy\.$#'
identifier: property.notFound
count: 1
count: 3
path: src/UnitOfWork.php
-

View File

@@ -98,6 +98,17 @@ parameters:
identifier: argument.unresolvableType
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Parameter \#1 \$asset of static method Doctrine\\ORM\\Mapping\\Driver\\DatabaseDriver\:\:getAssetName\(\) expects Doctrine\\DBAL\\Schema\\AbstractAsset, Doctrine\\DBAL\\Schema\\Column\|false given\.$#'
identifier: argument.type
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Instantiated class Doctrine\\DBAL\\Schema\\DefaultExpression\\\w+ not found\.$#'
identifier: class.notFound
path: src/Tools/SchemaTool.php
# To be removed in 4.0
-
message: '#Negated boolean expression is always false\.#'
@@ -149,8 +160,3 @@ parameters:
-
message: '~inferType.*never returns~'
path: src/Query/ParameterTypeInferer.php
# Compatibility with Symfony 8
-
message: '#^Call to function method_exists\(\) with ''Symfony\\\\Component\\\\VarExporter\\\\ProxyHelper'' and ''generateLazyGhost'' will always evaluate to true\.$#'
path: src/Proxy/ProxyFactory.php

View File

@@ -54,8 +54,3 @@ parameters:
-
message: '#Expression on left side of \?\? is not nullable.#'
path: src/Mapping/Driver/AttributeDriver.php
# Compatibility with Symfony 8
-
message: '#^Call to function method_exists\(\) with ''Symfony\\\\Component\\\\VarExporter\\\\ProxyHelper'' and ''generateLazyGhost'' will always evaluate to true\.$#'
path: src/Proxy/ProxyFactory.php

View File

@@ -2204,6 +2204,20 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
throw MappingException::invalidDiscriminatorColumnType($this->name, $columnDef['type']);
}
if (isset($columnDef['enumType'])) {
if (! enum_exists($columnDef['enumType'])) {
throw MappingException::nonEnumTypeMapped($this->name, $columnDef['fieldName'], $columnDef['enumType']);
}
if (
defined('Doctrine\DBAL\Types\Types::ENUM')
&& $columnDef['type'] === Types::ENUM
&& ! isset($columnDef['options']['values'])
) {
$columnDef['options']['values'] = array_column($columnDef['enumType']::cases(), 'value');
}
}
$this->discriminatorColumn = DiscriminatorColumnMapping::fromMappingArray($columnDef);
}
}
@@ -2222,6 +2236,8 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
* Used for JOINED and SINGLE_TABLE inheritance mapping strategies.
*
* @param array<int|string, string> $map
*
* @throws MappingException
*/
public function setDiscriminatorMap(array $map): void
{
@@ -2241,6 +2257,16 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
);
}
$values = $this->discriminatorColumn->options['values'] ?? null;
if ($values !== null) {
$diff = array_diff(array_keys($map), $values);
if ($diff !== []) {
throw MappingException::invalidEntriesInDiscriminatorMap(array_values($diff), $this->name, $this->discriminatorColumn->enumType);
}
}
foreach ($map as $value => $className) {
$this->addDiscriminatorMapClass($value, $className);
}
@@ -2454,9 +2480,9 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
if (! isset($mapping['default'])) {
if (in_array($mapping['type'], ['integer', 'bigint', 'smallint'], true)) {
$mapping['default'] = 1;
$mapping['options']['default'] = 1;
} elseif ($mapping['type'] === 'datetime') {
$mapping['default'] = 'CURRENT_TIMESTAMP';
$mapping['options']['default'] = 'CURRENT_TIMESTAMP';
} else {
throw MappingException::unsupportedOptimisticLockingType($this->name, $mapping['fieldName'], $mapping['type']);
}

View File

@@ -16,6 +16,7 @@ use LogicException;
use SimpleXMLElement;
use function assert;
use function class_exists;
use function constant;
use function count;
use function defined;
@@ -656,15 +657,30 @@ class XmlDriver extends FileDriver
* Parses (nested) option elements.
*
* @return mixed[] The options array.
* @phpstan-return array<int|string, array<int|string, mixed|string>|bool|string>
* @phpstan-return array<int|string, array<int|string, mixed|string>|bool|string|object>
*/
private function parseOptions(SimpleXMLElement|null $options): array
{
$array = [];
foreach ($options ?? [] as $option) {
$value = null;
if ($option->count()) {
$value = $this->parseOptions($option->children());
// Check if this option contains an <object> element
$children = $option->children();
$hasObjectElement = false;
foreach ($children as $child) {
if ($child->getName() === 'object') {
$value = $this->parseObjectElement($child);
$hasObjectElement = true;
break;
}
}
if (! $hasObjectElement) {
$value = $this->parseOptions($children);
}
} else {
$value = (string) $option;
}
@@ -684,6 +700,33 @@ class XmlDriver extends FileDriver
return $array;
}
/**
* Parses an <object> element and returns the instantiated object.
*
* @param SimpleXMLElement $objectElement The XML element.
*
* @return object The instantiated object.
*
* @throws MappingException If the object specification is invalid.
* @throws InvalidArgumentException If the class does not exist.
*/
private function parseObjectElement(SimpleXMLElement $objectElement): object
{
$attributes = $objectElement->attributes();
if (! isset($attributes->class)) {
throw MappingException::missingRequiredOption('object', 'class');
}
$className = (string) $attributes->class;
if (! class_exists($className)) {
throw new InvalidArgumentException(sprintf('Class "%s" does not exist', $className));
}
return new $className();
}
/**
* Constructs a joinColumn mapping array based on the information
* found in the given SimpleXMLElement.

View File

@@ -71,7 +71,9 @@ final class FieldMapping implements ArrayAccess
public string|null $declaredField = null;
public array|null $options = null;
public bool|null $version = null;
public string|int|null $default = null;
/** @deprecated Use options with 'default' key instead */
public string|int|null $default = null;
/**
* @param string $type The type name of the mapped field. Can be one of

View File

@@ -329,6 +329,24 @@ class MappingException extends PersistenceMappingException implements ORMExcepti
);
}
/**
* Returns an exception that indicates that discriminator entries used in a discriminator map
* does not exist in the backed enum provided by enumType option.
*
* @param array<int,int|string> $entries The discriminator entries that could not be found.
* @param string $owningClass The class that declares the discriminator map.
* @param string $enumType The enum that entries were checked against.
*/
public static function invalidEntriesInDiscriminatorMap(array $entries, string $owningClass, string $enumType): self
{
return new self(sprintf(
"The entries %s in the discriminator map of class '%s' do not correspond to enum cases of '%s'.",
implode(', ', array_map(static fn ($entry): string => sprintf("'%s'", $entry), $entries)),
$owningClass,
$enumType,
));
}
/**
* Returns an exception that indicates that a class used in a discriminator map does not exist.
* An example would be an outdated (maybe renamed) classname.

View File

@@ -1668,6 +1668,11 @@ class BasicEntityPersister implements EntityPersister
$value = [$value];
}
if ($value === []) {
$selectedColumns[] = '1=0';
continue;
}
$nullKeys = array_keys($value, null, true);
$nonNullValues = array_diff_key($value, array_flip($nullKeys));

View File

@@ -162,6 +162,7 @@ EOPHP;
);
}
// @phpstan-ignore function.impossibleType (This method has been removed in Symfony 8)
if (! method_exists(ProxyHelper::class, 'generateLazyGhost')) {
throw ORMInvalidArgumentException::lazyGhostUnavailable();
}
@@ -469,7 +470,7 @@ EOPHP;
private function generateUseLazyGhostTrait(ClassMetadata $class): string
{
// @phpstan-ignore staticMethod.deprecated (Because we support Symfony < 7.3)
// @phpstan-ignore staticMethod.notFound (This method has been removed in Symfony 8)
$code = ProxyHelper::generateLazyGhost($class->getReflectionClass());
$code = substr($code, 7 + (int) strpos($code, "\n{"));
$code = substr($code, 0, (int) strpos($code, "\n}"));

View File

@@ -117,6 +117,13 @@ class QueryBuilder implements Stringable
*/
private int $boundCounter = 0;
/**
* The hints to set on the query.
*
* @var array<string, string|int|bool|iterable<mixed>|object>
*/
private array $hints = [];
/**
* Initializes a new <tt>QueryBuilder</tt> that uses the given <tt>EntityManager</tt>.
*
@@ -208,6 +215,39 @@ class QueryBuilder implements Stringable
return $this;
}
/** @return array<string, string|int|bool|iterable<mixed>|object> */
public function getHints(): array
{
return $this->hints;
}
/**
* Gets the value of a query hint. If the hint name is not recognized, FALSE is returned.
*
* @return mixed The value of the hint or FALSE, if the hint name is not recognized.
*/
public function getHint(string $name): mixed
{
return $this->hints[$name] ?? false;
}
public function hasHint(string $name): bool
{
return isset($this->hints[$name]);
}
/**
* Adds hints for the query.
*
* @return $this
*/
public function setHint(string $name, mixed $value): static
{
$this->hints[$name] = $value;
return $this;
}
/** @phpstan-return Cache::MODE_*|null */
public function getCacheMode(): int|null
{
@@ -288,6 +328,10 @@ class QueryBuilder implements Stringable
$query->setCacheRegion($this->cacheRegion);
}
foreach ($this->hints as $name => $value) {
$query->setHint($name, $value);
}
return $query;
}

View File

@@ -18,11 +18,12 @@ trait ApplicationCompatibility
{
private static function addCommandToApplication(Application $application, Command $command): Command|null
{
// @phpstan-ignore function.alreadyNarrowedType (This method did not exist before Symfony 7.4)
if (method_exists(Application::class, 'addCommand')) {
// @phpstan-ignore method.notFound (This method will be added in Symfony 7.4)
return $application->addCommand($command);
}
// @phpstan-ignore method.notFound
return $application->add($command);
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command\Debug;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Console\Command\Command;
use function assert;
/** @internal */
abstract class AbstractCommand extends Command
{
public function __construct(private readonly ManagerRegistry $managerRegistry)
{
parent::__construct();
}
final protected function getEntityManager(string $name): EntityManagerInterface
{
$manager = $this->getManagerRegistry()->getManager($name);
assert($manager instanceof EntityManagerInterface);
return $manager;
}
final protected function getManagerRegistry(): ManagerRegistry
{
return $this->managerRegistry;
}
}

View File

@@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command\Debug;
use Doctrine\ORM\Mapping\ClassMetadata;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function array_keys;
use function array_merge;
use function array_unique;
use function array_values;
use function assert;
use function class_exists;
use function ksort;
use function ltrim;
use function sort;
use function sprintf;
final class DebugEntityListenersDoctrineCommand extends AbstractCommand
{
protected function configure(): void
{
$this
->setName('orm:debug:entity-listeners')
->setDescription('Lists entity listeners for a given entity')
->addArgument('entity', InputArgument::OPTIONAL, 'The fully-qualified entity class name')
->addArgument('event', InputArgument::OPTIONAL, 'The event name to filter by (e.g. postPersist)')
->setHelp(<<<'EOT'
The <info>%command.name%</info> command lists all entity listeners for a given entity:
<info>php %command.full_name% 'App\Entity\User'</info>
To show only listeners for a specific event, pass the event name:
<info>php %command.full_name% 'App\Entity\User' postPersist</info>
EOT);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
/** @var class-string|null $entityName */
$entityName = $input->getArgument('entity');
if ($entityName === null) {
$choices = $this->listAllEntities();
if ($choices === []) {
$io->error('No entities are configured.');
return self::FAILURE;
}
/** @var class-string $entityName */
$entityName = $io->choice('Which entity do you want to list listeners for?', $choices);
}
$entityName = ltrim($entityName, '\\');
$entityManager = $this->getManagerRegistry()->getManagerForClass($entityName);
if ($entityManager === null) {
$io->error(sprintf('No entity manager found for class "%s".', $entityName));
return self::FAILURE;
}
$classMetadata = $entityManager->getClassMetadata($entityName);
assert($classMetadata instanceof ClassMetadata);
$eventName = $input->getArgument('event');
if ($eventName === null) {
$allListeners = $classMetadata->entityListeners;
if (! $allListeners) {
$io->info(sprintf('No listeners are configured for the "%s" entity.', $entityName));
return self::SUCCESS;
}
ksort($allListeners);
} else {
if (! isset($classMetadata->entityListeners[$eventName])) {
$io->info(sprintf('No listeners are configured for the "%s" event.', $eventName));
return self::SUCCESS;
}
$allListeners = [$eventName => $classMetadata->entityListeners[$eventName]];
}
$io->title(sprintf('Entity listeners for <info>%s</info>', $entityName));
$rows = [];
foreach ($allListeners as $event => $listeners) {
if ($rows) {
$rows[] = new TableSeparator();
}
foreach ($listeners as $order => $listener) {
$rows[] = [$order === 0 ? $event : '', sprintf('#%d', ++$order), sprintf('%s::%s()', $listener['class'], $listener['method'])];
}
}
$io->table(['Event', 'Order', 'Listener'], $rows);
return self::SUCCESS;
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('entity')) {
$suggestions->suggestValues($this->listAllEntities());
return;
}
if ($input->mustSuggestArgumentValuesFor('event')) {
$entityName = ltrim($input->getArgument('entity'), '\\');
if (! class_exists($entityName)) {
return;
}
$entityManager = $this->getManagerRegistry()->getManagerForClass($entityName);
if ($entityManager === null) {
return;
}
$classMetadata = $entityManager->getClassMetadata($entityName);
assert($classMetadata instanceof ClassMetadata);
$suggestions->suggestValues(array_keys($classMetadata->entityListeners));
return;
}
}
/** @return list<class-string> */
private function listAllEntities(): array
{
$entities = [];
foreach (array_keys($this->getManagerRegistry()->getManagerNames()) as $managerName) {
$entities[] = $this->getEntityManager($managerName)->getConfiguration()->getMetadataDriverImpl()->getAllClassNames();
}
$entities = array_values(array_unique(array_merge(...$entities)));
sort($entities);
return $entities;
}
}

View File

@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command\Debug;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function array_keys;
use function array_values;
use function ksort;
use function method_exists;
use function sprintf;
final class DebugEventManagerDoctrineCommand extends AbstractCommand
{
protected function configure(): void
{
$this
->setName('orm:debug:event-manager')
->setDescription('Lists event listeners for an entity manager')
->addArgument('event', InputArgument::OPTIONAL, 'The event name to filter by (e.g. postPersist)')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'The entity manager to use for this command')
->setHelp(<<<'EOT'
The <info>%command.name%</info> command lists all event listeners for the default entity manager:
<info>php %command.full_name%</info>
You can also specify an entity manager:
<info>php %command.full_name% --em=default</info>
To show only listeners for a specific event, pass the event name as an argument:
<info>php %command.full_name% postPersist</info>
EOT);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$entityManagerName = $input->getOption('em') ?: $this->getManagerRegistry()->getDefaultManagerName();
$eventManager = $this->getEntityManager($entityManagerName)->getEventManager();
$eventName = $input->getArgument('event');
if ($eventName === null) {
$allListeners = $eventManager->getAllListeners();
if (! $allListeners) {
$io->info(sprintf('No listeners are configured for the "%s" entity manager.', $entityManagerName));
return self::SUCCESS;
}
ksort($allListeners);
} else {
$listeners = $eventManager->hasListeners($eventName) ? $eventManager->getListeners($eventName) : [];
if (! $listeners) {
$io->info(sprintf('No listeners are configured for the "%s" event.', $eventName));
return self::SUCCESS;
}
$allListeners = [$eventName => $listeners];
}
$io->title(sprintf('Event listeners for <info>%s</info> entity manager', $entityManagerName));
$rows = [];
foreach ($allListeners as $event => $listeners) {
if ($rows) {
$rows[] = new TableSeparator();
}
foreach (array_values($listeners) as $order => $listener) {
$method = method_exists($listener, '__invoke') ? '__invoke' : $event;
$rows[] = [$order === 0 ? $event : '', sprintf('#%d', ++$order), sprintf('%s::%s()', $listener::class, $method)];
}
}
$io->table(['Event', 'Order', 'Listener'], $rows);
return self::SUCCESS;
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('event')) {
$entityManagerName = $input->getOption('em') ?: $this->getManagerRegistry()->getDefaultManagerName();
$eventManager = $this->getEntityManager($entityManagerName)->getEventManager();
$suggestions->suggestValues(array_keys($eventManager->getAllListeners()));
return;
}
if ($input->mustSuggestOptionValuesFor('em')) {
$suggestions->suggestValues(array_keys($this->getManagerRegistry()->getManagerNames()));
return;
}
}
}

View File

@@ -23,6 +23,7 @@ use Doctrine\DBAL\Schema\PrimaryKeyConstraint;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Types\Types;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\Mapping\ClassMetadata;
@@ -484,7 +485,9 @@ class SchemaTool
$options['scale'] = $mapping->scale;
}
/** @phpstan-ignore property.deprecated */
if (isset($mapping->default)) {
/** @phpstan-ignore property.deprecated */
$options['default'] = $mapping->default;
}
@@ -505,7 +508,16 @@ class SchemaTool
], true)
&& $options['default'] === $this->platform->getCurrentTimestampSQL()
) {
/** @phpstan-ignore class.notFound (if DefaultExpression exists, CurrentTimestamp exists as well) */
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/12252',
<<<'DEPRECATION'
Using "%s" as a default value for datetime fields is deprecated and
will not be supported in Doctrine ORM 4.0.
Pass a `Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp` instance instead.
DEPRECATION,
$this->platform->getCurrentTimestampSQL(),
);
$options['default'] = new CurrentTimestamp();
}
@@ -513,7 +525,16 @@ class SchemaTool
in_array($mapping->type, [Types::TIME_MUTABLE, Types::TIME_IMMUTABLE], true)
&& $options['default'] === $this->platform->getCurrentTimeSQL()
) {
/** @phpstan-ignore class.notFound (if DefaultExpression exists, CurrentTime exists as well) */
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/12252',
<<<'DEPRECATION'
Using "%s" as a default value for time fields is deprecated and
will not be supported in Doctrine ORM 4.0.
Pass a `Doctrine\DBAL\Schema\DefaultExpression\CurrentTime` instance instead.
DEPRECATION,
$this->platform->getCurrentTimeSQL(),
);
$options['default'] = new CurrentTime();
}
@@ -521,7 +542,16 @@ class SchemaTool
in_array($mapping->type, [Types::DATE_MUTABLE, Types::DATE_IMMUTABLE], true)
&& $options['default'] === $this->platform->getCurrentDateSQL()
) {
/** @phpstan-ignore class.notFound (if DefaultExpression exists, CurrentDate exists as well) */
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/12252',
<<<'DEPRECATION'
Using "%s" as a default value for date fields is deprecated and
will not be supported in Doctrine ORM 4.0.
Pass a `Doctrine\DBAL\Schema\DefaultExpression\CurrentDate` instance instead.
DEPRECATION,
$this->platform->getCurrentDateSQL(),
);
$options['default'] = new CurrentDate();
}
}
@@ -1046,7 +1076,7 @@ class SchemaTool
{
return $asset instanceof NamedObject
? $asset->getObjectName()->toString()
// DBAL < 4.4
// @phpstan-ignore method.deprecated (DBAL < 4.4)
: $asset->getName();
}
}

View File

@@ -62,6 +62,7 @@ use function array_map;
use function array_sum;
use function array_values;
use function assert;
use function count;
use function current;
use function get_debug_type;
use function implode;
@@ -2594,8 +2595,14 @@ class UnitOfWork implements PropertyChangedListener
$reflField->setValue($entity, $pColl);
if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER) {
$isIteration = isset($hints[Query::HINT_INTERNAL_ITERATION]) && $hints[Query::HINT_INTERNAL_ITERATION];
if (! $isIteration && $assoc->isOneToMany() && ! $targetClass->isIdentifierComposite && ! $assoc->isIndexed()) {
if (
$assoc->isOneToMany()
// is iteration
&& ! (isset($hints[Query::HINT_INTERNAL_ITERATION]) && $hints[Query::HINT_INTERNAL_ITERATION])
// is foreign key composite
&& ! ($targetClass->hasAssociation($assoc->mappedBy) && count($targetClass->getAssociationMapping($assoc->mappedBy)->joinColumns) > 1)
&& ! $assoc->isIndexed()
) {
$this->scheduleCollectionForBatchLoading($pColl, $class);
} else {
$this->loadCollection($pColl);

View File

@@ -24,11 +24,16 @@ class RootEntity
#[ORM\OneToMany(mappedBy: 'root', targetEntity: SecondLevel::class, fetch: 'EAGER')]
private Collection $secondLevel;
/** @var Collection<int, SecondLevelWithoutCompositePrimaryKey> */
#[ORM\OneToMany(mappedBy: 'root', targetEntity: SecondLevelWithoutCompositePrimaryKey::class, fetch: 'EAGER')]
private Collection $anotherSecondLevel;
public function __construct(int $id, string $other)
{
$this->otherKey = $other;
$this->secondLevel = new ArrayCollection();
$this->id = $id;
$this->otherKey = $other;
$this->secondLevel = new ArrayCollection();
$this->anotherSecondLevel = new ArrayCollection();
$this->id = $id;
}
public function getId(): int|null

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\EagerFetchedCompositeOneToMany;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class SecondLevelWithoutCompositePrimaryKey
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer', nullable: false)]
private int|null $id;
#[ORM\ManyToOne(targetEntity: RootEntity::class, inversedBy: 'anotherSecondLevel')]
#[ORM\JoinColumn(name: 'root_id', referencedColumnName: 'id')]
#[ORM\JoinColumn(name: 'root_other_key', referencedColumnName: 'other_key')]
private RootEntity $root;
public function __construct(RootEntity $upper)
{
$this->root = $upper;
}
public function getId(): int|null
{
return $this->id;
}
}

View File

@@ -6,38 +6,187 @@ namespace Doctrine\Tests\ORM\Functional;
use DateTime;
use DateTimeImmutable;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Schema\DefaultExpression;
use Doctrine\DBAL\Schema\DefaultExpression\CurrentDate;
use Doctrine\DBAL\Schema\DefaultExpression\CurrentTime;
use Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp;
use Doctrine\DBAL\Types\Types;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
use PHPUnit\Framework\Attributes\RequiresMethod;
use function interface_exists;
class DefaultTimeExpressionTest extends OrmFunctionalTestCase
{
use VerifyDeprecations;
public function testUsingTimeRelatedDefaultExpressionCausesNoDbalDeprecation(): void
#[IgnoreDeprecations]
#[RequiresMethod(DefaultExpression::class, 'toSQL')]
public function testUsingTimeRelatedDefaultExpressionCausesAnOrmDeprecationAndNoDbalDeprecation(): void
{
$platform = $this->_em->getConnection()->getDatabasePlatform();
if (
$platform->getCurrentTimestampSQL() !== 'CURRENT_TIMESTAMP'
|| $platform->getCurrentTimeSQL() !== 'CURRENT_TIME'
|| $platform->getCurrentDateSQL() !== 'CURRENT_DATE'
) {
$this->markTestSkipped(
'This test requires platforms to support exactly CURRENT_TIMESTAMP, CURRENT_TIME and CURRENT_DATE.',
);
}
if ($platform instanceof AbstractMySQLPlatform) {
$this->markTestSkipped(
'MySQL platform does not support CURRENT_TIME or CURRENT_DATE as default expression.',
);
}
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/12252');
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/dbal/pull/7195');
$this->createSchemaForModels(LegacyTimeEntity::class);
$this->_em->persist($entity = new LegacyTimeEntity());
$this->_em->flush();
$this->_em->find(LegacyTimeEntity::class, $entity->id);
}
public function testNoDeprecationsAreTrownWhenTheyCannotBeAddressed(): void
{
if (interface_exists(DefaultExpression::class)) {
$this->markTestSkipped(
'This test requires Doctrine DBAL 4.3 or lower.',
);
}
$platform = $this->_em->getConnection()->getDatabasePlatform();
if (
$platform->getCurrentTimestampSQL() !== 'CURRENT_TIMESTAMP'
|| $platform->getCurrentTimeSQL() !== 'CURRENT_TIME'
|| $platform->getCurrentDateSQL() !== 'CURRENT_DATE'
) {
$this->markTestSkipped(
'This test requires platforms to support exactly CURRENT_TIMESTAMP, CURRENT_TIME and CURRENT_DATE.',
);
}
if ($platform instanceof AbstractMySQLPlatform) {
$this->markTestSkipped(
'MySQL platform does not support CURRENT_TIME or CURRENT_DATE as default expression.',
);
}
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/12252');
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/dbal/pull/7195');
$this->createSchemaForModels(LegacyTimeEntity::class);
$this->_em->persist($entity = new LegacyTimeEntity());
$this->_em->flush();
$this->_em->find(LegacyTimeEntity::class, $entity->id);
}
#[RequiresMethod(DefaultExpression::class, 'toSQL')]
public function testUsingDefaultExpressionInstancesCausesNoDeprecation(): void
{
$platform = $this->_em->getConnection()->getDatabasePlatform();
if ($platform instanceof AbstractMySQLPlatform) {
$this->markTestSkipped('MySQL platform does not support CURRENT_TIME or CURRENT_DATE as default expression.');
}
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/12252');
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/dbal/pull/7195');
$this->createSchemaForModels(TimeEntity::class);
$this->_em->persist($entity = new TimeEntity());
$this->_em->flush();
$this->_em->find(TimeEntity::class, $entity->id);
}
}
#[ORM\Entity]
class LegacyTimeEntity
{
#[ORM\Id]
#[ORM\Column]
#[ORM\GeneratedValue]
public int $id;
#[ORM\Column(
type: Types::DATETIME_MUTABLE,
options: ['default' => 'CURRENT_TIMESTAMP'],
insertable: false,
updatable: false,
)]
public DateTime $createdAt;
#[ORM\Column(
type: Types::DATETIME_IMMUTABLE,
options: ['default' => 'CURRENT_TIMESTAMP'],
insertable: false,
updatable: false,
)]
public DateTimeImmutable $createdAtImmutable;
#[ORM\Column(
type: Types::TIME_MUTABLE,
options: ['default' => 'CURRENT_TIME'],
insertable: false,
updatable: false,
)]
public DateTime $createdTime;
#[ORM\Column(
type: Types::DATE_MUTABLE,
options: ['default' => 'CURRENT_DATE'],
insertable: false,
updatable: false,
)]
public DateTime $createdDate;
}
#[ORM\Entity]
class TimeEntity
{
#[ORM\Id]
#[ORM\Column]
#[ORM\GeneratedValue]
public int $id;
#[ORM\Column(options: ['default' => 'CURRENT_TIMESTAMP'])]
#[ORM\Column(
type: Types::DATETIME_MUTABLE,
options: ['default' => new CurrentTimestamp()],
insertable: false,
updatable: false,
)]
public DateTime $createdAt;
#[ORM\Column(options: ['default' => 'CURRENT_TIMESTAMP'])]
#[ORM\Column(
type: Types::DATETIME_IMMUTABLE,
options: ['default' => new CurrentTimestamp()],
insertable: false,
updatable: false,
)]
public DateTimeImmutable $createdAtImmutable;
#[ORM\Column(options: ['default' => 'CURRENT_TIME'])]
#[ORM\Column(
type: Types::TIME_MUTABLE,
options: ['default' => new CurrentTime()],
insertable: false,
updatable: false,
)]
public DateTime $createdTime;
#[ORM\Column(options: ['default' => 'CURRENT_DATE'])]
#[ORM\Column(
type: Types::DATE_MUTABLE,
options: ['default' => new CurrentDate()],
insertable: false,
updatable: false,
)]
public DateTime $createdDate;
}

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use DateTime;
use DateTimeImmutable;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Schema\DefaultExpression;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\Mapping\Driver\XmlDriver;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
use PHPUnit\Framework\Attributes\RequiresMethod;
class DefaultTimeExpressionXmlTest extends OrmFunctionalTestCase
{
use VerifyDeprecations;
#[IgnoreDeprecations]
#[RequiresMethod(DefaultExpression::class, 'toSQL')]
public function testUsingTimeRelatedDefaultExpressionCausesAnOrmDeprecationAndNoDbalDeprecation(): void
{
$platform = $this->_em->getConnection()->getDatabasePlatform();
if (
$platform->getCurrentTimestampSQL() !== 'CURRENT_TIMESTAMP'
|| $platform->getCurrentTimeSQL() !== 'CURRENT_TIME'
|| $platform->getCurrentDateSQL() !== 'CURRENT_DATE'
) {
$this->markTestSkipped('Platform does not use standard SQL for current time expressions.');
}
if ($platform instanceof AbstractMySQLPlatform) {
$this->markTestSkipped(
'MySQL platform does not support CURRENT_TIME or CURRENT_DATE as default expression.',
);
}
$this->_em = $this->getEntityManager(
mappingDriver: new XmlDriver(__DIR__ . '/../Mapping/xml/'),
);
$this->_schemaTool = new SchemaTool($this->_em);
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/12252');
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/dbal/pull/7195');
$this->createSchemaForModels(XmlLegacyTimeEntity::class);
$this->_em->persist($entity = new XmlLegacyTimeEntity());
$this->_em->flush();
$this->_em->find(XmlLegacyTimeEntity::class, $entity->id);
}
#[RequiresMethod(DefaultExpression::class, 'toSQL')]
public function testUsingDefaultExpressionInstancesCausesNoDeprecationXmlDriver(): void
{
$platform = $this->_em->getConnection()->getDatabasePlatform();
if ($platform instanceof AbstractMySQLPlatform) {
$this->markTestSkipped(
'MySQL platform does not support CURRENT_TIME or CURRENT_DATE as default expression.',
);
}
$this->_em = $this->getEntityManager(
mappingDriver: new XmlDriver(__DIR__ . '/../Mapping/xml/'),
);
$this->_schemaTool = new SchemaTool($this->_em);
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/12252');
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/dbal/pull/7195');
$this->createSchemaForModels(XmlTimeEntity::class);
$this->_em->persist($entity = new XmlTimeEntity());
$this->_em->flush();
$this->_em->clear();
$this->_em->find(XmlTimeEntity::class, $entity->id);
}
}
class XmlLegacyTimeEntity
{
public int $id;
public DateTime $createdAt;
public DateTimeImmutable $createdAtImmutable;
public DateTime $createdTime;
public DateTime $createdDate;
}
class XmlTimeEntity
{
public int $id;
public DateTime $createdAt;
public DateTimeImmutable $createdAtImmutable;
public DateTime $createdTime;
public DateTime $createdDate;
}

View File

@@ -6,6 +6,7 @@ namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Tests\Models\EagerFetchedCompositeOneToMany\RootEntity;
use Doctrine\Tests\Models\EagerFetchedCompositeOneToMany\SecondLevel;
use Doctrine\Tests\Models\EagerFetchedCompositeOneToMany\SecondLevelWithoutCompositePrimaryKey;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;
@@ -14,7 +15,7 @@ final class EagerFetchOneToManyWithCompositeKeyTest extends OrmFunctionalTestCas
#[Group('GH11154')]
public function testItDoesNotThrowAnExceptionWhenTriggeringALoad(): void
{
$this->setUpEntitySchema([RootEntity::class, SecondLevel::class]);
$this->setUpEntitySchema([RootEntity::class, SecondLevel::class, SecondLevelWithoutCompositePrimaryKey::class]);
$a1 = new RootEntity(1, 'A');

View File

@@ -17,7 +17,6 @@ use Generator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
use ReflectionMethod;
use Symfony\Component\VarExporter\Instantiator;
use Symfony\Component\VarExporter\VarExporter;
use function file_get_contents;
@@ -81,11 +80,6 @@ class ParserResultSerializationTest extends OrmFunctionalTestCase
},
];
$instantiatorMethod = new ReflectionMethod(Instantiator::class, 'instantiate');
if ($instantiatorMethod->getReturnType() === null) {
self::markTestSkipped('symfony/var-exporter 5.4+ is required.');
}
yield 'symfony/var-exporter' => [
static function (ParserResult $parserResult): ParserResult {
return eval('return ' . VarExporter::export($parserResult) . ';');

View File

@@ -474,7 +474,9 @@ class QueryTest extends OrmFunctionalTestCase
$this->_em->flush();
$this->_em->clear();
$query = $this->_em->createQuery('select a, u, a.topic, a.text from ' . CmsArticle::class . ' a, ' . CmsUser::class . ' u WHERE a.user = u ');
$query = $this->_em->createQuery(
'select a, u, a.topic, a.text from ' . CmsArticle::class . ' a, ' . CmsUser::class . ' u WHERE a.user = u order by a.id asc',
);
$result = $query->toIterable();
$it = iterator_to_array($result);
@@ -517,7 +519,9 @@ class QueryTest extends OrmFunctionalTestCase
$this->_em->flush();
$this->_em->clear();
$query = $this->_em->createQuery('select a.topic, a.text from ' . CmsArticle::class . ' a ');
$query = $this->_em->createQuery(
'select a.topic, a.text from ' . CmsArticle::class . ' a order by a.id asc',
);
$result = $query->toIterable();
$it = iterator_to_array($result);
@@ -545,7 +549,9 @@ class QueryTest extends OrmFunctionalTestCase
$this->_em->flush();
$this->_em->clear();
$query = $this->_em->createQuery('select a from Doctrine\Tests\Models\CMS\CmsArticle a');
$query = $this->_em->createQuery(
'select a from Doctrine\Tests\Models\CMS\CmsArticle a order by a.id asc',
);
$articles = $query->toIterable();
$iteratedCount = 0;

View File

@@ -142,6 +142,8 @@ class MySqlSchemaToolTest extends OrmFunctionalTestCase
self::equalTo('CREATE TABLE boolean_model (id INT AUTO_INCREMENT NOT NULL, booleanField TINYINT(1) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'),
// DBAL 4.3 (see https://github.com/doctrine/dbal/pull/6864)
self::equalTo('CREATE TABLE boolean_model (id INT AUTO_INCREMENT NOT NULL, booleanField TINYINT(1) NOT NULL, PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'),
// DBAL 4.4 (see https://github.com/doctrine/dbal/pull/7221)
self::equalTo('CREATE TABLE boolean_model (id INT AUTO_INCREMENT NOT NULL, booleanField TINYINT NOT NULL, PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'),
));
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH12254Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
GH12254EntityA::class,
]);
$this->_em->persist(new GH12254EntityA());
$this->_em->flush();
$this->_em->clear();
}
public function testFindByEmptyArrayShouldReturnEmptyArray(): void
{
// pretend we are starting afresh
$this->_em = $this->getEntityManager();
$result = $this->_em->getRepository(GH12254EntityA::class)->findBy(['id' => []]);
$this->assertEmpty($result);
}
public function testFindByInNullableField(): void
{
$this->_em = $this->getEntityManager();
$result = $this->_em->getRepository(GH12254EntityA::class)->findBy(['name' => []]);
$this->assertEmpty($result);
}
}
#[Entity]
class GH12254EntityA
{
#[Column(type: 'integer')]
#[Id]
#[GeneratedValue(strategy: 'AUTO')]
public int $id;
#[Column(type: 'string', nullable: true)]
public string|null $name = null;
}

View File

@@ -247,7 +247,7 @@ class ClassMetadataBuilderTest extends OrmTestCase
FieldMapping::fromMappingArray([
'columnDefinition' => 'foobar',
'columnName' => 'username',
'default' => 1,
'options' => ['default' => 1],
'fieldName' => 'name',
'length' => 124,
'type' => 'integer',

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="Doctrine\Tests\ORM\Functional\XmlLegacyTimeEntity">
<id name="id" type="integer" column="id">
<generator strategy="AUTO"/>
</id>
<field name="createdAt" type="datetime" insertable="false" updatable="false">
<options>
<option name="default">CURRENT_TIMESTAMP</option>
</options>
</field>
<field name="createdAtImmutable" type="datetime_immutable" insertable="false" updatable="false">
<options>
<option name="default">CURRENT_TIMESTAMP</option>
</options>
</field>
<field name="createdTime" type="time" insertable="false" updatable="false">
<options>
<option name="default">CURRENT_TIME</option>
</options>
</field>
<field name="createdDate" type="date" insertable="false" updatable="false">
<options>
<option name="default">CURRENT_DATE</option>
</options>
</field>
</entity>
</doctrine-mapping>

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="Doctrine\Tests\ORM\Functional\XmlTimeEntity">
<id name="id" type="integer" column="id">
<generator strategy="AUTO"/>
</id>
<field name="createdAt" type="datetime" insertable="false" updatable="false">
<options>
<option name="default">
<object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp"/>
</option>
</options>
</field>
<field name="createdAtImmutable" type="datetime_immutable" insertable="false" updatable="false">
<options>
<option name="default">
<object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp"/>
</option>
</options>
</field>
<field name="createdTime" type="time" insertable="false" updatable="false">
<options>
<option name="default">
<object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentTime"/>
</option>
</options>
</field>
<field name="createdDate" type="date" insertable="false" updatable="false">
<options>
<option name="default">
<object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentDate"/>
</option>
</options>
</field>
</entity>
</doctrine-mapping>

View File

@@ -19,6 +19,7 @@ use ReflectionProperty;
use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\NullAdapter;
use function sys_get_temp_dir;
@@ -68,7 +69,7 @@ class ORMSetupTest extends TestCase
{
$this->expectNotToPerformAssertions();
ORMSetup::createXMLMetadataConfig(paths: [], isXsdValidationEnabled: false);
ORMSetup::createXMLMetadataConfig(paths: [], cache: new NullAdapter(), isXsdValidationEnabled: false);
}
#[RequiresPhpExtension('apcu')]

View File

@@ -147,6 +147,7 @@ class BasicEntityPersisterTypeValueSqlTest extends OrmTestCase
}
#[Group('DDC-3056')]
#[Group('GH12254')]
public function testSelectConditionStatementWithMultipleValuesContainingNull(): void
{
self::assertEquals(
@@ -168,6 +169,11 @@ class BasicEntityPersisterTypeValueSqlTest extends OrmTestCase
'(t0.id IN (?, ?) OR t0.id IS NULL)',
$this->persister->getSelectConditionStatementSQL('id', [123, null, 234]),
);
self::assertEquals(
'1=0',
$this->persister->getSelectConditionStatementSQL('id', []),
);
}
public function testCountCondition(): void

View File

@@ -24,6 +24,7 @@ use Doctrine\Tests\Models\CMS\CmsGroup;
use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\Tests\OrmTestCase;
use InvalidArgumentException;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
use PHPUnit\Framework\TestCase;
@@ -1375,4 +1376,99 @@ class QueryBuilderTest extends OrmTestCase
$qb->test();
}
#[DataProvider('provideHint')]
public function testSingleHint(mixed $expected): void
{
$qb = $this->entityManager->createQueryBuilder()
->delete(CmsUser::class, 'u')
->select('u.id', 'u.username')
->setHint('foo', $expected);
$this->assertValidQueryBuilder($qb, 'SELECT u.id, u.username FROM Doctrine\Tests\Models\CMS\CmsUser u');
$query = $qb->getQuery();
self::assertTrue($query->hasHint('foo'));
self::assertEquals($expected, $query->getHint('foo'));
}
public static function provideHint(): array
{
return [
['bar'],
[new CmsUser()],
[['a','b','c']],
[1],
[true],
];
}
public function testMultipleHints(): void
{
$object = new CmsUser();
$qb = $this->entityManager->createQueryBuilder()
->delete(CmsUser::class, 'u')
->select('u.id', 'u.username')
->setHint('string', 'bar')
->setHint('object', $object)
->setHint('array', ['a', 'b', 'c'])
->setHint('int', 5)
->setHint('bool', true);
$this->assertValidQueryBuilder($qb, 'SELECT u.id, u.username FROM Doctrine\Tests\Models\CMS\CmsUser u');
$query = $qb->getQuery();
self::assertTrue($query->hasHint('string'));
self::assertTrue($query->hasHint('object'));
self::assertTrue($query->hasHint('array'));
self::assertTrue($query->hasHint('int'));
self::assertTrue($query->hasHint('bool'));
self::assertEquals('bar', $query->getHint('string'));
self::assertInstanceOf(CmsUser::class, $query->getHint('object'));
self::assertEquals(['a', 'b', 'c'], $query->getHint('array'));
self::assertEquals(5, $query->getHint('int'));
self::assertTrue($query->getHint('bool'));
}
public function testHasHint(): void
{
$qb = $this->entityManager->createQueryBuilder()
->delete(CmsUser::class, 'u')
->select('u.id', 'u.username')
->setHint('foo', 'bar');
$this->assertValidQueryBuilder($qb, 'SELECT u.id, u.username FROM Doctrine\Tests\Models\CMS\CmsUser u');
self::assertTrue($qb->hasHint('foo'));
}
public function testGetHint(): void
{
$qb = $this->entityManager->createQueryBuilder()
->delete(CmsUser::class, 'u')
->select('u.id', 'u.username')
->setHint('foo', 'bar');
$this->assertValidQueryBuilder($qb, 'SELECT u.id, u.username FROM Doctrine\Tests\Models\CMS\CmsUser u');
self::assertEquals('bar', $qb->getHint('foo'));
}
public function testGetHints(): void
{
$object = new CmsUser();
$qb = $this->entityManager->createQueryBuilder()
->delete(CmsUser::class, 'u')
->select('u.id', 'u.username')
->setHint('string', 'bar')
->setHint('object', $object)
->setHint('array', ['a', 'b', 'c'])
->setHint('int', 5)
->setHint('bool', true);
$this->assertValidQueryBuilder($qb, 'SELECT u.id, u.username FROM Doctrine\Tests\Models\CMS\CmsUser u');
self::assertCount(5, $qb->getHints('foo'));
}
}

View File

@@ -0,0 +1,152 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Tools\Console\Command\Debug;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Tools\Console\ApplicationCompatibility;
use Doctrine\ORM\Tools\Console\Command\Debug\DebugEntityListenersDoctrineCommand;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\BarListener;
use Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\BazListener;
use Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\FooListener;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Tester\CommandTester;
use function array_map;
class DebugEntityListenersDoctrineCommandTest extends TestCase
{
use ApplicationCompatibility;
private DebugEntityListenersDoctrineCommand $command;
protected function setUp(): void
{
parent::setUp();
$application = new Application();
$this->command = new DebugEntityListenersDoctrineCommand($this->getMockManagerRegistry());
self::addCommandToApplication($application, $this->command);
}
public function testExecute(): void
{
$commandTester = new CommandTester($this->command);
$commandTester->execute(
['command' => $this->command->getName(), 'entity' => self::class],
);
self::assertSame(<<<'TXT'
Entity listeners for Doctrine\Tests\ORM\Tools\Console\Command\Debug\DebugEntityListenersDoctrineCommandTest
===========================================================================================================
------------- ------- ------------------------------------------------------------------------------------
Event Order Listener
------------- ------- ------------------------------------------------------------------------------------
postPersist #1 Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\BazListener::postPersist()
------------- ------- ------------------------------------------------------------------------------------
preUpdate #1 Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\FooListener::preUpdate()
#2 Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\BarListener::__invoke()
------------- ------- ------------------------------------------------------------------------------------
TXT
, $commandTester->getDisplay(true));
}
public function testExecuteWithEvent(): void
{
$commandTester = new CommandTester($this->command);
$commandTester->execute(
['command' => $this->command->getName(), 'entity' => self::class, 'event' => 'postPersist'],
);
self::assertSame(<<<'TXT'
Entity listeners for Doctrine\Tests\ORM\Tools\Console\Command\Debug\DebugEntityListenersDoctrineCommandTest
===========================================================================================================
------------- ------- ------------------------------------------------------------------------------------
Event Order Listener
------------- ------- ------------------------------------------------------------------------------------
postPersist #1 Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\BazListener::postPersist()
------------- ------- ------------------------------------------------------------------------------------
TXT
, $commandTester->getDisplay(true));
}
public function testExecuteWithMissingEvent(): void
{
$commandTester = new CommandTester($this->command);
$commandTester->execute(
['command' => $this->command->getName(), 'entity' => self::class, 'event' => 'preRemove'],
);
self::assertSame(<<<'TXT'
[INFO] No listeners are configured for the "preRemove" event.
TXT
, $commandTester->getDisplay(true));
}
/**
* @param list<string> $args
* @param list<string> $expectedSuggestions
*/
#[TestWith([['console'], 1, [self::class]])]
#[TestWith([['console', self::class], 2, ['preUpdate', 'postPersist']])]
#[TestWith([['console', 'NonExistentEntity'], 2, []])]
public function testComplete(array $args, int $currentIndex, array $expectedSuggestions): void
{
$input = CompletionInput::fromTokens($args, $currentIndex);
$input->bind($this->command->getDefinition());
$suggestions = new CompletionSuggestions();
$this->command->complete($input, $suggestions);
self::assertSame($expectedSuggestions, array_map(static fn (Suggestion $suggestion) => $suggestion->getValue(), $suggestions->getValueSuggestions()));
}
/** @return MockObject&ManagerRegistry */
private function getMockManagerRegistry(): ManagerRegistry
{
$mappingDriverMock = $this->createMock(MappingDriver::class);
$mappingDriverMock->method('getAllClassNames')->willReturn([self::class]);
$config = new Configuration();
$config->setMetadataDriverImpl($mappingDriverMock);
$classMetadata = new ClassMetadata(self::class);
$classMetadata->addEntityListener('preUpdate', FooListener::class, 'preUpdate');
$classMetadata->addEntityListener('preUpdate', BarListener::class, '__invoke');
$classMetadata->addEntityListener('postPersist', BazListener::class, 'postPersist');
$emMock = $this->createMock(EntityManagerInterface::class);
$emMock->method('getConfiguration')->willReturn($config);
$emMock->method('getClassMetadata')->willReturn($classMetadata);
$doctrineMock = $this->createMock(ManagerRegistry::class);
$doctrineMock->method('getManagerNames')->willReturn(['default' => 'entity_manager.default']);
$doctrineMock->method('getManager')->willReturn($emMock);
$doctrineMock->method('getManagerForClass')->willReturn($emMock);
return $doctrineMock;
}
}

View File

@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Tools\Console\Command\Debug;
use Doctrine\Common\EventManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\Console\ApplicationCompatibility;
use Doctrine\ORM\Tools\Console\Command\Debug\DebugEventManagerDoctrineCommand;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\BarListener;
use Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\BazListener;
use Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\FooListener;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Tester\CommandTester;
use function array_map;
class DebugEventManagerDoctrineCommandTest extends TestCase
{
use ApplicationCompatibility;
private DebugEventManagerDoctrineCommand $command;
protected function setUp(): void
{
parent::setUp();
$application = new Application();
$this->command = new DebugEventManagerDoctrineCommand($this->getMockManagerRegistry());
self::addCommandToApplication($application, $this->command);
}
public function testExecute(): void
{
$commandTester = new CommandTester($this->command);
$commandTester->execute(
['command' => $this->command->getName()],
);
self::assertSame(<<<'TXT'
Event listeners for default entity manager
==========================================
------------- ------- ------------------------------------------------------------------------------------
Event Order Listener
------------- ------- ------------------------------------------------------------------------------------
postPersist #1 Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\BazListener::postPersist()
------------- ------- ------------------------------------------------------------------------------------
preUpdate #1 Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\FooListener::preUpdate()
#2 Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\BarListener::__invoke()
------------- ------- ------------------------------------------------------------------------------------
TXT
, $commandTester->getDisplay(true));
}
public function testExecuteWithEvent(): void
{
$commandTester = new CommandTester($this->command);
$commandTester->execute(
['command' => $this->command->getName(), 'event' => 'postPersist'],
);
self::assertSame(<<<'TXT'
Event listeners for default entity manager
==========================================
------------- ------- ------------------------------------------------------------------------------------
Event Order Listener
------------- ------- ------------------------------------------------------------------------------------
postPersist #1 Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\BazListener::postPersist()
------------- ------- ------------------------------------------------------------------------------------
TXT
, $commandTester->getDisplay(true));
}
public function testExecuteWithMissingEvent(): void
{
$commandTester = new CommandTester($this->command);
$commandTester->execute(
['command' => $this->command->getName(), 'event' => 'preRemove'],
);
self::assertSame(<<<'TXT'
[INFO] No listeners are configured for the "preRemove" event.
TXT
, $commandTester->getDisplay(true));
}
/**
* @param list<string> $args
* @param list<string> $expectedSuggestions
*/
#[TestWith([['console'], 1, ['preUpdate', 'postPersist']])]
#[TestWith([['console', '--em'], 1, ['default']])]
public function testComplete(array $args, int $currentIndex, array $expectedSuggestions): void
{
$input = CompletionInput::fromTokens($args, $currentIndex);
$input->bind($this->command->getDefinition());
$suggestions = new CompletionSuggestions();
$this->command->complete($input, $suggestions);
self::assertSame($expectedSuggestions, array_map(static fn (Suggestion $suggestion) => $suggestion->getValue(), $suggestions->getValueSuggestions()));
}
/** @return MockObject&ManagerRegistry */
private function getMockManagerRegistry(): ManagerRegistry
{
$eventManager = new EventManager();
$eventManager->addEventListener('preUpdate', new FooListener());
$eventManager->addEventListener('preUpdate', new BarListener());
$eventManager->addEventListener('postPersist', new BazListener());
$emMock = $this->createMock(EntityManagerInterface::class);
$emMock->method('getEventManager')->willReturn($eventManager);
$doctrineMock = $this->createMock(ManagerRegistry::class);
$doctrineMock->method('getDefaultManagerName')->willReturn('default');
$doctrineMock->method('getManager')->willReturn($emMock);
$doctrineMock->method('getManagerNames')->willReturn(['default' => 'entity_manager.default']);
return $doctrineMock;
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures;
class BarListener
{
public function __invoke(): void
{
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures;
class BazListener
{
public function postPersist(): void
{
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures;
class FooListener
{
public function preUpdate(): void
{
}
}

View File

@@ -13,6 +13,8 @@ use Doctrine\DBAL\Schema\Name\UnqualifiedName;
use Doctrine\DBAL\Schema\PrimaryKeyConstraint;
use Doctrine\DBAL\Schema\PrimaryKeyConstraintEditor;
use Doctrine\DBAL\Schema\Table as DbalTable;
use Doctrine\DBAL\Types\EnumType;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
@@ -46,6 +48,7 @@ use Doctrine\Tests\Models\Enums\Card;
use Doctrine\Tests\Models\Enums\Suit;
use Doctrine\Tests\Models\Forum\ForumAvatar;
use Doctrine\Tests\Models\Forum\ForumUser;
use Doctrine\Tests\Models\GH10288\GH10288People;
use Doctrine\Tests\Models\NullDefault\NullDefaultColumn;
use Doctrine\Tests\OrmTestCase;
use PHPUnit\Framework\Attributes\Group;
@@ -272,6 +275,38 @@ class SchemaToolTest extends OrmTestCase
self::assertEquals(255, $column->getLength());
}
public function testSetDiscriminatorColumnWithEnumType(): void
{
if (! class_exists(EnumType::class)) {
self::markTestSkipped('Test valid for doctrine/dbal versions with EnumType only.');
}
$em = $this->getTestEntityManager();
$schemaTool = new SchemaTool($em);
$metadata = $em->getClassMetadata(FirstEntity::class);
$metadata->setInheritanceType(ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE);
$metadata->setDiscriminatorColumn([
'name' => 'discriminator',
'type' => Types::ENUM,
'enumType' => GH10288People::class,
]);
$schema = $schemaTool->getSchemaFromMetadata([$metadata]);
self::assertTrue($schema->hasTable('first_entity'));
$table = $schema->getTable('first_entity');
self::assertTrue($table->hasColumn('discriminator'));
$column = $table->getColumn('discriminator');
self::assertEquals(GH10288People::class, $column->getPlatformOption('enumType'));
self::assertEquals([0 => 'boss', 1 => 'employee'], $column->getValues());
$this->expectException(MappingException::class);
$this->expectExceptionMessage("The entries 'user' in the discriminator map of class '" . FirstEntity::class . "' do not correspond to enum cases of '" . GH10288People::class . "'.");
$metadata->setDiscriminatorMap(['user' => CmsUser::class, 'employee' => CmsEmployee::class]);
}
public function testDerivedCompositeKey(): void
{
$em = $this->getTestEntityManager();