Compare commits

...

101 Commits

Author SHA1 Message Date
Grégoire Paris 99b56af279 Merge pull request #12444 from mvanhorn/osc/12443-docs-wrapintransaction
docs: fix EntityManager transactional reference to wrapInTransaction
2026-04-24 19:47:18 +02:00
Matt Van Horn 62e6fb5856 docs: fix EntityManager transactional reference to wrapInTransaction
The `EntityManager` API exposes `wrapInTransaction` (`transactional`
was removed in 3.0); the code sample a few lines above already uses
`wrapInTransaction`, so the follow-up paragraph was the odd one out.

Fixes #12443
2026-04-24 01:56:04 -07:00
Grégoire Paris be6ddf001e Merge pull request #12436 from greg0ire/3.6.x
Merge 2.20.x up into 3.6.x
2026-04-22 08:51:34 +02:00
Grégoire Paris 08a930a424 Merge remote-tracking branch 'origin/2.20.x' into 3.6.x 2026-04-22 07:42:01 +02:00
HypeMC a381af4424 Document composite foreign keys (#12433) 2026-04-22 00:09:28 +02:00
HypeMC 81f6bb4d31 Fix errors in EBNF (#12430) 2026-04-21 23:52:48 +02:00
Grégoire Paris eca39efc6e Merge pull request #12416 from robske110/fix-9538
fix: Do not change readonly id when deleting (#9538)
2026-04-21 20:15:54 +02:00
Grégoire Paris 65f5f49809 Merge pull request #12431 from HypeMC/fix-tests-dir-structure
Move leftover tests to new directory structure
2026-04-11 10:48:21 +02:00
HypeMC 11d7a91e62 Move leftover tests to new directory structure 2026-04-11 03:40:30 +02:00
Grégoire Paris f414d89b05 Merge pull request #12429 from derrabus/bugfix/getting-started-link
Fix "Install Composer" link on getting started documentation
2026-04-09 12:07:38 +02:00
Alexander M. Turek aa2eb71555 Fix "Install Composer" link on getting started documentation 2026-04-09 10:24:06 +02:00
Alexander M. Turek eecb1d8efd Merge pull request #12426 from doctrine/dependabot/github_actions/2.20.x/doctrine-b32f1f77d9 2026-04-05 08:54:38 +02:00
dependabot[bot] b9dd4fc963 Bump the doctrine group with 4 updates
Bumps the doctrine group with 4 updates: [doctrine/.github/.github/workflows/coding-standards.yml](https://github.com/doctrine/.github), [doctrine/.github/.github/workflows/composer-lint.yml](https://github.com/doctrine/.github), [doctrine/.github/.github/workflows/documentation.yml](https://github.com/doctrine/.github) and [doctrine/.github/.github/workflows/release-on-milestone-closed.yml](https://github.com/doctrine/.github).


Updates `doctrine/.github/.github/workflows/coding-standards.yml` from 14.0.0 to 15.0.0
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/14.0.0...15.0.0)

Updates `doctrine/.github/.github/workflows/composer-lint.yml` from 14.0.0 to 15.0.0
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/14.0.0...15.0.0)

Updates `doctrine/.github/.github/workflows/documentation.yml` from 14.0.0 to 15.0.0
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/14.0.0...15.0.0)

Updates `doctrine/.github/.github/workflows/release-on-milestone-closed.yml` from 14.0.0 to 15.0.0
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/14.0.0...15.0.0)

---
updated-dependencies:
- dependency-name: doctrine/.github/.github/workflows/coding-standards.yml
  dependency-version: 15.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: doctrine
- dependency-name: doctrine/.github/.github/workflows/composer-lint.yml
  dependency-version: 15.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: doctrine
- dependency-name: doctrine/.github/.github/workflows/documentation.yml
  dependency-version: 15.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: doctrine
- dependency-name: doctrine/.github/.github/workflows/release-on-milestone-closed.yml
  dependency-version: 15.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: doctrine
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-05 06:12:23 +00:00
Grégoire Paris e88cd591f0 Merge pull request #12423 from greg0ire/3.6.x
Merge 2.20.x up into 3.6.x
2026-04-02 08:53:27 +02:00
Grégoire Paris 88a8f75f62 Merge remote-tracking branch 'origin/2.20.x' into 3.6.x 2026-04-02 08:27:51 +02:00
Tom Roar Furunes 9fe8ce4bf7 Merge pull request #12373 from tomme87/12225-fix-hydration-issue
12225 Fix hydration issue when using indexBy, SQL filter, and inheritance mapping
2026-04-02 08:18:54 +02:00
Grégoire Paris a46ff16339 Merge pull request #12414 from ahmed-bhs/docs/enum-type-mapping
Add documentation for enumType mapping with PHP backed enums
2026-04-01 00:35:46 +02:00
Grégoire Paris 94e60e4318 Merge pull request #12419 from greg0ire/backport-12222
Backport #12222
2026-04-01 00:35:01 +02:00
Grégoire Paris f59cd4019a Add ORDER BY clause to SELECT query
The order of results is not guaranteed unless we do so, and the test can
fail in some cases:

	There was 1 failure:

	1) Doctrine\Tests\ORM\Functional\QueryTest::testToIterableWithMixedResultArbitraryJoinsScalars
	Failed asserting that two strings are equal.
	--- Expected
	+++ Actual
	@@ @@
	-'Doctrine 2'
	+'lala 2'

	/home/runner/work/orm/orm/tests/Tests/ORM/Functional/QueryTest.php:481
