Compare commits

...

95 Commits
3.5.2 ... 3.5.3

Author SHA1 Message Date
Grégoire Paris
1220edf953 Merge pull request #12241 from greg0ire/3.5.x
Merge 2.20.x up into 3.5.x
2025-10-27 23:06:52 +01:00
Grégoire Paris
7e4693d629 Merge remote-tracking branch 'origin/2.20.x' into 3.5.x 2025-10-27 22:34:13 +01:00
Grégoire Paris
59938cae57 Merge pull request #12238 from mpdude/fix-collections-deprecation
Avoid triggering a deprecation notice in doctrine/collections
2025-10-27 22:19:59 +01:00
Grégoire Paris
298dc9bb6a Merge pull request #12183 from mpdude/paginator-confused-result-set-mapping-initialized
Paginator with output walker returns count 0 when the query has previously been executed
2025-10-27 21:54:38 +01:00
Matthias Pigulla
da67f323e0 Add PHPStan errors to persistence2 baseline file 2025-10-27 19:51:11 +01:00
Matthias Pigulla
63635cad0e Remove PHPStan error suppressions 2025-10-27 12:28:48 +01:00
Grégoire Paris
a0d401b688 Merge pull request #12239 from doctrine/dependabot/github_actions/2.20.x/actions/download-artifact-6
Bump actions/download-artifact from 5 to 6
2025-10-27 08:38:07 +01:00
Grégoire Paris
6acbadfbbe Merge pull request #12240 from doctrine/dependabot/github_actions/2.20.x/actions/upload-artifact-5
Bump actions/upload-artifact from 4 to 5
2025-10-27 08:37:52 +01:00
dependabot[bot]
c64dcb4d38 Bump actions/upload-artifact from 4 to 5
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 06:41:11 +00:00
dependabot[bot]
3304290b21 Bump actions/download-artifact from 5 to 6
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 06:22:30 +00:00
Matthias Pigulla
1865717721 Avoid triggering a deprecation notice in doctrine/collections
This updates the code to avoid triggering the deprecation introduced in https://github.com/doctrine/collections/pull/472.
2025-10-26 23:20:27 +01:00
Matthias Pigulla
828b06e20f Update the DQL walker cookbook example 2025-10-26 22:13:51 +01:00
Matthias Pigulla
c2b844d2e3 Update src/Tools/Pagination/Paginator.php
Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>
2025-10-26 22:08:56 +01:00
Grégoire Paris
4f36f0129a Merge pull request #12236 from eltharin/bugfix_fieldalias_entityInDto
bugfix - add fieldAlias for entities in dto
2025-10-23 08:51:42 +02:00
dependabot[bot]
b45d5329f8 Bump doctrine/.github from 10.1.0 to 12.0.0 (#12227) 2025-10-23 08:10:27 +02:00
Grégoire Paris
c1047b30e3 Merge pull request #12216 from paulinevos/docs-builder
Use docs builder in ORM repo
2025-10-23 07:48:31 +02:00
eltharin
f71aa73ef1 bugfix - add fieldAlias for entities in dto 2025-10-22 21:14:42 +02:00
Grégoire Paris
aa62efa30a Adapt to latest coding standard 2025-10-22 21:12:17 +02:00
Pauline Vos
f71956f001 Use docs-builder to generate ORM docs
Introduces the `composer docs` command to generate the docs locally, and
uses the same tool (`docs-builder`) in the documentation GH workflow.
2025-10-22 21:09:03 +02:00
Ali Sol
96f9b29573 Merge pull request #12233 from alisolphp/fix-docs-typos-2-20
Docs: fix typos and grammar across reference docs
2025-10-21 17:09:12 +02:00
Grégoire Paris
c6207b1793 Merge pull request #12202 from greg0ire/missing-suffix
Add missing "Test" suffix
2025-10-20 08:01:47 +02:00
Grégoire Paris
8c92903430 Specify the length of VARCHAR columns
Platforms in the MySQL/MariaDB family require that.
2025-10-18 14:26:12 +02:00
Grégoire Paris
8616a98023 Add missing "Test" suffix
That test never got executed.
2025-10-18 14:16:07 +02:00
Grégoire Paris
9a55cf4f30 Merge pull request #12190 from mpdude/criteria-matching-custom-type-retry
Fix collection filtering API for `IN`/`NOT IN` comparisons that require type conversions
2025-10-16 07:56:07 +02:00
Matthias Pigulla
9d680a6de4 Do not eagerly set metadata from ResolveTargetEntityListener (#12174)
* Do not eagerly set metadata from ResolveTargetEntityListener

When using the `ResolveTargetEntityListener` to substitute `targetEntities` in association mappings, do not eagerly put the resolved (target) entity into the class metadata cache under the class name of the original entity.

#### Motivation

I have a library that wants to distribute a MappedSuperclass as the base for some functionality. It will be necessary that clients using the library will extend the MappedSuperclass to fill in some blanks, creating the first real `#[Entity]` instance of it.

This client-provided entity will be the primary means of working with the class. Thus, I was following the [note in the documentation](https://www.doctrine-project.org/projects/doctrine-orm/en/3.5/reference/inheritance-mapping.html#mapped-superclasses:~:text=It%20is%2C%20however) and using the `ResolveTargetEntityListener` to declare that whenever an association refers to that mapped superclass, the particular entity class shall be used instead.

>  One-To-Many associations are not generally possible on a mapped superclass, since they require the "many" side to hold the foreign key.
> It is, however, possible to use the [ResolveTargetEntityListener](https://www.doctrine-project.org/projects/doctrine-orm/en/3.5/cookbook/resolve-target-entity-listener.html) to replace references to a mapped superclass with an entity class at runtime. As long as there is only one entity subclass inheriting from the mapped superclass and all references to the mapped superclass are resolved to that entity class at runtime, the mapped superclass can use One-To-Many associations and be named as the targetEntity on the owning sides.

#### Changes made

The `ResolveTargetEntityListener` primarily does what its name suggests: For newly loaded class metadata, it inspects all associations declared and replaces the `targetEntity` with new (resolved) values.

But additionally, when a loaded class is the target of such a resolution, it would also put the class metadata into the cache under the name of the original entity.

I think that extra step is wrong, and this PR removes it. It had the side effect that when other classes extending the MappedSuperclass were loaded _after_ the resolve target class has been seen for the first time, the metadata for those classes would not inherit from the mapped superclass anymore, but from the target entity class instead. In my real-life use case, this causes weird mapping errors down the road; as of ^3.0, it would throw a mapping exception asking to configure inheritance mapping. But note that there would be no inheritance between the two entity classes at all.

#### More background

The documentation [describes the use of `ResolveTargetEntityListener`](https://www.doctrine-project.org/projects/doctrine-orm/en/3.5/cookbook/resolve-target-entity-listener.html) with an interface that is resolved to an entity class. For an interface, adding the extra metadata does not make a difference, since it never interferes with actual entity or mapped superclasses.

The initial idea of adding a copy of the entity class metadata under the interface name came from commit
9c7f3f2747 in #385. The goal was to make it possible to also find entities by interface names, like so:

```
$em->find('Foo\BarBundle\Entity\PersonInterface', 1);
```

It then [turned out that this only worked when the resolution had already been applied](https://github.com/doctrine/orm/pull/385#issuecomment-6658893). So, the new `onClassMetadataNotFound` event was added and the resolution map would be checked in that case as well (#1181). The inital code stayed in place, possibly giving a small performance gain.

In my real-world use case and the test case I added in this PR, the associations are even self-referencing. That should not really be necessary for the problem to surface. I decided to keep it this way to show that the `targetEntity` need not be an interface after all, and that a MappedSuperclass can be used in the same way.
2025-10-15 16:10:51 +02:00
Grégoire Paris
7602a5341c Merge pull request #12224 from greg0ire/3.5.x
Merge 2.20.x up into 3.5.x
2025-10-13 23:12:23 +02:00
Matthias Pigulla
7a59281157 Fix collection filtering API for IN/NOT IN comparisons that require type conversions
This PR fixes the `Criteria` matching API for `IN` and `NIN` conditions with values that are arrays, by making sure that type information for the matched field is passed to the DBAL level correctly.

Passing the right parameter type to DBAL is important to make sure parameter conversions are applied before matching at the database level.

Memory-based collections (`ArrayCollection`s or initialized collection fields) would perform matching on the objects in memory where no type conversion to the database representation is required, giving correct results.

But uninitialized collections that have their conditions evaluated at the database level need to convert parameter values to the database representation before performing the comparison.

One extra challenge is that the DBAL type system does currently not support array-valued parameters for custom types. Only a [limited list of types](https://www.doctrine-project.org/projects/doctrine-dbal/en/4.2/reference/data-retrieval-and-manipulation.html#list-of-parameters-conversion) is supported.

I discussed this with @morozov at the Doctrine Hackathon and came to the conclusion that it would be best to work around this limitation at the ORM level. Thus, this fix recognizes array-valued parameters and creates multiple placeholders (like `?, ?, ?`) for them, flattening out the arrays in the parameter list and repeating the type information for each one of them.

Previous stalled attempt to fix this was in #11897.
2025-10-13 23:11:56 +02:00
Grégoire Paris
214b1ad739 Merge remote-tracking branch 'origin/2.20.x' into 3.5.x 2025-10-13 22:53:09 +02:00
Grégoire Paris
5def068fe9 Merge pull request #12223 from doctrine/dependabot/github_actions/2.20.x/doctrine/dot-github-10.1.0
Bump doctrine/.github from 8.0.0 to 10.1.0
2025-10-13 08:49:34 +02:00
dependabot[bot]
693acbf812 Bump doctrine/.github from 8.0.0 to 10.1.0
Bumps [doctrine/.github](https://github.com/doctrine/.github) from 8.0.0 to 10.1.0.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/8.0.0...10.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-13 06:11:35 +00:00
Grégoire Paris
cf11f1e453 Add ORDER BY clause to SELECT query (#12222)
The order of results is not guaranteed unless we do so, and the test can
fail in some cases:

	There was 1 failure:

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

	/home/runner/work/orm/orm/tests/Tests/ORM/Functional/QueryTest.php:481
2025-10-12 02:02:42 +02:00
Grégoire Paris
c1ce2bb687 Merge pull request #12220 from mbeccati/fix-mysql9-library
Escape library as a table name in tests (#12170)
2025-10-11 17:08:47 +02:00
Grégoire Paris
eae6577ce2 Merge pull request #12219 from greg0ire/rework-upgrade-md
Move introduction outside of paragraph about 3.x
2025-10-11 17:05:17 +02:00
Matteo Beccati
5f6896a2f9 Avoid using LIBRARY as table name (#12170)
It is a reserved word since MySQL 9.2:
https://dev.mysql.com/doc/refman/9.2/en/create-library.html
2025-10-11 11:56:29 +02:00
Grégoire Paris
e3106d439d Move introduction outside of paragraph about 3.x
It is not specific to 3.x and should stay forever.
2025-10-11 09:41:24 +02:00
Grégoire Paris
930a790a5a Merge pull request #12212 from mpdude/revert-11769
Revert "Fix fields of transient classes being considered duplicate with `reportFieldsWhereDeclared` (#11769)"
2025-10-10 08:17:09 +02:00
Alexander M. Turek
40aa44914f Ignore DBAL deprecation that we cannot fix (yet) (#12218) 2025-10-09 23:48:42 +02:00
Grégoire Paris
7c8a528914 Merge pull request #12211 from greg0ire/3.5.x
Merge 2.20.x up into 3.5.x
2025-10-09 22:07:50 +02:00
Christian Flothmann
3b7de17f2e add jobs using PHP 8.5 in the CI (#12180) 2025-10-09 16:39:28 +02:00
Grégoire Paris
40fedadecf Merge pull request #12217 from derrabus/bugfix/deprecated-get-name
Fix DBAL deprecation
2025-10-09 15:42:59 +02:00
Alexander M. Turek
0d97a44f28 Fix DBAL deprecation 2025-10-09 15:24:06 +02:00
Matthias Pigulla
8afaa63d73 Add a recommendation not to use multiple private fields of the same name in entity hierarchies 2025-10-09 11:11:51 +02:00
Matthias Pigulla
2ad720b304 Revert "Fix fields of transient classes being considered duplicate with reportFieldsWhereDeclared (#11769)"
This reverts commit 4feaa470af.
2025-10-09 10:23:55 +02:00
Grégoire Paris
64cd5cad20 Merge remote-tracking branch 'origin/2.20.x' into 3.5.x 2025-10-09 08:05:48 +02:00
Alexander M. Turek
d3e1440175 Handle quoted PK columns properly on DBAL 4 (#12208) 2025-10-08 18:25:12 +02:00
Benjamin Eberlei
a9bd00a70b Improve migration docs from ORM 2 to 3. (#12207)
* Improve migration docs from ORM 2 to 3.

* Improve wording

* Hint about following deprecations.
2025-10-08 16:37:16 +02:00
Benjamin Eberlei
a939dc2e0d [GH-9219] Add support for toIterable over mixed or scalar results. (#12187)
* [GH-9219] Add support for toIterable over mixed or scalar results.

* Housekeeping: phpcs

* Update test names
2025-10-08 15:07:52 +02:00
Christophe Coevoet
f8186b1203 Merge pull request #12200 from mpdude/fix-invalid-dql-test-case
Fix DQL JOIN syntax in two test cases
2025-10-08 11:08:24 +02:00
Alexander M. Turek
4f3a5c5514 Merge pull request #12201 from derrabus/bugfix/missing-import 2025-10-07 22:57:47 +02:00
Alexander M. Turek
6641989e35 Fix missing import 2025-10-07 18:02:20 +02:00
Matthias Pigulla
048e308241 Fix DQL JOIN syntax in two test cases 2025-10-07 17:54:33 +02:00
Alexander M. Turek
8ca72a4e96 Merge branch '2.20.x' into 3.5.x
* 2.20.x:
  Remove calls to getMockForAbstractClass() (#12003)
2025-10-07 17:25:14 +02:00
Alexander M. Turek
daf74b74b5 Merge commit 'c1af765960bf88cb5109f74a05d24c4df9aaf76a' into 3.5.x
* commit 'c1af765960bf88cb5109f74a05d24c4df9aaf76a':
  Upgrade to doctrine/coding-standard 14
  Bump doctrine/.github from 7.3.0 to 8.0.0
2025-10-07 16:49:06 +02:00
Alexander M. Turek
4274dac8a2 Remove calls to getMockForAbstractClass() (#12003) 2025-10-07 16:34:22 +02:00
Alexander M. Turek
c6db9feade Add a CI job that fails on deprecations (#12188) 2025-10-07 16:03:42 +02:00
Grégoire Paris
c1af765960 Merge pull request #12185 from doctrine/dependabot/github_actions/2.20.x/doctrine/dot-github-8.0.0
Bump doctrine/.github from 7.3.0 to 8.0.0
2025-10-07 15:40:44 +02:00
Alexander M. Turek
86d847edb8 Merge branch '2.20.x' into 3.5.x
* 2.20.x:
  use the empty string instead of null as an array offset (#12181)
  do not call setAccessible() on PHP >= 8.1 (#12182)
2025-10-07 15:23:19 +02:00
Christian Flothmann
5f3551852f use the empty string instead of null as an array offset (#12181) 2025-10-07 14:50:23 +02:00
Grégoire Paris
8144cad07c Upgrade to doctrine/coding-standard 14 2025-10-06 09:01:36 +02:00
dependabot[bot]
70fd68cf7f Bump doctrine/.github from 7.3.0 to 8.0.0
Bumps [doctrine/.github](https://github.com/doctrine/.github) from 7.3.0 to 8.0.0.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/7.3.0...8.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-06 06:14:08 +00:00
Matthias Pigulla
437259556c Fix CS 2025-10-01 19:48:04 +02:00
Matthias Pigulla
e7e2fef56c Fix creation of the count query, to that a new RSM is being created 2025-10-01 19:46:58 +02:00
Matthias Pigulla
3e34b8e86a Add a test to reproduce the issue 2025-10-01 19:35:18 +02:00
Christian Flothmann
7d950aba62 do not call setAccessible() on PHP >= 8.1 (#12182) 2025-10-01 16:17:00 +02:00
Massimiliano Arione
8ad560c34d Fix docs on final entities (#12176) 2025-09-26 00:28:08 +02:00
Grégoire Paris
ccfb620f31 Merge pull request #12175 from beberlei/Docs-RemoveDatabaseFirstChapter
Remove Database and Model First chapters that said little of value.
2025-09-21 22:00:51 +02:00
Benjamin Eberlei
94c4d48ae5 Remove Database and Model First chapters that said little of value. 2025-09-21 21:06:16 +02:00
Grégoire Paris
2ca63df90c Merge pull request #12161 from greg0ire/proper-attribute
Switch to IgnoreDeprecations
2025-09-12 08:23:02 +02:00
Grégoire Paris
0d4413c248 Switch to IgnoreDeprecations
Rather than disabling the error handler, this attribute available since
PHPUnit 10.5 allows to be more fine-grained and ignore only the deprecations.
2025-09-10 23:58:13 +02:00
Grégoire Paris
48434f4c53 Merge pull request #12150 from greg0ire/3.5.x
Merge 2.20.x up into 3.5.x
2025-08-28 07:45:06 +02:00
Grégoire Paris
200a505f36 Merge pull request #12148 from lucasmirloup/docs/generation-strategies-dbal-4
docs: generation strategies: differences between DBAL 3 and 4
2025-08-27 07:42:53 +02:00
Grégoire Paris
17d7814fdc Merge remote-tracking branch 'origin/2.20.x' into 3.5.x 2025-08-27 00:02:52 +02:00
Grégoire Paris
8fe1200edf Merge pull request #11895 from mpdude/fix-many-to-many-in-expression
Fix `IN`/`NOT IN` expression handling and support enums when matching on to-many-collections
2025-08-26 15:32:52 +02:00
Lucas Mirloup
f7d4e379bc docs: consistent PostgreSQL's name case 2025-08-26 11:37:07 +02:00
Lucas Mirloup
c164ae434f docs: generation strategies differences between DBAL 3 and 4 2025-08-26 11:03:48 +02:00
Grégoire Paris
21e9fcbfbb Merge pull request #12146 from greg0ire/upg-phpunit
PHPUnit 11
2025-08-24 08:30:10 +02:00
Gregoire PARIS
db456976ed Check extra condition to decide if a test was skipped
It seems that this could happen with PHPUnit 10, then tearDown() would
crash when calling `clear()` on null, but then PHPUnit 10 did not show
that exception.
2025-08-21 13:40:44 +02:00
Grégoire Paris
2d9091778f Use PHPUnit 11 when possible 2025-08-21 13:39:49 +02:00
Grégoire Paris
17059e5265 Migrate away from annotations in tests 2025-08-21 13:39:49 +02:00
Grégoire Paris
680a9ef632 Migrate away from assertStringNotMatchesFormat()
It has been deprecated.
2025-08-21 13:39:48 +02:00
Grégoire Paris
9d5f112c7e Migrate to willReturn()
self::returnValue() and self::onConsecutiveCalls() has been deprecated.
2025-08-21 13:39:48 +02:00
Grégoire Paris
b7423c96cf Migrate away from getMockForAbstractClass()
It has been deprecated.
2025-08-21 13:39:46 +02:00
Grégoire Paris
28735afae3 Merge pull request #12143 from greg0ire/3.5.x
Merge 2.20.x up into 3.5.x
2025-08-19 21:56:34 +02:00
Grégoire Paris
0f229fbb4b Merge remote-tracking branch 'origin/2.20.x' into 3.5.x 2025-08-19 21:44:21 +02:00
Alexander M. Turek
85c13edc80 Prefer non-deprecated AbstractAsset API (#12142)
Co-authored-by: Christian Flothmann <christian.flothmann@open.de>
2025-08-19 16:46:03 +02:00
Grégoire Paris
397358c308 Merge pull request #12133 from greg0ire/update-phpstan
PHPStan 2.1.22
2025-08-19 07:36:10 +02:00
Grégoire Paris
ac37a87a3d Merge pull request #12139 from doctrine/dependabot/github_actions/2.20.x/actions/checkout-5
Bump actions/checkout from 4 to 5
2025-08-18 17:31:47 +02:00
dependabot[bot]
613f52db5a Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-18 09:50:01 +00:00
Grégoire Paris
cf5503b0d8 PHPCS 3.13.2 (#12134) 2025-08-17 21:53:23 +02:00
Grégoire Paris
ae5e9c8c6c PHPStan 2.1.22 2025-08-17 19:24:33 +02:00
Grégoire Paris
1a2826d147 Merge pull request #12053 from alexislefebvre/chore-remove-run-all.sh
chore: remove run-all.sh
2025-08-11 14:14:20 +02:00
Grégoire Paris
05760f9454 Merge pull request #12127 from doctrine/dependabot/github_actions/2.20.x/actions/download-artifact-5
Bump actions/download-artifact from 4 to 5
2025-08-11 13:06:03 +02:00
dependabot[bot]
26af013842 Bump actions/download-artifact from 4 to 5
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-11 10:22:17 +00:00
Alexis Lefebvre
d68c1dcd6d chore: remove run-all.sh 2025-07-02 00:14:28 +02:00
Matthias Pigulla
9bf407f336 Fix IN/NOT IN expression handling and support enums when matching on to-many-collections
This fixes that using a `Criteria` with an `IN` or `NIN` expression on a to-many collection currently leads to an SQL error (#6173). The `ManyToMany` persister needs to know about the slightly different SQL syntax for `[NOT] IN ()`.

In the case of `[NOT] IN` expressions, the value will be an array, which also required me to change (I guess "fix") the parameter type handling. I have pulled the necessary code from the `BasicEntityPersister` and placed it as static helper methods in `PersisterHelper`.

This is somewhat inspired by #11516, which aims at fixing #11481: By re-using the parameter type handling code, it also fixes using backed enums in `EQ`, `IN` and `NIN` expressions within `Criteria` when `matching()` on one-to-many and many-to-many collections.
2025-03-30 22:44:12 +02:00
113 changed files with 1684 additions and 805 deletions

1
.gitattributes vendored
View File

@@ -11,7 +11,6 @@ build.properties.dev export-ignore
build.xml export-ignore
CONTRIBUTING.md export-ignore
phpunit.xml.dist export-ignore
run-all.sh export-ignore
phpcs.xml.dist export-ignore
phpbench.json export-ignore
phpstan.neon export-ignore

View File

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

View File

@@ -35,6 +35,7 @@ jobs:
- "8.2"
- "8.3"
- "8.4"
- "8.5"
dbal-version:
- "default"
- "3.7"
@@ -67,7 +68,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v4"
uses: "actions/checkout@v5"
with:
fetch-depth: 2
@@ -95,19 +96,69 @@ jobs:
ENABLE_SECOND_LEVEL_CACHE: 0
ENABLE_NATIVE_LAZY_OBJECTS: ${{ matrix.native_lazy }}
- name: "Run PHPUnit with Second Level Cache"
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --exclude-group performance,non-cacheable,locking_functional --coverage-clover=coverage-cache.xml"
- name: "Run PHPUnit with Second Level Cache and PHPUnit 10"
run: |
vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml \
--exclude-group=performance,non-cacheable,locking_functional \
--coverage-clover=coverage-cache.xml
if: "${{ matrix.php-version == '8.1' }}"
env:
ENABLE_SECOND_LEVEL_CACHE: 1
ENABLE_NATIVE_LAZY_OBJECTS: ${{ matrix.native_lazy }}
- name: "Run PHPUnit with Second Level Cache and PHPUnit 11+"
run: |
vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml \
--exclude-group=performance \
--exclude-group=non-cacheable \
--exclude-group=locking_functional \
--coverage-clover=coverage-cache.xml
if: "${{ matrix.php-version != '8.1' }}"
env:
ENABLE_SECOND_LEVEL_CACHE: 1
ENABLE_NATIVE_LAZY_OBJECTS: ${{ matrix.native_lazy }}
- name: "Upload coverage file"
uses: "actions/upload-artifact@v4"
uses: "actions/upload-artifact@v5"
with:
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-${{ matrix.native_lazy }}-coverage"
path: "coverage*.xml"
phpunit-deprecations:
name: "PHPUnit (fail on deprecations)"
runs-on: "ubuntu-24.04"
steps:
- name: "Checkout"
uses: "actions/checkout@v5"
with:
fetch-depth: 2
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
php-version: "8.5"
extensions: "apcu, pdo, sqlite3"
coverage: "pcov"
ini-values: "zend.assertions=1, apc.enable_cli=1"
- name: "Allow dev dependencies"
run: composer config minimum-stability dev
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v3"
with:
composer-options: "--ignore-platform-req=php+"
dependency-versions: "highest"
- name: "Run PHPUnit"
run: "vendor/bin/phpunit -c ci/github/phpunit/sqlite3.xml --fail-on-deprecation"
env:
ENABLE_SECOND_LEVEL_CACHE: 0
ENABLE_NATIVE_LAZY_OBJECTS: 1
phpunit-postgres:
name: "PHPUnit with PostgreSQL"
runs-on: "ubuntu-22.04"
@@ -119,6 +170,7 @@ jobs:
- "8.2"
- "8.3"
- "8.4"
- "8.5"
dbal-version:
- "default"
- "3.7"
@@ -151,7 +203,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v4"
uses: "actions/checkout@v5"
with:
fetch-depth: 2
@@ -176,7 +228,7 @@ jobs:
run: "vendor/bin/phpunit -c ci/github/phpunit/pdo_pgsql.xml --coverage-clover=coverage.xml"
- name: "Upload coverage file"
uses: "actions/upload-artifact@v4"
uses: "actions/upload-artifact@v5"
with:
name: "${{ github.job }}-${{ matrix.postgres-version }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.extension }}-coverage"
path: "coverage.xml"
@@ -193,6 +245,7 @@ jobs:
- "8.2"
- "8.3"
- "8.4"
- "8.5"
dbal-version:
- "default"
- "3.7"
@@ -218,7 +271,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v4"
uses: "actions/checkout@v5"
with:
fetch-depth: 2
@@ -243,7 +296,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@v4"
uses: "actions/upload-artifact@v5"
with:
name: "${{ github.job }}-${{ matrix.mariadb-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
path: "coverage.xml"
@@ -260,6 +313,7 @@ jobs:
- "8.2"
- "8.3"
- "8.4"
- "8.5"
dbal-version:
- "default"
- "3.7"
@@ -293,7 +347,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v4"
uses: "actions/checkout@v5"
with:
fetch-depth: 2
@@ -319,13 +373,27 @@ jobs:
env:
ENABLE_SECOND_LEVEL_CACHE: 0
- name: "Run PHPUnit with Second Level Cache"
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --exclude-group performance,non-cacheable,locking_functional --coverage-clover=coverage-no-cache.xml"
- name: "Run PHPUnit with Second Level Cache and PHPUnit 10"
run: |
vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml \
--exclude-group=performance,non-cacheable,locking_functional \
--coverage-clover=coverage-no-cache.xml"
if: "${{ matrix.php-version == '8.1' }}"
env:
ENABLE_SECOND_LEVEL_CACHE: 1
- name: "Run PHPUnit with Second Level Cache and PHPUnit 11+"
run: |
vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml \
--exclude-group=performance \
--exclude-group=non-cacheable \
--exclude-group=locking_functional \
--coverage-clover=coverage-no-cache.xml
if: "${{ matrix.php-version != '8.1' }}"
env:
ENABLE_SECOND_LEVEL_CACHE: 1
- name: "Upload coverage files"
uses: "actions/upload-artifact@v4"
uses: "actions/upload-artifact@v5"
with:
name: "${{ github.job }}-${{ matrix.mysql-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
path: "coverage*.xml"
@@ -343,12 +411,12 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v4"
uses: "actions/checkout@v5"
with:
fetch-depth: 2
- name: "Download coverage files"
uses: "actions/download-artifact@v4"
uses: "actions/download-artifact@v6"
with:
path: "reports"

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,36 @@
Note about upgrading: Doctrine uses static and runtime mechanisms to raise
awareness about deprecated code.
- Use of `@deprecated` docblock that is detected by IDEs (like PHPStorm) or
Static Analysis tools (like Psalm, phpstan)
- Use of our low-overhead runtime deprecation API, details:
https://github.com/doctrine/deprecations/
# Upgrade to 3.x General Notes
We recommend you upgrade to DBAL 3 first before upgrading to ORM 3. See
the DBAL upgrade docs: https://github.com/doctrine/dbal/blob/3.10.x/UPGRADE.md
Rather than doing several major upgrades at once, we recommend you do the following:
- upgrade to DBAL 3
- deploy and monitor
- upgrade to ORM 3
- deploy and monitor
- upgrade to DBAL 4
- deploy and monitor
If you are using Symfony, the recommended minimal Doctrine Bundle version is 2.15
to run with ORM 3.
At this point, we recommend upgrading to PHP 8.4 first and then directly from
ORM 2.19 to 3.5 and up so that you can skip the lazy ghost proxy generation
and directly start using native lazy objects.
# Upgrade to 3.5
See the General notes to upgrading to 3.x versions above.
## Deprecate not using native lazy objects on PHP 8.4+
Having native lazy objects disabled on PHP 8.4+ is deprecated and will not be
@@ -53,6 +84,8 @@ decide to remove it before it is used too widely.
# Upgrade to 3.4
See the General notes to upgrading to 3.x versions above.
## Discriminator Map class duplicates
Using the same class several times in a discriminator map is deprecated.
@@ -73,6 +106,8 @@ Companion accessor methods are deprecated as well.
# Upgrade to 3.3
See the General notes to upgrading to 3.x versions above.
## Deprecate `DatabaseDriver`
The class `Doctrine\ORM\Mapping\Driver\DatabaseDriver` is deprecated without replacement.
@@ -89,6 +124,8 @@ method. Details can be found at https://github.com/doctrine/orm/pull/11188.
# Upgrade to 3.2
See the General notes to upgrading to 3.x versions above.
## Deprecate the `NotSupported` exception
The class `Doctrine\ORM\Exception\NotSupported` is deprecated without replacement.
@@ -115,6 +152,8 @@ using the `\Doctrine\ORM\Mapping\UniqueConstraint` and `\Doctrine\ORM\Mapping\In
# Upgrade to 3.1
See the General notes to upgrading to 3.x versions above.
## Deprecate `Doctrine\ORM\Mapping\ReflectionEnumProperty`
This class is deprecated and will be removed in 4.0.
@@ -138,6 +177,8 @@ Using array access on instances of the following classes is deprecated:
# Upgrade to 3.0
See the General notes to upgrading to 3.x versions above.
## BC BREAK: Calling `ClassMetadata::getAssociationMappedByTargetField()` with the owning side of an association now throws an exception
Previously, calling
@@ -163,6 +204,9 @@ so `$targetEntity` is a first argument now. This change affects only non-named a
When using the `AUTO` strategy to let Doctrine determine the identity generation mechanism for
an entity, and when using `doctrine/dbal` 4, PostgreSQL now uses `IDENTITY`
instead of `SEQUENCE` or `SERIAL`.
There are three ways to handle this change.
* If you want to upgrade your existing tables to identity columns, you will need to follow [migration to identity columns on PostgreSQL](https://www.doctrine-project.org/projects/doctrine-dbal/en/4.0/how-to/postgresql-identity-migration.html)
* If you want to keep using SQL sequences, you need to configure the ORM this way:
```php
@@ -175,6 +219,27 @@ $configuration->setIdentityGenerationPreferences([
PostgreSQLPlatform::CLASS => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
]);
```
* You can change individual entities to use the `SEQUENCE` strategy instead of `AUTO`:
```php
diff --git a/src/Entity/Example.php b/src/Entity/Example.php
index 28be8df378..3b7d61bda6 100644
--- a/src/Entity/Example.php
+++ b/src/Entity/Example.php
@@ -38,7 +38,7 @@ class Example
#[ORM\Id]
#[ORM\Column(type: 'integer')]
- #[ORM\GeneratedValue(strategy: 'AUTO')]
+ #[ORM\GeneratedValue(strategy: 'SEQUENCE')]
private int $id;
#[Assert\Length(max: 255)]
```
The later two options require a small database migration that will remove the default
expression fetching the next value from the sequence. It's not strictly necessary to
do this migration because the code will work anyway. A benefit of this approach is
that you can just make and roll out the code changes first and then migrate the database later.
## BC BREAK: Throw exceptions when using illegal attributes on Embeddable

View File

@@ -12,6 +12,9 @@
{"name": "Jonathan Wage", "email": "jonwage@gmail.com"},
{"name": "Marco Pivetta", "email": "ocramius@gmail.com"}
],
"scripts": {
"docs": "composer --working-dir docs update && ./docs/vendor/bin/build-docs.sh @additional_args"
},
"config": {
"allow-plugins": {
"composer/package-versions-deprecated": true,
@@ -37,15 +40,14 @@
"symfony/var-exporter": "^6.3.9 || ^7.0"
},
"require-dev": {
"doctrine/coding-standard": "^13.0",
"doctrine/coding-standard": "^14.0",
"phpbench/phpbench": "^1.0",
"phpdocumentor/guides-cli": "^1.4",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "2.0.3",
"phpstan/phpstan": "2.1.22",
"phpstan/phpstan-deprecation-rules": "^2",
"phpunit/phpunit": "^10.4.0",
"phpunit/phpunit": "^10.5.0 || ^11.5",
"psr/log": "^1 || ^2 || ^3",
"squizlabs/php_codesniffer": "3.12.0",
"symfony/cache": "^5.4 || ^6.2 || ^7.0"
},
"suggest": {

2
docs/.gitignore vendored
View File

@@ -1,3 +1,3 @@
composer.lock
vendor/
build/
output/

View File

@@ -1,24 +0,0 @@
# Makefile for Doctrine ORM documentation
#
# You can set these variables from the command line.
DOCOPTS =
BUILD = vendor/bin/guides
BUILDDIR = build
# Internal variables.
ALLGUIDESOPTS = $(DOCOPTS) en/
.PHONY: help clean html
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
clean:
-rm -rf ./$(BUILDDIR)/*
html:
$(BUILD) $(ALLGUIDESOPTS) --output=$(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."

View File

@@ -4,19 +4,15 @@ The documentation is written in [ReStructured Text](https://docutils.sourceforge
## How to Generate:
In the `docs/` folder, run
In the project root, run
composer update
composer docs
Then compile the documentation with:
make html
This will generate the documentation into the `build` subdirectory.
This will generate the documentation into the `docs/output` subdirectory.
To browse the documentation, you need to run a webserver:
cd build/html
cd docs/output
php -S localhost:8000
Now the documentation is available at [http://localhost:8000](http://localhost:8000).

View File

@@ -3,8 +3,7 @@
"description": "Documentation for the Object-Relational Mapper\"",
"type": "library",
"license": "MIT",
"require": {
"phpdocumentor/guides-cli": "1.7.1",
"phpdocumentor/filesystem": "1.7.1"
"require-dev": {
"doctrine/docs-builder": "^1.0"
}
}

View File

@@ -101,8 +101,16 @@ The ``Paginate::count(Query $query)`` looks like:
{
static public function count(Query $query)
{
/** @var Query $countQuery */
$countQuery = clone $query;
/*
To avoid changing the $query passed into the method and to make sure a possibly existing
ResultSetMapping is discarded, we create a new query object any copy relevant data over.
*/
$countQuery = new Query($query->getEntityManager());
$countQuery->setDQL($query->getDQL());
$countQuery->setParameters(clone $query->getParameters());
foreach ($query->getHints() as $name => $value) {
$countQuery->setHint($name, $value);
}
$countQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('DoctrineExtensions\Paginate\CountSqlWalker'));
$countQuery->setFirstResult(null)->setMaxResults(null);
@@ -111,7 +119,7 @@ The ``Paginate::count(Query $query)`` looks like:
}
}
It clones the query, resets the limit clause first and max results
This resets the limit clause first and max results
and registers the ``CountSqlWalker`` custom tree walker which
will modify the AST to execute a count query. The walkers
implementation is:

View File

@@ -76,6 +76,8 @@ Configuration Options
The following sections describe all the configuration options
available on a ``Doctrine\ORM\Configuration`` instance.
.. _reference-native-lazy-objects:
Native Lazy Objects (**OPTIONAL**)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -302,7 +304,7 @@ requests.
Connection
----------
The ``$connection`` passed as the first argument to he constructor of
The ``$connection`` passed as the first argument to the constructor of
``EntityManager`` has to be an instance of ``Doctrine\DBAL\Connection``.
You can use the factory ``Doctrine\DBAL\DriverManager::getConnection()``
to create such a connection. The DBAL configuration is explained in the

View File

@@ -79,8 +79,9 @@ Entities
An entity is a lightweight, persistent domain object. An entity can
be any regular PHP class observing the following restrictions:
- An entity class must not be final nor read-only but
it may contain final methods or read-only properties.
- An entity class can be final or read-only when
you use :ref:`native lazy objects <reference-native-lazy-objects>`.
It may contain final methods or read-only properties too.
- Any two entity classes in a class hierarchy that inherit
directly or indirectly from one another must not have a mapped
property with the same name. That is, if B inherits from A then B
@@ -167,7 +168,7 @@ recommended, at least not as long as an entity instance still holds
references to proxy objects or is still managed by an EntityManager.
By default, serializing proxy objects does not initialize them. On
unserialization, resulting objects are detached from the entity
manager and cannot be initialiazed anymore. You can implement the
manager and cannot be initialized anymore. You can implement the
``__serialize()`` method if you want to change that behavior, but
then you need to ensure that you won't generate large serialized
object graphs and take care of circular associations.

View File

@@ -389,17 +389,19 @@ Here is the list of possible generation strategies:
- ``AUTO`` (default): Tells Doctrine to pick the strategy that is
preferred by the used database platform. The preferred strategies
are ``IDENTITY`` for MySQL, SQLite, MsSQL and SQL Anywhere and, for
historical reasons, ``SEQUENCE`` for Oracle and PostgreSQL. This
strategy provides full portability.
are ``IDENTITY`` for MySQL, SQLite, MsSQL, SQL Anywhere and
PostgreSQL (on DBAL 4) and, for historical reasons, ``SEQUENCE``
for Oracle and PostgreSQL (on DBAL 3). This strategy provides
full portability.
- ``IDENTITY``: Tells Doctrine to use special identity columns in
the database that generate a value on insertion of a row. This
strategy does currently not provide full portability and is
supported by the following platforms: MySQL/SQLite/SQL Anywhere
(``AUTO_INCREMENT``), MSSQL (``IDENTITY``) and PostgreSQL (``SERIAL``).
(``AUTO_INCREMENT``), MSSQL (``IDENTITY``) and PostgreSQL (``SERIAL``
on DBAL 3, ``GENERATED BY DEFAULT AS IDENTITY`` on DBAL 4).
- ``SEQUENCE``: Tells Doctrine to use a database sequence for ID
generation. This strategy does currently not provide full
portability. Sequences are supported by Oracle, PostgreSql and
portability. Sequences are supported by Oracle, PostgreSQL and
SQL Anywhere.
- ``NONE``: Tells Doctrine that the identifiers are assigned (and
thus generated) by your code. The assignment must take place before

View File

@@ -30,7 +30,7 @@ table alias of the SQL table of the entity.
For the filter to correctly function, the following rules must be followed. Failure to do so will lead to unexpected results from the query cache.
1. Parameters for the query should be set on the filter object by ``SQLFilter#setParameter()`` before the filter is used by the ORM ( i.e. do not set parameters inside ``SQLFilter#addFilterConstraint()`` function ).
2. The filter must be deterministic. Don't change the values base on external inputs.
2. The filter must be deterministic. Don't change the values based on external inputs.
The ``SQLFilter#getParameter()`` function takes care of the proper quoting of parameters.

View File

@@ -178,6 +178,14 @@ internally by the ORM currently refers to fields by their name only, without tak
class containing the field into consideration. This makes it impossible to keep separate
mapping configuration for both fields.
Apart from that, in the case of having multiple ``private`` fields of the same name within
the class hierarchy an entity or mapped superclass, the Collection filtering API cannot determine
the right field to look at. Even if only one of these fields is actually mapped, the ``ArrayCollection``
will not be able to tell, since it does not have access to any metadata.
Thus, to avoid problems in this regard, it is best to avoid having multiple ``private`` fields of the
same name in class hierarchies containing entity and mapped superclasses.
Known Issues
------------

View File

@@ -44,7 +44,7 @@ Something like below for an entity region:
If the entity holds a collection that also needs to be cached.
An collection region could look something like:
A collection region could look something like:
.. code-block:: php
@@ -518,7 +518,7 @@ DELETE / UPDATE queries
DQL UPDATE / DELETE statements are ported directly into a database and bypass
the second-level cache.
Entities that are already cached will NOT be invalidated.
However the cached data could be evicted using the cache API or an special query hint.
However the cached data could be evicted using the cache API or a special query hint.
Execute the ``UPDATE`` and invalidate ``all cache entries`` using ``Query::HINT_CACHE_EVICT``

View File

@@ -118,7 +118,7 @@ entity might look like this:
}
}
Now the possiblity of mass-assignment exists on this entity and can
Now the possibility of mass-assignment exists on this entity and can
be exploited by attackers to set the "isAdmin" flag to true on any
object when you pass the whole request data to this method like:

View File

@@ -5,8 +5,6 @@
:depth: 3
tutorials/getting-started
tutorials/getting-started-database
tutorials/getting-started-models
tutorials/working-with-indexed-associations
tutorials/extra-lazy-associations
tutorials/composite-primary-keys

View File

@@ -1,26 +0,0 @@
Getting Started: Database First
===============================
.. note:: *Development Workflows*
When you :doc:`Code First <getting-started>`, you
start with developing Objects and then map them onto your database. When
you :doc:`Model First <getting-started-models>`, you are modelling your application using tools (for
example UML) and generate database schema and PHP code from this model.
When you have a Database First, you already have a database schema
and generate the corresponding PHP code from it.
.. note::
This getting started guide is in development.
Development of new applications often starts with an existing database schema.
When the database schema is the starting point for your application, then
development is said to use the *Database First* approach to Doctrine.
In this workflow you would modify the database schema first and then
regenerate the PHP code to use with this schema. You need a flexible
code-generator for this task.
We spun off a subproject, Doctrine CodeGenerator, that will fill this gap and
allow you to do *Database First* development.

View File

@@ -1,24 +0,0 @@
Getting Started: Model First
============================
.. note:: *Development Workflows*
When you :doc:`Code First <getting-started>`, you
start with developing Objects and then map them onto your database. When
you Model First, you are modelling your application using tools (for
example UML) and generate database schema and PHP code from this model.
When you have a :doc:`Database First <getting-started-database>`, then you already have a database schema
and generate the corresponding PHP code from it.
.. note::
This getting started guide is in development.
There are applications when you start with a high-level description of the
model using modelling tools such as UML. Modelling tools could also be Excel,
XML or CSV files that describe the model in some structured way. If your
application is using a modelling tool, then the development workflow is said to
be a *Model First* approach to Doctrine2.
In this workflow you always change the model description and then regenerate
both PHP code and database schema from this model.

View File

@@ -49,8 +49,9 @@ An entity contains persistable properties. A persistable property
is an instance variable of the entity that is saved into and retrieved from the database
by Doctrine's data mapping capabilities.
An entity class must not be final nor read-only, although
it can contain final methods or read-only properties.
An entity class can be final or read-only when you use
:ref:`native lazy objects <reference-native-lazy-objects>`.
It may contain final methods or read-only properties too.
An Example Model: Bug Tracker
-----------------------------
@@ -534,7 +535,7 @@ the ``id`` tag. It has a ``generator`` tag nested inside, which
specifies that the primary key generation mechanism should automatically
use the database platform's native id generation strategy (for
example, AUTO INCREMENT in the case of MySql, or Sequences in the
case of PostgreSql and Oracle).
case of PostgreSQL and Oracle).
Now that we have defined our first entity and its metadata,
let's update the database schema:
@@ -1287,7 +1288,7 @@ The console output of this script is then:
result set to retrieve entities from the database. DQL boils down to a
Native SQL statement and a ``ResultSetMapping`` instance itself. Using
Native SQL you could even use stored procedures for data retrieval, or
make use of advanced non-portable database queries like PostgreSql's
make use of advanced non-portable database queries like PostgreSQL's
recursive queries.

View File

@@ -1,17 +1,5 @@
parameters:
ignoreErrors:
-
message: '#^Expression "\$setCacheEntry\(\$data\)" on a separate line does not do anything\.$#'
identifier: expr.resultUnused
count: 1
path: src/AbstractQuery.php
-
message: '#^Expression "\$setCacheEntry\(\$stmt\)" on a separate line does not do anything\.$#'
identifier: expr.resultUnused
count: 1
path: src/AbstractQuery.php
-
message: '#^Method Doctrine\\ORM\\AbstractQuery\:\:getParameter\(\) should return Doctrine\\ORM\\Query\\Parameter\|null but returns Doctrine\\ORM\\Query\\Parameter\|false\|null\.$#'
identifier: return.type
@@ -234,12 +222,6 @@ parameters:
count: 1
path: src/Cache/DefaultQueryCache.php
-
message: '#^Parameter \#1 \$result of class Doctrine\\ORM\\Cache\\QueryCacheEntry constructor expects array\<string, mixed\>, array\<int\|string, array\<string, array\<mixed\>\>\> given\.$#'
identifier: argument.type
count: 1
path: src/Cache/DefaultQueryCache.php
-
message: '#^Parameter \#2 \$key of method Doctrine\\ORM\\Cache\\Logging\\CacheLogger\:\:entityCacheHit\(\) expects Doctrine\\ORM\\Cache\\EntityCacheKey, Doctrine\\ORM\\Cache\\CacheKey given\.$#'
identifier: argument.type
@@ -516,6 +498,12 @@ parameters:
count: 1
path: src/Cache/TimestampQueryCacheValidator.php
-
message: '#^Call to function is_a\(\) with arguments class\-string\<Doctrine\\ORM\\EntityRepository\>, ''Doctrine\\\\ORM\\\\EntityRepository'' and true will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 1
path: src/Configuration.php
-
message: '#^Method Doctrine\\ORM\\Configuration\:\:getDefaultRepositoryClassName\(\) return type with generic class Doctrine\\ORM\\EntityRepository does not specify its types\: T$#'
identifier: missingType.generics
@@ -558,12 +546,6 @@ parameters:
count: 1
path: src/Decorator/EntityManagerDecorator.php
-
message: '#^Call to an undefined method object\:\:setEntityManager\(\)\.$#'
identifier: method.notFound
count: 1
path: src/EntityManager.php
-
message: '#^Method Doctrine\\ORM\\EntityManager\:\:checkLockRequirements\(\) has parameter \$class with generic class Doctrine\\ORM\\Mapping\\ClassMetadata but does not specify its types\: T$#'
identifier: missingType.generics
@@ -714,6 +696,12 @@ parameters:
count: 1
path: src/Internal/Hydration/AbstractHydrator.php
-
message: '#^Method Doctrine\\ORM\\Internal\\Hydration\\AbstractHydrator\:\:gatherRowData\(\) should return array\{data\: array\<array\>, newObjects\?\: array\<array\{class\: ReflectionClass, args\: array, obj\: object\}\>, scalars\?\: array\} but returns array\{data\: array, newObjects\: array\<array\{class\: ReflectionClass\<object\>, args\: array, obj\?\: object\}\>, scalars\?\: non\-empty\-array\}\.$#'
identifier: return.type
count: 1
path: src/Internal/Hydration/AbstractHydrator.php
-
message: '#^Method Doctrine\\ORM\\Internal\\Hydration\\AbstractHydrator\:\:getClassMetadata\(\) return type with generic class Doctrine\\ORM\\Mapping\\ClassMetadata does not specify its types\: T$#'
identifier: missingType.generics
@@ -733,13 +721,13 @@ parameters:
path: src/Internal/Hydration/AbstractHydrator.php
-
message: '#^Parameter &\$id by\-ref type of method Doctrine\\ORM\\Internal\\Hydration\\AbstractHydrator\:\:gatherRowData\(\) expects array\<string, string\>, array\<int\|string, string\> given\.$#'
message: '#^Parameter &\$id by\-ref type of method Doctrine\\ORM\\Internal\\Hydration\\AbstractHydrator\:\:gatherRowData\(\) expects array\<string, string\>, array\<string\> given\.$#'
identifier: parameterByRef.type
count: 1
path: src/Internal/Hydration/AbstractHydrator.php
-
message: '#^Parameter &\$nonemptyComponents by\-ref type of method Doctrine\\ORM\\Internal\\Hydration\\AbstractHydrator\:\:gatherRowData\(\) expects array\<string, bool\>, array\<int\|string, bool\> given\.$#'
message: '#^Parameter &\$nonemptyComponents by\-ref type of method Doctrine\\ORM\\Internal\\Hydration\\AbstractHydrator\:\:gatherRowData\(\) expects array\<string, bool\>, array\<bool\> given\.$#'
identifier: parameterByRef.type
count: 1
path: src/Internal/Hydration/AbstractHydrator.php
@@ -1080,6 +1068,12 @@ parameters:
count: 1
path: src/Mapping/ClassMetadata.php
-
message: '#^Template type T is declared as covariant, but occurs in invariant position in return type of method Doctrine\\ORM\\Mapping\\ClassMetadata\:\:getReflectionClass\(\)\.$#'
identifier: generics.variance
count: 1
path: src/Mapping/ClassMetadata.php
-
message: '#^Unable to resolve the template type C in call to method Doctrine\\ORM\\Mapping\\ClassMetadata\<T of object\>\:\:fullyQualifiedClassName\(\)$#'
identifier: argument.templateType
@@ -1374,12 +1368,6 @@ parameters:
count: 2
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Cannot call method getName\(\) on Doctrine\\DBAL\\Schema\\Column\|false\.$#'
identifier: method.nonObject
count: 1
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Instanceof between Doctrine\\ORM\\Mapping\\ClassMetadata\<T of object\> and Doctrine\\ORM\\Mapping\\ClassMetadata will always evaluate to true\.$#'
identifier: instanceof.alwaysTrue
@@ -1410,12 +1398,24 @@ parameters:
count: 1
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Method Doctrine\\ORM\\Mapping\\Driver\\DatabaseDriver\:\:getAssetName\(\) has parameter \$asset with generic class Doctrine\\DBAL\\Schema\\AbstractAsset but does not specify its types\: N$#'
identifier: missingType.generics
count: 1
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Method Doctrine\\ORM\\Mapping\\Driver\\DatabaseDriver\:\:getClassNameForTable\(\) should return class\-string but returns string\.$#'
identifier: return.type
count: 2
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Parameter \#1 \$asset of static method Doctrine\\ORM\\Mapping\\Driver\\DatabaseDriver\:\:getAssetName\(\) expects Doctrine\\DBAL\\Schema\\AbstractAsset, Doctrine\\DBAL\\Schema\\Column\|false given\.$#'
identifier: argument.type
count: 1
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Parameter \#2 \$columnName of method Doctrine\\ORM\\Mapping\\Driver\\DatabaseDriver\:\:getFieldNameForColumn\(\) expects string, string\|false given\.$#'
identifier: argument.type
@@ -2022,18 +2022,6 @@ parameters:
count: 4
path: src/Persisters/Entity/BasicEntityPersister.php
-
message: '#^Access to an undefined property Doctrine\\ORM\\Mapping\\ManyToManyOwningSideMapping\|Doctrine\\ORM\\Mapping\\ManyToOneAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToOneOwningSideMapping\:\:\$relationToTargetKeyColumns\.$#'
identifier: property.notFound
count: 1
path: src/Persisters/Entity/BasicEntityPersister.php
-
message: '#^Access to an undefined property Doctrine\\ORM\\Mapping\\ManyToManyOwningSideMapping\|Doctrine\\ORM\\Mapping\\ManyToOneAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToOneOwningSideMapping\:\:\$sourceToTargetKeyColumns\.$#'
identifier: property.notFound
count: 1
path: src/Persisters/Entity/BasicEntityPersister.php
-
message: '#^Call to an undefined method Doctrine\\ORM\\Mapping\\ManyToManyInverseSideMapping\|Doctrine\\ORM\\Mapping\\ManyToManyOwningSideMapping\|Doctrine\\ORM\\Mapping\\ManyToOneAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToManyAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToOneInverseSideMapping\|Doctrine\\ORM\\Mapping\\OneToOneOwningSideMapping\:\:indexBy\(\)\.$#'
identifier: method.notFound
@@ -2046,18 +2034,6 @@ parameters:
count: 1
path: src/Persisters/Entity/BasicEntityPersister.php
-
message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:expandCriteriaParameters\(\) should return array\{list\<mixed\>, list\<Doctrine\\DBAL\\ArrayParameterType\:\:ASCII\|Doctrine\\DBAL\\ArrayParameterType\:\:BINARY\|Doctrine\\DBAL\\ArrayParameterType\:\:INTEGER\|Doctrine\\DBAL\\ArrayParameterType\:\:STRING\|Doctrine\\DBAL\\ParameterType\:\:ASCII\|Doctrine\\DBAL\\ParameterType\:\:BINARY\|Doctrine\\DBAL\\ParameterType\:\:BOOLEAN\|Doctrine\\DBAL\\ParameterType\:\:INTEGER\|Doctrine\\DBAL\\ParameterType\:\:LARGE_OBJECT\|Doctrine\\DBAL\\ParameterType\:\:NULL\|Doctrine\\DBAL\\ParameterType\:\:STRING\|string\>\} but returns array\{array\<mixed\>, list\<Doctrine\\DBAL\\ArrayParameterType\:\:ASCII\|Doctrine\\DBAL\\ArrayParameterType\:\:BINARY\|Doctrine\\DBAL\\ArrayParameterType\:\:INTEGER\|Doctrine\\DBAL\\ArrayParameterType\:\:STRING\|Doctrine\\DBAL\\ParameterType\:\:ASCII\|Doctrine\\DBAL\\ParameterType\:\:BINARY\|Doctrine\\DBAL\\ParameterType\:\:BOOLEAN\|Doctrine\\DBAL\\ParameterType\:\:INTEGER\|Doctrine\\DBAL\\ParameterType\:\:LARGE_OBJECT\|Doctrine\\DBAL\\ParameterType\:\:NULL\|Doctrine\\DBAL\\ParameterType\:\:STRING\|string\>\}\.$#'
identifier: return.type
count: 1
path: src/Persisters/Entity/BasicEntityPersister.php
-
message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:expandParameters\(\) should return array\{list\<mixed\>, list\<Doctrine\\DBAL\\ArrayParameterType\:\:ASCII\|Doctrine\\DBAL\\ArrayParameterType\:\:BINARY\|Doctrine\\DBAL\\ArrayParameterType\:\:INTEGER\|Doctrine\\DBAL\\ArrayParameterType\:\:STRING\|Doctrine\\DBAL\\ParameterType\:\:ASCII\|Doctrine\\DBAL\\ParameterType\:\:BINARY\|Doctrine\\DBAL\\ParameterType\:\:BOOLEAN\|Doctrine\\DBAL\\ParameterType\:\:INTEGER\|Doctrine\\DBAL\\ParameterType\:\:LARGE_OBJECT\|Doctrine\\DBAL\\ParameterType\:\:NULL\|Doctrine\\DBAL\\ParameterType\:\:STRING\|string\>\} but returns array\{array\<mixed\>, list\<Doctrine\\DBAL\\ArrayParameterType\:\:ASCII\|Doctrine\\DBAL\\ArrayParameterType\:\:BINARY\|Doctrine\\DBAL\\ArrayParameterType\:\:INTEGER\|Doctrine\\DBAL\\ArrayParameterType\:\:STRING\|Doctrine\\DBAL\\ParameterType\:\:ASCII\|Doctrine\\DBAL\\ParameterType\:\:BINARY\|Doctrine\\DBAL\\ParameterType\:\:BOOLEAN\|Doctrine\\DBAL\\ParameterType\:\:INTEGER\|Doctrine\\DBAL\\ParameterType\:\:LARGE_OBJECT\|Doctrine\\DBAL\\ParameterType\:\:NULL\|Doctrine\\DBAL\\ParameterType\:\:STRING\|string\>\}\.$#'
identifier: return.type
count: 1
path: src/Persisters/Entity/BasicEntityPersister.php
-
message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:expandToManyParameters\(\) return type has no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
@@ -2094,12 +2070,6 @@ parameters:
count: 1
path: src/Persisters/Entity/BasicEntityPersister.php
-
message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:getIndividualValue\(\) should return list\<mixed\> but returns array\<mixed\>\.$#'
identifier: return.type
count: 1
path: src/Persisters/Entity/BasicEntityPersister.php
-
message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:getSelectColumnAssociationSQL\(\) has parameter \$class with generic class Doctrine\\ORM\\Mapping\\ClassMetadata but does not specify its types\: T$#'
identifier: missingType.generics
@@ -2112,18 +2082,6 @@ parameters:
count: 1
path: src/Persisters/Entity/BasicEntityPersister.php
-
message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:getTypes\(\) has parameter \$class with generic class Doctrine\\ORM\\Mapping\\ClassMetadata but does not specify its types\: T$#'
identifier: missingType.generics
count: 1
path: src/Persisters/Entity/BasicEntityPersister.php
-
message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:getTypes\(\) should return list\<Doctrine\\DBAL\\ArrayParameterType\:\:ASCII\|Doctrine\\DBAL\\ArrayParameterType\:\:BINARY\|Doctrine\\DBAL\\ArrayParameterType\:\:INTEGER\|Doctrine\\DBAL\\ArrayParameterType\:\:STRING\|Doctrine\\DBAL\\ParameterType\:\:ASCII\|Doctrine\\DBAL\\ParameterType\:\:BINARY\|Doctrine\\DBAL\\ParameterType\:\:BOOLEAN\|Doctrine\\DBAL\\ParameterType\:\:INTEGER\|Doctrine\\DBAL\\ParameterType\:\:LARGE_OBJECT\|Doctrine\\DBAL\\ParameterType\:\:NULL\|Doctrine\\DBAL\\ParameterType\:\:STRING\|string\> but returns list\<Doctrine\\DBAL\\ArrayParameterType\:\:ASCII\|Doctrine\\DBAL\\ArrayParameterType\:\:BINARY\|Doctrine\\DBAL\\ArrayParameterType\:\:INTEGER\|Doctrine\\DBAL\\ArrayParameterType\:\:STRING\|int\>\.$#'
identifier: return.type
count: 1
path: src/Persisters/Entity/BasicEntityPersister.php
-
message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:loadCollectionFromStatement\(\) has parameter \$coll with generic class Doctrine\\ORM\\PersistentCollection but does not specify its types\: TKey, T$#'
identifier: missingType.generics
@@ -2178,12 +2136,6 @@ parameters:
count: 1
path: src/Persisters/Entity/BasicEntityPersister.php
-
message: '#^Strict comparison using \=\=\= between string and null will always evaluate to false\.$#'
identifier: identical.alwaysFalse
count: 1
path: src/Persisters/Entity/BasicEntityPersister.php
-
message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\CachedPersisterContext\:\:__construct\(\) has parameter \$class with generic interface Doctrine\\Persistence\\Mapping\\ClassMetadata but does not specify its types\: T$#'
identifier: missingType.generics
@@ -3090,12 +3042,6 @@ parameters:
count: 1
path: src/Tools/ResolveTargetEntityListener.php
-
message: '#^Parameter \#1 \$className of method Doctrine\\Persistence\\Mapping\\AbstractClassMetadataFactory\<Doctrine\\ORM\\Mapping\\ClassMetadata\>\:\:setMetadataFor\(\) expects class\-string, \(int\|string\) given\.$#'
identifier: argument.type
count: 1
path: src/Tools/ResolveTargetEntityListener.php
-
message: '#^Access to an undefined property Doctrine\\ORM\\Mapping\\ManyToManyInverseSideMapping\|Doctrine\\ORM\\Mapping\\ManyToManyOwningSideMapping\|Doctrine\\ORM\\Mapping\\ManyToOneAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToManyAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToOneInverseSideMapping\|Doctrine\\ORM\\Mapping\\OneToOneOwningSideMapping\:\:\$joinColumns\.$#'
identifier: property.notFound
@@ -3231,6 +3177,12 @@ parameters:
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Method Doctrine\\ORM\\Tools\\SchemaTool\:\:getAssetName\(\) has parameter \$asset with generic class Doctrine\\DBAL\\Schema\\AbstractAsset but does not specify its types\: N$#'
identifier: missingType.generics
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Method Doctrine\\ORM\\Tools\\SchemaTool\:\:getCreateSchemaSql\(\) has parameter \$classes with generic class Doctrine\\ORM\\Mapping\\ClassMetadata but does not specify its types\: T$#'
identifier: missingType.generics
@@ -3303,24 +3255,6 @@ parameters:
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Parameter \#1 \$columnNames of method Doctrine\\DBAL\\Schema\\Table\:\:setPrimaryKey\(\) expects non\-empty\-list\<string\>, array\<string\> given\.$#'
identifier: argument.type
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Parameter \#1 \$value of static method Doctrine\\DBAL\\Schema\\Name\\Identifier\:\:unquoted\(\) expects non\-empty\-string, string given\.$#'
identifier: argument.type
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Parameter \#2 \$columnNames of class Doctrine\\DBAL\\Schema\\PrimaryKeyConstraint constructor expects non\-empty\-list\<Doctrine\\DBAL\\Schema\\Name\\UnqualifiedName\>, list\<Doctrine\\DBAL\\Schema\\Name\\UnqualifiedName\> given\.$#'
identifier: argument.type
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Parameter \#2 \$columns of class Doctrine\\DBAL\\Schema\\Index constructor expects non\-empty\-list\<string\>, list\<string\> given\.$#'
identifier: argument.type
@@ -3333,6 +3267,18 @@ parameters:
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Parameter \#2 \$primaryKeyColumns of method Doctrine\\ORM\\Tools\\SchemaTool\:\:addPrimaryKeyConstraint\(\) expects non\-empty\-array\<non\-empty\-string\>, list\<string\> given\.$#'
identifier: argument.type
count: 2
path: src/Tools/SchemaTool.php
-
message: '#^Parameter \#2 \$primaryKeyColumns of method Doctrine\\ORM\\Tools\\SchemaTool\:\:addPrimaryKeyConstraint\(\) expects non\-empty\-array\<non\-empty\-string\>, non\-empty\-list\<string\> given\.$#'
identifier: argument.type
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Parameter \#3 \$foreignColumnNames of method Doctrine\\DBAL\\Schema\\Table\:\:addForeignKeyConstraint\(\) expects non\-empty\-list\<string\>, list\<string\> given\.$#'
identifier: argument.type
@@ -3597,6 +3543,18 @@ parameters:
count: 1
path: src/Utility/PersisterHelper.php
-
message: '#^Access to an undefined property Doctrine\\ORM\\Mapping\\ManyToManyOwningSideMapping\|Doctrine\\ORM\\Mapping\\ManyToOneAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToOneOwningSideMapping\:\:\$relationToTargetKeyColumns\.$#'
identifier: property.notFound
count: 1
path: src/Utility/PersisterHelper.php
-
message: '#^Access to an undefined property Doctrine\\ORM\\Mapping\\ManyToManyOwningSideMapping\|Doctrine\\ORM\\Mapping\\ManyToOneAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToOneOwningSideMapping\:\:\$sourceToTargetKeyColumns\.$#'
identifier: property.notFound
count: 1
path: src/Utility/PersisterHelper.php
-
message: '#^Method Doctrine\\ORM\\Utility\\PersisterHelper\:\:getTypeOfColumn\(\) has parameter \$class with generic class Doctrine\\ORM\\Mapping\\ClassMetadata but does not specify its types\: T$#'
identifier: missingType.generics
@@ -3608,3 +3566,15 @@ parameters:
identifier: missingType.generics
count: 1
path: src/Utility/PersisterHelper.php
-
message: '#^Method Doctrine\\ORM\\Utility\\PersisterHelper\:\:inferParameterTypes\(\) has parameter \$class with generic class Doctrine\\ORM\\Mapping\\ClassMetadata but does not specify its types\: T$#'
identifier: missingType.generics
count: 1
path: src/Utility/PersisterHelper.php
-
message: '#^Method Doctrine\\ORM\\Utility\\PersisterHelper\:\:inferParameterTypes\(\) should return list\<Doctrine\\DBAL\\ArrayParameterType\:\:ASCII\|Doctrine\\DBAL\\ArrayParameterType\:\:BINARY\|Doctrine\\DBAL\\ArrayParameterType\:\:INTEGER\|Doctrine\\DBAL\\ArrayParameterType\:\:STRING\|Doctrine\\DBAL\\ParameterType\:\:ASCII\|Doctrine\\DBAL\\ParameterType\:\:BINARY\|Doctrine\\DBAL\\ParameterType\:\:BOOLEAN\|Doctrine\\DBAL\\ParameterType\:\:INTEGER\|Doctrine\\DBAL\\ParameterType\:\:LARGE_OBJECT\|Doctrine\\DBAL\\ParameterType\:\:NULL\|Doctrine\\DBAL\\ParameterType\:\:STRING\|string\> but returns list\<Doctrine\\DBAL\\ArrayParameterType\:\:ASCII\|Doctrine\\DBAL\\ArrayParameterType\:\:BINARY\|Doctrine\\DBAL\\ArrayParameterType\:\:INTEGER\|Doctrine\\DBAL\\ArrayParameterType\:\:STRING\|int\>\.$#'
identifier: return.type
count: 1
path: src/Utility/PersisterHelper.php

View File

@@ -11,7 +11,7 @@ parameters:
# We can be certain that those values are not matched.
-
message: '~^Match expression does not handle remaining values:~'
path: src/Persisters/Entity/BasicEntityPersister.php
path: src/Utility/PersisterHelper.php
# DBAL 4 compatibility
-
@@ -34,6 +34,14 @@ parameters:
message: '~^Call to an undefined method Doctrine\\DBAL\\Schema\\Table::addPrimaryKeyConstraint\(\)\.$~'
path: src/Tools/SchemaTool.php
-
message: '~^Call to static method quoted\(\) on an unknown class Doctrine\\DBAL\\Schema\\Name\\Identifier\.$~'
path: src/Tools/SchemaTool.php
-
message: '~^Call to an undefined method Doctrine\\DBAL\\Schema\\Table\:\:getObjectName\(\)\.$~'
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '~^Call to an undefined method Doctrine\\DBAL\\Schema\\ForeignKeyConstraint::get.*\.$~'
identifier: method.notFound
@@ -79,8 +87,12 @@ parameters:
message: '~^Call to method toString.*UnqualifiedName\.$~'
path: src/Tools/SchemaTool.php
- '~^Call to method getObjectName\(\) on an unknown class Doctrine\\DBAL\\Schema\\NamedObject\.$~'
- '~^Class Doctrine\\DBAL\\Platforms\\SQLitePlatform not found\.$~'
- '~^Class Doctrine\\DBAL\\Schema\\NamedObject not found\.$~'
-
message: '~sort~'
identifier: argument.unresolvableType
@@ -105,12 +117,12 @@ parameters:
path: src/Mapping/Driver/AttributeDriver.php
-
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: '~^Method Doctrine\\ORM\\Utility\\PersisterHelper\:\:getArrayBindingType\(\) never returns .* so it can be removed from the return type\.$~'
path: src/Utility/PersisterHelper.php
-
message: '~getTypes.*should return~'
path: src/Persisters/Entity/BasicEntityPersister.php
message: '~inferParameterTypes.*should return~'
path: src/Utility/PersisterHelper.php
-
message: '~.*appendLockHint.*expects.*LockMode given~'

View File

@@ -10,15 +10,15 @@ parameters:
# We can be certain that those values are not matched.
-
message: '~^Match expression does not handle remaining values:~'
path: src/Persisters/Entity/BasicEntityPersister.php
path: src/Utility/PersisterHelper.php
# DBAL 4 compatibility
-
message: '~^Method Doctrine\\ORM\\Query\\AST\\Functions\\TrimFunction::getTrimMode\(\) never returns .* so it can be removed from the return type\.$~'
path: src/Query/AST/Functions/TrimFunction.php
-
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: '~^Method Doctrine\\ORM\\Utility\\PersisterHelper\:\:getArrayBindingType\(\) never returns .* so it can be removed from the return type\.$~'
path: src/Utility/PersisterHelper.php
# Compatibility with DBAL 3
# See https://github.com/doctrine/dbal/pull/3480

View File

@@ -1,21 +0,0 @@
#!/bin/bash
# This script is a small convenience wrapper for running the doctrine testsuite against a large bunch of databases.
# Just create the phpunit.xmls as described in the array below and configure the specific files <php /> section
# to connect to that database. Just omit a file if you don't have that database and the tests will be skipped.
configs[1]="mysql.phpunit.xml"
configs[2]='postgres.phpunit.xml'
configs[3]='sqlite.phpunit.xml'
configs[4]='oracle.phpunit.xml'
configs[5]='db2.phpunit.xml'
configs[6]='pdo-ibm.phpunit.xml'
configs[7]='sqlsrv.phpunit.xml'
for i in "${configs[@]}"; do
if [ -f "$i" ];
then
echo "RUNNING TESTS WITH CONFIG $i"
phpunit -c "$i" "$@"
fi;
done

View File

@@ -18,7 +18,6 @@ use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\MappingException as ORMMappingException;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Query\Parameter;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Persistence\Mapping\MappingException;
use LogicException;
@@ -865,10 +864,6 @@ abstract class AbstractQuery
throw new LogicException('Uninitialized result set mapping.');
}
if ($rsm->isMixed && count($rsm->scalarMappings) > 0) {
throw QueryException::iterateWithMixedResultNotAllowed();
}
$stmt = $this->_doExecute();
return $this->em->newHydrator($this->hydrationMode)->toIterable($stmt, $rsm, $this->hints);

View File

@@ -23,9 +23,11 @@ use function array_keys;
use function array_map;
use function array_merge;
use function count;
use function current;
use function end;
use function in_array;
use function is_array;
use function is_object;
use function ksort;
/**
@@ -126,8 +128,10 @@ abstract class AbstractHydrator
} else {
yield from $result;
}
} else {
} elseif (is_object(current($result))) {
yield $result;
} else {
yield array_merge(...$result);
}
}
} finally {

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Driver;
use Doctrine\DBAL\Schema\AbstractAsset;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
@@ -11,6 +12,7 @@ use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Index\IndexedColumn;
use Doctrine\DBAL\Schema\Index\IndexType;
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
use Doctrine\DBAL\Schema\NamedObject;
use Doctrine\DBAL\Schema\PrimaryKeyConstraint;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Schema\Table;
@@ -143,14 +145,14 @@ class DatabaseDriver implements MappingDriver
$this->tables = $this->manyToManyTables = $this->classToTableNames = [];
foreach ($entityTables as $table) {
$className = $this->getClassNameForTable($table->getName());
$className = $this->getClassNameForTable(self::getAssetName($table));
$this->classToTableNames[$className] = $table->getName();
$this->tables[$table->getName()] = $table;
$this->classToTableNames[$className] = self::getAssetName($table);
$this->tables[self::getAssetName($table)] = $table;
}
foreach ($manyToManyTables as $table) {
$this->manyToManyTables[$table->getName()] = $table;
$this->manyToManyTables[self::getAssetName($table)] = $table;
}
}
@@ -219,13 +221,13 @@ class DatabaseDriver implements MappingDriver
$localColumn = current(self::getReferencingColumnNames($myFk));
$associationMapping = [];
$associationMapping['fieldName'] = $this->getFieldNameForColumn($manyTable->getName(), current(self::getReferencingColumnNames($otherFk)), true);
$associationMapping['fieldName'] = $this->getFieldNameForColumn(self::getAssetName($manyTable), current(self::getReferencingColumnNames($otherFk)), true);
$associationMapping['targetEntity'] = $this->getClassNameForTable(self::getReferencedTableName($otherFk));
if (current($manyTable->getColumns())->getName() === $localColumn) {
$associationMapping['inversedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current(self::getReferencingColumnNames($myFk)), true);
if (self::getAssetName(current($manyTable->getColumns())) === $localColumn) {
$associationMapping['inversedBy'] = $this->getFieldNameForColumn(self::getAssetName($manyTable), current(self::getReferencingColumnNames($myFk)), true);
$associationMapping['joinTable'] = [
'name' => strtolower($manyTable->getName()),
'name' => strtolower(self::getAssetName($manyTable)),
'joinColumns' => [],
'inverseJoinColumns' => [],
];
@@ -250,7 +252,14 @@ class DatabaseDriver implements MappingDriver
];
}
} else {
$associationMapping['mappedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current(self::getReferencingColumnNames($myFk)), true);
$associationMapping['mappedBy'] = $this->getFieldNameForColumn(
// @phpstan-ignore function.alreadyNarrowedType (DBAL 3 compatibility)
method_exists(Table::class, 'getObjectName')
? $manyTable->getObjectName()->toString()
: $manyTable->getName(), // DBAL < 4.4
current(self::getReferencingColumnNames($myFk)),
true,
);
}
$metadata->mapManyToMany($associationMapping);
@@ -270,7 +279,7 @@ class DatabaseDriver implements MappingDriver
$this->tables = $this->manyToManyTables = $this->classToTableNames = [];
foreach ($this->sm->listTables() as $table) {
$tableName = $table->getName();
$tableName = self::getAssetName($table);
$foreignKeys = $table->getForeignKeys();
$allForeignKeyColumns = [];
@@ -335,7 +344,7 @@ class DatabaseDriver implements MappingDriver
$isUnique = $index->isUnique();
}
$indexName = $index->getName();
$indexName = self::getAssetName($index);
$indexColumns = self::getIndexedColumns($index);
$constraintType = $isUnique
? 'uniqueConstraints'
@@ -364,13 +373,13 @@ class DatabaseDriver implements MappingDriver
$fieldMappings = [];
foreach ($columns as $column) {
if (in_array($column->getName(), $allForeignKeys, true)) {
if (in_array(self::getAssetName($column), $allForeignKeys, true)) {
continue;
}
$fieldMapping = $this->buildFieldMapping($tableName, $column);
if ($primaryKeys && in_array($column->getName(), $primaryKeys, true)) {
if ($primaryKeys && in_array(self::getAssetName($column), $primaryKeys, true)) {
$fieldMapping['id'] = true;
$ids[] = $fieldMapping;
}
@@ -411,8 +420,8 @@ class DatabaseDriver implements MappingDriver
private function buildFieldMapping(string $tableName, Column $column): array
{
$fieldMapping = [
'fieldName' => $this->getFieldNameForColumn($tableName, $column->getName(), false),
'columnName' => $column->getName(),
'fieldName' => $this->getFieldNameForColumn($tableName, self::getAssetName($column), false),
'columnName' => self::getAssetName($column),
'type' => Type::getTypeRegistry()->lookupName($column->getType()),
'nullable' => ! $column->getNotnull(),
'options' => [
@@ -619,4 +628,12 @@ class DatabaseDriver implements MappingDriver
return null;
}
private static function getAssetName(AbstractAsset $asset): string
{
return $asset instanceof NamedObject
? $asset->getObjectName()->toString()
// DBAL < 4.4
: $asset->getName();
}
}

View File

@@ -22,13 +22,8 @@ trait ReflectionBasedDriver
*/
private function isRepeatedPropertyDeclaration(ReflectionProperty $property, ClassMetadata $metadata): bool
{
/** @var class-string $declaringClass */
$declaringClass = $property->class;
if ($this->isTransient($declaringClass)) {
return isset($metadata->fieldMappings[$property->name]);
}
if (
isset($metadata->fieldMappings[$property->name]->declared)
&& $metadata->fieldMappings[$property->name]->declared === $declaringClass

View File

@@ -137,7 +137,6 @@ class XmlDriver extends FileDriver
];
if (isset($discrColumn['options'])) {
assert($discrColumn['options'] instanceof SimpleXMLElement);
$columnDef['options'] = $this->parseOptions($discrColumn['options']->children());
}

View File

@@ -19,6 +19,7 @@ use Doctrine\ORM\Query;
use Doctrine\ORM\Utility\PersisterHelper;
use function array_fill;
use function array_merge;
use function array_pop;
use function assert;
use function count;
@@ -247,10 +248,17 @@ class ManyToManyPersister extends AbstractCollectionPersister
if ($value === null && ($operator === Comparison::EQ || $operator === Comparison::NEQ)) {
$whereClauses[] = sprintf('te.%s %s NULL', $field, $operator === Comparison::EQ ? 'IS' : 'IS NOT');
} elseif ($operator === Comparison::IN || $operator === Comparison::NIN) {
$whereClauses[] = sprintf('te.%s %s (%s)', $field, $operator === Comparison::IN ? 'IN' : 'NOT IN', implode(', ', array_fill(0, count($value), '?')));
foreach ($value as $item) {
$params = array_merge($params, PersisterHelper::convertToParameterValue($item, $this->em));
$paramTypes = array_merge($paramTypes, PersisterHelper::inferParameterTypes($name, $item, $targetClass, $this->em));
}
} else {
$whereClauses[] = sprintf('te.%s %s ?', $field, $operator);
$params[] = $value;
$paramTypes[] = PersisterHelper::getTypeOfField($name, $targetClass, $this->em)[0];
$params = [...$params, ...PersisterHelper::convertToParameterValue($value, $this->em)];
$paramTypes = [...$paramTypes, ...PersisterHelper::inferParameterTypes($name, $value, $targetClass, $this->em)];
}
}

View File

@@ -89,7 +89,7 @@ class OneToManyPersister extends AbstractCollectionPersister
// only works with single id identifier entities. Will throw an
// exception in Entity Persisters if that is not the case for the
// 'mappedBy' field.
$criteria = new Criteria(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner()));
$criteria = Criteria::create(true)->where(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner()));
return $persister->count($criteria);
}
@@ -118,7 +118,7 @@ class OneToManyPersister extends AbstractCollectionPersister
// only works with single id identifier entities. Will throw an
// exception in Entity Persisters if that is not the case for the
// 'mappedBy' field.
$criteria = new Criteria();
$criteria = Criteria::create(true);
$criteria->andWhere(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner()));
$criteria->andWhere(Criteria::expr()->eq($mapping->indexBy(), $key));
@@ -138,7 +138,7 @@ class OneToManyPersister extends AbstractCollectionPersister
// only works with single id identifier entities. Will throw an
// exception in Entity Persisters if that is not the case for the
// 'mappedBy' field.
$criteria = new Criteria(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner()));
$criteria = Criteria::create(true)->where(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner()));
return $persister->exists($element, $criteria);
}

View File

@@ -31,9 +31,7 @@ use Doctrine\ORM\Persisters\Exception\InvalidOrientation;
use Doctrine\ORM\Persisters\Exception\UnrecognizedField;
use Doctrine\ORM\Persisters\SqlExpressionVisitor;
use Doctrine\ORM\Persisters\SqlValueVisitor;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\Repository\Exception\InvalidFindByCall;
use Doctrine\ORM\UnitOfWork;
@@ -43,17 +41,17 @@ use Doctrine\ORM\Utility\PersisterHelper;
use LengthException;
use function array_combine;
use function array_diff_key;
use function array_fill;
use function array_flip;
use function array_keys;
use function array_map;
use function array_merge;
use function array_search;
use function array_unique;
use function array_values;
use function assert;
use function count;
use function implode;
use function is_array;
use function is_object;
use function reset;
use function spl_object_id;
use function sprintf;
@@ -353,7 +351,7 @@ class BasicEntityPersister implements EntityPersister
$types = [];
foreach ($id as $field => $value) {
$types = [...$types, ...$this->getTypes($field, $value, $versionedClass)];
$types = [...$types, ...PersisterHelper::inferParameterTypes($field, $value, $versionedClass, $this->em)];
}
return $types;
@@ -919,8 +917,31 @@ class BasicEntityPersister implements EntityPersister
continue;
}
$sqlParams = [...$sqlParams, ...$this->getValues($value)];
$sqlTypes = [...$sqlTypes, ...$this->getTypes($field, $value, $this->class)];
if ($operator === Comparison::IN || $operator === Comparison::NIN) {
if (! is_array($value)) {
$value = [$value];
}
foreach ($value as $item) {
if ($item === null) {
/*
* Compare this to how \Doctrine\ORM\Persisters\Entity\BasicEntityPersister::getSelectConditionStatementSQL
* creates the "[NOT] IN (...)" expression - for NULL values, it does _not_ insert a placeholder in the
* SQL and instead adds an extra ... OR ... IS NULL condition. So we need to skip NULL values here as
* well to create a parameters list that matches the SQL.
*/
continue;
}
$sqlParams = [...$sqlParams, ...PersisterHelper::convertToParameterValue($item, $this->em)];
$sqlTypes = [...$sqlTypes, ...PersisterHelper::inferParameterTypes($field, $item, $this->class, $this->em)];
}
continue;
}
$sqlParams = [...$sqlParams, ...PersisterHelper::convertToParameterValue($value, $this->em)];
$sqlTypes = [...$sqlTypes, ...PersisterHelper::inferParameterTypes($field, $value, $this->class, $this->em)];
}
return [$sqlParams, $sqlTypes];
@@ -1608,6 +1629,8 @@ class BasicEntityPersister implements EntityPersister
AssociationMapping|null $assoc = null,
string|null $comparison = null,
): string {
$comparison ??= (is_array($value) ? Comparison::IN : Comparison::EQ);
$selectedColumns = [];
$columns = $this->getSelectConditionStatementColumnSQL($field, $assoc);
@@ -1627,46 +1650,45 @@ class BasicEntityPersister implements EntityPersister
$placeholder = $type->convertToDatabaseValueSQL($placeholder, $this->platform);
}
if ($comparison !== null) {
// special case null value handling
if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && $value === null) {
$selectedColumns[] = $column . ' IS NULL';
continue;
}
if ($comparison === Comparison::NEQ && $value === null) {
$selectedColumns[] = $column . ' IS NOT NULL';
continue;
}
$selectedColumns[] = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder);
// special case null value handling
if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && $value === null) {
$selectedColumns[] = $column . ' IS NULL';
continue;
}
if (is_array($value)) {
$in = sprintf('%s IN (%s)', $column, $placeholder);
if ($comparison === Comparison::NEQ && $value === null) {
$selectedColumns[] = $column . ' IS NOT NULL';
if (array_search(null, $value, true) !== false) {
$selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column);
continue;
}
continue;
if ($comparison === Comparison::IN || $comparison === Comparison::NIN) {
if (! is_array($value)) {
$value = [$value];
}
$selectedColumns[] = $in;
$nullKeys = array_keys($value, null, true);
$nonNullValues = array_diff_key($value, array_flip($nullKeys));
$placeholders = implode(', ', array_fill(0, count($nonNullValues), $placeholder));
$in = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholders);
if ($nullKeys) {
if ($nonNullValues) {
$selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column);
} else {
$selectedColumns[] = $column . ' IS NULL';
}
} else {
$selectedColumns[] = $in;
}
continue;
}
if ($value === null) {
$selectedColumns[] = sprintf('%s IS NULL', $column);
continue;
}
$selectedColumns[] = sprintf('%s = %s', $column, $placeholder);
$selectedColumns[] = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder);
}
return implode(' AND ', $selectedColumns);
@@ -1858,8 +1880,18 @@ class BasicEntityPersister implements EntityPersister
continue; // skip null values.
}
$types = [...$types, ...$this->getTypes($field, $value, $this->class)];
$params = array_merge($params, $this->getValues($value));
if (is_array($value)) {
$nonNullValues = array_diff_key($value, array_flip(array_keys($value, null, true)));
foreach ($nonNullValues as $item) {
$types = [...$types, ...PersisterHelper::inferParameterTypes($field, $item, $this->class, $this->em)];
$params = [...$params, ...PersisterHelper::convertToParameterValue($item, $this->em)];
}
continue;
}
$types = [...$types, ...PersisterHelper::inferParameterTypes($field, $value, $this->class, $this->em)];
$params = [...$params, ...PersisterHelper::convertToParameterValue($value, $this->em)];
}
return [$params, $types];
@@ -1887,130 +1919,13 @@ class BasicEntityPersister implements EntityPersister
continue; // skip null values.
}
$types = [...$types, ...$this->getTypes($criterion['field'], $criterion['value'], $criterion['class'])];
$params = array_merge($params, $this->getValues($criterion['value']));
$types = [...$types, ...PersisterHelper::inferParameterTypes($criterion['field'], $criterion['value'], $criterion['class'], $this->em)];
$params = [...$params, ...PersisterHelper::convertToParameterValue($criterion['value'], $this->em)];
}
return [$params, $types];
}
/**
* Infers field types to be used by parameter type casting.
*
* @return list<ParameterType|ArrayParameterType|int|string>
* @phpstan-return list<ParameterType::*|ArrayParameterType::*|string>
*
* @throws QueryException
*/
private function getTypes(string $field, mixed $value, ClassMetadata $class): array
{
$types = [];
switch (true) {
case isset($class->fieldMappings[$field]):
$types = array_merge($types, [$class->fieldMappings[$field]->type]);
break;
case isset($class->associationMappings[$field]):
$assoc = $this->em->getMetadataFactory()->getOwningSide($class->associationMappings[$field]);
$class = $this->em->getClassMetadata($assoc->targetEntity);
if ($assoc->isManyToManyOwningSide()) {
$columns = $assoc->relationToTargetKeyColumns;
} else {
assert($assoc->isToOneOwningSide());
$columns = $assoc->sourceToTargetKeyColumns;
}
foreach ($columns as $column) {
$types[] = PersisterHelper::getTypeOfColumn($column, $class, $this->em);
}
break;
default:
$types[] = ParameterType::STRING;
break;
}
if (is_array($value)) {
return array_map($this->getArrayBindingType(...), $types);
}
return $types;
}
/** @phpstan-return ArrayParameterType::* */
private function getArrayBindingType(ParameterType|int|string $type): ArrayParameterType|int
{
if (! $type instanceof ParameterType) {
$type = Type::getType((string) $type)->getBindingType();
}
return match ($type) {
ParameterType::STRING => ArrayParameterType::STRING,
ParameterType::INTEGER => ArrayParameterType::INTEGER,
ParameterType::ASCII => ArrayParameterType::ASCII,
ParameterType::BINARY => ArrayParameterType::BINARY,
};
}
/**
* Retrieves the parameters that identifies a value.
*
* @return mixed[]
*/
private function getValues(mixed $value): array
{
if (is_array($value)) {
$newValue = [];
foreach ($value as $itemValue) {
$newValue = array_merge($newValue, $this->getValues($itemValue));
}
return [$newValue];
}
return $this->getIndividualValue($value);
}
/**
* Retrieves an individual parameter value.
*
* @phpstan-return list<mixed>
*/
private function getIndividualValue(mixed $value): array
{
if (! is_object($value)) {
return [$value];
}
if ($value instanceof BackedEnum) {
return [$value->value];
}
$valueClass = DefaultProxyClassNameResolver::getClass($value);
if ($this->em->getMetadataFactory()->isTransient($valueClass)) {
return [$value];
}
$class = $this->em->getClassMetadata($valueClass);
if ($class->isIdentifierComposite) {
$newValue = [];
foreach ($class->getIdentifierValues($value) as $innerValue) {
$newValue = array_merge($newValue, $this->getValues($innerValue));
}
return $newValue;
}
return [$this->em->getUnitOfWork()->getSingleIdentifierValue($value)];
}
public function exists(object $entity, Criteria|null $extraConditions = null): bool
{
$criteria = $this->class->getIdentifierValues($entity);

View File

@@ -69,7 +69,7 @@ interface EntityPersister
/**
* Expands the parameters from the given criteria and use the correct binding types if found.
*
* @param string[] $criteria
* @param array<string, mixed> $criteria
*
* @phpstan-return array{list<mixed>, list<ParameterType::*|ArrayParameterType::*|string>}
*/

View File

@@ -61,7 +61,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
*/
private function getVersionedClassMetadata(): ClassMetadata
{
if (isset($this->class->fieldMappings[$this->class->versionField]->inherited)) {
if ($this->class->versionField !== null && isset($this->class->fieldMappings[$this->class->versionField]->inherited)) {
$definingClassName = $this->class->fieldMappings[$this->class->versionField]->inherited;
return $this->em->getClassMetadata($definingClassName);

View File

@@ -16,7 +16,11 @@ class EntityAsDtoArgumentExpression extends Node
public function __construct(
public mixed $expression,
public string|null $identificationVariable,
public string|null $aliasVariable = null,
) {
if (! $aliasVariable) {
$this->aliasVariable = $expression;
}
}
public function dispatch(SqlWalker $walker): string

View File

@@ -1147,7 +1147,7 @@ final class Parser
];
}
return new AST\EntityAsDtoArgumentExpression($expression, $identVariable);
return new AST\EntityAsDtoArgumentExpression($expression, $identVariable, $aliasResultVariable);
}
/**
@@ -1895,6 +1895,7 @@ final class Parser
$expression = $this->NewObjectExpression();
} elseif ($token->type === TokenType::T_IDENTIFIER && $peek->type !== TokenType::T_DOT && $peek->type !== TokenType::T_OPEN_PARENTHESIS) {
$expression = $this->EntityAsDtoArgumentExpression();
$fieldAlias = $expression->aliasVariable;
} else {
$expression = $this->ScalarExpression();
}

View File

@@ -1412,7 +1412,9 @@ class SqlWalker
$sqlParts[] = $col . ' AS ' . $columnAlias;
$this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
if ($resultAlias !== null) {
$this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
}
$this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name);
@@ -1445,7 +1447,9 @@ class SqlWalker
$sqlParts[] = $col . ' AS ' . $columnAlias;
$this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
if ($resultAlias !== null) {
$this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
}
$this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName);
}

View File

@@ -132,7 +132,9 @@ class LimitSubqueryOutputWalker extends SqlOutputWalker
$selectAliasToExpressionMap = [];
// Get any aliases that are available for select expressions.
foreach ($AST->selectClause->selectExpressions as $selectExpression) {
$selectAliasToExpressionMap[$selectExpression->fieldIdentificationVariable] = $selectExpression->expression;
if ($selectExpression->fieldIdentificationVariable !== null) {
$selectAliasToExpressionMap[$selectExpression->fieldIdentificationVariable] = $selectExpression->expression;
}
}
// Rebuild string orderby expressions to use the select expression they're referencing

View File

@@ -202,7 +202,22 @@ class Paginator implements Countable, IteratorAggregate
*/
private function getCountQuery(): Query
{
$countQuery = $this->cloneQuery($this->query);
/*
As opposed to using self::cloneQuery, the following code does not transfer
a potentially existing result set mapping (either set directly by the user,
or taken from the parser result from a previous invocation of Query::parse())
to the new query object. This is fine, since we are going to completely change the
select clause, so a previously existing result set mapping (RSM) is probably wrong anyway.
In the case of using output walkers, we are even creating a new RSM down below.
In the case of using a tree walker, we want to have a new RSM created by the parser.
*/
$countQuery = new Query($this->query->getEntityManager());
$countQuery->setDQL($this->query->getDQL());
$countQuery->setParameters(clone $this->query->getParameters());
$countQuery->setCacheable(false);
foreach ($this->query->getHints() as $name => $value) {
$countQuery->setHint($name, $value);
}
if (! $countQuery->hasHint(CountWalker::HINT_DISTINCT)) {
$countQuery->setHint(CountWalker::HINT_DISTINCT, true);

View File

@@ -75,12 +75,6 @@ class ResolveTargetEntityListener implements EventSubscriber
}
}
foreach ($this->resolveTargetEntities as $interface => $data) {
if ($data['targetEntity'] === $cm->getName()) {
$args->getEntityManager()->getMetadataFactory()->setMetadataFor($interface, $cm);
}
}
foreach ($cm->discriminatorMap as $value => $class) {
if (isset($this->resolveTargetEntities[$class])) {
$cm->addDiscriminatorMapClass($value, $this->resolveTargetEntities[$class]['targetEntity']);

View File

@@ -14,6 +14,7 @@ use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Index\IndexedColumn;
use Doctrine\DBAL\Schema\Name\Identifier;
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
use Doctrine\DBAL\Schema\NamedObject;
use Doctrine\DBAL\Schema\PrimaryKeyConstraint;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\Table;
@@ -38,6 +39,7 @@ use function array_filter;
use function array_flip;
use function array_intersect_key;
use function array_map;
use function array_values;
use function assert;
use function class_exists;
use function count;
@@ -46,6 +48,7 @@ use function implode;
use function in_array;
use function is_numeric;
use function method_exists;
use function preg_match;
use function strtolower;
/**
@@ -735,7 +738,7 @@ class SchemaTool
$theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName);
}
$compositeName = $theJoinTable->getName() . '.' . implode('', $localColumns);
$compositeName = $this->getAssetName($theJoinTable) . '.' . implode('', $localColumns);
if (
isset($addedFks[$compositeName])
&& ($foreignTableName !== $addedFks[$compositeName]['foreignTableName']
@@ -859,15 +862,15 @@ class SchemaTool
$deployedSchema = $this->schemaManager->introspectSchema();
foreach ($schema->getTables() as $table) {
if (! $deployedSchema->hasTable($table->getName())) {
$schema->dropTable($table->getName());
if (! $deployedSchema->hasTable($this->getAssetName($table))) {
$schema->dropTable($this->getAssetName($table));
}
}
if ($this->platform->supportsSequences()) {
foreach ($schema->getSequences() as $sequence) {
if (! $deployedSchema->hasSequence($sequence->getName())) {
$schema->dropSequence($sequence->getName());
if (! $deployedSchema->hasSequence($this->getAssetName($sequence))) {
$schema->dropSequence($this->getAssetName($sequence));
}
}
@@ -889,7 +892,7 @@ class SchemaTool
}
if (count($columns) === 1) {
$checkSequence = $table->getName() . '_' . $columns[0] . '_seq';
$checkSequence = $this->getAssetName($table) . '_' . $columns[0] . '_seq';
if ($deployedSchema->hasSequence($checkSequence) && ! $schema->hasSequence($checkSequence)) {
$schema->createSequence($checkSequence);
}
@@ -955,8 +958,9 @@ class SchemaTool
}
// whitelist assets we already know about in $toSchema, use the existing filter otherwise
$config->setSchemaAssetsFilter(static function ($asset) use ($previousFilter, $toSchema): bool {
$assetName = $asset instanceof AbstractAsset ? $asset->getName() : $asset;
$getAssetName = $this->getAssetName(...);
$config->setSchemaAssetsFilter(static function ($asset) use ($previousFilter, $toSchema, $getAssetName): bool {
$assetName = $asset instanceof AbstractAsset ? $getAssetName($asset) : $asset;
return $toSchema->hasTable($assetName) || $toSchema->hasSequence($assetName) || $previousFilter($asset);
});
@@ -969,20 +973,26 @@ class SchemaTool
}
}
/** @param string[] $primaryKeyColumns */
/** @param non-empty-array<non-empty-string> $primaryKeyColumns */
private function addPrimaryKeyConstraint(Table $table, array $primaryKeyColumns): void
{
if (class_exists(PrimaryKeyConstraint::class)) {
$primaryKeyColumnNames = [];
if (! class_exists(PrimaryKeyConstraint::class)) {
$table->setPrimaryKey(array_values($primaryKeyColumns));
foreach ($primaryKeyColumns as $primaryKeyColumn) {
return;
}
$primaryKeyColumnNames = [];
foreach ($primaryKeyColumns as $primaryKeyColumn) {
if (preg_match('/^"(.+)"$/', $primaryKeyColumn, $matches) === 1) {
$primaryKeyColumnNames[] = new UnqualifiedName(Identifier::quoted($matches[1]));
} else {
$primaryKeyColumnNames[] = new UnqualifiedName(Identifier::unquoted($primaryKeyColumn));
}
$table->addPrimaryKeyConstraint(new PrimaryKeyConstraint(null, $primaryKeyColumnNames, true));
} else {
$table->setPrimaryKey($primaryKeyColumns);
}
$table->addPrimaryKeyConstraint(new PrimaryKeyConstraint(null, $primaryKeyColumnNames, true));
}
/** @return string[] */
@@ -994,4 +1004,12 @@ class SchemaTool
return $index->getColumns();
}
private function getAssetName(AbstractAsset $asset): string
{
return $asset instanceof NamedObject
? $asset->getObjectName()->toString()
// DBAL < 4.4
: $asset->getName();
}
}

View File

@@ -4,11 +4,21 @@ declare(strict_types=1);
namespace Doctrine\ORM\Utility;
use BackedEnum;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Query\QueryException;
use RuntimeException;
use function array_map;
use function array_merge;
use function assert;
use function is_array;
use function is_object;
use function sprintf;
/**
@@ -105,4 +115,121 @@ class PersisterHelper
$class->getName(),
));
}
/**
* Infers field types to be used by parameter type casting.
*
* @return list<ParameterType|int|string>
* @phpstan-return list<ParameterType::*|ArrayParameterType::*|string>
*
* @throws QueryException
*/
public static function inferParameterTypes(
string $field,
mixed $value,
ClassMetadata $class,
EntityManagerInterface $em,
): array {
$types = [];
switch (true) {
case isset($class->fieldMappings[$field]):
$types = array_merge($types, [$class->fieldMappings[$field]->type]);
break;
case isset($class->associationMappings[$field]):
$assoc = $em->getMetadataFactory()->getOwningSide($class->associationMappings[$field]);
$class = $em->getClassMetadata($assoc->targetEntity);
if ($assoc->isManyToManyOwningSide()) {
$columns = $assoc->relationToTargetKeyColumns;
} else {
assert($assoc->isToOneOwningSide());
$columns = $assoc->sourceToTargetKeyColumns;
}
foreach ($columns as $column) {
$types[] = self::getTypeOfColumn($column, $class, $em);
}
break;
default:
$types[] = ParameterType::STRING;
break;
}
if (is_array($value)) {
return array_map(self::getArrayBindingType(...), $types);
}
return $types;
}
/** @phpstan-return ArrayParameterType::* */
private static function getArrayBindingType(ParameterType|int|string $type): ArrayParameterType|int
{
if (! $type instanceof ParameterType) {
$type = Type::getType((string) $type)->getBindingType();
}
return match ($type) {
ParameterType::STRING => ArrayParameterType::STRING,
ParameterType::INTEGER => ArrayParameterType::INTEGER,
ParameterType::ASCII => ArrayParameterType::ASCII,
ParameterType::BINARY => ArrayParameterType::BINARY,
};
}
/**
* Converts a value to the type and value required to bind it as a parameter.
*
* @return list<mixed>
*/
public static function convertToParameterValue(mixed $value, EntityManagerInterface $em): array
{
if (is_array($value)) {
$newValue = [];
foreach ($value as $itemValue) {
$newValue = array_merge($newValue, self::convertToParameterValue($itemValue, $em));
}
return [$newValue];
}
return self::convertIndividualValue($value, $em);
}
/** @phpstan-return list<mixed> */
private static function convertIndividualValue(mixed $value, EntityManagerInterface $em): array
{
if (! is_object($value)) {
return [$value];
}
if ($value instanceof BackedEnum) {
return [$value->value];
}
$valueClass = DefaultProxyClassNameResolver::getClass($value);
if ($em->getMetadataFactory()->isTransient($valueClass)) {
return [$value];
}
$class = $em->getClassMetadata($valueClass);
if ($class->isIdentifierComposite) {
$newValue = [];
foreach ($class->getIdentifierValues($value) as $innerValue) {
$newValue = array_merge($newValue, self::convertToParameterValue($innerValue, $em));
}
return $newValue;
}
return [$em->getUnitOfWork()->getSingleIdentifierValue($value)];
}
}

View File

@@ -11,7 +11,7 @@ use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH12063 extends OrmFunctionalTestCase
class GH12063Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
@@ -67,7 +67,7 @@ enum GH12063Code: string
class GH12063Association
{
#[Id]
#[Column]
#[Column(length: 3)]
public GH12063Code $code;
}
@@ -80,6 +80,6 @@ class GH12063Entity
public int|null $id = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(referencedColumnName: 'code')]
#[ORM\JoinColumn(referencedColumnName: 'code', options: ['length' => 3])]
public GH12063Association $association;
}

View File

@@ -13,6 +13,8 @@ class CmsUserDTONamedArgs
public int|null $phonenumbers = null,
public CmsAddressDTO|null $addressDto = null,
public CmsAddressDTONamedArgs|null $addressDtoNamedArgs = null,
public CmsDumbDTO|null $dumb = null,
public CmsAddress|null $addressEntity = null,
) {
}
}

View File

@@ -10,12 +10,14 @@ class CmsUserDTOVariadicArg
public string|null $email = null;
public string|null $address = null;
public int|null $phonenumbers = null;
public array $otherProperties = [];
public function __construct(...$args)
{
$this->name = $args['name'] ?? null;
$this->email = $args['email'] ?? null;
$this->phonenumbers = $args['phonenumbers'] ?? null;
$this->address = $args['address'] ?? null;
$this->name = $args['name'] ?? null;
$this->email = $args['email'] ?? null;
$this->phonenumbers = $args['phonenumbers'] ?? null;
$this->address = $args['address'] ?? null;
$this->otherProperties = $args;
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Enums;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\ManyToMany;
#[Entity]
class BookCategory
{
#[Id]
#[Column]
#[GeneratedValue]
public int $id;
#[ManyToMany(targetEntity: BookWithGenre::class, mappedBy: 'categories')]
public Collection $books;
public function __construct()
{
$this->books = new ArrayCollection();
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Enums;
enum BookGenre: string
{
case FICTION = 'fiction';
case NON_FICTION = 'non fiction';
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Enums;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\ManyToMany;
use Doctrine\ORM\Mapping\ManyToOne;
#[Entity]
class BookWithGenre
{
#[Id]
#[GeneratedValue]
#[Column]
public int $id;
#[ManyToOne(targetEntity: Library::class, inversedBy: 'books')]
public Library $library;
#[Column(enumType: BookGenre::class)]
public BookGenre $genre;
#[ManyToMany(targetEntity: BookCategory::class, inversedBy: 'books')]
public Collection $categories;
public function __construct(BookGenre $genre)
{
$this->genre = $genre;
$this->categories = new ArrayCollection();
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Enums;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Mapping\Table;
#[Entity]
#[Table('`library`')]
class Library
{
#[Id]
#[GeneratedValue]
#[Column]
public int $id;
#[OneToMany(targetEntity: BookWithGenre::class, mappedBy: 'library')]
public Collection $books;
public function __construct()
{
$this->books = new ArrayCollection();
}
}

View File

@@ -21,8 +21,12 @@ class InversedManyToManyEntity
#[Id]
public $id1;
/** @var string */
#[Column(type: 'rot13', length: 255, nullable: true)]
public $field = null;
/** @phpstan-var Collection<int, OwningManyToManyEntity> */
#[ManyToMany(targetEntity: 'OwningManyToManyEntity', mappedBy: 'associatedEntities')]
#[ManyToMany(targetEntity: OwningManyToManyEntity::class, mappedBy: 'associatedEntities')]
public $associatedEntities;
public function __construct()

View File

@@ -26,7 +26,7 @@ class InversedOneToManyEntity
public $associatedEntities;
/** @var string */
#[Column(type: 'string', name: 'some_property', length: 255)]
#[Column(type: 'string', name: 'some_property', length: 255, nullable: true)]
public $someProperty;
public function __construct()

View File

@@ -24,6 +24,10 @@ class OwningManyToManyEntity
#[Id]
public $id2;
/** @var string */
#[Column(type: 'rot13', length: 255, nullable: true)]
public $field = null;
/** @var Collection<int, InversedManyToManyEntity> */
#[JoinTable(name: 'vct_xref_manytomany')]
#[JoinColumn(name: 'owning_id', referencedColumnName: 'id2')]

View File

@@ -20,8 +20,12 @@ class OwningManyToOneEntity
#[Id]
public $id2;
/** @var string */
#[Column(type: 'rot13', length: 255, nullable: true)]
public $field = null;
/** @var InversedOneToManyEntity */
#[ManyToOne(targetEntity: 'InversedOneToManyEntity', inversedBy: 'associatedEntities')]
#[ManyToOne(targetEntity: InversedOneToManyEntity::class, inversedBy: 'associatedEntities')]
#[JoinColumn(name: 'associated_id', referencedColumnName: 'id1')]
public $associatedEntity;
}

View File

@@ -141,7 +141,7 @@ abstract class EntityPersisterTestCase extends OrmTestCase
public function testInvokeExpandCriteriaParameters(): void
{
$persister = $this->createPersisterDefault();
$criteria = new Criteria();
$criteria = Criteria::create(true);
$this->entityPersister->expects(self::once())
->method('expandCriteriaParameters')
@@ -320,7 +320,7 @@ abstract class EntityPersisterTestCase extends OrmTestCase
$rsm = new ResultSetMappingBuilder($this->em);
$persister = $this->createPersisterDefault();
$entity = new Country('Foo');
$criteria = new Criteria();
$criteria = Criteria::create(true);
$this->em->getUnitOfWork()->registerManaged($entity, ['id' => 1], ['id' => 1, 'name' => 'Foo']);
$rsm->addEntityResult(Country::class, 'c');

View File

@@ -18,8 +18,8 @@ use Doctrine\ORM\Proxy\ProxyFactory;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use Doctrine\Tests\Models\DDC753\DDC753CustomRepository;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
use PHPUnit\Framework\Attributes\RequiresPhp;
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemPoolInterface;
@@ -39,7 +39,7 @@ class ConfigurationTest extends TestCase
$this->configuration = new Configuration();
}
#[WithoutErrorHandler]
#[IgnoreDeprecations]
public function testSetGetProxyDir(): void
{
self::assertNull($this->configuration->getProxyDir()); // defaults
@@ -48,7 +48,7 @@ class ConfigurationTest extends TestCase
self::assertSame(__DIR__, $this->configuration->getProxyDir());
}
#[WithoutErrorHandler]
#[IgnoreDeprecations]
public function testSetGetAutoGenerateProxyClasses(): void
{
self::assertSame(ProxyFactory::AUTOGENERATE_ALWAYS, $this->configuration->getAutoGenerateProxyClasses()); // defaults
@@ -63,7 +63,7 @@ class ConfigurationTest extends TestCase
self::assertSame(ProxyFactory::AUTOGENERATE_FILE_NOT_EXISTS, $this->configuration->getAutoGenerateProxyClasses());
}
#[WithoutErrorHandler]
#[IgnoreDeprecations]
public function testSetGetProxyNamespace(): void
{
self::assertNull($this->configuration->getProxyNamespace()); // defaults
@@ -222,7 +222,7 @@ class ConfigurationTest extends TestCase
}
#[RequiresPhp('8.4')]
#[WithoutErrorHandler]
#[IgnoreDeprecations]
public function testDisablingNativeLazyObjectsIsDeprecated(): void
{
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/12005');

View File

@@ -494,13 +494,13 @@ class ClassTableInheritanceTest extends OrmFunctionalTestCase
$this->_em->flush();
$repository = $this->_em->getRepository(CompanyEmployee::class);
$users = $repository->matching(new Criteria(
$users = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->eq('department', 'IT'),
));
self::assertCount(1, $users);
$repository = $this->_em->getRepository(CompanyManager::class);
$users = $repository->matching(new Criteria(
$users = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->eq('department', 'IT'),
));
self::assertCount(1, $users);

View File

@@ -7,10 +7,11 @@ namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Tests\Models\EagerFetchedCompositeOneToMany\RootEntity;
use Doctrine\Tests\Models\EagerFetchedCompositeOneToMany\SecondLevel;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;
final class EagerFetchOneToManyWithCompositeKeyTest extends OrmFunctionalTestCase
{
/** @ticket 11154 */
#[Group('GH11154')]
public function testItDoesNotThrowAnExceptionWhenTriggeringALoad(): void
{
$this->setUpEntitySchema([RootEntity::class, SecondLevel::class]);

View File

@@ -66,7 +66,7 @@ class EntityRepositoryCriteriaTest extends OrmFunctionalTestCase
$this->loadFixture();
$repository = $this->_em->getRepository(DateTimeModel::class);
$dates = $repository->matching(new Criteria(
$dates = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->lte('datetime', new DateTime('today')),
));
@@ -98,7 +98,7 @@ class EntityRepositoryCriteriaTest extends OrmFunctionalTestCase
$this->loadNullFieldFixtures();
$repository = $this->_em->getRepository(DateTimeModel::class);
$dates = $repository->matching(new Criteria(
$dates = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->isNull('time'),
));
@@ -110,7 +110,7 @@ class EntityRepositoryCriteriaTest extends OrmFunctionalTestCase
$this->loadNullFieldFixtures();
$repository = $this->_em->getRepository(DateTimeModel::class);
$dates = $repository->matching(new Criteria(
$dates = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->eq('time', null),
));
@@ -122,7 +122,7 @@ class EntityRepositoryCriteriaTest extends OrmFunctionalTestCase
$this->loadNullFieldFixtures();
$repository = $this->_em->getRepository(DateTimeModel::class);
$dates = $repository->matching(new Criteria(
$dates = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->neq('time', null),
));
@@ -134,14 +134,14 @@ class EntityRepositoryCriteriaTest extends OrmFunctionalTestCase
$this->loadFixture();
$repository = $this->_em->getRepository(DateTimeModel::class);
$dates = $repository->matching(new Criteria());
$dates = $repository->matching(Criteria::create(true));
self::assertFalse($dates->isInitialized());
self::assertCount(3, $dates);
self::assertFalse($dates->isInitialized());
// Test it can work even with a constraint
$dates = $repository->matching(new Criteria(
$dates = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->lte('datetime', new DateTime('today')),
));
@@ -169,7 +169,7 @@ class EntityRepositoryCriteriaTest extends OrmFunctionalTestCase
$this->_em->clear();
$criteria = new Criteria();
$criteria = Criteria::create(true);
$criteria->andWhere($criteria->expr()->contains('content', 'Criteria'));
$user = $this->_em->find(User::class, $user->id);

View File

@@ -661,7 +661,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
$this->loadFixture();
$repository = $this->_em->getRepository(CmsUser::class);
$users = $repository->matching(new Criteria());
$users = $repository->matching(Criteria::create(true));
self::assertCount(4, $users);
}
@@ -672,7 +672,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
$this->loadFixture();
$repository = $this->_em->getRepository(CmsUser::class);
$users = $repository->matching(new Criteria(
$users = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->eq('username', 'beberlei'),
));
@@ -685,7 +685,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
$this->loadFixture();
$repository = $this->_em->getRepository(CmsUser::class);
$users = $repository->matching(new Criteria(
$users = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->neq('username', 'beberlei'),
));
@@ -698,7 +698,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
$this->loadFixture();
$repository = $this->_em->getRepository(CmsUser::class);
$users = $repository->matching(new Criteria(
$users = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->in('username', ['beberlei', 'gblanco']),
));
@@ -711,7 +711,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
$this->loadFixture();
$repository = $this->_em->getRepository(CmsUser::class);
$users = $repository->matching(new Criteria(
$users = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->notIn('username', ['beberlei', 'gblanco', 'asm89']),
));
@@ -724,7 +724,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
$firstUserId = $this->loadFixture();
$repository = $this->_em->getRepository(CmsUser::class);
$users = $repository->matching(new Criteria(
$users = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->lt('id', $firstUserId + 1),
));
@@ -737,7 +737,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
$firstUserId = $this->loadFixture();
$repository = $this->_em->getRepository(CmsUser::class);
$users = $repository->matching(new Criteria(
$users = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->lte('id', $firstUserId + 1),
));
@@ -750,7 +750,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
$firstUserId = $this->loadFixture();
$repository = $this->_em->getRepository(CmsUser::class);
$users = $repository->matching(new Criteria(
$users = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->gt('id', $firstUserId),
));
@@ -763,7 +763,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
$firstUserId = $this->loadFixture();
$repository = $this->_em->getRepository(CmsUser::class);
$users = $repository->matching(new Criteria(
$users = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->gte('id', $firstUserId),
));
@@ -777,7 +777,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
$user = $this->_em->find(CmsUser::class, $userId);
$criteria = new Criteria(
$criteria = Criteria::create(true)->where(
Criteria::expr()->eq('user', $user),
);
@@ -798,7 +798,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
$user = $this->_em->find(CmsUser::class, $userId);
$criteria = new Criteria(
$criteria = Criteria::create(true)->where(
Criteria::expr()->in('user', [$user]),
);
@@ -818,13 +818,13 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
$repository = $this->_em->getRepository(CmsUser::class);
$users = $repository->matching(new Criteria(Criteria::expr()->contains('name', 'Foobar')));
$users = $repository->matching(Criteria::create(true)->where(Criteria::expr()->contains('name', 'Foobar')));
self::assertCount(0, $users);
$users = $repository->matching(new Criteria(Criteria::expr()->contains('name', 'Rom')));
$users = $repository->matching(Criteria::create(true)->where(Criteria::expr()->contains('name', 'Rom')));
self::assertCount(1, $users);
$users = $repository->matching(new Criteria(Criteria::expr()->contains('status', 'dev')));
$users = $repository->matching(Criteria::create(true)->where(Criteria::expr()->contains('status', 'dev')));
self::assertCount(2, $users);
}
@@ -834,13 +834,19 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
$repository = $this->_em->getRepository(CmsUser::class);
$users = $repository->matching(new Criteria(Criteria::expr()->startsWith('name', 'Foo')));
$users = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->startsWith('name', 'Foo'),
));
self::assertCount(0, $users);
$users = $repository->matching(new Criteria(Criteria::expr()->startsWith('name', 'R')));
$users = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->startsWith('name', 'R'),
));
self::assertCount(1, $users);
$users = $repository->matching(new Criteria(Criteria::expr()->startsWith('status', 'de')));
$users = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->startsWith('status', 'de'),
));
self::assertCount(2, $users);
}
@@ -850,13 +856,19 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
$repository = $this->_em->getRepository(CmsUser::class);
$users = $repository->matching(new Criteria(Criteria::expr()->endsWith('name', 'foo')));
$users = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->endsWith('name', 'foo'),
));
self::assertCount(0, $users);
$users = $repository->matching(new Criteria(Criteria::expr()->endsWith('name', 'oman')));
$users = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->endsWith('name', 'oman'),
));
self::assertCount(1, $users);
$users = $repository->matching(new Criteria(Criteria::expr()->endsWith('status', 'ev')));
$users = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->endsWith('status', 'ev'),
));
self::assertCount(2, $users);
}
@@ -866,8 +878,8 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
$fixtures = $this->loadFixtureUserEmail();
$user = $this->_em->find(CmsUser::class, $fixtures[0]->id);
$repository = $this->_em->getRepository(CmsUser::class);
$criteriaIsNull = Criteria::create()->where(Criteria::expr()->isNull('email'));
$criteriaEqNull = Criteria::create()->where(Criteria::expr()->eq('email', null));
$criteriaIsNull = Criteria::create(true)->where(Criteria::expr()->isNull('email'));
$criteriaEqNull = Criteria::create(true)->where(Criteria::expr()->eq('email', null));
$user->setEmail(null);
$this->_em->persist($user);
@@ -924,7 +936,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
$this->expectExceptionMessage('Unrecognized field: ');
$repository = $this->_em->getRepository(CmsUser::class);
$result = $repository->matching(new Criteria(
$result = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->eq('username = ?; DELETE FROM cms_users; SELECT 1 WHERE 1', 'beberlei'),
));

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Expr\Comparison;
use Doctrine\DBAL\Types\EnumType;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\Mapping\Column;
@@ -13,10 +15,14 @@ use Doctrine\ORM\Query\Expr\Func;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\Tests\Models\DataTransferObjects\DtoWithArrayOfEnums;
use Doctrine\Tests\Models\DataTransferObjects\DtoWithEnum;
use Doctrine\Tests\Models\Enums\BookCategory;
use Doctrine\Tests\Models\Enums\BookGenre;
use Doctrine\Tests\Models\Enums\BookWithGenre;
use Doctrine\Tests\Models\Enums\Card;
use Doctrine\Tests\Models\Enums\CardNativeEnum;
use Doctrine\Tests\Models\Enums\CardWithDefault;
use Doctrine\Tests\Models\Enums\CardWithNullable;
use Doctrine\Tests\Models\Enums\Library;
use Doctrine\Tests\Models\Enums\Product;
use Doctrine\Tests\Models\Enums\Quantity;
use Doctrine\Tests\Models\Enums\Scale;
@@ -25,6 +31,7 @@ use Doctrine\Tests\Models\Enums\TypedCard;
use Doctrine\Tests\Models\Enums\TypedCardNativeEnum;
use Doctrine\Tests\Models\Enums\Unit;
use Doctrine\Tests\OrmFunctionalTestCase;
use Generator;
use PHPUnit\Framework\Attributes\DataProvider;
use function class_exists;
@@ -527,4 +534,68 @@ EXCEPTION
self::assertSame(Suit::Hearts, $card->suit);
}
#[DataProvider('provideGenreMatchingExpressions')]
public function testEnumCollectionMatchingOnOneToMany(Comparison $comparison): void
{
$this->setUpEntitySchema([BookWithGenre::class, Library::class, BookCategory::class]);
$library = new Library();
$fictionBook = new BookWithGenre(BookGenre::FICTION);
$fictionBook->library = $library;
$nonfictionBook = new BookWithGenre(BookGenre::NON_FICTION);
$nonfictionBook->library = $library;
$this->_em->persist($library);
$this->_em->persist($nonfictionBook);
$this->_em->persist($fictionBook);
$this->_em->flush();
$this->_em->clear();
$library = $this->_em->find(Library::class, $library->id);
self::assertFalse($library->books->isInitialized(), 'Pre-condition: lazy collection');
$result = $library->books->matching(Criteria::create(true)->where($comparison));
self::assertCount(1, $result);
self::assertSame($nonfictionBook->id, $result[0]->id);
}
#[DataProvider('provideGenreMatchingExpressions')]
public function testEnumCollectionMatchingOnManyToMany(Comparison $comparison): void
{
$this->setUpEntitySchema([Library::class, BookWithGenre::class, BookCategory::class]);
$category = new BookCategory();
$fictionBook = new BookWithGenre(BookGenre::FICTION);
$fictionBook->categories->add($category);
$nonfictionBook = new BookWithGenre(BookGenre::NON_FICTION);
$nonfictionBook->categories->add($category);
$this->_em->persist($category);
$this->_em->persist($nonfictionBook);
$this->_em->persist($fictionBook);
$this->_em->flush();
$this->_em->clear();
$category = $this->_em->find(BookCategory::class, $category->id);
self::assertFalse($category->books->isInitialized(), 'Pre-condition: lazy collection');
$result = $category->books->matching(Criteria::create(true)->where($comparison));
self::assertCount(1, $result);
self::assertSame($nonfictionBook->id, $result[0]->id);
}
public static function provideGenreMatchingExpressions(): Generator
{
yield [Criteria::expr()->eq('genre', BookGenre::NON_FICTION)];
yield [Criteria::expr()->in('genre', [BookGenre::NON_FICTION])];
}
}

View File

@@ -18,6 +18,7 @@ use PHPUnit\Framework\Attributes\Group;
use function assert;
use function class_exists;
use function get_class;
/**
* Basic many-to-many association tests.
@@ -436,7 +437,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
$user = $this->_em->find($user::class, $user->id);
$criteria = Criteria::create()
$criteria = Criteria::create(true)
->orderBy(['name' => class_exists(Order::class) ? Order::Ascending : Criteria::ASC]);
self::assertEquals(
@@ -476,7 +477,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
$user = $this->_em->find($user::class, $user->id);
$criteria = Criteria::create()
$criteria = Criteria::create(true)
->orderBy(['name' => class_exists(Order::class) ? Order::Ascending : Criteria::ASC]);
self::assertEquals(
@@ -499,7 +500,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
$groups = $user->groups;
self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection');
$criteria = Criteria::create()->setMaxResults(1);
$criteria = Criteria::create(true)->setMaxResults(1);
$result = $groups->matching($criteria);
self::assertCount(1, $result);
@@ -517,7 +518,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
$groups = $user->groups;
self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection');
$criteria = Criteria::create()->setFirstResult(1);
$criteria = Criteria::create(true)->setFirstResult(1);
$result = $groups->matching($criteria);
self::assertCount(1, $result);
@@ -538,7 +539,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
$groups = $user->groups;
self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection');
$criteria = Criteria::create()->setFirstResult(1)->setMaxResults(3);
$criteria = Criteria::create(true)->setFirstResult(1)->setMaxResults(3);
$result = $groups->matching($criteria);
self::assertCount(3, $result);
@@ -562,7 +563,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
$groups = $user->groups;
self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection');
$criteria = Criteria::create()->where(Criteria::expr()->eq('name', (string) 'Developers_0'));
$criteria = Criteria::create(true)->where(Criteria::expr()->eq('name', (string) 'Developers_0'));
$result = $groups->matching($criteria);
self::assertCount(1, $result);
@@ -573,6 +574,44 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
self::assertFalse($user->groups->isInitialized(), 'Post-condition: matching does not initialize collection');
}
public function testMatchingWithInCondition(): void
{
$user = $this->addCmsUserGblancoWithGroups(2);
$this->_em->clear();
$user = $this->_em->find(get_class($user), $user->id);
$groups = $user->groups;
self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection');
$criteria = Criteria::create(true)->where(Criteria::expr()->in('name', ['Developers_1']));
$result = $groups->matching($criteria);
self::assertCount(1, $result);
self::assertEquals('Developers_1', $result[0]->name);
self::assertFalse($user->groups->isInitialized(), 'Post-condition: matching does not initialize collection');
}
public function testMatchingWithNotInCondition(): void
{
$user = $this->addCmsUserGblancoWithGroups(2);
$this->_em->clear();
$user = $this->_em->find(get_class($user), $user->id);
$groups = $user->groups;
self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection');
$criteria = Criteria::create(true)->where(Criteria::expr()->notIn('name', ['Developers_0']));
$result = $groups->matching($criteria);
self::assertCount(1, $result);
self::assertEquals('Developers_1', $result[0]->name);
self::assertFalse($user->groups->isInitialized(), 'Post-condition: matching does not initialize collection');
}
private function removeTransactionCommandsFromQueryLog(): void
{
$log = $this->getQueryLog();

View File

@@ -1394,6 +1394,168 @@ class NewOperatorTest extends OrmFunctionalTestCase
self::assertSame($this->fixtures[2]->email->email, $result[2]->val2->val2);
}
public function testOnlyObjectInNamedDto(): void
{
$dql = '
SELECT
new named CmsUserDTOVariadicArg(
a,
new CmsDumbDTO(
u.name,
e.email
) as dumb
)
FROM
Doctrine\Tests\Models\CMS\CmsUser u
LEFT JOIN
u.email e
LEFT JOIN
u.address a
ORDER BY
u.name';
$query = $this->getEntityManager()->createQuery($dql);
$result = $query->getResult();
self::assertCount(3, $result);
self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[0]);
self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[1]);
self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[2]);
self::assertInstanceOf(CmsAddress::class, $result[0]->otherProperties['a']);
self::assertInstanceOf(CmsAddress::class, $result[1]->otherProperties['a']);
self::assertInstanceOf(CmsAddress::class, $result[2]->otherProperties['a']);
self::assertSame($this->fixtures[0]->address->city, $result[0]->otherProperties['a']->city);
self::assertSame($this->fixtures[1]->address->city, $result[1]->otherProperties['a']->city);
self::assertSame($this->fixtures[2]->address->city, $result[2]->otherProperties['a']->city);
self::assertSame($this->fixtures[0]->address->country, $result[0]->otherProperties['a']->country);
self::assertSame($this->fixtures[1]->address->country, $result[1]->otherProperties['a']->country);
self::assertSame($this->fixtures[2]->address->country, $result[2]->otherProperties['a']->country);
self::assertInstanceOf(CmsDumbDTO::class, $result[0]->otherProperties['dumb']);
self::assertInstanceOf(CmsDumbDTO::class, $result[1]->otherProperties['dumb']);
self::assertInstanceOf(CmsDumbDTO::class, $result[2]->otherProperties['dumb']);
self::assertSame($this->fixtures[0]->name, $result[0]->otherProperties['dumb']->val1);
self::assertSame($this->fixtures[1]->name, $result[1]->otherProperties['dumb']->val1);
self::assertSame($this->fixtures[2]->name, $result[2]->otherProperties['dumb']->val1);
self::assertSame($this->fixtures[0]->email->email, $result[0]->otherProperties['dumb']->val2);
self::assertSame($this->fixtures[1]->email->email, $result[1]->otherProperties['dumb']->val2);
self::assertSame($this->fixtures[2]->email->email, $result[2]->otherProperties['dumb']->val2);
}
public function testOnlyObjectInNamedDtoWithAlias(): void
{
$dql = '
SELECT
new named CmsUserDTOVariadicArg(
a as addr,
new CmsDumbDTO(
u.name,
e.email
) as dumb
)
FROM
Doctrine\Tests\Models\CMS\CmsUser u
LEFT JOIN
u.email e
LEFT JOIN
u.address a
ORDER BY
u.name';
$query = $this->getEntityManager()->createQuery($dql);
$result = $query->getResult();
self::assertCount(3, $result);
self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[0]);
self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[1]);
self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[2]);
self::assertInstanceOf(CmsAddress::class, $result[0]->otherProperties['addr']);
self::assertInstanceOf(CmsAddress::class, $result[1]->otherProperties['addr']);
self::assertInstanceOf(CmsAddress::class, $result[2]->otherProperties['addr']);
self::assertSame($this->fixtures[0]->address->city, $result[0]->otherProperties['addr']->city);
self::assertSame($this->fixtures[1]->address->city, $result[1]->otherProperties['addr']->city);
self::assertSame($this->fixtures[2]->address->city, $result[2]->otherProperties['addr']->city);
self::assertSame($this->fixtures[0]->address->country, $result[0]->otherProperties['addr']->country);
self::assertSame($this->fixtures[1]->address->country, $result[1]->otherProperties['addr']->country);
self::assertSame($this->fixtures[2]->address->country, $result[2]->otherProperties['addr']->country);
self::assertInstanceOf(CmsDumbDTO::class, $result[0]->otherProperties['dumb']);
self::assertInstanceOf(CmsDumbDTO::class, $result[1]->otherProperties['dumb']);
self::assertInstanceOf(CmsDumbDTO::class, $result[2]->otherProperties['dumb']);
self::assertSame($this->fixtures[0]->name, $result[0]->otherProperties['dumb']->val1);
self::assertSame($this->fixtures[1]->name, $result[1]->otherProperties['dumb']->val1);
self::assertSame($this->fixtures[2]->name, $result[2]->otherProperties['dumb']->val1);
self::assertSame($this->fixtures[0]->email->email, $result[0]->otherProperties['dumb']->val2);
self::assertSame($this->fixtures[1]->email->email, $result[1]->otherProperties['dumb']->val2);
self::assertSame($this->fixtures[2]->email->email, $result[2]->otherProperties['dumb']->val2);
}
public function testOnlyObjectInNamedDtoWithSameNameAsTheProperties(): void
{
$dql = '
SELECT
new named CmsUserDTONamedArgs(
addressEntity,
new CmsDumbDTO(
u.name,
e.email
) as dumb
)
FROM
Doctrine\Tests\Models\CMS\CmsUser u
LEFT JOIN
u.email e
LEFT JOIN
u.address addressEntity
ORDER BY
u.name';
$query = $this->getEntityManager()->createQuery($dql);
$result = $query->getResult();
self::assertCount(3, $result);
self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[0]);
self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[1]);
self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[2]);
self::assertInstanceOf(CmsAddress::class, $result[0]->addressEntity);
self::assertInstanceOf(CmsAddress::class, $result[1]->addressEntity);
self::assertInstanceOf(CmsAddress::class, $result[2]->addressEntity);
self::assertSame($this->fixtures[0]->address->city, $result[0]->addressEntity->city);
self::assertSame($this->fixtures[1]->address->city, $result[1]->addressEntity->city);
self::assertSame($this->fixtures[2]->address->city, $result[2]->addressEntity->city);
self::assertSame($this->fixtures[0]->address->country, $result[0]->addressEntity->country);
self::assertSame($this->fixtures[1]->address->country, $result[1]->addressEntity->country);
self::assertSame($this->fixtures[2]->address->country, $result[2]->addressEntity->country);
self::assertInstanceOf(CmsDumbDTO::class, $result[0]->dumb);
self::assertInstanceOf(CmsDumbDTO::class, $result[1]->dumb);
self::assertInstanceOf(CmsDumbDTO::class, $result[2]->dumb);
self::assertSame($this->fixtures[0]->name, $result[0]->dumb->val1);
self::assertSame($this->fixtures[1]->name, $result[1]->dumb->val1);
self::assertSame($this->fixtures[2]->name, $result[2]->dumb->val1);
self::assertSame($this->fixtures[0]->email->email, $result[0]->dumb->val2);
self::assertSame($this->fixtures[1]->email->email, $result[1]->dumb->val2);
self::assertSame($this->fixtures[2]->email->email, $result[2]->dumb->val2);
}
public function testNamedArguments(): void
{
$dql = <<<'SQL'

View File

@@ -164,14 +164,14 @@ class OneToManyBidirectionalAssociationTest extends OrmFunctionalTestCase
$product = $this->_em->find(ECommerceProduct::class, $this->product->getId());
$features = $product->getFeatures();
$results = $features->matching(new Criteria(
$results = $features->matching(Criteria::create(true)->where(
Criteria::expr()->eq('description', 'Model writing tutorial'),
));
self::assertInstanceOf(Collection::class, $results);
self::assertCount(1, $results);
$results = $features->matching(new Criteria());
$results = $features->matching(Criteria::create(true));
self::assertInstanceOf(Collection::class, $results);
self::assertCount(2, $results);
@@ -190,7 +190,7 @@ class OneToManyBidirectionalAssociationTest extends OrmFunctionalTestCase
$features = $product->getFeatures();
$features->add($thirdFeature);
$results = $features->matching(new Criteria(
$results = $features->matching(Criteria::create(true)->where(
Criteria::expr()->eq('description', 'Model writing tutorial'),
));
@@ -208,14 +208,14 @@ class OneToManyBidirectionalAssociationTest extends OrmFunctionalTestCase
$thirdFeature->setDescription('Third feature');
$product->addFeature($thirdFeature);
$results = $features->matching(new Criteria(
$results = $features->matching(Criteria::create(true)->where(
Criteria::expr()->eq('description', 'Third feature'),
));
self::assertInstanceOf(Collection::class, $results);
self::assertCount(1, $results);
$results = $features->matching(new Criteria());
$results = $features->matching(Criteria::create(true));
self::assertInstanceOf(Collection::class, $results);
self::assertCount(3, $results);

View File

@@ -14,7 +14,7 @@ use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Tests\OrmFunctionalTestCase;
use Generator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
use ReflectionMethod;
use Symfony\Component\VarExporter\Instantiator;
use Symfony\Component\VarExporter\VarExporter;
@@ -35,7 +35,7 @@ class ParserResultSerializationTest extends OrmFunctionalTestCase
/** @param Closure(ParserResult): ParserResult $toSerializedAndBack */
#[DataProvider('provideToSerializedAndBack')]
#[WithoutErrorHandler]
#[IgnoreDeprecations]
public function testSerializeParserResultForQueryWithSqlWalker(Closure $toSerializedAndBack): void
{
$query = $this->_em
@@ -131,7 +131,6 @@ class ParserResultSerializationTest extends OrmFunctionalTestCase
private static function parseQuery(Query $query): ParserResult
{
$r = new ReflectionMethod($query, 'parse');
$r->setAccessible(true);
return $r->invoke($query);
}

View File

@@ -80,7 +80,7 @@ class PersistentCollectionCriteriaTest extends OrmFunctionalTestCase
$repository = $this->_em->getRepository(User::class);
$user = $repository->findOneBy(['name' => 'ngal']);
$tweets = $user->tweets->matching(new Criteria());
$tweets = $user->tweets->matching(Criteria::create(true));
self::assertInstanceOf(LazyCriteriaCollection::class, $tweets);
self::assertFalse($tweets->isInitialized());
@@ -88,7 +88,7 @@ class PersistentCollectionCriteriaTest extends OrmFunctionalTestCase
self::assertFalse($tweets->isInitialized());
// Make sure it works with constraints
$tweets = $user->tweets->matching(new Criteria(
$tweets = $user->tweets->matching(Criteria::create(true)->where(
Criteria::expr()->eq('content', 'Foo'),
));
@@ -117,7 +117,7 @@ class PersistentCollectionCriteriaTest extends OrmFunctionalTestCase
$parent = $this->_em->find(OwningManyToManyExtraLazyEntity::class, $parent->id2);
$criteria = Criteria::create()->where(Criteria::expr()->eq('id1', 'Bob'));
$criteria = Criteria::create(true)->where(Criteria::expr()->eq('id1', 'Bob'));
$result = $parent->associatedEntities->matching($criteria);

View File

@@ -89,7 +89,7 @@ class PersistentCollectionTest extends OrmFunctionalTestCase
$this->_em->flush();
$this->_em->clear();
$criteria = new Criteria();
$criteria = Criteria::create(true);
$collectionHolder = $this->_em->find(PersistentCollectionHolder::class, $collectionHolder->getId());
$collectionHolder->getCollection()->matching($criteria);

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
use Doctrine\ORM\Query\ParserResult;
@@ -122,15 +123,15 @@ class QueryCacheTest extends OrmFunctionalTestCase
$query = $this->_em->createQuery('select ux from Doctrine\Tests\Models\CMS\CmsUser ux');
$sqlExecMock = $this->getMockBuilder(AbstractSqlExecutor::class)
->getMockForAbstractClass();
$sqlExecMock->expects(self::once())
->method('execute')
->willReturn(10);
$sqlExecutorStub = new class extends AbstractSqlExecutor {
public function execute(Connection $conn, array $params, array $types): int
{
return 10;
}
};
$parserResultMock = new ParserResult();
$parserResultMock->setSqlExecutor($sqlExecMock);
$parserResultMock->setSqlExecutor($sqlExecutorStub);
$cache = $this->createMock(CacheItemPoolInterface::class);

View File

@@ -403,13 +403,130 @@ class QueryTest extends OrmFunctionalTestCase
}
}
public function testToIterableWithMixedResultIsNotAllowed(): void
public function testToIterableWithMixedResultEntityScalars(): void
{
$this->expectException(QueryException::class);
$this->expectExceptionMessage('Iterating a query with mixed results (using scalars) is not supported.');
$author = new CmsUser();
$author->name = 'Ben';
$author->username = 'beberlei';
$query = $this->_em->createQuery('select a, a.topic from ' . CmsArticle::class . ' a');
$query->toIterable();
$article1 = new CmsArticle();
$article1->topic = 'Doctrine 2';
$article1->text = 'This is an introduction to Doctrine 2.';
$article1->setAuthor($author);
$article2 = new CmsArticle();
$article2->topic = 'Symfony 2';
$article2->text = 'This is an introduction to Symfony 2.';
$article2->setAuthor($author);
$article3 = new CmsArticle();
$article3->topic = 'lala 2';
$article3->text = 'This is an introduction to Symfony 2.';
$article3->setAuthor($author);
$this->_em->persist($article1);
$this->_em->persist($article2);
$this->_em->persist($article3);
$this->_em->persist($author);
$this->_em->flush();
$this->_em->clear();
$query = $this->_em->createQuery(
'select a, a.topic, a.text from ' . CmsArticle::class . ' a order by a.id asc',
);
$result = $query->toIterable();
$it = iterator_to_array($result);
$this->assertCount(3, $it);
$this->assertEquals('Doctrine 2', $it[0]['topic']);
$this->assertEquals('Doctrine 2', $it[0][0]->topic);
$this->assertEquals('lala 2', $it[2]['topic']);
$this->assertEquals('lala 2', $it[2][0]->topic);
}
public function testToIterableWithMixedResultArbitraryJoinsScalars(): void
{
$author = new CmsUser();
$author->name = 'Ben';
$author->username = 'beberlei';
$article1 = new CmsArticle();
$article1->topic = 'Doctrine 2';
$article1->text = 'This is an introduction to Doctrine 2.';
$article1->setAuthor($author);
$article2 = new CmsArticle();
$article2->topic = 'Symfony 2';
$article2->text = 'This is an introduction to Symfony 2.';
$article2->setAuthor($author);
$article3 = new CmsArticle();
$article3->topic = 'lala 2';
$article3->text = 'This is an introduction to Symfony 2.';
$article3->setAuthor($author);
$this->_em->persist($article1);
$this->_em->persist($article2);
$this->_em->persist($article3);
$this->_em->persist($author);
$this->_em->flush();
$this->_em->clear();
$query = $this->_em->createQuery('select a, u, a.topic, a.text from ' . CmsArticle::class . ' a, ' . CmsUser::class . ' u WHERE a.user = u ');
$result = $query->toIterable();
$it = iterator_to_array($result);
$this->assertCount(3, $it);
$this->assertEquals('Doctrine 2', $it[0]['topic']);
$this->assertEquals('Doctrine 2', $it[0][0]->topic);
$this->assertEquals('beberlei', $it[0][1]->username);
$this->assertEquals('lala 2', $it[2]['topic']);
$this->assertEquals('lala 2', $it[2][0]->topic);
$this->assertEquals('beberlei', $it[2][1]->username);
}
public function testToIterableWithMixedResultScalarsOnly(): void
{
$author = new CmsUser();
$author->name = 'Ben';
$author->username = 'beberlei';
$article1 = new CmsArticle();
$article1->topic = 'Doctrine 2';
$article1->text = 'This is an introduction to Doctrine 2.';
$article1->setAuthor($author);
$article2 = new CmsArticle();
$article2->topic = 'Symfony 2';
$article2->text = 'This is an introduction to Symfony 2.';
$article2->setAuthor($author);
$article3 = new CmsArticle();
$article3->topic = 'lala 2';
$article3->text = 'This is an introduction to Symfony 2.';
$article3->setAuthor($author);
$this->_em->persist($article1);
$this->_em->persist($article2);
$this->_em->persist($article3);
$this->_em->persist($author);
$this->_em->flush();
$this->_em->clear();
$query = $this->_em->createQuery('select a.topic, a.text from ' . CmsArticle::class . ' a ');
$result = $query->toIterable();
$it = iterator_to_array($result);
$this->assertEquals([
['topic' => 'Doctrine 2', 'text' => 'This is an introduction to Doctrine 2.'],
['topic' => 'Symfony 2', 'text' => 'This is an introduction to Symfony 2.'],
['topic' => 'lala 2', 'text' => 'This is an introduction to Symfony 2.'],
], $it);
}
public function testIterateResultClearEveryCycle(): void

View File

@@ -48,7 +48,6 @@ class SecondLevelCacheCountQueriesTest extends SecondLevelCacheFunctionalTestCas
if ($cacheUsage === 0) {
$metadataCacheReflection = new ReflectionProperty(ClassMetadata::class, 'cache');
$metadataCacheReflection->setAccessible(true);
$metadataCacheReflection->setValue($metadata, null);
return;

View File

@@ -26,7 +26,7 @@ class SecondLevelCacheCriteriaTest extends SecondLevelCacheFunctionalTestCase
$repository = $this->_em->getRepository(Country::class);
$this->getQueryLog()->reset()->enable();
$name = $this->countries[0]->getName();
$result1 = $repository->matching(new Criteria(
$result1 = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->eq('name', $name),
));
@@ -41,7 +41,7 @@ class SecondLevelCacheCriteriaTest extends SecondLevelCacheFunctionalTestCase
$this->_em->clear();
$result2 = $repository->matching(new Criteria(
$result2 = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->eq('name', $name),
));
@@ -65,7 +65,7 @@ class SecondLevelCacheCriteriaTest extends SecondLevelCacheFunctionalTestCase
$repository = $this->_em->getRepository(Country::class);
$this->getQueryLog()->reset()->enable();
$result1 = $repository->matching(new Criteria(
$result1 = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->eq('name', $this->countries[0]->getName()),
));
@@ -79,7 +79,7 @@ class SecondLevelCacheCriteriaTest extends SecondLevelCacheFunctionalTestCase
$this->_em->clear();
$result2 = $repository->matching(new Criteria(
$result2 = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->eq('name', $this->countries[0]->getName()),
));
@@ -94,7 +94,7 @@ class SecondLevelCacheCriteriaTest extends SecondLevelCacheFunctionalTestCase
self::assertEquals($this->countries[0]->getId(), $result2[0]->getId());
self::assertEquals($this->countries[0]->getName(), $result2[0]->getName());
$result3 = $repository->matching(new Criteria(
$result3 = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->eq('name', $this->countries[1]->getName()),
));
@@ -109,7 +109,7 @@ class SecondLevelCacheCriteriaTest extends SecondLevelCacheFunctionalTestCase
self::assertEquals($this->countries[1]->getId(), $result3[0]->getId());
self::assertEquals($this->countries[1]->getName(), $result3[0]->getName());
$result4 = $repository->matching(new Criteria(
$result4 = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->eq('name', $this->countries[1]->getName()),
));
@@ -134,7 +134,7 @@ class SecondLevelCacheCriteriaTest extends SecondLevelCacheFunctionalTestCase
$itemName = $this->states[0]->getCities()->get(0)->getName();
$this->getQueryLog()->reset()->enable();
$collection = $entity->getCities();
$matching = $collection->matching(new Criteria(
$matching = $collection->matching(Criteria::create(true)->where(
Criteria::expr()->eq('name', $itemName),
));
@@ -147,7 +147,7 @@ class SecondLevelCacheCriteriaTest extends SecondLevelCacheFunctionalTestCase
$entity = $this->_em->find(State::class, $this->states[0]->getId());
$this->getQueryLog()->reset()->enable();
$collection = $entity->getCities();
$matching = $collection->matching(new Criteria(
$matching = $collection->matching(Criteria::create(true)->where(
Criteria::expr()->eq('name', $itemName),
));

View File

@@ -354,13 +354,13 @@ class SingleTableInheritanceTest extends OrmFunctionalTestCase
$this->loadFullFixture();
$repository = $this->_em->getRepository(CompanyContract::class);
$contracts = $repository->matching(new Criteria(
$contracts = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->eq('salesPerson', $this->salesPerson),
));
self::assertCount(3, $contracts);
$repository = $this->_em->getRepository(CompanyFixContract::class);
$contracts = $repository->matching(new Criteria(
$contracts = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->eq('salesPerson', $this->salesPerson),
));
self::assertCount(1, $contracts);
@@ -376,7 +376,7 @@ class SingleTableInheritanceTest extends OrmFunctionalTestCase
$this->expectException(MatchingAssociationFieldRequiresObject::class);
$this->expectExceptionMessage('annot match on Doctrine\Tests\Models\Company\CompanyContract::salesPerson with a non-object value.');
$contracts = $repository->matching(new Criteria(
$contracts = $repository->matching(Criteria::create(true)->where(
Criteria::expr()->eq('salesPerson', $this->salesPerson->getId()),
));

View File

@@ -39,7 +39,7 @@ class DDC2106Test extends OrmFunctionalTestCase
$entityWithoutId = new DDC2106Entity();
$this->_em->persist($entityWithoutId);
$criteria = Criteria::create()->where(Criteria::expr()->eq('parent', $entityWithoutId));
$criteria = Criteria::create(true)->where(Criteria::expr()->eq('parent', $entityWithoutId));
self::assertCount(0, $entity->children->matching($criteria));
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\ForeignKeyConstraintEditor;
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
@@ -20,9 +21,9 @@ use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Mapping\Table;
use Doctrine\Tests\ORM\Functional\Ticket\Doctrine\Common\Collections\Collection;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
use function array_map;
use function assert;
@@ -31,10 +32,15 @@ use function reset;
class DDC2138Test extends OrmFunctionalTestCase
{
/**
* With this test, we will create the same foreign key twice which is fine, but we will tap a deprecation
* in DBAL 4.4. This has to be fixed during the validation of metadata. For now, we will simply ignore that
* deprecation.
*/
#[Group('DDC-2138')]
#[IgnoreDeprecations]
public function testForeignKeyOnSTIWithMultipleMapping(): void
{
$em = $this->_em;
$schema = $this->getSchemaForModels(
DDC2138User::class,
DDC2138Structure::class,
@@ -86,15 +92,13 @@ class DDC2138Test extends OrmFunctionalTestCase
#[Entity]
class DDC2138Structure
{
/** @var int */
#[Id]
#[Column(type: 'integer')]
#[Column]
#[GeneratedValue(strategy: 'AUTO')]
protected $id;
protected int|null $id = null;
/** @var string */
#[Column(type: 'string', length: 32, nullable: true)]
protected $name;
#[Column(length: 32, nullable: true)]
protected string|null $name = null;
}
#[Table(name: 'users_followed_objects')]
@@ -104,19 +108,10 @@ class DDC2138Structure
#[DiscriminatorMap([4 => 'DDC2138UserFollowedUser', 3 => 'DDC2138UserFollowedStructure'])]
abstract class DDC2138UserFollowedObject
{
/** @var int $id */
#[Column(name: 'id', type: 'integer')]
#[Id]
#[GeneratedValue(strategy: 'AUTO')]
protected $id;
/**
* Get id
*/
public function getId(): int
{
return $this->id;
}
public int|null $id = null;
}
#[Entity]
@@ -126,27 +121,14 @@ class DDC2138UserFollowedStructure extends DDC2138UserFollowedObject
* Construct a UserFollowedStructure entity
*/
public function __construct(
#[ManyToOne(targetEntity: 'DDC2138User', inversedBy: 'followedStructures')]
#[ManyToOne(inversedBy: 'followedStructures')]
#[JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: false)]
protected User $user,
#[ManyToOne(targetEntity: 'DDC2138Structure')]
public DDC2138User $user,
#[ManyToOne]
#[JoinColumn(name: 'object_id', referencedColumnName: 'id', nullable: false)]
private Structure $followedStructure,
public DDC2138Structure $followedStructure,
) {
}
public function getUser(): User
{
return $this->user;
}
/**
* Gets followed structure
*/
public function getFollowedStructure(): Structure
{
return $this->followedStructure;
}
}
#[Entity]
@@ -156,92 +138,39 @@ class DDC2138UserFollowedUser extends DDC2138UserFollowedObject
* Construct a UserFollowedUser entity
*/
public function __construct(
#[ManyToOne(targetEntity: 'DDC2138User', inversedBy: 'followedUsers')]
#[ManyToOne(inversedBy: 'followedUsers')]
#[JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: false)]
protected User $user,
#[ManyToOne(targetEntity: 'DDC2138User')]
public DDC2138User $user,
#[ManyToOne]
#[JoinColumn(name: 'object_id', referencedColumnName: 'id', nullable: false)]
private User $followedUser,
public DDC2138User $followedUser,
) {
}
public function getUser(): User
{
return $this->user;
}
/**
* Gets followed user
*/
public function getFollowedUser(): User
{
return $this->followedUser;
}
}
#[Table(name: 'users')]
#[Entity]
class DDC2138User
{
/** @var int */
#[Id]
#[Column(type: 'integer')]
#[Column]
#[GeneratedValue(strategy: 'AUTO')]
protected $id;
public int|null $id = null;
/** @var string */
#[Column(type: 'string', length: 32, nullable: true)]
protected $name;
#[Column(length: 32, nullable: true)]
public string|null $name = null;
/** @var ArrayCollection $followedUsers */
#[OneToMany(targetEntity: 'DDC2138UserFollowedUser', mappedBy: 'user', cascade: ['persist'], orphanRemoval: true)]
protected $followedUsers;
/** @var Collection<int, DDC2138UserFollowedUser> */
#[OneToMany(targetEntity: DDC2138UserFollowedUser::class, mappedBy: 'user', cascade: ['persist'], orphanRemoval: true)]
public Collection $followedUsers;
/** @var ArrayCollection $followedStructures */
#[OneToMany(targetEntity: 'DDC2138UserFollowedStructure', mappedBy: 'user', cascade: ['persist'], orphanRemoval: true)]
protected $followedStructures;
/** @var Collection<int, DDC2138UserFollowedStructure> */
#[OneToMany(targetEntity: DDC2138UserFollowedStructure::class, mappedBy: 'user', cascade: ['persist'], orphanRemoval: true)]
public Collection $followedStructures;
public function __construct()
{
$this->followedUsers = new ArrayCollection();
$this->followedStructures = new ArrayCollection();
}
public function addFollowedUser(UserFollowedUser $followedUsers): User
{
$this->followedUsers[] = $followedUsers;
return $this;
}
public function removeFollowedUser(UserFollowedUser $followedUsers): User
{
$this->followedUsers->removeElement($followedUsers);
return $this;
}
public function getFollowedUsers(): Collection
{
return $this->followedUsers;
}
public function addFollowedStructure(UserFollowedStructure $followedStructures): User
{
$this->followedStructures[] = $followedStructures;
return $this;
}
public function removeFollowedStructure(UserFollowedStructure $followedStructures): User
{
$this->followedStructures->removeElement($followedStructures);
return $this;
}
public function getFollowedStructures(): Collection
{
return $this->followedStructures;
}
}

View File

@@ -11,6 +11,8 @@ use Doctrine\ORM\Mapping\Id;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;
use function substr_count;
#[Group('DDC-3042')]
class DDC3042Test extends OrmFunctionalTestCase
{
@@ -23,14 +25,18 @@ class DDC3042Test extends OrmFunctionalTestCase
public function testSQLGenerationDoesNotProvokeAliasCollisions(): void
{
self::assertStringNotMatchesFormat(
'%sfield11%sfield11%s',
$this
self::assertSame(
1,
substr_count(
$this
->_em
->createQuery(
'SELECT f, b FROM ' . __NAMESPACE__ . '\DDC3042Foo f JOIN ' . __NAMESPACE__ . '\DDC3042Bar b WITH 1 = 1',
)
->getSQL(),
'field_11',
),
'The alias "field11" should only appear once in the SQL query.',
);
}
}

View File

@@ -44,7 +44,7 @@ class DDC3719Test extends OrmFunctionalTestCase
$contracts = $manager->managedContracts;
self::assertCount(2, $contracts);
$criteria = Criteria::create();
$criteria = Criteria::create(true);
$criteria->where(Criteria::expr()->eq('completed', true));
$completedContracts = $contracts->matching($criteria);

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH10049;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\DoesNotPerformAssertions;
class GH10049Test extends OrmFunctionalTestCase
{
@@ -18,7 +19,7 @@ class GH10049Test extends OrmFunctionalTestCase
);
}
/** @doesNotPerformAssertions */
#[DoesNotPerformAssertions]
public function testInheritedReadOnlyPropertyValueCanBeSet(): void
{
$child = new ReadOnlyPropertyInheritor(10049);

View File

@@ -30,15 +30,6 @@ class GH10450Test extends OrmTestCase
yield 'Entity class that redeclares a protected field inherited from a base entity' => [GH10450EntityChildProtected::class];
yield 'Entity class that redeclares a protected field inherited from a mapped superclass' => [GH10450MappedSuperclassChildProtected::class];
}
public function testFieldsOfTransientClassesAreNotConsideredDuplicate(): void
{
$em = $this->getTestEntityManager();
$metadata = $em->getClassMetadata(GH10450Cat::class);
self::assertArrayHasKey('id', $metadata->fieldMappings);
}
}
#[ORM\Entity]
@@ -122,26 +113,3 @@ class GH10450MappedSuperclassChildProtected extends GH10450BaseMappedSuperclassP
#[ORM\Column(type: 'text', name: 'child')]
protected string $field;
}
abstract class GH10450AbstractEntity
{
#[ORM\Column(type: 'integer')]
#[ORM\Id]
#[ORM\GeneratedValue]
protected int $id;
}
#[ORM\Entity]
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\DiscriminatorMap(['cat' => GH10450Cat::class])]
#[ORM\DiscriminatorColumn(name: 'type')]
abstract class GH10450Animal extends GH10450AbstractEntity
{
#[ORM\Column(type: 'text', name: 'base')]
private string $field;
}
#[ORM\Entity]
class GH10450Cat extends GH10450Animal
{
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Tools\ResolveTargetEntityListener;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH12174Test extends OrmFunctionalTestCase
{
use VerifyDeprecations;
protected function setUp(): void
{
parent::setUp();
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/10431'); // make test fail on 2.x; in 3.x, it would even throw
$resolveTargetEntity = new ResolveTargetEntityListener();
$resolveTargetEntity->addResolveTargetEntity(GH12174Smurf::class, GH12174BlueSmurf::class, []);
$this->_em->getEventManager()->addEventSubscriber($resolveTargetEntity);
$this->createSchemaForModels(
GH12174Smurf::class,
GH12174BlueSmurf::class,
GH12174PapaSmurf::class,
);
}
public function testMappedSuperclassNameCanBeUsedToResolveTargetEntityClass(): void
{
$smurf = $this->_em->getClassMetadata(GH12174Smurf::class);
self::assertTrue($smurf->isMappedSuperclass);
self::assertSame(GH12174Smurf::class, $smurf->getName());
self::assertSame(GH12174BlueSmurf::class, $smurf->getAssociationMapping('children')['targetEntity']);
$blue = $this->_em->getClassMetadata(GH12174BlueSmurf::class);
self::assertFalse($blue->isMappedSuperclass);
self::assertSame(GH12174BlueSmurf::class, $blue->getName());
$papa = $this->_em->getClassMetadata(GH12174PapaSmurf::class);
self::assertFalse($papa->isMappedSuperclass);
self::assertSame(GH12174PapaSmurf::class, $papa->getName());
}
}
#[ORM\MappedSuperclass]
class GH12174Smurf
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
public int $id;
#[ManyToOne(inversedBy: 'children', targetEntity: self::class)]
private GH12174Smurf $parent;
/** @var Collection<self::class> */
#[OneToMany(targetEntity: self::class, mappedBy: 'parent')]
private Collection $children;
}
#[ORM\Entity]
class GH12174BlueSmurf extends GH12174Smurf
{
}
#[ORM\Entity]
class GH12174PapaSmurf extends GH12174Smurf
{
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Doctrine\Tests\Models\CMS\CmsArticle;
use Doctrine\Tests\OrmFunctionalTestCase;
final class GH12183Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
$this->useModelSet('cms');
parent::setUp();
$article = new CmsArticle();
$article->topic = 'Loomings';
$article->text = 'Call me Ishmael.';
$this->_em->persist($article);
$this->_em->flush();
$this->_em->clear();
}
public function testPaginatorCountWithTreeWalkerAfterQueryHasBeenExecuted(): void
{
$query = $this->_em->createQuery('SELECT a FROM Doctrine\Tests\Models\CMS\CmsArticle a');
// Paginator::count is right when the query has not yet been executed
$paginator = new Paginator($query);
$paginator->setUseOutputWalkers(false);
self::assertSame(1, $paginator->count());
// Execute the query
$result = $query->getResult();
self::assertCount(1, $result);
$paginator = new Paginator($query);
$paginator->setUseOutputWalkers(false);
self::assertSame(1, $paginator->count());
}
}

View File

@@ -49,7 +49,7 @@ final class GH6740Test extends OrmFunctionalTestCase
public function testCollectionFilteringLteOperator(): void
{
$product = $this->_em->find(ECommerceProduct::class, $this->productId);
$criteria = Criteria::create()->where(Criteria::expr()->lte('id', $this->secondCategoryId));
$criteria = Criteria::create(true)->where(Criteria::expr()->lte('id', $this->secondCategoryId));
self::assertCount(2, $product->getCategories()->matching($criteria));
}
@@ -58,7 +58,7 @@ final class GH6740Test extends OrmFunctionalTestCase
public function testCollectionFilteringLtOperator(): void
{
$product = $this->_em->find(ECommerceProduct::class, $this->productId);
$criteria = Criteria::create()->where(Criteria::expr()->lt('id', $this->secondCategoryId));
$criteria = Criteria::create(true)->where(Criteria::expr()->lt('id', $this->secondCategoryId));
self::assertCount(1, $product->getCategories()->matching($criteria));
}
@@ -67,7 +67,7 @@ final class GH6740Test extends OrmFunctionalTestCase
public function testCollectionFilteringGteOperator(): void
{
$product = $this->_em->find(ECommerceProduct::class, $this->productId);
$criteria = Criteria::create()->where(Criteria::expr()->gte('id', $this->firstCategoryId));
$criteria = Criteria::create(true)->where(Criteria::expr()->gte('id', $this->firstCategoryId));
self::assertCount(2, $product->getCategories()->matching($criteria));
}
@@ -76,7 +76,7 @@ final class GH6740Test extends OrmFunctionalTestCase
public function testCollectionFilteringGtOperator(): void
{
$product = $this->_em->find(ECommerceProduct::class, $this->productId);
$criteria = Criteria::create()->where(Criteria::expr()->gt('id', $this->firstCategoryId));
$criteria = Criteria::create(true)->where(Criteria::expr()->gt('id', $this->firstCategoryId));
self::assertCount(1, $product->getCategories()->matching($criteria));
}
@@ -85,7 +85,7 @@ final class GH6740Test extends OrmFunctionalTestCase
public function testCollectionFilteringEqualsOperator(): void
{
$product = $this->_em->find(ECommerceProduct::class, $this->productId);
$criteria = Criteria::create()->where(Criteria::expr()->eq('id', $this->firstCategoryId));
$criteria = Criteria::create(true)->where(Criteria::expr()->eq('id', $this->firstCategoryId));
self::assertCount(1, $product->getCategories()->matching($criteria));
}

View File

@@ -37,6 +37,8 @@ final class GH7717Test extends OrmFunctionalTestCase
$parent = $this->_em->find(GH7717Parent::class, 1);
$this->assertCount(1, $parent->children->matching(new Criteria(Criteria::expr()->isNull('nullableProperty'))));
$this->assertCount(1, $parent->children->matching(Criteria::create(true)->where(
Criteria::expr()->isNull('nullableProperty'),
)));
}
}

View File

@@ -43,7 +43,7 @@ class GH7737Test extends OrmFunctionalTestCase
$query = $this->_em->createQueryBuilder()
->select('person')
->from(GH7737Person::class, 'person')
->addCriteria(Criteria::create()->where(Criteria::expr()->memberOf(':group', 'person.groups')))
->addCriteria(Criteria::create(true)->where(Criteria::expr()->memberOf(':group', 'person.groups')))
->getQuery();
$group1 = $this->_em->find(GH7737Group::class, 1);

View File

@@ -45,7 +45,7 @@ class GH7767Test extends OrmFunctionalTestCase
$parent = $this->_em->find(GH7767ParentEntity::class, 1);
assert($parent instanceof GH7767ParentEntity);
$children = $parent->getChildren()->matching(Criteria::create());
$children = $parent->getChildren()->matching(Criteria::create(true));
self::assertEquals(100, $children[0]->position);
self::assertEquals(200, $children[1]->position);
@@ -58,7 +58,7 @@ class GH7767Test extends OrmFunctionalTestCase
assert($parent instanceof GH7767ParentEntity);
$children = $parent->getChildren()->matching(
Criteria::create()->orderBy(['position' => class_exists(Order::class) ? Order::Descending : 'DESC']),
Criteria::create(true)->orderBy(['position' => class_exists(Order::class) ? Order::Descending : 'DESC']),
);
self::assertEquals(300, $children[0]->position);

View File

@@ -45,7 +45,7 @@ class GH7836Test extends OrmFunctionalTestCase
$parent = $this->_em->find(GH7836ParentEntity::class, 1);
assert($parent instanceof GH7836ParentEntity);
$children = $parent->getChildren()->matching(Criteria::create());
$children = $parent->getChildren()->matching(Criteria::create(true));
self::assertSame(100, $children[0]->position);
self::assertSame('bar', $children[0]->name);
@@ -61,7 +61,7 @@ class GH7836Test extends OrmFunctionalTestCase
assert($parent instanceof GH7836ParentEntity);
$children = $parent->getChildren()->matching(
Criteria::create()->orderBy(
Criteria::create(true)->orderBy(
class_exists(Order::class)
? ['position' => Order::Descending, 'name' => Order::Ascending]
: ['position' => 'DESC', 'name' => 'ASC'],
@@ -82,7 +82,7 @@ class GH7836Test extends OrmFunctionalTestCase
assert($parent instanceof GH7836ParentEntity);
$children = $parent->getChildren()->matching(
Criteria::create()->orderBy(
Criteria::create(true)->orderBy(
class_exists(Order::class)
? ['name' => Order::Ascending, 'position' => Order::Ascending]
: ['name' => 'ASC', 'position' => 'ASC'],

View File

@@ -67,7 +67,7 @@ class GH9109Test extends OrmFunctionalTestCase
self::assertEquals($userLastName, $user->getLastName());
// assert NOT QUOTED will WORK with Criteria
$criteria = Criteria::create();
$criteria = Criteria::create(true);
$criteria->where($criteria->expr()->eq('lastName', $userLastName));
$user = $persistedProduct->getBuyers()->matching($criteria)->first();
self::assertInstanceOf(GH9109User::class, $user);
@@ -79,7 +79,7 @@ class GH9109Test extends OrmFunctionalTestCase
self::assertEquals($userFirstName, $user->getFirstName());
// assert QUOTED will WORK with Criteria
$criteria = Criteria::create();
$criteria = Criteria::create(true);
$criteria->where($criteria->expr()->eq('firstName', $userFirstName));
$user = $persistedProduct->getBuyers()->matching($criteria)->first();
self::assertInstanceOf(GH9109User::class, $user);

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\ValueConversionType;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Expr\Comparison;
use Doctrine\Tests\Models\ValueConversionType\InversedManyToManyEntity;
use Doctrine\Tests\Models\ValueConversionType\OwningManyToManyEntity;
use Doctrine\Tests\OrmFunctionalTestCase;
use Generator;
use PHPUnit\Framework\Attributes\DataProvider;
class ManyToManyCriteriaMatchingTest extends OrmFunctionalTestCase
{
protected function setUp(): void
{
$this->useModelSet('vct_manytomany');
parent::setUp();
}
#[DataProvider('provideMatchingExpressions')]
public function testCriteriaMatchingOnFieldInManyToManyTarget(Comparison $comparison): void
{
$associated = new InversedManyToManyEntity();
$associated->id1 = 'associated';
$matching = new OwningManyToManyEntity();
$matching->id2 = 'first';
$matching->field = 'match this'; // stored as 'zngpu guvf'
$matching->associatedEntities->add($associated);
$nonMatching = new OwningManyToManyEntity();
$nonMatching->id2 = 'second';
$nonMatching->field = 'this is no match';
$nonMatching->associatedEntities->add($associated);
$this->_em->persist($associated);
$this->_em->persist($matching);
$this->_em->persist($nonMatching);
$this->_em->flush();
$this->_em->clear();
$this->getQueryLog()->reset()->enable();
$associated = $this->_em->find(InversedManyToManyEntity::class, 'associated');
self::assertFalse($associated->associatedEntities->isInitialized(), 'Pre-condition: lazy collection');
$result = $associated->associatedEntities->matching(Criteria::create(true)->where($comparison));
$l = $this->getQueryLog();
self::assertCount(1, $result);
self::assertSame('first', $result[0]->id2);
}
public static function provideMatchingExpressions(): Generator
{
yield [Criteria::expr()->eq('field', 'match this')]; // should convert to 'zngpu guvf'
yield [Criteria::expr()->in('field', ['match this'])]; // should convert to ['zngpu guvf']
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\ValueConversionType;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Expr\Comparison;
use Doctrine\Tests\Models\ValueConversionType\InversedOneToManyEntity;
use Doctrine\Tests\Models\ValueConversionType\OwningManyToOneEntity;
use Doctrine\Tests\OrmFunctionalTestCase;
use Generator;
use PHPUnit\Framework\Attributes\DataProvider;
class OneToManyCriteriaMatchingTest extends OrmFunctionalTestCase
{
protected function setUp(): void
{
$this->useModelSet('vct_onetomany');
parent::setUp();
}
#[DataProvider('provideMatchingExpressions')]
public function testCriteriaMatchingOnFieldInOneToManyTarget(Comparison $comparison): void
{
$entityWithCollection = new InversedOneToManyEntity();
$entityWithCollection->id1 = 'associated';
$matching = new OwningManyToOneEntity();
$matching->id2 = 'first';
$matching->field = 'match this'; // stored as 'zngpu guvf'
$matching->associatedEntity = $entityWithCollection;
$notMatching = new OwningManyToOneEntity();
$notMatching->id2 = 'second';
$notMatching->field = 'this is no match';
$notMatching->associatedEntity = $entityWithCollection;
$this->_em->persist($entityWithCollection);
$this->_em->persist($matching);
$this->_em->persist($notMatching);
$this->_em->flush();
$this->_em->clear();
$this->getQueryLog()->reset()->enable();
$entityWithCollection = $this->_em->find(InversedOneToManyEntity::class, 'associated');
self::assertFalse($entityWithCollection->associatedEntities->isInitialized(), 'Pre-condition: lazy collection');
$result = $entityWithCollection->associatedEntities->matching(Criteria::create(true)->where($comparison));
$l = $this->getQueryLog();
self::assertCount(1, $result);
self::assertSame('first', $result[0]->id2);
}
public static function provideMatchingExpressions(): Generator
{
yield [Criteria::expr()->eq('field', 'match this')]; // should convert to 'zngpu guvf'
yield [Criteria::expr()->in('field', ['match this'])]; // should convert to ['zngpu guvf']
}
}

View File

@@ -10,11 +10,11 @@ use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Result;
use Doctrine\ORM\EntityManagerInterface;
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 LogicException;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\MockObject;
@@ -27,7 +27,7 @@ class AbstractHydratorTest extends OrmFunctionalTestCase
private EventManager&MockObject $mockEventManager;
private Result&MockObject $mockResult;
private ResultSetMapping&MockObject $mockResultMapping;
private AbstractHydrator&MockObject $hydrator;
private DummyHydrator $hydrator;
protected function setUp(): void
{
@@ -52,10 +52,7 @@ class AbstractHydratorTest extends OrmFunctionalTestCase
->method('fetchAssociative')
->willReturn(false);
$this->hydrator = $this
->getMockBuilder(AbstractHydrator::class)
->setConstructorArgs([$mockEntityManagerInterface])
->getMockForAbstractClass();
$this->hydrator = new DummyHydrator($mockEntityManagerInterface);
}
/**
@@ -141,13 +138,9 @@ class AbstractHydratorTest extends OrmFunctionalTestCase
$this->assertTrue($eventListenerHasBeenRegistered);
});
$this
->hydrator
->expects(self::once())
->method('hydrateAllData')
->willThrowException($this->createStub(ORMException::class));
$this->hydrator->throwException = true;
$this->expectException(ORMException::class);
$this->expectException(LogicException::class);
$this->hydrator->hydrateAll($this->mockResult, $this->mockResultMapping);
}
@@ -180,3 +173,18 @@ class AbstractHydratorTest extends OrmFunctionalTestCase
self::assertCount(0, $evm->getListeners(Events::onClear));
}
}
class DummyHydrator extends AbstractHydrator
{
public bool $throwException = false;
/** @return array{} */
protected function hydrateAllData(): array
{
if ($this->throwException) {
throw new LogicException();
}
return [];
}
}

View File

@@ -11,7 +11,6 @@ use Doctrine\ORM\Persisters\Entity\EntityPersister;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use stdClass;
#[CoversClass(LazyCriteriaCollection::class)]
class LazyCriteriaCollectionTest extends TestCase
@@ -23,7 +22,7 @@ class LazyCriteriaCollectionTest extends TestCase
protected function setUp(): void
{
$this->persister = $this->createMock(EntityPersister::class);
$this->criteria = new Criteria();
$this->criteria = Criteria::create(true);
$this->lazyCriteriaCollection = new LazyCriteriaCollection($this->persister, $this->criteria);
}
@@ -64,9 +63,9 @@ class LazyCriteriaCollectionTest extends TestCase
public function testMatchingUsesThePersisterOnlyOnce(): void
{
$foo = new stdClass();
$bar = new stdClass();
$baz = new stdClass();
$foo = new LazyCriteriaCollectionTestObject();
$bar = new LazyCriteriaCollectionTestObject();
$baz = new LazyCriteriaCollectionTestObject();
$foo->val = 'foo';
$bar->val = 'bar';
@@ -79,7 +78,7 @@ class LazyCriteriaCollectionTest extends TestCase
->with($this->criteria)
->willReturn([$foo, $bar, $baz]);
$criteria = new Criteria();
$criteria = Criteria::create(true);
$criteria->andWhere($criteria->expr()->eq('val', 'foo'));
@@ -122,3 +121,9 @@ class LazyCriteriaCollectionTest extends TestCase
self::assertFalse($this->lazyCriteriaCollection->isEmpty());
}
}
class LazyCriteriaCollectionTestObject
{
/** @var mixed */
public $val;
}

View File

@@ -54,7 +54,7 @@ use DoctrineGlobalArticle;
use LogicException;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group as TestGroup;
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
use ReflectionClass;
use stdClass;
@@ -1126,7 +1126,7 @@ class ClassMetadataTest extends OrmTestCase
$metadata->addLifecycleCallback('foo', 'bar');
}
#[WithoutErrorHandler]
#[IgnoreDeprecations]
public function testGettingAnFQCNForNullIsDeprecated(): void
{
$metadata = new ClassMetadata(self::class);
@@ -1165,7 +1165,7 @@ class ClassMetadataTest extends OrmTestCase
);
}
#[WithoutErrorHandler]
#[IgnoreDeprecations]
public function testDiscriminatorMapWithSameClassMultipleTimesDeprecated(): void
{
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/3519');

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Mapping;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\Name\UnquotedIdentifierFolding;
use Doctrine\DBAL\Platforms\SQLitePlatform;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\DefaultQuoteStrategy;
use Doctrine\Tests\Models\NonPublicSchemaJoins\User as NonPublicSchemaUser;
@@ -13,8 +13,6 @@ use Doctrine\Tests\OrmTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use function assert;
use function enum_exists;
use function sprintf;
/**
@@ -29,8 +27,7 @@ class DefaultQuoteStrategyTest extends OrmTestCase
$em = $this->getTestEntityManager();
$metadata = $em->getClassMetadata(NonPublicSchemaUser::class);
$strategy = new DefaultQuoteStrategy();
$platform = $this->getMockForAbstractClass(AbstractPlatform::class, enum_exists(UnquotedIdentifierFolding::class) ? [UnquotedIdentifierFolding::UPPER] : []);
assert($platform instanceof AbstractPlatform);
$platform = new SQLitePlatform();
self::assertSame(
'readers.author_reader',

View File

@@ -6,14 +6,14 @@ namespace Doctrine\Tests\ORM\Mapping;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\Mapping\Table;
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
use PHPUnit\Framework\TestCase;
final class TableMappingTest extends TestCase
{
use VerifyDeprecations;
#[WithoutErrorHandler]
#[IgnoreDeprecations]
public function testDeprecationOnIndexesPropertyIsTriggered(): void
{
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/11357');
@@ -21,7 +21,7 @@ final class TableMappingTest extends TestCase
new Table(indexes: []);
}
#[WithoutErrorHandler]
#[IgnoreDeprecations]
public function testDeprecationOnUniqueConstraintsPropertyIsTriggered(): void
{
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/11357');

View File

@@ -11,9 +11,9 @@ use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Doctrine\ORM\Mapping\Driver\XmlDriver;
use Doctrine\ORM\ORMSetup;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use PHPUnit\Framework\Attributes\RequiresSetting;
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
use PHPUnit\Framework\TestCase;
use ReflectionProperty;
use Symfony\Component\Cache\Adapter\AbstractAdapter;
@@ -28,7 +28,7 @@ class ORMSetupTest extends TestCase
{
use VerifyDeprecations;
#[WithoutErrorHandler]
#[IgnoreDeprecations]
public function testAttributeConfiguration(): void
{
if (PHP_VERSION_ID >= 80400) {
@@ -51,7 +51,7 @@ class ORMSetupTest extends TestCase
self::assertInstanceOf(AttributeDriver::class, $config->getMetadataDriverImpl());
}
#[WithoutErrorHandler]
#[IgnoreDeprecations]
public function testXMLConfiguration(): void
{
if (PHP_VERSION_ID >= 80400) {
@@ -104,7 +104,7 @@ class ORMSetupTest extends TestCase
}
#[Group('DDC-1350')]
#[WithoutErrorHandler]
#[IgnoreDeprecations]
public function testConfigureProxyDir(): void
{
$config = ORMSetup::createAttributeMetadataConfiguration([], true, '/foo');

View File

@@ -46,7 +46,7 @@ class BasicEntityPersisterCompositeTypeParametersTest extends OrmTestCase
$country = new Country('IT', 'Italy');
$admin1 = new Admin1(10, 'Rome', $country);
$criteria = Criteria::create();
$criteria = Criteria::create(true);
$criteria->andWhere(Criteria::expr()->eq('admin1', $admin1));
[$values, $types] = $this->persister->expandCriteriaParameters($criteria);

View File

@@ -68,8 +68,7 @@ class BasicEntityPersisterTypeValueSqlTest extends OrmTestCase
$platform = $this->getMockBuilder(AbstractPlatform::class)
->setConstructorArgs(enum_exists(UnquotedIdentifierFolding::class) ? [UnquotedIdentifierFolding::UPPER] : [])
->onlyMethods(['supportsIdentityColumns'])
->getMockForAbstractClass();
->getMock();
$platform->method('supportsIdentityColumns')
->willReturn(true);
@@ -151,7 +150,7 @@ class BasicEntityPersisterTypeValueSqlTest extends OrmTestCase
public function testSelectConditionStatementWithMultipleValuesContainingNull(): void
{
self::assertEquals(
'(t0.id IN (?) OR t0.id IS NULL)',
't0.id IS NULL',
$this->persister->getSelectConditionStatementSQL('id', [null]),
);
@@ -164,6 +163,11 @@ class BasicEntityPersisterTypeValueSqlTest extends OrmTestCase
'(t0.id IN (?) OR t0.id IS NULL)',
$this->persister->getSelectConditionStatementSQL('id', [123, null]),
);
self::assertEquals(
'(t0.id IN (?, ?) OR t0.id IS NULL)',
$this->persister->getSelectConditionStatementSQL('id', [123, null, 234]),
);
}
public function testCountCondition(): void
@@ -175,7 +179,7 @@ class BasicEntityPersisterTypeValueSqlTest extends OrmTestCase
self::assertEquals('SELECT COUNT(*) FROM "not-a-simple-entity" t0 WHERE t0."simple-entity-value" = ?', $statement);
// Using a criteria object
$criteria = new Criteria(Criteria::expr()->eq('value', 'bar'));
$criteria = Criteria::create(true)->where(Criteria::expr()->eq('value', 'bar'));
$statement = $persister->getCountSQL($criteria);
self::assertEquals('SELECT COUNT(*) FROM "not-a-simple-entity" t0 WHERE t0."simple-entity-value" = ?', $statement);
}

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