Compare commits

...

194 Commits
3.0.1 ... 3.2.0

Author SHA1 Message Date
Grégoire Paris
37946d3a21 Merge pull request #11472 from nicolas-grekas/no-readonly
Remove readonly modifier from EntityManager
2024-05-23 16:27:52 +02:00
Nicolas Grekas
baf96cdad4 Remove readonly modifier from EntityManager 2024-05-23 14:33:01 +02:00
Alexander M. Turek
ce09c96427 Deprecate the NotSupported exception (#11470) 2024-05-22 21:53:12 +02:00
Alexander M. Turek
ae659fe650 Deprecate SequenceGenerator implementing Serializable (#11468) 2024-05-22 10:48:46 +02:00
Alexander M. Turek
0a177d5074 Merge branch '3.1.x' into 3.2.x
* 3.1.x:
  Psalm 5.24.0 (#11467)
  PHPStan 1.11.1 (#11466)
2024-05-21 14:24:54 +02:00
Alexander M. Turek
dbfe47b07b Merge branch '2.19.x' into 3.1.x
* 2.19.x:
  Psalm 5.24.0 (#11467)
  PHPStan 1.11.1 (#11466)
2024-05-21 14:24:20 +02:00
Alexander M. Turek
d31aabb40c Psalm 5.24.0 (#11467) 2024-05-21 14:21:50 +02:00
Alexander M. Turek
22b1f52c1c Merge branch '3.1.x' into 3.2.x
* 3.1.x:
  Fix failed merge (#11464)
  Test with actual lock modes (#11465)
  Backport test for Query::setLockMode() (#11463)
  Fix return type of Query::getLockMode() (#11462)
2024-05-21 14:04:38 +02:00
Alexander M. Turek
d66884403f PHPStan 1.11.1 (#11466) 2024-05-21 13:32:25 +02:00
Alexander M. Turek
a90ee5c495 Merge branch '2.19.x' into 3.1.x
* 2.19.x:
  Test with actual lock modes (#11465)
  Backport test for Query::setLockMode() (#11463)
2024-05-21 12:52:03 +02:00
Alexander M. Turek
11270425e5 Fix failed merge (#11464) 2024-05-21 12:30:56 +02:00
Alexander M. Turek
552eae37a3 Test with actual lock modes (#11465) 2024-05-21 12:30:36 +02:00
Alexander M. Turek
ee4b03aa78 Backport test for Query::setLockMode() (#11463) 2024-05-21 12:30:16 +02:00
Alexander M. Turek
f1246d57c2 Fix return type of Query::getLockMode() (#11462)
… for DBAL 4
2024-05-21 12:30:01 +02:00
Alexander M. Turek
a14ef7c279 Merge branch '3.1.x' into 3.2.x
* 3.1.x:
  Using an integer as discriminator value with ORM v3
  Using an integer as discriminator value with ORM v3
  Bump ramsey/composer-install from 2 to 3 (#11442)
  Use ramsey/composer-install in PHPBench workflow (#11444)
  Bump doctrine/.github from 3.0.0 to 5.0.1
  Upgrade codecov/codecov-action
  Setup Dependabot
2024-05-21 08:42:44 +02:00
Alexander M. Turek
54c29140fa Merge branch '2.19.x' into 3.1.x
* 2.19.x:
  Bump ramsey/composer-install from 2 to 3 (#11442)
  Bump doctrine/.github from 3.0.0 to 5.0.1
  Upgrade codecov/codecov-action
2024-05-21 08:42:11 +02:00
Grégoire Paris
daa99f197b Merge pull request #11456 from prohalexey/IntegerDescriminatorInInstanceOf
Using an integer as discriminator value with ORM v3
2024-05-17 08:19:34 +02:00
Alexey Prohorov
2b04cc2e3f Using an integer as discriminator value with ORM v3
This fixes a bug that occurred when configuring integers as discriminator values and using DQL instanceOf function in the queries. Doctrine throws a type error whenever the application generates these queries.
2024-05-16 11:53:29 +03:00
Grégoire Paris
3d9af3187f Merge pull request #11425 from prohalexey/FixForIntegerDescriminatorValue
Discriminator value could be an integer
2024-05-15 11:31:09 +02:00
Alexey Prohorov
e83d8a80ba Using an integer as discriminator value with ORM v3
This fixes a bug that occurred when configuring integers as discriminator values. Doctrine throws a type error whenever the application generates queries.
2024-05-15 10:42:04 +03:00
dependabot[bot]
c5291b4de8 Bump ramsey/composer-install from 2 to 3 (#11442)
Bumps [ramsey/composer-install](https://github.com/ramsey/composer-install) from 2 to 3.
- [Release notes](https://github.com/ramsey/composer-install/releases)
- [Commits](https://github.com/ramsey/composer-install/compare/v2...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-05 23:47:43 +02:00
Grégoire Paris
029ca611f0 Use ramsey/composer-install in PHPBench workflow (#11444)
It will handle caching for us.
2024-05-05 23:38:41 +02:00
Grégoire Paris
831d86548c Merge pull request #11441 from doctrine/dependabot/github_actions/2.19.x/doctrine/dot-github-5.0.1
Bump doctrine/.github from 3.0.0 to 5.0.1
2024-05-05 23:23:39 +02:00
dependabot[bot]
f26b3b9cf9 Bump doctrine/.github from 3.0.0 to 5.0.1
Bumps [doctrine/.github](https://github.com/doctrine/.github) from 3.0.0 to 5.0.1.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/3.0.0...5.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-05 21:17:24 +00:00
Grégoire Paris
9e7715f678 Merge pull request #11439 from greg0ire/setup-dependabot
Setup Dependabot
2024-05-05 23:16:51 +02:00
Grégoire Paris
9ab84f7478 Merge pull request #11440 from greg0ire/update-codecov
Upgrade codecov/codecov-action
2024-05-05 22:56:55 +02:00
Grégoire Paris
e6bb4ef20e Upgrade codecov/codecov-action 2024-05-05 22:43:51 +02:00
Grégoire Paris
0e26e3ed50 Setup Dependabot
Targeting 2.19.x, since we want the updates to bubble up. Since
Dependabot has had no effect on doctrine/dbal yet, I suppose that means
that "dependabot.yml" must be present on the default branch.
2024-05-05 22:41:40 +02:00
Grégoire Paris
63315c8e4a Merge pull request #11434 from doctrine/3.1.x-merge-up-into-3.2.x_sjLAVzN7
Merge release 3.1.3 into 3.2.x
2024-04-30 09:57:38 +02:00
Grégoire Paris
8ca99fdfdc Merge pull request #11433 from greg0ire/3.1.x
Merge 3.0.x up into 3.1.x
2024-04-30 09:14:13 +02:00
Grégoire Paris
2d8e466636 Merge remote-tracking branch 'origin/2.19.x' into 3.1.x 2024-04-30 09:00:59 +02:00
Grégoire Paris
94986af284 Merge pull request #11430 from W0rma/fix-deprecation-layer-orm-exception
Fix deprecation layer of Doctrine\ORM\ORMException
2024-04-30 08:49:54 +02:00
W0rma
ad5c8e4bdc Make test compatible with PHP 7.1 2024-04-30 08:35:06 +02:00
W0rma
c363f55ad1 Fix deprecation layer 2024-04-29 14:48:36 +02:00
Grégoire Paris
c973a62272 Merge pull request #11429 from SenseException/unused-test-group
Remove unused test group
2024-04-27 11:42:05 +02:00
Grégoire Paris
8d3446015a Merge pull request #11428 from xificurk/keep-removed-entity-in-identity-map
Prevent creation of new MANAGED entity instance by reloading REMOVED entity from database
2024-04-27 11:40:56 +02:00
Claudio Zizza
4e335f4044 Remove unused test group 2024-04-27 10:46:19 +02:00
Petr Morávek
bb36d49b38 Keep entities in identity map until the scheduled deletions are executed.
If the entity gets reloaded from database before the deletions are
executed UnitOfWork needs to be able to return the original instance in
REMOVED state.
2024-04-26 21:54:02 +02:00
Grégoire Paris
2b81a8e260 Merge pull request #11426 from nasimic/patch-1
Update association-mapping.rst
2024-04-26 21:27:07 +02:00
Nasimi Mammadov
7d3b3f28e9 Update association-mapping.rst
Changed capitalized column names to lowercase for consistency. Other occurances of column names mentioned as lowercase several times at this same page.
2024-04-26 21:24:28 +02:00
Simon Podlipsky
cbec236e8b fix: always cleanup in AbstractHydrator::toIterable() (#11101)
Previously it didn't cleanup anything as long as the iteration hasn't reached the final row.

Co-authored-by: Oleg Andreyev <oleg.andreyev@lampa.lv>
2024-04-25 10:32:40 +02:00
Grégoire Paris
306963fe79 Merge pull request #11422 from tomasz-ryba/bugfix/fetch-eager-order-by
Bugfix: respect orderBy for fetch EAGER mode
2024-04-25 00:09:43 +02:00
Tomasz Ryba
fb4578406f Respect orderBy for EAGER fetch mode
EAGER fetch mode ignores orderBy as of changes introduced with #8391

Fixes #11163
Fixes #11381
2024-04-24 22:44:16 +02:00
Grégoire Paris
bdc41e2b5e Merge pull request #11420 from tyteen4a03/patch-1
fix(docs): typo
2024-04-22 15:40:39 +02:00
Timothy Choi
90376a6431 fix(docs): typo 2024-04-22 15:30:56 +02:00
Alexander M. Turek
97634ae6a1 Merge branch '3.1.x' into 3.2.x
* 3.1.x:
  Revert "Merge pull request #11399 from ThomasLandauer/issue-11377" (#11415)
  Fix BIGINT validation (#11414)
  docs: update PHP version in doc
  Fix fromMappingArray definition
  Fix templated phpdoc return type (#11407)
  [Documentation] Merging "Query Result Formats" with "Hydration Modes"
  SchemaValidator: Changing mapping of BIGINT to string|int
  Fix psalm errors: remove override of template type
  Update dql-doctrine-query-language.rst
  Adding `NonUniqueResultException`
  [Documentation] Query Result Formats
2024-04-15 16:31:08 +02:00
Alexander M. Turek
f79d166a4e Merge branch '2.19.x' into 3.1.x
* 2.19.x:
  Fix BIGINT validation (#11414)
  Fix templated phpdoc return type (#11407)
  [Documentation] Merging "Query Result Formats" with "Hydration Modes"
  Fix psalm errors: remove override of template type
  Update dql-doctrine-query-language.rst
  Adding `NonUniqueResultException`
  [Documentation] Query Result Formats
2024-04-15 16:20:40 +02:00
Alexander M. Turek
9c22814cfa Revert "Merge pull request #11399 from ThomasLandauer/issue-11377" (#11415)
This reverts commit cbb6c897de, reversing
changes made to 9c56071392.
2024-04-15 16:03:33 +02:00
Alexander M. Turek
b274893486 Fix BIGINT validation (#11414) 2024-04-15 15:11:10 +02:00
Grégoire Paris
e0e55dc9c5 Merge pull request #11413 from Nayte91/doc/php-version
docs: update PHP version in doc
2024-04-15 14:32:58 +02:00
Nayte
010b1e0886 docs: update PHP version in doc 2024-04-15 09:46:56 +02:00
Grégoire Paris
93eb8a1bcb Merge pull request #11408 from VincentLanglet/fix/fromMappingArray
Fix fromMappingArray definition
2024-04-04 00:20:16 +02:00
Vincent Langlet
1464827220 Fix fromMappingArray definition 2024-04-03 19:54:16 +02:00
Vincent Langlet
8709fb38b0 Fix templated phpdoc return type (#11407)
* Improve getClassMetadata phpdoc

* Update baseline
2024-04-01 12:44:58 +02:00
Grégoire Paris
cbb6c897de Merge pull request #11399 from ThomasLandauer/issue-11377
SchemaValidator: Changing mapping of BIGINT to string|int
2024-03-28 21:37:07 +01:00
Grégoire Paris
e9e60f2fbc Merge pull request #11403 from ThomasLandauer/patch-10
[Documentation] Merging "Query Result Formats" with "Hydration Modes"
2024-03-28 07:51:53 +01:00
Thomas Landauer
5f3c1dbab8 [Documentation] Merging "Query Result Formats" with "Hydration Modes"
Page: https://www.doctrine-project.org/projects/doctrine-orm/en/2.19/reference/dql-doctrine-query-language.html#query-result-formats

As announced in https://github.com/doctrine/orm/pull/11372#issue-2190613801, I merged the (mostly) identical sections.

* I changed the `const`s from `Query` to `AbstractQuery`
* I deleted this - mainly cause I didn't find a nice place for it:
    > In parentheses are the constants of the ``Query`` class which you can use with the
general-purpose method ``Query::execute(array $params = [], $hydrationMode = Query::HYDRATE_OBJECT)``.
2024-03-27 13:52:50 +01:00
Thomas Landauer
753bc16c0b SchemaValidator: Changing mapping of BIGINT to string|int 2024-03-23 14:49:02 +01:00
Grégoire Paris
6090141e0b Merge pull request #11389 from tantegerda1/2.19.x
Fix psalm errors: remove override of template type
2024-03-23 11:13:39 +01:00
Grégoire Paris
e4a6c041b5 Merge pull request #11372 from ThomasLandauer/patch-12
[Documentation] Query Result Formats
2024-03-23 10:07:42 +01:00
Ludwig Rafelsberger
c54c557e02 Fix psalm errors: remove override of template type
See https://github.com/doctrine/collections/issues/368 for the same
issue in doctrine/collections which has been fixed there.

The issue happens when using ->contains(). Running psalm emits

  > InvalidArgument - Argument 1 of Doctrine\ORM\PersistentCollection::contains
  > expects
  > TMaybeContained:fn-doctrine\common\collections\readablecollection::contains
  > as mixed, but … provided.

Solution: we should either not define @template TMaybeContained or
re-define the complete psalm docblock from ReadableCollection.

Repairing the docblock necessitates an update to the psalm baseline:
one "known issue" is no longer an issue and thus removed.
2024-03-22 11:05:00 +01:00
Thomas Landauer
46d0865339 Update dql-doctrine-query-language.rst 2024-03-21 17:55:39 +01:00
Alexander M. Turek
4672d284ff Merge branch '3.1.x' into 3.2.x
* 3.1.x:
  Adjust PHPBench mocks
  Set column length explicitly (#11393)
  Add missing import
  Remove unused variable (#11391)
  Fixed proxy initialization for EnumReflectionProperty
  Remove older versions from the docs (#11383)
  [Documentation] Removing "Doctrine Mapping Types" ... (#11384)
  [GH-11185] Bugfix: do not use collection batch loading for indexBy assocations. (#11380)
  Improve lazy ghost performance by avoiding self-referencing closure. (#11376)
  Remove outdated git metadata files (#11362)
  Switch join columns around, otherwise index doesnt match
  Key on fk
  Fix entities and mapping.
  Minor code style fix in AbstractRemoteControl
  Do not schedule batch loading for target classes with composite identifier.
  Cleanup tests not to use model sets.
  provides a test case for github issue 11154
2024-03-21 14:44:21 +01:00
Alexander M. Turek
9c56071392 Adjust PHPBench mocks 2024-03-21 12:37:52 +01:00
Alexander M. Turek
0a1988b349 Merge branch '2.19.x' into 3.1.x
* 2.19.x:
  Set column length explicitly (#11393)
  Add missing import
  Remove unused variable (#11391)
  [Documentation] Removing "Doctrine Mapping Types" ... (#11384)
  [GH-11185] Bugfix: do not use collection batch loading for indexBy assocations. (#11380)
  Improve lazy ghost performance by avoiding self-referencing closure. (#11376)
2024-03-21 12:05:05 +01:00
Alexander M. Turek
1a5a4c674a Set column length explicitly (#11393) 2024-03-21 12:01:42 +01:00
Alexander M. Turek
95795c87a8 Add missing import 2024-03-21 10:38:59 +01:00
Alexander M. Turek
db6e702088 Remove unused variable (#11391) 2024-03-21 10:32:55 +01:00
Grégoire Paris
4175edf311 Merge pull request #11387 from valkars/enum-reflection
Fixed proxy initialization for EnumReflectionProperty
2024-03-21 10:20:42 +01:00
Valentin Karnauhov
67ac5a82da Fixed proxy initialization for EnumReflectionProperty 2024-03-21 10:54:26 +02:00
Claudio Zizza
e384978e0b Remove older versions from the docs (#11383)
To reduce Algolia operations and indexes older versions get removed
2024-03-20 23:35:25 +01:00
Thomas Landauer
5ccbc201bf [Documentation] Removing "Doctrine Mapping Types" ... (#11384)
... in favor of https://www.doctrine-project.org/projects/doctrine-dbal/en/3.8/reference/types.html#reference

Page: https://www.doctrine-project.org/projects/doctrine-orm/en/2.19/reference/basic-mapping.html#doctrine-mapping-types

As announced in https://github.com/doctrine/dbal/pull/6336#issuecomment-2003720361 , the goal is to remove this duplicated type information from ORM and replace it with a link to DBAL.

In https://github.com/doctrine/dbal/pull/6341 , I'm adding any detail which I'm deleting here to the DBAL.
2024-03-20 23:34:10 +01:00
Benjamin Eberlei
d15624f72f [GH-11185] Bugfix: do not use collection batch loading for indexBy assocations. (#11380) 2024-03-20 15:45:47 +01:00
Benjamin Eberlei
9d1a4973ae Improve lazy ghost performance by avoiding self-referencing closure. (#11376)
* Improve lazy ghost performance by avoiding self-referencing closure.

Co-authored-by: Nicolas Grekas <nicolas.grekas@gmail.com>

* update baselien

---------

Co-authored-by: Nicolas Grekas <nicolas.grekas@gmail.com>
2024-03-19 09:19:25 +01:00
Grégoire Paris
55c4845d57 Merge pull request #11379 from greg0ire/3.1.x
Merge 2.19.x up into 3.1.x
2024-03-18 21:07:12 +01:00
Grégoire Paris
a38f473a92 Merge remote-tracking branch 'origin/2.19.x' into 3.1.x 2024-03-18 20:57:55 +01:00
Grégoire Paris
40a0964f06 Merge pull request #11289 from themasch/reproduce-issue-11154-composite-key-eager-fetch-one
Do not use batch loading for collections with composite identifier
2024-03-18 20:12:56 +01:00
Grégoire Paris
08a9e60ed0 Remove outdated git metadata files (#11362)
Some of it seems related to the previous documentation build system,
some of it seems related to IntelliJ.
2024-03-17 23:06:30 +01:00
Benjamin Eberlei
3e3c023c95 Switch join columns around, otherwise index doesnt match 2024-03-17 19:50:56 +01:00
Benjamin Eberlei
5e6d5c06a9 Key on fk 2024-03-17 19:43:26 +01:00
Benjamin Eberlei
1622b7877d Fix entities and mapping. 2024-03-17 18:02:11 +01:00
Benjamin Eberlei
80aae2796d Merge pull request #11373 from kaznovac/patch-3
Minor code style fix in AbstractRemoteControl
2024-03-17 17:20:01 +01:00
Marko Kaznovac
528ef40fc4 Minor code style fix in AbstractRemoteControl 2024-03-17 15:55:54 +01:00
Thomas Landauer
4b4b9b7b6f Adding NonUniqueResultException 2024-03-17 12:25:05 +01:00
Thomas Landauer
ae842259f5 [Documentation] Query Result Formats
Page: https://www.doctrine-project.org/projects/doctrine-orm/en/2.19/reference/dql-doctrine-query-language.html#query-result-formats

Follow-up of https://github.com/doctrine/orm/pull/11359

The table I suggested is probably not working, since the text for each method is too long. And what I really wanted is to make it more *scanable*. So I tried boldfacing - if this doesn't work, I'll try something else.

Questions:

1. This section here is basically the same as https://www.doctrine-project.org/projects/doctrine-orm/en/2.10/reference/dql-doctrine-query-language.html#hydration-modes ! So I'll try to merge them (in another PR), OK? I think the list is a better format (more scanable) - since those methods all work the same, there's no need for a full-blown code sample for each, IMO.

2. `getSingleColumnResult()` is missing.
2024-03-17 12:24:10 +01:00
Grégoire Paris
69f51cc794 Merge pull request #11371 from doctrine/3.1.x
Merge 3.1.x up into 3.2.x
2024-03-17 10:43:23 +01:00
Grégoire Paris
7178b9d6b7 Merge pull request #11370 from greg0ire/forgotten-array-access
Avoid another occurrence of ArrayAccess
2024-03-17 10:42:16 +01:00
Grégoire Paris
8a14eee67a Avoid another occurrence of ArrayAccess 2024-03-17 09:38:54 +01:00
Grégoire Paris
f9331ee2b9 Merge pull request #11369 from doctrine/3.1.x
Merge 3.1.x up into 3.2.x
2024-03-17 09:34:02 +01:00
Grégoire Paris
c5315f86fb Merge pull request #11368 from greg0ire/address-deprecation
Avoid array access
2024-03-17 09:02:38 +01:00
Grégoire Paris
5820bb8f49 Avoid array access
It is deprecated.
2024-03-16 23:37:00 +01:00
Grégoire Paris
cb05f1aadf Merge pull request #11357 from DaDeather/11351-add-deprecation-for-obsolete-indexes-and-unique-constraint-properties-of-table-attribute
Deprecate obsolete and unnecessary properties from Table attribute (#11351)
2024-03-16 23:24:55 +01:00
Grégoire Paris
ab616f1a1d Merge pull request #11364 from doctrine/3.1.x
Merge 3.1.x up into 3.2.x
2024-03-16 23:24:34 +01:00
Benjamin Eberlei
820a0da4c1 Do not schedule batch loading for target classes with composite identifier. 2024-03-16 23:05:28 +01:00
Benjamin Eberlei
fcd02b1ee2 Cleanup tests not to use model sets. 2024-03-16 23:04:57 +01:00
Grégoire Paris
b0d07ffaba Merge pull request #11363 from greg0ire/3.1.x
Merge 2.19.x up into 3.1.x
2024-03-16 21:48:21 +01:00
Grégoire Paris
196d3a6996 Merge remote-tracking branch 'origin/2.19.x' into 3.1.x 2024-03-16 21:38:16 +01:00
Grégoire Paris
abcad6fa45 Merge pull request #11090 from dbannik/2.17.x-failed-getting-entity-with-fetch-eager
[2.17.x] Failed getting entity with fetch eager property
2024-03-16 21:23:13 +01:00
Benjamin Eberlei
1b6cf58a1a Rename tables to avoid pg related illegal table name 2024-03-16 21:08:30 +01:00
Benjamin Eberlei
6501890ab5 Static analysis enforces the extra isset() even though that just masks no sense. 2024-03-16 20:48:15 +01:00
Benjamin Eberlei
e399d21fb3 Simplify condition, improve comment on this edge case. 2024-03-16 20:41:24 +01:00
Benjamin Eberlei
16f355f0cc Remove tests for already working case as they add no value other than exploration, and we only need the regression test. 2024-03-16 20:31:09 +01:00
Ismail Özgün Turan
7d1444e5b6 Deprecate obsolete and unnecessary properties from Table attribute (#11351)
The properties `indexes` and `uniqueConstraints` were used by the
`AnnotationDriver` but were never implemented for the `AttributeDriver`.
Since the `AnnotationDriver` doesn't exist anymore these can become
deprecated and will then be removed afterwards.
2024-03-15 12:23:03 +01:00
Grégoire Paris
25d5936337 Merge pull request #11354 from greg0ire/depr--complete
Deprecate --complete option of orm:schema-tool:update
2024-03-15 07:42:35 +01:00
Grégoire Paris
68f9bf5dfa Deprecate --complete option of orm:schema-tool:update
It achieves nothing anymore.
2024-03-14 13:14:55 +01:00
Grégoire Paris
94d45a036f Merge pull request #11347 from greg0ire/remove-orphan
Remove guides-specific markup
2024-03-11 21:08:16 +01:00
Grégoire Paris
9acca2252f Remove guides-specific markup
doctrine/rst-parser does not appear to support orphan metadata yet, and
renders it verbatim on the website.

Let's move this to the CI job.
2024-03-11 20:31:22 +01:00
Alexander M. Turek
716fc97b70 Merge branch '2.19.x' into 3.1.x
* 2.19.x:
  Prepare releases 2.19 and 3.1 (#11335)
2024-03-03 18:45:20 +01:00
Alexander M. Turek
a809a71aa6 Prepare releases 2.19 and 3.1 (#11335) 2024-03-03 18:43:41 +01:00
Alexander M. Turek
4617a5e310 Merge branch '2.19.x' into 3.1.x
* 2.19.x:
2024-03-03 18:03:42 +01:00
Alexander M. Turek
e77c5a3a5e Merge branch '3.0.x' into 3.1.x
* 3.0.x:
  Fix annotation
  Bump CI workflows (#11336)
  Fix SchemaTool::getSchemaFromMetadata() uniqueConstraint without a predefined name (#11314)
2024-03-03 18:03:17 +01:00
Alexander M. Turek
c3cc0fdd8c Merge branch '2.18.x' into 3.0.x
* 2.18.x:
  Fix annotation
  Bump CI workflows (#11336)
  Fix SchemaTool::getSchemaFromMetadata() uniqueConstraint without a predefined name (#11314)
2024-03-03 17:13:16 +01:00
Alexander M. Turek
bd4449c462 Merge branch '2.18.x' into 2.19.x
* 2.18.x:
  Fix annotation
2024-03-03 16:49:22 +01:00
Alexander M. Turek
e3e96745cc Fix annotation 2024-03-03 16:49:00 +01:00
Alexander M. Turek
12e0cefba1 Merge branch '2.18.x' into 2.19.x
* 2.18.x:
  Bump CI workflows (#11336)
  Fix SchemaTool::getSchemaFromMetadata() uniqueConstraint without a predefined name (#11314)
2024-03-03 16:46:50 +01:00
Alexander M. Turek
21221f73cc Bump CI workflows (#11336) 2024-03-03 16:46:12 +01:00
Rok Motaln
ab5e9e393b Fix SchemaTool::getSchemaFromMetadata() uniqueConstraint without a predefined name (#11314)
* Fix loading SchemaTool::getSchemaFromMetadata() uniqueConstraint without a name

Fixes a type miss-match exception when reading a UniqueConstraint defined on an Entity which doesn't have a predefined name.

* Fix deprecation on DBAL 3

---------

Co-authored-by: Alexander M. Turek <me@derrabus.de>
2024-03-03 16:02:48 +01:00
Alexander M. Turek
507c73c073 Merge branch '2.19.x' into 3.1.x
* 2.19.x:
  Refator array_map into simple loop for performance. (#11332)
2024-03-03 14:21:12 +01:00
Grégoire Paris
ba0ea8953b Use class from persistence package (#11330)
* Use class from persistence package

It is meant to remove duplication between the ORM and the ODM.

* Update UPGRADE.md

Co-authored-by: Steve Todd <stodd@mashbo.com>

---------

Co-authored-by: Alexander M. Turek <me@derrabus.de>
Co-authored-by: Steve Todd <stodd@mashbo.com>
2024-03-03 13:08:37 +01:00
Benjamin Eberlei
e62571c8f4 Refator array_map into simple loop for performance. (#11332) 2024-03-02 23:11:11 +01:00
Alexander M. Turek
53763d432b Merge branch '2.19.x' into 3.1.x
* 2.19.x:
2024-03-01 10:57:47 +01:00
Alexander M. Turek
154920a0b3 Merge branch '3.0.x' into 3.1.x
* 3.0.x:
  Psalm 5.22.2 (#11326)
2024-03-01 10:56:28 +01:00
Alexander M. Turek
b8d0a85017 Merge branch '2.18.x' into 3.0.x
* 2.18.x:
  Psalm 5.22.2 (#11326)
2024-03-01 10:51:50 +01:00
Alexander M. Turek
98f9de2af6 Merge branch '2.18.x' into 2.19.x
* 2.18.x:
  Psalm 5.22.2 (#11326)
2024-03-01 10:48:12 +01:00
Alexander M. Turek
52a6a21387 Psalm 5.22.2 (#11326) 2024-03-01 10:47:18 +01:00
Alexander M. Turek
cb497826be Bump Doctrine Collections to 2.2 (#11325) 2024-03-01 09:27:30 +01:00
Alexander M. Turek
ba0d3842a9 Merge branch '3.0.x' into 3.1.x
* 3.0.x:
  Use enum_exists() for enums
2024-03-01 08:56:20 +01:00
Alexander M. Turek
bf49055a1f Use enum_exists() for enums 2024-03-01 08:56:07 +01:00
Alexander M. Turek
29e1935c65 Merge branch '3.0.x' into 3.1.x
* 3.0.x:
  Remove PHP 7 workarounds (#11324)
2024-03-01 08:51:50 +01:00
Alexander M. Turek
694413a888 Remove PHP 7 workarounds (#11324) 2024-03-01 08:51:21 +01:00
Alexander M. Turek
33e02b2796 Merge branch '2.19.x' into 3.1.x
* 2.19.x:
2024-02-29 17:17:59 +01:00
Alexander M. Turek
26f7588479 Merge branch '3.0.x' into 3.1.x
* 3.0.x:
  PHPStan 1.10.59 (#11320)
  Address deprecations from Collection 2.2 (#11315)
  Fix sql walker phpdoc
2024-02-29 17:17:42 +01:00
Alexander M. Turek
20a6efdff6 Merge branch '2.18.x' into 3.0.x
* 2.18.x:
  PHPStan 1.10.59 (#11320)
2024-02-29 16:52:42 +01:00
Alexander M. Turek
83c81f6c41 Merge branch '2.18.x' into 2.19.x
* 2.18.x:
  PHPStan 1.10.59 (#11320)
2024-02-29 16:48:49 +01:00
Alexander M. Turek
4fc8629414 PHPStan 1.10.59 (#11320) 2024-02-29 16:47:35 +01:00
Grégoire Paris
791667a9e4 Merge pull request #11317 from doctrine/2.18.x
Merge 2.18.x up into 2.19.x
2024-02-28 23:05:01 +01:00
Grégoire Paris
95da667862 Merge remote-tracking branch 'origin/2.18.x' into 3.0.x 2024-02-28 22:57:35 +01:00
Alexander M. Turek
feb27f00c1 Address deprecations from Collection 2.2 (#11315) 2024-02-27 17:37:52 +01:00
Grégoire Paris
c02ddd692f Merge pull request #11312 from greg0ire/3.1.x
Merge 3.0.x up into 3.1.x
2024-02-26 20:53:44 +01:00
Grégoire Paris
151a3fba9d Merge remote-tracking branch 'origin/3.0.x' into 3.1.x 2024-02-26 20:39:59 +01:00
Grégoire Paris
b187bc8588 Merge pull request #11308 from greg0ire/throw-instead-of-assert
Throw a full-fledged exception on invalid call
2024-02-26 20:38:49 +01:00
Grégoire Paris
1e056842fe Merge pull request #11310 from greg0ire/3.1.x
Merge 2.19.x up into 3.1.x
2024-02-26 20:38:36 +01:00
Grégoire Paris
ebb0c67ecc Merge remote-tracking branch 'origin/2.19.x' into 3.1.x 2024-02-26 08:48:34 +01:00
Grégoire Paris
abd9186d00 Merge pull request #11309 from greg0ire/deprecate-invalid-call
Deprecate invalid method call
2024-02-26 08:45:50 +01:00
Grégoire Paris
719d007a81 Merge pull request #11298 from VincentLanglet/sqlWalkerPhpdoc
Fix sqlWalker::walkSimpleArithmeticExpression phpdoc
2024-02-26 08:21:47 +01:00
Grégoire Paris
08d3f72755 Deprecate invalid method call
`getAssociationMappedByTargetField()` returns `null` when called with
the owning side of an association.
This is undocumented and wrong because the phpdoc advertises a string as
a return type.

Instead, callers should ensure they are calling that method with an
inverse side.

Closes #11250
2024-02-25 22:09:47 +01:00
Grégoire Paris
3f7a3333ad Throw a full-fledged exception on invalid call
In 2.x, getAssociationMappedByTargetField() used to return null when
called with the owning side of an association.
That was undocumented and wrong because the phpdoc advertises a string
as a return type.

In 6ce0cf4a3d, I wrongly assumed that
nobody would be calling this method with the owning side of an
association.

Let us throw a full fledged exception and advertise the proper way of
avoiding this situation.

Closes #11250
2024-02-25 21:49:03 +01:00
Grégoire Paris
2a8802af12 Merge pull request #11305 from doctrine/typo
Remove extra word
2024-02-25 13:20:55 +01:00
Grégoire Paris
9cc11d2541 Remove extra word 2024-02-25 11:20:44 +01:00
Grégoire Paris
ee5b2ce5b0 Merge pull request #11294 from greg0ire/sa-fqcn
Translate comment into code and annotations
2024-02-25 10:01:27 +01:00
Grégoire Paris
d54c9678d0 Deprecate passing null to ClassMetadata::fullyQualifiedClassName()
It can easily be avoided by the only caller.
2024-02-25 09:31:02 +01:00
Grégoire Paris
859e6af972 Translate comment into code and annotations
The phpdoc comment for the return type of
ClassMetadata::fullyQualifiedClassName() says that the return type will
be null if the input value is null. I have made it more precise by
using "if and only if", made the null check more strict and translated
that into template annotations. Also, since we say we return a
class-string, I've asserted that.
2024-02-24 22:13:13 +01:00
Grégoire Paris
8c3c9f115d Merge pull request #11303 from doctrine/3.0.x
Merge 3.0.x up into 3.1.x
2024-02-24 21:21:41 +01:00
Grégoire Paris
3907872046 Merge pull request #11302 from greg0ire/3.0.x
Merge 2.18.x up into 3.0.x
2024-02-24 21:03:34 +01:00
Grégoire Paris
779781173a Merge pull request #11301 from doctrine/2.18.x
Merge 2.18.x up into 2.19.x
2024-02-24 20:51:45 +01:00
Grégoire Paris
54cd70002c Merge remote-tracking branch 'origin/2.18.x' into 3.0.x 2024-02-24 20:47:36 +01:00
Grégoire Paris
76c4539ffa Merge pull request #11293 from greg0ire/wrong-type
Remove wrong annotation about return type
2024-02-24 13:05:08 +01:00
Vincent Langlet
0f8d193512 Fix sql walker phpdoc 2024-02-23 15:11:15 +01:00
Grégoire Paris
cc314d0fb7 Remove wrong annotation about return type
Although this method is guaranteed to return either null or something
that can be used as a fully qualified class name, it never actually
checks that the class actually exists. Adding such a check breaks
several tests, including some that expect a exceptions at some later
points in the execution.
2024-02-22 23:14:52 +01:00
Alexander M. Turek
2df4d75565 Merge branch '3.0.x' into 3.1.x
* 3.0.x:
  Test different ways of settings query parameters
  Be less restrictive in DiscriminatorColumnMapping phpdoc (#11226)
  Allow (Array)ParameterType in QueryBuilder
2024-02-22 13:26:11 +01:00
Alexander M. Turek
dc21ab63ac Merge branch '2.19.x' into 3.1.x
* 2.19.x:
  Backport QueryParameterTest (#11288)
2024-02-22 13:25:30 +01:00
Alexander M. Turek
c9c493b2fe Merge branch '2.18.x' into 2.19.x
* 2.18.x:
  Backport QueryParameterTest (#11288)
2024-02-22 13:23:21 +01:00
Mark Schmale
8d4718f875 provides a test case for github issue 11154
After 2.17 (some?) EAGER fetched OneToMany associations stopped working, if they have multiple join columns. Loads for these associations will trigger a `MessingPositionalParameter` exception "Positional parameter at index 1 does not have a bound value".

This test case should reproduce this issue, so it can be fixed.
2024-02-22 10:58:50 +01:00
Grégoire Paris
e4c27092cd Merge pull request #11276 from greg0ire/no-cast-filelock
Remove implicit casts in FileLock.php
2024-02-21 22:59:47 +01:00
Grégoire Paris
adadf1fb90 Do not implicitly cast glob's return type
The comment above mentions that on some platforms, it might return
false, and this is why there is a check in the first place. Let us do
exactly what is mentioned in the comment.
2024-02-21 22:36:01 +01:00
Grégoire Paris
380b5b62ef Do not cast file_put_contents's return type
If $lock->value was an empty string, this would fix a bug, but it never
is, it is a uniqid-generated string.
2024-02-21 22:35:38 +01:00
Grégoire Paris
a0e7a59572 Do not implicitly cast getLockTime()'s return type
This fixes a bug for files last modified on 1970-01-01 00:00:00, so… not
worth backporting IMO.
2024-02-21 22:35:38 +01:00
Grégoire Paris
fb6c0c1d8b Do not implicitly cast getLockContent()'s return value
Lock files are supposed to contain uniqid()-generated values, so they
cannot be falsy strings, but if they did, this would fix a bug.
2024-02-21 22:35:36 +01:00
Alexander M. Turek
fcf1116e33 Merge branch '3.0.x' into 3.1.x
* 3.0.x:
  Remove broken assertion from DateAddFunction and DateSubFunction (#11243)
  Remove unused trait
  [Documentation] Adding link to Postgres upgrade article (#11257)
  fix: support array-type arg in QB variadic calls (#11242)
2024-02-21 19:28:15 +01:00
Alexander M. Turek
78dc63df27 Merge branch '2.19.x' into 3.1.x
* 2.19.x:
  Fix Static Analysis folder reference (#11281)
  docs: recommend safer way to disable logging (#11269)
  Remove unused baseline entries
  Treat '0' as a legitimate trim char
  Add type field mapper documentation to the sidebar
  Mark document as orphan
  Use correction sectionauthor syntax
  Make docs valid according to guides 0.3.3 (#11252)
2024-02-21 19:28:06 +01:00
Alexander M. Turek
c0dfba2ef3 Merge branch '2.18.x' into 2.19.x
* 2.18.x:
  Fix Static Analysis folder reference (#11281)
  docs: recommend safer way to disable logging (#11269)
  Remove unused baseline entries
  Treat '0' as a legitimate trim char
2024-02-21 18:52:54 +01:00
Grégoire Paris
b1f553eba3 Merge pull request #11272 from greg0ire/sa-attach-entity-listener
Improve static analysis on AttachEntityListenersListener
2024-02-20 08:14:13 +01:00
Grégoire Paris
0c4aac5a35 Merge pull request #11275 from greg0ire/sa-inversed-by
Account for inversedBy being a non-falsy-string or null
2024-02-20 08:13:34 +01:00
Grégoire Paris
e0081b59be Account for inversedBy being a non-falsy-string or null
It is supposed to hold the name of a PHP property, and those cannot be
falsy strings.
2024-02-20 07:54:19 +01:00
Grégoire Paris
4bd574daee Improve static analysis on AttachEntityListenersListener
$listenerCallback is supposed to be a method name, so it is safe to
require it is not a falsy string.
2024-02-19 09:36:41 +01:00
Grégoire Paris
b59189ab48 Merge pull request #11267 from doctrine/2.18.x
Merge 2.18.x up into 2.19.x
2024-02-17 20:00:05 +01:00
Dmitry Bannik
e5e3166747 #11090 - Fix obtaining an identifier in cases where the hydration has not yet fully completed on eagerLoadCollections 2024-02-16 12:57:23 +03:00
Alexander M. Turek
6290747bf9 Validate more variadic parameters (#11261) 2024-02-14 00:33:12 +01:00
Alexander M. Turek
b6f4220493 Throw if a variadic parameter contains unexpected named arguments (#11260) 2024-02-13 18:28:17 +01:00
Grégoire Paris
afbf293c94 Merge pull request #11255 from doctrine/2.18.x
Merge 2.18.x up into 2.19.x
2024-02-13 12:07:19 +01:00
Alexander M. Turek
b7860c782b Merge branch '2.19.x' into 3.1.x
* 2.19.x:
  Remove references to deprecated constants from Lexer (#11234)
2024-02-07 15:43:24 +01:00
Alexander M. Turek
7baef1e120 Remove references to deprecated constants from Lexer (#11234) 2024-02-07 15:39:20 +01:00
Alexander M. Turek
9a24ce5fad Merge branch '2.19.x' into 3.1.x
* 2.19.x:
  Add TokenType class (#11228)
2024-02-07 14:21:22 +01:00
Alexander M. Turek
9fcb8f1305 Merge branch '3.0.x' into 3.1.x
* 3.0.x:
  Revert "Merge pull request #11229 from greg0ire/add-columns"
  Add columns for 3.1.x and 4.0x
  Update version ORM from 2 to 3 in docs (#11221)
  Clean up outdated sentence (#11224)
  Update README.md
  Point link to correct upgrade guide (#11220)
  Ignore subclasses without discriminatorValue when generating discriminator column condition SQL (#11200)
  Update branches in README
2024-02-07 13:48:24 +01:00
Karoly Gossler
5049b615c5 Add TokenType class (#11228)
* Add TokenType class
Co-authored-by: Alexander M. Turek <me@derrabus.de>

* Deprecated Lexer constants in favour of TokenType

* Replace all Lexer::T_ occurrences with TokenType::T_

* Add upgrade note

* Fixed import Lexer => TokenType

* Fixed deprecation phpdoc

* Replaced int value with matching constant of TokenType

* Update src/Query/Lexer.php

---------

Co-authored-by: Alexander M. Turek <me@derrabus.de>
2024-02-07 13:31:08 +01:00
Alexander M. Turek
1051817d92 Merge branch '3.0.x' into 3.1.x
* 3.0.x:
  Bump dependencies in the "getting started" docs page (#11219)
  DoctrineSetup was renamed to ORMSetup (#11218)
2024-02-04 17:45:24 +01:00
Alexander M. Turek
517d038e5b Merge branch '3.0.x' into 3.1.x
* 3.0.x:
  Switch back to stable dependencies (#11210)
  Update branch metadata
2024-02-04 11:49:25 +01:00
Grégoire Paris
3db79ebbf3 Merge pull request #11214 from greg0ire/followup-array-access
Replace more occurrences of array access
2024-02-04 00:16:01 +01:00
Grégoire Paris
a2faeb9a26 Replace more occurrences of array access
Not sure how I missed those.
2024-02-03 23:56:59 +01:00
Grégoire Paris
3764ebf7a3 Merge pull request #11212 from greg0ire/fix-grammar
Follow up on array access deprecation
2024-02-03 23:50:27 +01:00
Grégoire Paris
a7d5adb3ce Migrate more occurrences of array access 2024-02-03 23:07:27 +01:00
Grégoire Paris
6f507c322a Fix grammar issue in upgrade guide 2024-02-03 23:01:56 +01:00
Grégoire Paris
54013671a7 Merge pull request #11211 from greg0ire/deprecate-array-access
Deprecate array access
2024-02-03 22:50:38 +01:00
Grégoire Paris
f5dea25b6c Deprecate array access
We now have proper value objects with properties for everything we need.
2024-02-03 22:26:56 +01:00
109 changed files with 2369 additions and 958 deletions

View File

@@ -11,29 +11,41 @@
"slug": "latest",
"upcoming": true
},
{
"name": "3.2",
"branchName": "3.2.x",
"slug": "3.2",
"upcoming": true
},
{
"name": "3.1",
"branchName": "3.1.x",
"slug": "3.1",
"upcoming": true
"current": true
},
{
"name": "3.0",
"branchName": "3.0.x",
"slug": "3.0",
"current": true
"maintained": false
},
{
"name": "2.20",
"branchName": "2.20.x",
"slug": "2.20",
"upcoming": true
},
{
"name": "2.19",
"branchName": "2.19.x",
"slug": "2.19",
"upcoming": true
"maintained": true
},
{
"name": "2.18",
"branchName": "2.18.x",
"slug": "2.18",
"maintained": true
"maintained": false
},
{
"name": "2.17",
@@ -82,42 +94,6 @@
"branchName": "2.10.x",
"slug": "2.10",
"maintained": false
},
{
"name": "2.9",
"branchName": "2.9.x",
"slug": "2.9",
"maintained": false
},
{
"name": "2.8",
"branchName": "2.8.x",
"slug": "2.8",
"maintained": false
},
{
"name": "2.7",
"branchName": "2.7",
"slug": "2.7",
"maintained": false
},
{
"name": "2.6",
"branchName": "2.6",
"slug": "2.6",
"maintained": false
},
{
"name": "2.5",
"branchName": "2.5",
"slug": "2.5",
"maintained": false
},
{
"name": "2.4",
"branchName": "2.4",
"slug": "2.4",
"maintained": false
}
]
}

9
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels:
- "CI"
target-branch: "2.19.x"

View File

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

View File

@@ -75,7 +75,7 @@ jobs:
if: "${{ matrix.dbal-version != 'default' }}"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
uses: "ramsey/composer-install@v3"
with:
composer-options: "--ignore-platform-req=php+"
dependency-versions: "${{ matrix.deps }}"
@@ -91,9 +91,9 @@ jobs:
ENABLE_SECOND_LEVEL_CACHE: 1
- name: "Upload coverage file"
uses: "actions/upload-artifact@v3"
uses: "actions/upload-artifact@v4"
with:
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-coverage"
path: "coverage*.xml"
@@ -156,7 +156,7 @@ jobs:
if: "${{ matrix.dbal-version != 'default' }}"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
uses: "ramsey/composer-install@v3"
with:
composer-options: "--ignore-platform-req=php+"
@@ -164,9 +164,9 @@ jobs:
run: "vendor/bin/phpunit -c ci/github/phpunit/pdo_pgsql.xml --coverage-clover=coverage.xml"
- name: "Upload coverage file"
uses: "actions/upload-artifact@v3"
uses: "actions/upload-artifact@v4"
with:
name: "${{ github.job }}-${{ matrix.postgres-version }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
name: "${{ github.job }}-${{ matrix.postgres-version }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.extension }}-coverage"
path: "coverage.xml"
@@ -222,7 +222,7 @@ jobs:
extensions: "${{ matrix.extension }}"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
uses: "ramsey/composer-install@v3"
with:
composer-options: "--ignore-platform-req=php+"
@@ -230,7 +230,7 @@ jobs:
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --coverage-clover=coverage.xml"
- name: "Upload coverage file"
uses: "actions/upload-artifact@v3"
uses: "actions/upload-artifact@v4"
with:
name: "${{ github.job }}-${{ matrix.mariadb-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
path: "coverage.xml"
@@ -296,7 +296,7 @@ jobs:
if: "${{ matrix.dbal-version != 'default' }}"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
uses: "ramsey/composer-install@v3"
with:
composer-options: "--ignore-platform-req=php+"
@@ -311,7 +311,7 @@ jobs:
ENABLE_SECOND_LEVEL_CACHE: 1
- name: "Upload coverage files"
uses: "actions/upload-artifact@v3"
uses: "actions/upload-artifact@v4"
with:
name: "${{ github.job }}-${{ matrix.mysql-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
path: "coverage*.xml"
@@ -332,11 +332,13 @@ jobs:
fetch-depth: 2
- name: "Download coverage files"
uses: "actions/download-artifact@v3"
uses: "actions/download-artifact@v4"
with:
path: "reports"
- name: "Upload to Codecov"
uses: "codecov/codecov-action@v3"
uses: "codecov/codecov-action@v4"
with:
directory: reports
env:
CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}"

View File

@@ -27,7 +27,7 @@ jobs:
uses: "shivammathur/setup-php@v2"
with:
coverage: "none"
php-version: "8.2"
php-version: "8.3"
- name: "Remove existing composer file"
run: "rm composer.json"
@@ -36,9 +36,14 @@ jobs:
run: "composer require --dev phpdocumentor/guides-cli --no-update"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
uses: "ramsey/composer-install@v3"
with:
dependency-versions: "highest"
- name: "Add orphan metadata where needed"
run: |
printf '%s\n\n%s\n' ":orphan:" "$(cat docs/en/sidebar.rst)" > docs/en/sidebar.rst
printf '%s\n\n%s\n' ":orphan:" "$(cat docs/en/reference/installation.rst)" > docs/en/reference/installation.rst
- name: "Run guides-cli"
run: "vendor/bin/guides -vvv --no-progress docs/en 2>&1 | grep -v 'No template found for rendering directive' | ( ! grep WARNING )"

View File

@@ -47,15 +47,8 @@ jobs:
coverage: "pcov"
ini-values: "zend.assertions=1, apc.enable_cli=1"
- name: "Cache dependencies installed with composer"
uses: "actions/cache@v3"
with:
path: "~/.composer/cache"
key: "php-${{ matrix.php-version }}-composer-locked-${{ hashFiles('composer.lock') }}"
restore-keys: "php-${{ matrix.php-version }}-composer-locked-"
- name: "Install dependencies with composer"
run: "composer update --no-interaction --no-progress"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v3"
- name: "Run PHPBench"
run: "vendor/bin/phpbench run --report=default"

View File

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

View File

@@ -43,7 +43,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
coverage: none
php-version: "8.2"
php-version: "8.3"
tools: cs2pr
- name: Require specific DBAL version
@@ -75,7 +75,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
coverage: none
php-version: "8.2"
php-version: "8.3"
tools: cs2pr
- name: Require specific DBAL version
@@ -83,7 +83,7 @@ jobs:
if: "${{ matrix.dbal-version != 'default' }}"
- name: Install dependencies with Composer
uses: ramsey/composer-install@v2
uses: ramsey/composer-install@v3
- name: Run static analysis with Vimeo Psalm
run: vendor/bin/psalm --shepherd

View File

@@ -1,7 +1,7 @@
| [4.0.x][4.0] | [3.1.x][3.1] | [3.0.x][3.0] | [2.19.x][2.19] | [2.18.x][2.18] |
|:------------------------------------------------------:|:------------------------------------------------------:|:-------------------------------------------------------:|:--------------------------------------------------------:|:---------------------------------------------------------:|
| [![Build status][4.0 image]][4.0] | [![Build status][3.1 image]][3.1] | [![Build status][3.0 image]][3.0] | [![Build status][2.19 image]][2.19] | [![Build status][2.18 image]][2.18] |
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.1 coverage image]][3.1 coverage] | [![Coverage Status][3.0 coverage image]][3.0 coverage] | [![Coverage Status][2.19 coverage image]][2.19 coverage] | [![Coverage Status][2.18 coverage image]][2.18 coverage] |
| [4.0.x][4.0] | [3.2.x][3.2] | [3.1.x][3.1] | [2.20.x][2.20] | [2.19.x][2.19] |
|:------------------------------------------------------:|:------------------------------------------------------:|:------------------------------------------------------:|:--------------------------------------------------------:|:--------------------------------------------------------:|
| [![Build status][4.0 image]][4.0] | [![Build status][3.2 image]][3.2] | [![Build status][3.1 image]][3.1] | [![Build status][2.20 image]][2.20] | [![Build status][2.19 image]][2.19] |
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.2 coverage image]][3.2 coverage] | [![Coverage Status][3.1 coverage image]][3.1 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] | [![Coverage Status][2.19 coverage image]][2.19 coverage] |
[<h1 align="center">🇺🇦 UKRAINE NEEDS YOUR HELP NOW!</h1>](https://www.doctrine-project.org/stop-war.html)
@@ -22,19 +22,19 @@ without requiring unnecessary code duplication.
[4.0]: https://github.com/doctrine/orm/tree/4.0.x
[4.0 coverage image]: https://codecov.io/gh/doctrine/orm/branch/4.0.x/graph/badge.svg
[4.0 coverage]: https://codecov.io/gh/doctrine/orm/branch/4.0.x
[3.2 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.2.x
[3.2]: https://github.com/doctrine/orm/tree/3.2.x
[3.2 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.2.x/graph/badge.svg
[3.2 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.2.x
[3.1 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.1.x
[3.1]: https://github.com/doctrine/orm/tree/3.1.x
[3.1 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.1.x/graph/badge.svg
[3.1 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.1.x
[3.0 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.0.x
[3.0]: https://github.com/doctrine/orm/tree/3.0.x
[3.0 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.0.x/graph/badge.svg
[3.0 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.0.x
[2.20 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.20.x
[2.20]: https://github.com/doctrine/orm/tree/2.20.x
[2.20 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.20.x/graph/badge.svg
[2.20 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.20.x
[2.19 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.19.x
[2.19]: https://github.com/doctrine/orm/tree/2.19.x
[2.19 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.19.x/graph/badge.svg
[2.19 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.19.x
[2.18 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.18.x
[2.18]: https://github.com/doctrine/orm/tree/2.18.x
[2.18 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.18.x/graph/badge.svg
[2.18 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.18.x

View File

@@ -1,5 +1,65 @@
# Upgrade to 3.2
## Deprecate the `NotSupported` exception
The class `Doctrine\ORM\Exception\NotSupported` is deprecated without replacement.
## Deprecate remaining `Serializable` implementation
Relying on `SequenceGenerator` implementing the `Serializable` is deprecated
because that interface won't be implemented in ORM 4 anymore.
The following methods are deprecated:
* `SequenceGenerator::serialize()`
* `SequenceGenerator::unserialize()`
## `orm:schema-tool:update` option `--complete` is deprecated
That option behaves as a no-op, and is deprecated. It will be removed in 4.0.
## Deprecate properties `$indexes` and `$uniqueConstraints` of `Doctrine\ORM\Mapping\Table`
The properties `$indexes` and `$uniqueConstraints` have been deprecated since they had no effect at all.
The preferred way of defining indices and unique constraints is by
using the `\Doctrine\ORM\Mapping\UniqueConstraint` and `\Doctrine\ORM\Mapping\Index` attributes.
# Upgrade to 3.1
## Deprecate `Doctrine\ORM\Mapping\ReflectionEnumProperty`
This class is deprecated and will be removed in 4.0.
Instead, use `Doctrine\Persistence\Reflection\EnumReflectionProperty` from
`doctrine/persistence`.
## Deprecate passing null to `ClassMetadata::fullyQualifiedClassName()`
Passing `null` to `Doctrine\ORM\ClassMetadata::fullyQualifiedClassName()` is
deprecated and will no longer be possible in 4.0.
## Deprecate array access
Using array access on instances of the following classes is deprecated:
- `Doctrine\ORM\Mapping\DiscriminatorColumnMapping`
- `Doctrine\ORM\Mapping\EmbedClassMapping`
- `Doctrine\ORM\Mapping\FieldMapping`
- `Doctrine\ORM\Mapping\JoinColumnMapping`
- `Doctrine\ORM\Mapping\JoinTableMapping`
# Upgrade to 3.0
## BC BREAK: Calling `ClassMetadata::getAssociationMappedByTargetField()` with the owning side of an association now throws an exception
Previously, calling
`Doctrine\ORM\Mapping\ClassMetadata::getAssociationMappedByTargetField()` with
the owning side of an association returned `null`, which was undocumented, and
wrong according to the phpdoc of the parent method.
If you do not know whether you are on the owning or inverse side of an association,
you can use `Doctrine\ORM\Mapping\ClassMetadata::isAssociationInverseSide()`
to find out.
## BC BREAK: `Doctrine\ORM\Proxy\Autoloader` no longer extends `Doctrine\Common\Proxy\Autoloader`
Make sure to use the former when writing a type declaration or an `instanceof` check.
@@ -663,6 +723,23 @@ following classes and methods:
Use `toIterable()` instead.
# Upgrade to 2.19
## Deprecate calling `ClassMetadata::getAssociationMappedByTargetField()` with the owning side of an association
Calling
`Doctrine\ORM\Mapping\ClassMetadata::getAssociationMappedByTargetField()` with
the owning side of an association returns `null`, which is undocumented, and
wrong according to the phpdoc of the parent method.
If you do not know whether you are on the owning or inverse side of an association,
you can use `Doctrine\ORM\Mapping\ClassMetadata::isAssociationInverseSide()`
to find out.
## Deprecate `Doctrine\ORM\Query\Lexer::T_*` constants
Use `Doctrine\ORM\Query\TokenType::T_*` instead.
# Upgrade to 2.17
## Deprecate annotations classes for named queries

View File

@@ -23,27 +23,27 @@
"php": "^8.1",
"composer-runtime-api": "^2",
"ext-ctype": "*",
"doctrine/collections": "^2.1",
"doctrine/collections": "^2.2",
"doctrine/dbal": "^3.8.2 || ^4",
"doctrine/deprecations": "^0.5.3 || ^1",
"doctrine/event-manager": "^1.2 || ^2",
"doctrine/inflector": "^1.4 || ^2.0",
"doctrine/instantiator": "^1.3 || ^2",
"doctrine/lexer": "^3",
"doctrine/persistence": "^3.1.1",
"doctrine/persistence": "^3.3.1",
"psr/cache": "^1 || ^2 || ^3",
"symfony/console": "^5.4 || ^6.0 || ^7.0",
"symfony/var-exporter": "~6.2.13 || ^6.3.2 || ^7.0"
"symfony/var-exporter": "^6.3.9 || ^7.0"
},
"require-dev": {
"doctrine/coding-standard": "^12.0",
"phpbench/phpbench": "^1.0",
"phpstan/phpstan": "1.10.35",
"phpstan/phpstan": "1.11.1",
"phpunit/phpunit": "^10.4.0",
"psr/log": "^1 || ^2 || ^3",
"squizlabs/php_codesniffer": "3.7.2",
"symfony/cache": "^5.4 || ^6.2 || ^7.0",
"vimeo/psalm": "5.16.0"
"vimeo/psalm": "5.24.0"
},
"suggest": {
"ext-dom": "Provides support for XSD validation for XML mapping files",

4
docs/.gitignore vendored
View File

@@ -1,4 +0,0 @@
en/_exts/configurationblock.pyc
build
en/_build
.idea

3
docs/.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "en/_theme"]
path = en/_theme
url = https://github.com/doctrine/doctrine-sphinx-theme.git

View File

@@ -11,7 +11,7 @@ What we offer are hooks to execute any kind of validation.
.. note::
You don't need to validate your entities in the lifecycle
events. Its only one of many options. Of course you can also
events. It is only one of many options. Of course you can also
perform validations in value setters or any other method of your
entities that are used in your code.

View File

@@ -18,7 +18,7 @@ well.
Requirements
------------
Doctrine ORM requires a minimum of PHP 7.1. For greatly improved
Doctrine ORM requires a minimum of PHP 8.1. For greatly improved
performance it is also recommended that you use APC with PHP.
Doctrine ORM Packages

View File

@@ -870,8 +870,8 @@ This is essentially the same as the following, more verbose, mapping:
* @var Collection<int, Group>
*/
#[JoinTable(name: 'User_Group')]
#[JoinColumn(name: 'User_id', referencedColumnName: 'id')]
#[InverseJoinColumn(name: 'Group_id', referencedColumnName: 'id')]
#[JoinColumn(name: 'user_id', referencedColumnName: 'id')]
#[InverseJoinColumn(name: 'group_id', referencedColumnName: 'id')]
#[ManyToMany(targetEntity: Group::class)]
private Collection $groups;
// ...
@@ -884,10 +884,10 @@ This is essentially the same as the following, more verbose, mapping:
<many-to-many field="groups" target-entity="Group">
<join-table name="User_Group">
<join-columns>
<join-column id="User_id" referenced-column-name="id" />
<join-column id="user_id" referenced-column-name="id" />
</join-columns>
<inverse-join-columns>
<join-column id="Group_id" referenced-column-name="id" />
<join-column id="group_id" referenced-column-name="id" />
</inverse-join-columns>
</join-table>
</many-to-many>

View File

@@ -228,50 +228,12 @@ and a custom ``Doctrine\ORM\Mapping\TypedFieldMapper`` implementation.
Doctrine Mapping Types
----------------------
The ``type`` option used in the ``@Column`` accepts any of the existing
Doctrine types or even your own custom types. A Doctrine type defines
The ``type`` option used in the ``@Column`` accepts any of the
`existing Doctrine DBAL types <https://docs.doctrine-project.org/projects/doctrine-dbal/en/stable/reference/types.html#reference>`_
or :doc:`your own custom mapping types
<../cookbook/custom-mapping-types>`. A Doctrine type defines
the conversion between PHP and SQL types, independent from the database vendor
you are using. All Mapping Types that ship with Doctrine are fully portable
between the supported database systems.
As an example, the Doctrine Mapping Type ``string`` defines the
mapping from a PHP string to a SQL VARCHAR (or VARCHAR2 etc.
depending on the RDBMS brand). Here is a quick overview of the
built-in mapping types:
- ``string``: Type that maps a SQL VARCHAR to a PHP string.
- ``integer``: Type that maps a SQL INT to a PHP integer.
- ``smallint``: Type that maps a database SMALLINT to a PHP
integer.
- ``bigint``: Type that maps a database BIGINT to a PHP string.
- ``boolean``: Type that maps a SQL boolean or equivalent (TINYINT) to a PHP boolean.
- ``decimal``: Type that maps a SQL DECIMAL to a PHP string.
- ``date``: Type that maps a SQL DATETIME to a PHP DateTime
object.
- ``time``: Type that maps a SQL TIME to a PHP DateTime object.
- ``datetime``: Type that maps a SQL DATETIME/TIMESTAMP to a PHP
DateTime object.
- ``datetimetz``: Type that maps a SQL DATETIME/TIMESTAMP to a PHP
DateTime object with timezone.
- ``text``: Type that maps a SQL CLOB to a PHP string.
- ``object``: Type that maps a SQL CLOB to a PHP object using
``serialize()`` and ``unserialize()``
- ``array``: Type that maps a SQL CLOB to a PHP array using
``serialize()`` and ``unserialize()``
- ``simple_array``: Type that maps a SQL CLOB to a PHP array using
``implode()`` and ``explode()``, with a comma as delimiter. *IMPORTANT*
Only use this type if you are sure that your values cannot contain a ",".
- ``json_array``: Type that maps a SQL CLOB to a PHP array using
``json_encode()`` and ``json_decode()``
- ``float``: Type that maps a SQL Float (Double Precision) to a
PHP double. *IMPORTANT*: Works only with locale settings that use
decimal points as separator.
- ``guid``: Type that maps a database GUID/UUID to a PHP string. Defaults to
varchar but uses a specific type if the platform supports it.
- ``blob``: Type that maps a SQL BLOB to a PHP resource stream
A cookbook article shows how to define :doc:`your own custom mapping types
<../cookbook/custom-mapping-types>`.
you are using.
.. note::

View File

@@ -976,7 +976,7 @@ The Query class
---------------
An instance of the ``Doctrine\ORM\Query`` class represents a DQL
query. You create a Query instance be calling
query. You create a Query instance by calling
``EntityManager#createQuery($dql)``, passing the DQL query string.
Alternatively you can create an empty ``Query`` instance and invoke
``Query#setDQL($dql)`` afterwards. Here are some examples:
@@ -993,58 +993,146 @@ Alternatively you can create an empty ``Query`` instance and invoke
$q = $em->createQuery();
$q->setDQL('select u from MyProject\Model\User u');
Query Result Formats
~~~~~~~~~~~~~~~~~~~~
Query Result Formats (Hydration Modes)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The format in which the result of a DQL SELECT query is returned
can be influenced by a so-called ``hydration mode``. A hydration
mode specifies a particular way in which a SQL result set is
transformed. Each hydration mode has its own dedicated method on
the Query class. Here they are:
The way in which the SQL result set of a DQL SELECT query is transformed
to PHP is determined by the so-called "hydration mode".
``getResult()``
^^^^^^^^^^^^^^^
- ``Query#getResult()``: Retrieves a collection of objects. The
result is either a plain collection of objects (pure) or an array
where the objects are nested in the result rows (mixed).
- ``Query#getSingleResult()``: Retrieves a single object. If the
result contains more than one object, an ``NonUniqueResultException``
is thrown. If the result contains no objects, an ``NoResultException``
is thrown. The pure/mixed distinction does not apply.
- ``Query#getOneOrNullResult()``: Retrieve a single object. If the
result contains more than one object, a ``NonUniqueResultException``
is thrown. If no object is found null will be returned.
- ``Query#getArrayResult()``: Retrieves an array graph (a nested
array) that is largely interchangeable with the object graph
generated by ``Query#getResult()`` for read-only purposes.
Retrieves a collection of objects. The result is either a plain collection of objects (pure) or an array
where the objects are nested in the result rows (mixed):
.. note::
.. code-block:: php
An array graph can differ from the corresponding object
graph in certain scenarios due to the difference of the identity
semantics between arrays and objects.
<?php
use Doctrine\ORM\AbstractQuery;
$query = $em->createQuery('SELECT u FROM User u');
$users = $query->getResult();
// same as:
$users = $query->getResult(AbstractQuery::HYDRATE_OBJECT);
- Objects fetched in a FROM clause are returned as a Set, that means every
object is only ever included in the resulting array once. This is the case
even when using JOIN or GROUP BY in ways that return the same row for an
object multiple times. If the hydrator sees the same object multiple times,
then it makes sure it is only returned once.
- ``Query#getScalarResult()``: Retrieves a flat/rectangular result
set of scalar values that can contain duplicate data. The
pure/mixed distinction does not apply.
- ``Query#getSingleScalarResult()``: Retrieves a single scalar
value from the result returned by the dbms. If the result contains
more than a single scalar value, an exception is thrown. The
pure/mixed distinction does not apply.
- If an object is already in memory from a previous query of any kind, then
then the previous object is used, even if the database may contain more
recent data. This even happens if the previous object is still an unloaded proxy.
Instead of using these methods, you can alternatively use the
general-purpose method
``Query#execute(array $params = [], $hydrationMode = Query::HYDRATE_OBJECT)``.
Using this method you can directly supply the hydration mode as the
second parameter via one of the Query constants. In fact, the
methods mentioned earlier are just convenient shortcuts for the
execute method. For example, the method ``Query#getResult()``
internally invokes execute, passing in ``Query::HYDRATE_OBJECT`` as
the hydration mode.
``getArrayResult()``
^^^^^^^^^^^^^^^^^^^^
The use of the methods mentioned earlier is generally preferred as
it leads to more concise code.
Retrieves an array graph (a nested array) for read-only purposes.
.. note::
An array graph can differ from the corresponding object
graph in certain scenarios due to the difference of the identity
semantics between arrays and objects.
.. code-block:: php
<?php
$users = $query->getArrayResult();
// same as:
$users = $query->getResult(AbstractQuery::HYDRATE_ARRAY);
``getScalarResult()``
^^^^^^^^^^^^^^^^^^^^^
Retrieves a flat/rectangular result set of scalar values that can contain duplicate data. The
pure/mixed distinction does not apply.
.. code-block:: php
<?php
$users = $query->getScalarResult();
// same as:
$users = $query->getResult(AbstractQuery::HYDRATE_SCALAR);
Fields from classes are prefixed by the DQL alias in the result.
A query of the kind `SELECT u.name ...` returns a key `u_name` in the result rows.
``getSingleScalarResult()``
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Retrieves a single scalar value from the result returned by the database. If the result contains
more than a single scalar value, a ``NonUniqueResultException`` is thrown. The pure/mixed distinction does not apply.
.. code-block:: php
<?php
$query = $em->createQuery('SELECT COUNT(u.id) FROM User u');
$numUsers = $query->getSingleScalarResult();
// same as:
$numUsers = $query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR);
``getSingleColumnResult()``
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Retrieves an array from a one-dimensional array of scalar values:
.. code-block:: php
<?php
$query = $em->createQuery('SELECT a.id FROM User u');
$ids = $query->getSingleColumnResult();
// same as:
$ids = $query->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN);
``getSingleResult()``
^^^^^^^^^^^^^^^^^^^^^
Retrieves a single object. If the result contains more than one object, a ``NonUniqueResultException``
is thrown. If the result contains no objects, a ``NoResultException`` is thrown. The pure/mixed distinction does not apply.
``getOneOrNullResult()``
^^^^^^^^^^^^^^^^^^^^^^^^
Retrieves a single object. If the result contains more than one object, a ``NonUniqueResultException``
is thrown. If no object is found, ``null`` will be returned.
Custom Hydration Modes
^^^^^^^^^^^^^^^^^^^^^^
You can easily add your own custom hydration modes by first
creating a class which extends ``AbstractHydrator``:
.. code-block:: php
<?php
namespace MyProject\Hydrators;
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
class CustomHydrator extends AbstractHydrator
{
protected function _hydrateAll()
{
return $this->_stmt->fetchAllAssociative();
}
}
Next you just need to add the class to the ORM configuration:
.. code-block:: php
<?php
$em->getConfiguration()->addCustomHydrationMode('CustomHydrator', 'MyProject\Hydrators\CustomHydrator');
Now the hydrator is ready to be used in your queries:
.. code-block:: php
<?php
$query = $em->createQuery('SELECT u FROM CmsUser u');
$results = $query->getResult('CustomHydrator');
Pure and Mixed Results
~~~~~~~~~~~~~~~~~~~~~~
@@ -1148,165 +1236,6 @@ will return the rows iterating the different top-level entities.
[2] => Object (User)
[3] => Object (Group)
Hydration Modes
~~~~~~~~~~~~~~~
Each of the Hydration Modes makes assumptions about how the result
is returned to user land. You should know about all the details to
make best use of the different result formats:
The constants for the different hydration modes are:
- ``Query::HYDRATE_OBJECT``
- ``Query::HYDRATE_ARRAY``
- ``Query::HYDRATE_SCALAR``
- ``Query::HYDRATE_SINGLE_SCALAR``
- ``Query::HYDRATE_SCALAR_COLUMN``
Object Hydration
^^^^^^^^^^^^^^^^
Object hydration hydrates the result set into the object graph:
.. code-block:: php
<?php
$query = $em->createQuery('SELECT u FROM CmsUser u');
$users = $query->getResult(Query::HYDRATE_OBJECT);
Sometimes the behavior in the object hydrator can be confusing, which is
why we are listing as many of the assumptions here for reference:
- Objects fetched in a FROM clause are returned as a Set, that means every
object is only ever included in the resulting array once. This is the case
even when using JOIN or GROUP BY in ways that return the same row for an
object multiple times. If the hydrator sees the same object multiple times,
then it makes sure it is only returned once.
- If an object is already in memory from a previous query of any kind, then
then the previous object is used, even if the database may contain more
recent data. Data from the database is discarded. This even happens if the
previous object is still an unloaded proxy.
This list might be incomplete.
Array Hydration
^^^^^^^^^^^^^^^
You can run the same query with array hydration and the result set
is hydrated into an array that represents the object graph:
.. code-block:: php
<?php
$query = $em->createQuery('SELECT u FROM CmsUser u');
$users = $query->getResult(Query::HYDRATE_ARRAY);
You can use the ``getArrayResult()`` shortcut as well:
.. code-block:: php
<?php
$users = $query->getArrayResult();
Scalar Hydration
^^^^^^^^^^^^^^^^
If you want to return a flat rectangular result set instead of an
object graph you can use scalar hydration:
.. code-block:: php
<?php
$query = $em->createQuery('SELECT u FROM CmsUser u');
$users = $query->getResult(Query::HYDRATE_SCALAR);
echo $users[0]['u_id'];
The following assumptions are made about selected fields using
Scalar Hydration:
1. Fields from classes are prefixed by the DQL alias in the result.
A query of the kind 'SELECT u.name ..' returns a key 'u_name' in
the result rows.
Single Scalar Hydration
^^^^^^^^^^^^^^^^^^^^^^^
If you have a query which returns just a single scalar value you can use
single scalar hydration:
.. code-block:: php
<?php
$query = $em->createQuery('SELECT COUNT(a.id) FROM CmsUser u LEFT JOIN u.articles a WHERE u.username = ?1 GROUP BY u.id');
$query->setParameter(1, 'jwage');
$numArticles = $query->getResult(Query::HYDRATE_SINGLE_SCALAR);
You can use the ``getSingleScalarResult()`` shortcut as well:
.. code-block:: php
<?php
$numArticles = $query->getSingleScalarResult();
Scalar Column Hydration
^^^^^^^^^^^^^^^^^^^^^^^
If you have a query which returns a one-dimensional array of scalar values
you can use scalar column hydration:
.. code-block:: php
<?php
$query = $em->createQuery('SELECT a.id FROM CmsUser u');
$ids = $query->getResult(Query::HYDRATE_SCALAR_COLUMN);
You can use the ``getSingleColumnResult()`` shortcut as well:
.. code-block:: php
<?php
$ids = $query->getSingleColumnResult();
Custom Hydration Modes
^^^^^^^^^^^^^^^^^^^^^^
You can easily add your own custom hydration modes by first
creating a class which extends ``AbstractHydrator``:
.. code-block:: php
<?php
namespace MyProject\Hydrators;
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
class CustomHydrator extends AbstractHydrator
{
protected function _hydrateAll()
{
return $this->_stmt->fetchAllAssociative();
}
}
Next you just need to add the class to the ORM configuration:
.. code-block:: php
<?php
$em->getConfiguration()->addCustomHydrationMode('CustomHydrator', 'MyProject\Hydrators\CustomHydrator');
Now the hydrator is ready to be used in your queries:
.. code-block:: php
<?php
$query = $em->createQuery('SELECT u FROM CmsUser u');
$results = $query->getResult('CustomHydrator');
Iterating Large Result Sets
~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -1,5 +1,3 @@
:orphan:
Installation
============

View File

@@ -1,5 +1,3 @@
:orphan:
.. toc::
.. tocheader:: Tutorials

View File

@@ -27,7 +27,7 @@ What is Doctrine?
-----------------
Doctrine ORM is an `object-relational mapper (ORM) <https://en.wikipedia.org/wiki/Object-relational_mapping>`_
for PHP 7.1+ that provides transparent persistence for PHP objects. It uses the Data Mapper
for PHP that provides transparent persistence for PHP objects. It uses the Data Mapper
pattern at the heart, aiming for a complete separation of your domain/business
logic from the persistence in a relational database management system.

View File

@@ -115,21 +115,11 @@ parameters:
count: 1
path: src/EntityManagerInterface.php
-
message: "#^Template type T of method Doctrine\\\\ORM\\\\EntityManagerInterface\\:\\:getClassMetadata\\(\\) is not referenced in a parameter\\.$#"
count: 1
path: src/EntityManagerInterface.php
-
message: "#^Method Doctrine\\\\ORM\\\\EntityRepository\\:\\:matching\\(\\) should return Doctrine\\\\Common\\\\Collections\\\\AbstractLazyCollection\\<int, T of object\\>&Doctrine\\\\Common\\\\Collections\\\\Selectable\\<int, T of object\\> but returns Doctrine\\\\ORM\\\\LazyCriteriaCollection\\<\\(int\\|string\\), object\\>\\.$#"
count: 1
path: src/EntityRepository.php
-
message: "#^Method Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata\\:\\:fullyQualifiedClassName\\(\\) should return class\\-string\\|null but returns string\\|null\\.$#"
count: 1
path: src/Mapping/ClassMetadata.php
-
message: "#^If condition is always true\\.$#"
count: 1

View File

@@ -20,10 +20,6 @@ parameters:
message: '~^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:getArrayBindingType\(\) never returns .* so it can be removed from the return type\.$~'
path: src/Persisters/Entity/BasicEntityPersister.php
-
message: '~^Unreachable statement \- code above always terminates\.$~'
path: src/Mapping/AssociationMapping.php
- '~^Class Doctrine\\DBAL\\Platforms\\SQLitePlatform not found\.$~'
# To be removed in 4.0

View File

@@ -20,17 +20,13 @@ parameters:
message: '~^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:getArrayBindingType\(\) never returns .* so it can be removed from the return type\.$~'
path: src/Persisters/Entity/BasicEntityPersister.php
-
message: '~^Unreachable statement \- code above always terminates\.$~'
path: src/Mapping/AssociationMapping.php
# Compatibility with DBAL 3
# See https://github.com/doctrine/dbal/pull/3480
-
message: '~^Result of method Doctrine\\DBAL\\Connection::commit\(\) \(void\) is used\.$~'
path: src/UnitOfWork.php
-
message: '~^Strict comparison using === between void and false will always evaluate to false\.$~'
message: '~^Strict comparison using === between null and false will always evaluate to false\.$~'
path: src/UnitOfWork.php
-
message: '~^Variable \$e on left side of \?\? always exists and is not nullable\.$~'

File diff suppressed because it is too large Load Diff

View File

@@ -170,6 +170,12 @@
<file name="src/Mapping/ClassMetadataFactory.php"/>
</errorLevel>
</ReferenceConstraintViolation>
<RiskyTruthyFalsyComparison>
<!-- TODO: Enable this new rule on higher branches. -->
<errorLevel type="suppress">
<directory name="src" />
</errorLevel>
</RiskyTruthyFalsyComparison>
<TooManyArguments>
<errorLevel type="suppress">
<!-- Symfony cache supports passing a key prefix to the clear method. -->

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Cache\Persister\Entity;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\CollectionCacheKey;
@@ -201,8 +202,8 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
/**
* Generates a string of currently query
*
* @param string[]|Criteria $criteria
* @param string[]|null $orderBy
* @param string[]|Criteria $criteria
* @param array<string, Order>|null $orderBy
*/
protected function getHash(
string $query,
@@ -426,7 +427,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
*/
public function loadCriteria(Criteria $criteria): array
{
$orderBy = $criteria->getOrderings();
$orderBy = $criteria->orderings();
$limit = $criteria->getMaxResults();
$offset = $criteria->getFirstResult();
$query = $this->persister->getSelectSQL($criteria);

View File

@@ -67,7 +67,7 @@ class FileLockRegion implements ConcurrentRegion
$time = $this->getLockTime($filename);
$content = $this->getLockContent($filename);
if (! $content || ! $time) {
if ($content === false || $time === false) {
@unlink($filename);
return false;
@@ -156,12 +156,10 @@ class FileLockRegion implements ConcurrentRegion
{
// The check below is necessary because on some platforms glob returns false
// when nothing matched (even though no errors occurred)
$filenames = glob(sprintf('%s/*.%s', $this->directory, self::LOCK_EXTENSION));
$filenames = glob(sprintf('%s/*.%s', $this->directory, self::LOCK_EXTENSION)) ?: [];
if ($filenames) {
foreach ($filenames as $filename) {
@unlink($filename);
}
foreach ($filenames as $filename) {
@unlink($filename);
}
return $this->region->evictAll();
@@ -176,7 +174,7 @@ class FileLockRegion implements ConcurrentRegion
$lock = Lock::createLockRead();
$filename = $this->getLockFileName($key);
if (! @file_put_contents($filename, $lock->value, LOCK_EX)) {
if (@file_put_contents($filename, $lock->value, LOCK_EX) === false) {
return null;
}

View File

@@ -63,27 +63,27 @@ class EntityManager implements EntityManagerInterface
/**
* The metadata factory, used to retrieve the ORM metadata of entity classes.
*/
private readonly ClassMetadataFactory $metadataFactory;
private ClassMetadataFactory $metadataFactory;
/**
* The UnitOfWork used to coordinate object-level transactions.
*/
private readonly UnitOfWork $unitOfWork;
private UnitOfWork $unitOfWork;
/**
* The event manager that is the central point of the event system.
*/
private readonly EventManager $eventManager;
private EventManager $eventManager;
/**
* The proxy factory used to create dynamic proxies.
*/
private readonly ProxyFactory $proxyFactory;
private ProxyFactory $proxyFactory;
/**
* The repository factory used to create dynamic repositories.
*/
private readonly RepositoryFactory $repositoryFactory;
private RepositoryFactory $repositoryFactory;
/**
* The expression builder instance used to generate query expressions.
@@ -112,8 +112,8 @@ class EntityManager implements EntityManagerInterface
* @param Connection $conn The database connection used by the EntityManager.
*/
public function __construct(
private readonly Connection $conn,
private readonly Configuration $config,
private Connection $conn,
private Configuration $config,
EventManager|null $eventManager = null,
) {
if (! $config->getMetadataDriverImpl()) {

View File

@@ -234,7 +234,7 @@ interface EntityManagerInterface extends ObjectManager
*
* @psalm-param string|class-string<T> $className
*
* @psalm-return Mapping\ClassMetadata<T>
* @psalm-return ($className is class-string<T> ? Mapping\ClassMetadata<T> : Mapping\ClassMetadata<object>)
*
* @psalm-template T of object
*/

View File

@@ -8,6 +8,7 @@ use LogicException;
use function sprintf;
/** @deprecated */
final class NotSupported extends LogicException implements ORMException
{
public static function create(): self

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Id;
use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Serializable;
@@ -65,8 +66,17 @@ class SequenceGenerator extends AbstractIdGenerator implements Serializable
return $this->nextValue;
}
/** @deprecated without replacement. */
final public function serialize(): string
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11468',
'%s() is deprecated, use __serialize() instead. %s won\'t implement the Serializable interface anymore in ORM 4.',
__METHOD__,
self::class,
);
return serialize($this->__serialize());
}
@@ -79,8 +89,17 @@ class SequenceGenerator extends AbstractIdGenerator implements Serializable
];
}
/** @deprecated without replacement. */
final public function unserialize(string $serialized): void
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11468',
'%s() is deprecated, use __unserialize() instead. %s won\'t implement the Serializable interface anymore in ORM 4.',
__METHOD__,
self::class,
);
$this->__unserialize(unserialize($serialized));
}

View File

@@ -104,29 +104,31 @@ abstract class AbstractHydrator
$this->prepare();
while (true) {
$row = $this->statement()->fetchAssociative();
try {
while (true) {
$row = $this->statement()->fetchAssociative();
if ($row === false) {
$this->cleanup();
break;
}
$result = [];
$this->hydrateRowData($row, $result);
$this->cleanupAfterRowIteration();
if (count($result) === 1) {
if (count($resultSetMapping->indexByMap) === 0) {
yield end($result);
} else {
yield from $result;
if ($row === false) {
break;
}
$result = [];
$this->hydrateRowData($row, $result);
$this->cleanupAfterRowIteration();
if (count($result) === 1) {
if (count($resultSetMapping->indexByMap) === 0) {
yield end($result);
} else {
yield from $result;
}
} else {
yield $result;
}
} else {
yield $result;
}
} finally {
$this->cleanup();
}
}

View File

@@ -91,7 +91,7 @@ class ObjectHydrator extends AbstractHydrator
}
// handle fetch-joined owning side bi-directional one-to-one associations
if ($assoc->inversedBy) {
if ($assoc->inversedBy !== null) {
$class = $this->getClassMetadata($className);
$inverseAssoc = $class->associationMappings[$assoc->inversedBy];
@@ -439,7 +439,7 @@ class ObjectHydrator extends AbstractHydrator
if ($relation->isOwningSide()) {
// TODO: Just check hints['fetched'] here?
// If there is an inverse mapping on the target class its bidirectional
if ($relation->inversedBy) {
if ($relation->inversedBy !== null) {
$inverseAssoc = $targetClass->associationMappings[$relation->inversedBy];
if ($inverseAssoc->isToOne()) {
$targetClass->reflFields[$inverseAssoc->fieldName]->setValue($element, $parentObject);

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Internal;
use BadMethodCallException;
use function array_filter;
use function array_is_list;
use function array_keys;
use function array_values;
use function assert;
use function debug_backtrace;
use function implode;
use function is_string;
use function sprintf;
use const DEBUG_BACKTRACE_IGNORE_ARGS;
/**
* Checks if a variadic parameter contains unexpected named arguments.
*
* @internal
*/
trait NoUnknownNamedArguments
{
/**
* @param TItem[] $parameter
*
* @template TItem
* @psalm-assert list<TItem> $parameter
*/
private static function validateVariadicParameter(array $parameter): void
{
if (array_is_list($parameter)) {
return;
}
[, $trace] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
assert(isset($trace['class']));
$additionalArguments = array_values(array_filter(
array_keys($parameter),
is_string(...),
));
throw new BadMethodCallException(sprintf(
'Invalid call to %s::%s(), unknown named arguments: %s',
$trace['class'],
$trace['function'],
implode(', ', $additionalArguments),
));
}
}

View File

@@ -64,11 +64,11 @@ class LazyCriteriaCollection extends AbstractLazyCollection implements Selectabl
}
/**
* {@inheritDoc}
*
* Do an optimized search of an element
*
* @template TMaybeContained
* @param mixed $element The element to search for.
*
* @return bool TRUE if the collection contains $element, FALSE otherwise.
*/
public function contains(mixed $element): bool
{

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Doctrine\Deprecations\Deprecation;
use InvalidArgumentException;
use function property_exists;
@@ -14,12 +15,26 @@ trait ArrayAccessImplementation
/** @param string $offset */
public function offsetExists(mixed $offset): bool
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11211',
'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.',
static::class,
);
return isset($this->$offset);
}
/** @param string $offset */
public function offsetGet(mixed $offset): mixed
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11211',
'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.',
static::class,
);
if (! property_exists($this, $offset)) {
throw new InvalidArgumentException('Undefined property: ' . $offset);
}
@@ -30,12 +45,26 @@ trait ArrayAccessImplementation
/** @param string $offset */
public function offsetSet(mixed $offset, mixed $value): void
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11211',
'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.',
static::class,
);
$this->$offset = $value;
}
/** @param string $offset */
public function offsetUnset(mixed $offset): void
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11211',
'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.',
static::class,
);
$this->$offset = null;
}
}

View File

@@ -27,7 +27,7 @@ abstract class AssociationMapping implements ArrayAccess
/**
* The fetching strategy to use for the association, usually defaults to FETCH_LAZY.
*
* @var ClassMetadata::FETCH_*
* @var ClassMetadata::FETCH_*|null
*/
public int|null $fetch = null;
@@ -96,13 +96,26 @@ abstract class AssociationMapping implements ArrayAccess
}
/**
* @param mixed[] $mappingArray
* @psalm-param array{
* fieldName: string,
* sourceEntity: class-string,
* targetEntity: class-string,
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
* fetch?: ClassMetadata::FETCH_*|null,
* inherited?: class-string|null,
* declared?: class-string|null,
* cache?: array<mixed>|null,
* id?: bool|null,
* isOnDeleteCascade?: bool|null,
* originalClass?: class-string|null,
* originalField?: string|null,
* orphanRemoval?: bool,
* unique?: bool|null,
* joinTable?: mixed[]|null,
* type?: int,
* isOwningSide: bool, ...} $mappingArray
* isOwningSide: bool,
* } $mappingArray
*/
public static function fromMappingArray(array $mappingArray): static
{

View File

@@ -288,7 +288,7 @@ class ClassMetadataBuilder
): ClassMetadataBuilder {
$builder = $this->createManyToOne($name, $targetEntity);
if ($inversedBy) {
if ($inversedBy !== null) {
$builder->inversedBy($inversedBy);
}
@@ -348,7 +348,7 @@ class ClassMetadataBuilder
): ClassMetadataBuilder {
$builder = $this->createOneToOne($name, $targetEntity);
if ($inversedBy) {
if ($inversedBy !== null) {
$builder->inversedBy($inversedBy);
}
@@ -380,7 +380,7 @@ class ClassMetadataBuilder
): ClassMetadataBuilder {
$builder = $this->createManyToMany($name, $targetEntity);
if ($inversedBy) {
if ($inversedBy !== null) {
$builder->inversedBy($inversedBy);
}

View File

@@ -4,18 +4,20 @@ declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Doctrine\ORM\Internal\NoUnknownNamedArguments;
use ReflectionProperty;
final class ChainTypedFieldMapper implements TypedFieldMapper
{
/**
* @readonly
* @var TypedFieldMapper[] $typedFieldMappers
*/
use NoUnknownNamedArguments;
/** @var list<TypedFieldMapper> $typedFieldMappers */
private readonly array $typedFieldMappers;
public function __construct(TypedFieldMapper ...$typedFieldMappers)
{
self::validateVariadicParameter($typedFieldMappers);
$this->typedFieldMappers = $typedFieldMappers;
}

View File

@@ -7,6 +7,7 @@ namespace Doctrine\ORM\Mapping;
use BackedEnum;
use BadMethodCallException;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Deprecations\Deprecation;
use Doctrine\Instantiator\Instantiator;
use Doctrine\Instantiator\InstantiatorInterface;
use Doctrine\ORM\Cache\Exception\NonCacheableEntityAssociation;
@@ -14,6 +15,7 @@ use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Id\AbstractIdGenerator;
use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata;
use Doctrine\Persistence\Mapping\ReflectionService;
use Doctrine\Persistence\Reflection\EnumReflectionProperty;
use InvalidArgumentException;
use LogicException;
use ReflectionClass;
@@ -41,6 +43,7 @@ use function is_subclass_of;
use function ltrim;
use function method_exists;
use function spl_object_id;
use function sprintf;
use function str_contains;
use function str_replace;
use function strtolower;
@@ -820,7 +823,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
assert($childProperty !== null);
if (isset($mapping->enumType)) {
$childProperty = new ReflectionEnumProperty(
$childProperty = new EnumReflectionProperty(
$childProperty,
$mapping->enumType,
);
@@ -839,7 +842,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
: $this->getAccessibleProperty($reflService, $this->name, $field);
if (isset($mapping->enumType) && $this->reflFields[$field] !== null) {
$this->reflFields[$field] = new ReflectionEnumProperty(
$this->reflFields[$field] = new EnumReflectionProperty(
$this->reflFields[$field],
$mapping->enumType,
);
@@ -1149,7 +1152,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
* fieldName?: string,
* columnName?: string,
* id?: bool,
* generated?: int,
* generated?: self::GENERATED_*,
* enumType?: class-string,
* } $mapping The field mapping to validate & complete.
*
@@ -2003,6 +2006,12 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
*/
public function setCustomRepositoryClass(string|null $repositoryClassName): void
{
if ($repositoryClassName === null) {
$this->customRepositoryClassName = null;
return;
}
$this->customRepositoryClassName = $this->fullyQualifiedClassName($repositoryClassName);
}
@@ -2457,21 +2466,43 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
public function getAssociationMappedByTargetField(string $assocName): string
{
$assoc = $this->associationMappings[$assocName];
$assoc = $this->getAssociationMapping($assocName);
assert($assoc instanceof InverseSideMapping);
if (! $assoc instanceof InverseSideMapping) {
throw new LogicException(sprintf(
<<<'EXCEPTION'
Context: Calling %s() with "%s", which is the owning side of an association.
Problem: The owning side of an association has no "mappedBy" field.
Solution: Call %s::isAssociationInverseSide() to check first.
EXCEPTION,
__METHOD__,
$assocName,
self::class,
));
}
return $assoc->mappedBy;
}
/**
* @return string|null null if the input value is null
* @psalm-return class-string|null
* @param C $className
*
* @return string|null null if and only if the input value is null
* @psalm-return (C is class-string ? class-string : (C is string ? string : null))
*
* @template C of string|null
*/
public function fullyQualifiedClassName(string|null $className): string|null
{
if (empty($className)) {
return $className;
if ($className === null) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11294',
'Passing null to %s is deprecated and will not be supported in Doctrine ORM 4.0',
__METHOD__,
);
return null;
}
if (! str_contains($className, '\\') && $this->namespace) {
@@ -2514,12 +2545,8 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
throw MappingException::missingEmbeddedClass($mapping['fieldName']);
}
$fqcn = $this->fullyQualifiedClassName($mapping['class']);
assert($fqcn !== null);
$this->embeddedClasses[$mapping['fieldName']] = EmbeddedClassMapping::fromMappingArray([
'class' => $fqcn,
'class' => $this->fullyQualifiedClassName($mapping['class']),
'columnPrefix' => $mapping['columnPrefix'] ?? null,
'declaredField' => $mapping['declaredField'] ?? null,
'originalField' => $mapping['originalField'] ?? null,

View File

@@ -642,7 +642,7 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
configuration.
We currently recommend "SEQUENCE" for "%s", when using DBAL 3,
and "IDENTITY" when using DBAL 4,
so you should use probably use the following configuration before upgrading to DBAL 4,
so you should probably use the following configuration before upgrading to DBAL 4,
and remove it after deploying that upgrade:
$configuration->setIdentityGenerationPreferences([

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use BackedEnum;
use DateInterval;
use DateTime;
use DateTimeImmutable;
@@ -16,6 +17,7 @@ use ReflectionProperty;
use function array_merge;
use function assert;
use function enum_exists;
use function is_a;
/** @psalm-type ScalarName = 'array'|'bool'|'float'|'int'|'string' */
final class DefaultTypedFieldMapper implements TypedFieldMapper
@@ -52,18 +54,18 @@ final class DefaultTypedFieldMapper implements TypedFieldMapper
&& ($type instanceof ReflectionNamedType)
) {
if (! $type->isBuiltin() && enum_exists($type->getName())) {
$mapping['enumType'] = $type->getName();
$reflection = new ReflectionEnum($type->getName());
if (! $reflection->isBacked()) {
throw MappingException::backedEnumTypeRequired(
$field->class,
$mapping['fieldName'],
$mapping['enumType'],
$type->getName(),
);
}
$type = $reflection->getBackingType();
assert(is_a($type->getName(), BackedEnum::class, true));
$mapping['enumType'] = $type->getName();
$type = $reflection->getBackingType();
assert($type instanceof ReflectionNamedType);
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Driver;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\MappingException;
@@ -20,6 +21,7 @@ use function assert;
use function constant;
use function count;
use function defined;
use function enum_exists;
use function explode;
use function extension_loaded;
use function file_get_contents;
@@ -403,9 +405,10 @@ class XmlDriver extends FileDriver
if (isset($oneToManyElement->{'order-by'})) {
$orderBy = [];
foreach ($oneToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) {
/** @psalm-suppress DeprecatedConstant */
$orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
? (string) $orderByField['direction']
: Criteria::ASC;
: (enum_exists(Order::class) ? Order::Ascending->value : Criteria::ASC);
}
$mapping['orderBy'] = $orderBy;
@@ -531,9 +534,10 @@ class XmlDriver extends FileDriver
if (isset($manyToManyElement->{'order-by'})) {
$orderBy = [];
foreach ($manyToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) {
/** @psalm-suppress DeprecatedConstant */
$orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
? (string) $orderByField['direction']
: Criteria::ASC;
: (enum_exists(Order::class) ? Order::Ascending->value : Criteria::ASC);
}
$mapping['orderBy'] = $orderBy;

View File

@@ -49,10 +49,12 @@ final class EmbeddedClassMapping implements ArrayAccess
/**
* @psalm-param array{
* class: class-string,
* columnPrefix?: false|string|null,
* declaredField?: string|null,
* originalField?: string|null
* class: class-string,
* columnPrefix?: false|string|null,
* declaredField?: string|null,
* originalField?: string|null,
* inherited?: class-string|null,
* declared?: class-string|null,
* } $mappingArray
*/
public static function fromMappingArray(array $mappingArray): self

View File

@@ -26,7 +26,7 @@ final class FieldMapping implements ArrayAccess
public bool|null $notInsertable = null;
public bool|null $notUpdatable = null;
public string|null $columnDefinition = null;
/** @psalm-var ClassMetadata::GENERATED_* */
/** @psalm-var ClassMetadata::GENERATED_*|null */
public int|null $generated = null;
/** @var class-string<BackedEnum>|null */
public string|null $enumType = null;
@@ -83,7 +83,34 @@ final class FieldMapping implements ArrayAccess
) {
}
/** @param array{type: string, fieldName: string, columnName: string} $mappingArray */
/**
* @param array<string, mixed> $mappingArray
* @psalm-param array{
* type: string,
* fieldName: string,
* columnName: string,
* length?: int|null,
* id?: bool|null,
* nullable?: bool|null,
* notInsertable?: bool|null,
* notUpdatable?: bool|null,
* columnDefinition?: string|null,
* generated?: ClassMetadata::GENERATED_*|null,
* enumType?: string|null,
* precision?: int|null,
* scale?: int|null,
* unique?: bool|null,
* inherited?: string|null,
* originalClass?: string|null,
* originalField?: string|null,
* quoted?: bool|null,
* declared?: string|null,
* declaredField?: string|null,
* options?: array<string, mixed>|null,
* version?: bool|null,
* default?: string|int|null,
* } $mappingArray
*/
public static function fromMappingArray(array $mappingArray): self
{
$mapping = new self(

View File

@@ -31,7 +31,17 @@ final class JoinColumnMapping implements ArrayAccess
/**
* @param array<string, mixed> $mappingArray
* @psalm-param array{name: string, referencedColumnName: string, ...} $mappingArray
* @psalm-param array{
* name: string,
* referencedColumnName: string,
* unique?: bool|null,
* quoted?: bool|null,
* fieldName?: string|null,
* onDelete?: string|null,
* columnDefinition?: string|null,
* nullable?: bool|null,
* options?: array<string, mixed>|null,
* } $mappingArray
*/
public static function fromMappingArray(array $mappingArray): self
{

View File

@@ -35,10 +35,10 @@ final class JoinTableMapping implements ArrayAccess
* @param mixed[] $mappingArray
* @psalm-param array{
* name: string,
* quoted?: bool,
* quoted?: bool|null,
* joinColumns?: mixed[],
* inverseJoinColumns?: mixed[],
* schema?: string,
* schema?: string|null,
* options?: array<string, mixed>
* } $mappingArray
*/

View File

@@ -41,9 +41,21 @@ final class ManyToManyOwningSideMapping extends ToManyOwningSideMapping implemen
* fieldName: string,
* sourceEntity: class-string,
* targetEntity: class-string,
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
* fetch?: ClassMetadata::FETCH_*|null,
* inherited?: class-string|null,
* declared?: class-string|null,
* cache?: array<mixed>|null,
* id?: bool|null,
* isOnDeleteCascade?: bool|null,
* originalClass?: class-string|null,
* originalField?: string|null,
* orphanRemoval?: bool,
* unique?: bool|null,
* joinTable?: mixed[]|null,
* type?: int,
* isOwningSide: bool, ...} $mappingArray
* isOwningSide: bool,
* } $mappingArray
*/
public static function fromMappingArrayAndNamingStrategy(array $mappingArray, NamingStrategy $namingStrategy): self
{

View File

@@ -12,9 +12,21 @@ final class OneToManyAssociationMapping extends ToManyInverseSideMapping
* fieldName: string,
* sourceEntity: class-string,
* targetEntity: class-string,
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
* fetch?: ClassMetadata::FETCH_*|null,
* inherited?: class-string|null,
* declared?: class-string|null,
* cache?: array<mixed>|null,
* id?: bool|null,
* isOnDeleteCascade?: bool|null,
* originalClass?: class-string|null,
* originalField?: string|null,
* orphanRemoval?: bool,
* unique?: bool|null,
* joinTable?: mixed[]|null,
* type?: int,
* isOwningSide: bool, ...} $mappingArray
* isOwningSide: bool,
* } $mappingArray
*/
public static function fromMappingArray(array $mappingArray): static
{
@@ -33,9 +45,21 @@ final class OneToManyAssociationMapping extends ToManyInverseSideMapping
* fieldName: string,
* sourceEntity: class-string,
* targetEntity: class-string,
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
* fetch?: ClassMetadata::FETCH_*|null,
* inherited?: class-string|null,
* declared?: class-string|null,
* cache?: array<mixed>|null,
* id?: bool|null,
* isOnDeleteCascade?: bool|null,
* originalClass?: class-string|null,
* originalField?: string|null,
* orphanRemoval?: bool,
* unique?: bool|null,
* joinTable?: mixed[]|null,
* type?: int,
* isOwningSide: bool, ...} $mappingArray
* isOwningSide: bool,
* } $mappingArray
*/
public static function fromMappingArrayAndName(array $mappingArray, string $name): static
{

View File

@@ -30,7 +30,7 @@ final class ReflectionEmbeddedProperty extends ReflectionProperty
private readonly ReflectionProperty $childProperty,
private readonly string $embeddedClass,
) {
parent::__construct($childProperty->class, $childProperty->name);
parent::__construct($childProperty->getDeclaringClass()->name, $childProperty->getName());
}
public function getValue(object|null $object = null): mixed

View File

@@ -11,6 +11,7 @@ use ValueError;
use function array_map;
use function is_array;
/** @deprecated use Doctrine\Persistence\Reflection\EnumReflectionProperty instead */
final class ReflectionEnumProperty extends ReflectionProperty
{
/** @param class-string<BackedEnum> $enumType */

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
use Doctrine\Deprecations\Deprecation;
#[Attribute(Attribute::TARGET_CLASS)]
final class Table implements MappingAttribute
@@ -21,5 +22,24 @@ final class Table implements MappingAttribute
public readonly array|null $uniqueConstraints = null,
public readonly array $options = [],
) {
if ($this->indexes !== null) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11357',
'Providing the property $indexes on %s does not have any effect and will be removed in Doctrine ORM 4.0. Please use the %s attribute instead.',
self::class,
Index::class,
);
}
if ($this->uniqueConstraints !== null) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11357',
'Providing the property $uniqueConstraints on %s does not have any effect and will be removed in Doctrine ORM 4.0. Please use the %s attribute instead.',
self::class,
UniqueConstraint::class,
);
}
}
}

View File

@@ -13,8 +13,21 @@ abstract class ToOneInverseSideMapping extends InverseSideMapping
* fieldName: string,
* sourceEntity: class-string,
* targetEntity: class-string,
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
* fetch?: ClassMetadata::FETCH_*|null,
* inherited?: class-string|null,
* declared?: class-string|null,
* cache?: array<mixed>|null,
* id?: bool|null,
* isOnDeleteCascade?: bool|null,
* originalClass?: class-string|null,
* originalField?: string|null,
* orphanRemoval?: bool,
* unique?: bool|null,
* joinTable?: mixed[]|null,
* type?: int,
* isOwningSide: bool,
* } $mappingArray
* } $mappingArray
*/
public static function fromMappingArrayAndName(
array $mappingArray,

View File

@@ -31,8 +31,22 @@ abstract class ToOneOwningSideMapping extends OwningSideMapping implements ToOne
* fieldName: string,
* sourceEntity: class-string,
* targetEntity: class-string,
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
* fetch?: ClassMetadata::FETCH_*|null,
* inherited?: class-string|null,
* declared?: class-string|null,
* cache?: array<mixed>|null,
* id?: bool|null,
* isOnDeleteCascade?: bool|null,
* originalClass?: class-string|null,
* originalField?: string|null,
* orphanRemoval?: bool,
* unique?: bool|null,
* joinTable?: mixed[]|null,
* type?: int,
* isOwningSide: bool,
* joinColumns?: mixed[]|null,
* isOwningSide: bool, ...} $mappingArray
* } $mappingArray
*/
public static function fromMappingArray(array $mappingArray): static
{
@@ -64,8 +78,22 @@ abstract class ToOneOwningSideMapping extends OwningSideMapping implements ToOne
* fieldName: string,
* sourceEntity: class-string,
* targetEntity: class-string,
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
* fetch?: ClassMetadata::FETCH_*|null,
* inherited?: class-string|null,
* declared?: class-string|null,
* cache?: array<mixed>|null,
* id?: bool|null,
* isOnDeleteCascade?: bool|null,
* originalClass?: class-string|null,
* originalField?: string|null,
* orphanRemoval?: bool,
* unique?: bool|null,
* joinTable?: mixed[]|null,
* type?: int,
* isOwningSide: bool,
* joinColumns?: mixed[]|null,
* isOwningSide: bool, ...} $mappingArray
* } $mappingArray
*/
public static function fromMappingArrayAndName(
array $mappingArray,

View File

@@ -8,6 +8,7 @@ use Doctrine\Common\Collections\AbstractLazyCollection;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\Common\Collections\Selectable;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\Mapping\ClassMetadata;
@@ -23,6 +24,7 @@ use function array_walk;
use function assert;
use function is_object;
use function spl_object_id;
use function strtoupper;
/**
* A PersistentCollection represents a collection of elements that have persistent state.
@@ -348,11 +350,6 @@ final class PersistentCollection extends AbstractLazyCollection implements Selec
return parent::containsKey($key);
}
/**
* {@inheritDoc}
*
* @template TMaybeContained
*/
public function contains(mixed $element): bool
{
if (! $this->initialized && $this->getMapping()->fetch === ClassMetadata::FETCH_EXTRA_LAZY) {
@@ -585,7 +582,12 @@ final class PersistentCollection extends AbstractLazyCollection implements Selec
$criteria = clone $criteria;
$criteria->where($expression);
$criteria->orderBy($criteria->getOrderings() ?: $association->orderBy());
$criteria->orderBy(
$criteria->orderings() ?: array_map(
static fn (string $order): Order => Order::from(strtoupper($order)),
$association->orderBy(),
),
);
$persister = $this->getUnitOfWork()->getEntityPersister($association->targetEntity);

View File

@@ -732,7 +732,7 @@ class ManyToManyPersister extends AbstractCollectionPersister
private function getOrderingSql(Criteria $criteria, ClassMetadata $targetClass): string
{
$orderings = $criteria->getOrderings();
$orderings = $criteria->orderings();
if ($orderings) {
$orderBy = [];
foreach ($orderings as $name => $direction) {
@@ -741,7 +741,7 @@ class ManyToManyPersister extends AbstractCollectionPersister
$targetClass,
$this->platform,
);
$orderBy[] = $field . ' ' . $direction;
$orderBy[] = $field . ' ' . $direction->value;
}
return ' ORDER BY ' . implode(', ', $orderBy);

View File

@@ -7,6 +7,7 @@ namespace Doctrine\ORM\Persisters\Entity;
use BackedEnum;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Expr\Comparison;
use Doctrine\Common\Collections\Order;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\LockMode;
@@ -764,7 +765,7 @@ class BasicEntityPersister implements EntityPersister
$targetClass = $this->em->getClassMetadata($assoc->targetEntity);
if ($assoc->isOwningSide()) {
$isInverseSingleValued = $assoc->inversedBy && ! $targetClass->isCollectionValuedAssociation($assoc->inversedBy);
$isInverseSingleValued = $assoc->inversedBy !== null && ! $targetClass->isCollectionValuedAssociation($assoc->inversedBy);
// Mark inverse side as fetched in the hints, otherwise the UoW would
// try to load it in a separate query (remember: to-one inverse sides can not be lazy).
@@ -842,7 +843,10 @@ class BasicEntityPersister implements EntityPersister
*/
public function loadCriteria(Criteria $criteria): array
{
$orderBy = $criteria->getOrderings();
$orderBy = array_map(
static fn (Order $order): string => $order->value,
$criteria->orderings(),
);
$limit = $criteria->getMaxResults();
$offset = $criteria->getFirstResult();
$query = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
@@ -1151,7 +1155,7 @@ class BasicEntityPersister implements EntityPersister
if (isset($this->class->fieldMappings[$fieldName])) {
$tableAlias = isset($this->class->fieldMappings[$fieldName]->inherited)
? $this->getSQLTableAlias($this->class->fieldMappings[$fieldName]['inherited'])
? $this->getSQLTableAlias($this->class->fieldMappings[$fieldName]->inherited)
: $baseTableAlias;
$columnName = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform);

View File

@@ -466,7 +466,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
|| isset($this->class->associationMappings[$name]->inherited)
|| ($this->class->isVersioned && $this->class->versionField === $name)
|| isset($this->class->embeddedClasses[$name])
|| isset($this->class->fieldMappings[$name]['notInsertable'])
|| isset($this->class->fieldMappings[$name]->notInsertable)
) {
continue;
}
@@ -519,9 +519,9 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
$class = null;
if ($this->class->isVersioned && $key === $versionedClass->versionField) {
$class = $versionedClass;
} elseif (isset($column['generated'])) {
$class = isset($column['inherited'])
? $this->em->getClassMetadata($column['inherited'])
} elseif (isset($column->generated)) {
$class = isset($column->inherited)
? $this->em->getClassMetadata($column->inherited)
: $this->class;
} else {
continue;

View File

@@ -216,11 +216,11 @@ EOPHP;
*/
private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure
{
return static function (InternalProxy $proxy, InternalProxy $original) use ($entityPersister, $classMetadata, $identifierFlattener): void {
$identifier = $classMetadata->getIdentifierValues($original);
$entity = $entityPersister->loadById($identifier, $original);
return static function (InternalProxy $proxy) use ($entityPersister, $classMetadata, $identifierFlattener): void {
$identifier = $classMetadata->getIdentifierValues($proxy);
$original = $entityPersister->loadById($identifier);
if ($entity === null) {
if ($original === null) {
throw EntityNotFoundException::fromClassNameAndIdentifier(
$classMetadata->getName(),
$identifierFlattener->flattenIdentifier($classMetadata, $identifier),
@@ -234,11 +234,11 @@ EOPHP;
$class = $entityPersister->getClassMetadata();
foreach ($class->getReflectionProperties() as $property) {
if (! $property || ! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) {
if (! $property || ! $class->hasField($property->getName()) && ! $class->hasAssociation($property->getName())) {
continue;
}
$property->setValue($proxy, $property->getValue($entity));
$property->setValue($proxy, $property->getValue($original));
}
};
}
@@ -283,9 +283,7 @@ EOPHP;
$identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers);
$proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy {
$proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, &$proxy): void {
$initializer($object, $proxy);
}, $skippedProperties);
$proxy = self::createLazyGhost($initializer, $skippedProperties);
foreach ($identifierFields as $idField => $reflector) {
if (! isset($identifier[$idField])) {

View File

@@ -635,9 +635,10 @@ class Query extends AbstractQuery
/**
* Get the current lock mode for this query.
*
* @return int|null The current lock mode of this query or NULL if no specific lock mode is set.
* @return LockMode|int|null The current lock mode of this query or NULL if no specific lock mode is set.
* @psalm-return LockMode::*|null
*/
public function getLockMode(): int|null
public function getLockMode(): LockMode|int|null
{
$lockMode = $this->getHint(self::HINT_LOCK_MODE);

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Query;
use Doctrine\ORM\Internal\NoUnknownNamedArguments;
use Traversable;
use function implode;
@@ -23,6 +24,8 @@ use function str_replace;
*/
class Expr
{
use NoUnknownNamedArguments;
/**
* Creates a conjunction of the given boolean expressions.
*
@@ -38,6 +41,8 @@ class Expr
*/
public function andX(Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x): Expr\Andx
{
self::validateVariadicParameter($x);
return new Expr\Andx($x);
}
@@ -56,6 +61,8 @@ class Expr
*/
public function orX(Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x): Expr\Orx
{
self::validateVariadicParameter($x);
return new Expr\Orx($x);
}
@@ -225,6 +232,8 @@ class Expr
*/
public function countDistinct(mixed ...$x): string
{
self::validateVariadicParameter($x);
return 'COUNT(DISTINCT ' . implode(', ', $x) . ')';
}
@@ -470,6 +479,8 @@ class Expr
*/
public function concat(mixed ...$x): Expr\Func
{
self::validateVariadicParameter($x);
return new Expr\Func('CONCAT', $x);
}

View File

@@ -134,7 +134,7 @@ class ResultSetMappingBuilder extends ResultSetMapping implements Stringable
$this->addFieldResult($alias, $columnAlias, $propertyName);
$enumType = $classMetadata->getFieldMapping($propertyName)['enumType'] ?? null;
$enumType = $classMetadata->getFieldMapping($propertyName)->enumType ?? null;
if (! empty($enumType)) {
$this->addEnumResult($columnAlias, $enumType);
}

View File

@@ -30,6 +30,7 @@ use function count;
use function implode;
use function is_array;
use function is_float;
use function is_int;
use function is_numeric;
use function is_string;
use function preg_match;
@@ -384,7 +385,9 @@ class SqlWalker
$values = [];
if ($class->discriminatorValue !== null) { // discriminators can be 0
$values[] = $conn->quote($class->discriminatorValue);
$values[] = $class->getDiscriminatorColumn()->type === 'integer' && is_int($class->discriminatorValue)
? $class->discriminatorValue
: $conn->quote((string) $class->discriminatorValue);
}
foreach ($class->subClasses as $subclassName) {
@@ -396,7 +399,9 @@ class SqlWalker
continue;
}
$values[] = $conn->quote((string) $subclassMetadata->discriminatorValue);
$values[] = $subclassMetadata->getDiscriminatorColumn()->type === 'integer' && is_int($subclassMetadata->discriminatorValue)
? $subclassMetadata->discriminatorValue
: $conn->quote((string) $subclassMetadata->discriminatorValue);
}
if ($values !== []) {
@@ -798,7 +803,7 @@ class SqlWalker
$class = $this->getMetadataForDqlAlias($alias);
if (isset($class->associationMappings[$fieldName]->inherited)) {
$class = $this->em->getClassMetadata($class->associationMappings[$fieldName]['inherited']);
$class = $this->em->getClassMetadata($class->associationMappings[$fieldName]->inherited);
}
$association = $class->associationMappings[$fieldName];
@@ -2246,8 +2251,10 @@ class SqlWalker
$discriminators += HierarchyDiscriminatorResolver::resolveDiscriminatorsForClass($metadata, $this->em);
}
foreach (array_keys($discriminators) as $dis) {
$sqlParameterList[] = $this->conn->quote($dis);
foreach (array_keys($discriminators) as $discriminatorValue) {
$sqlParameterList[] = $rootClass->getDiscriminatorColumn()->type === 'integer' && is_int($discriminatorValue)
? $discriminatorValue
: $this->conn->quote((string) $discriminatorValue);
}
return '(' . implode(', ', $sqlParameterList) . ')';

View File

@@ -8,6 +8,7 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\ParameterType;
use Doctrine\ORM\Internal\NoUnknownNamedArguments;
use Doctrine\ORM\Internal\QueryType;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\Query\Parameter;
@@ -40,6 +41,8 @@ use function substr;
*/
class QueryBuilder implements Stringable
{
use NoUnknownNamedArguments;
/**
* The array of DQL parts collected.
*
@@ -613,6 +616,8 @@ class QueryBuilder implements Stringable
*/
public function select(mixed ...$select): static
{
self::validateVariadicParameter($select);
$this->type = QueryType::Select;
if ($select === []) {
@@ -659,6 +664,8 @@ class QueryBuilder implements Stringable
*/
public function addSelect(mixed ...$select): static
{
self::validateVariadicParameter($select);
$this->type = QueryType::Select;
if ($select === []) {
@@ -953,6 +960,8 @@ class QueryBuilder implements Stringable
*/
public function where(mixed ...$predicates): static
{
self::validateVariadicParameter($predicates);
if (! (count($predicates) === 1 && $predicates[0] instanceof Expr\Composite)) {
$predicates = new Expr\Andx($predicates);
}
@@ -978,6 +987,8 @@ class QueryBuilder implements Stringable
*/
public function andWhere(mixed ...$where): static
{
self::validateVariadicParameter($where);
$dql = $this->getDQLPart('where');
if ($dql instanceof Expr\Andx) {
@@ -1008,6 +1019,8 @@ class QueryBuilder implements Stringable
*/
public function orWhere(mixed ...$where): static
{
self::validateVariadicParameter($where);
$dql = $this->getDQLPart('where');
if ($dql instanceof Expr\Orx) {
@@ -1035,6 +1048,8 @@ class QueryBuilder implements Stringable
*/
public function groupBy(string ...$groupBy): static
{
self::validateVariadicParameter($groupBy);
return $this->add('groupBy', new Expr\GroupBy($groupBy));
}
@@ -1053,6 +1068,8 @@ class QueryBuilder implements Stringable
*/
public function addGroupBy(string ...$groupBy): static
{
self::validateVariadicParameter($groupBy);
return $this->add('groupBy', new Expr\GroupBy($groupBy), true);
}
@@ -1064,6 +1081,8 @@ class QueryBuilder implements Stringable
*/
public function having(mixed ...$having): static
{
self::validateVariadicParameter($having);
if (! (count($having) === 1 && ($having[0] instanceof Expr\Andx || $having[0] instanceof Expr\Orx))) {
$having = new Expr\Andx($having);
}
@@ -1079,6 +1098,8 @@ class QueryBuilder implements Stringable
*/
public function andHaving(mixed ...$having): static
{
self::validateVariadicParameter($having);
$dql = $this->getDQLPart('having');
if ($dql instanceof Expr\Andx) {
@@ -1099,6 +1120,8 @@ class QueryBuilder implements Stringable
*/
public function orHaving(mixed ...$having): static
{
self::validateVariadicParameter($having);
$dql = $this->getDQLPart('having');
if ($dql instanceof Expr\Orx) {
@@ -1164,22 +1187,20 @@ class QueryBuilder implements Stringable
}
}
if ($criteria->getOrderings()) {
foreach ($criteria->getOrderings() as $sort => $order) {
$hasValidAlias = false;
foreach ($allAliases as $alias) {
if (str_starts_with($sort . '.', $alias . '.')) {
$hasValidAlias = true;
break;
}
foreach ($criteria->orderings() as $sort => $order) {
$hasValidAlias = false;
foreach ($allAliases as $alias) {
if (str_starts_with($sort . '.', $alias . '.')) {
$hasValidAlias = true;
break;
}
if (! $hasValidAlias) {
$sort = $allAliases[0] . '.' . $sort;
}
$this->addOrderBy($sort, $order);
}
if (! $hasValidAlias) {
$sort = $allAliases[0] . '.' . $sort;
}
$this->addOrderBy($sort, $order->value);
}
// Overwrite limits only if they was set in criteria

View File

@@ -5,8 +5,10 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder;
use function assert;
use function ltrim;
/**
@@ -14,16 +16,22 @@ use function ltrim;
*/
class AttachEntityListenersListener
{
/** @var mixed[][] */
/**
* @var array<class-string, list<array{
* event: Events::*|null,
* class: class-string,
* method: string|null,
* }>>
*/
private array $entityListeners = [];
/**
* Adds an entity listener for a specific entity.
*
* @param string $entityClass The entity to attach the listener.
* @param string $listenerClass The listener class.
* @param string|null $eventName The entity lifecycle event.
* @param string|null $listenerCallback The listener callback method or NULL to use $eventName.
* @param class-string $entityClass The entity to attach the listener.
* @param class-string $listenerClass The listener class.
* @param Events::*|null $eventName The entity lifecycle event.
* @param non-falsy-string|null $listenerCallback The listener callback method or NULL to use $eventName.
*/
public function addEntityListener(
string $entityClass,
@@ -34,7 +42,7 @@ class AttachEntityListenersListener
$this->entityListeners[ltrim($entityClass, '\\')][] = [
'event' => $eventName,
'class' => $listenerClass,
'method' => $listenerCallback ?: $eventName,
'method' => $listenerCallback ?? $eventName,
];
}
@@ -53,6 +61,7 @@ class AttachEntityListenersListener
if ($listener['event'] === null) {
EntityListenerBuilder::bindEntityListener($metadata, $listener['class']);
} else {
assert($listener['method'] !== null);
$metadata->addEntityListener($listener['event'], $listener['class'], $listener['method']);
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command\SchemaTool;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Tools\SchemaTool;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -28,7 +29,7 @@ class UpdateCommand extends AbstractCommand
$this->setName($this->name)
->setDescription('Executes (or dumps) the SQL needed to update the database schema to match the current mapping metadata')
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->addOption('complete', null, InputOption::VALUE_NONE, 'This option is a no-op and will be removed in 4.0')
->addOption('complete', null, InputOption::VALUE_NONE, 'This option is a no-op, is deprecated and will be removed in 4.0')
->addOption('dump-sql', null, InputOption::VALUE_NONE, 'Dumps the generated SQL statements to the screen (does not execute them).')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Causes the generated SQL statements to be physically executed against your database.')
->setHelp(<<<'EOT'
@@ -75,6 +76,15 @@ EOT);
{
$notificationUi = $ui->getErrorStyle();
if ($input->getOption('complete') === true) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11354',
'The --complete option is a no-op, is deprecated and will be removed in Doctrine ORM 4.0.',
);
$notificationUi->warning('The --complete option is a no-op, is deprecated and will be removed in Doctrine ORM 4.0.');
}
$sqls = $schemaTool->getUpdateSchemaSql($metadatas);
if (empty($sqls)) {

View File

@@ -337,7 +337,7 @@ class SchemaTool
if (isset($class->table['uniqueConstraints'])) {
foreach ($class->table['uniqueConstraints'] as $indexName => $indexData) {
$uniqIndex = new Index($indexName, $this->getIndexColumns($class, $indexData), true, false, [], $indexData['options'] ?? []);
$uniqIndex = new Index('tmp__' . $indexName, $this->getIndexColumns($class, $indexData), true, false, [], $indexData['options'] ?? []);
foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
if ($tableIndex->isFulfilledBy($uniqIndex)) {

View File

@@ -52,18 +52,18 @@ class SchemaValidator
* It maps built-in Doctrine types to PHP types
*/
private const BUILTIN_TYPES_MAP = [
AsciiStringType::class => 'string',
BigIntType::class => 'string',
BooleanType::class => 'bool',
DecimalType::class => 'string',
FloatType::class => 'float',
GuidType::class => 'string',
IntegerType::class => 'int',
JsonType::class => 'array',
SimpleArrayType::class => 'array',
SmallIntType::class => 'int',
StringType::class => 'string',
TextType::class => 'string',
AsciiStringType::class => ['string'],
BigIntType::class => ['int', 'string'],
BooleanType::class => ['bool'],
DecimalType::class => ['string'],
FloatType::class => ['float'],
GuidType::class => ['string'],
IntegerType::class => ['int'],
JsonType::class => ['array'],
SimpleArrayType::class => ['array'],
SmallIntType::class => ['int'],
StringType::class => ['string'],
TextType::class => ['string'],
];
public function __construct(
@@ -162,7 +162,7 @@ class SchemaValidator
}
}
if ($assoc->isOwningSide() && $assoc->inversedBy) {
if ($assoc->isOwningSide() && $assoc->inversedBy !== null) {
if ($targetMetadata->hasField($assoc->inversedBy)) {
$ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the inverse side ' .
'field ' . $assoc->targetEntity . '#' . $assoc->inversedBy . ' which is not defined as association.';
@@ -343,7 +343,7 @@ class SchemaValidator
return null;
}
$metadataFieldType = $this->findBuiltInType(Type::getType($fieldMapping['type']));
$metadataFieldType = $this->findBuiltInType(Type::getType($fieldMapping->type));
//If the metadata field type is not a mapped built-in type, we cannot check it
if ($metadataFieldType === null) {
@@ -353,25 +353,25 @@ class SchemaValidator
$propertyType = $propertyType->getName();
// If the property type is the same as the metadata field type, we are ok
if ($propertyType === $metadataFieldType) {
if (in_array($propertyType, $metadataFieldType, true)) {
return null;
}
if (is_a($propertyType, BackedEnum::class, true)) {
$backingType = (string) (new ReflectionEnum($propertyType))->getBackingType();
if ($metadataFieldType !== $backingType) {
if (! in_array($backingType, $metadataFieldType, true)) {
return sprintf(
"The field '%s#%s' has the property type '%s' with a backing type of '%s' that differs from the metadata field type '%s'.",
$class->name,
$fieldName,
$propertyType,
$backingType,
$metadataFieldType,
implode('|', $metadataFieldType),
);
}
if (! isset($fieldMapping['enumType']) || $propertyType === $fieldMapping['enumType']) {
if (! isset($fieldMapping->enumType) || $propertyType === $fieldMapping->enumType) {
return null;
}
@@ -380,19 +380,19 @@ class SchemaValidator
$class->name,
$fieldName,
$propertyType,
$fieldMapping['enumType'],
$fieldMapping->enumType,
);
}
if (
isset($fieldMapping['enumType'])
&& $propertyType !== $fieldMapping['enumType']
isset($fieldMapping->enumType)
&& $propertyType !== $fieldMapping->enumType
&& interface_exists($propertyType)
&& is_a($fieldMapping['enumType'], $propertyType, true)
&& is_a($fieldMapping->enumType, $propertyType, true)
) {
$backingType = (string) (new ReflectionEnum($fieldMapping['enumType']))->getBackingType();
$backingType = (string) (new ReflectionEnum($fieldMapping->enumType))->getBackingType();
if ($metadataFieldType === $backingType) {
if (in_array($backingType, $metadataFieldType, true)) {
return null;
}
@@ -400,14 +400,14 @@ class SchemaValidator
"The field '%s#%s' has the metadata enumType '%s' with a backing type of '%s' that differs from the metadata field type '%s'.",
$class->name,
$fieldName,
$fieldMapping['enumType'],
$fieldMapping->enumType,
$backingType,
$metadataFieldType,
implode('|', $metadataFieldType),
);
}
if (
$fieldMapping['type'] === 'json'
$fieldMapping->type === 'json'
&& in_array($propertyType, ['string', 'int', 'float', 'bool', 'true', 'false', 'null'], true)
) {
return null;
@@ -418,8 +418,8 @@ class SchemaValidator
$class->name,
$fieldName,
$propertyType,
$metadataFieldType,
$fieldMapping['type'],
implode('|', $metadataFieldType),
$fieldMapping->type,
);
},
$class->fieldMappings,
@@ -431,8 +431,10 @@ class SchemaValidator
/**
* The exact DBAL type must be used (no subclasses), since consumers of doctrine/orm may have their own
* customization around field types.
*
* @return list<string>|null
*/
private function findBuiltInType(Type $type): string|null
private function findBuiltInType(Type $type): array|null
{
$typeName = $type::class;

View File

@@ -997,7 +997,7 @@ class UnitOfWork implements PropertyChangedListener
foreach ($actualData as $propName => $actualValue) {
$orgValue = $originalData[$propName] ?? null;
if (isset($class->fieldMappings[$propName]['enumType'])) {
if (isset($class->fieldMappings[$propName]->enumType)) {
if (is_array($orgValue)) {
foreach ($orgValue as $id => $val) {
if ($val instanceof BackedEnum) {
@@ -1143,6 +1143,8 @@ class UnitOfWork implements PropertyChangedListener
$eventsToDispatch = [];
foreach ($entities as $entity) {
$this->removeFromIdentityMap($entity);
$oid = spl_object_id($entity);
$class = $this->em->getClassMetadata($entity::class);
$persister = $this->getEntityPersister($class->name);
@@ -1267,16 +1269,16 @@ class UnitOfWork implements PropertyChangedListener
}
$joinColumns = reset($assoc->joinColumns);
if (! isset($joinColumns['onDelete'])) {
if (! isset($joinColumns->onDelete)) {
continue;
}
$onDeleteOption = strtolower($joinColumns['onDelete']);
$onDeleteOption = strtolower($joinColumns->onDelete);
if ($onDeleteOption !== 'cascade') {
continue;
}
$targetEntity = $class->getFieldValue($entity, $assoc['fieldName']);
$targetEntity = $class->getFieldValue($entity, $assoc->fieldName);
// If the association does not refer to another entity or that entity
// is not to be deleted, there is no ordering problem and we can
@@ -1484,8 +1486,6 @@ class UnitOfWork implements PropertyChangedListener
return;
}
$this->removeFromIdentityMap($entity);
unset($this->entityUpdates[$oid]);
if (! isset($this->entityDeletions[$oid])) {
@@ -1557,18 +1557,15 @@ class UnitOfWork implements PropertyChangedListener
*/
final public static function getIdHashByIdentifier(array $identifier): string
{
foreach ($identifier as $k => $value) {
if ($value instanceof BackedEnum) {
$identifier[$k] = $value->value;
}
}
return implode(
' ',
array_map(
static function ($value) {
if ($value instanceof BackedEnum) {
return $value->value;
}
return $value;
},
$identifier,
),
$identifier,
);
}
@@ -2550,7 +2547,7 @@ class UnitOfWork implements PropertyChangedListener
$this->originalEntityData[$oid][$field] = $newValue;
$class->reflFields[$field]->setValue($entity, $newValue);
if ($assoc->inversedBy && $assoc->isOneToOne() && $newValue !== null) {
if ($assoc->inversedBy !== null && $assoc->isOneToOne() && $newValue !== null) {
$inverseAssoc = $targetClass->associationMappings[$assoc->inversedBy];
$targetClass->reflFields[$inverseAssoc->fieldName]->setValue($newValue, $entity);
}
@@ -2584,9 +2581,9 @@ class UnitOfWork implements PropertyChangedListener
if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER) {
$isIteration = isset($hints[Query::HINT_INTERNAL_ITERATION]) && $hints[Query::HINT_INTERNAL_ITERATION];
if (! $isIteration && $assoc->isOneToMany()) {
if (! $isIteration && $assoc->isOneToMany() && ! $targetClass->isIdentifierComposite && ! $assoc->isIndexed()) {
$this->scheduleCollectionForBatchLoading($pColl, $class);
} elseif (($isIteration && $assoc->isOneToMany()) || $assoc->isManyToMany()) {
} else {
$this->loadCollection($pColl);
$pColl->takeSnapshot();
}
@@ -2656,7 +2653,7 @@ class UnitOfWork implements PropertyChangedListener
$entities[] = $collection->getOwner();
}
$found = $this->getEntityPersister($targetEntity)->loadAll([$mappedBy => $entities]);
$found = $this->getEntityPersister($targetEntity)->loadAll([$mappedBy => $entities], $mapping->orderBy);
$targetClass = $this->em->getClassMetadata($targetEntity);
$targetProperty = $targetClass->getReflectionProperty($mappedBy);
@@ -2665,7 +2662,19 @@ class UnitOfWork implements PropertyChangedListener
foreach ($found as $targetValue) {
$sourceEntity = $targetProperty->getValue($targetValue);
$id = $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($sourceEntity));
if ($sourceEntity === null && isset($targetClass->associationMappings[$mappedBy]->joinColumns)) {
// case where the hydration $targetValue itself has not yet fully completed, for example
// in case a bi-directional association is being hydrated and deferring eager loading is
// not possible due to subclassing.
$data = $this->getOriginalEntityData($targetValue);
$id = [];
foreach ($targetClass->associationMappings[$mappedBy]->joinColumns as $joinColumn) {
$id[] = $data[$joinColumn->name];
}
} else {
$id = $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($sourceEntity));
}
$idHash = implode(' ', $id);
if ($mapping->indexBy !== null) {
@@ -2713,7 +2722,7 @@ class UnitOfWork implements PropertyChangedListener
private function scheduleCollectionForBatchLoading(PersistentCollection $collection, ClassMetadata $sourceClass): void
{
$mapping = $collection->getMapping();
$name = $mapping['sourceEntity'] . '#' . $mapping['fieldName'];
$name = $mapping->sourceEntity . '#' . $mapping->fieldName;
if (! isset($this->eagerLoadingCollections[$name])) {
$this->eagerLoadingCollections[$name] = [

View File

@@ -4,8 +4,7 @@ declare(strict_types=1);
namespace Doctrine\Performance\Mock;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
/**
@@ -13,22 +12,14 @@ use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
*/
class NonLoadingPersister extends BasicEntityPersister
{
public function __construct()
{
public function __construct(
ClassMetadata $class,
) {
$this->class = $class;
}
/**
* {@inheritDoc}
*/
public function load(
array $criteria,
object|null $entity = null,
AssociationMapping|null $assoc = null,
array $hints = [],
LockMode|int|null $lockMode = null,
int|null $limit = null,
array|null $orderBy = null,
): object|null {
return $entity;
public function loadById(array $identifier, object|null $entity = null): object|null
{
return $entity ?? new ($this->class->name)();
}
}

View File

@@ -57,7 +57,7 @@ class NonProxyLoadingEntityManager implements EntityManagerInterface
public function getUnitOfWork(): UnitOfWork
{
return new NonProxyLoadingUnitOfWork();
return new NonProxyLoadingUnitOfWork($this);
}
public function getCache(): Cache|null

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\Performance\Mock;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\UnitOfWork;
/**
@@ -11,15 +12,17 @@ use Doctrine\ORM\UnitOfWork;
*/
class NonProxyLoadingUnitOfWork extends UnitOfWork
{
private NonLoadingPersister $entityPersister;
/** @var array<class-string, NonLoadingPersister> */
private array $entityPersisters = [];
public function __construct()
{
$this->entityPersister = new NonLoadingPersister();
public function __construct(
private EntityManagerInterface $entityManager,
) {
}
public function getEntityPersister(string $entityName): NonLoadingPersister
{
return $this->entityPersister;
return $this->entityPersisters[$entityName]
??= new NonLoadingPersister($this->entityManager->getClassMetadata($entityName));
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\AbstractFetchEager;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'abstract_fetch_eager_remote_control')]
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\DiscriminatorColumn(name: 'type', type: 'string')]
#[ORM\DiscriminatorMap(['mobile' => 'MobileRemoteControl'])]
abstract class AbstractRemoteControl
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
public int $id;
#[ORM\Column(type: 'string')]
public string $name;
/** @var Collection<User> */
#[ORM\OneToMany(targetEntity: User::class, mappedBy: 'remoteControl', fetch: 'EAGER')]
public Collection $users;
public function __construct(string $name)
{
$this->name = $name;
$this->users = new ArrayCollection();
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\AbstractFetchEager;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class MobileRemoteControl extends AbstractRemoteControl
{
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\AbstractFetchEager;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'abstract_fetch_eager_user')]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
public int $id;
#[ORM\ManyToOne(targetEntity: AbstractRemoteControl::class, inversedBy: 'users')]
#[ORM\JoinColumn(nullable: false)]
public AbstractRemoteControl $remoteControl;
public function __construct(AbstractRemoteControl $control)
{
$this->remoteControl = $control;
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\BigIntegers;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class BigIntegers
{
#[ORM\Column]
#[ORM\Id]
#[ORM\GeneratedValue]
public int|null $id = null;
#[ORM\Column(type: Types::BIGINT)]
public int $one = 1;
#[ORM\Column(type: Types::BIGINT)]
public string $two = '2';
#[ORM\Column(type: Types::BIGINT)]
public float $three = 3.0;
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\EagerFetchedCompositeOneToMany;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'eager_composite_join_root')]
class RootEntity
{
#[ORM\Id]
#[ORM\Column(type: 'integer', nullable: false)]
private int|null $id = null;
#[ORM\Id]
#[ORM\Column(type: 'string', nullable: false, name: 'other_key', length: 42)]
private string $otherKey;
/** @var Collection<int, SecondLevel> */
#[ORM\OneToMany(mappedBy: 'root', targetEntity: SecondLevel::class, fetch: 'EAGER')]
private Collection $secondLevel;
public function __construct(int $id, string $other)
{
$this->otherKey = $other;
$this->secondLevel = new ArrayCollection();
$this->id = $id;
}
public function getId(): int|null
{
return $this->id;
}
public function getOtherKey(): string
{
return $this->otherKey;
}
}

View File

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

View File

@@ -6,6 +6,7 @@ namespace Doctrine\Tests\ORM\Cache\Persister\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\Cache\Persister\CachedPersister;
use Doctrine\ORM\Cache\Persister\Entity\AbstractEntityPersister;
use Doctrine\ORM\Cache\Persister\Entity\CachedEntityPersister;
@@ -97,7 +98,7 @@ abstract class EntityPersisterTestCase extends OrmTestCase
->with(
self::identicalTo(['name' => 'Foo']),
self::identicalTo($associationMapping),
self::identicalTo(1),
self::identicalTo(LockMode::OPTIMISTIC),
self::identicalTo(2),
self::identicalTo(3),
self::identicalTo([4]),
@@ -107,7 +108,7 @@ abstract class EntityPersisterTestCase extends OrmTestCase
self::assertSame('SELECT * FROM foo WERE name = ?', $persister->getSelectSQL(
['name' => 'Foo'],
$associationMapping,
1,
LockMode::OPTIMISTIC,
2,
3,
[4],
@@ -233,13 +234,21 @@ abstract class EntityPersisterTestCase extends OrmTestCase
self::identicalTo($entity),
self::identicalTo($associationMapping),
self::identicalTo([1]),
self::identicalTo(2),
self::identicalTo(LockMode::PESSIMISTIC_READ),
self::identicalTo(3),
self::identicalTo([4]),
)
->willReturn($entity);
self::assertSame($entity, $persister->load(['id' => 1], $entity, $associationMapping, [1], 2, 3, [4]));
self::assertSame($entity, $persister->load(
['id' => 1],
$entity,
$associationMapping,
[1],
LockMode::PESSIMISTIC_READ,
3,
[4],
));
}
public function testInvokeLoadAll(): void
@@ -402,9 +411,9 @@ abstract class EntityPersisterTestCase extends OrmTestCase
$this->entityPersister->expects(self::once())
->method('lock')
->with(self::identicalTo($identifier), self::identicalTo(1));
->with(self::identicalTo($identifier), self::identicalTo(LockMode::OPTIMISTIC));
$persister->lock($identifier, 1);
$persister->lock($identifier, LockMode::OPTIMISTIC);
}
public function testInvokeExists(): void

View File

@@ -7,6 +7,7 @@ namespace Doctrine\Tests\ORM;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Exception\EntityManagerClosed;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
@@ -21,7 +22,9 @@ use Doctrine\Tests\OrmTestCase;
use Generator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use ReflectionProperty;
use stdClass;
use Symfony\Component\VarExporter\LazyGhostTrait;
use TypeError;
class EntityManagerTest extends OrmTestCase
@@ -172,4 +175,36 @@ class EntityManagerTest extends OrmTestCase
self::assertFalse($this->entityManager->isOpen());
}
}
/** Resetting the EntityManager relies on lazy objects until https://github.com/doctrine/orm/issues/5933 is resolved */
public function testLazyGhostEntityManager(): void
{
$em = new class () extends EntityManager {
use LazyGhostTrait;
public function __construct()
{
}
};
$em = $em::createLazyGhost(static function ($em): void {
$r = new ReflectionProperty(EntityManager::class, 'unitOfWork');
$r->setValue($em, new class () extends UnitOfWork {
public function __construct()
{
}
public function clear(): void
{
}
});
});
$this->assertTrue($em->isOpen());
$em->close();
$this->assertFalse($em->isOpen());
$em->resetLazyObject();
$this->assertTrue($em->isOpen());
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Tests\Models\AbstractFetchEager\AbstractRemoteControl;
use Doctrine\Tests\Models\AbstractFetchEager\MobileRemoteControl;
use Doctrine\Tests\Models\AbstractFetchEager\User;
use Doctrine\Tests\OrmFunctionalTestCase;
final class AbstractFetchEagerTest extends OrmFunctionalTestCase
{
public function testWithAbstractFetchEager(): void
{
$this->createSchemaForModels(
AbstractRemoteControl::class,
User::class,
);
$control = new MobileRemoteControl('smart');
$user = new User($control);
$entityManage = $this->getEntityManager();
$entityManage->persist($control);
$entityManage->persist($user);
$entityManage->flush();
$entityManage->clear();
$user = $entityManage->find(User::class, $user->id);
self::assertNotNull($user);
self::assertEquals('smart', $user->remoteControl->name);
self::assertTrue($user->remoteControl->users->contains($user));
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Tests\Models\EagerFetchedCompositeOneToMany\RootEntity;
use Doctrine\Tests\Models\EagerFetchedCompositeOneToMany\SecondLevel;
use Doctrine\Tests\OrmFunctionalTestCase;
final class EagerFetchOneToManyWithCompositeKeyTest extends OrmFunctionalTestCase
{
/** @ticket 11154 */
public function testItDoesNotThrowAnExceptionWhenTriggeringALoad(): void
{
$this->setUpEntitySchema([RootEntity::class, SecondLevel::class]);
$a1 = new RootEntity(1, 'A');
$this->_em->persist($a1);
$this->_em->flush();
$this->_em->clear();
self::assertCount(1, $this->_em->getRepository(RootEntity::class)->findAll());
}
}

View File

@@ -6,6 +6,7 @@ namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\UnitOfWork;
@@ -16,6 +17,7 @@ use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;
use function assert;
use function class_exists;
/**
* Basic many-to-many association tests.
@@ -435,7 +437,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
$user = $this->_em->find($user::class, $user->id);
$criteria = Criteria::create()
->orderBy(['name' => Criteria::ASC]);
->orderBy(['name' => class_exists(Order::class) ? Order::Ascending : Criteria::ASC]);
self::assertEquals(
['A', 'B', 'C', 'Developers_0'],
@@ -475,7 +477,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
$user = $this->_em->find($user::class, $user->id);
$criteria = Criteria::create()
->orderBy(['name' => Criteria::ASC]);
->orderBy(['name' => class_exists(Order::class) ? Order::Ascending : Criteria::ASC]);
self::assertEquals(
['A', 'B', 'C'],

View File

@@ -252,7 +252,6 @@ class QueryDqlFunctionTest extends OrmFunctionalTestCase
self::assertEquals(1_600_000, $result[3]['op']);
}
#[Group('test')]
public function testOperatorDiv(): void
{
$result = $this->_em->createQuery('SELECT m, (m.salary/0.5) AS op FROM Doctrine\Tests\Models\Company\CompanyManager m ORDER BY m.salary ASC')

View File

@@ -25,8 +25,8 @@ class GH11135Test extends OrmFunctionalTestCase
$cm1 = $this->_em->getClassMetadata(GH11135EntityWithOverride::class);
$cm2 = $this->_em->getClassMetadata(GH11135EntityWithoutOverride::class);
self::assertSame($cm1->getFieldMapping('id')['declared'], $cm2->getFieldMapping('id')['declared']);
self::assertSame($cm1->getAssociationMapping('ref')['declared'], $cm2->getAssociationMapping('ref')['declared']);
self::assertSame($cm1->getFieldMapping('id')->declared, $cm2->getFieldMapping('id')->declared);
self::assertSame($cm1->getAssociationMapping('ref')->declared, $cm2->getAssociationMapping('ref')->declared);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11149;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table('gh11149_eager_product')]
class EagerProduct
{
#[ORM\Id]
#[ORM\Column]
public int $id;
/** @var Collection<string, EagerProductTranslation> */
#[ORM\OneToMany(
targetEntity: EagerProductTranslation::class,
mappedBy: 'product',
fetch: 'EAGER',
indexBy: 'locale_code',
)]
public Collection $translations;
public function __construct(int $id)
{
$this->id = $id;
$this->translations = new ArrayCollection();
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11149;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table('gh11149_eager_product_translation')]
class EagerProductTranslation
{
#[ORM\Id]
#[ORM\Column]
private int $id;
#[ORM\ManyToOne(inversedBy: 'translations')]
#[ORM\JoinColumn(nullable: false)]
public EagerProduct $product;
#[ORM\ManyToOne]
#[ORM\JoinColumn(name: 'locale_code', referencedColumnName: 'code', nullable: false)]
public Locale $locale;
public function __construct(int $id, EagerProduct $product, Locale $locale)
{
$this->id = $id;
$this->product = $product;
$this->locale = $locale;
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11149;
use Doctrine\ORM\PersistentCollection;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH11149Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
Locale::class,
EagerProduct::class,
EagerProductTranslation::class,
]);
}
public function testFetchEagerModeWithIndexBy(): void
{
// Load entities into database
$this->_em->persist($product = new EagerProduct(11149));
$this->_em->persist($locale = new Locale('fr_FR'));
$this->_em->persist(new EagerProductTranslation(11149, $product, $locale));
$this->_em->flush();
$this->_em->clear();
// Fetch entity from database
$product = $this->_em->find(EagerProduct::class, 11149);
// Assert associated entity is loaded eagerly
static::assertInstanceOf(EagerProduct::class, $product);
static::assertInstanceOf(PersistentCollection::class, $product->translations);
static::assertTrue($product->translations->isInitialized());
static::assertCount(1, $product->translations);
// Assert associated entity is indexed by given property
$translation = $product->translations->get('fr_FR');
static::assertInstanceOf(EagerProductTranslation::class, $translation);
static::assertNotInstanceOf(Proxy::class, $translation);
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11149;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table('gh11149_locale')]
class Locale
{
#[ORM\Id]
#[ORM\Column(length: 5)]
public string $code;
public function __construct(string $code)
{
$this->code = $code;
}
}

View File

@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\PersistentCollection;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH11163Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
GH11163Bucket::class,
GH11163BucketItem::class,
]);
}
public function tearDown(): void
{
parent::tearDown();
$conn = static::$sharedConn;
$conn->executeStatement('DELETE FROM GH11163BucketItem');
$conn->executeStatement('DELETE FROM GH11163Bucket');
}
public function testFetchEagerModeWithOrderBy(): void
{
// Load entities into database
$this->_em->persist($bucket = new GH11163Bucket(11163));
$this->_em->persist(new GH11163BucketItem(1, $bucket, 2));
$this->_em->persist(new GH11163BucketItem(2, $bucket, 3));
$this->_em->persist(new GH11163BucketItem(3, $bucket, 1));
$this->_em->flush();
$this->_em->clear();
// Fetch entity from database
$dql = 'SELECT bucket FROM ' . GH11163Bucket::class . ' bucket WHERE bucket.id = :id';
$bucket = $this->_em->createQuery($dql)
->setParameter('id', 11163)
->getSingleResult();
// Assert associated entity is loaded eagerly
static::assertInstanceOf(GH11163Bucket::class, $bucket);
static::assertInstanceOf(PersistentCollection::class, $bucket->items);
static::assertTrue($bucket->items->isInitialized());
static::assertCount(3, $bucket->items);
// Assert order of entities
static::assertSame(1, $bucket->items[0]->position);
static::assertSame(3, $bucket->items[0]->id);
static::assertSame(2, $bucket->items[1]->position);
static::assertSame(1, $bucket->items[1]->id);
static::assertSame(3, $bucket->items[2]->position);
static::assertSame(2, $bucket->items[2]->id);
}
}
#[ORM\Entity]
class GH11163Bucket
{
#[ORM\Id]
#[ORM\Column(type: 'integer')]
private int $id;
/** @var Collection<int, GH11163BucketItem> */
#[ORM\OneToMany(
targetEntity: GH11163BucketItem::class,
mappedBy: 'bucket',
fetch: 'EAGER',
)]
#[ORM\OrderBy(['position' => 'ASC'])]
public Collection $items;
public function __construct(int $id)
{
$this->id = $id;
$this->items = new ArrayCollection();
}
}
#[ORM\Entity]
class GH11163BucketItem
{
#[ORM\ManyToOne(targetEntity: GH11163Bucket::class, inversedBy: 'items')]
#[ORM\JoinColumn(nullable: false)]
private GH11163Bucket $bucket;
#[ORM\Id]
#[ORM\Column(type: 'integer')]
public int $id;
#[ORM\Column(type: 'integer')]
public int $position;
public function __construct(int $id, GH11163Bucket $bucket, int $position)
{
$this->id = $id;
$this->bucket = $bucket;
$this->position = $position;
}
}

View File

@@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
use Generator;
use PHPUnit\Framework\Attributes\DataProvider;
class GH11341Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
IntegerBaseClass::class,
IntegerFooEntity::class,
IntegerBarEntity::class,
StringAsIntBaseClass::class,
StringAsIntFooEntity::class,
StringAsIntBarEntity::class,
StringBaseClass::class,
StringFooEntity::class,
StringBarEntity::class,
]);
}
public static function dqlStatements(): Generator
{
yield ['SELECT e FROM ' . IntegerBaseClass::class . ' e', '/WHERE [a-z]0_.type IN \(1, 2\)$/'];
yield ['SELECT e FROM ' . IntegerFooEntity::class . ' e', '/WHERE [a-z]0_.type IN \(1\)$/'];
yield ['SELECT e FROM ' . IntegerBarEntity::class . ' e', '/WHERE [a-z]0_.type IN \(2\)$/'];
yield ['SELECT e FROM ' . StringAsIntBaseClass::class . ' e', '/WHERE [a-z]0_.type IN \(\'1\', \'2\'\)$/'];
yield ['SELECT e FROM ' . StringAsIntFooEntity::class . ' e', '/WHERE [a-z]0_.type IN \(\'1\'\)$/'];
yield ['SELECT e FROM ' . StringAsIntBarEntity::class . ' e', '/WHERE [a-z]0_.type IN \(\'2\'\)$/'];
yield ['SELECT e FROM ' . StringBaseClass::class . ' e', '/WHERE [a-z]0_.type IN \(\'1\', \'2\'\)$/'];
yield ['SELECT e FROM ' . StringFooEntity::class . ' e', '/WHERE [a-z]0_.type IN \(\'1\'\)$/'];
yield ['SELECT e FROM ' . StringBarEntity::class . ' e', '/WHERE [a-z]0_.type IN \(\'2\'\)$/'];
}
#[DataProvider('dqlStatements')]
public function testDiscriminatorValue(string $dql, string $expectedDiscriminatorValues): void
{
$query = $this->_em->createQuery($dql);
$sql = $query->getSQL();
self::assertMatchesRegularExpression($expectedDiscriminatorValues, $sql);
}
public static function dqlStatementsForInstanceOf(): Generator
{
yield [IntegerBaseClass::class, IntegerFooEntity::class];
yield [StringBaseClass::class, StringFooEntity::class];
yield [StringAsIntBaseClass::class, StringAsIntFooEntity::class];
}
/**
* @psalm-param class-string $baseClass
* @psalm-param class-string $inheritedClass
*/
#[DataProvider('dqlStatementsForInstanceOf')]
public function testInstanceOf(string $baseClass, string $inheritedClass): void
{
$this->_em->persist(new $inheritedClass());
$this->_em->flush();
$dql = 'SELECT p FROM ' . $baseClass . ' p WHERE p INSTANCE OF ' . $baseClass;
$query = $this->_em->createQuery($dql);
$result = $query->getResult();
self::assertCount(1, $result);
self::assertContainsOnlyInstancesOf($baseClass, $result);
}
}
#[ORM\Entity]
#[ORM\Table(name: 'integer_discriminator')]
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\DiscriminatorColumn(name: 'type', type: 'integer')]
#[ORM\DiscriminatorMap([
1 => IntegerFooEntity::class,
2 => IntegerBarEntity::class,
])]
class IntegerBaseClass
{
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[ORM\Column(type: 'integer')]
private int|null $id = null;
}
#[ORM\Entity]
class IntegerFooEntity extends IntegerBaseClass
{
}
#[ORM\Entity]
class IntegerBarEntity extends IntegerBaseClass
{
}
#[ORM\Entity]
#[ORM\Table(name: 'string_as_int_discriminator')]
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\DiscriminatorColumn(name: 'type', type: 'string')]
#[ORM\DiscriminatorMap([
1 => StringAsIntFooEntity::class,
2 => StringAsIntBarEntity::class,
])]
class StringAsIntBaseClass
{
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[ORM\Column(type: 'integer')]
private int|null $id = null;
}
#[ORM\Entity]
class StringAsIntFooEntity extends StringAsIntBaseClass
{
}
#[ORM\Entity]
class StringAsIntBarEntity extends StringAsIntBaseClass
{
}
#[ORM\Entity]
#[ORM\Table(name: 'string_discriminator')]
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\DiscriminatorColumn(name: 'type', type: 'string')]
#[ORM\DiscriminatorMap([
'1' => StringFooEntity::class,
'2' => StringBarEntity::class,
])]
class StringBaseClass
{
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[ORM\Column(type: 'integer')]
private int|null $id = null;
}
#[ORM\Entity]
class StringFooEntity extends StringBaseClass
{
}
#[ORM\Entity]
class StringBarEntity extends StringBaseClass
{
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11386;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToOne;
#[Entity]
class GH11386EntityCart
{
#[Id]
#[GeneratedValue]
#[Column]
private int|null $id = null;
#[Column]
private int|null $amount = null;
#[OneToOne(inversedBy: 'cart', cascade: ['persist', 'remove'], orphanRemoval: true)]
private GH11386EntityCustomer|null $customer = null;
public function getId(): int|null
{
return $this->id;
}
public function getAmount(): int|null
{
return $this->amount;
}
public function setAmount(int $amount): static
{
$this->amount = $amount;
return $this;
}
public function getCustomer(): GH11386EntityCustomer|null
{
return $this->customer;
}
public function setCustomer(GH11386EntityCustomer|null $customer): self
{
$this->customer = $customer;
return $this;
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11386;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToOne;
#[Entity]
class GH11386EntityCustomer
{
#[Id]
#[GeneratedValue]
#[Column]
private int|null $id = null;
#[Column]
private string|null $name = null;
#[Column(type: 'smallint', nullable: true, enumType: GH11386EnumType::class, options: ['unsigned' => true])]
private GH11386EnumType|null $type = null;
#[OneToOne(mappedBy: 'customer')]
private GH11386EntityCart|null $cart = null;
public function getId(): int|null
{
return $this->id;
}
public function getName(): string|null
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getType(): GH11386EnumType|null
{
return $this->type;
}
public function setType(GH11386EnumType $type): static
{
$this->type = $type;
return $this;
}
public function getCart(): GH11386EntityCart|null
{
return $this->cart;
}
public function setCart(GH11386EntityCart|null $cart): self
{
// unset the owning side of the relation if necessary
if ($cart === null && $this->cart !== null) {
$this->cart->setCustomer(null);
}
// set the owning side of the relation if necessary
if ($cart !== null && $cart->getCustomer() !== $this) {
$cart->setCustomer($this);
}
$this->cart = $cart;
return $this;
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11386;
enum GH11386EnumType: int
{
case MALE = 1;
case FEMALE = 2;
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11386;
use Doctrine\Tests\OrmFunctionalTestCase;
final class GH11386Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
GH11386EntityCart::class,
GH11386EntityCustomer::class,
);
}
public function testInitializeClonedProxy(): void
{
$cart = new GH11386EntityCart();
$cart->setAmount(1000);
$customer = new GH11386EntityCustomer();
$customer->setName('John Doe')
->setType(GH11386EnumType::MALE)
->setCart($cart);
$this->_em->persist($cart);
$this->_em->flush();
$this->_em->clear();
$cart = $this->_em->find(GH11386EntityCart::class, 1);
$customer = clone $cart->getCustomer();
self::assertEquals('John Doe', $customer->getName());
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\UnitOfWork;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH6123Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
GH6123Entity::class,
);
}
public function testLoadingRemovedEntityFromDatabaseDoesNotCreateNewManagedEntityInstance(): void
{
$entity = new GH6123Entity();
$this->_em->persist($entity);
$this->_em->flush();
self::assertSame(UnitOfWork::STATE_MANAGED, $this->_em->getUnitOfWork()->getEntityState($entity));
self::assertFalse($this->_em->getUnitOfWork()->isScheduledForDelete($entity));
$this->_em->remove($entity);
$freshEntity = $this->loadEntityFromDatabase($entity->id);
self::assertSame($entity, $freshEntity);
self::assertSame(UnitOfWork::STATE_REMOVED, $this->_em->getUnitOfWork()->getEntityState($freshEntity));
self::assertTrue($this->_em->getUnitOfWork()->isScheduledForDelete($freshEntity));
}
public function testRemovedEntityCanBePersistedAgain(): void
{
$entity = new GH6123Entity();
$this->_em->persist($entity);
$this->_em->flush();
$this->_em->remove($entity);
self::assertSame(UnitOfWork::STATE_REMOVED, $this->_em->getUnitOfWork()->getEntityState($entity));
self::assertTrue($this->_em->getUnitOfWork()->isScheduledForDelete($entity));
$this->loadEntityFromDatabase($entity->id);
$this->_em->persist($entity);
self::assertSame(UnitOfWork::STATE_MANAGED, $this->_em->getUnitOfWork()->getEntityState($entity));
self::assertFalse($this->_em->getUnitOfWork()->isScheduledForDelete($entity));
$this->_em->flush();
}
private function loadEntityFromDatabase(int $id): GH6123Entity|null
{
return $this->_em->createQueryBuilder()
->select('e')
->from(GH6123Entity::class, 'e')
->where('e.id = :id')
->setParameter('id', $id)
->getQuery()
->getOneOrNullResult();
}
}
#[ORM\Entity]
class GH6123Entity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
public int $id;
}

View File

@@ -6,6 +6,8 @@ namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\Common\Collections\Selectable;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
@@ -17,6 +19,7 @@ use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;
use function assert;
use function class_exists;
#[Group('GH7767')]
class GH7767Test extends OrmFunctionalTestCase
@@ -54,7 +57,9 @@ class GH7767Test extends OrmFunctionalTestCase
$parent = $this->_em->find(GH7767ParentEntity::class, 1);
assert($parent instanceof GH7767ParentEntity);
$children = $parent->getChildren()->matching(Criteria::create()->orderBy(['position' => 'DESC']));
$children = $parent->getChildren()->matching(
Criteria::create()->orderBy(['position' => class_exists(Order::class) ? Order::Descending : 'DESC']),
);
self::assertEquals(300, $children[0]->position);
self::assertEquals(200, $children[1]->position);
@@ -70,7 +75,7 @@ class GH7767ParentEntity
#[GeneratedValue]
private int $id;
/** @psalm-var Collection<int, GH7767ChildEntity> */
/** @psalm-var Collection<int, GH7767ChildEntity>&Selectable<int, GH7767ChildEntity> */
#[OneToMany(targetEntity: GH7767ChildEntity::class, mappedBy: 'parent', fetch: 'EXTRA_LAZY', cascade: ['persist'])]
#[OrderBy(['position' => 'ASC'])]
private $children;
@@ -80,7 +85,7 @@ class GH7767ParentEntity
$this->children[] = new GH7767ChildEntity($this, $position);
}
/** @psalm-return Collection<int, GH7767ChildEntity> */
/** @psalm-return Collection<int, GH7767ChildEntity>&Selectable<int, GH7767ChildEntity> */
public function getChildren(): Collection
{
return $this->children;

View File

@@ -6,6 +6,8 @@ namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\Common\Collections\Selectable;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
@@ -17,6 +19,7 @@ use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;
use function assert;
use function class_exists;
#[Group('GH7836')]
class GH7836Test extends OrmFunctionalTestCase
@@ -57,7 +60,13 @@ class GH7836Test extends OrmFunctionalTestCase
$parent = $this->_em->find(GH7836ParentEntity::class, 1);
assert($parent instanceof GH7836ParentEntity);
$children = $parent->getChildren()->matching(Criteria::create()->orderBy(['position' => 'DESC', 'name' => 'ASC']));
$children = $parent->getChildren()->matching(
Criteria::create()->orderBy(
class_exists(Order::class)
? ['position' => Order::Descending, 'name' => Order::Ascending]
: ['position' => 'DESC', 'name' => 'ASC'],
),
);
self::assertSame(200, $children[0]->position);
self::assertSame('baz', $children[0]->name);
@@ -72,7 +81,13 @@ class GH7836Test extends OrmFunctionalTestCase
$parent = $this->_em->find(GH7836ParentEntity::class, 1);
assert($parent instanceof GH7836ParentEntity);
$children = $parent->getChildren()->matching(Criteria::create()->orderBy(['name' => 'ASC', 'position' => 'ASC']));
$children = $parent->getChildren()->matching(
Criteria::create()->orderBy(
class_exists(Order::class)
? ['name' => Order::Ascending, 'position' => Order::Ascending]
: ['name' => 'ASC', 'position' => 'ASC'],
),
);
self::assertSame(100, $children[0]->position);
self::assertSame('bar', $children[0]->name);
@@ -91,7 +106,7 @@ class GH7836ParentEntity
#[GeneratedValue]
private int $id;
/** @var Collection<int, GH7836ChildEntity> */
/** @var Collection<int, GH7836ChildEntity>&Selectable<int, GH7836ChildEntity> */
#[OneToMany(targetEntity: GH7836ChildEntity::class, mappedBy: 'parent', fetch: 'EXTRA_LAZY', cascade: ['persist'])]
#[OrderBy(['position' => 'ASC', 'name' => 'ASC'])]
private $children;
@@ -101,7 +116,7 @@ class GH7836ParentEntity
$this->children[] = new GH7836ChildEntity($this, $position, $name);
}
/** @psalm-return Collection<int, GH7836ChildEntity> */
/** @psalm-return Collection<int, GH7836ChildEntity>&Selectable<int, GH7836ChildEntity> */
public function getChildren(): Collection
{
return $this->children;

View File

@@ -13,6 +13,7 @@ use Doctrine\ORM\Events;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Tests\Models\Hydration\SimpleEntity;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
@@ -149,4 +150,33 @@ class AbstractHydratorTest extends OrmFunctionalTestCase
$this->expectException(ORMException::class);
$this->hydrator->hydrateAll($this->mockResult, $this->mockResultMapping);
}
public function testToIterableIfYieldAndBreakBeforeFinishAlwaysCleansUp(): void
{
$this->setUpEntitySchema([SimpleEntity::class]);
$entity1 = new SimpleEntity();
$this->_em->persist($entity1);
$entity2 = new SimpleEntity();
$this->_em->persist($entity2);
$this->_em->flush();
$this->_em->clear();
$evm = $this->_em->getEventManager();
$q = $this->_em->createQuery('SELECT e.id FROM ' . SimpleEntity::class . ' e');
// select two entities, but do no iterate
$q->toIterable();
self::assertCount(0, $evm->getListeners(Events::onClear));
// select two entities, but abort after first record
foreach ($q->toIterable() as $result) {
self::assertCount(1, $evm->getListeners(Events::onClear));
break;
}
self::assertCount(0, $evm->getListeners(Events::onClear));
}
}

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