2026-03-29 10:33:06 +02:00
Grégoire Paris 81558a8b2a Merge pull request #12418 from doctrine/dependabot/github_actions/2.20.x/codecov/codecov-action-6
Bump codecov/codecov-action from 5 to 6
2026-03-29 10:10:19 +02:00
dependabot[bot] e431ee113d Bump codecov/codecov-action from 5 to 6
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5 to 6.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-29 06:12:22 +00:00
Tim (robske_110) 9b226894e7 fix: Do not change readonly id when deleting (#9538)
If the identifier is a readonly attribute, we cannot set it to null.
While this effectively prevents reuse of the object, skipping the setting
allows to use readonly identifiers for entities that need to be removed.
2026-03-28 16:19:05 +01:00
Ahmed EBEN HASSINE de4ec208fd Mention PostgreSQL jsonb as a reason to favor json over simple_array 2026-03-26 08:52:24 +01:00
Ahmed EBEN HASSINE 脳の流れ df014a74c9 Update docs/en/reference/basic-mapping.rst
Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>
2026-03-26 08:52:24 +01:00
Ahmed EBEN HASSINE 2f46d95028 Add nullable enums, default values, query usage and XML mapping to enum docs
Cover additional enumType behaviors: nullable columns, database default
values with enum cases, usage in DQL/QueryBuilder/findBy, and XML
mapping syntax with enum-type attribute.
2026-03-26 08:52:24 +01:00
Ahmed EBEN HASSINE 3fda5629f6 Add documentation for enumType mapping with PHP backed enums
The enumType option on #[Column] was barely mentioned in the docs and
had no dedicated section. This adds a complete reference covering
single-value columns, collection types (json, simple_array), automatic
type inference, validation behavior, and platform compatibility.
2026-03-26 08:52:00 +01:00
Grégoire Paris 6b273234d6 Merge pull request #12407 from doctrine/dependabot/github_actions/2.20.x/doctrine/dot-github/dot-github/workflows/composer-lint.yml-14.0.0
Bump doctrine/.github/.github/workflows/composer-lint.yml from 13.1.0 to 14.0.0
2026-03-26 08:08:34 +01:00
Grégoire Paris 580a95ce3f Merge pull request #12413 from greg0ire/group-doctrine-updates
Group Doctrine workflow updates together
2026-03-26 06:53:01 +01:00
Grégoire Paris 8b91e248eb Group Doctrine workflow updates together
We do very small releases, often touching only one workflow, which means
it is unlikely to have several issues when attempting to bump several
references to this repository.
2026-03-25 20:48:20 +01:00
Grégoire Paris d2266c7d0c Merge pull request #12408 from doctrine/dependabot/github_actions/2.20.x/doctrine/dot-github/dot-github/workflows/documentation.yml-14.0.0
Bump doctrine/.github/.github/workflows/documentation.yml from 13.1.0 to 14.0.0
2026-03-22 13:53:32 +00:00
Grégoire Paris eb0485869a Merge pull request #12406 from doctrine/dependabot/github_actions/2.20.x/ramsey/composer-install-4
Bump ramsey/composer-install from 3 to 4
2026-03-22 13:52:17 +00:00
Grégoire Paris 8372d600c6 Merge pull request #12409 from doctrine/dependabot/github_actions/2.20.x/doctrine/dot-github/dot-github/workflows/coding-standards.yml-14.0.0
Bump doctrine/.github/.github/workflows/coding-standards.yml from 13.1.0 to 14.0.0
2026-03-22 08:41:51 +00:00
Grégoire Paris dde1d71b34 Merge pull request #12410 from doctrine/dependabot/github_actions/2.20.x/doctrine/dot-github/dot-github/workflows/release-on-milestone-closed.yml-14.0.0
Bump doctrine/.github/.github/workflows/release-on-milestone-closed.yml from 13.1.0 to 14.0.0
2026-03-22 08:38:30 +00:00
dependabot[bot] 0d03255061 Bump doctrine/.github/.github/workflows/release-on-milestone-closed.yml
Bumps [doctrine/.github/.github/workflows/release-on-milestone-closed.yml](https://github.com/doctrine/.github) from 13.1.0 to 14.0.0.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/13.1.0...14.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-22 06:12:28 +00:00
dependabot[bot] 75a18090d9 Bump doctrine/.github/.github/workflows/coding-standards.yml
Bumps [doctrine/.github/.github/workflows/coding-standards.yml](https://github.com/doctrine/.github) from 13.1.0 to 14.0.0.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/13.1.0...14.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-22 06:12:27 +00:00
dependabot[bot] 77ffd2ab68 Bump doctrine/.github/.github/workflows/documentation.yml
Bumps [doctrine/.github/.github/workflows/documentation.yml](https://github.com/doctrine/.github) from 13.1.0 to 14.0.0.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/13.1.0...14.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-22 06:12:25 +00:00
dependabot[bot] 401cc06d71 Bump doctrine/.github/.github/workflows/composer-lint.yml
Bumps [doctrine/.github/.github/workflows/composer-lint.yml](https://github.com/doctrine/.github) from 13.1.0 to 14.0.0.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/13.1.0...14.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-22 06:12:22 +00:00
dependabot[bot] 5029b193ee Bump ramsey/composer-install from 3 to 4
Bumps [ramsey/composer-install](https://github.com/ramsey/composer-install) from 3 to 4.
- [Release notes](https://github.com/ramsey/composer-install/releases)
- [Commits](https://github.com/ramsey/composer-install/compare/v3...v4)

---
updated-dependencies:
- dependency-name: ramsey/composer-install
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-22 06:12:19 +00:00
Alexander M. Turek 27c33cf88d Merge branch '2.20.x' into 3.6.x
* 2.20.x:
  Make the data provider static
  Raise proper exception for invalid arguments in Base::add() (#12394)
2026-03-12 09:31:56 +01:00
Alexander M. Turek 6068b61a0d Make the data provider static 2026-03-12 09:24:03 +01:00
Alexander M. Turek 00024f7d88 Raise proper exception for invalid arguments in Base::add() (#12394) 2026-03-12 09:05:27 +01:00
Alexander M. Turek 331f8b52cb Merge branch '2.20.x' into 3.6.x
* 2.20.x:
  Fix code style (#12395)
  Bump actions/upload-artifact from 6 to 7 (#12387)
  Bump actions/download-artifact from 7 to 8 (#12386)
2026-03-11 16:55:13 +01:00
Alexander M. Turek b2faba62b7 Fix code style (#12395) 2026-03-11 16:51:15 +01:00
dependabot[bot] da426a0036 Bump actions/upload-artifact from 6 to 7 (#12387)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  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>
2026-03-05 00:34:23 +01:00
dependabot[bot] 1891a76f13 Bump actions/download-artifact from 7 to 8 (#12386)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  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>
2026-03-05 00:33:50 +01:00
vvaswani 4262eb495b fix: update index to be serialized in __sleep() (#12366)
Signed-off-by: Vikram Vaswani <2571660+vvaswani@users.noreply.github.com`>
2026-01-30 22:41:41 +01:00
Grégoire Paris d3b47d2cbb Merge pull request #12355 from doctrine/2.20.x
Merge 2.20.x up into 3.6.x
2026-01-25 12:48:28 +01:00
Grégoire Paris 026f5bfe1b Merge pull request #12350 from greg0ire/missing-order-by
Add missing ORDER BY clause
2026-01-18 10:41:46 +01:00
Grégoire Paris 0b0f2f4d86 Add missing ORDER BY clause
This causes transient failures with PostgreSQL. Order is not guaranteed.
2026-01-17 13:39:44 +01:00
Grégoire Paris 0bd839a720 Merge pull request #12345 from greg0ire/3.6.x
Merge 2.20.x up into 3.6.x
2026-01-16 18:27:03 +01:00
Grégoire Paris b65004fc26 Merge remote-tracking branch 'origin/2.20.x' into 3.6.x 2026-01-16 18:24:38 +01:00
Grégoire Paris d2418ab074 Merge pull request #12344 from greg0ire/update-baseline
Update PHPStan baseline
2026-01-15 23:38:15 +01:00
Grégoire Paris 39a05e31c9 Update PHPStan baseline
This is caused by the release of doctrine/collections 2.7.0. The error
message is a bit shorter now.
2026-01-15 20:13:05 +01:00
sasezaki ab156a551c Update phpstan-dbal2 to phpstan-dbal3 in .gitattributes (#12343) 2026-01-12 12:53:04 +01:00
Grégoire Paris 2148940290 Merge pull request #12335 from greg0ire/gh-12166
Avoid lazy object initialization when initializing read-only property
2026-01-09 06:28:15 +01:00
Grégoire Paris d3538095fd Merge pull request #12337 from greg0ire/3.6.x
Merge 2.20.x up into 3.6.x
2026-01-09 06:26:06 +01:00
Grégoire Paris 0c1bf14729 Merge remote-tracking branch 'origin/2.20.x' into 3.6.x 2026-01-08 08:55:21 +01:00
Grégoire Paris 3b8c23c51d Merge pull request #12336 from greg0ire/dmwydo
Stop mocking EventManager
2026-01-08 07:19:16 +01:00
Grégoire Paris 60d4ea694a Stop mocking EventManager
It is defined outside this repository, so let us not mock what we do not
own.
2026-01-07 21:52:47 +01:00
Grégoire Paris e923bbc932 Avoid lazy object initialization when initializing read-only property
Initializing e.g. a readonly ID does not require loading any data from
the database. However, calling isInitialized() on the reflection of a
readonly property triggers the native lazy object initialization.
If we have a lazy property at hand, then the property cannot be initialized
already, so it is safe to skip the call.
2026-01-07 13:04:14 +01:00
n0099 8cbd34c666 Merge pull request #12060 from n0099/patch-1
Update doc `dql-custom-walkers.rst` with an output walker to interpolate parameters into SQL
2026-01-01 18:27:48 +01:00
Vladislav Sultanov 8bdefef6d1 Handle int-backed enums for values stored as string values in MySQL ENUM columns (#12275)
* FIX: Handle int-backed enums for values stored as string values in MySQL ENUM columns

Related issue: doctrine#12274

* FIX: Apply coding standard changes

Related issue: doctrine#12274

* FIX: Add unit test cases

Related issue: doctrine#12274
2025-12-30 18:21:10 +01:00
Grégoire Paris 0f8730a6e5 Merge pull request #12331 from greg0ire/a-the
Fix grammatical errors
2025-12-24 11:39:26 +01:00
Grégoire Paris 62477b5d42 Update branch metadata (#12327)
* Update branch metadata

3.6.0 has been released. As a consequence:

- 3.7.x is the next minor branch;
- 3.6.x is the current branch;
- 3.5.x is no longer maintained.

* doc: drop old releases

We should reduce the number of versions we have, so let's remove docs
for versions that have less than 2k downloads per day.
2025-12-21 00:29:57 +01:00
HypeMC 12116aa3c2 Fix docs regarding query hints (#12328) 2025-12-21 00:29:08 +01:00
Grégoire Paris 0aeddd0592 Fix grammatical errors 2025-12-20 15:34:16 +01:00
Grégoire Paris 2491c4b20d Merge pull request #12329 from HypeMC/fix-result-cache-examples
Fix result cache examples in docs
2025-12-20 09:14:39 +01:00
HypeMC 08d6167243 Fix result cache examples in docs 2025-12-20 01:21:11 +01:00
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
Alexis Lefebvre 01fd55e9ea chore: show parameters in name of CI jobs 2025-11-29 23:12:27 +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 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 f0562f4120 Merge pull request #12273 from greg0ire/deprecate-default
Deprecate FieldMapping::$default
2025-11-19 06:50:43 +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 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 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 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
143 changed files with 3289 additions and 541 deletions
+6 -51
View File
@@ -12,42 +12,17 @@
"upcoming": true
},
{
"name": "3.6",
"branchName": "3.6.x",
"slug": "3.6",
"name": "3.7",
"branchName": "3.7.x",
"slug": "3.7",
"upcoming": true
},
{
"name": "3.5",
"branchName": "3.5.x",
"slug": "3.5",
"name": "3.6",
"branchName": "3.6.x",
"slug": "3.6",
"current": true
},
{
"name": "3.4",
"slug": "3.4",
"maintained": false
},
{
"name": "3.3",
"slug": "3.3",
"maintained": false
},
{
"name": "3.2",
"slug": "3.2",
"maintained": false
},
{
"name": "3.1",
"slug": "3.1",
"maintained": false
},
{
"name": "3.0",
"slug": "3.0",
"maintained": false
},
{
"name": "2.21",
"branchName": "2.21.x",
@@ -89,26 +64,6 @@
"name": "2.14",
"slug": "2.14",
"maintained": false
},
{
"name": "2.13",
"slug": "2.13",
"maintained": false
},
{
"name": "2.12",
"slug": "2.12",
"maintained": false
},
{
"name": "2.11",
"slug": "2.11",
"maintained": false
},
{
"name": "2.10",
"slug": "2.10",
"maintained": false
}
]
}
+1 -1
View File
@@ -15,6 +15,6 @@ phpcs.xml.dist export-ignore
phpbench.json export-ignore
phpstan.neon export-ignore
phpstan-baseline.neon export-ignore
phpstan-dbal2.neon export-ignore
phpstan-dbal3.neon export-ignore
phpstan-params.neon export-ignore
phpstan-persistence2.neon export-ignore
+4
View File
@@ -7,3 +7,7 @@ updates:
labels:
- "CI"
target-branch: "2.20.x"
groups:
doctrine:
patterns:
- "doctrine/*"
+1 -1
View File
@@ -24,4 +24,4 @@ on:
jobs:
coding-standards:
uses: "doctrine/.github/.github/workflows/coding-standards.yml@13.0.0"
uses: "doctrine/.github/.github/workflows/coding-standards.yml@15.0.0"
+1 -1
View File
@@ -17,4 +17,4 @@ on:
jobs:
composer-lint:
name: "Composer Lint"
uses: "doctrine/.github/.github/workflows/composer-lint.yml@13.0.0"
uses: "doctrine/.github/.github/workflows/composer-lint.yml@15.0.0"
+61 -16
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:
@@ -44,26 +51,38 @@ jobs:
- "pdo_sqlite"
deps:
- "highest"
stability:
- "stable"
native_lazy:
- "0"
include:
- php-version: "8.2"
dbal-version: "4@dev"
extension: "pdo_sqlite"
stability: "stable"
native_lazy: "0"
- php-version: "8.2"
dbal-version: "4@dev"
extension: "sqlite3"
stability: "stable"
native_lazy: "0"
- php-version: "8.1"
dbal-version: "default"
deps: "lowest"
extension: "pdo_sqlite"
stability: "stable"
native_lazy: "0"
- php-version: "8.4"
dbal-version: "default"
deps: "highest"
extension: "pdo_sqlite"
stability: "stable"
native_lazy: "1"
- php-version: "8.4"
dbal-version: "default"
deps: "highest"
extension: "sqlite3"
stability: "dev"
native_lazy: "1"
steps:
@@ -80,6 +99,14 @@ jobs:
coverage: "pcov"
ini-values: "zend.assertions=1, apc.enable_cli=1"
- name: "Allow dev dependencies"
run: |
composer config minimum-stability dev
composer remove --no-update --dev phpbench/phpbench phpdocumentor/guides-cli
composer require --no-update symfony/console:^8 symfony/var-exporter:^8 doctrine/dbal:^4.4
composer require --dev --no-update symfony/cache:^8
if: "${{ matrix.stability == 'dev' }}"
- name: "Require specific DBAL version"
run: "composer require doctrine/dbal ^${{ matrix.dbal-version }} --no-update"
if: "${{ matrix.dbal-version != 'default' }}"
@@ -89,7 +116,7 @@ jobs:
if: "${{ matrix.native_lazy == '0' }}"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v3"
uses: "ramsey/composer-install@v4"
with:
composer-options: "--ignore-platform-req=php+"
dependency-versions: "${{ matrix.deps }}"
@@ -123,9 +150,9 @@ jobs:
ENABLE_NATIVE_LAZY_OBJECTS: ${{ matrix.native_lazy }}
- name: "Upload coverage file"
uses: "actions/upload-artifact@v5"
uses: "actions/upload-artifact@v7"
with:
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-${{ matrix.native_lazy }}-coverage"
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-${{ matrix.stability }}-${{ matrix.native_lazy }}-coverage"
path: "coverage*.xml"
@@ -164,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"
@@ -224,7 +257,7 @@ jobs:
if: "${{ matrix.dbal-version != 'default' }}"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v3"
uses: "ramsey/composer-install@v4"
with:
composer-options: "--ignore-platform-req=php+"
@@ -232,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@v7"
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"
@@ -292,7 +331,7 @@ jobs:
extensions: "${{ matrix.extension }}"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v3"
uses: "ramsey/composer-install@v4"
with:
composer-options: "--ignore-platform-req=php+"
@@ -300,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@v7"
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"
@@ -368,7 +413,7 @@ jobs:
if: "${{ matrix.dbal-version != 'default' }}"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v3"
uses: "ramsey/composer-install@v4"
with:
composer-options: "--ignore-platform-req=php+"
@@ -397,7 +442,7 @@ jobs:
ENABLE_SECOND_LEVEL_CACHE: 1
- name: "Upload coverage files"
uses: "actions/upload-artifact@v5"
uses: "actions/upload-artifact@v7"
with:
name: "${{ github.job }}-${{ matrix.mysql-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
path: "coverage*.xml"
@@ -420,12 +465,12 @@ jobs:
fetch-depth: 2
- name: "Download coverage files"
uses: "actions/download-artifact@v6"
uses: "actions/download-artifact@v8"
with:
path: "reports"
- name: "Upload to Codecov"
uses: "codecov/codecov-action@v5"
uses: "codecov/codecov-action@v6"
with:
directory: reports
env:
+1 -1
View File
@@ -17,4 +17,4 @@ on:
jobs:
documentation:
name: "Documentation"
uses: "doctrine/.github/.github/workflows/documentation.yml@13.0.0"
uses: "doctrine/.github/.github/workflows/documentation.yml@15.0.0"
+1 -1
View File
@@ -48,7 +48,7 @@ jobs:
ini-values: "zend.assertions=1, apc.enable_cli=1"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v3"
uses: "ramsey/composer-install@v4"
- name: "Run PHPBench"
run: "vendor/bin/phpbench run --report=default"
@@ -7,7 +7,7 @@ on:
jobs:
release:
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@13.0.0"
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@15.0.0"
secrets:
GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}
+1 -1
View File
@@ -50,7 +50,7 @@ jobs:
- name: Install dependencies with Composer
uses: ramsey/composer-install@v2
uses: ramsey/composer-install@v4
- name: Run static analysis with phpstan/phpstan
run: "vendor/bin/phpstan analyse -c ${{ matrix.config }} --error-format=checkstyle | cs2pr"
+8 -8
View File
@@ -1,7 +1,7 @@
| [4.0.x][4.0] | [3.6.x][3.6] | [3.5.x][3.5] | [2.21.x][2.21] | [2.20.x][2.20] |
| [4.0.x][4.0] | [3.7.x][3.7] | [3.6.x][3.6] | [2.21.x][2.21] | [2.20.x][2.20] |
|:------------------------------------------------------:|:------------------------------------------------------:|:------------------------------------------------------:|:--------------------------------------------------------:|:--------------------------------------------------------:|
| [![Build status][4.0 image]][4.0 workflow] | [![Build status][3.6 image]][3.6 workflow] | [![Build status][3.5 image]][3.5 workflow] | [![Build status][2.21 image]][2.21 workflow] | [![Build status][2.20 image]][2.20 workflow] |
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.6 coverage image]][3.6 coverage] | [![Coverage Status][3.5 coverage image]][3.5 coverage] | [![Coverage Status][2.21 coverage image]][2.21 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] |
| [![Build status][4.0 image]][4.0 workflow] | [![Build status][3.7 image]][3.7 workflow] | [![Build status][3.6 image]][3.6 workflow] | [![Build status][2.21 image]][2.21 workflow] | [![Build status][2.20 image]][2.20 workflow] |
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.7 coverage image]][3.7 coverage] | [![Coverage Status][3.6 coverage image]][3.6 coverage] | [![Coverage Status][2.21 coverage image]][2.21 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] |
Doctrine ORM is an object-relational mapper for PHP 8.1+ that provides transparent persistence
for PHP objects. It sits on top of a powerful database abstraction layer (DBAL). One of its key features
@@ -21,16 +21,16 @@ without requiring unnecessary code duplication.
[4.0 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A4.0.x
[4.0 coverage image]: https://codecov.io/gh/doctrine/orm/branch/4.0.x/graph/badge.svg
[4.0 coverage]: https://codecov.io/gh/doctrine/orm/branch/4.0.x
[3.7 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.7.x
[3.7]: https://github.com/doctrine/orm/tree/3.7.x
[3.7 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A3.7.x
[3.7 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.7.x/graph/badge.svg
[3.7 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.7.x
[3.6 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.6.x
[3.6]: https://github.com/doctrine/orm/tree/3.6.x
[3.6 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A3.6.x
[3.6 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.6.x/graph/badge.svg
[3.6 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.6.x
[3.5 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.5.x
[3.5]: https://github.com/doctrine/orm/tree/3.5.x
[3.5 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A3.5.x
[3.5 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.5.x/graph/badge.svg
[3.5 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.5.x
[2.21 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.21.x
[2.21]: https://github.com/doctrine/orm/tree/2.21.x
[2.21 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A2.21.x
+143 -1
View File
@@ -27,6 +27,148 @@ At this point, we recommend upgrading to PHP 8.4 first and then directly from
ORM 2.19 to 3.5 and up so that you can skip the lazy ghost proxy generation
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
deprecated and will be an error in 4.0.
This can happen when using a join column mapping together with an id mapping,
or when using a join column mapping or an inverse join column mapping on a
many-to-many relationship.
```diff
class User
{
#[ORM\Id]
#[ORM\Column(type: 'integer')]
private int $id;
#[ORM\Id]
#[ORM\ManyToOne(targetEntity: Family::class, inversedBy: 'users')]
- #[ORM\JoinColumn(name: 'family_id', referencedColumnName: 'id', nullable: true)]
+ #[ORM\JoinColumn(name: 'family_id', referencedColumnName: 'id')]
private ?Family $family;
#[ORM\ManyToMany(targetEntity: Group::class)]
#[ORM\JoinTable(name: 'user_group')]
- #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: true)]
- #[ORM\InverseJoinColumn(name: 'group_id', referencedColumnName: 'id', nullable: true)]
+ #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id')]
+ #[ORM\InverseJoinColumn(name: 'group_id', referencedColumnName: 'id')]
private Collection $groups;
}
```
## Deprecate `Doctrine\ORM\QueryBuilder::add('join', ...)` with a list of join parts
Using `Doctrine\ORM\QueryBuilder::add('join', ...)` with a list of join parts
is deprecated in favor of using an associative array of join parts with the
root alias as key.
## Deprecate using the `WITH` keyword for arbitrary DQL joins
Using the `WITH` keyword to specify the condition for an arbitrary DQL join is
deprecated in favor of using the `ON` keyword (similar to the SQL syntax for
joins).
The `WITH` keyword is now meant to be used only for filtering conditions in
association joins.
# Upgrade to 3.5
See the General notes to upgrading to 3.x versions above.
@@ -2159,7 +2301,7 @@ from 2.0 have to configure the annotation driver if they don't use `Configuratio
## Scalar mappings can now be omitted from DQL result
You are now allowed to mark scalar SELECT expressions as HIDDEN an they are not hydrated anymore.
You are now allowed to mark scalar SELECT expressions as HIDDEN and they are not hydrated anymore.
Example:
SELECT u, SUM(a.id) AS HIDDEN numArticles FROM User u LEFT JOIN u.Articles a ORDER BY numArticles DESC HAVING numArticles > 10
+39 -2
View File
@@ -40,7 +40,8 @@ Now this is all awfully technical, so let me come to some use-cases
fast to keep you motivated. Using walker implementation you can for
example:
- Modify the Output walker to get the raw SQL via ``Query->getSQL()``
with interpolated parameters.
- Modify the AST to generate a Count Query to be used with a
paginator for any given DQL query.
- Modify the Output Walker to generate vendor-specific SQL
@@ -50,7 +51,7 @@ example:
- Modify the Output walker to pretty print the SQL for debugging
purposes.
In this cookbook-entry I will show examples of the first two
In this cookbook-entry I will show examples of the first three
points. There are probably much more use-cases.
Generic count query for pagination
@@ -223,3 +224,39 @@ huge benefits with using vendor specific features. This would still
allow you write DQL queries instead of NativeQueries to make use of
vendor specific features.
Modifying the Output Walker to get the raw SQL with interpolated parameters
---------------------------------------------------------------------------
Sometimes we may want to log or trace the raw SQL being generated from its DQL
for profiling slow queries afterwards or audit queries that changed many rows
``$query->getSQL()`` will give us the prepared statement being passed to database
with all values of SQL parameters being replaced by positional ``?`` or named ``:name``
as parameters are interpolated into prepared statements by the database while executing the SQL.
``$query->getParameters()`` will give us details about SQL parameters that we've provided.
So we can create an output walker to interpolate all SQL parameters that will be
passed into prepared statement in PHP before database handle them internally:
.. literalinclude:: dql-custom-walkers/InterpolateParametersSQLOutputWalker.php
:language: php
Then you may get the raw SQL with this output walker:
.. code-block:: php
<?php
$query
->where('t.int IN (:ints)')->setParameter(':ints', [1, 2])
->orWhere('t.string IN (?0)')->setParameter(0, ['3', '4'])
->orWhere("t.bool = ?1")->setParameter('?1', true)
->orWhere("t.string = :string")->setParameter(':string', 'ABC')
->setHint(\Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER, InterpolateParametersSQLOutputWalker::class)
->getSQL();
The where clause of the returned SQL should be like:
.. code-block:: sql
WHERE t0_.int IN (1, 2)
OR t0_.string IN ('3', '4')
OR t0_.bool = 1
OR t0_.string = 'ABC'
@@ -0,0 +1,47 @@
<?php
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Types\BooleanType;
use Doctrine\DBAL\Types\Exception\ValueNotConvertible;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Query\AST;
use Doctrine\ORM\Query\SqlOutputWalker;
class InterpolateParametersSQLOutputWalker extends SqlOutputWalker
{
/** {@inheritdoc} */
public function walkInputParameter(AST\InputParameter $inputParam): string
{
$parameter = $this->getQuery()->getParameter($inputParam->name);
if ($parameter === null) {
return '?';
}
$value = $parameter->getValue();
/** @var ParameterType|ArrayParameterType|int|string $typeName */
/** @see \Doctrine\ORM\Query\ParameterTypeInferer::inferType() */
$typeName = $parameter->getType();
$platform = $this->getConnection()->getDatabasePlatform();
$processParameterType = static fn(ParameterType $type) => static fn($value): string =>
(match ($type) { /** @see Type::getBindingType() */
ParameterType::NULL => 'NULL',
ParameterType::INTEGER => $value,
ParameterType::BOOLEAN => (new BooleanType())->convertToDatabaseValue($value, $platform),
ParameterType::STRING, ParameterType::ASCII => $platform->quoteStringLiteral($value),
default => throw new ValueNotConvertible($value, $type->name)
});
if (is_string($typeName) && Type::hasType($typeName)) {
return Type::getType($typeName)->convertToDatabaseValue($value, $platform);
}
if ($typeName instanceof ParameterType) {
return $processParameterType($typeName)($value);
}
if ($typeName instanceof ArrayParameterType && is_array($value)) {
$type = ArrayParameterType::toElementParameterType($typeName);
return implode(', ', array_map($processParameterType($type), $value));
}
throw new ValueNotConvertible($value, $typeName);
}
}
+50 -6
View File
@@ -29,7 +29,7 @@ steps of configuration.
$config = new Configuration;
$config->setMetadataCache($metadataCache);
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true);
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities']);
$config->setMetadataDriverImpl($driverImpl);
$config->setQueryCache($queryCache);
@@ -156,15 +156,59 @@ The attribute driver can be injected in the ``Doctrine\ORM\Configuration``:
<?php
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true);
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities']);
$config->setMetadataDriverImpl($driverImpl);
The path information to the entities is required for the attribute
driver, because otherwise mass-operations on all entities through
the console could not work correctly. All of metadata drivers
accept either a single directory as a string or an array of
directories. With this feature a single driver can support multiple
directories of Entities.
the console could not work correctly. Metadata drivers can accept either
a single directory as a string or an array of directories.
AttributeDriver also accepts ``Doctrine\Persistence\Mapping\Driver\ClassLocator``,
allowing one to customize file discovery logic. You may choose to use Symfony Finder, or
utilize directory scan with ``FileClassLocator::createFromDirectories()``:
.. code-block:: php
<?php
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Doctrine\Persistence\Mapping\Driver\FileClassLocator;
$paths = ['/path/to/lib/MyProject/Entities'];
$classLocator = FileClassLocator::createFromDirectories($paths);
$driverImpl = new AttributeDriver($classLocator);
$config->setMetadataDriverImpl($driverImpl);
With this feature, you're empowered to provide a fine-grained iterator of only necessary
files to the Driver. For example, if you are using Vertical Slice architecture, you can
exclude ``*Test.php``, ``*Controller.php``, ``*Service.php``, etc.:
.. code-block:: php
<?php
use Symfony\Component\Finder\Finder;
$finder = new Finder()->files()->in($paths)
->name('*.php')
->notName(['*Test.php', '*Controller.php', '*Service.php']);
$classLocator = new FileClassLocator($finder);
If you know the list of class names you want to track, use
``Doctrine\Persistence\Mapping\Driver\ClassNames``:
.. code-block:: php
<?php
use Doctrine\Persistence\Mapping\Driver\ClassNames;
use App\Entity\{Article, Book};
$entityClasses = [Article::class, Book::class];
$classLocator = new ClassNames($entityClasses);
$driverImpl = new AttributeDriver($classLocator);
$config->setMetadataDriverImpl($driverImpl);
Metadata Cache (**RECOMMENDED**)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -30,6 +30,13 @@ a property referring to the other side.
To gain a full understanding of associations you should also read about :doc:`owning and
inverse sides of associations <unitofwork-associations>`
Composite Foreign Keys
----------------------
When the target entity has a composite primary key, you need to use multiple
join column mappings, one for each column of the composite key. See the
:doc:`Composite and Foreign Keys <../tutorials/composite-primary-keys>` tutorial for details.
Many-To-One, Unidirectional
---------------------------
@@ -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
+171 -1
View File
@@ -168,7 +168,7 @@ Here is a complete list of ``Column``s attributes (all optional):
- ``insertable`` (default: ``true``): Whether the column should be inserted.
- ``updatable`` (default: ``true``): Whether the column should be updated.
- ``generated`` (default: ``null``): Whether the generated strategy should be ``'NEVER'``, ``'INSERT'`` and ``ALWAYS``.
- ``enumType`` (requires PHP 8.1 and ``doctrine/orm`` 2.11): The PHP enum class name to convert the database value into.
- ``enumType`` (requires PHP 8.1 and ``doctrine/orm`` 2.11): The PHP enum class name to convert the database value into. See :ref:`reference-enum-mapping`.
- ``precision`` (default: 0): The precision for a decimal (exact numeric) column
(applies only for decimal column),
which is the maximum number of digits that are stored for the values.
@@ -190,6 +190,22 @@ 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
@@ -305,6 +321,160 @@ that value and raw value are different, you have to use the raw value representa
$messageRepository = $entityManager->getRepository(Message::class);
$deMessages = $messageRepository->findBy(['language' => 'de']); // Use lower case here for raw value representation
.. _reference-enum-mapping:
Mapping PHP Enums
-----------------
.. versionadded:: 2.11
Doctrine natively supports mapping PHP backed enums to database columns.
A backed enum is a PHP enum that the same scalar type (``string`` or ``int``)
assigned to each case. Doctrine stores the scalar value in the database and
converts it back to the enum instance when hydrating the entity.
Using ``enumType`` provides three main benefits:
- **Automatic conversion**: Doctrine handles the conversion in both directions
transparently. When loading an entity, scalar values from the database are
converted into enum instances. When persisting, enum instances are reduced
to their scalar ``->value`` before being sent to the database.
- **Type-safety**: Entity properties contain enum instances directly. Your
getters return ``Suit`` instead of ``string``, removing the need to call
``Suit::from()`` manually.
- **Validation**: When a database value does not match any enum case, Doctrine
throws a ``MappingException`` during hydration instead of silently returning
an invalid value.
This feature works with all database platforms supported by Doctrine (MySQL,
PostgreSQL, SQLite, etc.) as it relies on standard column types (``string``,
``integer``, ``json``, ``simple_array``) rather than any vendor-specific enum
type.
.. note::
This is unrelated to the MySQL-specific ``ENUM`` column type covered in
:doc:`the MySQL Enums cookbook entry </cookbook/mysql-enums>`.
Defining an Enum
~~~~~~~~~~~~~~~~
.. literalinclude:: basic-mapping/Suit.php
:language: php
Only backed enums (``string`` or ``int``) are supported. Unit enums (without
a scalar value) cannot be mapped.
Single-Value Columns
~~~~~~~~~~~~~~~~~~~~
Use the ``enumType`` option on ``#[Column]`` to map a property to a backed enum.
The underlying database column stores the enum's scalar value (``string`` or ``int``).
.. literalinclude:: basic-mapping/EnumMapping.php
:language: php
When the PHP property is typed with the enum class, Doctrine automatically
infers the appropriate column type (``string`` for string-backed enums,
``integer`` for int-backed enums) and sets ``enumType``. You can also specify
the column ``type`` explicitly.
Storing Collections of Enums
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can store multiple enum values in a single column by combining ``enumType``
with a collection column type: ``json`` or ``simple_array``.
.. note::
Automatic type inference does not apply to collection columns. When the
PHP property is typed as ``array``, Doctrine cannot detect the enum class.
You must specify both ``type`` and ``enumType`` explicitly.
.. literalinclude:: basic-mapping/EnumCollectionMapping.php
:language: php
With ``json``, the values are stored as a JSON array (e.g. ``["hearts","spades"]``).
With ``simple_array``, the values are stored as a comma-separated string
(e.g. ``hearts,spades``).
In both cases, Doctrine converts each element to and from the enum
automatically during hydration and persistence.
.. tip::
Use ``json`` when enum values may contain commas, when you need to store
int-backed enums (as it preserves value types), when the column also
stores complex/nested data structures, or when you want to query individual
values using database-native JSON operators (e.g. PostgreSQL ``jsonb``).
Prefer ``simple_array`` for a compact, human-readable storage of
string-backed enums whose values do not contain commas.
+-------------------+-----------------------------+-------------------------------+
| Column type | Database storage | PHP type |
+===================+=============================+===============================+
| ``string`` | ``hearts`` | ``Suit`` |
+-------------------+-----------------------------+-------------------------------+
| ``integer`` | ``1`` | ``Priority`` |
+-------------------+-----------------------------+-------------------------------+
| ``json`` | ``["hearts","spades"]`` | ``array<Suit>`` |
+-------------------+-----------------------------+-------------------------------+
| ``simple_array`` | ``hearts,spades`` | ``array<Suit>`` |
+-------------------+-----------------------------+-------------------------------+
Nullable Enums
~~~~~~~~~~~~~~
Enum columns can be nullable. When the database value is ``NULL``, Doctrine
preserves it as ``null`` without triggering any validation error.
.. code-block:: php
<?php
#[ORM\Column(type: 'string', nullable: true, enumType: Suit::class)]
private Suit|null $suit = null;
Default Values
~~~~~~~~~~~~~~
You can specify a database-level default using an enum case directly in the
column options:
.. code-block:: php
<?php
#[ORM\Column(options: ['default' => Suit::Hearts])]
public Suit $suit;
Using Enums in Queries
~~~~~~~~~~~~~~~~~~~~~~
Enum instances can be used directly as parameters in DQL, QueryBuilder, and
repository methods. Doctrine converts them to their scalar value automatically.
.. code-block:: php
<?php
// QueryBuilder
$qb = $em->createQueryBuilder();
$qb->select('c')
->from(Card::class, 'c')
->where('c.suit = :suit')
->setParameter('suit', Suit::Clubs);
// Repository
$cards = $em->getRepository(Card::class)->findBy(['suit' => Suit::Clubs]);
XML Mapping
~~~~~~~~~~~
When using XML mapping, the ``enum-type`` attribute is used on ``<field>``
elements:
.. code-block:: xml
<field name="suit" type="string" enum-type="App\Entity\Suit" />
.. _reference-mapping-types:
Doctrine Mapping Types
@@ -4,6 +4,8 @@ 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;
@@ -12,4 +14,7 @@ class Message
{
#[Column(options: ['default' => 'Hello World!'])]
private string $text;
#[Column(options: ['default' => new CurrentTimestamp()], insertable: false, updatable: false)]
private DateTime $createdAt;
}
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Player
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private int $id;
/** @var list<Suit> */
#[ORM\Column(type: 'json', enumType: Suit::class)]
private array $favouriteSuits = [];
/** @var list<Suit> */
#[ORM\Column(type: 'simple_array', enumType: Suit::class)]
private array $allowedSuits = [];
}
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Card
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private int $id;
#[ORM\Column(enumType: Suit::class)]
private Suit $suit;
}
+13
View File
@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Entity;
enum Suit: string
{
case Hearts = 'hearts';
case Diamonds = 'diamonds';
case Clubs = 'clubs';
case Spades = 'spades';
}
@@ -5,5 +5,12 @@
<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>
@@ -490,7 +490,7 @@ where you can generate an arbitrary join with the following syntax:
.. code-block:: php
<?php
$query = $em->createQuery('SELECT u FROM User u JOIN Banlist b WITH u.email = b.email');
$query = $em->createQuery('SELECT u FROM User u JOIN Banlist b ON u.email = b.email');
With an arbitrary join the result differs from the joins using a mapped property.
The result of an arbitrary join is an one dimensional array with a mix of the entity from the ``SELECT``
@@ -513,13 +513,15 @@ it loads all the related ``Banlist`` objects corresponding to this ``User``. Thi
when the DQL is switched to an arbitrary join.
.. note::
The differences between WHERE, WITH and HAVING clauses may be
The differences between WHERE, WITH, ON and HAVING clauses may be
confusing.
- WHERE is applied to the results of an entire query
- WITH is applied to a join as an additional condition. For
arbitrary joins (SELECT f, b FROM Foo f, Bar b WITH f.id = b.id)
the WITH is required, even if it is 1 = 1
- ON is applied to arbitrary joins as the join condition. For
arbitrary joins (SELECT f, b FROM Foo f, Bar b ON f.id = b.id)
the ON is required, even if it is 1 = 1. WITH is also
supported as alternative keyword for that case for BC reasons.
- WITH is applied to an association join as an additional condition.
- HAVING is applied to the results of a query after
aggregation (GROUP BY)
@@ -1409,8 +1411,7 @@ Result Cache API:
$query->setResultCacheDriver(new ApcCache());
$query->useResultCache(true)
->setResultCacheLifeTime(3600);
$query->enableResultCache(3600);
$result = $query->getResult(); // cache miss
@@ -1420,8 +1421,8 @@ Result Cache API:
$query->setResultCacheId('my_query_result');
$result = $query->getResult(); // saved in given result cache id.
// or call useResultCache() with all parameters:
$query->useResultCache(true, 3600, 'my_query_result');
// or call enableResultCache() with all parameters:
$query->enableResultCache(3600, 'my_query_result');
$result = $query->getResult(); // cache hit!
// Introspection
@@ -1609,16 +1610,16 @@ Identifiers
IdentificationVariable ::= identifier
/* Alias Identification declaration (the "u" of "FROM User u") */
AliasIdentificationVariable :: = identifier
AliasIdentificationVariable ::= identifier
/* identifier that must be a class name (the "User" of "FROM User u"), possibly as a fully qualified class name */
AbstractSchemaName ::= fully_qualified_name | identifier
/* Alias ResultVariable declaration (the "total" of "COUNT(*) AS total") */
AliasResultVariable = identifier
AliasResultVariable ::= identifier
/* ResultVariable identifier usage of mapped field aliases (the "total" of "COUNT(*) AS total") */
ResultVariable = identifier
ResultVariable ::= identifier
/* identifier that must be a field (the "name" of "u.name") */
/* This is responsible to know if the field exists in Object, no matter if it's a relation or a simple field */
@@ -1699,9 +1700,14 @@ From, Join and Index by
SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration
RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy]
Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" (JoinAssociationDeclaration | RangeVariableDeclaration) ["WITH" ConditionalExpression]
Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" (JoinAssociationDeclaration ["WITH" ConditionalExpression] | RangeVariableDeclaration [("ON" | "WITH") ConditionalExpression])
IndexBy ::= "INDEX" "BY" SingleValuedPathExpression
.. note::
Using the ``WITH`` keyword for the ``ConditionalExpression`` of a
``RangeVariableDeclaration`` is deprecated and will be removed in
ORM 4.0. Use the ``ON`` keyword instead.
Select Expressions
~~~~~~~~~~~~~~~~~~
@@ -1774,7 +1780,7 @@ Scalar and Type Expressions
.. code-block:: php
ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary | StateFieldPathExpression | BooleanPrimary | CaseExpression | InstanceOfExpression
ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | StateFieldPathExpression | BooleanPrimary | CaseExpression | InstanceOfExpression
StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")"
StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression
BooleanExpression ::= BooleanPrimary | "(" Subselect ")"
@@ -1800,14 +1806,14 @@ Case Expressions
.. code-block:: php
CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression
CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullIfExpression
GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
CaseOperand ::= StateFieldPathExpression
SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
Other Expressions
~~~~~~~~~~~~~~~~~
@@ -1832,7 +1838,7 @@ Functions
.. code-block:: php
FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDateTime
FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDatetime
FunctionsReturningNumerics ::=
"LENGTH" "(" StringPrimary ")" |
@@ -1845,7 +1851,7 @@ Functions
"BIT_AND" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |
"BIT_OR" "(" ArithmeticPrimary "," ArithmeticPrimary ")"
FunctionsReturningDateTime ::=
FunctionsReturningDatetime ::=
"CURRENT_DATE" |
"CURRENT_TIME" |
"CURRENT_TIMESTAMP" |
+16
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
+24 -7
View File
@@ -344,10 +344,10 @@ the Query object which can be retrieved from ``EntityManager#createQuery()``.
Executing a Query
^^^^^^^^^^^^^^^^^
The QueryBuilder is a builder object only - it has no means of actually
executing the Query. Additionally a set of parameters such as query hints
cannot be set on the QueryBuilder itself. This is why you always have to convert
a querybuilder instance into a Query object:
The QueryBuilder is only a builder object - it has no means of actually
executing the Query. Additional functionality, such as enabling the result cache,
cannot be set on the QueryBuilder itself. This is why you must always convert
a QueryBuilder instance into a Query object:
.. code-block:: php
@@ -355,9 +355,8 @@ a querybuilder instance into a Query object:
// $qb instanceof QueryBuilder
$query = $qb->getQuery();
// Set additional Query options
$query->setQueryHint('foo', 'bar');
$query->useResultCache('my_cache_id');
// Enable the result cache
$query->enableResultCache(3600, 'my_custom_id');
// Execute Query
$result = $query->getResult();
@@ -555,6 +554,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
^^^^^^^^^^^^^
+1 -1
View File
@@ -133,7 +133,7 @@ Caching mode
* Read Write cache employs locks before update/delete.
* Use if data needs to be updated.
* Slowest strategy.
* To use it a the cache region implementation must support locking.
* To use it the cache region implementation must support locking.
Built-in cached persisters
+5 -1
View File
@@ -22,7 +22,7 @@ have to register them yourself.
All the commands of the Doctrine Console require access to the
``EntityManager``. You have to inject it into the console application.
Here is an example of a the project-specific ``bin/doctrine`` binary.
Here is an example of a project-specific ``bin/doctrine`` binary.
.. code-block:: php
@@ -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:
@@ -114,7 +114,7 @@ functionally equivalent to the previously shown code looks as follows:
});
The difference between ``Connection#transactional($func)`` and
``EntityManager#transactional($func)`` is that the latter
``EntityManager#wrapInTransaction($func)`` is that the latter
abstraction flushes the ``EntityManager`` prior to transaction
commit and in case of an exception the ``EntityManager`` gets closed
in addition to the transaction rollback.
+64 -3
View File
@@ -93,14 +93,75 @@ And for querying you can use arrays to both DQL and EntityRepositories:
->setParameter(2, 2010)
->getSingleResult();
You can also use this entity in associations. Doctrine will then generate two foreign keys one for ``name``
and to ``year`` to the related entities.
.. note::
This example shows how you can nicely solve the requirement for existing
values before ``EntityManager#persist()``: By adding them as mandatory values for the constructor.
You can also use this entity in associations. Doctrine will then generate a composite foreign key
using the ``name`` and ``year`` columns on the related entities.
To define such an association, you need to use multiple join column mappings, one for each
column of the composite primary key:
.. configuration-block::
.. code-block:: attribute
<?php
namespace VehicleCatalogue\Model;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
#[Entity]
class Registration
{
#[Id, Column, GeneratedValue]
private int|null $id = null;
#[ManyToOne(targetEntity: Car::class)]
#[JoinColumn(name: 'car_name', referencedColumnName: 'name')]
#[JoinColumn(name: 'car_year', referencedColumnName: 'year')]
private Car|null $car = null;
}
.. code-block:: xml
<?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="VehicleCatalogue\Model\Registration">
<id name="id" type="integer">
<generator strategy="AUTO" />
</id>
<many-to-one field="car" target-entity="Car">
<join-column name="car_name" referenced-column-name="name" />
<join-column name="car_year" referenced-column-name="year" />
</many-to-one>
</entity>
</doctrine-mapping>
This generates the following SQL:
.. code-block:: sql
CREATE TABLE Registration (
id INT AUTO_INCREMENT NOT NULL,
car_name VARCHAR(255) DEFAULT NULL,
car_year INT DEFAULT NULL,
PRIMARY KEY(id)
) ENGINE = InnoDB;
ALTER TABLE Registration ADD FOREIGN KEY (car_name, car_year) REFERENCES Car(name, year);
Identity through foreign Entities
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+1 -2
View File
@@ -18,8 +18,7 @@ before. There are some prerequisites for the tutorial that have to be
installed:
- PHP (latest stable version)
- Composer Package Manager (\ `Install Composer
<https://getcomposer.org/doc/00-intro.md>`_)
- Composer Package Manager (\ `Install Composer <https://getcomposer.org/doc/00-intro.md>`_)
The code of this tutorial is `available on Github <https://github.com/doctrine/doctrine2-orm-tutorial>`_.
+13 -3
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"/>
+4 -28
View File
@@ -619,7 +619,7 @@ parameters:
path: src/EntityRepository.php
-
message: '#^Method Doctrine\\ORM\\EntityRepository\:\:matching\(\) should return Doctrine\\Common\\Collections\\AbstractLazyCollection\<int, T of object\>&Doctrine\\Common\\Collections\\Selectable\<int, T of object\> but returns Doctrine\\ORM\\LazyCriteriaCollection\<\(int\|string\), object\>\.$#'
message: '#^Method Doctrine\\ORM\\EntityRepository\:\:matching\(\) should return Doctrine\\Common\\Collections\\AbstractLazyCollection\<int, T of object\> but returns Doctrine\\ORM\\LazyCriteriaCollection\<\(int\|string\), object\>\.$#'
identifier: return.type
count: 1
path: src/EntityRepository.php
@@ -1441,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
@@ -1471,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
@@ -2557,7 +2557,7 @@ parameters:
path: src/Query/Exec/MultiTableUpdateExecutor.php
-
message: '#^Parameter \#3 \$types of method Doctrine\\DBAL\\Connection\:\:executeStatement\(\) expects array\<int\<0, max\>\|string, Doctrine\\DBAL\\ArrayParameterType\|Doctrine\\DBAL\\ParameterType\|Doctrine\\DBAL\\Types\\Type\|string\>, list\<Doctrine\\DBAL\\ArrayParameterType\|Doctrine\\DBAL\\ParameterType\|Doctrine\\DBAL\\Types\\Type\|int\|string\> given\.$#'
message: '#^Parameter \#3 \$types of method Doctrine\\DBAL\\Connection\:\:executeStatement\(\) expects array\<int\<0, max\>\|string, Doctrine\\DBAL\\ArrayParameterType\|Doctrine\\DBAL\\ParameterType\|Doctrine\\DBAL\\Types\\Type\|string\>, list\<Doctrine\\DBAL\\ArrayParameterType\:\:ASCII\|Doctrine\\DBAL\\ArrayParameterType\:\:BINARY\|Doctrine\\DBAL\\ArrayParameterType\:\:INTEGER\|Doctrine\\DBAL\\ArrayParameterType\:\:STRING\|Doctrine\\DBAL\\ParameterType\:\:ASCII\|Doctrine\\DBAL\\ParameterType\:\:BINARY\|Doctrine\\DBAL\\ParameterType\:\:BOOLEAN\|Doctrine\\DBAL\\ParameterType\:\:INTEGER\|Doctrine\\DBAL\\ParameterType\:\:LARGE_OBJECT\|Doctrine\\DBAL\\ParameterType\:\:NULL\|Doctrine\\DBAL\\ParameterType\:\:STRING\|Doctrine\\DBAL\\Types\\Type\|int\|string\> given\.$#'
identifier: argument.type
count: 1
path: src/Query/Exec/MultiTableUpdateExecutor.php
@@ -2580,36 +2580,12 @@ parameters:
count: 1
path: src/Query/Exec/SingleTableDeleteUpdateExecutor.php
-
message: '#^PHPDoc type array\<string\> of property Doctrine\\ORM\\Query\\Expr\\Andx\:\:\$allowedClasses is not covariant with PHPDoc type list\<class\-string\> of overridden property Doctrine\\ORM\\Query\\Expr\\Base\:\:\$allowedClasses\.$#'
identifier: property.phpDocType
count: 1
path: src/Query/Expr/Andx.php
-
message: '#^Method Doctrine\\ORM\\Query\\Expr\\Func\:\:getArguments\(\) should return list\<mixed\> but returns array\<mixed\>\.$#'
identifier: return.type
count: 1
path: src/Query/Expr/Func.php
-
message: '#^PHPDoc type array\<string\> of property Doctrine\\ORM\\Query\\Expr\\Orx\:\:\$allowedClasses is not covariant with PHPDoc type list\<class\-string\> of overridden property Doctrine\\ORM\\Query\\Expr\\Base\:\:\$allowedClasses\.$#'
identifier: property.phpDocType
count: 1
path: src/Query/Expr/Orx.php
-
message: '#^PHPDoc type array\<string\> of property Doctrine\\ORM\\Query\\Expr\\Select\:\:\$allowedClasses is not covariant with PHPDoc type list\<class\-string\> of overridden property Doctrine\\ORM\\Query\\Expr\\Base\:\:\$allowedClasses\.$#'
identifier: property.phpDocType
count: 1
path: src/Query/Expr/Select.php
-
message: '#^Property Doctrine\\ORM\\Query\\Filter\\SQLFilter\:\:\$parameters \(array\<string, array\{type\: string, value\: mixed, is_list\: bool\}\>\) does not accept non\-empty\-array\<string, array\{value\: mixed, type\: Doctrine\\DBAL\\ArrayParameterType\|Doctrine\\DBAL\\ParameterType\|int\|string, is_list\: bool\}\>\.$#'
identifier: assign.propertyType
count: 2
path: src/Query/Filter/SQLFilter.php
-
message: '#^Method Doctrine\\ORM\\Query\\ParameterTypeInferer\:\:inferType\(\) never returns int so it can be removed from the return type\.$#'
identifier: return.unusedType
+4
View File
@@ -12,6 +12,10 @@ parameters:
message: '~^Match expression does not handle remaining values:~'
path: src/Utility/PersisterHelper.php
# The return type is already narrow enough.
- '~^Method Doctrine\\ORM\\Query\\ParameterTypeInferer\:\:inferType\(\) never returns ''[a-z_]+'' so it can be removed from the return type\.$~'
- '~^Method Doctrine\\ORM\\Query\\ParameterTypeInferer\:\:inferType\(\) never returns Doctrine\\DBAL\\(?:Array)?ParameterType\:\:[A-Z_]+ so it can be removed from the return type\.$~'
# DBAL 4 compatibility
-
message: '~^Method Doctrine\\ORM\\Query\\AST\\Functions\\TrimFunction::getTrimMode\(\) never returns .* so it can be removed from the return type\.$~'
+1 -1
View File
@@ -1065,7 +1065,7 @@ abstract class AbstractQuery
}
/**
* Executes the query and returns a the resulting Statement object.
* Executes the query and returns the resulting Statement object.
*
* @return Result|int The executed database statement that holds
* the results, or an integer indicating how
+7 -1
View File
@@ -17,6 +17,7 @@ use Doctrine\ORM\UnitOfWork;
use Generator;
use LogicException;
use ReflectionClass;
use ReflectionEnum;
use function array_key_exists;
use function array_keys;
@@ -597,13 +598,18 @@ abstract class AbstractHydrator
*/
final protected function buildEnum(mixed $value, string $enumType): BackedEnum|array
{
$reflection = new ReflectionEnum($enumType);
$isIntBacked = $reflection->isBacked() && $reflection->getBackingType()->getName() === 'int';
if (is_array($value)) {
return array_map(
static fn ($value) => $enumType::from($value),
static fn ($value) => $enumType::from($isIntBacked ? (int) $value : $value),
$value,
);
}
$value = $isIntBacked ? (int) $value : $value;
return $enumType::from($value);
}
}
@@ -7,7 +7,6 @@ namespace Doctrine\ORM\Internal\Hydration;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\ORM\Exception\MultipleSelectorsFoundException;
use function array_column;
use function count;
/**
@@ -27,8 +26,6 @@ final class ScalarColumnHydrator extends AbstractHydrator
throw MultipleSelectorsFoundException::create($this->resultSetMapping()->fieldMappings);
}
$result = $this->statement()->fetchAllNumeric();
return array_column($result, 0);
return $this->statement()->fetchFirstColumn();
}
}
@@ -113,6 +113,10 @@ class AssociationBuilder
string|null $onDelete = null,
string|null $columnDef = null,
): static {
if ($this->mapping['id'] ?? false) {
$nullable = null;
}
$this->joinColumns[] = [
'name' => $columnName,
'referencedColumnName' => $referencedColumnName,
@@ -133,6 +137,9 @@ class AssociationBuilder
public function makePrimaryKey(): static
{
$this->mapping['id'] = true;
foreach ($this->joinColumns ?? [] as $i => $joinColumn) {
$this->joinColumns[$i]['nullable'] = null;
}
return $this;
}
@@ -24,6 +24,30 @@ class ManyToManyAssociationBuilder extends OneToManyAssociationBuilder
return $this;
}
/**
* Add Join Columns.
*
* @return $this
*/
public function addJoinColumn(
string $columnName,
string $referencedColumnName,
bool $nullable = true,
bool $unique = false,
string|null $onDelete = null,
string|null $columnDef = null,
): static {
$this->joinColumns[] = [
'name' => $columnName,
'referencedColumnName' => $referencedColumnName,
'unique' => $unique,
'onDelete' => $onDelete,
'columnDefinition' => $columnDef,
];
return $this;
}
/**
* Adds Inverse Join Columns.
*
@@ -40,7 +64,6 @@ class ManyToManyAssociationBuilder extends OneToManyAssociationBuilder
$this->inverseJoinColumns[] = [
'name' => $columnName,
'referencedColumnName' => $referencedColumnName,
'nullable' => $nullable,
'unique' => $unique,
'onDelete' => $onDelete,
'columnDefinition' => $columnDef,
+30 -4
View File
@@ -546,7 +546,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
*/
public LegacyReflectionFields|array $reflFields = [];
/** @var array<string, PropertyAccessors\PropertyAccessor> */
/** @var array<string, PropertyAccessor> */
public array $propertyAccessors = [];
private InstantiatorInterface|null $instantiator = null;
@@ -584,7 +584,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
/**
* Gets the ReflectionProperties of the mapped class.
*
* @return PropertyAccessor[] An array of PropertyAccessor instances.
* @return array<string, PropertyAccessor> An array of PropertyAccessor instances by name.
*/
public function getPropertyAccessors(): array
{
@@ -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']);
}
+10 -4
View File
@@ -10,6 +10,7 @@ use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata;
use Doctrine\Persistence\Mapping\Driver\ClassLocator;
use Doctrine\Persistence\Mapping\Driver\ColocatedMappingDriver;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use InvalidArgumentException;
@@ -35,10 +36,10 @@ class AttributeDriver implements MappingDriver
private readonly AttributeReader $reader;
/**
* @param array<string> $paths
* @param true $reportFieldsWhereDeclared no-op, to be removed in 4.0
* @param string[]|ClassLocator $paths a ClassLocator, or an array of directories.
* @param true $reportFieldsWhereDeclared no-op, to be removed in 4.0
*/
public function __construct(array $paths, bool $reportFieldsWhereDeclared = true)
public function __construct(array|ClassLocator $paths, bool $reportFieldsWhereDeclared = true)
{
if (! $reportFieldsWhereDeclared) {
throw new InvalidArgumentException(sprintf(
@@ -48,7 +49,12 @@ class AttributeDriver implements MappingDriver
}
$this->reader = new AttributeReader();
$this->addPaths($paths);
if ($paths instanceof ClassLocator) {
$this->classLocator = $paths;
} else {
$this->addPaths($paths);
}
}
public function isTransient(string $className): bool
+47 -13
View File
@@ -4,8 +4,6 @@ declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Driver;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\MappingException;
@@ -18,10 +16,10 @@ use LogicException;
use SimpleXMLElement;
use function assert;
use function class_exists;
use function constant;
use function count;
use function defined;
use function enum_exists;
use function explode;
use function extension_loaded;
use function file_get_contents;
@@ -409,10 +407,7 @@ class XmlDriver extends FileDriver
if (isset($oneToManyElement->{'order-by'})) {
$orderBy = [];
foreach ($oneToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) {
$orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
? (string) $orderByField['direction']
// @phpstan-ignore classConstant.deprecated
: (enum_exists(Order::class) ? Order::Ascending->value : Criteria::ASC);
$orderBy[(string) $orderByField['name']] = (string) ($orderByField['direction'] ?? 'ASC');
}
$mapping['orderBy'] = $orderBy;
@@ -538,10 +533,7 @@ class XmlDriver extends FileDriver
if (isset($manyToManyElement->{'order-by'})) {
$orderBy = [];
foreach ($manyToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) {
$orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
? (string) $orderByField['direction']
// @phpstan-ignore classConstant.deprecated
: (enum_exists(Order::class) ? Order::Ascending->value : Criteria::ASC);
$orderBy[(string) $orderByField['name']] = (string) ($orderByField['direction'] ?? 'ASC');
}
$mapping['orderBy'] = $orderBy;
@@ -665,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;
}
@@ -693,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.
+4 -2
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
@@ -140,7 +142,7 @@ final class FieldMapping implements ArrayAccess
{
$serialized = ['type', 'fieldName', 'columnName'];
foreach (['nullable', 'notInsertable', 'notUpdatable', 'id', 'unique', 'version', 'quoted'] as $boolKey) {
foreach (['nullable', 'notInsertable', 'notUpdatable', 'id', 'unique', 'version', 'quoted', 'index'] as $boolKey) {
if ($this->$boolKey) {
$serialized[] = $boolKey;
}
+7 -2
View File
@@ -84,9 +84,14 @@ final class JoinTableMapping implements ArrayAccess
/** @return mixed[] */
public function toArray(): array
{
$array = (array) $this;
$array = (array) $this;
$toArray = static function (JoinColumnMapping $column) {
$array = (array) $column;
$toArray = static fn (JoinColumnMapping $column): array => (array) $column;
unset($array['nullable']);
return $array;
};
$array['joinColumns'] = array_map($toArray, $array['joinColumns']);
$array['inverseJoinColumns'] = array_map($toArray, $array['inverseJoinColumns']);
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Doctrine\Deprecations\Deprecation;
use function strtolower;
use function trim;
@@ -127,6 +129,20 @@ final class ManyToManyOwningSideMapping extends ToManyOwningSideMapping implemen
$mapping->joinTableColumns = [];
foreach ($mapping->joinTable->joinColumns as $joinColumn) {
if ($joinColumn->nullable !== null) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12126',
<<<'DEPRECATION'
Specifying the "nullable" attribute for join columns in many-to-many associations (here, %s::$%s) is a no-op.
The ORM will always set it to false.
Doing so is deprecated and will be an error in 4.0.
DEPRECATION,
$mapping->sourceEntity,
$mapping->fieldName,
);
}
$joinColumn->nullable = false;
if (empty($joinColumn->referencedColumnName)) {
@@ -152,6 +168,20 @@ final class ManyToManyOwningSideMapping extends ToManyOwningSideMapping implemen
}
foreach ($mapping->joinTable->inverseJoinColumns as $inverseJoinColumn) {
if ($inverseJoinColumn->nullable !== null) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12126',
<<<'DEPRECATION'
Specifying the "nullable" attribute for join columns in many-to-many associations (here, %s::$%s) is a no-op.
The ORM will always set it to false.
Doing so is deprecated and will be an error in 4.0.
DEPRECATION,
$mapping->targetEntity,
$mapping->fieldName,
);
}
$inverseJoinColumn->nullable = false;
if (empty($inverseJoinColumn->referencedColumnName)) {
+19 -1
View File
@@ -179,7 +179,7 @@ class MappingException extends PersistenceMappingException implements ORMExcepti
public static function joinTableRequired(string $fieldName): self
{
return new self(sprintf("The mapping of field '%s' requires an the 'joinTable' attribute.", $fieldName));
return new self(sprintf("The mapping of field '%s' requires the 'joinTable' attribute.", $fieldName));
}
/**
@@ -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.
@@ -10,6 +10,8 @@ use ReflectionProperty;
use function sprintf;
use const PHP_VERSION_ID;
/** @internal */
class ReadonlyAccessor implements PropertyAccessor
{
@@ -26,7 +28,12 @@ class ReadonlyAccessor implements PropertyAccessor
public function setValue(object $object, mixed $value): void
{
if (! $this->reflectionProperty->isInitialized($object)) {
/* For lazy properties, skip the isInitialized() check
because it would trigger the initialization of the whole object. */
if (
PHP_VERSION_ID >= 80400 && $this->reflectionProperty->isLazy($object)
|| ! $this->reflectionProperty->isInitialized($object)
) {
$this->parent->setValue($object, $value);
return;
+21 -1
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Doctrine\Deprecations\Deprecation;
use RuntimeException;
use function array_flip;
@@ -131,6 +132,20 @@ abstract class ToOneOwningSideMapping extends OwningSideMapping implements ToOne
foreach ($mapping->joinColumns as $joinColumn) {
if ($mapping->id) {
if ($joinColumn->nullable !== null) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12126',
<<<'DEPRECATION'
Specifying the "nullable" attribute for join columns in to-one associations (here, %s::$%s) that are part of the identifier is a no-op.
The ORM will always set it to false.
Doing so is deprecated and will be an error in 4.0.
DEPRECATION,
$mapping->sourceEntity,
$mapping->fieldName,
);
}
$joinColumn->nullable = false;
} elseif ($joinColumn->nullable === null) {
$joinColumn->nullable = true;
@@ -200,7 +215,12 @@ abstract class ToOneOwningSideMapping extends OwningSideMapping implements ToOne
$joinColumns = [];
foreach ($array['joinColumns'] as $column) {
$joinColumns[] = (array) $column;
$columnArray = (array) $column;
if ($this->id) {
unset($columnArray['nullable']);
}
$joinColumns[] = $columnArray;
}
$array['joinColumns'] = $joinColumns;
+5 -4
View File
@@ -7,6 +7,7 @@ namespace Doctrine\ORM;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Doctrine\ORM\Mapping\Driver\XmlDriver;
use Doctrine\Persistence\Mapping\Driver\ClassLocator;
use Psr\Cache\CacheItemPoolInterface;
use Redis;
use RuntimeException;
@@ -28,10 +29,10 @@ final class ORMSetup
/**
* Creates a configuration with an attribute metadata driver.
*
* @param string[] $paths
* @param string[]|ClassLocator $paths
*/
public static function createAttributeMetadataConfiguration(
array $paths,
array|ClassLocator $paths,
bool $isDevMode = false,
string|null $proxyDir = null,
CacheItemPoolInterface|null $cache = null,
@@ -55,10 +56,10 @@ final class ORMSetup
/**
* Creates a configuration with an attribute metadata driver.
*
* @param string[] $paths
* @param string[]|ClassLocator $paths
*/
public static function createAttributeMetadataConfig(
array $paths,
array|ClassLocator $paths,
bool $isDevMode = false,
string|null $cacheNamespaceSeed = null,
CacheItemPoolInterface|null $cache = null,
@@ -40,13 +40,21 @@ abstract class AbstractEntityInheritancePersister extends BasicEntityPersister
{
$tableAlias = $alias === 'r' ? '' : $alias;
$fieldMapping = $class->fieldMappings[$field];
$columnAlias = $this->getSQLColumnAlias($fieldMapping->columnName);
$sql = sprintf(
'%s.%s',
$this->getSQLTableAlias($class->name, $tableAlias),
$this->quoteStrategy->getColumnName($field, $class, $this->platform),
);
$columnAlias = null;
if ($this->currentPersisterContext->rsm->hasColumnAliasByField($alias, $field)) {
$columnAlias = $this->currentPersisterContext->rsm->getColumnAliasByField($alias, $field);
}
if ($columnAlias === null) {
$columnAlias = $this->getSQLColumnAlias($fieldMapping->columnName);
}
$this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field, $class->name);
$type = Type::getType($fieldMapping->type);
+10
View File
@@ -6,18 +6,28 @@ namespace Doctrine\ORM\Query\Exec;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Result;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Query\AST\SelectStatement;
use Doctrine\ORM\Query\SqlWalker;
/**
* Executor that executes the SQL statement for simple DQL SELECT statements.
*
* @deprecated This class is no longer needed by the ORM and will be removed in 4.0.
*
* @link www.doctrine-project.org
*/
class SingleSelectExecutor extends AbstractSqlExecutor
{
public function __construct(SelectStatement $AST, SqlWalker $sqlWalker)
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11188/',
'The %s is no longer needed by the ORM and will be removed in 4.0',
self::class,
);
$this->sqlStatements = $sqlWalker->walkSelectStatement($AST);
}
+3 -1
View File
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Doctrine\ORM\Query\Expr;
use Stringable;
/**
* Expression class for building DQL and parts.
*
@@ -13,7 +15,7 @@ class Andx extends Composite
{
protected string $separator = ' AND ';
/** @var string[] */
/** @var list<class-string<Stringable>> */
protected array $allowedClasses = [
Comparison::class,
Func::class,
+6 -2
View File
@@ -13,6 +13,7 @@ use function get_debug_type;
use function implode;
use function in_array;
use function is_array;
use function is_object;
use function is_string;
use function sprintf;
@@ -27,7 +28,7 @@ abstract class Base implements Stringable
protected string $separator = ', ';
protected string $postSeparator = ')';
/** @var list<class-string> */
/** @var list<class-string<Stringable>> */
protected array $allowedClasses = [];
/** @var list<string|Stringable> */
@@ -58,6 +59,8 @@ abstract class Base implements Stringable
}
/**
* @param string|Stringable|null $arg
*
* @return $this
*
* @throws InvalidArgumentException
@@ -66,7 +69,8 @@ abstract class Base implements Stringable
{
if ($arg !== null && (! $arg instanceof self || $arg->count() > 0)) {
// If we decide to keep Expr\Base instances, we can use this check
if (! is_string($arg) && ! in_array($arg::class, $this->allowedClasses, true)) {
// @phpstan-ignore function.alreadyNarrowedType (input validation)
if (! is_string($arg) && ! (is_object($arg) && in_array($arg::class, $this->allowedClasses, true))) {
throw new InvalidArgumentException(sprintf(
"Expression of type '%s' not allowed in this context.",
get_debug_type($arg),
+3 -1
View File
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Doctrine\ORM\Query\Expr;
use Stringable;
/**
* Expression class for building DQL OR clauses.
*
@@ -13,7 +15,7 @@ class Orx extends Composite
{
protected string $separator = ' OR ';
/** @var string[] */
/** @var list<class-string<Stringable>> */
protected array $allowedClasses = [
Comparison::class,
Func::class,
+3 -1
View File
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Doctrine\ORM\Query\Expr;
use Stringable;
/**
* Expression class for building DQL select statements.
*
@@ -14,7 +16,7 @@ class Select extends Base
protected string $preSeparator = '';
protected string $postSeparator = '';
/** @var string[] */
/** @var list<class-string<Stringable>> */
protected array $allowedClasses = [Func::class];
/** @phpstan-var list<string|Func> */
+20
View File
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Query\Filter;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\ParameterType;
/** @internal */
final class Parameter
{
/** @param ParameterType::*|ArrayParameterType::*|string $type */
public function __construct(
public readonly mixed $value,
public readonly ParameterType|ArrayParameterType|int|string $type,
public readonly bool $isList,
) {
}
}
+19 -12
View File
@@ -29,7 +29,7 @@ abstract class SQLFilter implements Stringable
/**
* Parameters for the filter.
*
* @phpstan-var array<string,array{type: string, value: mixed, is_list: bool}>
* @phpstan-var array<string, Parameter>
*/
private array $parameters = [];
@@ -49,7 +49,7 @@ abstract class SQLFilter implements Stringable
*/
final public function setParameterList(string $name, array $values, string $type = Types::STRING): static
{
$this->parameters[$name] = ['value' => $values, 'type' => $type, 'is_list' => true];
$this->parameters[$name] = new Parameter(value: $values, type: $type, isList: true);
// Keep the parameters sorted for the hash
ksort($this->parameters);
@@ -71,11 +71,11 @@ abstract class SQLFilter implements Stringable
*/
final public function setParameter(string $name, mixed $value, string|null $type = null): static
{
if ($type === null) {
$type = ParameterTypeInferer::inferType($value);
}
$this->parameters[$name] = ['value' => $value, 'type' => $type, 'is_list' => false];
$this->parameters[$name] = new Parameter(
value: $value,
type: $type ?? ParameterTypeInferer::inferType($value),
isList: false,
);
// Keep the parameters sorted for the hash
ksort($this->parameters);
@@ -102,11 +102,11 @@ abstract class SQLFilter implements Stringable
throw new InvalidArgumentException("Parameter '" . $name . "' does not exist.");
}
if ($this->parameters[$name]['is_list']) {
if ($this->parameters[$name]->isList) {
throw FilterException::cannotConvertListParameterIntoSingleValue($name);
}
return $this->em->getConnection()->quote((string) $this->parameters[$name]['value']);
return $this->em->getConnection()->quote((string) $this->parameters[$name]->value);
}
/**
@@ -124,7 +124,7 @@ abstract class SQLFilter implements Stringable
throw new InvalidArgumentException("Parameter '" . $name . "' does not exist.");
}
if ($this->parameters[$name]['is_list'] === false) {
if (! $this->parameters[$name]->isList) {
throw FilterException::cannotConvertSingleParameterIntoListValue($name);
}
@@ -133,7 +133,7 @@ abstract class SQLFilter implements Stringable
$quoted = array_map(
static fn (mixed $value): string => $connection->quote((string) $value),
$param['value'],
$param->value,
);
return implode(',', $quoted);
@@ -152,7 +152,14 @@ abstract class SQLFilter implements Stringable
*/
final public function __toString(): string
{
return serialize($this->parameters);
return serialize(array_map(
static fn (Parameter $value): array => [
'value' => $value->value,
'type' => $value->type,
'is_list' => $value->isList,
],
$this->parameters,
));
}
/**
+3 -3
View File
@@ -25,9 +25,9 @@ use function is_int;
final class ParameterTypeInferer
{
/**
* Infers type of a given value, returning a compatible constant:
* - Type (\Doctrine\DBAL\Types\Type::*)
* - Connection (\Doctrine\DBAL\Connection::PARAM_*)
* Infers the type of a given value
*
* @return ParameterType::*|ArrayParameterType::*|Types::*
*/
public static function inferType(mixed $value): ParameterType|ArrayParameterType|int|string
{
+28 -19
View File
@@ -1609,8 +1609,7 @@ final class Parser
/**
* Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN"
* (JoinAssociationDeclaration | RangeVariableDeclaration)
* ["WITH" ConditionalExpression]
* (JoinAssociationDeclaration ["WITH" ConditionalExpression] | RangeVariableDeclaration [("ON" | "WITH") ConditionalExpression])
*/
public function Join(): AST\Join
{
@@ -1644,21 +1643,31 @@ final class Parser
$next = $this->lexer->glimpse();
assert($next !== null);
$joinDeclaration = $next->type === TokenType::T_DOT ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration();
$adhocConditions = $this->lexer->isNextToken(TokenType::T_WITH);
$join = new AST\Join($joinType, $joinDeclaration);
$conditionalExpression = null;
// Describe non-root join declaration
if ($joinDeclaration instanceof AST\RangeVariableDeclaration) {
if ($next->type === TokenType::T_DOT) {
$joinDeclaration = $this->JoinAssociationDeclaration();
if ($this->lexer->isNextToken(TokenType::T_WITH)) {
$this->match(TokenType::T_WITH);
$conditionalExpression = $this->ConditionalExpression();
}
} else {
$joinDeclaration = $this->RangeVariableDeclaration();
$joinDeclaration->isRoot = false;
if ($this->lexer->isNextToken(TokenType::T_ON)) {
$this->match(TokenType::T_ON);
$conditionalExpression = $this->ConditionalExpression();
} elseif ($this->lexer->isNextToken(TokenType::T_WITH)) {
$this->match(TokenType::T_WITH);
$conditionalExpression = $this->ConditionalExpression();
Deprecation::trigger('doctrine/orm', 'https://github.com/doctrine/orm/issues/12192', 'Using WITH for the join condition of arbitrary joins is deprecated. Use ON instead.');
}
}
// Check for ad-hoc Join conditions
if ($adhocConditions) {
$this->match(TokenType::T_WITH);
$join->conditionalExpression = $this->ConditionalExpression();
}
$join = new AST\Join($joinType, $joinDeclaration);
$join->conditionalExpression = $conditionalExpression;
return $join;
}
@@ -1928,7 +1937,7 @@ final class Parser
}
/**
* ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary |
* ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary |
* StateFieldPathExpression | BooleanPrimary | CaseExpression |
* InstanceOfExpression
*
@@ -2002,14 +2011,14 @@ final class Parser
}
/**
* CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression
* CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullIfExpression
* GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
* WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
* SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
* CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
* CaseOperand ::= StateFieldPathExpression
* SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
* CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
* NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
* NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
*
* @return mixed One of the possible expressions or subexpressions.
*/
@@ -2107,7 +2116,7 @@ final class Parser
/**
* SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
* CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
* CaseOperand ::= StateFieldPathExpression
*/
public function SimpleCaseExpression(): AST\SimpleCaseExpression
{
@@ -3426,7 +3435,7 @@ final class Parser
}
/**
* FunctionsReturningDateTime ::=
* FunctionsReturningDatetime ::=
* "CURRENT_DATE" |
* "CURRENT_TIME" |
* "CURRENT_TIMESTAMP" |
+19 -2
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Query;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
use Doctrine\ORM\Query\Exec\SqlFinalizer;
@@ -71,20 +72,36 @@ class ParserResult
/**
* Sets the SQL executor that should be used for this ParserResult.
*
* @deprecated
* @deprecated The SqlExecutor will be removed from ParserResult in 4.0. Provide a SqlFinalizer instead that can create the executor.
*/
public function setSqlExecutor(AbstractSqlExecutor $executor): void
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11188',
'The SqlExecutor will be removed from %s in 4.0. Provide a %s instead that can create the executor.',
self::class,
SqlFinalizer::class,
);
$this->sqlExecutor = $executor;
}
/**
* Gets the SQL executor used by this ParserResult.
*
* @deprecated
* @deprecated The SqlExecutor will be removed from ParserResult in 4.0. Provide a SqlFinalizer instead that can create the executor.
*/
public function getSqlExecutor(): AbstractSqlExecutor
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11188',
'The SqlExecutor will be removed from %s in 4.0. Provide a %s instead that can create the executor.',
self::class,
SqlFinalizer::class,
);
if ($this->sqlExecutor === null) {
throw new LogicException(sprintf(
'Executor not set yet. Call %s::setSqlExecutor() first.',
+4
View File
@@ -363,6 +363,10 @@ class ResultSetMapping
public function hasColumnAliasByField(string $alias, string $fieldName): bool
{
if (! isset($this->aliasMap[$alias])) {
return false;
}
$declaringClass = $this->aliasMap[$alias];
return isset($this->columnAliasMappings[$declaringClass][$alias][$fieldName]);
+9
View File
@@ -9,6 +9,7 @@ use Doctrine\DBAL\Connection;
use Doctrine\DBAL\LockMode;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\QuoteStrategy;
@@ -230,6 +231,14 @@ class SqlWalker
*/
public function getExecutor(AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement $statement): Exec\AbstractSqlExecutor
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11188/',
'Output walkers should implement %s. That way, the %s method is no longer needed and will be removed in 4.0',
OutputWalker::class,
__METHOD__,
);
return match (true) {
$statement instanceof AST\UpdateStatement => $this->createUpdateStatementExecutor($statement),
$statement instanceof AST\DeleteStatement => $this->createDeleteStatementExecutor($statement),
+1
View File
@@ -90,4 +90,5 @@ enum TokenType: int
case T_WHERE = 255;
case T_WITH = 256;
case T_NAMED = 257;
case T_ON = 258;
}
+71 -6
View File
@@ -8,6 +8,7 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\ParameterType;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Internal\NoUnknownNamedArguments;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\Query\Parameter;
@@ -116,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>.
*
@@ -207,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
{
@@ -287,6 +328,10 @@ class QueryBuilder implements Stringable
$query->setCacheRegion($this->cacheRegion);
}
foreach ($this->hints as $name => $value) {
$query->setHint($name, $value);
}
return $query;
}
@@ -305,8 +350,13 @@ class QueryBuilder implements Stringable
} else {
// Should never happen with correct joining order. Might be
// thoughtful to throw exception instead.
// @phpstan-ignore method.deprecated
$rootAlias = $this->getRootAlias();
$aliases = $this->getRootAliases();
if (! isset($aliases[0])) {
throw new RuntimeException('No alias was set before invoking getRootAlias().');
}
$rootAlias = $aliases[0];
}
$this->joinRootAliases[$alias] = $rootAlias;
@@ -541,6 +591,10 @@ class QueryBuilder implements Stringable
*/
public function setMaxResults(int|null $maxResults): static
{
if ($this->type === QueryType::Delete || $this->type === QueryType::Update) {
throw new RuntimeException('Setting a limit is not supported for delete or update queries.');
}
$this->maxResults = $maxResults;
return $this;
@@ -582,14 +636,25 @@ class QueryBuilder implements Stringable
$dqlPart = reset($dqlPart);
}
// This is introduced for backwards compatibility reasons.
// TODO: Remove for 3.0
if ($dqlPartName === 'join') {
$newDqlPart = [];
foreach ($dqlPart as $k => $v) {
// @phpstan-ignore method.deprecated
$k = is_numeric($k) ? $this->getRootAlias() : $k;
if (is_numeric($k)) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12051',
'Using numeric keys in %s for join parts is deprecated and will not be supported in 4.0. Use an associative array with the root alias as key instead.',
__METHOD__,
);
$aliases = $this->getRootAliases();
if (! isset($aliases[0])) {
throw new RuntimeException('No alias was set before invoking add().');
}
$k = $aliases[0];
}
$newDqlPart[$k] = $v;
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
}
@@ -10,6 +10,7 @@ use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\FieldMapping;
use Doctrine\Persistence\Mapping\MappingException;
use InvalidArgumentException;
use JsonException;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument;
@@ -52,9 +53,17 @@ final class MappingDescribeCommand extends AbstractEntityManagerCommand
protected function configure(): void
{
$this->setName('orm:mapping:describe')
->addArgument('entityName', InputArgument::REQUIRED, 'Full or partial name of entity')
->setDescription('Display information about mapped objects')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->addArgument('entityName', InputArgument::REQUIRED, 'Full or partial name of entity')
->setDescription('Display information about mapped objects')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->addOption(
'format',
null,
InputOption::VALUE_REQUIRED,
'Output format (text, json)',
MappingDescribeCommandFormat::TEXT->value,
array_map(static fn (MappingDescribeCommandFormat $format) => $format->value, MappingDescribeCommandFormat::cases()),
)
->setHelp(<<<'EOT'
The %command.full_name% command describes the metadata for the given full or partial entity class name.
@@ -63,6 +72,13 @@ The %command.full_name% command describes the metadata for the given full or par
Or:
<info>%command.full_name%</info> MyEntity
To output the metadata in JSON format, use the <info>--format</info> option:
<info>%command.full_name% My\Namespace\Entity\MyEntity --format=json</info>
To use a specific entity manager (e.g., for multi-DB projects), use the <info>--em</info> option:
<info>%command.full_name% My\Namespace\Entity\MyEntity --em=my_custom_entity_manager</info>
EOT);
}
@@ -70,9 +86,11 @@ EOT);
{
$ui = new SymfonyStyle($input, $output);
$format = MappingDescribeCommandFormat::from($input->getOption('format'));
$entityManager = $this->getEntityManager($input);
$this->displayEntity($input->getArgument('entityName'), $entityManager, $ui);
$this->displayEntity($input->getArgument('entityName'), $entityManager, $ui, $format);
return 0;
}
@@ -89,6 +107,10 @@ EOT);
$suggestions->suggestValues(array_values($entities));
}
if ($input->mustSuggestOptionValuesFor('format')) {
$suggestions->suggestValues(array_map(static fn (MappingDescribeCommandFormat $format) => $format->value, MappingDescribeCommandFormat::cases()));
}
}
/**
@@ -100,9 +122,47 @@ EOT);
string $entityName,
EntityManagerInterface $entityManager,
SymfonyStyle $ui,
MappingDescribeCommandFormat $format,
): void {
$metadata = $this->getClassMetadata($entityName, $entityManager);
if ($format === MappingDescribeCommandFormat::JSON) {
$ui->text(json_encode(
[
'name' => $metadata->name,
'rootEntityName' => $metadata->rootEntityName,
'customGeneratorDefinition' => $this->formatValueAsJson($metadata->customGeneratorDefinition),
'customRepositoryClassName' => $metadata->customRepositoryClassName,
'isMappedSuperclass' => $metadata->isMappedSuperclass,
'isEmbeddedClass' => $metadata->isEmbeddedClass,
'parentClasses' => $metadata->parentClasses,
'subClasses' => $metadata->subClasses,
'embeddedClasses' => $metadata->embeddedClasses,
'identifier' => $metadata->identifier,
'inheritanceType' => $metadata->inheritanceType,
'discriminatorColumn' => $this->formatValueAsJson($metadata->discriminatorColumn),
'discriminatorValue' => $metadata->discriminatorValue,
'discriminatorMap' => $metadata->discriminatorMap,
'generatorType' => $metadata->generatorType,
'table' => $this->formatValueAsJson($metadata->table),
'isIdentifierComposite' => $metadata->isIdentifierComposite,
'containsForeignIdentifier' => $metadata->containsForeignIdentifier,
'containsEnumIdentifier' => $metadata->containsEnumIdentifier,
'sequenceGeneratorDefinition' => $this->formatValueAsJson($metadata->sequenceGeneratorDefinition),
'changeTrackingPolicy' => $metadata->changeTrackingPolicy,
'isVersioned' => $metadata->isVersioned,
'versionField' => $metadata->versionField,
'isReadOnly' => $metadata->isReadOnly,
'entityListeners' => $metadata->entityListeners,
'associationMappings' => $this->formatMappingsAsJson($metadata->associationMappings),
'fieldMappings' => $this->formatMappingsAsJson($metadata->fieldMappings),
],
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR,
));
return;
}
$ui->table(
['Field', 'Value'],
array_merge(
@@ -240,6 +300,22 @@ EOT);
throw new InvalidArgumentException(sprintf('Do not know how to format value "%s"', print_r($value, true)));
}
/** @throws JsonException */
private function formatValueAsJson(mixed $value): mixed
{
if (is_object($value)) {
$value = (array) $value;
}
if (is_array($value)) {
foreach ($value as $k => $v) {
$value[$k] = $this->formatValueAsJson($v);
}
}
return $value;
}
/**
* Add the given label and value to the two column table output
*
@@ -281,6 +357,22 @@ EOT);
return $output;
}
/**
* @param array<string, FieldMapping|AssociationMapping> $propertyMappings
*
* @return array<string, mixed>
*/
private function formatMappingsAsJson(array $propertyMappings): array
{
$output = [];
foreach ($propertyMappings as $propertyName => $mapping) {
$output[$propertyName] = $this->formatValueAsJson((array) $mapping);
}
return $output;
}
/**
* Format the entity listeners
*
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
enum MappingDescribeCommandFormat: string
{
case TEXT = 'text';
case JSON = 'json';
}
+33
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,6 +508,16 @@ class SchemaTool
], true)
&& $options['default'] === $this->platform->getCurrentTimestampSQL()
) {
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();
}
@@ -512,6 +525,16 @@ class SchemaTool
in_array($mapping->type, [Types::TIME_MUTABLE, Types::TIME_IMMUTABLE], true)
&& $options['default'] === $this->platform->getCurrentTimeSQL()
) {
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();
}
@@ -519,6 +542,16 @@ class SchemaTool
in_array($mapping->type, [Types::DATE_MUTABLE, Types::DATE_IMMUTABLE], true)
&& $options['default'] === $this->platform->getCurrentDateSQL()
) {
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();
}
}
+5 -1
View File
@@ -35,6 +35,7 @@ use Doctrine\ORM\Internal\UnitOfWork\InsertBatch;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\Mapping\PropertyAccessors\ReadonlyAccessor;
use Doctrine\ORM\Mapping\ToManyInverseSideMapping;
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
use Doctrine\ORM\Persisters\Collection\ManyToManyPersister;
@@ -1176,7 +1177,10 @@ class UnitOfWork implements PropertyChangedListener
// Entity with this $oid after deletion treated as NEW, even if the $oid
// is obtained by a new entity because the old one went out of scope.
//$this->entityStates[$oid] = self::STATE_NEW;
if (! $class->isIdentifierNatural()) {
if (
! $class->isIdentifierNatural() &&
! $class->propertyAccessors[$class->identifier[0]] instanceof ReadonlyAccessor
) {
$class->propertyAccessors[$class->identifier[0]]->setValue($entity, null);
}
@@ -1,85 +0,0 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH12063Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(GH12063Association::class, GH12063Entity::class);
}
public function testLoadedAssociationWithBackedEnum(): void
{
$association = new GH12063Association();
$association->code = GH12063Code::One;
$this->_em->persist($association);
$this->_em->flush();
$this->_em->clear();
$entity = new GH12063Entity();
$entity->association = $this->_em->find(GH12063Association::class, GH12063Code::One);
$this->_em->persist($entity);
$this->_em->flush();
$this->assertNotNull($entity->id);
}
public function testProxyAssociationWithBackedEnum(): void
{
$association = new GH12063Association();
$association->code = GH12063Code::Two;
$this->_em->persist($association);
$this->_em->flush();
$this->_em->clear();
$entity = new GH12063Entity();
$entity->association = $this->_em->getReference(GH12063Association::class, GH12063Code::Two);
$this->_em->persist($entity);
$this->_em->flush();
$this->assertNotNull($entity->id);
}
}
enum GH12063Code: string
{
case One = 'one';
case Two = 'two';
}
#[Entity]
class GH12063Association
{
#[Id]
#[Column(length: 3)]
public GH12063Code $code;
}
#[Entity]
class GH12063Entity
{
#[Id]
#[Column]
#[GeneratedValue]
public int|null $id = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(referencedColumnName: 'code', options: ['length' => 3])]
public GH12063Association $association;
}
+8 -9
View File
@@ -13,14 +13,13 @@ use Doctrine\DBAL\Result;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Doctrine\ORM\Proxy\ProxyFactory;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\Tests\Mocks\ArrayResultFactory;
use Doctrine\Tests\Mocks\AttributeDriverFactory;
use Doctrine\Tests\TestUtil;
use function array_map;
use function realpath;
final class EntityManagerFactory
{
@@ -30,9 +29,9 @@ final class EntityManagerFactory
TestUtil::configureProxies($config);
$config->setAutoGenerateProxyClasses(ProxyFactory::AUTOGENERATE_EVAL);
$config->setMetadataDriverImpl(new AttributeDriver([
realpath(__DIR__ . '/Models/Cache'),
realpath(__DIR__ . '/Models/GeoNames'),
$config->setMetadataDriverImpl(AttributeDriverFactory::createAttributeDriver([
__DIR__ . '/../Tests/Models/Cache',
__DIR__ . '/../Tests/Models/GeoNames',
]));
$entityManager = new EntityManager(
@@ -55,10 +54,10 @@ final class EntityManagerFactory
TestUtil::configureProxies($config);
$config->setAutoGenerateProxyClasses(ProxyFactory::AUTOGENERATE_EVAL);
$config->setMetadataDriverImpl(new AttributeDriver([
realpath(__DIR__ . '/Models/Cache'),
realpath(__DIR__ . '/Models/Generic'),
realpath(__DIR__ . '/Models/GeoNames'),
$config->setMetadataDriverImpl(AttributeDriverFactory::createAttributeDriver([
__DIR__ . '/../Tests/Models/Cache',
__DIR__ . '/../Tests/Models/Generic',
__DIR__ . '/../Tests/Models/GeoNames',
]));
// A connection that doesn't really do anything
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Mocks;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Doctrine\Persistence\Mapping\Driver\ClassLocator;
use Doctrine\Persistence\Mapping\Driver\FileClassLocator;
use function interface_exists;
final class AttributeDriverFactory
{
/** @param list<string> $paths */
public static function createAttributeDriver(array $paths = []): AttributeDriver
{
if (! self::isClassLocatorSupported()) {
// Persistence < 4.1
return new AttributeDriver($paths);
}
// Persistence >= 4.1
$classLocator = FileClassLocator::createFromDirectories($paths);
return new AttributeDriver($classLocator);
}
/** Supported since doctrine/persistence >= 4.1 */
public static function isClassLocatorSupported(): bool
{
return interface_exists(ClassLocator::class);
}
}
+1 -2
View File
@@ -8,7 +8,6 @@ use Doctrine\Common\EventManager;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Doctrine\ORM\Proxy\ProxyFactory;
use Doctrine\ORM\UnitOfWork;
use Doctrine\Tests\TestUtil;
@@ -26,7 +25,7 @@ class EntityManagerMock extends EntityManager
if ($config === null) {
$config = new Configuration();
TestUtil::configureProxies($config);
$config->setMetadataDriverImpl(new AttributeDriver([]));
$config->setMetadataDriverImpl(AttributeDriverFactory::createAttributeDriver());
}
parent::__construct($conn, $config, $eventManager);
+2 -1
View File
@@ -6,13 +6,14 @@ namespace Doctrine\Tests\Models\DDC3579;
use Doctrine\ORM\Mapping\AssociationOverride;
use Doctrine\ORM\Mapping\AssociationOverrides;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\Entity;
#[Entity]
#[AssociationOverrides([new AssociationOverride(name: 'groups', inversedBy: 'admins')])]
class DDC3579Admin extends DDC3579User
{
public static function loadMetadata($metadata): void
public static function loadMetadata(ClassMetadata $metadata): void
{
$metadata->setAssociationOverride('groups', ['inversedBy' => 'admins']);
}
+1 -1
View File
@@ -58,7 +58,7 @@ class DDC3579User
return $this->groups;
}
public static function loadMetadata($metadata): void
public static function loadMetadata(ClassMetadata $metadata): void
{
$metadata->isMappedSuperclass = true;
+1 -1
View File
@@ -16,7 +16,7 @@ class GH10334Foo
{
#[Id]
#[ManyToOne(targetEntity: GH10334FooCollection::class, inversedBy: 'foos')]
#[JoinColumn(name: 'foo_collection_id', referencedColumnName: 'id', nullable: false)]
#[JoinColumn(name: 'foo_collection_id', referencedColumnName: 'id')]
#[GeneratedValue]
protected GH10334FooCollection $collection;
@@ -17,7 +17,7 @@ class InverseSide
/** Associative id (owning identifier) */
#[Id]
#[OneToOne(targetEntity: InverseSideIdTarget::class, inversedBy: 'inverseSide')]
#[JoinColumn(nullable: false, name: 'associativeId')]
#[JoinColumn(name: 'associativeId')]
public InverseSideIdTarget $associativeId;
#[OneToOne(targetEntity: OwningSide::class, mappedBy: 'inverse')]
@@ -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;
}
@@ -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;
}
+4 -3
View File
@@ -9,10 +9,10 @@ use Doctrine\Common\Collections\Expr\Comparison;
use Doctrine\DBAL\Types\EnumType;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\Query\Expr\Func;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\Tests\Mocks\AttributeDriverFactory;
use Doctrine\Tests\Models\DataTransferObjects\DtoWithArrayOfEnums;
use Doctrine\Tests\Models\DataTransferObjects\DtoWithEnum;
use Doctrine\Tests\Models\Enums\BookCategory;
@@ -35,7 +35,6 @@ use Generator;
use PHPUnit\Framework\Attributes\DataProvider;
use function class_exists;
use function dirname;
use function sprintf;
use function uniqid;
@@ -45,7 +44,9 @@ class EnumTest extends OrmFunctionalTestCase
{
parent::setUp();
$this->_em = $this->getEntityManager(null, new AttributeDriver([dirname(__DIR__, 2) . '/Models/Enums'], true));
$mappingDriver = AttributeDriverFactory::createAttributeDriver([__DIR__ . '/../../Models/Enums']);
$this->_em = $this->getEntityManager(null, $mappingDriver);
$this->_schemaTool = new SchemaTool($this->_em);
if ($this->isSecondLevelCacheEnabled) {
@@ -9,6 +9,7 @@ use Doctrine\DBAL\Connection;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Tests\Mocks\AttributeDriverFactory;
use Doctrine\Tests\ORM\Functional\Locking\Doctrine\ORM\Query;
use Doctrine\Tests\TestUtil;
use GearmanWorker;
@@ -116,8 +117,8 @@ class LockAgentWorker
TestUtil::configureProxies($config);
$config->setAutoGenerateProxyClasses(true);
$annotDriver = new AttributeDriver([__DIR__ . '/../../../Models/']);
$config->setMetadataDriverImpl($annotDriver);
$attributeDriver = AttributeDriverFactory::createAttributeDriver([__DIR__ . '/../../../Models']);
$config->setMetadataDriverImpl($attributeDriver);
$config->setMetadataCache(new ArrayAdapter());
$config->setQueryCache(new ArrayAdapter());
@@ -17,7 +17,6 @@ use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;
use function assert;
use function class_exists;
use function get_class;
/**
@@ -438,7 +437,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
$user = $this->_em->find($user::class, $user->id);
$criteria = Criteria::create(true)
->orderBy(['name' => class_exists(Order::class) ? Order::Ascending : Criteria::ASC]);
->orderBy(['name' => Order::Ascending]);
self::assertEquals(
['A', 'B', 'C', 'Developers_0'],
@@ -478,7 +477,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
$user = $this->_em->find($user::class, $user->id);
$criteria = Criteria::create(true)
->orderBy(['name' => class_exists(Order::class) ? Order::Ascending : Criteria::ASC]);
->orderBy(['name' => Order::Ascending]);
self::assertEquals(
['A', 'B', 'C'],
@@ -678,7 +678,7 @@ SQL,
public function testDifferentResultLengthsDoNotRequireExtraQueryCacheEntries(): void
{
$dql = 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id >= :id';
$dql = 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id >= :id ORDER BY u.id';
$query = $this->_em->createQuery($dql);
$query->setMaxResults(10);
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Closure;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\Exec\FinalizedSelectExecutor;
use Doctrine\ORM\Query\Exec\PreparedExecutorFinalizer;
@@ -25,6 +26,8 @@ use function unserialize;
class ParserResultSerializationTest extends OrmFunctionalTestCase
{
use VerifyDeprecations;
protected function setUp(): void
{
$this->useModelSet('company');
@@ -92,6 +95,8 @@ class ParserResultSerializationTest extends OrmFunctionalTestCase
$this->assertInstanceOf(ParserResult::class, $unserialized);
$this->assertInstanceOf(ResultSetMapping::class, $unserialized->getResultSetMapping());
$this->assertEquals(['name' => [0]], $unserialized->getParameterMappings());
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/11188');
$this->assertInstanceOf(SingleSelectExecutor::class, $unserialized->getSqlExecutor());
$this->assertIsString($unserialized->getSqlExecutor()->getSqlStatements());
}
@@ -7,6 +7,7 @@ namespace Doctrine\Tests\ORM\Functional;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
use Doctrine\ORM\Query\Exec\SqlFinalizer;
use Doctrine\ORM\Query\ParserResult;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Depends;
@@ -130,8 +131,11 @@ class QueryCacheTest extends OrmFunctionalTestCase
}
};
$sqlFinalizerMock = $this->createMock(SqlFinalizer::class);
$sqlFinalizerMock->method('createExecutor')->with($query)->willReturn($sqlExecutorStub);
$parserResultMock = new ParserResult();
$parserResultMock->setSqlExecutor($sqlExecutorStub);
$parserResultMock->setSqlFinalizer($sqlFinalizerMock);
$cache = $this->createMock(CacheItemPoolInterface::class);
+3 -3
View File
@@ -390,7 +390,7 @@ class QueryTest extends OrmFunctionalTestCase
$this->_em->flush();
$this->_em->clear();
$query = $this->_em->createQuery('select a, u from ' . CmsArticle::class . ' a JOIN ' . CmsUser::class . ' u WITH a.user = u');
$query = $this->_em->createQuery('select a, u from ' . CmsArticle::class . ' a JOIN ' . CmsUser::class . ' u ON a.user = u');
$result = iterator_to_array($query->toIterable());
@@ -1067,7 +1067,7 @@ class QueryTest extends OrmFunctionalTestCase
$query = $this->_em->createQuery('
SELECT u, p
FROM Doctrine\Tests\Models\CMS\CmsUser u
INNER JOIN Doctrine\Tests\Models\CMS\CmsPhonenumber p WITH u = p.user
INNER JOIN Doctrine\Tests\Models\CMS\CmsPhonenumber p ON u = p.user
');
$users = $query->execute();
@@ -1100,7 +1100,7 @@ class QueryTest extends OrmFunctionalTestCase
$query = $this->_em->createQuery('
SELECT u, p
FROM Doctrine\Tests\Models\CMS\CmsUser u
LEFT JOIN Doctrine\Tests\Models\CMS\CmsPhonenumber p WITH u = p.user
LEFT JOIN Doctrine\Tests\Models\CMS\CmsPhonenumber p ON u = p.user
');
$users = $query->execute();
@@ -4,16 +4,14 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\Tests\Mocks\AttributeDriverFactory;
use Doctrine\Tests\Models\ReadonlyProperties\Author;
use Doctrine\Tests\Models\ReadonlyProperties\Book;
use Doctrine\Tests\Models\ReadonlyProperties\SimpleBook;
use Doctrine\Tests\OrmFunctionalTestCase;
use Doctrine\Tests\TestUtil;
use function dirname;
class ReadonlyPropertiesTest extends OrmFunctionalTestCase
{
protected function setUp(): void
@@ -22,10 +20,9 @@ class ReadonlyPropertiesTest extends OrmFunctionalTestCase
static::$sharedConn = TestUtil::getConnection();
}
$this->_em = $this->getEntityManager(null, new AttributeDriver(
[dirname(__DIR__, 2) . '/Models/ReadonlyProperties'],
true,
));
$attributeDriver = AttributeDriverFactory::createAttributeDriver([__DIR__ . '/../../Models/ReadonlyProperties']);
$this->_em = $this->getEntityManager(null, $attributeDriver);
$this->_schemaTool = new SchemaTool($this->_em);
parent::setUp();
@@ -102,7 +102,7 @@ class DDC1209Two
public function __construct(
#[Id]
#[ManyToOne(targetEntity: 'DDC1209One')]
#[JoinColumn(referencedColumnName: 'id', nullable: false)]
#[JoinColumn(referencedColumnName: 'id')]
private DDC1209One $future1,
) {
$this->startingDatetime = new DateTime2();
@@ -50,7 +50,7 @@ class DDC1225TestEntity1
{
#[Id]
#[ManyToOne(targetEntity: 'Doctrine\Tests\ORM\Functional\Ticket\DDC1225TestEntity2')]
#[JoinColumn(name: 'test_entity2_id', referencedColumnName: 'id', nullable: false)]
#[JoinColumn(name: 'test_entity2_id', referencedColumnName: 'id')]
private DDC1225TestEntity2|null $testEntity2 = null;
public function setTestEntity2(DDC1225TestEntity2 $testEntity2): void
@@ -50,7 +50,7 @@ class DDC1452Test extends OrmFunctionalTestCase
$this->_em->flush();
$this->_em->clear();
$dql = 'SELECT a, b, ba FROM ' . __NAMESPACE__ . '\DDC1452EntityA AS a LEFT JOIN a.entitiesB AS b LEFT JOIN b.entityATo AS ba';
$dql = 'SELECT a, b, ba FROM ' . __NAMESPACE__ . '\DDC1452EntityA AS a LEFT JOIN a.entitiesB AS b LEFT JOIN b.entityATo AS ba ORDER BY a.id';
$results = $this->_em->createQuery($dql)->setMaxResults(1)->getResult();
self::assertSame($results[0], $results[0]->entitiesB[0]->entityAFrom);
@@ -80,7 +80,7 @@ class MyEntity1
public function __construct(
#[Id]
#[OneToOne(targetEntity: 'MyEntity2')]
#[JoinColumn(name: 'entity2_id', referencedColumnName: 'id', nullable: false)]
#[JoinColumn(name: 'entity2_id', referencedColumnName: 'id')]
private MyEntity2 $entity2,
) {
}
@@ -49,7 +49,7 @@ class DDC2359Test extends TestCase
$entityManager->expects(self::any())->method('getConnection')->willReturn($connection);
$entityManager
->method('getEventManager')
->willReturn($this->createMock(EventManager::class));
->willReturn(new EventManager());
$metadataFactory->method('newClassMetadataInstance')->willReturn($mockMetadata);
$metadataFactory->expects(self::once())->method('wakeupReflection');
@@ -119,10 +119,10 @@ class DDC2575A
public function __construct(
#[Id]
#[OneToOne(targetEntity: 'DDC2575Root', inversedBy: 'aRelation')]
#[JoinColumn(name: 'root_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
#[JoinColumn(name: 'root_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
public DDC2575Root $rootRelation,
#[ManyToOne(targetEntity: 'DDC2575B')]
#[JoinColumn(name: 'b_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
#[JoinColumn(name: 'b_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
public DDC2575B $bRelation,
) {
}
@@ -31,7 +31,7 @@ class DDC3042Test extends OrmFunctionalTestCase
$this
->_em
->createQuery(
'SELECT f, b FROM ' . __NAMESPACE__ . '\DDC3042Foo f JOIN ' . __NAMESPACE__ . '\DDC3042Bar b WITH 1 = 1',
'SELECT f, b FROM ' . __NAMESPACE__ . '\DDC3042Foo f JOIN ' . __NAMESPACE__ . '\DDC3042Bar b ON 1 = 1',
)
->getSQL(),
'field_11',
@@ -34,7 +34,7 @@ class GH10387Test extends OrmTestCase
{
yield 'hierarchy with Entity classes only' => [[GH10387EntitiesOnlyRoot::class, GH10387EntitiesOnlyMiddle::class, GH10387EntitiesOnlyLeaf::class]];
yield 'MappedSuperclass in the middle of the hierarchy' => [[GH10387MappedSuperclassRoot::class, GH10387MappedSuperclassMiddle::class, GH10387MappedSuperclassLeaf::class]];
yield 'abstract entity the the root and in the middle of the hierarchy' => [[GH10387AbstractEntitiesRoot::class, GH10387AbstractEntitiesMiddle::class, GH10387AbstractEntitiesLeaf::class]];
yield 'abstract entity at the root and in the middle of the hierarchy' => [[GH10387AbstractEntitiesRoot::class, GH10387AbstractEntitiesMiddle::class, GH10387AbstractEntitiesLeaf::class]];
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH12166;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH12166Test extends OrmFunctionalTestCase
{
public function testProxyWithReadonlyIdIsNotInitializedImmediately(): void
{
$this->createSchemaForModels(LazyEntityWithReadonlyId::class);
$this->_em->persist(new LazyEntityWithReadonlyId(123, 'Test Name'));
$this->_em->flush();
$this->_em->clear();
$proxy = $this->_em->getReference(LazyEntityWithReadonlyId::class, 123);
$reflClass = $this->_em->getClassMetadata(LazyEntityWithReadonlyId::class)->reflClass;
self::assertTrue(
$this->isUninitializedObject($proxy),
'Proxy should remain uninitialized after creation',
);
$id = $proxy->getId();
self::assertSame(123, $id);
self::assertTrue(
$this->isUninitializedObject($proxy),
'Proxy should remain uninitialized after accessing ID',
);
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH12166;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\Table;
#[Entity]
#[Table(name: 'gh12166_lazy_entity')]
class LazyEntityWithReadonlyId
{
#[Column(type: 'integer')]
#[Id]
private readonly int $id;
#[Column(type: 'string')]
private string $name;
public function __construct(int $id, string $name)
{
$this->id = $id;
$this->name = $name;
}
public function getId(): int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
}

Some files were not shown because too many files have changed in this diff Show More