Compare commits

..

45 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
35 changed files with 580 additions and 149 deletions
+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.1.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.1.0"
uses: "doctrine/.github/.github/workflows/composer-lint.yml@15.0.0"
+10 -10
View File
@@ -116,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 }}"
@@ -150,7 +150,7 @@ jobs:
ENABLE_NATIVE_LAZY_OBJECTS: ${{ matrix.native_lazy }}
- name: "Upload coverage file"
uses: "actions/upload-artifact@v6"
uses: "actions/upload-artifact@v7"
with:
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-${{ matrix.stability }}-${{ matrix.native_lazy }}-coverage"
path: "coverage*.xml"
@@ -257,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+"
@@ -265,7 +265,7 @@ jobs:
run: "vendor/bin/phpunit -c ci/github/phpunit/pdo_pgsql.xml --coverage-clover=coverage.xml"
- name: "Upload coverage file"
uses: "actions/upload-artifact@v6"
uses: "actions/upload-artifact@v7"
with:
name: "${{ github.job }}-${{ matrix.postgres-version }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.extension }}-coverage"
path: "coverage.xml"
@@ -331,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+"
@@ -339,7 +339,7 @@ jobs:
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --coverage-clover=coverage.xml"
- name: "Upload coverage file"
uses: "actions/upload-artifact@v6"
uses: "actions/upload-artifact@v7"
with:
name: "${{ github.job }}-${{ matrix.mariadb-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
path: "coverage.xml"
@@ -413,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+"
@@ -442,7 +442,7 @@ jobs:
ENABLE_SECOND_LEVEL_CACHE: 1
- name: "Upload coverage files"
uses: "actions/upload-artifact@v6"
uses: "actions/upload-artifact@v7"
with:
name: "${{ github.job }}-${{ matrix.mysql-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
path: "coverage*.xml"
@@ -465,12 +465,12 @@ jobs:
fetch-depth: 2
- name: "Download coverage files"
uses: "actions/download-artifact@v7"
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.1.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.1.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"
@@ -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
---------------------------
+155 -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.
@@ -321,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
@@ -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';
}
@@ -1610,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 */
@@ -1780,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 ")"
@@ -1806,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
~~~~~~~~~~~~~~~~~
@@ -1838,7 +1838,7 @@ Functions
.. code-block:: php
FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDateTime
FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDatetime
FunctionsReturningNumerics ::=
"LENGTH" "(" StringPrimary ")" |
@@ -1851,7 +1851,7 @@ Functions
"BIT_AND" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |
"BIT_OR" "(" ArithmeticPrimary "," ArithmeticPrimary ")"
FunctionsReturningDateTime ::=
FunctionsReturningDatetime ::=
"CURRENT_DATE" |
"CURRENT_TIME" |
"CURRENT_TIMESTAMP" |
@@ -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>`_.
-18
View File
@@ -2580,30 +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: '#^Method Doctrine\\ORM\\Query\\ParameterTypeInferer\:\:inferType\(\) never returns int so it can be removed from the return type\.$#'
identifier: return.unusedType
@@ -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);
+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> */
+6 -6
View File
@@ -1937,7 +1937,7 @@ final class Parser
}
/**
* ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary |
* ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary |
* StateFieldPathExpression | BooleanPrimary | CaseExpression |
* InstanceOfExpression
*
@@ -2011,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.
*/
@@ -2116,7 +2116,7 @@ final class Parser
/**
* SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
* CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
* CaseOperand ::= StateFieldPathExpression
*/
public function SimpleCaseExpression(): AST\SimpleCaseExpression
{
@@ -3435,7 +3435,7 @@ final class Parser
}
/**
* FunctionsReturningDateTime ::=
* FunctionsReturningDatetime ::=
* "CURRENT_DATE" |
* "CURRENT_TIME" |
* "CURRENT_TIMESTAMP" |
+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]);
+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;
}
@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH12225;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\DiscriminatorColumn;
use Doctrine\ORM\Mapping\DiscriminatorMap;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\Index;
use Doctrine\ORM\Mapping\InheritanceType;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Mapping\Table;
#[Entity]
#[Table(name: 'gh_12225_directory')]
#[Index(columns: ['dir_key'])]
#[InheritanceType('SINGLE_TABLE')]
#[DiscriminatorColumn(name: 'type', type: 'string')]
#[DiscriminatorMap(['main' => ConcreteDirectory::class])]
class AbstractDirectory
{
#[Id]
#[GeneratedValue]
#[Column(name: 'id', type: 'integer')]
private int $id;
#[Column(name: 'dir_key', type: 'string')]
private string $dirKey;
#[Column(name: 'deleted_at', type: 'datetime_immutable', nullable: true)]
private DateTimeImmutable|null $deletedAt = null;
#[ManyToOne(targetEntity: self::class, fetch: 'LAZY', inversedBy: 'directories')]
private AbstractDirectory|null $parent = null;
/** @var Collection<string, self> */
#[OneToMany(mappedBy: 'parent', targetEntity: self::class, fetch: 'EXTRA_LAZY', indexBy: 'dirKey')]
private Collection $children;
public function __construct(string $dirKey)
{
$this->dirKey = $dirKey;
$this->children = new ArrayCollection();
}
public function getId(): int
{
return $this->id;
}
public function getDirKey(): string
{
return $this->dirKey;
}
public function getDeletedAt(): DateTimeImmutable|null
{
return $this->deletedAt;
}
public function setDeletedAt(DateTimeImmutable|null $deletedAt): AbstractDirectory
{
$this->deletedAt = $deletedAt;
return $this;
}
public function getParent(): AbstractDirectory|null
{
return $this->parent;
}
public function setParent(AbstractDirectory|null $parent): AbstractDirectory
{
$this->parent = $parent;
return $this;
}
/** @return Collection<string, self> */
public function getChildren(): Collection
{
return $this->children;
}
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH12225;
use Doctrine\ORM\Mapping\Entity;
#[Entity]
class ConcreteDirectory extends AbstractDirectory
{
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH12225;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH12225Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
AbstractDirectory::class,
ConcreteDirectory::class,
]);
}
public function testHydrateWithIndexByFilterAndInheritanceMapping(): void
{
// Enable the filter
$this->_em->getConfiguration()->addFilter('my_filter', MyFilter::class);
$this->_em->getFilters()->enable('my_filter');
// Load entities into database
$parent = new ConcreteDirectory('parent');
$child = (new ConcreteDirectory('child'))->setParent($parent);
$this->_em->persist($parent);
$this->_em->persist($child);
$this->_em->flush();
$this->_em->clear();
$repository = $this->_em->getRepository(AbstractDirectory::class);
// Fetch entities from database while changing filters
$this->_em->getFilters()->suspend('my_filter');
$directories = $repository->findBy(['parent' => null]);
$this->_em->getFilters()->restore('my_filter');
// Ensure we got the parent directory
self::assertCount(1, $directories);
self::assertEquals('parent', $directories[0]->getDirKey());
// Try to hydrate all children of the parent directory (toArray is important here to initialize the collection)
self::assertCount(1, $directories[0]->getChildren()->toArray());
}
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH12225;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
class MyFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
return $targetTableAlias . '.deleted_at IS NULL';
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH9538Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
GH9538EntityA::class,
]);
}
public function testCanRemoveEntityWithReadonlyId(): void
{
$this->_em->persist($entity = new GH9538EntityA());
$this->_em->flush();
$this->_em->remove($entity);
$this->_em->flush();
$this->expectNotToPerformAssertions();
}
}
#[Entity]
class GH9538EntityA
{
#[Column(type: 'integer')]
#[Id]
#[GeneratedValue(strategy: 'AUTO')]
public readonly int $id;
}
+17 -1
View File
@@ -374,11 +374,27 @@ class ExprTest extends OrmTestCase
public function testAddThrowsException(): void
{
$this->expectException(InvalidArgumentException::class);
$orExpr = $this->expr->orX();
$this->expectException(InvalidArgumentException::class);
$orExpr->add($this->expr->quot(5, 2));
}
#[DataProvider('provideInvalidTypesForAdd')]
public function testAddThrowsExceptionOnInvalidType(mixed $arg): void
{
$orExpr = $this->expr->orX();
$this->expectException(InvalidArgumentException::class);
$orExpr->add($arg);
}
/** @return Generator<string, array{mixed}> */
public static function provideInvalidTypesForAdd(): Generator
{
yield 'integer 1' => [1];
yield 'object' => [(object) ['foo' => 'bar']];
yield 'array' => [['foo' => 'bar']];
}
#[Group('DDC-1683')]
public function testBooleanLiteral(): void
{