Compare commits

...

100 Commits

Author SHA1 Message Date
Grégoire Paris c322c71cd4 Merge pull request #12120 from greg0ire/fix-broken-comments
Fix broken comments
2025-08-08 08:55:44 +02:00
Grégoire Paris 3c733a2fee Add missing phpdoc 2025-08-08 08:34:07 +02:00
Grégoire Paris 5984ad586a Fix broken phpdoc comments 2025-08-08 08:33:39 +02:00
Grégoire Paris ee2c3a506b Merge pull request #12097 from mvorisek/add_fixed_id_insert_count_query_test
Add 2nd level cache test for insert without post-inserted ID
2025-08-08 08:26:23 +02:00
Grégoire Paris 04694a9f7b Merge pull request #11835 from gseidel/fix-pre-persist-call-persist
fix: calling scheduleForInsert twice
2025-08-07 06:25:35 +02:00
Michael Voříšek 5b2060e25f Add 2nd level cache test for insert without post-inserted ID
Inserts without post-inserted ID can be sent to DB grouped together
hence the extra test.
2025-08-06 08:22:48 +02:00
Grégoire Paris 39e35fc06c Merge pull request #12099 from alexislefebvre/2.20.x-update-supported-branches-on-README
doc: update supported branches on README (2.20.x)
2025-08-04 16:59:49 +02:00
Alexis Lefebvre 7f061c3870 doc: update supported branches on README 2025-08-04 16:38:55 +02:00
Grégoire Paris 74495711fb Merge pull request #11934 from mvorisek/fix_joined_subclass_persister_insert_of_multiple_entities
Fix JoinedSubclassPersister when multiple entities are inserted
2025-08-02 08:34:29 +02:00
Michael Voříšek 97a7cb8d2f Unify JoinedSubclassPersister dequeue
Fix JoinedSubclassPersister as BasicEntityPersister was already fixed in GH-10735.

The fix can be verified by modifying UnitOfWork to execute `BasicEntityPersister::executeInserts()` for multiple entities at once for the same entity class/persister instance - https://github.com/doctrine/orm/blob/2.20.3/src/UnitOfWork.php#L1186 - then reproducible on `Doctrine\Tests\ORM\Functional\Ticket\GH10531Test::testInserts` test.

As extending/modifying UnitOfWork in tests in not easily possible, I submit this fix for v2.x without a test.
2025-08-01 15:31:18 +02:00
Grégoire Paris e0052390e1 Merge pull request #12087 from mvorisek/improve_basic_entity_persister
Improve BasicEntityPersister to be more flexible and cleaner
2025-07-30 09:44:24 +02:00
Michael Voříšek 8c6419e0e0 Prefer strict empty-array comparison over empty() call 2025-07-29 15:15:31 +02:00
Michael Voříšek 6f5ce1aca2 BasicEntityPersister: refactor $values variable into $placeholders
The new variable name is much more clearer.
2025-07-29 15:15:31 +02:00
Michael Voříšek 98e7a53b42 Remove BasicEntityPersister::$insertSql cache property
When the persister is extended to do a multi update, the caching is not
wanted. The impact is minimal as the CPU/time overhead per query is
much bigger and the prepared statement is not cached anyway.
2025-07-29 15:15:31 +02:00
Gerhard Seidel 3aaaf37dfb fix: PrePersistEventTest typos and unnecessary comments 2025-07-29 14:40:20 +02:00
Grégoire Paris 154a4652ee Merge pull request #12086 from mvorisek/add_cache_rw_strict_locking_test
Add functional strict-locking 2nd level cache test
2025-07-29 11:48:25 +02:00
Michael Voříšek ae7489ff19 Add functional strict-locking 2nd level cache test 2025-07-28 12:14:50 +02:00
Grégoire Paris 1ee01f4473 Merge pull request #12078 from stlgaits/2.20.x
Fix embedded classes display in orm:mapping:command output
2025-07-22 08:48:52 +02:00
stlgaits 8a9ed138a8 Fix embedded classes display in orm:mapping:command output 2025-07-21 15:44:46 +02:00
Alexis Lefebvre 41ea59ac66 chore: use a shorter name for CI on GitHub Actions (#12055) 2025-07-04 00:19:01 +02:00
Alexis Lefebvre e605e6d569 doc: add links to GitHub Actions on README (#12054) 2025-07-04 00:13:51 +02:00
Grégoire Paris 6307b4fa7d Merge pull request #8012 from sgehrig/bug/#8011-ordering-with-arithmetic-expression
Bug/#8011 ordering with arithmetic expression
2025-06-24 19:50:46 +02:00
Alexander M. Turek 5d21bb158b Fix calls to Application::add() (#12006) 2025-06-18 08:58:26 +02:00
Grégoire Paris 71550106d4 Merge pull request #11975 from doctrine/dependabot/github_actions/2.20.x/doctrine/dot-github-7.3.0
Bump doctrine/.github from 7.2.2 to 7.3.0
2025-06-09 22:24:12 +02:00
dependabot[bot] 36011f0d0f Bump doctrine/.github from 7.2.2 to 7.3.0
Bumps [doctrine/.github](https://github.com/doctrine/.github) from 7.2.2 to 7.3.0.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/7.2.2...7.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-09 07:13:58 +00:00
Grégoire Paris c97d775370 Merge pull request #11963 from dbu/update-doc-building
cleanup doc building instructions
2025-06-09 07:45:09 +02:00
Grégoire Paris e9f0345a97 Merge pull request #11966 from greg0ire/partial-revert-10162-2
Partially revert to stdout
2025-06-07 09:41:03 +02:00
Grégoire Paris 0feb09d0d6 Partially revert to stdout
This command's purpose is to provide structured data, except for a call
to caution() that warns the user in case they do not have any mapped
entities or they have errors.
2025-06-06 07:57:38 +02:00
Grégoire Paris fe5f8bbaa1 Merge pull request #11965 from greg0ire/partial-revert-10162
Revert to stdout for MappingDescribeCommand
2025-06-06 00:04:07 +02:00
Grégoire Paris ecf3cec376 chore: ignore deprecations from Symfony
Symfony 7.3 is not available to all of our users, so we cannot switch to
native lazy objects, which require a PHP version higher than the lowest
PHP version we support.
2025-06-05 23:19:32 +02:00
Grégoire Paris 0a714db4d9 Revert to stdout for MappingDescribeCommand
In f256d996cc, I did a global move to
stderr for notifications, and went a bit overboard for
MappingDescribeCommand, which purpose is to output a description.
2025-06-05 23:07:28 +02:00
David Buchmann 471fda8d0b cleanup doc building instructions 2025-06-05 07:44:37 +02:00
Grégoire Paris dfe32c2f74 Unwrap literalinclude block (#11962)
For some reason, it does not appear to work when nested inside a
code-block directive. Anyway, if you specify the language attribute, you
get markup identical to what you obtain when using code-block and
literalinclude, so this wrapping seems unneeded.
2025-06-04 00:18:59 +02:00
Grégoire Paris c51ba3ce6b Merge pull request #11951 from dbu/fix-doc-syntax
insert blank line before code in code-block
2025-05-27 14:08:44 +02:00
David Buchmann fe025e8d23 insert blank line before code in code-block 2025-05-27 08:59:14 +02:00
Grégoire Paris ae2957cf7e Merge pull request #11932 from dbannik/2.20.3-issue-11931
#11931 Bug when change sql filter [Related issue #11694]
2025-05-06 07:53:48 +02:00
Dmitry Bannik e172b3bf9c #11931 Bug when change sql filter [Related issue #11694]
This fix takes into account the invalidation of the filter sql for SingleTablePersister and JoinedSubclassPersister
2025-05-05 23:43:27 +03:00
Grégoire Paris c9c6e8da2e Merge pull request #11834 from dbu/document-generated-columns
document how to work with generated columns
2025-05-05 10:05:55 +02:00
Grégoire Paris 528b8837e1 Merge pull request #11929 from doctrine/2.20.x-merge-up-into-2.21.x_KkdqS0u7
Merge release 2.20.3 into 2.21.x
2025-05-02 21:57:23 +02:00
Grégoire Paris 17d28b5c4c Merge pull request #11917 from stof/lazy_ghost_postload
Fix the initialization of lazy-ghost proxies with postLoad listeners
2025-05-02 19:07:53 +02:00
Christophe Coevoet a2d510c6f4 Fix the initialization of lazy-ghost proxies with postLoad listeners
PostLoad listeners might initialize values for transient properties, so
the proxy should not skip initialization when using transient
properties.

Co-authored-by: Nicolas Grekas <nicolas.grekas@gmail.com>
2025-05-02 15:27:59 +02:00
Christophe Coevoet 2a4ebca90e Refactor tests to avoid using instance properties to track postLoad
The old proxy implementation of doctrine/common was triggered by public
methods rather than access to properties (making public properties
unsupported in entities), so tests could use public instance properties
to track the state of postLoad lifecycle callbacks without triggering
the proxy initialization when reading that state (which then changes the
state of triggering the postLoad callback).
As the new proxy implementation hooks into properties instead, the tests
now use a static method (ensuring it is reset properly before loading
the instance for which we care about the tracking) instead of an
instance property.
2025-04-22 17:39:29 +02:00
Grégoire Paris cc29ae0d36 Merge pull request #11891 from mpdude/expression-matching-caveats
Add more detailed caveats for using the Collection filtering API
2025-03-29 11:31:36 +01:00
Grégoire Paris bd4a053d29 Merge pull request #11894 from DavidPetrasek/3.3.x
Fix URL's in xml-mapping.rst
2025-03-27 20:59:26 +01:00
David Petrásek 52fbfb3785 Revert to http for namespace name
These URLs are meant as identifiers rather than actual urls intended to
be used to perform an HTTP request.
2025-03-27 17:39:10 +01:00
Matthias Pigulla c259371e5f Remove property hooks mention 2025-03-26 18:58:17 +01:00
Matthias Pigulla dcdd58b642 working-with-associations.rst aktualisieren
Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>
2025-03-26 18:14:01 +01:00
Matthias Pigulla 7b9c53854f Add more detailed caveats for using the Collection filtering API 2025-03-26 13:38:52 +01:00
Grégoire Paris cdc5fe11dd Merge pull request #11889 from Rixafy/docs-typo-fix
Fix docs typo (nulable -> nullable)
2025-03-25 23:16:01 +01:00
Rixafy 69ece00564 Fix docs typo (nulable -> nullable) 2025-03-25 22:25:00 +01:00
Grégoire Paris c679d1b007 Merge pull request #11885 from greg0ire/no-triple-stars
Avoid triple stars
2025-03-25 07:51:08 +01:00
Grégoire Paris 1e15b22dcb Avoid triple stars
They don't have a special meaning, and are rendered like this:
<strong>*REQUIRED</strong>*.
2025-03-25 07:49:53 +01:00
Grégoire Paris 44057b4683 Merge pull request #11845 from lacatoire/update-message-annotation-to-attribute
Update message of `ORMInvalidArgumentException`
2025-03-24 10:58:02 +01:00
Grégoire Paris 013df03795 Upgrade to doctrine/coding-standard 13 (#11881) 2025-03-24 07:18:07 +01:00
Louis-Arnaud 2d2a34407c Use attributes in exception message 2025-03-23 16:07:31 +01:00
Stefan Gehrig 067ad51b3f fixes sqlite sql inconsistency 2025-03-17 08:48:30 +01:00
Stefan Gehrig 00c77213fb fixes codesniffer violation 2025-03-15 09:42:21 +01:00
Matteo Beccati 3303cd3b5d Fix non-deterministic test (#11866) 2025-03-14 00:09:36 +01:00
Grégoire Paris afcf91e839 Merge pull request #11863 from doctrine/dependabot/github_actions/2.20.x/doctrine/dot-github-7.2.2
Bump doctrine/.github from 7.2.1 to 7.2.2
2025-03-10 09:24:12 +01:00
dependabot[bot] c61a9b3b6d Bump doctrine/.github from 7.2.1 to 7.2.2
Bumps [doctrine/.github](https://github.com/doctrine/.github) from 7.2.1 to 7.2.2.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/7.2.1...7.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 06:43:02 +00:00
Stefan Gehrig c68b8f90b3 adds a test for postgres that uses a HIDDEN result variable for ordering based on arithmetic expression 2025-03-05 09:28:52 +01:00
Stefan Gehrig aa4f9ce9e9 CS fix based on PHP_CodeSniffer report 2025-03-05 09:22:57 +01:00
Stefan Gehrig d96fc23327 skips tests when running on postgres 2025-02-27 10:30:21 +01:00
Louis-Arnaud 3aed6912a3 Update ORMInvalidArgumentException.php
update message to use attribute instead of annotation
2025-02-21 10:10:22 +01:00
Gerhard Seidel 4fb044d5f6 fix: cs 2025-02-20 10:01:35 +08:00
David Buchmann 8ce7b310c5 document how to work with generated columns 2025-02-17 15:06:18 +01:00
Gerhard Seidel 2a953c5e2b fix: PrePersistEventTest and cs 2025-02-17 14:01:08 +08:00
Gerhard Seidel abc6a40ccb fix: calling scheduleForInsert twice
If scheduleForInsert was called in prePersist hook already, then persistNew need to check this case first, otherwise a ORMInvalidArgumentException will be thrown
2025-02-14 12:45:13 +08:00
Grégoire Paris 158605bf24 Merge pull request #11833 from HypeMC/fix-dql
Fix DQL example with composite key
2025-02-13 23:49:22 +01:00
Grégoire Paris 2c2ef65817 Merge pull request #11826 from aprat84/gh-11741
Clone query hints and parameters in `LimitSubqueryOutputWalker` constructor
2025-02-12 08:52:33 +01:00
HypeMC 1c33a86983 Fix DQL example with composite key 2025-02-11 16:10:59 +01:00
Albert Prat 310fe1cccb Clone query hints and parameters in LimitSubqueryOutputWalker constructor
This fixes a bug that arises when using Pagination and an entity relation is mapped with fetch-mode EAGER but setFetchMode LAZY (or anything that is not EAGER) has been used on the query. If the query use WITH condition, an exception is incorrectly raised (Associations with fetch-mode=EAGER may not be using WITH conditions).
The class LimitSubqueryOutputWalker clones the query, but not its parameters and hints, so the generated subquery does not know that fetch-mode has been overridden.

Fixes #11741
2025-02-11 10:48:14 +01:00
Grégoire Paris a67f677747 Merge pull request #11707 from jorenMartens/2.20.x
[DDC-551] fix, add filter support in oneToOne relation 2.20.x
2025-02-07 08:23:53 +01:00
Grégoire Paris 73e68f3c7d Merge pull request #11821 from doctrine/2.20.x-merge-up-into-2.21.x_8O8nHxqC
Merge release 2.20.2 into 2.21.x
2025-02-04 20:24:01 +01:00
Grégoire Paris 19912de927 Merge pull request #11820 from doctrine/dependabot/github_actions/2.20.x/doctrine/dot-github-7.2.1
Bump doctrine/.github from 7.1.0 to 7.2.1
2025-02-04 20:17:01 +01:00
dependabot[bot] 737cca5b78 Bump doctrine/.github from 7.1.0 to 7.2.1
Bumps [doctrine/.github](https://github.com/doctrine/.github) from 7.1.0 to 7.2.1.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/7.1.0...7.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-04 18:23:54 +00:00
Grégoire Paris 9e999ea1ff Merge pull request #11792 from dbannik/11783-failure-with-indexed-relation
11783 failure with indexed relation
2025-01-28 15:32:25 +01:00
Bob van de Vijver 6755bb0c7b Fix Hydration when use ManyToMany[indexBy]
The bug related (#11694) and fixed mapping of sql column alias to field in entity (#11783) and
invalidate cache [cache/persisted/entity|cache/persisted/collection] when sql filter changes
2025-01-27 15:35:59 +03:00
Alexander M. Turek 73777d0bd4 Merge branch '2.20.x' into 2.21.x
* 2.20.x:
  Introduce testNotListedValueInEnumArray
  Fix documentation for JoinColumn nullable (#11798)
  Ignore deprecations from doctrine/common
  Fix fields of transient classes being considered duplicate with `reportFieldsWhereDeclared`
2025-01-26 19:56:20 +01:00
Grégoire Paris c2a49327a7 Merge pull request #11799 from HypeMC/enum-array-error
Fix invalid enum value in array of enums
2025-01-22 11:54:59 +01:00
Maxime COLIN 9bd7242376 Introduce testNotListedValueInEnumArray 2025-01-22 02:25:34 +01:00
pawel-slowik fff085b63f Fix documentation for JoinColumn nullable (#11798)
Nullability is not inherited from the PHP type. The change that enabled
this feature was reversed in https://github.com/doctrine/orm/pull/8732.
2025-01-22 00:12:00 +01:00
Grégoire Paris 5ad5b11ae1 Merge pull request #11769 from HypeMC/fix-reportfieldswheredeclared
Fix fields of transient classes being considered duplicate with `reportFieldsWhereDeclared`
2025-01-20 23:48:32 +01:00
Grégoire Paris c12fd2cb94 Merge pull request #11793 from greg0ire/doctrine-common-support
Ignore deprecations from doctrine/common
2025-01-18 22:29:13 +01:00
Grégoire Paris 44d5d4a779 Ignore deprecations from doctrine/common
These new issues are caused by doctrine/common 3.5.0, released 2 weeks
ago.
2025-01-17 08:33:24 +01:00
Stefan Gehrig ec6d1b9f72 fixes whitespace
Signed-off-by: Stefan Gehrig <stefan.gehrig.hn@googlemail.com>
2025-01-07 08:51:19 +01:00
Stefan Gehrig d809fed52a fixes code sniffer complaints
Signed-off-by: Stefan Gehrig <stefan.gehrig.hn@googlemail.com>
2025-01-07 08:48:42 +01:00
Stefan Gehrig 0e4786dfa8 adds testcases for order by items enclosed in ((...)) (double brackets - just one bracket does not work)
just one bracket (...) gives

Exception : [Doctrine\ORM\Query\QueryException] [Syntax Error] line 0, col xx: Error: Expected Doctrine\ORM\Query\Lexer::T_IDENTIFIER, got '('
2025-01-03 10:45:08 +01:00
Stefan Gehrig c429262f02 adds detection of literals/result variables at the beginning of an order by item with arithmetic expression
Not sure whether this covers the whole problem regarding complex expressions in order by items but it fixes the provided test cases
2025-01-03 10:45:07 +01:00
Stefan Gehrig f4fdcbcdcb adds more test cases 2025-01-03 10:44:17 +01:00
Stefan Gehrig b0806469d5 adds test case for GH issue #8011 2025-01-03 10:44:17 +01:00
Grégoire Paris e89b58a13f Merge pull request #11771 from doctrine/2.20.x-merge-up-into-2.21.x_3Yg2ZYgM
Merge release 2.20.1 into 2.21.x
2024-12-19 08:16:04 +01:00
HypeMC 4feaa470af Fix fields of transient classes being considered duplicate with reportFieldsWhereDeclared 2024-12-18 15:42:12 +01:00
Grégoire Paris 2b94ec18b9 Merge pull request #11759 from doctrine/2.20.x
Merge 2.20.x up into 2.21.x
2024-12-08 14:33:31 +01:00
Grégoire Paris 2a662149f4 Merge pull request #11754 from doctrine/2.20.x
Merge 2.20.x up into 2.21.x
2024-12-07 15:39:29 +01:00
Grégoire Paris 37051d57ce Merge pull request #11739 from doctrine/2.20.x
Merge 2.20.x up into 2.21.x
2024-11-28 08:23:12 +01:00
Grégoire Paris 4563f2f9a7 Merge pull request #11737 from doctrine/2.20.x
Merge 2.20.x up into 2.21.x
2024-11-27 22:10:21 +01:00
Grégoire Paris 91201c094a Merge pull request #11722 from doctrine/2.20.x
Merge 2.20.x up into 2.21.x
2024-11-23 19:35:45 +01:00
Joren Martens 14866461c5 [DDC-551] fix, add filter support in oneToOne relation 2024-11-07 10:48:16 +01:00
Grégoire Paris a4a15ad243 Merge pull request #11687 from doctrine/2.20.x
Merge 2.20.x up into 2.21.x
2024-10-16 23:37:08 +02:00
101 changed files with 2027 additions and 313 deletions
+1 -1
View File
@@ -24,4 +24,4 @@ on:
jobs:
coding-standards:
uses: "doctrine/.github/.github/workflows/coding-standards.yml@7.1.0"
uses: "doctrine/.github/.github/workflows/coding-standards.yml@7.3.0"
+1 -1
View File
@@ -1,4 +1,4 @@
name: "Continuous Integration"
name: "CI"
on:
pull_request:
+1 -1
View File
@@ -17,4 +17,4 @@ on:
jobs:
documentation:
name: "Documentation"
uses: "doctrine/.github/.github/workflows/documentation.yml@7.1.0"
uses: "doctrine/.github/.github/workflows/documentation.yml@7.3.0"
@@ -7,7 +7,7 @@ on:
jobs:
release:
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@7.1.0"
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@7.3.0"
secrets:
GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}
+16 -11
View File
@@ -1,7 +1,7 @@
| [4.0.x][4.0] | [3.4.x][3.4] | [3.3.x][3.3] | [2.21.x][2.21] | [2.20.x][2.20] |
| [4.0.x][4.0] | [3.6.x][3.6] | [3.5.x][3.5] | [2.21.x][2.21] | [2.20.x][2.20] |
|:------------------------------------------------------:|:------------------------------------------------------:|:------------------------------------------------------:|:--------------------------------------------------------:|:--------------------------------------------------------:|
| [![Build status][4.0 image]][4.0] | [![Build status][3.4 image]][3.4] | [![Build status][3.3 image]][3.3] | [![Build status][2.21 image]][2.21] | [![Build status][2.20 image]][2.20] |
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.4 coverage image]][3.4 coverage] | [![Coverage Status][3.3 coverage image]][3.3 coverage] | [![Coverage Status][2.21 coverage image]][2.21 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] |
| [![Build status][4.0 image]][4.0 workflow] | [![Build status][3.6 image]][3.6 workflow] | [![Build status][3.5 image]][3.5 workflow] | [![Build status][2.21 image]][2.21 workflow] | [![Build status][2.20 image]][2.20 workflow] |
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.6 coverage image]][3.6 coverage] | [![Coverage Status][3.5 coverage image]][3.5 coverage] | [![Coverage Status][2.21 coverage image]][2.21 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] |
[<h1 align="center">🇺🇦 UKRAINE NEEDS YOUR HELP NOW!</h1>](https://www.doctrine-project.org/stop-war.html)
@@ -20,21 +20,26 @@ without requiring unnecessary code duplication.
[4.0 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=4.0.x
[4.0]: https://github.com/doctrine/orm/tree/4.0.x
[4.0 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A4.0.x
[4.0 coverage image]: https://codecov.io/gh/doctrine/orm/branch/4.0.x/graph/badge.svg
[4.0 coverage]: https://codecov.io/gh/doctrine/orm/branch/4.0.x
[3.4 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.4.x
[3.4]: https://github.com/doctrine/orm/tree/3.4.x
[3.4 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.4.x/graph/badge.svg
[3.4 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.4.x
[3.3 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.3.x
[3.3]: https://github.com/doctrine/orm/tree/3.3.x
[3.3 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.3.x/graph/badge.svg
[3.3 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.3.x
[3.6 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.6.x
[3.6]: https://github.com/doctrine/orm/tree/3.6.x
[3.6 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A3.6.x
[3.6 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.6.x/graph/badge.svg
[3.6 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.6.x
[3.5 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.5.x
[3.5]: https://github.com/doctrine/orm/tree/3.5.x
[3.5 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A3.5.x
[3.5 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.5.x/graph/badge.svg
[3.5 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.5.x
[2.21 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.21.x
[2.21]: https://github.com/doctrine/orm/tree/2.21.x
[2.21 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A2.21.x
[2.21 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.21.x/graph/badge.svg
[2.21 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.21.x
[2.20 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.20.x
[2.20]: https://github.com/doctrine/orm/tree/2.20.x
[2.20 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A2.20.x
[2.20 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.20.x/graph/badge.svg
[2.20 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.20.x
+2 -2
View File
@@ -41,14 +41,14 @@
},
"require-dev": {
"doctrine/annotations": "^1.13 || ^2",
"doctrine/coding-standard": "^9.0.2 || ^12.0",
"doctrine/coding-standard": "^9.0.2 || ^13.0",
"phpbench/phpbench": "^0.16.10 || ^1.0",
"phpstan/extension-installer": "~1.1.0 || ^1.4",
"phpstan/phpstan": "~1.4.10 || 2.0.3",
"phpstan/phpstan-deprecation-rules": "^1 || ^2",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6",
"psr/log": "^1 || ^2 || ^3",
"squizlabs/php_codesniffer": "3.7.2",
"squizlabs/php_codesniffer": "3.12.0",
"symfony/cache": "^4.4 || ^5.4 || ^6.4 || ^7.0",
"symfony/var-exporter": "^4.4 || ^5.4 || ^6.2 || ^7.0",
"symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0"
+3
View File
@@ -0,0 +1,3 @@
composer.lock
vendor/
build/
+24
View File
@@ -0,0 +1,24 @@
# Makefile for Doctrine ORM documentation
#
# You can set these variables from the command line.
DOCOPTS =
BUILD = vendor/bin/guides
BUILDDIR = build
# Internal variables.
ALLGUIDESOPTS = $(DOCOPTS) en/
.PHONY: help clean html
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
clean:
-rm -rf ./$(BUILDDIR)/*
html:
$(BUILD) $(ALLGUIDESOPTS) --output=$(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+13 -9
View File
@@ -1,18 +1,22 @@
# Doctrine ORM Documentation
The documentation is written in [ReStructured Text](https://docutils.sourceforge.io/rst.html).
## How to Generate:
Using Ubuntu 14.04 LTS:
1. Run ./bin/install-dependencies.sh
2. Run ./bin/generate-docs.sh
In the `docs/` folder, run
It will generate the documentation into the build directory of the checkout.
composer update
Then compile the documentation with:
## Theme issues
make html
If you get a "Theme error", check if the `en/_theme` subdirectory is empty,
in which case you will need to run:
This will generate the documentation into the `build` subdirectory.
1. git submodule init
2. git submodule update
To browse the documentation, you need to run a webserver:
cd build/html
php -S localhost:8000
Now the documentation is available at [http://localhost:8000](http://localhost:8000).
-10
View File
@@ -1,10 +0,0 @@
#!/bin/bash
EXECPATH=`dirname $0`
cd $EXECPATH
cd ..
rm build -Rf
sphinx-build en build
sphinx-build -b latex en build/pdf
rubber --into build/pdf --pdf build/pdf/Doctrine2ORM.tex
-2
View File
@@ -1,2 +0,0 @@
#!/bin/bash
sudo apt-get update && sudo apt-get install -y python2.7 python-sphinx python-pygments
+10
View File
@@ -0,0 +1,10 @@
{
"name": "doctrine/orm-docs",
"description": "Documentation for the Object-Relational Mapper\"",
"type": "library",
"license": "MIT",
"require": {
"phpdocumentor/guides-cli": "1.7.1",
"phpdocumentor/filesystem": "1.7.1"
}
}
-89
View File
@@ -1,89 +0,0 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Doctrine2ORM.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Doctrine2ORM.qhc"
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
"run these through (pdf)latex."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
+44
View File
@@ -0,0 +1,44 @@
Generated Columns
=================
Generated columns, sometimes also called virtual columns, are populated by
the database engine itself. They are a tool for performance optimization, to
avoid calculating a value on each query.
You can define generated columns on entities and have Doctrine map the values
to your entity.
Declaring a generated column
----------------------------
There is no explicit mapping instruction for generated columns. Instead, you
specify that the column should not be written to, and define a custom column
definition.
.. literalinclude:: generated-columns/Person.php
:language: php
* ``insertable``, ``updatable``: Setting these to false tells Doctrine to never
write this column - writing to a generated column would result in an error
from the database.
* ``columnDefinition``: We specify the full DDL to create the column. To allow
to use database specific features, this attribute does not use Doctrine Query
Language but native SQL. Note that you need to reference columns by their
database name (either explicitly set in the mapping or per the current
:doc:`naming strategy <../reference/namingstrategy>`).
Be aware that specifying a column definition makes the ``SchemaTool``
completely ignore all other configuration for this column. See also
:ref:`#[Column] <attrref_column>`
* ``generated``: Specifying that this column is always generated tells Doctrine
to update the field on the entity with the value from the database after
every write operation.
Advanced example: Extracting a value from a JSON structure
----------------------------------------------------------
Lets assume we have an entity that stores a blogpost as structured JSON.
To avoid extracting all titles on the fly when listing the posts, we create a
generated column with the field.
.. literalinclude:: generated-columns/Article.php
:language: php
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Article
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private int $id;
/**
* When working with Postgres, it is recommended to use the jsonb
* format for better performance.
*/
#[ORM\Column(options: ['jsonb' => true])]
private array $content;
/**
* Because we specify NOT NULL, inserting will fail if the content does
* not have a string in the title field.
*/
#[ORM\Column(
insertable: false,
updatable: false,
columnDefinition: "VARCHAR(255) generated always as (content->>'title') stored NOT NULL",
generated: 'ALWAYS',
)]
private string $title;
}
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Person
{
#[ORM\Column(type: 'string')]
private string $firstName;
#[ORM\Column(type: 'string', name: 'name')]
private string $lastName;
#[ORM\Column(
type: 'string',
insertable: false,
updatable: false,
columnDefinition: "VARCHAR(255) GENERATED ALWAYS AS (concat(firstName, ' ', name) stored NOT NULL",
generated: 'ALWAYS',
)]
private string $fullName;
}
+2
View File
@@ -103,6 +103,7 @@ Cookbook
* **Patterns**:
:doc:`Aggregate Fields <cookbook/aggregate-fields>` \|
:doc:`Generated/Virtual Columns <cookbook/generated-columns>` \|
:doc:`Decorator Pattern <cookbook/decorator-pattern>` \|
:doc:`Strategy Pattern <cookbook/strategy-cookbook-introduction>`
@@ -123,4 +124,5 @@ Cookbook
* **Custom Datatypes**
:doc:`MySQL Enums <cookbook/mysql-enums>`
:doc:`Custom Mapping Types <cookbook/custom-mapping-types>`
:doc:`Advanced Field Value Conversion <cookbook/advanced-field-value-conversion-using-custom-mapping-types>`
+16 -16
View File
@@ -71,8 +71,8 @@ Configuration Options
The following sections describe all the configuration options
available on a ``Doctrine\ORM\Configuration`` instance.
Proxy Directory (***REQUIRED***)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Proxy Directory (**REQUIRED**)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: php
@@ -85,8 +85,8 @@ classes. For a detailed explanation on proxy classes and how they
are used in Doctrine, refer to the "Proxy Objects" section further
down.
Proxy Namespace (***REQUIRED***)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Proxy Namespace (**REQUIRED**)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: php
@@ -98,8 +98,8 @@ Gets or sets the namespace to use for generated proxy classes. For
a detailed explanation on proxy classes and how they are used in
Doctrine, refer to the "Proxy Objects" section further down.
Metadata Driver (***REQUIRED***)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Metadata Driver (**REQUIRED**)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: php
@@ -144,8 +144,8 @@ 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.
Metadata Cache (***RECOMMENDED***)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Metadata Cache (**RECOMMENDED**)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: php
@@ -166,8 +166,8 @@ For development you should use an array cache like
``Symfony\Component\Cache\Adapter\ArrayAdapter``
which only caches data on a per-request basis.
Query Cache (***RECOMMENDED***)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Query Cache (**RECOMMENDED**)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: php
@@ -189,8 +189,8 @@ For development you should use an array cache like
``Symfony\Component\Cache\Adapter\ArrayAdapter``
which only caches data on a per-request basis.
SQL Logger (***Optional***)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
SQL Logger (**Optional**)
~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: php
@@ -202,8 +202,8 @@ Gets or sets the logger to use for logging all SQL statements
executed by Doctrine. The logger class must implement the
deprecated ``Doctrine\DBAL\Logging\SQLLogger`` interface.
Auto-generating Proxy Classes (***OPTIONAL***)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Auto-generating Proxy Classes (**OPTIONAL**)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Proxy classes can either be generated manually through the Doctrine
Console or automatically at runtime by Doctrine. The configuration
@@ -446,7 +446,7 @@ correctly if sub-namespaces use different metadata driver
implementations.
Default Repository (***OPTIONAL***)
Default Repository (**OPTIONAL**)
-----------------------------------
Specifies the FQCN of a subclass of the EntityRepository.
@@ -461,7 +461,7 @@ That will be available for all entities without a custom repository class.
The default value is ``Doctrine\ORM\EntityRepository``.
Any repository class must be a subclass of EntityRepository otherwise you got an ORMException
Ignoring entities (***OPTIONAL***)
Ignoring entities (**OPTIONAL**)
-----------------------------------
Specifies the Entity FQCNs to ignore.
+4 -6
View File
@@ -1368,8 +1368,7 @@ defaults to "id", just as in one-to-one or many-to-one mappings.
Additionally, when using typed properties with Doctrine 2.9 or newer
you can skip ``targetEntity`` in ``ManyToOne`` and ``OneToOne``
associations as they will be set based on type. Also ``nullable``
attribute on ``JoinColumn`` will be inherited from PHP type. So that:
associations as they will be set based on type. So that:
.. configuration-block::
@@ -1409,7 +1408,7 @@ Is essentially the same as following:
<?php
/** One Product has One Shipment. */
#[OneToOne(targetEntity: Shipment::class)]
#[JoinColumn(name: 'shipment_id', referencedColumnName: 'id', nullable: false)]
#[JoinColumn(name: 'shipment_id', referencedColumnName: 'id')]
private Shipment $shipment;
.. code-block:: annotation
@@ -1418,7 +1417,7 @@ Is essentially the same as following:
/**
* One Product has One Shipment.
* @OneToOne(targetEntity="Shipment")
* @JoinColumn(name="shipment_id", referencedColumnName="id", nullable=false)
* @JoinColumn(name="shipment_id", referencedColumnName="id")
*/
private Shipment $shipment;
@@ -1427,7 +1426,7 @@ Is essentially the same as following:
<doctrine-mapping>
<entity class="Product">
<one-to-one field="shipment" target-entity="Shipment">
<join-column name="shipment_id" referenced-column-name="id" nulable=false />
<join-column name="shipment_id" referenced-column-name="id" nullable=false />
</one-to-one>
</entity>
</doctrine-mapping>
@@ -1442,7 +1441,6 @@ Is essentially the same as following:
joinColumn:
name: shipment_id
referencedColumnName: id
nullable: false
If you accept these defaults, you can reduce the mapping code to a
minimum.
+14 -8
View File
@@ -213,12 +213,15 @@ Optional parameters:
- ``check``: Adds a check constraint type to the column (might not
be supported by all vendors).
- **columnDefinition**: DDL SQL snippet that starts after the column
- **columnDefinition**: Specify the DDL SQL snippet that starts after the column
name and specifies the complete (non-portable!) column definition.
This attribute allows to make use of advanced RMDBS features.
However you should make careful use of this feature and the
consequences. ``SchemaTool`` will not detect changes on the column correctly
anymore if you use ``columnDefinition``.
However, as this needs to be specified in the DDL native to the database,
the resulting schema changes are no longer portable. If you specify a
``columnDefinition``, the ``SchemaTool`` ignores all other attributes
that are normally used to build the definition DDL. Changes to the
``columnDefinition`` are not detected, you will need to manually create a
migration to apply changes.
Additionally you should remember that the ``type``
attribute still handles the conversion between PHP and Database
@@ -261,10 +264,11 @@ Examples:
)]
protected $loginCount;
// MySQL example: full_name char(41) GENERATED ALWAYS AS (concat(firstname,' ',lastname)),
// columnDefinition is raw SQL, not DQL. This example works for MySQL:
#[Column(
type: "string",
name: "user_fullname",
columnDefinition: "VARCHAR(255) GENERATED ALWAYS AS (concat(firstname,' ',lastname))",
insertable: false,
updatable: false
)]
@@ -366,7 +370,7 @@ Optional parameters:
- **type**: By default this is string.
- **length**: By default this is 255.
- **columnDefinition**: By default this is null the definition according to the type will be used. This option allows to override it.
- **columnDefinition**: Allows to override how the column is generated. See the "columnDefinition" attribute on :ref:`#[Column] <attrref_column>`
- **enumType**: By default this is `null`. Allows to map discriminatorColumn value to PHP enum
- **options**: See "options" attribute on :ref:`#[Column] <attrref_column>`.
@@ -678,8 +682,10 @@ Optional parameters:
- **onDelete**: Cascade Action (Database-level)
- **columnDefinition**: DDL SQL snippet that starts after the column
name and specifies the complete (non-portable!) column definition.
This attribute enables the use of advanced RMDBS features. Using
this attribute on ``#[JoinColumn]`` is necessary if you need slightly
This attribute enables the use of advanced RMDBS features. Note that you
need to reference columns by their database name (either explicitly set in
the mapping or per the current :doc:`naming strategy <namingstrategy>`).
Using this attribute on ``#[JoinColumn]`` is necessary if you need
different column definitions for joining columns, for example
regarding NULL/NOT NULL defaults. However by default a
"columnDefinition" attribute on :ref:`#[Column] <attrref_column>` also sets
+1 -1
View File
@@ -299,7 +299,7 @@ specific to a particular entity class's lifecycle.
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
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="User">
+2 -2
View File
@@ -323,7 +323,7 @@ level cache region.
<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
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="Country">
@@ -431,7 +431,7 @@ It caches the primary keys of association and cache each element will be cached
<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
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="State">
@@ -736,6 +736,35 @@ methods:
.. note::
There is a limitation on the compatibility of Criteria comparisons.
You have to use scalar values only as the value in a comparison or
the behaviour between different backends is not the same.
Depending on whether the collection has already been loaded from the
database or not, criteria matching may happen at the database/SQL level
or on objects in memory. This may lead to different results and come
surprising, for example when a code change in one place leads to a collection
becoming initialized and, as a side effect, returning a different result
or even breaking a ``matching()`` call somewhere else. Also, collection
initialization state in practical use cases may differ from the one covered
in unit tests.
Database level comparisons are based on scalar representations of the values
stored in entity properties. The field names passed to expressions correspond
to property names. Comparison and sorting may be affected by
database-specific behavior. For example, MySQL enum types sort by index position,
not lexicographically by value.
In-memory handling is based on the ``Selectable`` API of `Doctrine Collections <https://www.doctrine-project.org/projects/doctrine-collections/en/stable/index.html#matching>`.
In this case, field names passed to expressions are being used to derive accessor
method names. Strict type comparisons are used for equal and not-equal checks,
and generally PHP language rules are being used for other comparison operators
or sorting.
As a general guidance, for consistent results use the Criteria API with scalar
values only. Note that ``DateTime`` and ``DateTimeImmutable`` are two predominant
examples of value objects that are *not* scalars.
Refrain from using special database-level column types or custom Doctrine Types
that may lead to database-specific comparison or sorting rules being applied, or
to database-level values being different from object field values.
Provide accessor methods for all entity fields used in criteria expressions,
and implement those methods in a way that their return value is the
same as the database-level value.
+3 -3
View File
@@ -17,7 +17,7 @@ setup for the latest code in trunk.
.. code-block:: xml
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
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">
@@ -103,7 +103,7 @@ of several common elements:
// Doctrine.Tests.ORM.Mapping.User.dcm.xml
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
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">
@@ -770,7 +770,7 @@ entity relationship. You can define this in XML with the "association-key" attri
.. code-block:: xml
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
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">
+1
View File
@@ -65,6 +65,7 @@
cookbook/decorator-pattern
cookbook/dql-custom-walkers
cookbook/dql-user-defined-functions
cookbook/generated-columns
cookbook/implementing-arrayaccess-for-domain-objects
cookbook/implementing-the-notify-changetracking-policy
cookbook/resolve-target-entity-listener
+6 -5
View File
@@ -86,7 +86,7 @@ and year of production as primary keys:
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
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">
@@ -127,11 +127,12 @@ And for querying you can use arrays to both DQL and EntityRepositories:
namespace VehicleCatalogue\Model;
// $em is the EntityManager
$audi = $em->find("VehicleCatalogue\Model\Car", array("name" => "Audi A8", "year" => 2010));
$audi = $em->find("VehicleCatalogue\Model\Car", ["name" => "Audi A8", "year" => 2010]);
$dql = "SELECT c FROM VehicleCatalogue\Model\Car c WHERE c.id = ?1";
$dql = "SELECT c FROM VehicleCatalogue\Model\Car c WHERE c.name = ?1 AND c.year = ?2";
$audi = $em->createQuery($dql)
->setParameter(1, ["name" => "Audi A8", "year" => 2010])
->setParameter(1, "Audi A8")
->setParameter(2, 2010)
->getSingleResult();
You can also use this entity in associations. Doctrine will then generate two foreign keys one for ``name``
@@ -268,7 +269,7 @@ We keep up the example of an Article with arbitrary attributes, the mapping look
.. code-block:: xml
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
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">
@@ -87,7 +87,7 @@ switch to extra lazy as shown in these examples:
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
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">
+4 -4
View File
@@ -558,7 +558,7 @@ methods, but you only need to choose one.
<!-- config/xml/Product.dcm.xml -->
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
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">
@@ -1139,7 +1139,7 @@ the ``Product`` before:
<!-- config/xml/Bug.dcm.xml -->
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
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">
@@ -1294,7 +1294,7 @@ Finally, we'll add metadata mappings for the ``User`` entity.
<!-- config/xml/User.dcm.xml -->
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
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">
@@ -1818,7 +1818,7 @@ we have to adjust the metadata slightly.
.. code-block:: xml
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
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">
@@ -128,7 +128,7 @@ here are the code and mappings for it:
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
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">
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
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">
+63
View File
@@ -3144,6 +3144,15 @@ parameters:
count: 1
path: src/Persisters/SqlValueVisitor.php
-
message: '''
#^Class Doctrine\\ORM\\Proxy\\Autoloader extends deprecated class Doctrine\\Common\\Proxy\\Autoloader\:
The Autoloader class is deprecated since doctrine/common 3\.5\.$#
'''
identifier: class.extendsDeprecatedClass
count: 1
path: src/Proxy/Autoloader.php
-
message: '#^Method Doctrine\\ORM\\Proxy\\DefaultProxyClassNameResolver\:\:resolveClassName\(\) should return class\-string\<T of object\> but returns class\-string\<Doctrine\\Persistence\\Proxy\<T of object\>\>\|class\-string\<T of object\>\.$#'
identifier: return.type
@@ -3186,6 +3195,42 @@ parameters:
count: 1
path: src/Proxy/ProxyFactory.php
-
message: '''
#^Call to method __construct\(\) of deprecated class Doctrine\\Common\\Proxy\\AbstractProxyFactory\:
The AbstractProxyFactory class is deprecated since doctrine/common 3\.5\.$#
'''
identifier: staticMethod.deprecatedClass
count: 1
path: src/Proxy/ProxyFactory.php
-
message: '''
#^Call to method generateProxyClasses\(\) of deprecated class Doctrine\\Common\\Proxy\\AbstractProxyFactory\:
The AbstractProxyFactory class is deprecated since doctrine/common 3\.5\.$#
'''
identifier: staticMethod.deprecatedClass
count: 1
path: src/Proxy/ProxyFactory.php
-
message: '''
#^Call to method getProxy\(\) of deprecated class Doctrine\\Common\\Proxy\\AbstractProxyFactory\:
The AbstractProxyFactory class is deprecated since doctrine/common 3\.5\.$#
'''
identifier: staticMethod.deprecatedClass
count: 1
path: src/Proxy/ProxyFactory.php
-
message: '''
#^Class Doctrine\\ORM\\Proxy\\ProxyFactory extends deprecated class Doctrine\\Common\\Proxy\\AbstractProxyFactory\:
The AbstractProxyFactory class is deprecated since doctrine/common 3\.5\.$#
'''
identifier: class.extendsDeprecatedClass
count: 1
path: src/Proxy/ProxyFactory.php
-
message: '#^Comparison operation "\<" between 0\|1\|2\|3\|4 and 0 is always false\.$#'
identifier: smaller.alwaysFalse
@@ -3198,6 +3243,15 @@ parameters:
count: 1
path: src/Proxy/ProxyFactory.php
-
message: '''
#^Instantiation of deprecated class Doctrine\\Common\\Proxy\\ProxyGenerator\:
The ProxyGenerator class is deprecated since doctrine/common 3\.5\.$#
'''
identifier: new.deprecated
count: 1
path: src/Proxy/ProxyFactory.php
-
message: '#^Method Doctrine\\ORM\\Proxy\\ProxyFactory\:\:createCloner\(\) has Doctrine\\ORM\\EntityNotFoundException in PHPDoc @throws tag but it''s not thrown\.$#'
identifier: throws.unusedType
@@ -3336,6 +3390,15 @@ parameters:
count: 1
path: src/Proxy/ProxyFactory.php
-
message: '''
#^Return type of method Doctrine\\ORM\\Proxy\\ProxyFactory\:\:getProxy\(\) has typehint with deprecated interface Doctrine\\Common\\Proxy\\Proxy\:
The Proxy interface is deprecated since doctrine/common 3\.5\.$#
'''
identifier: return.deprecatedInterface
count: 1
path: src/Proxy/ProxyFactory.php
-
message: '#^Call to function method_exists\(\) with ''Doctrine\\\\DBAL\\\\Cache\\\\QueryCacheProfile'' and ''getResultCache'' will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
+1 -1
View File
@@ -11,7 +11,7 @@ parameters:
# Fallback logic for DBAL 2
-
message: '/Application::add\(\) expects Symfony\\Component\\Console\\Command\\Command/'
message: '/Parameter #2 \$command of static method Doctrine\\ORM\\Tools\\Console\\ConsoleRunner\:\:addCommandToApplication\(\) expects Symfony\\Component\\Console\\Command\\Command/'
path: src/Tools/Console/ConsoleRunner.php
- '/^Class Doctrine\\DBAL\\Platforms\\SQLAnywherePlatform not found\.$/'
+1 -1
View File
@@ -9,7 +9,7 @@ parameters:
# Fallback logic for DBAL 2
-
message: '/Application::add\(\) expects Symfony\\Component\\Console\\Command\\Command/'
message: '/Parameter #2 \$command of static method Doctrine\\ORM\\Tools\\Console\\ConsoleRunner\:\:addCommandToApplication\(\) expects Symfony\\Component\\Console\\Command\\Command/'
path: src/Tools/Console/ConsoleRunner.php
- '/^Class Doctrine\\DBAL\\Platforms\\SQLAnywherePlatform not found\.$/'
+4 -2
View File
@@ -43,7 +43,7 @@ class CollectionCacheKey extends CacheKey
* @param string $association The field name that represents the association.
* @param array<string, mixed> $ownerIdentifier The identifier of the owning entity.
*/
public function __construct($entityClass, $association, array $ownerIdentifier)
public function __construct($entityClass, $association, array $ownerIdentifier, string $filterHash = '')
{
ksort($ownerIdentifier);
@@ -51,6 +51,8 @@ class CollectionCacheKey extends CacheKey
$this->entityClass = (string) $entityClass;
$this->association = (string) $association;
parent::__construct(str_replace('\\', '.', strtolower($entityClass)) . '_' . implode(' ', $ownerIdentifier) . '__' . $association);
$filterHash = $filterHash === '' ? '' : '_' . $filterHash;
parent::__construct(str_replace('\\', '.', strtolower($entityClass)) . '_' . implode(' ', $ownerIdentifier) . '__' . $association . $filterHash);
}
}
@@ -19,6 +19,7 @@ use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Query\FilterCollection;
use Doctrine\ORM\UnitOfWork;
use function array_values;
@@ -55,6 +56,9 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister
/** @var string */
protected $regionName;
/** @var FilterCollection */
protected $filters;
/** @var CollectionHydrator */
protected $hydrator;
@@ -76,6 +80,7 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister
$this->region = $region;
$this->persister = $persister;
$this->association = $association;
$this->filters = $em->getFilters();
$this->regionName = $region->getName();
$this->uow = $em->getUnitOfWork();
$this->metadataFactory = $em->getMetadataFactory();
@@ -189,7 +194,7 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister
public function count(PersistentCollection $collection)
{
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId, $this->filters->getHash());
$entry = $this->region->get($key);
if ($entry !== null) {
@@ -241,7 +246,8 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister
$key = new CollectionCacheKey(
$this->sourceEntity->rootEntityName,
$this->association['fieldName'],
$this->uow->getEntityIdentifier($collection->getOwner())
$this->uow->getEntityIdentifier($collection->getOwner()),
$this->filters->getHash()
);
$this->region->evict($key);
@@ -45,7 +45,7 @@ class NonStrictReadWriteCachedCollectionPersister extends AbstractCollectionPers
public function delete(PersistentCollection $collection)
{
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId, $this->filters->getHash());
$this->persister->delete($collection);
@@ -65,7 +65,7 @@ class NonStrictReadWriteCachedCollectionPersister extends AbstractCollectionPers
}
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId, $this->filters->getHash());
// Invalidate non initialized collections OR ordered collection
if ($isDirty && ! $isInitialized || isset($this->association['orderBy'])) {
@@ -68,7 +68,7 @@ class ReadWriteCachedCollectionPersister extends AbstractCollectionPersister
public function delete(PersistentCollection $collection)
{
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId, $this->filters->getHash());
$lock = $this->region->lock($key);
$this->persister->delete($collection);
@@ -98,7 +98,7 @@ class ReadWriteCachedCollectionPersister extends AbstractCollectionPersister
$this->persister->update($collection);
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId, $this->filters->getHash());
$lock = $this->region->lock($key);
if ($lock === null) {
@@ -22,9 +22,11 @@ use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Persisters\Entity\EntityPersister;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Query\FilterCollection;
use Doctrine\ORM\UnitOfWork;
use function array_merge;
use function func_get_args;
use function serialize;
use function sha1;
@@ -62,6 +64,9 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
/** @var Cache */
protected $cache;
/** @var FilterCollection */
protected $filters;
/** @var CacheLogger|null */
protected $cacheLogger;
@@ -91,6 +96,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
$this->region = $region;
$this->persister = $persister;
$this->cache = $em->getCache();
$this->filters = $em->getFilters();
$this->regionName = $region->getName();
$this->uow = $em->getUnitOfWork();
$this->metadataFactory = $em->getMetadataFactory();
@@ -261,7 +267,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
? $this->persister->expandCriteriaParameters($criteria)
: $this->persister->expandParameters($criteria);
return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset);
return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset . $this->filters->getHash());
}
/**
@@ -524,7 +530,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
}
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = $this->buildCollectionCacheKey($assoc, $ownerId);
$key = $this->buildCollectionCacheKey($assoc, $ownerId, $this->filters->getHash());
$list = $persister->loadCollectionCache($collection, $key);
if ($list !== null) {
@@ -559,7 +565,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
}
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = $this->buildCollectionCacheKey($assoc, $ownerId);
$key = $this->buildCollectionCacheKey($assoc, $ownerId, $this->filters->getHash());
$list = $persister->loadCollectionCache($collection, $key);
if ($list !== null) {
@@ -611,12 +617,15 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
*
* @return CollectionCacheKey
*/
protected function buildCollectionCacheKey(array $association, $ownerId)
protected function buildCollectionCacheKey(array $association, $ownerId/*, string $filterHash */)
{
$filterHash = (string) (func_get_args()[2] ?? ''); // todo: move to argument in next major release
return new CollectionCacheKey(
$this->metadataFactory->getMetadataFor($association['sourceEntity'])->rootEntityName,
$association['fieldName'],
$ownerId
$ownerId,
$filterHash
);
}
}
+1
View File
@@ -1109,6 +1109,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
public function setLazyGhostObjectEnabled(bool $flag): void
{
// @phpstan-ignore classConstant.deprecatedTrait (Because we support Symfony < 7.3)
if ($flag && ! trait_exists(LazyGhostTrait::class)) {
throw new LogicException(
'Lazy ghost objects cannot be enabled because the "symfony/var-exporter" library'
@@ -16,6 +16,7 @@ use function array_keys;
use function array_search;
use function count;
use function in_array;
use function is_array;
use function key;
use function reset;
use function sprintf;
@@ -143,14 +144,21 @@ class SimpleObjectHydrator extends AbstractHydrator
}
if ($value !== null && isset($cacheKeyInfo['enumType'])) {
$originalValue = $value;
$originalValue = $currentValue = $value;
try {
$value = $this->buildEnum($originalValue, $cacheKeyInfo['enumType']);
if (! is_array($originalValue)) {
$value = $this->buildEnum($originalValue, $cacheKeyInfo['enumType']);
} else {
$value = [];
foreach ($originalValue as $i => $currentValue) {
$value[$i] = $this->buildEnum($currentValue, $cacheKeyInfo['enumType']);
}
}
} catch (ValueError $e) {
throw MappingException::invalidEnumValue(
$entityName,
$cacheKeyInfo['fieldName'],
(string) $originalValue,
(string) $currentValue,
$cacheKeyInfo['enumType'],
$e
);
-1
View File
@@ -1278,7 +1278,6 @@ class ClassMetadataInfo implements ClassMetadata
/**
* @param string $fieldName
* @param array $cache
* @phpstan-param array{usage?: int|null, region?: string|null} $cache
*
* @return int[]|string[]
@@ -32,8 +32,13 @@ trait ReflectionBasedDriver
|| $metadata->isInheritedEmbeddedClass($property->name);
}
/** @var class-string $declaringClass */
$declaringClass = $property->class;
if ($this->isTransient($declaringClass)) {
return isset($metadata->fieldMappings[$property->name]);
}
if (
isset($metadata->fieldMappings[$property->name]['declared'])
&& $metadata->fieldMappings[$property->name]['declared'] === $declaringClass
+1 -1
View File
@@ -309,7 +309,7 @@ EXCEPTION
. ' configured to cascade persist operations for entity: ' . self::objToStr($entity) . '.'
. ' To solve this issue: Either explicitly call EntityManager#persist()'
. ' on this unknown entity or configure cascade persist'
. ' this association in the mapping for example @ManyToOne(..,cascade={"persist"}).'
. ' this association in the mapping for example #[ORM\ManyToOne(..., cascade: [\'persist\'])].'
. (method_exists($entity, '__toString')
? ''
: ' If you cannot find out which entity causes the problem implement \''
+38 -29
View File
@@ -168,14 +168,6 @@ class BasicEntityPersister implements EntityPersister
*/
protected $quotedColumns = [];
/**
* The INSERT SQL statement used for entities handled by this persister.
* This SQL is only generated once per request, if at all.
*
* @var string|null
*/
private $insertSql;
/**
* The quote strategy.
*
@@ -226,6 +218,16 @@ class BasicEntityPersister implements EntityPersister
);
}
final protected function isFilterHashUpToDate(): bool
{
return $this->filterHash === $this->em->getFilters()->getHash();
}
final protected function updateFilterHash(): void
{
$this->filterHash = $this->em->getFilters()->getHash();
}
/**
* {@inheritDoc}
*/
@@ -300,8 +302,8 @@ class BasicEntityPersister implements EntityPersister
$this->assignDefaultVersionAndUpsertableValues($entity, $id);
}
// Unset this queued insert, so that the prepareUpdateData() method knows right away
// (for the next entity already) that the current entity has been written to the database
// Unset this queued insert, so that the prepareUpdateData() method (called via prepareInsertData() method)
// knows right away (for the next entity already) that the current entity has been written to the database
// and no extra updates need to be scheduled to refer to it.
//
// In \Doctrine\ORM\UnitOfWork::executeInserts(), the UoW already removed entities
@@ -1274,7 +1276,7 @@ class BasicEntityPersister implements EntityPersister
*/
protected function getSelectColumnsSQL()
{
if ($this->currentPersisterContext->selectColumnListSql !== null && $this->filterHash === $this->em->getFilters()->getHash()) {
if ($this->currentPersisterContext->selectColumnListSql !== null && $this->isFilterHashUpToDate()) {
return $this->currentPersisterContext->selectColumnListSql;
}
@@ -1374,6 +1376,12 @@ class BasicEntityPersister implements EntityPersister
$joinCondition[] = $this->getSQLTableAlias($association['sourceEntity'], $assocAlias) . '.' . $sourceCol . ' = '
. $this->getSQLTableAlias($association['targetEntity']) . '.' . $targetCol;
}
// Add filter SQL
$filterSql = $this->generateFilterConditionSQL($eagerEntity, $joinTableAlias);
if ($filterSql) {
$joinCondition[] = $filterSql;
}
}
$this->currentPersisterContext->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON ';
@@ -1381,7 +1389,7 @@ class BasicEntityPersister implements EntityPersister
}
$this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
$this->filterHash = $this->em->getFilters()->getHash();
$this->updateFilterHash();
return $this->currentPersisterContext->selectColumnListSql;
}
@@ -1457,22 +1465,17 @@ class BasicEntityPersister implements EntityPersister
*/
public function getInsertSQL()
{
if ($this->insertSql !== null) {
return $this->insertSql;
}
$columns = $this->getInsertColumnList();
$tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
if (empty($columns)) {
$identityColumn = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform);
$this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
if ($columns === []) {
$identityColumn = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform);
return $this->insertSql;
return $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
}
$values = [];
$columns = array_unique($columns);
$placeholders = [];
$columns = array_unique($columns);
foreach ($columns as $column) {
$placeholder = '?';
@@ -1486,15 +1489,13 @@ class BasicEntityPersister implements EntityPersister
$placeholder = $type->convertToDatabaseValueSQL('?', $this->platform);
}
$values[] = $placeholder;
$placeholders[] = $placeholder;
}
$columns = implode(', ', $columns);
$values = implode(', ', $values);
$columns = implode(', ', $columns);
$placeholders = implode(', ', $placeholders);
$this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $values);
return $this->insertSql;
return sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $placeholders);
}
/**
@@ -1560,7 +1561,15 @@ class BasicEntityPersister implements EntityPersister
$tableAlias = $this->getSQLTableAlias($class->name, $root);
$fieldMapping = $class->fieldMappings[$field];
$sql = sprintf('%s.%s', $tableAlias, $this->quoteStrategy->getColumnName($field, $class, $this->platform));
$columnAlias = $this->getSQLColumnAlias($fieldMapping['columnName']);
$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);
if (! empty($fieldMapping['enumType'])) {
@@ -149,7 +149,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
// Execute all inserts. For each entity:
// 1) Insert on root table
// 2) Insert on sub tables
foreach ($this->queuedInserts as $entity) {
foreach ($this->queuedInserts as $key => $entity) {
$insertData = $this->prepareInsertData($entity);
// Execute insert on root table
@@ -194,9 +194,16 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
if ($this->class->requiresFetchAfterChange) {
$this->assignDefaultVersionAndUpsertableValues($entity, $id);
}
}
$this->queuedInserts = [];
// Unset this queued insert, so that the prepareUpdateData() method (called via prepareInsertData() method)
// knows right away (for the next entity already) that the current entity has been written to the database
// and no extra updates need to be scheduled to refer to it.
//
// In \Doctrine\ORM\UnitOfWork::executeInserts(), the UoW already removed entities
// from its own list (\Doctrine\ORM\UnitOfWork::$entityInsertions) right after they
// were given to our addInsert() method.
unset($this->queuedInserts[$key]);
}
}
/**
@@ -404,7 +411,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
protected function getSelectColumnsSQL()
{
// Create the column list fragment only once
if ($this->currentPersisterContext->selectColumnListSql !== null) {
if ($this->currentPersisterContext->selectColumnListSql !== null && $this->isFilterHashUpToDate()) {
return $this->currentPersisterContext->selectColumnListSql;
}
@@ -495,6 +502,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
}
$this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
$this->updateFilterHash();
return $this->currentPersisterContext->selectColumnListSql;
}
@@ -38,7 +38,7 @@ class SingleTablePersister extends AbstractEntityInheritancePersister
*/
protected function getSelectColumnsSQL()
{
if ($this->currentPersisterContext->selectColumnListSql !== null) {
if ($this->currentPersisterContext->selectColumnListSql !== null && $this->isFilterHashUpToDate()) {
return $this->currentPersisterContext->selectColumnListSql;
}
@@ -92,6 +92,7 @@ class SingleTablePersister extends AbstractEntityInheritancePersister
}
$this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
$this->updateFilterHash();
return $this->currentPersisterContext->selectColumnListSql;
}
+3 -2
View File
@@ -378,7 +378,7 @@ EOPHP;
$class = $entityPersister->getClassMetadata();
foreach ($class->getReflectionProperties() as $property) {
if (isset($identifier[$property->name]) || ! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) {
if (isset($identifier[$property->name])) {
continue;
}
@@ -448,7 +448,7 @@ EOPHP;
foreach ($reflector->getProperties($filter) as $property) {
$name = $property->name;
if ($property->isStatic() || (($class->hasField($name) || $class->hasAssociation($name)) && ! isset($identifiers[$name]))) {
if ($property->isStatic() || ! isset($identifiers[$name])) {
continue;
}
@@ -568,6 +568,7 @@ EOPHP;
private function generateUseLazyGhostTrait(ClassMetadata $class): string
{
// @phpstan-ignore staticMethod.deprecated (Because we support Symfony < 7.3)
$code = ProxyHelper::generateLazyGhost($class->getReflectionClass());
$code = substr($code, 7 + (int) strpos($code, "\n{"));
$code = substr($code, 0, (int) strpos($code, "\n}"));
+1 -1
View File
@@ -1554,7 +1554,7 @@ class Parser
assert($this->lexer->lookahead !== null);
switch (true) {
case $this->isMathOperator($peek):
case $this->isMathOperator($peek) || $this->isMathOperator($glimpse):
$expr = $this->SimpleArithmeticExpression();
break;
+25 -1
View File
@@ -69,6 +69,13 @@ class ResultSetMapping
*/
public $fieldMappings = [];
/**
* Map field names for each class to alias
*
* @var array<class-string, array<string, array<string, string>>>
*/
public $columnAliasMappings = [];
/**
* Maps column names in the result set to the alias/field name to use in the mapped result.
*
@@ -335,7 +342,10 @@ class ResultSetMapping
// column name => alias of owner
$this->columnOwnerMap[$columnName] = $alias;
// field name => class name of declaring class
$this->declaringClasses[$columnName] = $declaringClass ?: $this->aliasMap[$alias];
$declaringClass = $declaringClass ?: $this->aliasMap[$alias];
$this->declaringClasses[$columnName] = $declaringClass;
$this->columnAliasMappings[$declaringClass][$alias][$fieldName] = $columnName;
if (! $this->isMixed && $this->scalarMappings) {
$this->isMixed = true;
@@ -344,6 +354,20 @@ class ResultSetMapping
return $this;
}
public function hasColumnAliasByField(string $alias, string $fieldName): bool
{
$declaringClass = $this->aliasMap[$alias];
return isset($this->columnAliasMappings[$declaringClass][$alias][$fieldName]);
}
public function getColumnAliasByField(string $alias, string $fieldName): string
{
$declaringClass = $this->aliasMap[$alias];
return $this->columnAliasMappings[$declaringClass][$alias][$fieldName];
}
/**
* Adds a joined entity result.
*
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use function method_exists;
/**
* Forward compatibility with Symfony Console 7.4
*
* @internal
*/
trait ApplicationCompatibility
{
private static function addCommandToApplication(Application $application, Command $command): ?Command
{
if (method_exists(Application::class, 'addCommand')) {
// @phpstan-ignore method.notFound (This method will be added in Symfony 7.4)
return $application->addCommand($command);
}
return $application->add($command);
}
}
+2 -2
View File
@@ -39,7 +39,7 @@ EOT
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$ui = new SymfonyStyle($input, $output);
$entityManager = $this->getEntityManager($input);
@@ -48,7 +48,7 @@ EOT
->getAllClassNames();
if (! $entityClassNames) {
$ui->caution(
$ui->getErrorStyle()->caution(
[
'You do not have any mapped Doctrine ORM entities according to the current configuration.',
'If you have entities or mapping files you should check your mapping configuration for errors.',
@@ -65,7 +65,7 @@ EOT
protected function execute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$ui = new SymfonyStyle($input, $output);
$entityManager = $this->getEntityManager($input);
@@ -98,7 +98,7 @@ EOT
$this->formatField('Embedded class?', $metadata->isEmbeddedClass),
$this->formatField('Parent classes', $metadata->parentClasses),
$this->formatField('Sub classes', $metadata->subClasses),
$this->formatField('Embedded classes', $metadata->subClasses),
$this->formatField('Embedded classes', $metadata->embeddedClasses),
$this->formatField('Named queries', $metadata->namedQueries),
$this->formatField('Named native queries', $metadata->namedNativeQueries),
$this->formatField('SQL result set mappings', $metadata->sqlResultSetMappings),
+3 -1
View File
@@ -23,6 +23,8 @@ use function class_exists;
*/
final class ConsoleRunner
{
use ApplicationCompatibility;
/**
* Create a Symfony Console HelperSet
*
@@ -91,7 +93,7 @@ final class ConsoleRunner
$connectionProvider = new ConnectionFromManagerProvider($entityManagerProvider);
if (class_exists(DBALConsole\Command\ImportCommand::class)) {
$cli->add(new DBALConsole\Command\ImportCommand());
self::addCommandToApplication($cli, new DBALConsole\Command\ImportCommand());
}
$cli->addCommands(
@@ -105,17 +105,24 @@ class LimitSubqueryOutputWalker extends SqlOutputWalker
$this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();
$this->rsm = $parserResult->getResultSetMapping();
$query = clone $query;
$cloneQuery = clone $query;
$cloneQuery->setParameters(clone $query->getParameters());
$cloneQuery->setCacheable(false);
foreach ($query->getHints() as $name => $value) {
$cloneQuery->setHint($name, $value);
}
// Reset limit and offset
$this->firstResult = $query->getFirstResult();
$this->maxResults = $query->getMaxResults();
$query->setFirstResult(0)->setMaxResults(null);
$this->firstResult = $cloneQuery->getFirstResult();
$this->maxResults = $cloneQuery->getMaxResults();
$cloneQuery->setFirstResult(0)->setMaxResults(null);
$this->em = $query->getEntityManager();
$this->em = $cloneQuery->getEntityManager();
$this->quoteStrategy = $this->em->getConfiguration()->getQuoteStrategy();
parent::__construct($query, $parserResult, $queryComponents);
parent::__construct($cloneQuery, $parserResult, $queryComponents);
}
/**
+9 -1
View File
@@ -49,6 +49,7 @@ use Doctrine\Persistence\PropertyChangedListener;
use Exception;
use InvalidArgumentException;
use RuntimeException;
use Symfony\Component\VarExporter\Hydrator;
use UnexpectedValueException;
use function array_chunk;
@@ -1059,7 +1060,9 @@ class UnitOfWork implements PropertyChangedListener
$this->entityStates[$oid] = self::STATE_MANAGED;
$this->scheduleForInsert($entity);
if (! isset($this->entityInsertions[$oid])) {
$this->scheduleForInsert($entity);
}
}
/** @param mixed[] $idValue */
@@ -2944,6 +2947,11 @@ EXCEPTION
if ($this->isUninitializedObject($entity)) {
$entity->__setInitialized(true);
if ($this->em->getConfiguration()->isLazyGhostObjectEnabled()) {
// Initialize properties that have default values to their default value (similar to what
Hydrator::hydrate($entity, (array) $class->reflClass->newInstanceWithoutConstructor());
}
} else {
if (
! isset($hints[Query::HINT_REFRESH])
@@ -0,0 +1,238 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\Tests\Models\Company\CompanyEmployee;
use Doctrine\Tests\OrmFunctionalTestCase;
use function count;
/**
* Functional tests for ordering with arithmetic expression.
*
* @group GH8011
*/
class GH8011Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
$this->useModelSet('company');
parent::setUp();
$this->generateFixture();
}
private function skipIfPostgres(string $test): void
{
$platform = $this->_em->getConnection()->getDatabasePlatform();
if ($platform instanceof PostgreSQLPlatform) {
self::markTestSkipped(
'The ' . $test . ' test does not work on postgresql (see https://github.com/doctrine/orm/pull/8012).'
);
}
}
public function testOrderWithArithmeticExpressionWithSingleValuedPathExpression(): void
{
$dql = 'SELECT p ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY p.id + p.id ASC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Benjamin E.', $result[0]->getName());
$this->assertEquals('Guilherme B.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithLiteralAndSingleValuedPathExpression(): void
{
$dql = 'SELECT p ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY 1 + p.id ASC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Benjamin E.', $result[0]->getName());
$this->assertEquals('Guilherme B.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithLiteralAndSingleValuedPathExpression2(): void
{
$dql = 'SELECT p ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY ((1 + p.id)) ASC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Benjamin E.', $result[0]->getName());
$this->assertEquals('Guilherme B.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithSingleValuedPathExpressionAndLiteral(): void
{
$dql = 'SELECT p ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY p.id + 1 ASC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Benjamin E.', $result[0]->getName());
$this->assertEquals('Guilherme B.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithResultVariableAndLiteral(): void
{
$this->skipIfPostgres(__FUNCTION__);
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY s + 1 DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithResultVariableAndLiteral2(): void
{
$this->skipIfPostgres(__FUNCTION__);
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY ((s + 1)) DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithLiteralAndResultVariable(): void
{
$this->skipIfPostgres(__FUNCTION__);
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY 1 + s DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithLiteralAndResultVariable2(): void
{
$this->skipIfPostgres(__FUNCTION__);
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY ((1 + s)) DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithResultVariableAndSingleValuedPathExpression(): void
{
$this->skipIfPostgres(__FUNCTION__);
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY s + p.id DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithResultVariableAndSingleValuedPathExpression2(): void
{
$this->skipIfPostgres(__FUNCTION__);
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY ((s + p.id)) DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithSingleValuedPathExpressionAndResultVariable(): void
{
$this->skipIfPostgres(__FUNCTION__);
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY p.id + s DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithLiteralAndResultVariableUsingHiddenResultVariable(): void
{
$dql = 'SELECT p, 1 + p.salary AS HIDDEN _order ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY _order DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function generateFixture(): void
{
$person1 = new CompanyEmployee();
$person1->setName('Benjamin E.');
$person1->setDepartment('IT');
$person1->setSalary(200000);
$person2 = new CompanyEmployee();
$person2->setName('Guilherme B.');
$person2->setDepartment('IT2');
$person2->setSalary(400000);
$this->_em->persist($person1);
$this->_em->persist($person2);
$this->_em->flush();
$this->_em->clear();
}
}
-2
View File
@@ -118,13 +118,11 @@ class ConnectionMock extends Connection
$this->_platformMock = $platform;
}
/** @return array */
public function getExecuteStatements(): array
{
return $this->_executeStatements;
}
/** @return array */
public function getDeletes(): array
{
return $this->_deletes;
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\GH11524;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="gh11524_entities")
*/
class GH11524Entity
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var int|null
*/
public $id = null;
/**
* @ORM\ManyToOne(targetEntity="GH11524Relation")
* @ORM\JoinColumn(name="relation_id", referencedColumnName="id", nullable=true)
*
* @var GH11524Relation|null
*/
public $relation = null;
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\GH11524;
use Doctrine\ORM\Event\PostLoadEventArgs;
class GH11524Listener
{
public function postLoad(PostloadEventArgs $eventArgs): void
{
$object = $eventArgs->getObject();
if (! $object instanceof GH11524Relation) {
return;
}
$object->setCurrentLocale('en');
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\GH11524;
use Doctrine\ORM\Mapping as ORM;
use LogicException;
/**
* @ORM\Entity
* @ORM\Table(name="gh11524_relations")
*/
class GH11524Relation
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var int|null
*/
public $id;
/**
* @ORM\Column(type="string")
*
* @var string
*/
public $name;
/**
* @var string|null
*/
private $currentLocale;
public function setCurrentLocale(string $locale): void
{
$this->currentLocale = $locale;
}
public function getTranslation(): string
{
if ($this->currentLocale === null) {
throw new LogicException('The current locale must be set to retrieve translation.');
}
return 'fake';
}
}
@@ -7,9 +7,11 @@ namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Doctrine\Tests\OrmFunctionalTestCase;
use function count;
use function iterator_to_array;
class EagerFetchCollectionTest extends OrmFunctionalTestCase
{
@@ -96,6 +98,16 @@ class EagerFetchCollectionTest extends OrmFunctionalTestCase
$this->assertIsString($query->getSql());
}
public function testSubselectFetchJoinWithAllowedWhenOverriddenNotEagerPaginator(): void
{
$query = $this->_em->createQuery('SELECT o, c FROM ' . EagerFetchOwner::class . ' o JOIN o.children c WITH c.id = 1');
$query->setMaxResults(1);
$query->setFetchMode(EagerFetchChild::class, 'owner', ORM\ClassMetadata::FETCH_LAZY);
$paginator = new Paginator($query, true);
$this->assertIsArray(iterator_to_array($paginator));
}
public function testEagerFetchWithIterable(): void
{
$this->createOwnerWithChildren(2);
@@ -66,10 +66,11 @@ class LifecycleCallbackTest extends OrmFunctionalTestCase
self::assertTrue($entity->postPersistCallbackInvoked);
$this->_em->clear();
LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation
$query = $this->_em->createQuery('select e from Doctrine\Tests\ORM\Functional\LifecycleCallbackTestEntity e');
$result = $query->getResult();
self::assertTrue($result[0]->postLoadCallbackInvoked);
self::assertTrue($result[0]::$postLoadCallbackInvoked);
$result[0]->value = 'hello again';
@@ -130,12 +131,14 @@ class LifecycleCallbackTest extends OrmFunctionalTestCase
$id = $entity->getId();
$this->_em->clear();
LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation
$reference = $this->_em->getReference(LifecycleCallbackTestEntity::class, $id);
self::assertFalse($reference->postLoadCallbackInvoked);
self::assertFalse($reference::$postLoadCallbackInvoked);
$this->assertTrue($this->isUninitializedObject($reference));
$reference->getValue(); // trigger proxy load
self::assertTrue($reference->postLoadCallbackInvoked);
self::assertTrue($reference::$postLoadCallbackInvoked);
}
/** @group DDC-958 */
@@ -148,13 +151,14 @@ class LifecycleCallbackTest extends OrmFunctionalTestCase
$id = $entity->getId();
$this->_em->clear();
LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation
$reference = $this->_em->find(LifecycleCallbackTestEntity::class, $id);
self::assertTrue($reference->postLoadCallbackInvoked);
$reference->postLoadCallbackInvoked = false;
self::assertTrue($reference::$postLoadCallbackInvoked);
$reference::$postLoadCallbackInvoked = false;
$this->_em->refresh($reference);
self::assertTrue($reference->postLoadCallbackInvoked, 'postLoad should be invoked when refresh() is called.');
self::assertTrue($reference::$postLoadCallbackInvoked, 'postLoad should be invoked when refresh() is called.');
}
/** @group DDC-113 */
@@ -197,6 +201,7 @@ class LifecycleCallbackTest extends OrmFunctionalTestCase
$this->_em->flush();
$this->_em->clear();
LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation
$dql = <<<'DQL'
SELECT
@@ -214,9 +219,9 @@ DQL;
->createQuery(sprintf($dql, $e1->getId(), $e2->getId()))
->getResult();
self::assertTrue(current($entities)->postLoadCallbackInvoked);
self::assertTrue(current($entities)::$postLoadCallbackInvoked);
self::assertTrue(current($entities)->postLoadCascaderNotNull);
self::assertTrue(current($entities)->cascader->postLoadCallbackInvoked);
self::assertTrue(current($entities)->cascader::$postLoadCallbackInvoked);
self::assertEquals(current($entities)->cascader->postLoadEntitiesCount, 2);
}
@@ -239,6 +244,8 @@ DQL;
$this->_em->flush();
$this->_em->clear();
LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation
LifecycleCallbackCascader::$postLoadCallbackInvoked = false;
$dql = <<<'DQL'
SELECT
@@ -256,7 +263,7 @@ DQL;
$result = iterator_to_array($query->iterate());
foreach ($result as $entity) {
self::assertTrue($entity[0]->postLoadCallbackInvoked);
self::assertTrue($entity[0]::$postLoadCallbackInvoked);
self::assertFalse($entity[0]->postLoadCascaderNotNull);
break;
@@ -265,7 +272,7 @@ DQL;
$iterableResult = iterator_to_array($query->toIterable());
foreach ($iterableResult as $entity) {
self::assertTrue($entity->postLoadCallbackInvoked);
self::assertTrue($entity::$postLoadCallbackInvoked);
self::assertFalse($entity->postLoadCascaderNotNull);
break;
@@ -283,6 +290,7 @@ DQL;
$this->_em->flush();
$this->_em->clear();
LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation
$query = $this->_em->createQuery(
'SELECT e FROM Doctrine\Tests\ORM\Functional\LifecycleCallbackTestEntity AS e'
@@ -291,7 +299,7 @@ DQL;
$result = iterator_to_array($query->iterate(null, Query::HYDRATE_SIMPLEOBJECT));
foreach ($result as $entity) {
self::assertTrue($entity[0]->postLoadCallbackInvoked);
self::assertTrue($entity[0]::$postLoadCallbackInvoked);
self::assertFalse($entity[0]->postLoadCascaderNotNull);
break;
@@ -300,7 +308,7 @@ DQL;
$result = iterator_to_array($query->toIterable([], Query::HYDRATE_SIMPLEOBJECT));
foreach ($result as $entity) {
self::assertTrue($entity->postLoadCallbackInvoked);
self::assertTrue($entity::$postLoadCallbackInvoked);
self::assertFalse($entity->postLoadCascaderNotNull);
break;
@@ -325,6 +333,8 @@ DQL;
$this->_em->flush();
$this->_em->clear();
LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation
LifecycleCallbackCascader::$postLoadCallbackInvoked = false;
$dql = <<<'DQL'
SELECT
@@ -342,9 +352,9 @@ DQL;
->createQuery($dql)->setParameter('entA_id', $entA->getId())
->getOneOrNullResult();
self::assertTrue($fetchedA->postLoadCallbackInvoked);
self::assertTrue($fetchedA::$postLoadCallbackInvoked);
foreach ($fetchedA->entities as $fetchJoinedEntB) {
self::assertTrue($fetchJoinedEntB->postLoadCallbackInvoked);
self::assertTrue($fetchJoinedEntB::$postLoadCallbackInvoked);
}
}
@@ -492,7 +502,7 @@ class LifecycleCallbackTestEntity
public $postPersistCallbackInvoked = false;
/** @var bool */
public $postLoadCallbackInvoked = false;
public static $postLoadCallbackInvoked = false;
/** @var bool */
public $postLoadCascaderNotNull = false;
@@ -546,7 +556,7 @@ class LifecycleCallbackTestEntity
/** @PostLoad */
public function doStuffOnPostLoad(): void
{
$this->postLoadCallbackInvoked = true;
self::$postLoadCallbackInvoked = true;
$this->postLoadCascaderNotNull = isset($this->cascader);
}
@@ -572,7 +582,7 @@ class LifecycleCallbackCascader
{
/* test stuff */
/** @var bool */
public $postLoadCallbackInvoked = false;
public static $postLoadCallbackInvoked = false;
/** @var int */
public $postLoadEntitiesCount = 0;
@@ -599,7 +609,7 @@ class LifecycleCallbackCascader
/** @PostLoad */
public function doStuffOnPostLoad(): void
{
$this->postLoadCallbackInvoked = true;
self::$postLoadCallbackInvoked = true;
$this->postLoadEntitiesCount = count($this->entities);
}
@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\ORM\Event\PrePersistEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\Tests\OrmFunctionalTestCase;
use function uniqid;
class PrePersistEventTest extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
EntityWithUnmappedEntity::class,
EntityWithCascadeAssociation::class
);
}
public function testCallingPersistInPrePersistHook(): void
{
$entityWithUnmapped = new EntityWithUnmappedEntity();
$entityWithCascade = new EntityWithCascadeAssociation();
$entityWithUnmapped->unmapped = $entityWithCascade;
$entityWithCascade->cascaded = $entityWithUnmapped;
$this->_em->getEventManager()->addEventListener(Events::prePersist, new PrePersistUnmappedPersistListener());
$this->_em->persist($entityWithUnmapped);
$this->assertTrue($this->_em->getUnitOfWork()->isScheduledForInsert($entityWithCascade));
$this->assertTrue($this->_em->getUnitOfWork()->isScheduledForInsert($entityWithUnmapped));
}
}
class PrePersistUnmappedPersistListener
{
public function prePersist(PrePersistEventArgs $args): void
{
$object = $args->getObject();
if ($object instanceof EntityWithUnmappedEntity) {
$uow = $args->getObjectManager()->getUnitOfWork();
if ($object->unmapped && ! $uow->isInIdentityMap($object->unmapped) && ! $uow->isScheduledForInsert($object->unmapped)) {
$args->getObjectManager()->persist($object->unmapped);
}
}
}
}
/** @Entity */
#[Entity]
class EntityWithUnmappedEntity
{
/**
* @var string
* @Id
* @Column(type="string", length=255)
* @GeneratedValue(strategy="NONE")
*/
#[Id]
#[Column(type: 'string', length: 255)]
#[GeneratedValue(strategy: 'NONE')]
public $id;
/** @var ?EntityWithCascadeAssociation */
public $unmapped = null;
public function __construct()
{
$this->id = uniqid(self::class, true);
}
}
/** @Entity */
#[Entity]
class EntityWithCascadeAssociation
{
/**
* @var string
* @Id
* @Column(type="string", length=255)
* @GeneratedValue(strategy="NONE")
*/
#[Id]
#[Column(type: 'string', length: 255)]
#[GeneratedValue(strategy: 'NONE')]
public $id;
/**
* @var ?EntityWithUnmappedEntity
* @ManyToOne(targetEntity=EntityWithUnmappedEntity::class, cascade={"persist"})
*/
#[ManyToOne(targetEntity: EntityWithUnmappedEntity::class, cascade: ['persist'])]
public $cascaded = null;
public function __construct()
{
$this->id = uniqid(self::class, true);
}
}
+5 -3
View File
@@ -558,7 +558,9 @@ class QueryTest extends OrmFunctionalTestCase
$this->_em->flush();
$this->_em->clear();
$data = $this->_em->createQuery('SELECT u FROM ' . CmsUser::class . ' u')
$query = 'SELECT u FROM ' . CmsUser::class . ' u ORDER BY u.username';
$data = $this->_em->createQuery($query)
->setFirstResult(1)
->setMaxResults(2)
->getResult();
@@ -567,7 +569,7 @@ class QueryTest extends OrmFunctionalTestCase
self::assertEquals('gblanco1', $data[0]->username);
self::assertEquals('gblanco2', $data[1]->username);
$data = $this->_em->createQuery('SELECT u FROM ' . CmsUser::class . ' u')
$data = $this->_em->createQuery($query)
->setFirstResult(3)
->setMaxResults(2)
->getResult();
@@ -576,7 +578,7 @@ class QueryTest extends OrmFunctionalTestCase
self::assertEquals('gblanco3', $data[0]->username);
self::assertEquals('gblanco4', $data[1]->username);
$data = $this->_em->createQuery('SELECT u FROM ' . CmsUser::class . ' u')
$data = $this->_em->createQuery($query)
->setFirstResult(3)
->setMaxResults(2)
->getScalarResult();
@@ -537,6 +537,21 @@ class SQLFilterTest extends OrmFunctionalTestCase
self::assertEquals(2, count($query->getResult()));
}
public function testOneToOneInverseSideWithFilter(): void
{
$this->loadFixtureData();
$conf = $this->_em->getConfiguration();
$conf->addFilter('country', '\Doctrine\Tests\ORM\Functional\CMSCountryFilter');
$this->_em->getFilters()->enable('country')->setParameterList('country', ['Germany'], Types::STRING);
$user = $this->_em->find(CmsUser::class, $this->userId);
self::assertNotEmpty($user->address);
$user2 = $this->_em->find(CmsUser::class, $this->userId2);
self::assertEmpty($user2->address);
}
public function testManyToManyFilter(): void
{
$this->loadFixtureData();
@@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\ORM\Id\AssignedGenerator;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\Tests\Models\Cache\Country;
use ReflectionProperty;
use function array_diff;
use function array_filter;
use function file_exists;
use function rmdir;
use function scandir;
use function strpos;
use function sys_get_temp_dir;
use const DIRECTORY_SEPARATOR;
/**
* @group DDC-2183
* @phpstan-type SupportedCacheUsage 0|ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE|ClassMetadata::CACHE_USAGE_READ_WRITE
*/
class SecondLevelCacheCountQueriesTest extends SecondLevelCacheFunctionalTestCase
{
/** @var string */
private $tmpDir;
protected function tearDown(): void
{
if ($this->tmpDir !== null && file_exists($this->tmpDir)) {
foreach (array_diff(scandir($this->tmpDir), ['.', '..']) as $f) {
rmdir($this->tmpDir . DIRECTORY_SEPARATOR . $f);
}
rmdir($this->tmpDir);
}
parent::tearDown();
}
/** @param SupportedCacheUsage $cacheUsage */
private function setupCountryModel(int $cacheUsage): void
{
$metadata = $this->_em->getClassMetaData(Country::class);
if ($cacheUsage === 0) {
$metadataCacheReflection = new ReflectionProperty(ClassMetadataInfo::class, 'cache');
$metadataCacheReflection->setAccessible(true);
$metadataCacheReflection->setValue($metadata, null);
return;
}
if ($cacheUsage === ClassMetadata::CACHE_USAGE_READ_WRITE) {
$this->tmpDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . self::class;
$this->secondLevelCacheFactory->setFileLockRegionDirectory($this->tmpDir);
}
$metadata->enableCache(['usage' => $cacheUsage]);
}
private function loadFixturesCountriesWithoutPostInsertIdentifier(): void
{
$metadata = $this->_em->getClassMetaData(Country::class);
$metadata->setIdGenerator(new AssignedGenerator());
$c1 = new Country('Brazil');
$c1->setId(10);
$c2 = new Country('Germany');
$c2->setId(20);
$this->countries[] = $c1;
$this->countries[] = $c2;
$this->_em->persist($c1);
$this->_em->persist($c2);
$this->_em->flush();
}
/** @param 'INSERT'|'UPDATE'|'DELETE' $type */
private function assertQueryCountByType(string $type, int $expectedCount): void
{
$queries = array_filter($this->getQueryLog()->queries, static function (array $entry) use ($type): bool {
return strpos($entry['sql'], $type) === 0;
});
self::assertCount($expectedCount, $queries);
}
/**
* @param SupportedCacheUsage $cacheUsage
*
* @dataProvider cacheUsageProvider
*/
public function testInsertWithPostInsertIdentifier(int $cacheUsage): void
{
$this->setupCountryModel($cacheUsage);
self::assertQueryCountByType('INSERT', 0);
$this->loadFixturesCountries();
self::assertCount(2, $this->countries);
self::assertQueryCountByType('INSERT', 2);
}
/**
* @param SupportedCacheUsage $cacheUsage
*
* @dataProvider cacheUsageProvider
*/
public function testInsertWithoutPostInsertIdentifier(int $cacheUsage): void
{
$this->setupCountryModel($cacheUsage);
self::assertQueryCountByType('INSERT', 0);
$this->loadFixturesCountriesWithoutPostInsertIdentifier();
self::assertCount(2, $this->countries);
self::assertQueryCountByType('INSERT', 2);
}
/**
/* @param SupportedCacheUsage $cacheUsage
*
* @dataProvider cacheUsageProvider
*/
public function testDelete(int $cacheUsage): void
{
$this->setupCountryModel($cacheUsage);
$this->loadFixturesCountries();
$c1 = $this->_em->find(Country::class, $this->countries[0]->getId());
$c2 = $this->_em->find(Country::class, $this->countries[1]->getId());
$this->_em->remove($c1);
$this->_em->remove($c2);
$this->_em->flush();
self::assertQueryCountByType('DELETE', 2);
}
/**
* @param SupportedCacheUsage $cacheUsage
*
* @dataProvider cacheUsageProvider
*/
public function testUpdate(int $cacheUsage): void
{
$this->setupCountryModel($cacheUsage);
$this->loadFixturesCountries();
$c1 = $this->_em->find(Country::class, $this->countries[0]->getId());
$c2 = $this->_em->find(Country::class, $this->countries[1]->getId());
$c1->setName('Czech Republic');
$c2->setName('Hungary');
$this->_em->persist($c1);
$this->_em->persist($c2);
$this->_em->flush();
self::assertQueryCountByType('UPDATE', 2);
}
/** @return list<array{SupportedCacheUsage}> */
public static function cacheUsageProvider(): array
{
return [
[0],
[ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE],
[ClassMetadata::CACHE_USAGE_READ_WRITE],
];
}
}
@@ -51,15 +51,20 @@ class DDC1690Test extends OrmFunctionalTestCase
$parentId = $parent->getId();
$childId = $child->getId();
unset($parent, $child);
DDC1690Parent::$addPropertyChangedListenerInvoked = false;
DDC1690Child::$addPropertyChangedListenerInvoked = false;
$parent = $this->_em->find(DDC1690Parent::class, $parentId);
$child = $this->_em->find(DDC1690Child::class, $childId);
self::assertTrue($parent::$addPropertyChangedListenerInvoked);
self::assertEquals(1, count($parent->listeners));
self::assertCount(0, $child->listeners);
$this->assertTrue($this->isUninitializedObject($child));
self::assertFalse($child::$addPropertyChangedListenerInvoked);
$this->_em->getUnitOfWork()->initializeObject($child);
self::assertTrue($child::$addPropertyChangedListenerInvoked);
self::assertCount(1, $child->listeners);
unset($parent, $child);
@@ -106,6 +111,11 @@ class NotifyBaseEntity implements NotifyPropertyChanged
*/
class DDC1690Parent extends NotifyBaseEntity
{
/**
* @var bool
*/
public static $addPropertyChangedListenerInvoked = false;
/**
* @var int
* @Id
@@ -151,11 +161,23 @@ class DDC1690Parent extends NotifyBaseEntity
{
return $this->child;
}
public function addPropertyChangedListener(PropertyChangedListener $listener): void
{
self::$addPropertyChangedListenerInvoked = true;
parent::addPropertyChangedListener($listener);
}
}
/** @Entity */
class DDC1690Child extends NotifyBaseEntity
{
/**
* @var bool
*/
public static $addPropertyChangedListenerInvoked = false;
/**
* @var int
* @Id
@@ -201,4 +223,11 @@ class DDC1690Child extends NotifyBaseEntity
{
return $this->parent;
}
public function addPropertyChangedListener(PropertyChangedListener $listener): void
{
self::$addPropertyChangedListenerInvoked = true;
parent::addPropertyChangedListener($listener);
}
}
@@ -57,16 +57,17 @@ class DDC2230Test extends OrmFunctionalTestCase
$this->_em->persist($insertedAddress);
$this->_em->flush();
$this->_em->clear();
DDC2230Address::$listener = null; // Reset the tracking state
$addressProxy = $this->_em->getReference(DDC2230Address::class, $insertedAddress->id);
assert($addressProxy instanceof DDC2230Address);
self::assertTrue($this->isUninitializedObject($addressProxy));
self::assertNull($addressProxy->listener);
self::assertNull($addressProxy::$listener);
$this->_em->getUnitOfWork()->initializeObject($addressProxy);
self::assertSame($this->_em->getUnitOfWork(), $addressProxy->listener);
self::assertSame($this->_em->getUnitOfWork(), $addressProxy::$listener);
}
}
@@ -102,12 +103,12 @@ class DDC2230Address implements NotifyPropertyChanged
*/
public $id;
/** @var \Doctrine\Common\PropertyChangedListener */
public $listener;
/** @var \Doctrine\Common\PropertyChangedListener|null */
public static $listener;
/** {@inheritDoc} */
public function addPropertyChangedListener(PropertyChangedListener $listener)
{
$this->listener = $listener;
self::$listener = $listener;
}
}
@@ -32,6 +32,15 @@ class GH10450Test extends OrmTestCase
yield 'Entity class that redeclares a protected field inherited from a base entity' => [GH10450EntityChildProtected::class];
yield 'Entity class that redeclares a protected field inherited from a mapped superclass' => [GH10450MappedSuperclassChildProtected::class];
}
public function testFieldsOfTransientClassesAreNotConsideredDuplicate(): void
{
$em = $this->getTestEntityManager();
$metadata = $em->getClassMetadata(GH10450Cat::class);
self::assertArrayHasKey('id', $metadata->fieldMappings);
}
}
/**
@@ -179,3 +188,38 @@ class GH10450MappedSuperclassChildProtected extends GH10450BaseMappedSuperclassP
*/
protected $field;
}
abstract class GH10450AbstractEntity
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*
* @var int
*/
protected $id;
}
/**
* @ORM\Entity
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorMap({ "cat": "GH10450Cat" })
* @ORM\DiscriminatorColumn(name="type")
*/
abstract class GH10450Animal extends GH10450AbstractEntity
{
/**
* @ORM\Column(type="text", name="base")
*
* @var string
*/
private $field;
}
/**
* @ORM\Entity
*/
class GH10450Cat extends GH10450Animal
{
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Events;
use Doctrine\Tests\Models\GH11524\GH11524Entity;
use Doctrine\Tests\Models\GH11524\GH11524Listener;
use Doctrine\Tests\Models\GH11524\GH11524Relation;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH11524Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
GH11524Entity::class,
GH11524Relation::class
);
$this->_em->getEventManager()->addEventListener(Events::postLoad, new GH11524Listener());
}
public function testPostLoadCalledOnProxy(): void
{
$relation = new GH11524Relation();
$relation->name = 'test';
$this->_em->persist($relation);
$entity = new GH11524Entity();
$entity->relation = $relation;
$this->_em->persist($entity);
$this->_em->flush();
$this->_em->clear();
$reloadedEntity = $this->_em->find(GH11524Entity::class, $entity->id);
$reloadedRelation = $reloadedEntity->relation;
$this->assertTrue($this->isUninitializedObject($reloadedRelation));
$this->assertSame('fake', $reloadedRelation->getTranslation(), 'The property set by the postLoad listener must get initialized on usage.');
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter;
use Doctrine\Tests\OrmFunctionalTestCase;
use function sprintf;
use function str_replace;
abstract class AbstractTest extends OrmFunctionalTestCase
{
protected function generateMessage(string $message): string
{
$log = $this->getLastLoggedQuery();
return sprintf("%s\nSQL: %s", $message, str_replace(['?'], (array) $log['params'], $log['sql']));
}
/**
* @param object ...$entities
*/
protected function clearCachedData(...$entities): void
{
foreach ($entities as $entity) {
$this->_em->refresh($entity);
}
}
/**
* @param object ...$entities
*/
protected function persistFlushClear(...$entities): void
{
foreach ($entities as $entity) {
$this->_em->persist($entity);
}
$this->_em->flush();
$this->_em->clear();
}
}
@@ -4,12 +4,11 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter;
use Doctrine\Tests\OrmFunctionalTestCase;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\Order;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\User;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\SQLFilter\CompanySQLFilter;
use function sprintf;
use function str_replace;
final class ChangeFiltersTest extends OrmFunctionalTestCase
final class ChangeFiltersTest extends AbstractTest
{
private const COMPANY_A = 'A';
private const COMPANY_B = 'B';
@@ -132,11 +131,4 @@ final class ChangeFiltersTest extends OrmFunctionalTestCase
self::assertInstanceOf(User::class, $order->user);
self::assertEquals($companyB['userId'], $order->user->id);
}
private function generateMessage(string $message): string
{
$log = $this->getLastLoggedQuery();
return sprintf("%s\nSQL: %s", $message, str_replace(['?'], (array) $log['params'], $log['sql']));
}
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity()
*/
class Insurance
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
/**
* @ORM\Column(type="string")
*
* @var string
*/
public $name;
/**
* @ORM\ManyToOne(targetEntity=Practice::class)
*
* @var Practice
*/
public $practice;
public function __construct(Practice $practice, string $name)
{
$this->practice = $practice;
$this->name = $name;
}
}
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter;
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\ORM\Mapping as ORM;
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity()
*/
class Patient
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
/**
* @ORM\Column(type="string")
*
* @var string
*/
public $name;
/**
* @ORM\OneToMany(targetEntity=PatientInsurance::class, mappedBy="patient", fetch="LAZY", cascade={"persist"})
*
* @var Collection<int, PatientInsurance>
*/
public $insurances;
public function __construct(string $name)
{
$this->name = $name;
$this->insurances = new ArrayCollection();
}
/**
* @return Collection<PrimaryPatInsurance>
*/
public function getPrimaryInsurances(): Collection
{
return $this->insurances->filter(static function (PatientInsurance $insurances) {
return $insurances instanceof PrimaryPatInsurance;
});
}
/**
* @return Collection<SecondaryPatInsurance>
*/
public function getSecondaryInsurances(): Collection
{
return $this->insurances->filter(static function (PatientInsurance $insurances) {
return $insurances instanceof SecondaryPatInsurance;
});
}
public function addPrimaryInsurance(Insurance $insurance): void
{
$this->insurances[] = new PrimaryPatInsurance($this, $insurance);
}
public function addSecondaryInsurance(Insurance $insurance): void
{
$this->insurances[] = new SecondaryPatInsurance($this, $insurance);
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity()
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorMap({"primary": PrimaryPatInsurance::class, "secondary": SecondaryPatInsurance::class})
* @ORM\DiscriminatorColumn(name="type", type="string")
*/
abstract class PatientInsurance
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
/**
* @ORM\ManyToOne(targetEntity=Insurance::class, fetch="EAGER", cascade={"persist"})
* @ORM\JoinColumn(referencedColumnName="id", nullable=false)
*
* @var Insurance
*/
public $insurance;
/**
* @ORM\ManyToOne(targetEntity=Patient::class, inversedBy="insurances")
*
* @var Patient
*/
public $patient;
public function __construct(Patient $patient, Insurance $insurance)
{
$this->patient = $patient;
$this->insurance = $insurance;
}
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Practice
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
/**
* @ORM\Column(type="string")
*
* @var string
*/
public $name;
public function __construct(string $name)
{
$this->name = $name;
}
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity()
*/
class PrimaryPatInsurance extends PatientInsurance
{
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity()
*/
class SecondaryPatInsurance extends PatientInsurance
{
}
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter;
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\ORM\Mapping as ORM;
@@ -2,10 +2,12 @@
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter;
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\SQLFilter;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\Order;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\User;
use function sprintf;
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\SQLFilter;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\Insurance;
use function sprintf;
class PracticeContextSQLFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
if (! $this->hasParameter('practiceId') || $this->getParameter('practiceId') === null) {
return '';
}
if ($targetEntity->getName() === Insurance::class) {
return sprintf(
'%s.%s = %s',
$targetTableAlias,
$targetEntity->associationMappings['practice']['joinColumns'][0]['name'],
$this->getParameter('practiceId')
);
}
return '';
}
}
@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter;
use Doctrine\ORM\Query\Filter\SQLFilter;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\Insurance;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\Patient;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\PatientInsurance;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\Practice;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\PrimaryPatInsurance;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\SecondaryPatInsurance;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\SQLFilter\PracticeContextSQLFilter;
final class SwitchContextTest extends AbstractTest
{
/**
* @var SQLFilter|PracticeContextSQLFilter
*/
private $sqlFilter;
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
Practice::class,
Patient::class,
PatientInsurance::class,
PrimaryPatInsurance::class,
SecondaryPatInsurance::class,
Insurance::class
);
$this->_em->getConfiguration()->addFilter(PracticeContextSQLFilter::class, PracticeContextSQLFilter::class);
$this->sqlFilter = $this->_em->getFilters()->enable(PracticeContextSQLFilter::class);
}
/**
* @return array{Patient, Patient}
*/
private function fixtureGenerate(): array
{
$practiceA = new Practice('Practice A');
$practiceB = new Practice('Practice B');
$insuranceAetna = new Insurance($practiceA, 'Aetna in Practice A');
$insuranceBHumana = new Insurance($practiceB, 'Humana in Practice B');
$insuranceBCustom = new Insurance($practiceB, 'Custom in Practice B');
$patientEgor = new Patient('Egor');
$patientEgor->addPrimaryInsurance($insuranceAetna);
$patientEgor->addPrimaryInsurance($insuranceBHumana);
$patientGena = new Patient('Gena');
$patientGena->addPrimaryInsurance($insuranceBHumana);
$patientGena->addSecondaryInsurance($insuranceBCustom);
$this->persistFlushClear(
$practiceA,
$practiceB,
$insuranceAetna,
$insuranceBHumana,
$insuranceBCustom,
$patientEgor,
$patientGena
);
return [
$this->_em->getReference(Patient::class, $patientEgor->id),
$this->_em->getReference(Patient::class, $patientGena->id),
];
}
/**
* @param callable(): T $callback
*
* @return T
*
* @template T
*/
private function switchPracticeContext(Practice $practice, callable $callback)
{
$this->sqlFilter->setParameter('practiceId', $practice->id);
try {
return $callback();
} finally {
$this->sqlFilter->setParameter('practiceId', null);
}
}
public function testSwitchContext(): void
{
[$patientEgor, $patentGena] = $this->fixtureGenerate();
$practiceA = $this->_em->getRepository(Practice::class)->findOneBy(['name' => 'Practice A']);
$practiceB = $this->_em->getRepository(Practice::class)->findOneBy(['name' => 'Practice B']);
$this->switchPracticeContext($practiceA, function () use ($patientEgor, $patentGena): void {
$this->clearCachedData($patentGena, $patientEgor);
self::assertCount(1, $patientEgor->insurances);
self::assertInstanceOf(PrimaryPatInsurance::class, $patientEgor->getPrimaryInsurances()->first());
self::assertEquals('Aetna in Practice A', $patientEgor->getPrimaryInsurances()->first()->insurance->name);
self::assertCount(0, $patentGena->insurances);
});
$this->switchPracticeContext($practiceB, function () use ($patientEgor, $patentGena): void {
$this->clearCachedData($patentGena, $patientEgor);
self::assertCount(1, $patientEgor->insurances);
self::assertInstanceOf(PrimaryPatInsurance::class, $patientEgor->getPrimaryInsurances()->first());
self::assertEquals('Humana in Practice B', $patientEgor->getPrimaryInsurances()->first()->insurance->name);
self::assertCount(2, $patentGena->insurances);
self::assertInstanceOf(PrimaryPatInsurance::class, $patentGena->getPrimaryInsurances()->first());
self::assertInstanceOf(SecondaryPatInsurance::class, $patentGena->getSecondaryInsurances()->first());
self::assertEquals('Humana in Practice B', $patentGena->getPrimaryInsurances()->first()->insurance->name);
self::assertEquals('Custom in Practice B', $patentGena->getSecondaryInsurances()->first()->insurance->name);
});
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilterAndIndexedRelation;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="Category_Master")
*/
class Category
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*
* @var int
*/
public $id;
/**
* @ORM\Column(type="string")
*
* @var string
*/
public $name;
/**
* @ORM\Column(type="string")
*
* @var string
*/
public $type;
public function __construct(string $name, string $type)
{
$this->name = $name;
$this->type = $type;
}
}
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilterAndIndexedRelation;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
use function sprintf;
class CategoryTypeSQLFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
if ($targetEntity->getName() === Category::class) {
return sprintf('%s.%s = %s', $targetTableAlias, $targetEntity->fieldMappings['type']['fieldName'], $this->getParameter('type'));
}
return '';
}
}
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilterAndIndexedRelation;
use Doctrine\Tests\OrmFunctionalTestCase;
final class ChangeFiltersTest extends OrmFunctionalTestCase
{
private const COMPANY_A = 'A';
private const CAT_BAR = 'bar';
private const CAT_FOO = 'foo';
public function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
Company::class,
Category::class,
]);
}
private function prepareData(): void
{
$cat1 = new Category('cat1', self::CAT_FOO);
$cat2 = new Category('cat2', self::CAT_BAR);
$companyA = new Company(self::COMPANY_A, [$cat1, $cat2]);
$this->_em->persist($cat1);
$this->_em->persist($cat2);
$this->_em->persist($companyA);
$this->_em->flush();
$this->_em->clear();
}
public function testIndexAliasUpdatedWithUpdatedFilter(): void
{
$this->prepareData();
$company = $this->_em->getRepository(Company::class)->findOneBy([]);
self::assertCount(2, $company->categories);
self::assertEquals([self::CAT_FOO, self::CAT_BAR], $company->categories->map(static function (Category $c): string {
return $c->type;
})->getValues());
$this->_em->clear();
$this->_em->getConfiguration()->addFilter(CategoryTypeSQLFilter::class, CategoryTypeSQLFilter::class);
$this->_em->getFilters()->enable(CategoryTypeSQLFilter::class)->setParameter('type', self::CAT_FOO);
$company = $this->_em->getRepository(Company::class)->findOneBy([]);
self::assertCount(1, $company->categories);
self::assertEquals([self::CAT_FOO], $company->categories->map(static function (Category $c): string {
return $c->type;
})->getValues());
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilterAndIndexedRelation;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="Company_Master")
*/
class Company
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*
* @var int
*/
public $id;
/**
* @ORM\Column(type="string")
*
* @var string
*/
public $name;
/**
* @ORM\ManyToMany(targetEntity="Category", fetch="EAGER", indexBy="type")
*
* @var Collection<int, Category>
*/
public $categories;
/** @param Category[] $categories */
public function __construct(string $name, array $categories)
{
$this->name = $name;
$this->categories = new ArrayCollection($categories);
}
}
@@ -7,12 +7,15 @@ namespace Doctrine\Tests\ORM\Hydration;
use Doctrine\DBAL\Types\Type as DBALType;
use Doctrine\ORM\Internal\Hydration\HydrationException;
use Doctrine\ORM\Internal\Hydration\SimpleObjectHydrator;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Tests\DbalTypes\GH8565EmployeePayloadType;
use Doctrine\Tests\DbalTypes\GH8565ManagerPayloadType;
use Doctrine\Tests\Mocks\ArrayResultFactory;
use Doctrine\Tests\Models\CMS\CmsAddress;
use Doctrine\Tests\Models\Company\CompanyPerson;
use Doctrine\Tests\Models\Enums\Scale;
use Doctrine\Tests\Models\Enums\Unit;
use Doctrine\Tests\Models\GH8565\GH8565Employee;
use Doctrine\Tests\Models\GH8565\GH8565Manager;
use Doctrine\Tests\Models\GH8565\GH8565Person;
@@ -155,4 +158,28 @@ class SimpleObjectHydratorTest extends HydrationTestCase
$result = $hydrator->hydrateAll($stmt, $rsm);
self::assertEquals($result[0], $expectedEntity);
}
/**
* @requires PHP 8.1
*/
public function testNotListedValueInEnumArray(): void
{
$this->expectException(MappingException::class);
$this->expectExceptionMessage('Case "unknown_case" is not listed in enum "Doctrine\Tests\Models\Enums\Unit"');
$rsm = new ResultSetMapping();
$rsm->addEntityResult(Scale::class, 's');
$rsm->addFieldResult('s', 's__id', 'id');
$rsm->addFieldResult('s', 's__supported_units', 'supportedUnits');
$rsm->addEnumResult('s__supported_units', Unit::class);
$resultSet = [
[
's__id' => 1,
's__supported_units' => 'g,m,unknown_case',
],
];
$stmt = ArrayResultFactory::createFromArray($resultSet);
$hydrator = new SimpleObjectHydrator($this->entityManager);
$hydrator->hydrateAll($stmt, $rsm);
}
}
@@ -84,7 +84,7 @@ class ReflectionPropertiesGetterTest extends TestCase
public function testPropertyGetterIsIdempotent(): void
{
$getter = (new ReflectionPropertiesGetter(new RuntimeReflectionService()));
$getter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
self::assertSame(
$getter->getProperties(ClassWithMixedProperties::class),
@@ -110,7 +110,7 @@ class ReflectionPropertiesGetterTest extends TestCase
->expects(self::atLeastOnce())
->method('getAccessibleProperty');
$getter = (new ReflectionPropertiesGetter($reflectionService));
$getter = new ReflectionPropertiesGetter($reflectionService);
self::assertEmpty($getter->getProperties(ClassWithMixedProperties::class));
}
@@ -127,7 +127,7 @@ class ReflectionPropertiesGetterTest extends TestCase
$reflectionService->expects(self::never())->method('getAccessibleProperty');
$getter = (new ReflectionPropertiesGetter($reflectionService));
$getter = new ReflectionPropertiesGetter($reflectionService);
self::assertEmpty($getter->getProperties(ClassWithMixedProperties::class));
}
@@ -85,7 +85,7 @@ class ORMInvalidArgumentExceptionTest extends TestCase
. 'persist operations for entity: stdClass@' . spl_object_id($entity1)
. '. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity '
. 'or configure cascade persist this association in the mapping for example '
. '@ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem '
. '#[ORM\ManyToOne(..., cascade: [\'persist\'])]. If you cannot find out which entity causes the problem '
. 'implement \'baz1#__toString()\' to get a clue.',
],
'two entities found' => [
@@ -104,13 +104,13 @@ class ORMInvalidArgumentExceptionTest extends TestCase
. 'cascade persist operations for entity: stdClass@' . spl_object_id($entity1) . '. '
. 'To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity '
. 'or configure cascade persist this association in the mapping for example '
. '@ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem '
. '#[ORM\ManyToOne(..., cascade: [\'persist\'])]. If you cannot find out which entity causes the problem '
. 'implement \'baz1#__toString()\' to get a clue.' . "\n"
. ' * A new entity was found through the relationship \'foo2#bar2\' that was not configured to '
. 'cascade persist operations for entity: stdClass@' . spl_object_id($entity2) . '. To solve '
. 'this issue: Either explicitly call EntityManager#persist() on this unknown entity or '
. 'configure cascade persist this association in the mapping for example '
. '@ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem '
. '#[ORM\ManyToOne(..., cascade: [\'persist\'])]. If you cannot find out which entity causes the problem '
. 'implement \'baz2#__toString()\' to get a clue.',
],
'two entities found, one is stringable' => [
@@ -124,7 +124,7 @@ class ORMInvalidArgumentExceptionTest extends TestCase
. 'persist operations for entity: ThisIsAStringRepresentationOfEntity3'
. '. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity '
. 'or configure cascade persist this association in the mapping for example '
. '@ManyToOne(..,cascade={"persist"}).',
. '#[ORM\ManyToOne(..., cascade: [\'persist\'])].',
],
];
}
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\ApplicationCompatibility;
use Doctrine\ORM\Tools\Console\Command\ClearCache\CollectionRegionCommand;
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
use Doctrine\Tests\Models\Cache\State;
@@ -14,6 +15,8 @@ use Symfony\Component\Console\Tester\CommandTester;
/** @group DDC-2183 */
class ClearCacheCollectionRegionCommandTest extends OrmFunctionalTestCase
{
use ApplicationCompatibility;
/** @var Application */
private $application;
@@ -29,7 +32,7 @@ class ClearCacheCollectionRegionCommandTest extends OrmFunctionalTestCase
$this->command = new CollectionRegionCommand(new SingleManagerProvider($this->_em));
$this->application = new Application();
$this->application->add($this->command);
self::addCommandToApplication($this->application, $this->command);
}
public function testClearAllRegion(): void
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\ApplicationCompatibility;
use Doctrine\ORM\Tools\Console\Command\ClearCache\EntityRegionCommand;
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
use Doctrine\Tests\Models\Cache\Country;
@@ -17,6 +18,8 @@ use function trim;
/** @group DDC-2183 */
class ClearCacheEntityRegionCommandTest extends OrmFunctionalTestCase
{
use ApplicationCompatibility;
/** @var Application */
private $application;
@@ -32,7 +35,7 @@ class ClearCacheEntityRegionCommandTest extends OrmFunctionalTestCase
$this->command = new EntityRegionCommand(new SingleManagerProvider($this->_em));
$this->application = new Application();
$this->application->add($this->command);
self::addCommandToApplication($this->application, $this->command);
}
public function testClearAllRegion(): void
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\ApplicationCompatibility;
use Doctrine\ORM\Tools\Console\Command\ClearCache\QueryRegionCommand;
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
use Doctrine\Tests\OrmFunctionalTestCase;
@@ -13,6 +14,8 @@ use Symfony\Component\Console\Tester\CommandTester;
/** @group DDC-2183 */
class ClearCacheQueryRegionCommandTest extends OrmFunctionalTestCase
{
use ApplicationCompatibility;
/** @var Application */
private $application;
@@ -28,7 +31,7 @@ class ClearCacheQueryRegionCommandTest extends OrmFunctionalTestCase
$this->command = new QueryRegionCommand(new SingleManagerProvider($this->_em));
$this->application = new Application();
$this->application->add($this->command);
self::addCommandToApplication($this->application, $this->command);
}
public function testClearAllRegion(): void
@@ -7,6 +7,7 @@ namespace Doctrine\Tests\ORM\Tools\Console\Command;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\Console\ApplicationCompatibility;
use Doctrine\ORM\Tools\Console\Command\EnsureProductionSettingsCommand;
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
use Doctrine\Tests\DoctrineTestCase;
@@ -18,6 +19,8 @@ use function array_merge;
class EnsureProductionSettingsCommandTest extends DoctrineTestCase
{
use ApplicationCompatibility;
public function testExecute(): void
{
$em = $this->createMock(EntityManagerInterface::class);
@@ -103,7 +106,7 @@ class EnsureProductionSettingsCommandTest extends DoctrineTestCase
array $input = []
): int {
$application = new Application();
$application->add(new EnsureProductionSettingsCommand(new SingleManagerProvider($em)));
self::addCommandToApplication($application, new EnsureProductionSettingsCommand(new SingleManagerProvider($em)));
$command = $application->find('orm:ensure-production-settings');
$tester = new CommandTester($command);
@@ -7,6 +7,7 @@ namespace Doctrine\Tests\ORM\Tools\Console\Command;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Tools\Console\ApplicationCompatibility;
use Doctrine\ORM\Tools\Console\Command\GenerateRepositoriesCommand;
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
use Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper;
@@ -35,6 +36,8 @@ use const DIRECTORY_SEPARATOR;
class GenerateRepositoriesCommandTest extends OrmFunctionalTestCase
{
use ApplicationCompatibility;
/** @var Application */
private $application;
@@ -53,7 +56,7 @@ class GenerateRepositoriesCommandTest extends OrmFunctionalTestCase
$metadataDriver->addPaths([__DIR__ . '/../../../../Models/DDC3231/']);
$this->application = new Application();
$this->application->add(new GenerateRepositoriesCommand(new SingleManagerProvider($this->_em)));
self::addCommandToApplication($this->application, new GenerateRepositoriesCommand(new SingleManagerProvider($this->_em)));
}
public function tearDown(): void
@@ -163,7 +166,7 @@ class GenerateRepositoriesCommandTest extends OrmFunctionalTestCase
$application = new Application();
$application->setHelperSet(new HelperSet(['em' => new EntityManagerHelper($em)]));
$application->add(new GenerateRepositoriesCommand());
self::addCommandToApplication($application, new GenerateRepositoriesCommand());
$command = $application->find('orm:generate-repositories');
$tester = new CommandTester($command);
@@ -7,6 +7,7 @@ namespace Doctrine\Tests\ORM\Tools\Console\Command;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\Tools\Console\ApplicationCompatibility;
use Doctrine\ORM\Tools\Console\Command\InfoCommand;
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
use Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper;
@@ -20,6 +21,8 @@ use Symfony\Component\Console\Tester\CommandTester;
class InfoCommandTest extends OrmFunctionalTestCase
{
use ApplicationCompatibility;
/** @var Application */
private $application;
@@ -35,7 +38,7 @@ class InfoCommandTest extends OrmFunctionalTestCase
$this->application = new Application();
$this->application->add(new InfoCommand(new SingleManagerProvider($this->_em)));
self::addCommandToApplication($this->application, new InfoCommand(new SingleManagerProvider($this->_em)));
$this->command = $this->application->find('orm:info');
$this->tester = new CommandTester($this->command);
@@ -66,7 +69,7 @@ class InfoCommandTest extends OrmFunctionalTestCase
$application = new Application();
$application->setHelperSet(new HelperSet(['em' => new EntityManagerHelper($em)]));
$application->add(new InfoCommand());
self::addCommandToApplication($application, new InfoCommand());
$command = $application->find('orm:info');
$tester = new CommandTester($command);
@@ -105,7 +108,7 @@ class InfoCommandTest extends OrmFunctionalTestCase
$application = new Application();
$application->setHelperSet(new HelperSet(['em' => new EntityManagerHelper($em)]));
$application->add(new InfoCommand());
self::addCommandToApplication($application, new InfoCommand());
$command = $application->find('orm:info');
$tester = new CommandTester($command);
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\ApplicationCompatibility;
use Doctrine\ORM\Tools\Console\Command\MappingDescribeCommand;
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
use Doctrine\Tests\Models\Cache\AttractionInfo;
@@ -18,6 +19,8 @@ use Symfony\Component\Console\Tester\CommandTester;
*/
class MappingDescribeCommandTest extends OrmFunctionalTestCase
{
use ApplicationCompatibility;
/** @var Application */
private $application;
@@ -32,7 +35,7 @@ class MappingDescribeCommandTest extends OrmFunctionalTestCase
parent::setUp();
$this->application = new Application();
$this->application->add(new MappingDescribeCommand(new SingleManagerProvider($this->_em)));
self::addCommandToApplication($this->application, new MappingDescribeCommand(new SingleManagerProvider($this->_em)));
$this->command = $this->application->find('orm:mapping:describe');
$this->tester = new CommandTester($this->command);
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\ApplicationCompatibility;
use Doctrine\ORM\Tools\Console\Command\RunDqlCommand;
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
use Doctrine\Tests\Models\Generic\DateTimeModel;
@@ -20,6 +21,8 @@ use function trim;
*/
class RunDqlCommandTest extends OrmFunctionalTestCase
{
use ApplicationCompatibility;
/** @var Application */
private $application;
@@ -38,7 +41,7 @@ class RunDqlCommandTest extends OrmFunctionalTestCase
$this->command = new RunDqlCommand(new SingleManagerProvider($this->_em));
$this->application = new Application();
$this->application->add($this->command);
self::addCommandToApplication($this->application, $this->command);
$this->tester = new CommandTester($this->command);
}
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Tools\Console\Command;
use Doctrine\DBAL\Platforms\SqlitePlatform;
use Doctrine\ORM\Tools\Console\ApplicationCompatibility;
use Doctrine\ORM\Tools\Console\Command\ValidateSchemaCommand;
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
use Doctrine\Tests\OrmFunctionalTestCase;
@@ -19,6 +20,8 @@ use Symfony\Component\Console\Tester\CommandTester;
*/
class ValidateSchemaCommandTest extends OrmFunctionalTestCase
{
use ApplicationCompatibility;
/** @var ValidateSchemaCommand */
private $command;
@@ -34,7 +37,7 @@ class ValidateSchemaCommandTest extends OrmFunctionalTestCase
}
$application = new Application();
$application->add(new ValidateSchemaCommand(new SingleManagerProvider($this->_em)));
self::addCommandToApplication($application, new ValidateSchemaCommand(new SingleManagerProvider($this->_em)));
$this->command = $application->find('orm:validate-schema');
$this->tester = new CommandTester($this->command);
@@ -46,6 +46,24 @@ final class LimitSubqueryOutputWalkerTest extends PaginationTestCase
$this->entityManager->getConnection()->setDatabasePlatform($platform);
}
public function testSubqueryClonedCompletely(): void
{
$query = $this->createQuery('SELECT p FROM Doctrine\Tests\ORM\Tools\Pagination\MyBlogPost p');
$query->setParameter('dummy-param', 123);
$query->setHint('dummy-hint', 'dummy-value');
$query->setCacheable(true);
$walker = new LimitSubqueryOutputWalker($query, new Query\ParserResult(), []);
self::assertNotSame($query, $walker->getQuery());
self::assertTrue($walker->getQuery()->hasHint('dummy-hint'));
self::assertSame('dummy-value', $walker->getQuery()->getHint('dummy-hint'));
self::assertNotSame($query->getParameters(), $walker->getQuery()->getParameters());
self::assertInstanceOf(Query\Parameter::class, $param = $walker->getQuery()->getParameter('dummy-param'));
self::assertSame(123, $param->getValue());
self::assertFalse($walker->getQuery()->isCacheable());
}
public function testLimitSubquery(): void
{
$query = $this->createQuery('SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\MyBlogPost p JOIN p.category c JOIN p.author a');

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