Compare commits

...

150 Commits
3.2.0 ... 3.3.0

Author SHA1 Message Date
Grégoire Paris
69958152e6 Merge pull request #11653 from beberlei/GH-8471-RevertPartialObjects2
[GH-8471] Undeprecate PARTIAL for objects in DQL
2024-10-12 22:07:18 +02:00
Benjamin Eberlei
cf8f5f9f93 Apply suggestions from code review
Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>
2024-10-12 21:01:04 +02:00
Alexander M. Turek
60c245413d Auto-detect values for EnumType columns (#11666) 2024-10-12 17:35:44 +02:00
Benjamin Eberlei
7c9b74221f fix phpbench tests. 2024-10-12 16:31:02 +02:00
Benjamin Eberlei
c7e5605d11 Fix test 2024-10-12 15:43:46 +02:00
Benjamin Eberlei
c19afa1529 Merge 3.3.x 2024-10-12 15:42:38 +02:00
Benjamin Eberlei
516b593193 Fix phpcs 2024-10-12 15:41:31 +02:00
Benjamin Eberlei
e2b971d7c5 Merge branch '3.3.x' into GH-8471-RevertPartialObjects2 2024-10-12 15:40:18 +02:00
Grégoire Paris
19d9244a88 Merge pull request #11575 from eltharin/named_arguments
Allow named Arguments to be passed to Dto
2024-10-12 12:07:20 +02:00
Grégoire Paris
10a5a3ff73 Merge pull request #11665 from doctrine/3.2.x-merge-up-into-3.3.x_pbOwHc2w
Merge release 3.2.3 into 3.3.x
2024-10-12 11:58:33 +02:00
Benjamin Eberlei
f5fb400d0f Address review comments. 2024-10-12 02:32:15 +02:00
Grégoire Paris
a321331c89 Merge origin/2.20.x into 3.3.x (using imerge) 2024-10-11 22:11:52 +02:00
Grégoire Paris
522863116a Update branch metadata (#11663)
2.20.0 just got released
2024-10-11 20:23:33 +02:00
Benjamin Eberlei
5bfb744967 The MySQL/Maria EnumType added in DBAL 4.2 has a new known option "values". (#11657) 2024-10-11 15:29:19 +02:00
Grégoire Paris
8ed6c2234a Merge pull request #11661 from doctrine/2.19.x
Merge 2.19.x up into 2.20.x
2024-10-11 13:47:24 +02:00
Grégoire Paris
ff612b9678 Merge pull request #11660 from simPod/test-method
test: cover all transactional methods in `EntityManagerTest::testItPreservesTheOriginalExceptionOnRollbackFailure()`
2024-10-11 13:11:31 +02:00
Simon Podlipsky
ee0d7197dd test: cover all transactional methods in EntityManagerTest::testItPreservesTheOriginalExceptionOnRollbackFailure() 2024-10-11 13:00:52 +02:00
Matthias Pigulla
39d2136f46 Fix different first/max result values taking up query cache space (#11188)
* Add a test covering the #11112 issue

* Add new OutputWalker and SqlFinalizer interfaces

* Add a SingleSelectSqlFinalizer that can take care of adding offset/limit as well as locking mode statements to a given SQL query.

Add a FinalizedSelectExecutor that executes given, finalized SQL statements.

* In SqlWalker, split SQL query generation into the two parts that shall happen before and after the finalization phase.

Move the part that generates "pre-finalization" SQL into a dedicated method. Use a side channel in SingleSelectSqlFinalizer to access the "finalization" logic and avoid duplication.

* Fix CS violations

* Skip the GH11112 test while applying refactorings

* Avoid a Psalm complaint due to invalid (?) docblock syntax

* Establish alternate code path - queries can obtain the sql executor through the finalizer, parser knows about output walkers yielding finalizers

* Remove a possibly premature comment

* Re-enable the #11112 test

* Fix CS

* Make RootTypeWalker inherit from SqlOutputWalker so it becomes finalizer-aware

* Update QueryCacheTest, since first/max results no longer need extra cache entries

* Fix ParserResultSerializationTest by forcing the parser to produce a ParserResult of the old kind (with the executor already constructed)

* Fix WhereInWalkerTest

* Update lib/Doctrine/ORM/Query/Exec/PreparedExecutorFinalizer.php

Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>

* Fix tests

* Fix a Psalm complaint

* Fix a test

* Fix CS

* Make the NullSqlWalker an instance of SqlOutputWalker

* Avoid multiple cache entries caused by LimitSubqueryOutputWalker

* Fix Psalm complaints

* Fix static analysis complaints

* Remove experimental code that I committed accidentally

* Remove unnecessary baseline entry

* Make AddUnknownQueryComponentWalker subclass SqlOutputWalker

That way, we have no remaining classes in the codebase subclassing SqlWalker but not SqlOutputWalker

* Use more expressive exception classes

* Add a deprecation message

* Move SqlExecutor creation to ParserResult, to minimize public methods available on it

* Avoid keeping the SqlExecutor in the Query, since it must be generated just in time (e. g. in case Query parameters change)

* Address PHPStan complaints

* Fix tests

* Small refactorings

* Add an upgrade notice

* Small refactorings

* Update the Psalm baseline

* Add a missing namespace import

* Update Psalm baseline

* Fix CS

* Fix Psalm baseline

---------

Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>
2024-10-10 15:15:08 +02:00
eltharin
c223b8f635 Allow named Arguments to be passed to Dto
Allow to change argument order or use variadic argument in dto constructor using new named keyword
2024-10-10 14:33:16 +02:00
Benjamin Eberlei
bea454eefc [GH-8471] undeprecate partials completly (#11647)
* [GH-8471] Undeprecate all PARTIAL object usage.
2024-10-10 13:54:34 +02:00
Grégoire Paris
14f2572e4e Merge pull request #11656 from doctrine/2.19.x
Merge 2.19.x up into 2.20.x
2024-10-10 13:53:05 +02:00
Grégoire Paris
c2c500077b Merge pull request #11646 from greg0ire/finally-fix-bug
Run risky code in finally block
2024-10-10 11:46:49 +02:00
Grégoire Paris
6281c2b79f Merge pull request #11655 from greg0ire/submodule-cleanup
Submodule cleanup
2024-10-10 11:12:12 +02:00
Grégoire Paris
bac1c17eab Remove submodule remnant
This should make a warning we have in the CI go away.

>  fatal: No url found for submodule path 'docs/en/_theme' in .gitmodules
2024-10-10 11:07:38 +02:00
Grégoire Paris
b6137c8911 Add guard clause
It maybe happen that the SQL COMMIT statement is successful, but then
something goes wrong. In that kind of case, you do not want to attempt a
rollback.

This was implemented in UnitOfWork::commit(), but for some reason not in
the similar EntityManager methods.
2024-10-10 10:58:24 +02:00
Benjamin Eberlei
da7854f586 Fix imports 2024-10-10 10:41:04 +02:00
Benjamin Eberlei
5f4ecfd1d8 Fix imports 2024-10-10 10:38:33 +02:00
Grégoire Paris
51be1b1d52 Run risky code in finally block
catch blocks are not supposed to fail. If you want to do something
despite an exception happening, you should do it in a finally block.

Closes #7545
2024-10-10 10:06:12 +02:00
Alexander M. Turek
5f39343bfd Merge branch '2.20.x' into 3.3.x
* 2.20.x:
  Remove vendor prefix of PHPDoc referencing class-string (#11643)
  Deprecate the `\Doctrine\ORM\Query\Parser::setCustomOutputTreeWalker()` method (#11641)
2024-10-09 22:37:30 +02:00
Benjamin Eberlei
7ef1f0a379 Phpcs fixes 2024-10-09 22:14:01 +02:00
Alexander M. Turek
488a5dd3bf Remove vendor prefix of PHPDoc referencing class-string (#11643) 2024-10-09 21:58:37 +02:00
Alexander M. Turek
30795559dc Merge branch '2.20.x' into 3.3.x
* 2.20.x:
  Stop recommending vendor-prefixed PHPDoc (#11640)
  Let PHPStan detect deprecated usages (#11639)
  Add upgrade note about property hooks (#11636)
  Prepare PHP 8.4 support: Prevent property hooks from being used (#11628)
  Use E_ALL instead of E_ALL | E_STRICT
  Add CI job for PHP 8.4
  fix generating duplicate method stubs
2024-10-09 21:32:07 +02:00
Benjamin Eberlei
f71725575c Revert undprecate PARTIAL for objects in DQL. 2024-10-09 17:34:59 +02:00
Benjamin Eberlei
4a3c7f05bf Revert "Remove unused exception"
This reverts commit 689da1f251.
2024-10-09 17:13:58 +02:00
Matthias Pigulla
896c65504d Deprecate the \Doctrine\ORM\Query\Parser::setCustomOutputTreeWalker() method (#11641)
We use this method only from within one of our own test cases, and I don't see how it would be useful to anybody else outside – it has to be called on the `Parser` instance which exists internally in the `Query` only.

Deprecating and removing it in 3.x allows for a slight simplification in the `Parser` there, since we do no longer need the field (it can be a local variable).
2024-10-09 16:12:41 +02:00
Matthias Pigulla
16a8f10fd2 Remove a misleading comment (#11644) 2024-10-09 15:37:04 +02:00
Alexander M. Turek
498de4c564 Merge branch '3.2.x' into 3.3.x
* 3.2.x:
  PHPStan 1.12.6 (#11635)
2024-10-09 15:06:48 +02:00
Alexander M. Turek
d80a831157 Stop recommending vendor-prefixed PHPDoc (#11640) 2024-10-09 14:48:42 +02:00
Alexander M. Turek
52660297ab Let PHPStan detect deprecated usages (#11639) 2024-10-09 14:47:57 +02:00
Alexander M. Turek
b44774285b Merge branch '2.19.x' into 3.2.x
* 2.19.x:
  PHPStan 1.12.6 (#11635)
2024-10-09 11:09:19 +02:00
Alexander M. Turek
58287bb731 Merge branch '2.19.x' into 2.20.x
* 2.19.x:
  PHPStan 1.12.6 (#11635)
2024-10-09 11:08:42 +02:00
Alexander M. Turek
bc37f75b41 PHPStan 1.12.6 (#11635) 2024-10-09 11:08:02 +02:00
Grégoire Paris
8a25b264f7 Add upgrade note about property hooks (#11636)
People that might have experimented with property hooks while still
using ORM < 2.20.0 need to know that they need to remove their
experiment or upgrade to a version that explicitly supports them.
2024-10-09 11:05:58 +02:00
Benjamin Eberlei
0e48b19cd3 Prepare PHP 8.4 support: Prevent property hooks from being used (#11628)
Prevent property hooks from being used as they currently would work on external non-raw values without explicit code.
2024-10-09 10:36:21 +02:00
Grégoire Paris
d2978303f0 Merge remote-tracking branch 'origin/3.2.x' into 3.3.x 2024-10-09 10:13:04 +02:00
Grégoire Paris
109042e5af Merge pull request #11631 from greg0ire/php84-ci
Add CI job for PHP 8.4
2024-10-09 09:42:08 +02:00
Grégoire Paris
08328adc6c Use E_ALL instead of E_ALL | E_STRICT
E_STRICT is deprecated as of PHP 8.4
2024-10-09 09:19:32 +02:00
Grégoire Paris
191a5366b1 Merge pull request #11629 from greg0ire/3.2.x
Merge 2.19.x up into 3.2.x
2024-10-08 17:57:30 +02:00
Grégoire Paris
65806884b0 Add CI job for PHP 8.4
For now doctrine/common generates proxies that trigger deprecation, so
let us only test with lazy ghosts only.
2024-10-08 17:56:38 +02:00
Alexander M. Turek
ad80e8281a Merge branch '2.19.x' into 2.20.x
* 2.19.x:
  Replace custom directives with native option
2024-10-08 15:54:57 +02:00
Grégoire Paris
44dddb2eee Merge remote-tracking branch 'origin/2.19.x' into 3.2.x 2024-10-08 15:37:53 +02:00
Grégoire Paris
0c0c61c51b Merge pull request #11627 from greg0ire/no-custom-directives
Replace custom directives with native option
2024-10-08 15:26:44 +02:00
Grégoire Paris
cc28fed9f5 Replace custom directives with native option 2024-10-08 14:43:18 +02:00
Alexander M. Turek
2245149588 Merge branch '2.19.x' into 2.20.x
* 2.19.x:
  Make nullable parameters explicit in generated entities (#11625)
  Update attributes-reference.rst
  Bump doctrine/.github from 5.0.1 to 5.1.0 (#11616)
  Move orphan metadata to where it belongs
  PHPStan 1.12 (#11585)
2024-10-08 12:26:50 +02:00
Alexander M. Turek
b13564c6c0 Make nullable parameters explicit in generated entities (#11625) 2024-10-08 12:25:31 +02:00
Max Mustermann
91709c1275 fix generating duplicate method stubs
When adding the same lifecycle event callback to two or more lifecycle events, the generator will create a stub for each event resulting in fatal 'Cannot redeclare' errors. That is, only if the callback name contains uppercase letters.
2024-10-05 13:40:04 +02:00
Grégoire Paris
434b7cee2a Merge pull request #11619 from eltharin/change_EBNF
add nested new in EBNF documentation
2024-10-04 18:53:50 +02:00
eltharin
7f0a181e39 add nested new in EBNF documentation 2024-10-02 09:48:08 +02:00
Grégoire Paris
d18126aac5 Merge pull request #11618 from n0099/patch-1
unclosed `]` in attributes-reference.rst
2024-10-01 17:27:04 +02:00
n0099
b7fd8241cf Update attributes-reference.rst 2024-10-01 21:19:44 +08:00
dependabot[bot]
2432939e4f Bump doctrine/.github from 5.0.1 to 5.1.0 (#11616)
Bumps [doctrine/.github](https://github.com/doctrine/.github) from 5.0.1 to 5.1.0.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/5.0.1...5.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 09:04:06 +02:00
Grégoire Paris
93ce84fa6e Merge pull request #11614 from greg0ire/guides-as-dep
Guides as dev dependency
2024-09-29 09:23:14 +02:00
Grégoire Paris
1bf4603422 Merge pull request #11615 from greg0ire/move-orphan
Move orphan metadata to where it belongs
2024-09-29 09:22:54 +02:00
Grégoire Paris
e6961bd968 Install guides-cli as a dev requirement
It is better if contributors can check the docs by themselves.
2024-09-27 19:44:34 +02:00
Grégoire Paris
25d5bc5b46 Move orphan metadata to where it belongs
The goal here was to retain compatibility with doctrine/rst-parser,
which is no longer in use in the website.
2024-09-27 19:42:13 +02:00
Grégoire Paris
5724e6279e Merge pull request #11596 from doctrine/3.2.x
Merge 3.2.x up into 3.3.x
2024-09-05 22:03:08 +02:00
Alexander M. Turek
cfc0655a1c Fix compatibility with DBAL 4.2 (#11592) 2024-09-03 18:44:06 +02:00
Alexander M. Turek
6cde337777 PHPStan 1.12 (#11585) 2024-08-27 12:10:07 +02:00
Vincent Langlet
c6b2d89748 Precise EntityRepository::count (#11579) 2024-08-24 07:29:36 +02:00
Grégoire Paris
e1dc94d1c2 Merge pull request #11583 from doctrine/3.2.x-merge-up-into-3.3.x_lYlOLiIT
Merge release 3.2.2 into 3.3.x
2024-08-23 12:31:30 +02:00
Grégoire Paris
74ef28295a Merge pull request #11582 from doctrine/2.19.x-merge-up-into-2.20.x_0oKsBvVN
Merge release 2.19.7 into 2.20.x
2024-08-23 12:29:43 +02:00
Grégoire Paris
831a1eb7d2 Merge pull request #11581 from greg0ire/3.2.x
Merge 2.19.x up into 3.2.x
2024-08-23 12:03:52 +02:00
Grégoire Paris
3a82b153f3 Merge remote-tracking branch 'origin/2.19.x' into 3.2.x 2024-08-23 09:07:18 +02:00
Grégoire Paris
168ac31084 Merge pull request #11109 from mcurland/Fix11108
Original entity data resolves inverse 1-1 joins
2024-08-23 08:54:57 +02:00
Grégoire Paris
6f93cebe6e Merge pull request #11576 from eltharin/newnestedDto
Re: Re: Add support for using nested DTOs
2024-08-19 22:52:53 +02:00
eltharin
8c582a49d3 Add support for using nested DTOs
This feature allow use of nested new operators

Co-authored-by: Tomas Norkūnas <norkunas.tom@gmail.com>
Co-authored-by: Sergey Protko <fesors@gmail.com>
Co-authored-by: Łukasz Zakrzewski <contact@lzakrzewski.com>

Update docs/en/reference/dql-doctrine-query-language.rst

Co-authored-by: Claudio Zizza <859964+SenseException@users.noreply.github.com>
2024-08-19 22:36:10 +02:00
Grégoire Paris
5f1fe1587c Merge pull request #11557 from d-ph/feature/make-count-walker-use-count-star-query-sometimes
Make CountWalker use COUNT(*) when $distinct is explicitly set to false (#11552)
2024-08-19 22:31:49 +02:00
Matthew Curland
fe4a2e83cf Original entity data resolves inverse 1-1 joins
If the source entity for an inverse (non-owning) 1-1 relationship is
identified by an association then the identifying association may not
be set when an inverse one-to-one association is resolved. This means
that no data is available in the entity to resolve the needed column
value for the join query.

The original entity data can be retrieved from the unit of work and
is used as a fallback to populate the query condition.

Fixes #11108
2024-08-17 11:50:56 +02:00
Grégoire Paris
205b2f5f20 Merge pull request #11550 from janedbal/patch-1
DQL custom functions: document TypedExpression
2024-08-09 22:50:14 +02:00
Jan Nedbal
3f550c19e3 DQL custom functions: document TypedExpression
Partially related to https://github.com/doctrine/orm/issues/11537

Co-authored-by: Claudio Zizza <859964+SenseException@users.noreply.github.com>
2024-08-06 09:16:45 +02:00
Grégoire Paris
8ac6a13ca0 Merge pull request #11564 from gitbugr/GH11501_fix_o2m_persister_single_inheritence_parent_relation_bugfix
GH11551 - fix OneToManyPersister::deleteEntityCollection case where single-inheritence table parent entity is targetEntity.
2024-08-05 07:47:46 +02:00
gitbugr
2707b09a07 fix spacing
Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>
2024-08-03 21:38:49 +01:00
Kyron Taylor
121158f92c GH11551 - fix OneToManyPersister::deleteEntityCollection when using
single-inheritence entity parent as targetEntity.

When using the parent entity for a single-inheritence table as the
targetEntity for a property, the discriminator value should be all
of the values in the discriminator map.
OneToManyPersister::deleteEntityCollection has been amended to
reflect this.
2024-08-03 16:55:14 +01:00
Marc Eichenseher
56cd688c4a Remove unused $pkColumns when gathering columns (#11560) 2024-08-02 01:01:23 +02:00
Grégoire Paris
96546caceb Merge pull request #11365 from beberlei/ReintroducePartialForArrayHydration
Reintroduce PARTIAL, but only for non-object hydration.
2024-07-24 09:28:05 +02:00
d-ph
57247ed6ca Make CountWalker use COUNT(*) when $distinct is explicitly set to false (#11552)
This change makes CountWalker use COUNT(*) instead of
COUNT(tbl.id), when the user declared that their query
does not need to use (SELECT) DISTINCT, which is
commonly the case when there are no JOINs in the query,
or when the JOINs are only *ToOne.

Research showed that COUNT(*) allows databases to use
index(-only) scans more eagerly from any of the
indexed columns, especially when the query is using
a WHERE-condition that filters on an indexed column.
2024-07-22 10:22:51 +01:00
Grégoire Paris
12817076c3 Merge pull request #11528 from norkunas/namedparams
Add `createNamedParameter` to `QueryBuilder`
2024-07-11 23:53:20 +02:00
Grégoire Paris
4c2f104d42 Merge pull request #11547 from greg0ire/3.3.x
Merge 2.20.x up into 3.3.x
2024-07-10 09:00:45 +02:00
Grégoire Paris
ef64cf7c33 Merge remote-tracking branch 'origin/2.20.x' into 3.3.x 2024-07-10 08:57:09 +02:00
Tomas
0983d3a4af Add createNamedParameter to QueryBuilder 2024-07-08 06:54:02 +03:00
Grégoire Paris
51ad860a25 Merge pull request #11543 from stof/fix_native_query_parameter_type
Fix the support for custom parameter types in native queries
2024-07-04 20:12:59 +02:00
Christophe Coevoet
9bd51aaeb6 Fix the support for custom parameter types in native queries
The Query class (used for DQL queries) takes care of using the value and
type as is when a type was specified for a parameter instead of going
through the default processing of values.
The NativeQuery class was missing the equivalent check, making the
custom type work only if the default processing of values does not
convert the value to a different one.
2024-07-04 16:25:34 +02:00
Xesau
1fe1a6a048 Fix incorrect exception message for ManyToOne attribute in embeddable class (#11536)
When a ManyToOne attribute is encountered on an Embeddable class, the exception message reads "Attribute "Doctrine\ORM\Mapping\OneToMany" on embeddable [class] is not allowed.". This should be "Doctrine\ORM\Mapping\ManyToOne" on embeddable [class] is not allowed.".
2024-07-01 21:57:36 +02:00
Grégoire Paris
c37b115450 Merge pull request #11534 from k00ni/patch-1
working-with-objects.rst: added missing white space
2024-06-28 09:03:54 +02:00
Konrad Abicht
19129e9f8a working-with-objects.rst: added missing white space 2024-06-28 09:00:12 +02:00
Grégoire Paris
efe62e3f0b Merge pull request #11532 from doctrine/3.2.x-merge-up-into-3.3.x_ws8Fgk2X
Merge release 3.2.1 into 3.3.x
2024-06-27 17:57:03 +02:00
Grégoire Paris
7d01f19667 Merge pull request #11531 from doctrine/2.19.x-merge-up-into-2.20.x_QMtlHSin
Merge release 2.19.6 into 2.20.x
2024-06-27 17:50:50 +02:00
Grégoire Paris
722cea6536 Merge pull request #11525 from greg0ire/3.2.x
Merge 2.19.x up into 3.2.x
2024-06-26 23:48:58 +02:00
Grégoire Paris
c1bb2ccf4b Merge pull request #11526 from GromNaN/patch-1
doc: Use modern array syntax in getting started
2024-06-26 19:24:40 +02:00
Jérôme Tamarelle
e3d7c6076c Use modern array syntax in the doc 2024-06-26 19:18:32 +02:00
Grégoire Paris
ce7d93f14d Merge remote-tracking branch 'origin/2.19.x' into 3.2.x 2024-06-26 16:53:24 +02:00
Alexander M. Turek
a139a1b63c Merge branch '3.2.x' into 3.3.x
* 3.2.x:
  Fix deprecated array access usage (#11517)
  Address doctrine/persistence 3.3.3 release
  Add the propoer void return type on the __load method of proxies
  Remove unneeded CS rule
2024-06-21 13:32:17 +02:00
Alexander M. Turek
1153b9468c Fix deprecated array access usage (#11517) 2024-06-21 13:31:45 +02:00
Grégoire Paris
40f299f1eb Merge pull request #11506 from michalbundyra/composite-key-relations-3
[2.19.x] Fetching entities with Composite Key Relations and null values
2024-06-21 08:12:27 +02:00
Grégoire Paris
d0e9177121 Merge pull request #11514 from doctrine/2.19.x
Merge 2.19.x up into 2.20.x
2024-06-20 22:51:33 +02:00
Grégoire Paris
428032ca7c Merge remote-tracking branch 'origin/2.19.x' into HEAD 2024-06-20 22:18:24 +02:00
Grégoire Paris
68af854f46 Merge pull request #11513 from greg0ire/address-persistence-3.3.3-release
Address doctrine/persistence 3.3.3 release
2024-06-20 22:14:52 +02:00
Grégoire Paris
77467cd824 Address doctrine/persistence 3.3.3 release
FileDriver became templatable, and some very wrong phpdoc has been
fixed, causing Psalm to better understand the 2 FileDriver classes in
this project.
2024-06-20 22:00:33 +02:00
Grégoire Paris
f666aa641e Merge pull request #11512 from greg0ire/deprecate-db-driver
Deprecate DatabaseDriver
2024-06-20 21:31:58 +02:00
Grégoire Paris
ca3319c2f6 Merge pull request #11511 from doctrine/stof-patch-1
Add the proper void return type on the __load method of proxies
2024-06-20 11:46:52 +02:00
Christophe Coevoet
c06f6b9376 Add the propoer void return type on the __load method of proxies
When using ghost objects, the method was leaking a `static` return type due to the way it was implemented, which is incompatible with the native return type that will be added in doctrine/persistence v4.
2024-06-20 09:08:10 +02:00
Grégoire Paris
802f20b8e7 Merge pull request #11509 from greg0ire/remove-unneeded-rule
Remove unneeded CS rule
2024-06-19 23:49:15 +02:00
Michał Bundyra
96d13ac62a Fetching entities with Composite Key Relations and null values
Remove redundant condition to check if target class contains foreign
identifier in order to allow fetching a null for relations with
composite keys, when part of the key value is null.
2024-06-19 21:54:02 +01:00
Grégoire Paris
ed53defaa1 Deprecate DatabaseDriver
It was used for the reverse engineering feature, which has been removed.
2024-06-19 21:59:29 +02:00
Grégoire Paris
2ea6a1a5fb Remove unneeded CS rule 2024-06-19 21:47:55 +02:00
Alexander M. Turek
41cb5fbbbf Merge branch '2.19.x' into 3.2.x
* 2.19.x:
  Fix OneToManyPersister::deleteEntityCollection missing discriminator column/value. (GH-11500)
  Skip joined entity creation for empty relation (#10889)
  ci: maintained and stable mariadb version (11.4 current lts) (#11490)
  fix(docs): use string value in `addAttribute`
  Replace assertion with exception (#11489)
  Use ramsey/composer-install in PHPBench workflow
  update EntityManager#transactional to EntityManager#wrapInTransaction
  Fix cloning entities
  Consider usage of setFetchMode when checking for simultaneous usage of fetch-mode EAGER and WITH condition.
2024-06-19 12:21:35 +02:00
Alexander M. Turek
83851a9716 Merge branch '2.19.x' into 2.20.x
* 2.19.x:
  Fix OneToManyPersister::deleteEntityCollection missing discriminator column/value. (GH-11500)
  Skip joined entity creation for empty relation (#10889)
  ci: maintained and stable mariadb version (11.4 current lts) (#11490)
  fix(docs): use string value in `addAttribute`
  Replace assertion with exception (#11489)
  Use ramsey/composer-install in PHPBench workflow
  update EntityManager#transactional to EntityManager#wrapInTransaction
  Fix cloning entities
  Consider usage of setFetchMode when checking for simultaneous usage of fetch-mode EAGER and WITH condition.
2024-06-18 14:19:19 +02:00
Alexander M. Turek
066ec1ac81 Fix upgrade guide for 2.20 (#11504) 2024-06-18 14:18:37 +02:00
Benjamin Eberlei
68744489f0 Undeprecate PARTIAL for array hydration. (#11366)
* Undeprecate PARTIAL for array hydration.

* note about undeprecate partial in UPGRADE.md
2024-06-18 14:15:31 +02:00
Grégoire Paris
cc2ad1993c Merge pull request #11501 from gitbugr/2.19.x
Fix OneToManyPersister::deleteEntityCollection missing discriminator column/value. (GH-11500)
2024-06-17 21:40:07 +02:00
Kyron Taylor
e4d46c4276 Fix OneToManyPersister::deleteEntityCollection missing discriminator column/value. (GH-11500) 2024-06-15 21:58:08 +01:00
Grégoire Paris
858a1adc3b Merge pull request #11194 from noemi-salaun/fix/gh10889
Skip joined entity creation for empty relation (#10889)
2024-06-14 20:06:59 +02:00
Noemi Salaun
3b499132d9 Skip joined entity creation for empty relation (#10889) 2024-06-14 14:34:04 +02:00
Daniel Black
39153fd88a ci: maintained and stable mariadb version (11.4 current lts) (#11490)
Also use MARIADB env names and the healthcheck.sh included in the container.
2024-06-13 19:34:46 +02:00
Grégoire Paris
bdc9679e37 Merge pull request #11493 from SamMousa/fix-docs-11492
fix(docs): use string value in `addAttribute`
2024-06-11 15:26:45 +01:00
Sam Mousa
87a8ee21c9 fix(docs): use string value in addAttribute 2024-06-11 16:21:28 +02:00
Grégoire Paris
59c8bc09ab Replace assertion with exception (#11489) 2024-06-03 23:08:27 +02:00
Grégoire Paris
3a7d7c9f57 Merge pull request #11484 from greg0ire/backport-ramsey
Use ramsey/composer-install in PHPBench workflow
2024-06-02 15:26:00 +02:00
Grégoire Paris
06eca40134 Use ramsey/composer-install in PHPBench workflow
It will handle caching for us.
2024-06-02 15:22:59 +02:00
Grégoire Paris
23b35e9554 Merge pull request #11475 from nicolas-grekas/fix-clone
Fix cloning entities
2024-06-01 22:47:57 +02:00
Grégoire Paris
e063926cbd Merge pull request #11445 from aprat84/gh-11128
Consider usage of setFetchMode when checking for simultaneous usage of fetch-mode EAGER and WITH condition
2024-05-30 17:24:11 +02:00
Grégoire Paris
4a01a76a17 Merge pull request #11460 from IndraGunawan/update-transactional-doc
docs: update EntityManager#transactional to EntityManager#wrapInTransaction
2024-05-28 14:07:06 +02:00
Indra Gunawan
93c2dd9d4b update EntityManager#transactional to EntityManager#wrapInTransaction
One has been deprecated in favor of the other.
2024-05-28 13:59:17 +02:00
Nicolas Grekas
75bc22980e Fix cloning entities 2024-05-27 14:53:58 +02:00
Alix Mauro
9696c3434d Consider usage of setFetchMode when checking for simultaneous usage of fetch-mode EAGER and WITH condition.
This fixes a bug that arises when an entity relation is mapped with
fetch-mode EAGER but setFetchMode LAZY (or anything that is not EAGER)
has been used on the query. If the query use WITH condition, an
exception is incorrectly raised (Associations with fetch-mode=EAGER may
not be using WITH conditions).

Fixes #11128

Co-Authored-By: Albert Prat <albert.prat@interactiu.cat>
2024-05-25 14:22:20 +02:00
Grégoire Paris
9d4f54b9a4 Update branch metadata (#11474) 2024-05-24 00:25:01 +02:00
Alexander M. Turek
bf3e082c00 Merge branch '2.19.x' into 2.20.x
* 2.19.x:
  Psalm 5.24.0 (#11467)
  PHPStan 1.11.1 (#11466)
  Test with actual lock modes (#11465)
  Backport test for Query::setLockMode() (#11463)
2024-05-21 14:22:18 +02:00
Alexander M. Turek
eb49f66926 Merge branch '2.19.x' into 2.20.x
* 2.19.x:
  Bump ramsey/composer-install from 2 to 3 (#11442)
  Bump doctrine/.github from 3.0.0 to 5.0.1
  Upgrade codecov/codecov-action
2024-05-21 08:40:37 +02:00
Grégoire Paris
8b6a58fa0e Merge pull request #11432 from doctrine/2.19.x-merge-up-into-2.20.x_IfraK93L
Merge release 2.19.5 into 2.20.x
2024-04-30 09:04:52 +02:00
Alexander M. Turek
b725908c83 Merge branch '2.19.x' into 2.20.x
* 2.19.x:
  Fix BIGINT validation (#11414)
  Fix templated phpdoc return type (#11407)
  [Documentation] Merging "Query Result Formats" with "Hydration Modes"
  Fix psalm errors: remove override of template type
  Update dql-doctrine-query-language.rst
  Adding `NonUniqueResultException`
  [Documentation] Query Result Formats
2024-04-15 16:26:53 +02:00
Alexander M. Turek
be307edba8 Merge release 2.19.3 into 2.20.x (#11398) 2024-03-22 12:11:39 +01:00
Alexander M. Turek
083f642cfa Merge branch '2.19.x' into 2.20.x
* 2.19.x:
  Remove unused variable (#11391)
2024-03-21 10:33:34 +01:00
Alexander M. Turek
716da7e538 Merge branch '2.19.x' into 2.20.x
* 2.19.x:
  [Documentation] Removing "Doctrine Mapping Types" ... (#11384)
  [GH-11185] Bugfix: do not use collection batch loading for indexBy assocations. (#11380)
  Improve lazy ghost performance by avoiding self-referencing closure. (#11376)
2024-03-21 10:12:37 +01:00
Grégoire Paris
bcdc5bdaf4 Merge pull request #11378 from doctrine/2.19.x-merge-up-into-2.20.x_eyF2lMAL
Merge release 2.19.2 into 2.20.x
2024-03-18 20:22:04 +01:00
Benjamin Eberlei
80278c545e Update docs/en/reference/partial-hydration.rst
Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>
2024-03-16 23:36:13 +01:00
Benjamin Eberlei
90962f060a Use id dynamically in array hydration test. 2024-03-16 22:39:38 +01:00
Benjamin Eberlei
758f0d7605 Remove Query::HINT_FORCE_PARTIAL_LOAD constant, not needed to be reintroduced. 2024-03-16 22:36:21 +01:00
Benjamin Eberlei
eb8510ff5c Add tests for adjusted functionality. 2024-03-16 22:34:54 +01:00
Benjamin Eberlei
d5fdd676f4 Reintroduce PARTIAL, but only for non-object hydration. 2024-03-16 22:20:34 +01:00
Grégoire Paris
a3e3a3bbf3 Merge pull request #11360 from doctrine/2.19.x-merge-up-into-2.20.x_aXnS7Xw9
Merge release 2.19.1 into 2.20.x
2024-03-16 21:32:18 +01:00
157 changed files with 4005 additions and 638 deletions

View File

@@ -11,17 +11,23 @@
"slug": "latest",
"upcoming": true
},
{
"name": "3.3",
"branchName": "3.3.x",
"slug": "3.3",
"upcoming": true
},
{
"name": "3.2",
"branchName": "3.2.x",
"slug": "3.2",
"upcoming": true
"current": true
},
{
"name": "3.1",
"branchName": "3.1.x",
"slug": "3.1",
"current": true
"maintained": false
},
{
"name": "3.0",
@@ -29,17 +35,23 @@
"slug": "3.0",
"maintained": false
},
{
"name": "2.21",
"branchName": "2.21.x",
"slug": "2.21",
"upcoming": true
},
{
"name": "2.20",
"branchName": "2.20.x",
"slug": "2.20",
"upcoming": true
"maintained": true
},
{
"name": "2.19",
"branchName": "2.19.x",
"slug": "2.19",
"maintained": true
"maintained": false
},
{
"name": "2.18",

View File

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

View File

@@ -36,6 +36,7 @@ jobs:
- "8.1"
- "8.2"
- "8.3"
- "8.4"
dbal-version:
- "default"
- "3.7"
@@ -107,6 +108,7 @@ jobs:
php-version:
- "8.2"
- "8.3"
- "8.4"
dbal-version:
- "default"
- "3.7"
@@ -180,12 +182,13 @@ jobs:
php-version:
- "8.2"
- "8.3"
- "8.4"
dbal-version:
- "default"
- "3.7"
- "4@dev"
mariadb-version:
- "10.9"
- "11.4"
extension:
- "mysqli"
- "pdo_mysql"
@@ -194,11 +197,11 @@ jobs:
mariadb:
image: "mariadb:${{ matrix.mariadb-version }}"
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: "doctrine_tests"
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: yes
MARIADB_DATABASE: "doctrine_tests"
options: >-
--health-cmd "mysqladmin ping --silent"
--health-cmd "healthcheck.sh --connect --innodb_initialized"
ports:
- "3306:3306"
@@ -246,6 +249,7 @@ jobs:
php-version:
- "8.2"
- "8.3"
- "8.4"
dbal-version:
- "default"
- "3.7"

View File

@@ -5,45 +5,16 @@ on:
branches:
- "*.x"
paths:
- .github/workflows/documentation.yml
- docs/**
- ".github/workflows/documentation.yml"
- "docs/**"
push:
branches:
- "*.x"
paths:
- .github/workflows/documentation.yml
- docs/**
- ".github/workflows/documentation.yml"
- "docs/**"
jobs:
validate-with-guides:
name: "Validate documentation with phpDocumentor/guides"
runs-on: "ubuntu-22.04"
steps:
- name: "Checkout code"
uses: "actions/checkout@v4"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
coverage: "none"
php-version: "8.3"
- name: "Remove existing composer file"
run: "rm composer.json"
- name: "Require phpdocumentor/guides-cli"
run: "composer require --dev phpdocumentor/guides-cli --no-update"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v3"
with:
dependency-versions: "highest"
- name: "Add orphan metadata where needed"
run: |
printf '%s\n\n%s\n' ":orphan:" "$(cat docs/en/sidebar.rst)" > docs/en/sidebar.rst
printf '%s\n\n%s\n' ":orphan:" "$(cat docs/en/reference/installation.rst)" > docs/en/reference/installation.rst
- name: "Run guides-cli"
run: "vendor/bin/guides -vvv --no-progress docs/en 2>&1 | grep -v 'No template found for rendering directive' | ( ! grep WARNING )"
documentation:
name: "Documentation"
uses: "doctrine/.github/.github/workflows/documentation.yml@5.1.0"

View File

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

View File

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

View File

@@ -1,3 +1,9 @@
# Upgrade to 3.3
## Deprecate `DatabaseDriver`
The class `Doctrine\ORM\Mapping\Driver\DatabaseDriver` is deprecated without replacement.
# Upgrade to 3.2
## Deprecate the `NotSupported` exception
@@ -95,9 +101,11 @@ now they throw an exception.
## BC BREAK: Partial objects are removed
- The `PARTIAL` keyword in DQL no longer exists.
- `Doctrine\ORM\Query\AST\PartialObjectExpression`is removed.
- `Doctrine\ORM\Query\SqlWalker::HINT_PARTIAL` and
WARNING: This was relaxed in ORM 3.2 when partial was re-allowed for array-hydration.
- The `PARTIAL` keyword in DQL no longer exists (reintroduced in ORM 3.2)
- `Doctrine\ORM\Query\AST\PartialObjectExpression` is removed. (reintroduced in ORM 3.2)
- `Doctrine\ORM\Query\SqlWalker::HINT_PARTIAL` (reintroduced in ORM 3.2) and
`Doctrine\ORM\Query::HINT_FORCE_PARTIAL_LOAD` are removed.
- `Doctrine\ORM\EntityManager*::getPartialReference()` is removed.
@@ -723,6 +731,42 @@ following classes and methods:
Use `toIterable()` instead.
# Upgrade to 2.20
## Add `Doctrine\ORM\Query\OutputWalker` interface, deprecate `Doctrine\ORM\Query\SqlWalker::getExecutor()`
Output walkers should implement the new `\Doctrine\ORM\Query\OutputWalker` interface and create
`Doctrine\ORM\Query\Exec\SqlFinalizer` instances instead of `Doctrine\ORM\Query\Exec\AbstractSqlExecutor`s.
The output walker must not base its workings on the query `firstResult`/`maxResult` values, so that the
`SqlFinalizer` can be kept in the query cache and used regardless of the actual `firstResult`/`maxResult` values.
Any operation dependent on `firstResult`/`maxResult` should take place within the `SqlFinalizer::createExecutor()`
method. Details can be found at https://github.com/doctrine/orm/pull/11188.
## Explictly forbid property hooks
Property hooks are not supported yet by Doctrine ORM. Until support is added,
they are explicitly forbidden because the support would result in a breaking
change in behavior.
Progress on this is tracked at https://github.com/doctrine/orm/issues/11624 .
## PARTIAL DQL syntax is undeprecated
Use of the PARTIAL keyword is not deprecated anymore in DQL, because we will be
able to support PARTIAL objects with PHP 8.4 Lazy Objects and
Symfony/VarExporter in a better way. When we decided to remove this feature
these two abstractions did not exist yet.
WARNING: If you want to upgrade to 3.x and still use PARTIAL keyword in DQL
with array or object hydrators, then you have to directly migrate to ORM 3.3.x or higher.
PARTIAL keyword in DQL is not available in 3.0, 3.1 and 3.2 of ORM.
## Deprecate `\Doctrine\ORM\Query\Parser::setCustomOutputTreeWalker()`
Use the `\Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER` query hint to set the output walker
class instead of setting it through the `\Doctrine\ORM\Query\Parser::setCustomOutputTreeWalker()` method
on the parser instance.
# Upgrade to 2.19
## Deprecate calling `ClassMetadata::getAssociationMappedByTargetField()` with the owning side of an association

View File

@@ -15,7 +15,8 @@
"config": {
"allow-plugins": {
"composer/package-versions-deprecated": true,
"dealerdirect/phpcodesniffer-composer-installer": true
"dealerdirect/phpcodesniffer-composer-installer": true,
"phpstan/extension-installer": true
},
"sort-packages": true
},
@@ -38,7 +39,10 @@
"require-dev": {
"doctrine/coding-standard": "^12.0",
"phpbench/phpbench": "^1.0",
"phpstan/phpstan": "1.11.1",
"phpdocumentor/guides-cli": "^1.4",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "1.12.6",
"phpstan/phpstan-deprecation-rules": "^1.2",
"phpunit/phpunit": "^10.4.0",
"psr/log": "^1 || ^2 || ^3",
"squizlabs/php_codesniffer": "3.7.2",

Submodule docs/en/_theme deleted from 6f1bc8bead

View File

@@ -232,6 +232,33 @@ vendors SQL parser to show us further errors in the parsing
process, for example if the Unit would not be one of the supported
values by MySql.
Typed functions
---------------
By default, result of custom functions is fetched as-is from the database driver.
If you want to be sure that the type is always the same, then your custom function needs to
implement ``Doctrine\ORM\Query\AST\TypedExpression``. Then, the result is wired
through ``Doctrine\DBAL\Types\Type::convertToPhpValue()`` of the ``Type`` returned in ``getReturnType()``.
.. code-block:: php
<?php
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\AST\TypedExpression;
class DateDiff extends FunctionNode implements TypedExpression
{
// ...
public function getReturnType(): Type
{
return Type::getType(Types::INTEGER);
}
}
Conclusion
----------

View File

@@ -73,6 +73,8 @@ Advanced Topics
* :doc:`TypedFieldMapper <reference/typedfieldmapper>`
* :doc:`Improving Performance <reference/improving-performance>`
* :doc:`Caching <reference/caching>`
* :doc:`Partial Hydration <reference/partial-hydration>`
* :doc:`Partial Objects <reference/partial-objects>`
* :doc:`Change Tracking Policies <reference/change-tracking-policies>`
* :doc:`Best Practices <reference/best-practices>`
* :doc:`Metadata Drivers <reference/metadata-drivers>`

View File

@@ -15,7 +15,7 @@ Index
- :ref:`#[AttributeOverride] <attrref_attributeoverride>`
- :ref:`#[Column] <attrref_column>`
- :ref:`#[Cache] <attrref_cache>`
- :ref:`#[ChangeTrackingPolicy <attrref_changetrackingpolicy>`
- :ref:`#[ChangeTrackingPolicy] <attrref_changetrackingpolicy>`
- :ref:`#[CustomIdGenerator] <attrref_customidgenerator>`
- :ref:`#[DiscriminatorColumn] <attrref_discriminatorcolumn>`
- :ref:`#[DiscriminatorMap] <attrref_discriminatormap>`

View File

@@ -523,6 +523,34 @@ when the DQL is switched to an arbitrary join.
- HAVING is applied to the results of a query after
aggregation (GROUP BY)
Partial Hydration Syntax
^^^^^^^^^^^^^^^^^^^^^^^^
By default when you run a DQL query in Doctrine and select only a
subset of the fields for a given entity, you do not receive objects
back. Instead, you receive only arrays as a flat rectangular result
set, similar to how you would if you were just using SQL directly
and joining some data.
If you want to select partial objects or fields in array hydration you can use the ``partial``
DQL keyword:
.. code-block:: php
<?php
$query = $em->createQuery('SELECT partial u.{id, username} FROM CmsUser u');
$users = $query->getResult(); // array of partially loaded CmsUser objects
You can use the partial syntax when joining as well:
.. code-block:: php
<?php
$query = $em->createQuery('SELECT partial u.{id, username}, partial a.{id, name} FROM CmsUser u JOIN u.articles a');
$usersArray = $query->getArrayResult(); // array of partially loaded CmsUser and CmsArticle fields
$users = $query->getResult(); // array of partially loaded CmsUser objects
"NEW" Operator Syntax
^^^^^^^^^^^^^^^^^^^^^
@@ -560,7 +588,91 @@ And then use the ``NEW`` DQL keyword :
$query = $em->createQuery('SELECT NEW CustomerDTO(c.name, e.email, a.city, SUM(o.value)) FROM Customer c JOIN c.email e JOIN c.address a JOIN c.orders o GROUP BY c');
$users = $query->getResult(); // array of CustomerDTO
Note that you can only pass scalar expressions to the constructor.
You can also nest several DTO :
.. code-block:: php
<?php
class CustomerDTO
{
public function __construct(string $name, string $email, AddressDTO $address, string|null $value = null)
{
// Bind values to the object properties.
}
}
class AddressDTO
{
public function __construct(string $street, string $city, string $zip)
{
// Bind values to the object properties.
}
}
.. code-block:: php
<?php
$query = $em->createQuery('SELECT NEW CustomerDTO(c.name, e.email, NEW AddressDTO(a.street, a.city, a.zip)) FROM Customer c JOIN c.email e JOIN c.address a');
$users = $query->getResult(); // array of CustomerDTO
Note that you can only pass scalar expressions or other Data Transfer Objects to the constructor.
If you use your data transfer objects for multiple queries, and you would rather not have to
specify arguments that precede the ones you are really interested in, you can use named arguments.
Consider the following DTO, which uses optional arguments:
.. code-block:: php
<?php
class CustomerDTO
{
public function __construct(
public string|null $name = null,
public string|null $email = null,
public string|null $city = null,
public mixed|null $value = null,
public AddressDTO|null $address = null,
) {
}
}
You can specify arbitrary arguments in an arbitrary order by using the named argument syntax, and the ORM will try to match argument names with the selected column names.
The syntax relies on the NAMED keyword, like so:
.. code-block:: php
<?php
$query = $em->createQuery('SELECT NEW NAMED CustomerDTO(a.city, c.name) FROM Customer c JOIN c.address a');
$users = $query->getResult(); // array of CustomerDTO
// CustomerDTO => {name : 'SMITH', email: null, city: 'London', value: null}
ORM will also give precedence to column aliases over column names :
.. code-block:: php
<?php
$query = $em->createQuery('SELECT NEW NAMED CustomerDTO(c.name, CONCAT(a.city, ' ' , a.zip) AS value) FROM Customer c JOIN c.address a');
$users = $query->getResult(); // array of CustomerDTO
// CustomerDTO => {name : 'DOE', email: null, city: null, value: 'New York 10011'}
To define a custom name for a DTO constructor argument, you can either alias the column with the ``AS`` keyword.
The ``NAMED`` keyword must precede all DTO you want to instantiate :
.. code-block:: php
<?php
$query = $em->createQuery('SELECT NEW NAMED CustomerDTO(c.name, NEW NAMED AddressDTO(a.street, a.city, a.zip) AS address) FROM Customer c JOIN c.address a');
$users = $query->getResult(); // array of CustomerDTO
// CustomerDTO => {name : 'DOE', email: null, city: null, value: 'New York 10011'}
If two arguments have the same name, a ``DuplicateFieldException`` is thrown.
If a field cannot be matched with a property name, a ``NoMatchingPropertyException`` is thrown. This typically happens when using functions without aliasing them.
Using INDEX BY
~~~~~~~~~~~~~~
@@ -1324,6 +1436,15 @@ exist mostly internal query hints that are not be consumed in
userland. However the following few hints are to be used in
userland:
- ``Query::HINT_FORCE_PARTIAL_LOAD`` - Allows to hydrate objects
although not all their columns are fetched. This query hint can be
used to handle memory consumption problems with large result-sets
that contain char or binary data. Doctrine has no way of implicitly
reloading this data. Partially loaded objects have to be passed to
``EntityManager::refresh()`` if they are to be reloaded fully from
the database. This query hint is deprecated and will be removed
in the future (\ `Details <https://github.com/doctrine/orm/issues/8471>`_)
- ``Query::HINT_REFRESH`` - This query is used internally by
``EntityManager::refresh()`` and can be used in userland as well.
If you specify this hint and a query returns the data for an entity
@@ -1576,10 +1697,12 @@ Select Expressions
.. code-block:: php
SelectExpression ::= (IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | "(" Subselect ")" | CaseExpression | NewObjectExpression) [["AS"] ["HIDDEN"] AliasResultVariable]
SelectExpression ::= (IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression) [["AS"] ["HIDDEN"] AliasResultVariable]
SimpleSelectExpression ::= (StateFieldPathExpression | IdentificationVariable | FunctionDeclaration | AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] AliasResultVariable]
PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
NewObjectArg ::= ScalarExpression | "(" Subselect ")"
NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]
Conditional Expressions
~~~~~~~~~~~~~~~~~~~~~~~

View File

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

View File

@@ -70,8 +70,8 @@ implements the ``MappingDriver`` interface:
/**
* Loads the metadata for the specified class into the provided container.
*
* @psalm-param class-string<T> $className
* @psalm-param ClassMetadata<T> $metadata
* @param class-string<T> $className
* @param ClassMetadata<T> $metadata
*
* @return void
*
@@ -82,8 +82,7 @@ implements the ``MappingDriver`` interface:
/**
* Gets the names of all mapped classes known to this driver.
*
* @return array<int, string> The names of all mapped classes known to this driver.
* @psalm-return list<class-string>
* @return list<class-string> The names of all mapped classes known to this driver.
*/
public function getAllClassNames();
@@ -91,7 +90,7 @@ implements the ``MappingDriver`` interface:
* Returns whether the class with the specified name should have its metadata loaded.
* This is only the case if it is either mapped as an Entity or a MappedSuperclass.
*
* @psalm-param class-string $className
* @param class-string $className
*
* @return bool
*/

View File

@@ -0,0 +1,15 @@
Partial Hydration
=================
Partial hydration of entities is allowed in the array hydrator, when
only a subset of the fields of an entity are loaded from the database
and the nested results are still created based on the entity relationship structure.
.. code-block:: php
<?php
$users = $em->createQuery("SELECT PARTIAL u.{id,name}, partial a.{id,street} FROM MyApp\Domain\User u JOIN u.addresses a")
->getArrayResult();
This is a useful optimization when you are not interested in all fields of an entity
for performance reasons, for example in use-cases for exporting or rendering lots of data.

View File

@@ -0,0 +1,88 @@
Partial Objects
===============
A partial object is an object whose state is not fully initialized
after being reconstituted from the database and that is
disconnected from the rest of its data. The following section will
describe why partial objects are problematic and what the approach
of Doctrine to this problem is.
.. note::
The partial object problem in general does not apply to
methods or queries where you do not retrieve the query result as
objects. Examples are: ``Query#getArrayResult()``,
``Query#getScalarResult()``, ``Query#getSingleScalarResult()``,
etc.
.. warning::
Use of partial objects is tricky. Fields that are not retrieved
from the database will not be updated by the UnitOfWork even if they
get changed in your objects. You can only promote a partial object
to a fully-loaded object by calling ``EntityManager#refresh()``
or a DQL query with the refresh flag.
What is the problem?
--------------------
In short, partial objects are problematic because they are usually
objects with broken invariants. As such, code that uses these
partial objects tends to be very fragile and either needs to "know"
which fields or methods can be safely accessed or add checks around
every field access or method invocation. The same holds true for
the internals, i.e. the method implementations, of such objects.
You usually simply assume the state you need in the method is
available, after all you properly constructed this object before
you pushed it into the database, right? These blind assumptions can
quickly lead to null reference errors when working with such
partial objects.
It gets worse with the scenario of an optional association (0..1 to
1). When the associated field is NULL, you don't know whether this
object does not have an associated object or whether it was simply
not loaded when the owning object was loaded from the database.
These are reasons why many ORMs do not allow partial objects at all
and instead you always have to load an object with all its fields
(associations being proxied). One secure way to allow partial
objects is if the programming language/platform allows the ORM tool
to hook deeply into the object and instrument it in such a way that
individual fields (not only associations) can be loaded lazily on
first access. This is possible in Java, for example, through
bytecode instrumentation. In PHP though this is not possible, so
there is no way to have "secure" partial objects in an ORM with
transparent persistence.
Doctrine, by default, does not allow partial objects. That means,
any query that only selects partial object data and wants to
retrieve the result as objects (i.e. ``Query#getResult()``) will
raise an exception telling you that partial objects are dangerous.
If you want to force a query to return you partial objects,
possibly as a performance tweak, you can use the ``partial``
keyword as follows:
.. code-block:: php
<?php
$q = $em->createQuery("select partial u.{id,name} from MyApp\Domain\User u");
You can also get a partial reference instead of a proxy reference by
calling:
.. code-block:: php
<?php
$reference = $em->getPartialReference('MyApp\Domain\User', 1);
Partial references are objects with only the identifiers set as they
are passed to the second argument of the ``getPartialReference()`` method.
All other fields are null.
When should I force partial objects?
------------------------------------
Mainly for optimization purposes, but be careful of premature
optimization as partial objects lead to potentially more fragile
code.

View File

@@ -611,3 +611,21 @@ same query of example 6 written using
->add('from', new Expr\From('User', 'u'))
->add('where', new Expr\Comparison('u.id', '=', '?1'))
->add('orderBy', new Expr\OrderBy('u.name', 'ASC'));
Binding Parameters to Placeholders
----------------------------------
It is often not necessary to know about the exact placeholder names when
building a query. You can use a helper method to bind a value to a placeholder
and directly use that placeholder in your query as a return value:
.. code-block:: php
<?php
// $qb instanceof QueryBuilder
$qb->select('u')
->from('User', 'u')
->where('u.email = ' . $qb->createNamedParameter($userInputEmail))
;
// SELECT u FROM User u WHERE email = :dcValue1

View File

@@ -88,7 +88,7 @@ requirement.
A more convenient alternative for explicit transaction demarcation is the use
of provided control abstractions in the form of
``Connection#transactional($func)`` and ``EntityManager#transactional($func)``.
``Connection#transactional($func)`` and ``EntityManager#wrapInTransaction($func)``.
When used, these control abstractions ensure that you never forget to rollback
the transaction, in addition to the obvious code reduction. An example that is
functionally equivalent to the previously shown code looks as follows:
@@ -96,21 +96,23 @@ functionally equivalent to the previously shown code looks as follows:
.. code-block:: php
<?php
// transactional with Connection instance
// $conn instanceof Connection
$conn->transactional(function($conn) {
// ... do some work
$user = new User;
$user->setName('George');
});
// transactional with EntityManager instance
// $em instanceof EntityManager
$em->transactional(function($em) {
$em->wrapInTransaction(function($em) {
// ... do some work
$user = new User;
$user->setName('George');
$em->persist($user);
});
.. warning::
For historical reasons, ``EntityManager#transactional($func)`` will return
``true`` whenever the return value of ``$func`` is loosely false.
Some examples of this include ``array()``, ``"0"``, ``""``, ``0``, and
``null``.
The difference between ``Connection#transactional($func)`` and
``EntityManager#transactional($func)`` is that the latter
abstraction flushes the ``EntityManager`` prior to transaction

View File

@@ -338,10 +338,11 @@ Performance of different deletion strategies
Deleting an object with all its associated objects can be achieved
in multiple ways with very different performance impacts.
1. If an association is marked as ``CASCADE=REMOVE`` Doctrine ORM
will fetch this association. If its a Single association it will
pass this entity to
``EntityManager#remove()``. If the association is a collection, Doctrine will loop over all its elements and pass them to``EntityManager#remove()``.
1. If an association is marked as ``CASCADE=REMOVE`` Doctrine ORM will
fetch this association. If it's a Single association it will pass
this entity to ``EntityManager#remove()``. If the association is a
collection, Doctrine will loop over all its elements and pass them to
``EntityManager#remove()``.
In both cases the cascade remove semantics are applied recursively.
For large object graphs this removal strategy can be very costly.
2. Using a DQL ``DELETE`` statement allows you to delete multiple

View File

@@ -1,80 +1,75 @@
.. toc::
:orphan:
.. tocheader:: Tutorials
.. toctree::
:caption: Tutorials
:depth: 3
.. toctree::
: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
tutorials/ordered-associations
tutorials/override-field-association-mappings-in-subclasses
tutorials/pagination
tutorials/embeddables
tutorials/getting-started
tutorials/getting-started-database
tutorials/getting-started-models
tutorials/working-with-indexed-associations
tutorials/extra-lazy-associations
tutorials/composite-primary-keys
tutorials/ordered-associations
tutorials/override-field-association-mappings-in-subclasses
tutorials/pagination
tutorials/embeddables
.. toctree::
:caption: Reference
:depth: 3
.. toc::
reference/architecture
reference/configuration
reference/faq
reference/basic-mapping
reference/association-mapping
reference/inheritance-mapping
reference/working-with-objects
reference/working-with-associations
reference/typedfieldmapper
reference/events
reference/unitofwork
reference/unitofwork-associations
reference/transactions-and-concurrency
reference/batch-processing
reference/dql-doctrine-query-language
reference/query-builder
reference/native-sql
reference/change-tracking-policies
reference/partial-hydration
reference/partial-objects
reference/attributes-reference
reference/xml-mapping
reference/php-mapping
reference/caching
reference/improving-performance
reference/tools
reference/metadata-drivers
reference/best-practices
reference/limitations-and-known-issues
tutorials/pagination
reference/filters
reference/namingstrategy
reference/advanced-configuration
reference/second-level-cache
reference/security
.. tocheader:: Reference
.. toctree::
:caption: Cookbook
:depth: 3
.. toctree::
:depth: 3
reference/architecture
reference/configuration
reference/faq
reference/basic-mapping
reference/association-mapping
reference/inheritance-mapping
reference/working-with-objects
reference/working-with-associations
reference/typedfieldmapper
reference/events
reference/unitofwork
reference/unitofwork-associations
reference/transactions-and-concurrency
reference/batch-processing
reference/dql-doctrine-query-language
reference/query-builder
reference/native-sql
reference/change-tracking-policies
reference/attributes-reference
reference/xml-mapping
reference/php-mapping
reference/caching
reference/improving-performance
reference/tools
reference/metadata-drivers
reference/best-practices
reference/limitations-and-known-issues
tutorials/pagination
reference/filters
reference/namingstrategy
reference/advanced-configuration
reference/second-level-cache
reference/security
.. toc::
.. tocheader:: Cookbook
.. toctree::
:depth: 3
cookbook/aggregate-fields
cookbook/custom-mapping-types
cookbook/decorator-pattern
cookbook/dql-custom-walkers
cookbook/dql-user-defined-functions
cookbook/implementing-arrayaccess-for-domain-objects
cookbook/resolve-target-entity-listener
cookbook/sql-table-prefixes
cookbook/strategy-cookbook-introduction
cookbook/validation-of-entities
cookbook/working-with-datetime
cookbook/mysql-enums
cookbook/advanced-field-value-conversion-using-custom-mapping-types
cookbook/entities-in-session
cookbook/aggregate-fields
cookbook/custom-mapping-types
cookbook/decorator-pattern
cookbook/dql-custom-walkers
cookbook/dql-user-defined-functions
cookbook/implementing-arrayaccess-for-domain-objects
cookbook/resolve-target-entity-listener
cookbook/sql-table-prefixes
cookbook/strategy-cookbook-introduction
cookbook/validation-of-entities
cookbook/working-with-datetime
cookbook/mysql-enums
cookbook/advanced-field-value-conversion-using-custom-mapping-types
cookbook/entities-in-session

View File

@@ -145,7 +145,7 @@ We keep up the example of an Article with arbitrary attributes, the mapping look
#[OneToMany(targetEntity: ArticleAttribute::class, mappedBy: 'article', cascade: ['ALL'], indexBy: 'attribute')]
private Collection $attributes;
public function addAttribute(string $name, ArticleAttribute $value): void
public function addAttribute(string $name, string $value): void
{
$this->attributes[$name] = new ArticleAttribute($name, $value, $this);
}

View File

@@ -139,12 +139,12 @@ step:
// Create a simple "default" Doctrine ORM configuration for Attributes
$config = ORMSetup::createAttributeMetadataConfiguration(
paths: array(__DIR__."/src"),
paths: [__DIR__ . '/src'],
isDevMode: true,
);
// or if you prefer XML
// $config = ORMSetup::createXMLMetadataConfiguration(
// paths: array(__DIR__."/config/xml"),
// paths: [__DIR__ . '/config/xml'],
// isDevMode: true,
//);

View File

@@ -14,7 +14,6 @@
<file>src</file>
<file>tests</file>
<exclude-pattern>*/src/Mapping/InverseJoinColumn.php</exclude-pattern>
<exclude-pattern>*/tests/Tests/Proxies/__CG__*</exclude-pattern>
<exclude-pattern>*/tests/Tests/ORM/Tools/Export/export/*</exclude-pattern>

View File

@@ -27,3 +27,10 @@ parameters:
message: '#Negated boolean expression is always false\.#'
paths:
- src/Mapping/Driver/AttributeDriver.php
-
message: '~^Call to deprecated method getEventManager\(\) of class Doctrine\\DBAL\\Connection\.$~'
path: src/EntityManager.php
-
message: '~deprecated class Doctrine\\DBAL\\Tools\\Console\\Command\\ReservedWordsCommand\:~'
path: src/Tools/Console/ConsoleRunner.php

View File

@@ -212,6 +212,10 @@
</ParamNameMismatch>
</file>
<file src="src/Internal/Hydration/AbstractHydrator.php">
<PossiblyUndefinedArrayOffset>
<code><![CDATA[$newObject['args']]]></code>
<code><![CDATA[$newObject['args']]]></code>
</PossiblyUndefinedArrayOffset>
<ReferenceConstraintViolation>
<code><![CDATA[return $rowData;]]></code>
<code><![CDATA[return $rowData;]]></code>
@@ -228,9 +232,6 @@
<code><![CDATA[$result[$resultKey]]]></code>
<code><![CDATA[$result[$resultKey]]]></code>
</PossiblyNullArrayAssignment>
<PossiblyUndefinedArrayOffset>
<code><![CDATA[$newObject['args']]]></code>
</PossiblyUndefinedArrayOffset>
<ReferenceConstraintViolation>
<code><![CDATA[$result]]></code>
</ReferenceConstraintViolation>
@@ -265,9 +266,6 @@
<code><![CDATA[setValue]]></code>
<code><![CDATA[setValue]]></code>
</PossiblyNullReference>
<PossiblyUndefinedArrayOffset>
<code><![CDATA[$newObject['args']]]></code>
</PossiblyUndefinedArrayOffset>
</file>
<file src="src/Mapping/AssociationMapping.php">
<LessSpecificReturnStatement>
@@ -404,6 +402,7 @@
<file src="src/Mapping/DefaultTypedFieldMapper.php">
<LessSpecificReturnStatement>
<code><![CDATA[$mapping]]></code>
<code><![CDATA[$mapping]]></code>
</LessSpecificReturnStatement>
<MoreSpecificReturnType>
<code><![CDATA[array]]></code>
@@ -497,13 +496,8 @@
<InvalidPropertyAssignmentValue>
<code><![CDATA[$metadata->table]]></code>
</InvalidPropertyAssignmentValue>
<InvalidPropertyFetch>
<code><![CDATA[$xmlRoot->{'discriminator-column'}]]></code>
<code><![CDATA[$xmlRoot->{'discriminator-map'}]]></code>
</InvalidPropertyFetch>
<InvalidReturnStatement>
<code><![CDATA[$mapping]]></code>
<code><![CDATA[$result]]></code>
<code><![CDATA[[
'usage' => $usage,
'region' => $region,
@@ -527,20 +521,10 @@
* options?: array
* }]]></code>
<code><![CDATA[array{usage: int|null, region?: string}]]></code>
<code><![CDATA[loadMappingFile]]></code>
</InvalidReturnType>
<MoreSpecificImplementedParamType>
<code><![CDATA[$metadata]]></code>
</MoreSpecificImplementedParamType>
<NoInterfaceProperties>
<code><![CDATA[$xmlRoot->{'discriminator-column'}]]></code>
<code><![CDATA[$xmlRoot->{'discriminator-map'}]]></code>
</NoInterfaceProperties>
<TypeDoesNotContainType>
<code><![CDATA[$xmlRoot->getName() === 'embeddable']]></code>
<code><![CDATA[$xmlRoot->getName() === 'entity']]></code>
<code><![CDATA[$xmlRoot->getName() === 'mapped-superclass']]></code>
</TypeDoesNotContainType>
</file>
<file src="src/Mapping/ManyToManyInverseSideMapping.php">
<PropertyNotSetInConstructor>
@@ -756,7 +740,9 @@
<code><![CDATA[$autoGenerate > 4]]></code>
</TypeDoesNotContainType>
<UndefinedMethod>
<code><![CDATA[self::createLazyGhost($initializer, $skippedProperties)]]></code>
<code><![CDATA[self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void {
$initializer($object, $identifier);
}, $skippedProperties)]]></code>
</UndefinedMethod>
<UnresolvableInclude>
<code><![CDATA[require $fileName]]></code>
@@ -938,6 +924,9 @@
<ArgumentTypeCoercion>
<code><![CDATA[$stringPattern]]></code>
</ArgumentTypeCoercion>
<DeprecatedMethod>
<code><![CDATA[setSqlExecutor]]></code>
</DeprecatedMethod>
<InvalidNullableReturnType>
<code><![CDATA[AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement]]></code>
</InvalidNullableReturnType>
@@ -1128,6 +1117,12 @@
</RedundantConditionGivenDocblockType>
</file>
<file src="src/Tools/Pagination/LimitSubqueryOutputWalker.php">
<InvalidReturnStatement>
<code><![CDATA[$abstractSqlExecutor->getSqlStatements()]]></code>
</InvalidReturnStatement>
<InvalidReturnType>
<code><![CDATA[string]]></code>
</InvalidReturnType>
<PossiblyFalseArgument>
<code><![CDATA[strrpos($orderByItemString, ' ')]]></code>
</PossiblyFalseArgument>

View File

@@ -22,8 +22,8 @@ class CollectionCacheKey extends CacheKey
public readonly array $ownerIdentifier;
/**
* @param class-string $entityClass The owner entity class.
* @param array<string, mixed> $ownerIdentifier The identifier of the owning entity.
* @param class-string $entityClass The owner entity class
*/
public function __construct(
public readonly string $entityClass,

View File

@@ -16,6 +16,7 @@ use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\UnitOfWork;
use function array_map;
@@ -210,6 +211,10 @@ class DefaultQueryCache implements QueryCache
throw FeatureNotImplemented::nonSelectStatements();
}
if (($hints[SqlWalker::HINT_PARTIAL] ?? false) === true || ($hints[Query::HINT_FORCE_PARTIAL_LOAD] ?? false) === true) {
throw FeatureNotImplemented::partialEntities();
}
if (! ($key->cacheMode & Cache::MODE_PUT)) {
return false;
}

View File

@@ -11,8 +11,8 @@ use function array_map;
class EntityCacheEntry implements CacheEntry
{
/**
* @param array<string,mixed> $data The entity map data
* @psalm-param class-string $class The entity class name
* @param class-string $class The entity class name
* @param array<string,mixed> $data The entity map data
*/
public function __construct(
public readonly string $class,

View File

@@ -20,4 +20,9 @@ class FeatureNotImplemented extends CacheException
{
return new self('Second-level cache query supports only select statements.');
}
public static function partialEntities(): self
{
return new self('Second level cache does not support partial entities.');
}
}

View File

@@ -263,8 +263,8 @@ class Configuration extends \Doctrine\DBAL\Configuration
*
* Any previously added numeric functions are discarded.
*
* @psalm-param array<string, class-string> $functions The map of custom
* DQL numeric functions.
* @param array<string, class-string> $functions The map of custom
* DQL numeric functions.
*/
public function setCustomNumericFunctions(array $functions): void
{
@@ -291,7 +291,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
/**
* Gets the implementation class name of a registered custom date/time DQL function.
*
* @psalm-return class-string|callable|null
* @return class-string|callable|null
*/
public function getCustomDatetimeFunction(string $name): string|callable|null
{
@@ -351,7 +351,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
/**
* Gets the hydrator class for the given hydration mode name.
*
* @psalm-return class-string<AbstractHydrator>|null
* @return class-string<AbstractHydrator>|null
*/
public function getCustomHydrationMode(string $modeName): string|null
{
@@ -361,7 +361,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
/**
* Adds a custom hydration mode.
*
* @psalm-param class-string<AbstractHydrator> $hydrator
* @param class-string<AbstractHydrator> $hydrator
*/
public function addCustomHydrationMode(string $modeName, string $hydrator): void
{
@@ -371,14 +371,14 @@ class Configuration extends \Doctrine\DBAL\Configuration
/**
* Sets a class metadata factory.
*
* @psalm-param class-string $cmfName
* @param class-string $cmfName
*/
public function setClassMetadataFactoryName(string $cmfName): void
{
$this->attributes['classMetadataFactoryName'] = $cmfName;
}
/** @psalm-return class-string */
/** @return class-string */
public function getClassMetadataFactoryName(): string
{
if (! isset($this->attributes['classMetadataFactoryName'])) {
@@ -391,8 +391,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
/**
* Adds a filter to the list of possible filters.
*
* @param string $className The class name of the filter.
* @psalm-param class-string<SQLFilter> $className
* @param class-string<SQLFilter> $className The class name of the filter.
*/
public function addFilter(string $name, string $className): void
{
@@ -402,9 +401,8 @@ class Configuration extends \Doctrine\DBAL\Configuration
/**
* Gets the class name for a given filter name.
*
* @return string|null The class name of the filter, or null if it is not
* defined.
* @psalm-return class-string<SQLFilter>|null
* @return class-string<SQLFilter>|null The class name of the filter,
* or null if it is not defined.
*/
public function getFilterClassName(string $name): string|null
{
@@ -414,7 +412,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
/**
* Sets default repository class.
*
* @psalm-param class-string<EntityRepository> $className
* @param class-string<EntityRepository> $className
*
* @throws InvalidEntityRepository If $classname is not an ObjectRepository.
*/
@@ -430,7 +428,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
/**
* Get default repository class.
*
* @psalm-return class-string<EntityRepository>
* @return class-string<EntityRepository>
*/
public function getDefaultRepositoryClassName(): string
{

View File

@@ -24,7 +24,6 @@ use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\Query\FilterCollection;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\Repository\RepositoryFactory;
use Throwable;
use function array_keys;
use function is_array;
@@ -178,18 +177,24 @@ class EntityManager implements EntityManagerInterface
{
$this->conn->beginTransaction();
$successful = false;
try {
$return = $func($this);
$this->flush();
$this->conn->commit();
return $return;
} catch (Throwable $e) {
$this->close();
$this->conn->rollBack();
$successful = true;
throw $e;
return $return;
} finally {
if (! $successful) {
$this->close();
if ($this->conn->isTransactionActive()) {
$this->conn->rollBack();
}
}
}
}
@@ -479,9 +484,9 @@ class EntityManager implements EntityManagerInterface
/**
* Gets the repository for an entity class.
*
* @psalm-param class-string<T> $className
* @param class-string<T> $className The name of the entity.
*
* @psalm-return EntityRepository<T>
* @return EntityRepository<T> The repository class.
*
* @template T of object
*/

View File

@@ -22,9 +22,9 @@ interface EntityManagerInterface extends ObjectManager
/**
* {@inheritDoc}
*
* @psalm-param class-string<T> $className
* @param class-string<T> $className
*
* @psalm-return EntityRepository<T>
* @return EntityRepository<T>
*
* @template T of object
*/
@@ -151,11 +151,10 @@ interface EntityManagerInterface extends ObjectManager
* Gets a reference to the entity identified by the given type and identifier
* without actually loading it, if the entity is not yet loaded.
*
* @param string $entityName The name of the entity type.
* @param mixed $id The entity identifier.
* @psalm-param class-string<T> $entityName
* @param class-string<T> $entityName The name of the entity type.
* @param mixed $id The entity identifier.
*
* @psalm-return T|null
* @return T|null The entity reference.
*
* @throws ORMException
*
@@ -232,7 +231,7 @@ interface EntityManagerInterface extends ObjectManager
/**
* {@inheritDoc}
*
* @psalm-param string|class-string<T> $className
* @param string|class-string<T> $className
*
* @psalm-return ($className is class-string<T> ? Mapping\ClassMetadata<T> : Mapping\ClassMetadata<object>)
*

View File

@@ -35,11 +35,11 @@ use function substr;
*/
class EntityRepository implements ObjectRepository, Selectable
{
/** @psalm-var class-string<T> */
/** @var class-string<T> */
private readonly string $entityName;
private static Inflector|null $inflector = null;
/** @psalm-param ClassMetadata<T> $class */
/** @param ClassMetadata<T> $class */
public function __construct(
private readonly EntityManagerInterface $em,
private readonly ClassMetadata $class,
@@ -131,6 +131,7 @@ class EntityRepository implements ObjectRepository, Selectable
* @psalm-param array<string, mixed> $criteria
*
* @return int The cardinality of the objects that match the given criteria.
* @psalm-return 0|positive-int
*
* @todo Add this method to `ObjectRepository` interface in the next major release
*/
@@ -168,7 +169,7 @@ class EntityRepository implements ObjectRepository, Selectable
));
}
/** @psalm-return class-string<T> */
/** @return class-string<T> */
protected function getEntityName(): string
{
return $this->entityName;

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use LogicException;
use function sprintf;
class DuplicateFieldException extends LogicException implements ORMException
{
public static function create(string $argName, string $columnName): self
{
return new self(sprintf('Name "%s" for "%s" already in use.', $argName, $columnName));
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use LogicException;
use function sprintf;
class NoMatchingPropertyException extends LogicException implements ORMException
{
public static function create(string $property): self
{
return new self(sprintf('Column name "%s" does not match any property name. Consider aliasing it to the name of an existing property.', $property));
}
}

View File

@@ -252,15 +252,16 @@ abstract class AbstractHydrator
* @psalm-return array{
* data: array<array-key, array>,
* newObjects?: array<array-key, array{
* class: mixed,
* args?: array
* class: ReflectionClass,
* args: array,
* obj: object
* }>,
* scalars?: array
* }
*/
protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents): array
{
$rowData = ['data' => []];
$rowData = ['data' => [], 'newObjects' => []];
foreach ($data as $key => $value) {
$cacheKeyInfo = $this->hydrateColumnInfo($key);
@@ -335,6 +336,25 @@ abstract class AbstractHydrator
}
}
foreach ($this->resultSetMapping()->nestedNewObjectArguments as $objIndex => ['ownerIndex' => $ownerIndex, 'argIndex' => $argIndex]) {
if (! isset($rowData['newObjects'][$ownerIndex . ':' . $argIndex])) {
continue;
}
$newObject = $rowData['newObjects'][$ownerIndex . ':' . $argIndex];
unset($rowData['newObjects'][$ownerIndex . ':' . $argIndex]);
$obj = $newObject['class']->newInstanceArgs($newObject['args']);
$rowData['newObjects'][$ownerIndex]['args'][$argIndex] = $obj;
}
foreach ($rowData['newObjects'] as $objIndex => $newObject) {
$obj = $newObject['class']->newInstanceArgs($newObject['args']);
$rowData['newObjects'][$objIndex]['obj'] = $obj;
}
return $rowData;
}

View File

@@ -214,9 +214,8 @@ class ArrayHydrator extends AbstractHydrator
$scalarCount = (isset($rowData['scalars']) ? count($rowData['scalars']) : 0);
foreach ($rowData['newObjects'] as $objIndex => $newObject) {
$class = $newObject['class'];
$args = $newObject['args'];
$obj = $class->newInstanceArgs($args);
$args = $newObject['args'];
$obj = $newObject['obj'];
if (count($args) === $scalarCount || ($scalarCount === 0 && count($rowData['newObjects']) === 1)) {
$result[$resultKey] = $obj;

View File

@@ -64,4 +64,9 @@ class HydrationException extends Exception implements ORMException
implode('", "', $discrValues),
));
}
public static function partialObjectHydrationDisallowed(): self
{
return new self('Hydration of entity objects is not allowed when DQL PARTIAL keyword is used.');
}
}

View File

@@ -265,7 +265,7 @@ class ObjectHydrator extends AbstractHydrator
}
/**
* @psalm-param class-string $className
* @param class-string $className
* @psalm-param array<string, mixed> $data
*/
private function getEntityFromIdentityMap(string $className, array $data): object|bool
@@ -356,11 +356,15 @@ class ObjectHydrator extends AbstractHydrator
$parentObject = $this->resultPointers[$parentAlias];
} else {
// Parent object of relation not found, mark as not-fetched again
$element = $this->getEntity($data, $dqlAlias);
if (isset($nonemptyComponents[$dqlAlias])) {
$element = $this->getEntity($data, $dqlAlias);
// Update result pointer and provide initial fetch data for parent
$this->resultPointers[$dqlAlias] = $element;
$rowData['data'][$parentAlias][$relationField] = $element;
// Update result pointer and provide initial fetch data for parent
$this->resultPointers[$dqlAlias] = $element;
$rowData['data'][$parentAlias][$relationField] = $element;
} else {
$element = null;
}
// Mark as not-fetched again
unset($this->hints['fetched'][$parentAlias][$relationField]);
@@ -552,9 +556,7 @@ class ObjectHydrator extends AbstractHydrator
$scalarCount = (isset($rowData['scalars']) ? count($rowData['scalars']) : 0);
foreach ($rowData['newObjects'] as $objIndex => $newObject) {
$class = $newObject['class'];
$args = $newObject['args'];
$obj = $class->newInstanceArgs($args);
$obj = $newObject['obj'];
if ($scalarCount === 0 && count($rowData['newObjects']) === 1) {
$result[$resultKey] = $obj;

View File

@@ -169,8 +169,8 @@ class ClassMetadataBuilder
/**
* Sets the discriminator column details.
*
* @psalm-param class-string<BackedEnum>|null $enumType
* @psalm-param array<string, mixed> $options
* @param class-string<BackedEnum>|null $enumType
* @param array<string, mixed> $options
*
* @return $this
*/

View File

@@ -7,6 +7,7 @@ namespace Doctrine\ORM\Mapping;
use BackedEnum;
use BadMethodCallException;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Types;
use Doctrine\Deprecations\Deprecation;
use Doctrine\Instantiator\Instantiator;
use Doctrine\Instantiator\InstantiatorInterface;
@@ -23,6 +24,7 @@ use ReflectionNamedType;
use ReflectionProperty;
use Stringable;
use function array_column;
use function array_diff;
use function array_intersect;
use function array_key_exists;
@@ -34,6 +36,7 @@ use function array_values;
use function assert;
use function class_exists;
use function count;
use function defined;
use function enum_exists;
use function explode;
use function in_array;
@@ -1039,6 +1042,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
*/
public function getColumnName(string $fieldName): string
{
// @phpstan-ignore property.deprecated
return $this->columnNames[$fieldName] ?? $fieldName;
}
@@ -1118,9 +1122,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
{
$field = $this->reflClass->getProperty($mapping['fieldName']);
$mapping = $this->typedFieldMapper->validateAndComplete($mapping, $field);
return $mapping;
return $this->typedFieldMapper->validateAndComplete($mapping, $field);
}
/**
@@ -1188,6 +1190,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
$mapping->quoted = true;
}
// @phpstan-ignore property.deprecated
$this->columnNames[$mapping->fieldName] = $mapping->columnName;
if (isset($this->fieldNames[$mapping->columnName]) || ($this->discriminatorColumn && $this->discriminatorColumn->name === $mapping->columnName)) {
@@ -1230,6 +1233,14 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
if (! empty($mapping->id)) {
$this->containsEnumIdentifier = true;
}
if (
defined('Doctrine\DBAL\Types\Types::ENUM')
&& $mapping->type === Types::ENUM
&& ! isset($mapping->options['values'])
) {
$mapping->options['values'] = array_column($mapping->enumType::cases(), 'value');
}
}
return $mapping;
@@ -1768,6 +1779,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
unset($this->fieldMappings[$fieldName]);
unset($this->fieldNames[$mapping->columnName]);
// @phpstan-ignore property.deprecated
unset($this->columnNames[$mapping->fieldName]);
$overrideMapping = $this->validateAndCompleteFieldMapping($overrideMapping);
@@ -1918,8 +1930,9 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
public function addInheritedFieldMapping(FieldMapping $fieldMapping): void
{
$this->fieldMappings[$fieldMapping->fieldName] = $fieldMapping;
$this->columnNames[$fieldMapping->fieldName] = $fieldMapping->columnName;
$this->fieldNames[$fieldMapping->columnName] = $fieldMapping->fieldName;
// @phpstan-ignore property.deprecated
$this->columnNames[$fieldMapping->fieldName] = $fieldMapping->columnName;
$this->fieldNames[$fieldMapping->columnName] = $fieldMapping->fieldName;
if (isset($fieldMapping->generated)) {
$this->requiresFetchAfterChange = true;

View File

@@ -399,7 +399,7 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
/**
* Gets the lower-case short name of a class.
*
* @psalm-param class-string $className
* @param class-string $className
*/
private function getShortName(string $className): string
{

View File

@@ -11,7 +11,7 @@ use function trim;
*/
class DefaultEntityListenerResolver implements EntityListenerResolver
{
/** @psalm-var array<class-string, object> Map to store entity listener instances. */
/** @var array<class-string, object> Map to store entity listener instances. */
private array $instances = [];
public function clear(string|null $className = null): void

View File

@@ -16,6 +16,7 @@ use ReflectionProperty;
use function array_merge;
use function assert;
use function defined;
use function enum_exists;
use function is_a;
@@ -49,30 +50,40 @@ final class DefaultTypedFieldMapper implements TypedFieldMapper
{
$type = $field->getType();
if (! $type instanceof ReflectionNamedType) {
return $mapping;
}
if (
! isset($mapping['type'])
&& ($type instanceof ReflectionNamedType)
! $type->isBuiltin()
&& enum_exists($type->getName())
&& (! isset($mapping['type']) || (
defined('Doctrine\DBAL\Types\Types::ENUM')
&& $mapping['type'] === Types::ENUM
))
) {
if (! $type->isBuiltin() && enum_exists($type->getName())) {
$reflection = new ReflectionEnum($type->getName());
if (! $reflection->isBacked()) {
throw MappingException::backedEnumTypeRequired(
$field->class,
$mapping['fieldName'],
$type->getName(),
);
}
assert(is_a($type->getName(), BackedEnum::class, true));
$mapping['enumType'] = $type->getName();
$type = $reflection->getBackingType();
assert($type instanceof ReflectionNamedType);
$reflection = new ReflectionEnum($type->getName());
if (! $reflection->isBacked()) {
throw MappingException::backedEnumTypeRequired(
$field->class,
$mapping['fieldName'],
$type->getName(),
);
}
if (isset($this->typedFieldMappings[$type->getName()])) {
$mapping['type'] = $this->typedFieldMappings[$type->getName()];
}
assert(is_a($type->getName(), BackedEnum::class, true));
$mapping['enumType'] = $type->getName();
$type = $reflection->getBackingType();
assert($type instanceof ReflectionNamedType);
}
if (isset($mapping['type'])) {
return $mapping;
}
if (isset($this->typedFieldMappings[$type->getName()])) {
$mapping['type'] = $this->typedFieldMappings[$type->getName()];
}
return $mapping;

View File

@@ -69,8 +69,8 @@ class AttributeDriver implements MappingDriver
/**
* {@inheritDoc}
*
* @psalm-param class-string<T> $className
* @psalm-param ClassMetadata<T> $metadata
* @param class-string<T> $className
* @param ClassMetadata<T> $metadata
*
* @template T of object
*/
@@ -390,7 +390,7 @@ class AttributeDriver implements MappingDriver
$metadata->mapOneToMany($mapping);
} elseif ($manyToOneAttribute !== null) {
if ($metadata->isEmbeddedClass) {
throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\OneToMany::class);
throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\ManyToOne::class);
}
$idAttribute = $this->reader->getPropertyAttribute($property, Mapping\Id::class);

View File

@@ -35,6 +35,8 @@ use function strtolower;
/**
* The DatabaseDriver reverse engineers the mapping metadata from a database.
*
* @deprecated No replacement planned
*
* @link www.doctrine-project.org
*/
class DatabaseDriver implements MappingDriver
@@ -151,8 +153,8 @@ class DatabaseDriver implements MappingDriver
/**
* {@inheritDoc}
*
* @psalm-param class-string<T> $className
* @psalm-param ClassMetadata<T> $metadata
* @param class-string<T> $className
* @param ClassMetadata<T> $metadata
*
* @template T of object
*/
@@ -491,7 +493,7 @@ class DatabaseDriver implements MappingDriver
/**
* Returns the mapped class name for a table if it exists. Otherwise return "classified" version.
*
* @psalm-return class-string
* @return class-string
*/
private function getClassNameForTable(string $tableName): string
{

View File

@@ -38,6 +38,8 @@ use function strtoupper;
* XmlDriver is a metadata driver that enables mapping through XML files.
*
* @link www.doctrine-project.org
*
* @template-extends FileDriver<SimpleXMLElement>
*/
class XmlDriver extends FileDriver
{
@@ -70,15 +72,14 @@ class XmlDriver extends FileDriver
/**
* {@inheritDoc}
*
* @psalm-param class-string<T> $className
* @psalm-param ClassMetadata<T> $metadata
* @param class-string<T> $className
* @param ClassMetadata<T> $metadata
*
* @template T of object
*/
public function loadMetadataForClass($className, PersistenceClassMetadata $metadata): void
{
$xmlRoot = $this->getElement($className);
assert($xmlRoot instanceof SimpleXMLElement);
if ($xmlRoot->getName() === 'entity') {
if (isset($xmlRoot['repository-class'])) {
@@ -134,6 +135,7 @@ class XmlDriver extends FileDriver
];
if (isset($discrColumn['options'])) {
assert($discrColumn['options'] instanceof SimpleXMLElement);
$columnDef['options'] = $this->parseOptions($discrColumn['options']->children());
}
@@ -145,6 +147,7 @@ class XmlDriver extends FileDriver
// Evaluate <discriminator-map...>
if (isset($xmlRoot->{'discriminator-map'})) {
$map = [];
assert($xmlRoot->{'discriminator-map'}->{'discriminator-mapping'} instanceof SimpleXMLElement);
foreach ($xmlRoot->{'discriminator-map'}->{'discriminator-mapping'} as $discrMapElement) {
$map[(string) $discrMapElement['value']] = (string) $discrMapElement['class'];
}
@@ -408,6 +411,7 @@ class XmlDriver extends FileDriver
/** @psalm-suppress DeprecatedConstant */
$orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
? (string) $orderByField['direction']
// @phpstan-ignore classConstant.deprecated
: (enum_exists(Order::class) ? Order::Ascending->value : Criteria::ASC);
}
@@ -537,6 +541,7 @@ class XmlDriver extends FileDriver
/** @psalm-suppress DeprecatedConstant */
$orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
? (string) $orderByField['direction']
// @phpstan-ignore classConstant.deprecated
: (enum_exists(Order::class) ? Order::Ascending->value : Criteria::ASC);
}
@@ -886,19 +891,19 @@ class XmlDriver extends FileDriver
if (isset($xmlElement->entity)) {
foreach ($xmlElement->entity as $entityElement) {
/** @psalm-var class-string $entityName */
/** @var class-string $entityName */
$entityName = (string) $entityElement['name'];
$result[$entityName] = $entityElement;
}
} elseif (isset($xmlElement->{'mapped-superclass'})) {
foreach ($xmlElement->{'mapped-superclass'} as $mappedSuperClass) {
/** @psalm-var class-string $className */
/** @var class-string $className */
$className = (string) $mappedSuperClass['name'];
$result[$className] = $mappedSuperClass;
}
} elseif (isset($xmlElement->embeddable)) {
foreach ($xmlElement->embeddable as $embeddableElement) {
/** @psalm-var class-string $embeddableName */
/** @var class-string $embeddableName */
$embeddableName = (string) $embeddableElement['name'];
$result[$embeddableName] = $embeddableElement;
}

View File

@@ -2,7 +2,6 @@
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;

View File

@@ -10,7 +10,7 @@ use Doctrine\ORM\EntityRepository;
#[Attribute(Attribute::TARGET_CLASS)]
final class MappedSuperclass implements MappingAttribute
{
/** @psalm-param class-string<EntityRepository>|null $repositoryClass */
/** @param class-string<EntityRepository>|null $repositoryClass */
public function __construct(
public readonly string|null $repositoryClass = null,
) {

View File

@@ -40,7 +40,15 @@ class NativeQuery extends AbstractQuery
$types = [];
foreach ($this->getParameters() as $parameter) {
$name = $parameter->getName();
$name = $parameter->getName();
if ($parameter->typeWasSpecified()) {
$parameters[$name] = $parameter->getValue();
$types[$name] = $parameter->getType();
continue;
}
$value = $this->processParameterValue($parameter->getValue());
$type = $parameter->getValue() === $value
? $parameter->getType()

View File

@@ -8,13 +8,18 @@ use BadMethodCallException;
use Doctrine\Common\Collections\Criteria;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\EntityNotFoundException;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\Mapping\OneToManyAssociationMapping;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Utility\PersisterHelper;
use function array_fill;
use function array_keys;
use function array_reverse;
use function array_values;
use function assert;
use function count;
use function implode;
use function is_int;
use function is_string;
@@ -146,7 +151,11 @@ class OneToManyPersister extends AbstractCollectionPersister
throw new BadMethodCallException('Filtering a collection by Criteria is not supported by this CollectionPersister.');
}
/** @throws DBALException */
/**
* @throws DBALException
* @throws EntityNotFoundException
* @throws MappingException
*/
private function deleteEntityCollection(PersistentCollection $collection): int
{
$mapping = $this->getMapping($collection);
@@ -166,6 +175,16 @@ class OneToManyPersister extends AbstractCollectionPersister
$statement = 'DELETE FROM ' . $this->quoteStrategy->getTableName($targetClass, $this->platform)
. ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
if ($targetClass->isInheritanceTypeSingleTable()) {
$discriminatorColumn = $targetClass->getDiscriminatorColumn();
$discriminatorValues = $targetClass->discriminatorValue ? [$targetClass->discriminatorValue] : array_keys($targetClass->discriminatorMap);
$statement .= ' AND ' . $discriminatorColumn->name . ' IN (' . implode(', ', array_fill(0, count($discriminatorValues), '?')) . ')';
foreach ($discriminatorValues as $discriminatorValue) {
$parameters[] = $discriminatorValue;
$types[] = $discriminatorColumn->type;
}
}
$numAffected = $this->conn->executeStatement($statement, $parameters, $types);
assert(is_int($numAffected));

View File

@@ -792,17 +792,42 @@ class BasicEntityPersister implements EntityPersister
$computedIdentifier = [];
/** @var array<string,mixed>|null $sourceEntityData */
$sourceEntityData = null;
// TRICKY: since the association is specular source and target are flipped
foreach ($owningAssoc->targetToSourceKeyColumns as $sourceKeyColumn => $targetKeyColumn) {
if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) {
throw MappingException::joinColumnMustPointToMappedField(
$sourceClass->name,
$sourceKeyColumn,
);
}
// The likely case here is that the column is a join column
// in an association mapping. However, there is no guarantee
// at this point that a corresponding (generally identifying)
// association has been mapped in the source entity. To handle
// this case we directly reference the column-keyed data used
// to initialize the source entity before throwing an exception.
$resolvedSourceData = false;
if (! isset($sourceEntityData)) {
$sourceEntityData = $this->em->getUnitOfWork()->getOriginalEntityData($sourceEntity);
}
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
$sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
if (isset($sourceEntityData[$sourceKeyColumn])) {
$dataValue = $sourceEntityData[$sourceKeyColumn];
if ($dataValue !== null) {
$resolvedSourceData = true;
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
$dataValue;
}
}
if (! $resolvedSourceData) {
throw MappingException::joinColumnMustPointToMappedField(
$sourceClass->name,
$sourceKeyColumn,
);
}
} else {
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
$sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
}
}
$targetEntity = $this->load($computedIdentifier, null, $assoc);
@@ -835,7 +860,10 @@ class BasicEntityPersister implements EntityPersister
? $this->expandCriteriaParameters($criteria)
: $this->expandParameters($criteria);
return (int) $this->conn->executeQuery($sql, $params, $types)->fetchOne();
$count = (int) $this->conn->executeQuery($sql, $params, $types)->fetchOne();
assert($count >= 0);
return $count;
}
/**

View File

@@ -125,6 +125,8 @@ interface EntityPersister
* Count entities (optionally filtered by a criteria)
*
* @param mixed[]|Criteria $criteria
*
* @psalm-return 0|positive-int
*/
public function count(array|Criteria $criteria = []): int;

View File

@@ -210,15 +210,14 @@ EOPHP;
/**
* Creates a closure capable of initializing a proxy
*
* @return Closure(InternalProxy, InternalProxy):void
* @return Closure(InternalProxy, array):void
*
* @throws EntityNotFoundException
*/
private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure
{
return static function (InternalProxy $proxy) use ($entityPersister, $classMetadata, $identifierFlattener): void {
$identifier = $classMetadata->getIdentifierValues($proxy);
$original = $entityPersister->loadById($identifier);
return static function (InternalProxy $proxy, array $identifier) use ($entityPersister, $classMetadata, $identifierFlattener): void {
$original = $entityPersister->loadById($identifier);
if ($original === null) {
throw EntityNotFoundException::fromClassNameAndIdentifier(
@@ -234,7 +233,7 @@ EOPHP;
$class = $entityPersister->getClassMetadata();
foreach ($class->getReflectionProperties() as $property) {
if (! $property || ! $class->hasField($property->getName()) && ! $class->hasAssociation($property->getName())) {
if (! $property || isset($identifier[$property->getName()]) || ! $class->hasField($property->getName()) && ! $class->hasAssociation($property->getName())) {
continue;
}
@@ -283,7 +282,9 @@ EOPHP;
$identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers);
$proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy {
$proxy = self::createLazyGhost($initializer, $skippedProperties);
$proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void {
$initializer($object, $identifier);
}, $skippedProperties);
foreach ($identifierFields as $idField => $reflector) {
if (! isset($identifier[$idField])) {
@@ -386,12 +387,18 @@ EOPHP;
$code = substr($code, 7 + (int) strpos($code, "\n{"));
$code = substr($code, 0, (int) strpos($code, "\n}"));
$code = str_replace('LazyGhostTrait;', str_replace("\n ", "\n", 'LazyGhostTrait {
initializeLazyObject as __load;
initializeLazyObject as private;
setLazyObjectAsInitialized as public __setInitialized;
isLazyObjectInitialized as private;
createLazyGhost as private;
resetLazyObject as private;
}'), $code);
}
public function __load(): void
{
$this->initializeLazyObject();
}
'), $code);
return $code;
}

View File

@@ -7,11 +7,14 @@ namespace Doctrine\ORM;
use Doctrine\DBAL\LockMode;
use Doctrine\DBAL\Result;
use Doctrine\DBAL\Types\Type;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\AST\DeleteStatement;
use Doctrine\ORM\Query\AST\SelectStatement;
use Doctrine\ORM\Query\AST\UpdateStatement;
use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
use Doctrine\ORM\Query\Exec\SqlFinalizer;
use Doctrine\ORM\Query\OutputWalker;
use Doctrine\ORM\Query\Parameter;
use Doctrine\ORM\Query\ParameterTypeInferer;
use Doctrine\ORM\Query\Parser;
@@ -27,6 +30,7 @@ use function assert;
use function count;
use function get_debug_type;
use function in_array;
use function is_a;
use function ksort;
use function md5;
use function reset;
@@ -70,6 +74,14 @@ class Query extends AbstractQuery
*/
public const HINT_REFRESH_ENTITY = 'doctrine.refresh.entity';
/**
* The forcePartialLoad query hint forces a particular query to return
* partial objects.
*
* @todo Rename: HINT_OPTIMIZE
*/
public const HINT_FORCE_PARTIAL_LOAD = 'doctrine.forcePartialLoad';
/**
* The includeMetaColumns query hint causes meta columns like foreign keys and
* discriminator columns to be selected and returned as part of the query result.
@@ -163,7 +175,7 @@ class Query extends AbstractQuery
*/
public function getSQL(): string|array
{
return $this->parse()->getSqlExecutor()->getSqlStatements();
return $this->getSqlExecutor()->getSqlStatements();
}
/**
@@ -242,7 +254,7 @@ class Query extends AbstractQuery
protected function _doExecute(): Result|int
{
$executor = $this->parse()->getSqlExecutor();
$executor = $this->getSqlExecutor();
if ($this->queryCacheProfile) {
$executor->setQueryCacheProfile($this->queryCacheProfile);
@@ -656,11 +668,31 @@ class Query extends AbstractQuery
{
ksort($this->hints);
if (! $this->hasHint(self::HINT_CUSTOM_OUTPUT_WALKER)) {
// Assume Parser will create the SqlOutputWalker; save is_a call, which might trigger a class load
$firstAndMaxResult = '';
} else {
$outputWalkerClass = $this->getHint(self::HINT_CUSTOM_OUTPUT_WALKER);
if (is_a($outputWalkerClass, OutputWalker::class, true)) {
$firstAndMaxResult = '';
} else {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11188/',
'Your output walker class %s should implement %s in order to provide a %s. This also means the output walker should not use the query firstResult/maxResult values, which should be read from the query by the SqlFinalizer only.',
$outputWalkerClass,
OutputWalker::class,
SqlFinalizer::class,
);
$firstAndMaxResult = '&firstResult=' . $this->firstResult . '&maxResult=' . $this->maxResults;
}
}
return md5(
$this->getDQL() . serialize($this->hints) .
'&platform=' . get_debug_type($this->getEntityManager()->getConnection()->getDatabasePlatform()) .
($this->em->hasFilters() ? $this->em->getFilters()->getHash() : '') .
'&firstResult=' . $this->firstResult . '&maxResult=' . $this->maxResults .
$firstAndMaxResult .
'&hydrationMode=' . $this->hydrationMode . '&types=' . serialize($this->parsedTypes) . 'DOCTRINE_QUERY_CACHE_SALT',
);
}
@@ -679,4 +711,9 @@ class Query extends AbstractQuery
$this->state = self::STATE_DIRTY;
}
private function getSqlExecutor(): AbstractSqlExecutor
{
return $this->parse()->prepareSqlExecutor($this);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Query\AST;
class PartialObjectExpression extends Node
{
/** @param mixed[] $partialFieldSet */
public function __construct(
public string $identificationVariable,
public array $partialFieldSet,
) {
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Query\Exec;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Result;
/**
* SQL executor for a given, final, single SELECT SQL query
*
* @method string getSqlStatements()
*/
class FinalizedSelectExecutor extends AbstractSqlExecutor
{
public function __construct(string $sql)
{
$this->sqlStatements = $sql;
}
/**
* {@inheritDoc}
*/
public function execute(Connection $conn, array $params, array $types): Result
{
return $conn->executeQuery($this->getSqlStatements(), $params, $types, $this->queryCacheProfile);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Query\Exec;
use Doctrine\ORM\Query;
/**
* PreparedExecutorFinalizer is a wrapper for the SQL finalization
* phase that does nothing - it is constructed with the sql executor
* already.
*/
final class PreparedExecutorFinalizer implements SqlFinalizer
{
private AbstractSqlExecutor $executor;
public function __construct(AbstractSqlExecutor $exeutor)
{
$this->executor = $exeutor;
}
public function createExecutor(Query $query): AbstractSqlExecutor
{
return $this->executor;
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Query\Exec;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Utility\LockSqlHelper;
/**
* SingleSelectSqlFinalizer finalizes a given SQL query by applying
* the query's firstResult/maxResult values as well as extra read lock/write lock
* statements, both through the platform-specific methods.
*
* The resulting, "finalized" SQL is passed to a FinalizedSelectExecutor.
*/
class SingleSelectSqlFinalizer implements SqlFinalizer
{
use LockSqlHelper;
public function __construct(private string $sql)
{
}
/**
* This method exists temporarily to support old SqlWalker interfaces.
*
* @internal
*
* @psalm-internal Doctrine\ORM
*/
public function finalizeSql(Query $query): string
{
$platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();
$sql = $platform->modifyLimitQuery($this->sql, $query->getMaxResults(), $query->getFirstResult());
$lockMode = $query->getHint(Query::HINT_LOCK_MODE) ?: LockMode::NONE;
if ($lockMode !== LockMode::NONE && $lockMode !== LockMode::OPTIMISTIC && $lockMode !== LockMode::PESSIMISTIC_READ && $lockMode !== LockMode::PESSIMISTIC_WRITE) {
throw QueryException::invalidLockMode();
}
if ($lockMode === LockMode::PESSIMISTIC_READ) {
$sql .= ' ' . $this->getReadLockSQL($platform);
} elseif ($lockMode === LockMode::PESSIMISTIC_WRITE) {
$sql .= ' ' . $this->getWriteLockSQL($platform);
}
return $sql;
}
/** @return FinalizedSelectExecutor */
public function createExecutor(Query $query): AbstractSqlExecutor
{
return new FinalizedSelectExecutor($this->finalizeSql($query));
}
}

View File

@@ -14,8 +14,6 @@ use Doctrine\ORM\Query\SqlWalker;
* that are mapped to a single table.
*
* @link www.doctrine-project.org
*
* @todo This is exactly the same as SingleSelectExecutor. Unify in SingleStatementExecutor.
*/
class SingleTableDeleteUpdateExecutor extends AbstractSqlExecutor
{

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Query\Exec;
use Doctrine\ORM\Query;
/**
* SqlFinalizers are created by OutputWalkers that traversed the DQL AST.
* The SqlFinalizer instance can be kept in the query cache and re-used
* at a later time.
*
* Once the SqlFinalizer has been created or retrieved from the query cache,
* it receives the Query object again in order to yield the AbstractSqlExecutor
* that will then be used to execute the query.
*
* The SqlFinalizer may assume that the DQL that was used to build the AST
* and run the OutputWalker (which created the SqlFinalizer) is equivalent to
* the query that will be passed to the createExecutor() method. Potential differences
* are the parameter values or firstResult/maxResult settings.
*/
interface SqlFinalizer
{
public function createExecutor(Query $query): AbstractSqlExecutor;
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Query;
use Doctrine\ORM\Query\Exec\SqlFinalizer;
/**
* Interface for output walkers
*
* Output walkers, like tree walkers, can traverse the DQL AST to perform
* their purpose.
*
* The goal of an OutputWalker is to ultimately provide the SqlFinalizer
* which produces the final, executable SQL statement in a "finalization" phase.
*
* It must be possible to use the same SqlFinalizer for Queries with different
* firstResult/maxResult values. In other words, SQL produced by the
* output walker should not depend on those values, and any SQL generation/modification
* specific to them should happen in the finalizer's `\Doctrine\ORM\Query\Exec\SqlFinalizer::createExecutor()`
* method instead.
*/
interface OutputWalker
{
public function getFinalizer(AST\DeleteStatement|AST\UpdateStatement|AST\SelectStatement $AST): SqlFinalizer;
}

View File

@@ -5,14 +5,20 @@ declare(strict_types=1);
namespace Doctrine\ORM\Query;
use Doctrine\Common\Lexer\Token;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Exception\DuplicateFieldException;
use Doctrine\ORM\Exception\NoMatchingPropertyException;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\AST\Functions;
use Doctrine\ORM\Query\Exec\SqlFinalizer;
use LogicException;
use ReflectionClass;
use function array_intersect;
use function array_key_exists;
use function array_search;
use function assert;
use function class_exists;
@@ -28,6 +34,7 @@ use function strpos;
use function strrpos;
use function strtolower;
use function substr;
use function trim;
/**
* An LL(*) recursive-descent parser for the context-free grammar of the Doctrine Query Language.
@@ -48,7 +55,7 @@ final class Parser
{
/**
* @readonly Maps BUILT-IN string function names to AST class names.
* @psalm-var array<string, class-string<Functions\FunctionNode>>
* @var array<string, class-string<Functions\FunctionNode>>
*/
private static array $stringFunctions = [
'concat' => Functions\ConcatFunction::class,
@@ -61,7 +68,7 @@ final class Parser
/**
* @readonly Maps BUILT-IN numeric function names to AST class names.
* @psalm-var array<string, class-string<Functions\FunctionNode>>
* @var array<string, class-string<Functions\FunctionNode>>
*/
private static array $numericFunctions = [
'length' => Functions\LengthFunction::class,
@@ -84,7 +91,7 @@ final class Parser
/**
* @readonly Maps BUILT-IN datetime function names to AST class names.
* @psalm-var array<string, class-string<Functions\FunctionNode>>
* @var array<string, class-string<Functions\FunctionNode>>
*/
private static array $datetimeFunctions = [
'current_date' => Functions\CurrentDateFunction::class,
@@ -102,6 +109,9 @@ final class Parser
/** @psalm-var list<array{token: DqlToken|null, expression: mixed, nestingLevel: int}> */
private array $deferredIdentificationVariables = [];
/** @psalm-var list<array{token: DqlToken|null, expression: AST\PartialObjectExpression, nestingLevel: int}> */
private array $deferredPartialObjectExpressions = [];
/** @psalm-var list<array{token: DqlToken|null, expression: AST\PathExpression, nestingLevel: int}> */
private array $deferredPathExpressions = [];
@@ -141,7 +151,7 @@ final class Parser
/**
* Any additional custom tree walkers that modify the AST.
*
* @psalm-var list<class-string<TreeWalker>>
* @var list<class-string<TreeWalker>>
*/
private array $customTreeWalkers = [];
@@ -171,17 +181,24 @@ final class Parser
* Sets a custom tree walker that produces output.
* This tree walker will be run last over the AST, after any other walkers.
*
* @psalm-param class-string<SqlWalker> $className
* @param class-string<SqlWalker> $className
*/
public function setCustomOutputTreeWalker(string $className): void
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11641',
'%s is deprecated, set the output walker class with the \Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER query hint instead',
__METHOD__,
);
$this->customOutputWalker = $className;
}
/**
* Adds a custom tree walker for modifying the AST.
*
* @psalm-param class-string<TreeWalker> $className
* @param class-string<TreeWalker> $className
*/
public function addCustomTreeWalker(string $className): void
{
@@ -224,6 +241,10 @@ final class Parser
// This also allows post-processing of the AST for modification purposes.
$this->processDeferredIdentificationVariables();
if ($this->deferredPartialObjectExpressions) {
$this->processDeferredPartialObjectExpressions();
}
if ($this->deferredPathExpressions) {
$this->processDeferredPathExpressions();
}
@@ -335,11 +356,26 @@ final class Parser
$this->queryComponents = $treeWalkerChain->getQueryComponents();
}
$outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class;
$outputWalkerClass = $this->customOutputWalker ?: SqlOutputWalker::class;
$outputWalker = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);
// Assign an SQL executor to the parser result
$this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST));
if ($outputWalker instanceof OutputWalker) {
$finalizer = $outputWalker->getFinalizer($AST);
$this->parserResult->setSqlFinalizer($finalizer);
} else {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11188/',
'Your output walker class %s should implement %s in order to provide a %s. This also means the output walker should not use the query firstResult/maxResult values, which should be read from the query by the SqlFinalizer only.',
$outputWalkerClass,
OutputWalker::class,
SqlFinalizer::class,
);
// @phpstan-ignore method.deprecated
$executor = $outputWalker->getExecutor($AST);
// @phpstan-ignore method.deprecated
$this->parserResult->setSqlExecutor($executor);
}
return $this->parserResult;
}
@@ -599,6 +635,44 @@ final class Parser
}
}
/**
* Validates that the given <tt>PartialObjectExpression</tt> is semantically correct.
* It must exist in query components list.
*/
private function processDeferredPartialObjectExpressions(): void
{
foreach ($this->deferredPartialObjectExpressions as $deferredItem) {
$expr = $deferredItem['expression'];
$class = $this->getMetadataForDqlAlias($expr->identificationVariable);
foreach ($expr->partialFieldSet as $field) {
if (isset($class->fieldMappings[$field])) {
continue;
}
if (
isset($class->associationMappings[$field]) &&
$class->associationMappings[$field]->isToOneOwningSide()
) {
continue;
}
$this->semanticalError(sprintf(
"There is no mapped field named '%s' on class %s.",
$field,
$class->name,
), $deferredItem['token']);
}
if (array_intersect($class->identifier, $expr->partialFieldSet) !== $class->identifier) {
$this->semanticalError(
'The partial field selection of class ' . $class->name . ' must contain the identifier.',
$deferredItem['token'],
);
}
}
}
/**
* Validates that the given <tt>ResultVariable</tt> is semantically correct.
* It must exist in query components list.
@@ -1621,25 +1695,88 @@ final class Parser
return new AST\JoinAssociationDeclaration($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy);
}
/**
* PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
* PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
*/
public function PartialObjectExpression(): AST\PartialObjectExpression
{
$this->match(TokenType::T_PARTIAL);
$partialFieldSet = [];
$identificationVariable = $this->IdentificationVariable();
$this->match(TokenType::T_DOT);
$this->match(TokenType::T_OPEN_CURLY_BRACE);
$this->match(TokenType::T_IDENTIFIER);
assert($this->lexer->token !== null);
$field = $this->lexer->token->value;
// First field in partial expression might be embeddable property
while ($this->lexer->isNextToken(TokenType::T_DOT)) {
$this->match(TokenType::T_DOT);
$this->match(TokenType::T_IDENTIFIER);
$field .= '.' . $this->lexer->token->value;
}
$partialFieldSet[] = $field;
while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
$this->match(TokenType::T_COMMA);
$this->match(TokenType::T_IDENTIFIER);
$field = $this->lexer->token->value;
while ($this->lexer->isNextToken(TokenType::T_DOT)) {
$this->match(TokenType::T_DOT);
$this->match(TokenType::T_IDENTIFIER);
$field .= '.' . $this->lexer->token->value;
}
$partialFieldSet[] = $field;
}
$this->match(TokenType::T_CLOSE_CURLY_BRACE);
$partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet);
// Defer PartialObjectExpression validation
$this->deferredPartialObjectExpressions[] = [
'expression' => $partialObjectExpression,
'nestingLevel' => $this->nestingLevel,
'token' => $this->lexer->token,
];
return $partialObjectExpression;
}
/**
* NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
*/
public function NewObjectExpression(): AST\NewObjectExpression
{
$args = [];
$useNamedArguments = false;
$args = [];
$argFieldAlias = [];
$this->match(TokenType::T_NEW);
if ($this->lexer->isNextToken(TokenType::T_NAMED)) {
$this->match(TokenType::T_NAMED);
$useNamedArguments = true;
}
$className = $this->AbstractSchemaName(); // note that this is not yet validated
$token = $this->lexer->token;
$this->match(TokenType::T_OPEN_PARENTHESIS);
$args[] = $this->NewObjectArg();
$this->addArgument($args, $useNamedArguments);
while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
$this->match(TokenType::T_COMMA);
$args[] = $this->NewObjectArg();
$this->addArgument($args, $useNamedArguments);
}
$this->match(TokenType::T_CLOSE_PARENTHESIS);
@@ -1656,25 +1793,71 @@ final class Parser
return $expression;
}
/**
* NewObjectArg ::= ScalarExpression | "(" Subselect ")"
*/
public function NewObjectArg(): mixed
/** @param array<mixed> $args */
public function addArgument(array &$args, bool $useNamedArguments): void
{
$fieldAlias = null;
if ($useNamedArguments) {
$startToken = $this->lexer->lookahead?->position ?? 0;
$newArg = $this->NewObjectArg($fieldAlias);
$key = $fieldAlias ?? $newArg->field ?? null;
if ($key === null) {
throw NoMatchingPropertyException::create(trim(substr(
($this->query->getDQL() ?? ''),
$startToken,
($this->lexer->lookahead->position ?? 0) - $startToken,
)));
}
if (array_key_exists($key, $args)) {
throw DuplicateFieldException::create($key, trim(substr(
($this->query->getDQL() ?? ''),
$startToken,
($this->lexer->lookahead->position ?? 0) - $startToken,
)));
}
$args[$key] = $newArg;
} else {
$args[] = $this->NewObjectArg($fieldAlias);
}
}
/**
* NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]
*/
public function NewObjectArg(string|null &$fieldAlias = null): mixed
{
$fieldAlias = null;
assert($this->lexer->lookahead !== null);
$token = $this->lexer->lookahead;
$peek = $this->lexer->glimpse();
assert($peek !== null);
$expression = null;
if ($token->type === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT) {
$this->match(TokenType::T_OPEN_PARENTHESIS);
$expression = $this->Subselect();
$this->match(TokenType::T_CLOSE_PARENTHESIS);
return $expression;
} elseif ($token->type === TokenType::T_NEW) {
$expression = $this->NewObjectExpression();
} else {
$expression = $this->ScalarExpression();
}
return $this->ScalarExpression();
if ($this->lexer->isNextToken(TokenType::T_AS)) {
$this->match(TokenType::T_AS);
$fieldAlias = $this->AliasIdentificationVariable();
}
return $expression;
}
/**
@@ -1920,7 +2103,7 @@ final class Parser
/**
* SelectExpression ::= (
* IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration |
* "(" Subselect ")" | CaseExpression | NewObjectExpression
* PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression
* ) [["AS"] ["HIDDEN"] AliasResultVariable]
*/
public function SelectExpression(): AST\SelectExpression
@@ -1961,6 +2144,12 @@ final class Parser
break;
// PartialObjectExpression (PARTIAL u.{id, name})
case $lookaheadType === TokenType::T_PARTIAL:
$expression = $this->PartialObjectExpression();
$identVariable = $expression->identificationVariable;
break;
// Subselect
case $lookaheadType === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT:
$this->match(TokenType::T_OPEN_PARENTHESIS);
@@ -1986,7 +2175,7 @@ final class Parser
default:
$this->syntaxError(
'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | "(" Subselect ")" | CaseExpression',
'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression',
$this->lexer->lookahead,
);
}
@@ -2563,7 +2752,10 @@ final class Parser
return new AST\ParenthesisExpression($expr);
}
assert($this->lexer->lookahead !== null);
if ($this->lexer->lookahead === null) {
$this->syntaxError('ArithmeticPrimary');
}
switch ($this->lexer->lookahead->type) {
case TokenType::T_COALESCE:
case TokenType::T_NULLIF:

View File

@@ -4,7 +4,9 @@ declare(strict_types=1);
namespace Doctrine\ORM\Query;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
use Doctrine\ORM\Query\Exec\SqlFinalizer;
use LogicException;
use function sprintf;
@@ -22,6 +24,11 @@ class ParserResult
*/
private AbstractSqlExecutor|null $sqlExecutor = null;
/**
* The SQL executor used for executing the SQL.
*/
private SqlFinalizer|null $sqlFinalizer = null;
/**
* The ResultSetMapping that describes how to map the SQL result set.
*/
@@ -63,6 +70,8 @@ class ParserResult
/**
* Sets the SQL executor that should be used for this ParserResult.
*
* @deprecated
*/
public function setSqlExecutor(AbstractSqlExecutor $executor): void
{
@@ -71,6 +80,8 @@ class ParserResult
/**
* Gets the SQL executor used by this ParserResult.
*
* @deprecated
*/
public function getSqlExecutor(): AbstractSqlExecutor
{
@@ -84,6 +95,24 @@ class ParserResult
return $this->sqlExecutor;
}
public function setSqlFinalizer(SqlFinalizer $finalizer): void
{
$this->sqlFinalizer = $finalizer;
}
public function prepareSqlExecutor(Query $query): AbstractSqlExecutor
{
if ($this->sqlFinalizer !== null) {
return $this->sqlFinalizer->createExecutor($query);
}
if ($this->sqlExecutor !== null) {
return $this->sqlExecutor;
}
throw new LogicException('This ParserResult lacks both the SqlFinalizer as well as the (legacy) SqlExecutor');
}
/**
* Adds a DQL to SQL parameter mapping. One DQL parameter name/position can map to
* several SQL parameter positions.

View File

@@ -88,6 +88,15 @@ class QueryException extends Exception implements ORMException
);
}
public static function partialObjectsAreDangerous(): self
{
return new self(
'Loading partial objects is dangerous. Fetch full objects or consider ' .
'using a different fetch mode. If you really want partial objects, ' .
'set the doctrine.forcePartialLoad query hint to TRUE.',
);
}
/**
* @param string[] $assoc
* @psalm-param array<string, string> $assoc

View File

@@ -38,7 +38,7 @@ class ResultSetMapping
* Maps alias names to class names.
*
* @ignore
* @psalm-var array<string, class-string>
* @var array<string, class-string>
*/
public array $aliasMap = [];
@@ -134,7 +134,7 @@ class ResultSetMapping
* Map from column names to class names that declare the field the column is mapped to.
*
* @ignore
* @psalm-var array<string, class-string>
* @var array<string, class-string>
*/
public array $declaringClasses = [];
@@ -152,6 +152,13 @@ class ResultSetMapping
*/
public array $newObjectMappings = [];
/**
* Maps last argument for new objects in order to initiate object construction
*
* @psalm-var array<int|string, array{ownerIndex: string|int, argIndex: int|string}>
*/
public array $nestedNewObjectArguments = [];
/**
* Maps metadata parameter names to the metadata attribute.
*
@@ -169,12 +176,11 @@ class ResultSetMapping
/**
* Adds an entity result to this ResultSetMapping.
*
* @param string $class The class name of the entity.
* @param string $alias The alias for the class. The alias must be unique among all entity
* results or joined entity results within this ResultSetMapping.
* @param string|null $resultAlias The result alias with which the entity result should be
* placed in the result structure.
* @psalm-param class-string $class
* @param class-string $class The class name of the entity.
* @param string $alias The alias for the class. The alias must be unique among all entity
* results or joined entity results within this ResultSetMapping.
* @param string|null $resultAlias The result alias with which the entity result should be
* placed in the result structure.
*
* @return $this
*
@@ -302,15 +308,14 @@ class ResultSetMapping
/**
* Adds a field to the result that belongs to an entity or joined entity.
*
* @param string $alias The alias of the root entity or joined entity to which the field belongs.
* @param string $columnName The name of the column in the SQL result set.
* @param string $fieldName The name of the field on the declaring class.
* @param string|null $declaringClass The name of the class that declares/owns the specified field.
* When $alias refers to a superclass in a mapped hierarchy but
* the field $fieldName is defined on a subclass, specify that here.
* If not specified, the field is assumed to belong to the class
* designated by $alias.
* @psalm-param class-string|null $declaringClass
* @param string $alias The alias of the root entity or joined entity to which the field belongs.
* @param string $columnName The name of the column in the SQL result set.
* @param string $fieldName The name of the field on the declaring class.
* @param class-string|null $declaringClass The name of the class that declares/owns the specified field.
* When $alias refers to a superclass in a mapped hierarchy but
* the field $fieldName is defined on a subclass, specify that here.
* If not specified, the field is assumed to belong to the class
* designated by $alias.
*
* @return $this
*
@@ -335,12 +340,11 @@ class ResultSetMapping
/**
* Adds a joined entity result.
*
* @param string $class The class name of the joined entity.
* @param string $alias The unique alias to use for the joined entity.
* @param string $parentAlias The alias of the entity result that is the parent of this joined result.
* @param string $relation The association field that connects the parent entity result
* with the joined entity result.
* @psalm-param class-string $class
* @param class-string $class The class name of the joined entity.
* @param string $alias The unique alias to use for the joined entity.
* @param string $parentAlias The alias of the entity result that is the parent of this joined result.
* @param string $relation The association field that connects the parent entity result
* with the joined entity result.
*
* @return $this
*
@@ -484,7 +488,7 @@ class ResultSetMapping
return $this->fieldMappings[$columnName];
}
/** @psalm-return array<string, class-string> */
/** @return array<string, class-string> */
public function getAliasMap(): array
{
return $this->aliasMap;

View File

@@ -56,11 +56,9 @@ class ResultSetMappingBuilder extends ResultSetMapping implements Stringable
/**
* Adds a root entity and all of its fields to the result set.
*
* @param string $class The class name of the root entity.
* @param string $alias The unique alias to use for the root entity.
* @param string[] $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName).
* @psalm-param class-string $class
* @psalm-param array<string, string> $renamedColumns
* @param class-string $class The class name of the root entity.
* @param string $alias The unique alias to use for the root entity.
* @param array<string, string> $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName).
* @psalm-param self::COLUMN_RENAMING_*|null $renameMode
*/
public function addRootEntityFromClassMetadata(
@@ -79,14 +77,12 @@ class ResultSetMappingBuilder extends ResultSetMapping implements Stringable
/**
* Adds a joined entity and all of its fields to the result set.
*
* @param string $class The class name of the joined entity.
* @param string $alias The unique alias to use for the joined entity.
* @param string $parentAlias The alias of the entity result that is the parent of this joined result.
* @param string $relation The association field that connects the parent entity result
* with the joined entity result.
* @param string[] $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName).
* @psalm-param class-string $class
* @psalm-param array<string, string> $renamedColumns
* @param class-string $class The class name of the joined entity.
* @param string $alias The unique alias to use for the joined entity.
* @param string $parentAlias The alias of the entity result that is the parent of this joined result.
* @param string $relation The association field that connects the parent entity result
* with the joined entity result.
* @param array<string, string> $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName).
* @psalm-param self::COLUMN_RENAMING_*|null $renameMode
*/
public function addJoinedEntityFromClassMetadata(
@@ -197,12 +193,11 @@ class ResultSetMappingBuilder extends ResultSetMapping implements Stringable
*
* This depends on the renaming mode selected by the user.
*
* @psalm-param class-string $className
* @param class-string $className
* @psalm-param self::COLUMN_RENAMING_* $mode
* @psalm-param array<string, string> $customRenameColumns
*
* @return string[]
* @psalm-return array<array-key, string>
*/
private function getColumnAliasMap(
string $className,

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Query;
use Doctrine\ORM\Query\Exec\PreparedExecutorFinalizer;
use Doctrine\ORM\Query\Exec\SingleSelectSqlFinalizer;
use Doctrine\ORM\Query\Exec\SqlFinalizer;
use LogicException;
class SqlOutputWalker extends SqlWalker implements OutputWalker
{
public function getFinalizer(AST\DeleteStatement|AST\UpdateStatement|AST\SelectStatement $AST): SqlFinalizer
{
switch (true) {
case $AST instanceof AST\SelectStatement:
return new SingleSelectSqlFinalizer($this->createSqlForFinalizer($AST));
case $AST instanceof AST\UpdateStatement:
return new PreparedExecutorFinalizer($this->createUpdateStatementExecutor($AST));
case $AST instanceof AST\DeleteStatement:
return new PreparedExecutorFinalizer($this->createDeleteStatementExecutor($AST));
}
throw new LogicException('Unexpected AST node type');
}
}

View File

@@ -15,7 +15,6 @@ use Doctrine\ORM\Mapping\QuoteStrategy;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\Query;
use Doctrine\ORM\Utility\HierarchyDiscriminatorResolver;
use Doctrine\ORM\Utility\LockSqlHelper;
use Doctrine\ORM\Utility\PersisterHelper;
use InvalidArgumentException;
use LogicException;
@@ -25,9 +24,12 @@ use function array_filter;
use function array_keys;
use function array_map;
use function array_merge;
use function array_pop;
use function assert;
use function count;
use function end;
use function implode;
use function in_array;
use function is_array;
use function is_float;
use function is_int;
@@ -48,10 +50,13 @@ use function trim;
*/
class SqlWalker
{
use LockSqlHelper;
public const HINT_DISTINCT = 'doctrine.distinct';
/**
* Used to mark a query as containing a PARTIAL expression, which needs to be known by SLC.
*/
public const HINT_PARTIAL = 'doctrine.partial';
private readonly ResultSetMapping $rsm;
/**
@@ -79,6 +84,13 @@ class SqlWalker
*/
private int $newObjectCounter = 0;
/**
* Contains nesting levels of new objects arguments
*
* @psalm-var array<int, array{0: string|int, 1: int}>
*/
private array $newObjectStack = [];
private readonly EntityManagerInterface $em;
private readonly Connection $conn;
@@ -220,23 +232,40 @@ class SqlWalker
/**
* Gets an executor that can be used to execute the result of this walker.
*
* @deprecated Output walkers should no longer create the executor directly, but instead provide
* a SqlFinalizer by implementing the `OutputWalker` interface. Thus, this method is
* no longer needed and will be removed in 4.0.
*/
public function getExecutor(AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement $statement): Exec\AbstractSqlExecutor
{
return match (true) {
$statement instanceof AST\SelectStatement
=> new Exec\SingleSelectExecutor($statement, $this),
$statement instanceof AST\UpdateStatement
=> $this->em->getClassMetadata($statement->updateClause->abstractSchemaName)->isInheritanceTypeJoined()
? new Exec\MultiTableUpdateExecutor($statement, $this)
: new Exec\SingleTableDeleteUpdateExecutor($statement, $this),
$statement instanceof AST\DeleteStatement
=> $this->em->getClassMetadata($statement->deleteClause->abstractSchemaName)->isInheritanceTypeJoined()
? new Exec\MultiTableDeleteExecutor($statement, $this)
: new Exec\SingleTableDeleteUpdateExecutor($statement, $this),
$statement instanceof AST\UpdateStatement => $this->createUpdateStatementExecutor($statement),
$statement instanceof AST\DeleteStatement => $this->createDeleteStatementExecutor($statement),
default => new Exec\SingleSelectExecutor($statement, $this),
};
}
/** @psalm-internal Doctrine\ORM */
protected function createUpdateStatementExecutor(AST\UpdateStatement $AST): Exec\AbstractSqlExecutor
{
$primaryClass = $this->em->getClassMetadata($AST->updateClause->abstractSchemaName);
return $primaryClass->isInheritanceTypeJoined()
? new Exec\MultiTableUpdateExecutor($AST, $this)
: new Exec\SingleTableDeleteUpdateExecutor($AST, $this);
}
/** @psalm-internal Doctrine\ORM */
protected function createDeleteStatementExecutor(AST\DeleteStatement $AST): Exec\AbstractSqlExecutor
{
$primaryClass = $this->em->getClassMetadata($AST->deleteClause->abstractSchemaName);
return $primaryClass->isInheritanceTypeJoined()
? new Exec\MultiTableDeleteExecutor($AST, $this)
: new Exec\SingleTableDeleteUpdateExecutor($AST, $this);
}
/**
* Generates a unique, short SQL table alias.
*/
@@ -306,6 +335,11 @@ class SqlWalker
$sql .= implode(' AND ', array_filter($sqlParts));
}
// Ignore subclassing inclusion if partial objects is disallowed
if ($this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) {
return $sql;
}
// LEFT JOIN child class tables
foreach ($class->subClasses as $subClassName) {
$subClass = $this->em->getClassMetadata($subClassName);
@@ -464,10 +498,15 @@ class SqlWalker
*/
public function walkSelectStatement(AST\SelectStatement $selectStatement): string
{
$limit = $this->query->getMaxResults();
$offset = $this->query->getFirstResult();
$lockMode = $this->query->getHint(Query::HINT_LOCK_MODE) ?: LockMode::NONE;
$sql = $this->walkSelectClause($selectStatement->selectClause)
$sql = $this->createSqlForFinalizer($selectStatement);
$finalizer = new Exec\SingleSelectSqlFinalizer($sql);
return $finalizer->finalizeSql($this->query);
}
protected function createSqlForFinalizer(AST\SelectStatement $selectStatement): string
{
$sql = $this->walkSelectClause($selectStatement->selectClause)
. $this->walkFromClause($selectStatement->fromClause)
. $this->walkWhereClause($selectStatement->whereClause);
@@ -488,33 +527,24 @@ class SqlWalker
$sql .= ' ORDER BY ' . $orderBySql;
}
$sql = $this->platform->modifyLimitQuery($sql, $limit, $offset);
if ($lockMode === LockMode::NONE) {
return $sql;
}
if ($lockMode === LockMode::PESSIMISTIC_READ) {
return $sql . ' ' . $this->getReadLockSQL($this->platform);
}
if ($lockMode === LockMode::PESSIMISTIC_WRITE) {
return $sql . ' ' . $this->getWriteLockSQL($this->platform);
}
if ($lockMode !== LockMode::OPTIMISTIC) {
throw QueryException::invalidLockMode();
}
foreach ($this->selectedClasses as $selectedClass) {
if (! $selectedClass['class']->isVersioned) {
throw OptimisticLockException::lockFailed($selectedClass['class']->name);
}
}
$this->assertOptimisticLockingHasAllClassesVersioned();
return $sql;
}
private function assertOptimisticLockingHasAllClassesVersioned(): void
{
$lockMode = $this->query->getHint(Query::HINT_LOCK_MODE) ?: LockMode::NONE;
if ($lockMode === LockMode::OPTIMISTIC) {
foreach ($this->selectedClasses as $selectedClass) {
if (! $selectedClass['class']->isVersioned) {
throw OptimisticLockException::lockFailed($selectedClass['class']->name);
}
}
}
}
/**
* Walks down a UpdateStatement AST node, thereby generating the appropriate SQL.
*/
@@ -644,7 +674,8 @@ class SqlWalker
$this->query->setHint(self::HINT_DISTINCT, true);
}
$addMetaColumns = $this->query->getHydrationMode() === Query::HYDRATE_OBJECT
$addMetaColumns = ! $this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD) &&
$this->query->getHydrationMode() === Query::HYDRATE_OBJECT
|| $this->query->getHint(Query::HINT_INCLUDE_META_COLUMNS);
foreach ($this->selectedClasses as $selectedClass) {
@@ -911,7 +942,9 @@ class SqlWalker
}
}
if ($relation->fetch === ClassMetadata::FETCH_EAGER && $condExpr !== null) {
$fetchMode = $this->query->getHint('fetchMode')[$assoc->sourceEntity][$assoc->fieldName] ?? $relation->fetch;
if ($fetchMode === ClassMetadata::FETCH_EAGER && $condExpr !== null) {
throw QueryException::eagerFetchJoinWithNotAllowed($assoc->sourceEntity, $assoc->fieldName);
}
@@ -1323,7 +1356,17 @@ class SqlWalker
break;
default:
$dqlAlias = $expr;
// IdentificationVariable or PartialObjectExpression
if ($expr instanceof AST\PartialObjectExpression) {
$this->query->setHint(self::HINT_PARTIAL, true);
$dqlAlias = $expr->identificationVariable;
$partialFieldSet = $expr->partialFieldSet;
} else {
$dqlAlias = $expr;
$partialFieldSet = [];
}
$class = $this->getMetadataForDqlAlias($dqlAlias);
$resultAlias = $selectExpression->fieldIdentificationVariable ?: null;
@@ -1339,6 +1382,10 @@ class SqlWalker
// Select all fields from the queried class
foreach ($class->fieldMappings as $fieldName => $mapping) {
if ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true)) {
continue;
}
$tableName = isset($mapping->inherited)
? $this->em->getClassMetadata($mapping->inherited)->getTableName()
: $class->getTableName();
@@ -1365,29 +1412,32 @@ class SqlWalker
// Add any additional fields of subclasses (excluding inherited fields)
// 1) on Single Table Inheritance: always, since its marginal overhead
// 2) on Class Table Inheritance
foreach ($class->subClasses as $subClassName) {
$subClass = $this->em->getClassMetadata($subClassName);
$sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias);
// 2) on Class Table Inheritance only if partial objects are disallowed,
// since it requires outer joining subtables.
if ($class->isInheritanceTypeSingleTable() || ! $this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) {
foreach ($class->subClasses as $subClassName) {
$subClass = $this->em->getClassMetadata($subClassName);
$sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias);
foreach ($subClass->fieldMappings as $fieldName => $mapping) {
if (isset($mapping->inherited)) {
continue;
foreach ($subClass->fieldMappings as $fieldName => $mapping) {
if (isset($mapping->inherited) || ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true))) {
continue;
}
$columnAlias = $this->getSQLColumnAlias($mapping->columnName);
$quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform);
$col = $sqlTableAlias . '.' . $quotedColumnName;
$type = Type::getType($mapping->type);
$col = $type->convertToPHPValueSQL($col, $this->platform);
$sqlParts[] = $col . ' AS ' . $columnAlias;
$this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
$this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName);
}
$columnAlias = $this->getSQLColumnAlias($mapping->columnName);
$quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform);
$col = $sqlTableAlias . '.' . $quotedColumnName;
$type = Type::getType($mapping->type);
$col = $type->convertToPHPValueSQL($col, $this->platform);
$sqlParts[] = $col . ' AS ' . $columnAlias;
$this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
$this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName);
}
}
@@ -1459,7 +1509,14 @@ class SqlWalker
public function walkNewObject(AST\NewObjectExpression $newObjectExpression, string|null $newObjectResultAlias = null): string
{
$sqlSelectExpressions = [];
$objIndex = $newObjectResultAlias ?: $this->newObjectCounter++;
$objOwner = $objOwnerIdx = null;
if ($this->newObjectStack !== []) {
[$objOwner, $objOwnerIdx] = end($this->newObjectStack);
$objIndex = $objOwner . ':' . $objOwnerIdx;
} else {
$objIndex = $newObjectResultAlias ?: $this->newObjectCounter++;
}
foreach ($newObjectExpression->args as $argIndex => $e) {
$resultAlias = $this->scalarResultCounter++;
@@ -1468,7 +1525,10 @@ class SqlWalker
switch (true) {
case $e instanceof AST\NewObjectExpression:
$this->newObjectStack[] = [$objIndex, $argIndex];
$sqlSelectExpressions[] = $e->dispatch($this);
array_pop($this->newObjectStack);
$this->rsm->nestedNewObjectArguments[$columnAlias] = ['ownerIndex' => $objIndex, 'argIndex' => $argIndex];
break;
case $e instanceof AST\Subselect:

View File

@@ -77,6 +77,7 @@ enum TokenType: int
case T_OR = 242;
case T_ORDER = 243;
case T_OUTER = 244;
case T_PARTIAL = 245;
case T_SELECT = 246;
case T_SET = 247;
case T_SOME = 248;
@@ -88,4 +89,5 @@ enum TokenType: int
case T_WHEN = 254;
case T_WHERE = 255;
case T_WITH = 256;
case T_NAMED = 257;
}

View File

@@ -19,8 +19,7 @@ class TreeWalkerChain implements TreeWalker
/**
* The tree walkers.
*
* @var string[]
* @psalm-var list<class-string<TreeWalker>>
* @var list<class-string<TreeWalker>>
*/
private array $walkers = [];
@@ -47,8 +46,7 @@ class TreeWalkerChain implements TreeWalker
/**
* Adds a tree walker to the chain.
*
* @param string $walkerClass The class of the walker to instantiate.
* @psalm-param class-string<TreeWalker> $walkerClass
* @param class-string<TreeWalker> $walkerClass The class of the walker to instantiate.
*/
public function addTreeWalker(string $walkerClass): void
{

View File

@@ -110,6 +110,13 @@ class QueryBuilder implements Stringable
protected int $lifetime = 0;
/**
* The counter of bound parameters.
*
* @var int<0, max>
*/
private int $boundCounter = 0;
/**
* Initializes a new <tt>QueryBuilder</tt> that uses the given <tt>EntityManager</tt>.
*
@@ -294,6 +301,7 @@ class QueryBuilder implements Stringable
} else {
// Should never happen with correct joining order. Might be
// thoughtful to throw exception instead.
// @phpstan-ignore method.deprecated
$rootAlias = $this->getRootAlias();
}
@@ -576,6 +584,7 @@ class QueryBuilder implements Stringable
$newDqlPart = [];
foreach ($dqlPart as $k => $v) {
// @phpstan-ignore method.deprecated
$k = is_numeric($k) ? $this->getRootAlias() : $k;
$newDqlPart[$k] = $v;
@@ -1336,6 +1345,41 @@ class QueryBuilder implements Stringable
return $this;
}
/**
* Creates a new named parameter and bind the value $value to it.
*
* The parameter $value specifies the value that you want to bind. If
* $placeholder is not provided createNamedParameter() will automatically
* create a placeholder for you. An automatic placeholder will be of the
* name ':dcValue1', ':dcValue2' etc.
*
* Example:
* <code>
* $qb = $em->createQueryBuilder();
* $qb
* ->select('u')
* ->from('User', 'u')
* ->where('u.username = ' . $qb->createNamedParameter('Foo', Types::STRING))
* ->orWhere('u.username = ' . $qb->createNamedParameter('Bar', Types::STRING))
* </code>
*
* @param ParameterType|ArrayParameterType|string|int|null $type ParameterType::*, ArrayParameterType::* or \Doctrine\DBAL\Types\Type::* constant
* @param non-empty-string|null $placeholder The name to bind with. The string must start with a colon ':'.
*
* @return non-empty-string the placeholder name used.
*/
public function createNamedParameter(mixed $value, ParameterType|ArrayParameterType|string|int|null $type = null, string|null $placeholder = null): string
{
if ($placeholder === null) {
$this->boundCounter++;
$placeholder = ':dcValue' . $this->boundCounter;
}
$this->setParameter(substr($placeholder, 1), $value, $type);
return $placeholder;
}
/**
* Gets a string representation of this QueryBuilder which corresponds to
* the final DQL query being constructed.

View File

@@ -127,8 +127,7 @@ EOT);
/**
* Return all mapped entity class names
*
* @return string[]
* @psalm-return class-string[]
* @return class-string[]
*/
private function getMappedEntities(EntityManagerInterface $entityManager): array
{

View File

@@ -11,7 +11,7 @@ use Doctrine\ORM\Query\AST\SelectStatement;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\ParserResult;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\SqlOutputWalker;
use RuntimeException;
use function array_diff;
@@ -37,7 +37,7 @@ use function sprintf;
*
* @psalm-import-type QueryComponent from Parser
*/
class CountOutputWalker extends SqlWalker
class CountOutputWalker extends SqlOutputWalker
{
private readonly AbstractPlatform $platform;
private readonly ResultSetMapping $rsm;
@@ -53,13 +53,13 @@ class CountOutputWalker extends SqlWalker
parent::__construct($query, $parserResult, $queryComponents);
}
public function walkSelectStatement(SelectStatement $selectStatement): string
protected function createSqlForFinalizer(SelectStatement $selectStatement): string
{
if ($this->platform instanceof SQLServerPlatform) {
$selectStatement->orderByClause = null;
}
$sql = parent::walkSelectStatement($selectStatement);
$sql = parent::createSqlForFinalizer($selectStatement);
if ($selectStatement->groupByClause) {
return sprintf(

View File

@@ -37,27 +37,31 @@ class CountWalker extends TreeWalkerAdapter
throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction');
}
$fromRoot = reset($from);
$rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
$rootClass = $this->getMetadataForDqlAlias($rootAlias);
$identifierFieldName = $rootClass->getSingleIdentifierFieldName();
$distinct = $this->_getQuery()->getHint(self::HINT_DISTINCT);
$pathType = PathExpression::TYPE_STATE_FIELD;
if (isset($rootClass->associationMappings[$identifierFieldName])) {
$pathType = PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION;
$countPathExpressionOrLiteral = '*';
if ($distinct) {
$fromRoot = reset($from);
$rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
$rootClass = $this->getMetadataForDqlAlias($rootAlias);
$identifierFieldName = $rootClass->getSingleIdentifierFieldName();
$pathType = PathExpression::TYPE_STATE_FIELD;
if (isset($rootClass->associationMappings[$identifierFieldName])) {
$pathType = PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION;
}
$countPathExpressionOrLiteral = new PathExpression(
PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION,
$rootAlias,
$identifierFieldName,
);
$countPathExpressionOrLiteral->type = $pathType;
}
$pathExpression = new PathExpression(
PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION,
$rootAlias,
$identifierFieldName,
);
$pathExpression->type = $pathType;
$distinct = $this->_getQuery()->getHint(self::HINT_DISTINCT);
$selectStatement->selectClause->selectExpressions = [
new SelectExpression(
new AggregateExpression('count', $pathExpression, $distinct),
new AggregateExpression('count', $countPathExpressionOrLiteral, $distinct),
null,
),
];

View File

@@ -13,16 +13,20 @@ use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\QuoteStrategy;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\AST;
use Doctrine\ORM\Query\AST\OrderByClause;
use Doctrine\ORM\Query\AST\PathExpression;
use Doctrine\ORM\Query\AST\SelectExpression;
use Doctrine\ORM\Query\AST\SelectStatement;
use Doctrine\ORM\Query\AST\Subselect;
use Doctrine\ORM\Query\Exec\SingleSelectSqlFinalizer;
use Doctrine\ORM\Query\Exec\SqlFinalizer;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\ParserResult;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\SqlOutputWalker;
use LogicException;
use RuntimeException;
use function array_diff;
@@ -50,7 +54,7 @@ use function substr;
*
* @psalm-import-type QueryComponent from Parser
*/
class LimitSubqueryOutputWalker extends SqlWalker
class LimitSubqueryOutputWalker extends SqlOutputWalker
{
private const ORDER_BY_PATH_EXPRESSION = '/(?<![a-z0-9_])%s\.%s(?![a-z0-9_])/i';
@@ -85,6 +89,8 @@ class LimitSubqueryOutputWalker extends SqlWalker
$this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();
$this->rsm = $parserResult->getResultSetMapping();
$query = clone $query;
// Reset limit and offset
$this->firstResult = $query->getFirstResult();
$this->maxResults = $query->getMaxResults();
@@ -139,11 +145,28 @@ class LimitSubqueryOutputWalker extends SqlWalker
public function walkSelectStatement(SelectStatement $selectStatement): string
{
if ($this->platformSupportsRowNumber()) {
return $this->walkSelectStatementWithRowNumber($selectStatement);
$sqlFinalizer = $this->getFinalizer($selectStatement);
$query = $this->getQuery();
$abstractSqlExecutor = $sqlFinalizer->createExecutor($query);
return $abstractSqlExecutor->getSqlStatements();
}
public function getFinalizer(AST\DeleteStatement|AST\UpdateStatement|AST\SelectStatement $AST): SqlFinalizer
{
if (! $AST instanceof SelectStatement) {
throw new LogicException(self::class . ' is to be used on SelectStatements only');
}
return $this->walkSelectStatementWithoutRowNumber($selectStatement);
if ($this->platformSupportsRowNumber()) {
$sql = $this->createSqlWithRowNumber($AST);
} else {
$sql = $this->createSqlWithoutRowNumber($AST);
}
return new SingleSelectSqlFinalizer($sql);
}
/**
@@ -153,6 +176,16 @@ class LimitSubqueryOutputWalker extends SqlWalker
* @throws RuntimeException
*/
public function walkSelectStatementWithRowNumber(SelectStatement $AST): string
{
// Apply the limit and offset.
return $this->platform->modifyLimitQuery(
$this->createSqlWithRowNumber($AST),
$this->maxResults,
$this->firstResult,
);
}
private function createSqlWithRowNumber(SelectStatement $AST): string
{
$hasOrderBy = false;
$outerOrderBy = ' ORDER BY dctrn_minrownum ASC';
@@ -182,13 +215,6 @@ class LimitSubqueryOutputWalker extends SqlWalker
$sql .= $orderGroupBy . $outerOrderBy;
}
// Apply the limit and offset.
$sql = $this->platform->modifyLimitQuery(
$sql,
$this->maxResults,
$this->firstResult,
);
// Add the columns to the ResultSetMapping. It's not really nice but
// it works. Preferably I'd clear the RSM or simply create a new one
// but that is not possible from inside the output walker, so we dirty
@@ -207,6 +233,16 @@ class LimitSubqueryOutputWalker extends SqlWalker
* @throws RuntimeException
*/
public function walkSelectStatementWithoutRowNumber(SelectStatement $AST, bool $addMissingItemsFromOrderByToSelect = true): string
{
// Apply the limit and offset.
return $this->platform->modifyLimitQuery(
$this->createSqlWithoutRowNumber($AST, $addMissingItemsFromOrderByToSelect),
$this->maxResults,
$this->firstResult,
);
}
private function createSqlWithoutRowNumber(SelectStatement $AST, bool $addMissingItemsFromOrderByToSelect = true): string
{
// We don't want to call this recursively!
if ($AST->orderByClause instanceof OrderByClause && $addMissingItemsFromOrderByToSelect) {
@@ -235,13 +271,6 @@ class LimitSubqueryOutputWalker extends SqlWalker
// https://github.com/doctrine/orm/issues/2630
$sql = $this->preserveSqlOrdering($sqlIdentifier, $innerSql, $sql, $orderByClause);
// Apply the limit and offset.
$sql = $this->platform->modifyLimitQuery(
$sql,
$this->maxResults,
$this->firstResult,
);
// Add the columns to the ResultSetMapping. It's not really nice but
// it works. Preferably I'd clear the RSM or simply create a new one
// but that is not possible from inside the output walker, so we dirty

View File

@@ -183,7 +183,7 @@ class Paginator implements Countable, IteratorAggregate
/**
* Appends a custom tree walker to the tree walkers hint.
*
* @psalm-param class-string $walkerClass
* @param class-string $walkerClass
*/
private function appendTreeWalker(Query $query, string $walkerClass): void
{

View File

@@ -5,7 +5,10 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Pagination;
use Doctrine\ORM\Query\AST;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\Exec\FinalizedSelectExecutor;
use Doctrine\ORM\Query\Exec\PreparedExecutorFinalizer;
use Doctrine\ORM\Query\Exec\SqlFinalizer;
use Doctrine\ORM\Query\SqlOutputWalker;
use Doctrine\ORM\Utility\PersisterHelper;
use RuntimeException;
@@ -22,7 +25,7 @@ use function reset;
* Returning the type instead of a "real" SQL statement is a slight hack. However, it has the
* benefit that the DQL -> root entity id type resolution can be cached in the query cache.
*/
final class RootTypeWalker extends SqlWalker
final class RootTypeWalker extends SqlOutputWalker
{
public function walkSelectStatement(AST\SelectStatement $selectStatement): string
{
@@ -45,4 +48,13 @@ final class RootTypeWalker extends SqlWalker
->getEntityManager(),
)[0];
}
public function getFinalizer(AST\DeleteStatement|AST\UpdateStatement|AST\SelectStatement $AST): SqlFinalizer
{
if (! $AST instanceof AST\SelectStatement) {
throw new RuntimeException(self::class . ' is to be used on SelectStatements only');
}
return new PreparedExecutorFinalizer(new FinalizedSelectExecutor($this->walkSelectStatement($AST)));
}
}

View File

@@ -47,7 +47,7 @@ use function strtolower;
*/
class SchemaTool
{
private const KNOWN_COLUMN_OPTIONS = ['comment', 'unsigned', 'fixed', 'default'];
private const KNOWN_COLUMN_OPTIONS = ['comment', 'unsigned', 'fixed', 'default', 'values'];
private readonly AbstractPlatform $platform;
private readonly QuoteStrategy $quoteStrategy;
@@ -421,18 +421,12 @@ class SchemaTool
*/
private function gatherColumns(ClassMetadata $class, Table $table): void
{
$pkColumns = [];
foreach ($class->fieldMappings as $mapping) {
if ($class->isInheritanceTypeSingleTable() && isset($mapping->inherited)) {
continue;
}
$this->gatherColumn($class, $mapping, $table);
if ($class->isIdentifier($mapping->fieldName)) {
$pkColumns[] = $this->quoteStrategy->getColumnName($mapping->fieldName, $class, $this->platform);
}
}
}

View File

@@ -49,7 +49,6 @@ use Exception;
use InvalidArgumentException;
use RuntimeException;
use Stringable;
use Throwable;
use UnexpectedValueException;
use function array_chunk;
@@ -119,7 +118,7 @@ class UnitOfWork implements PropertyChangedListener
* Since all classes in a hierarchy must share the same identifier set,
* we always take the root class name of the hierarchy.
*
* @psalm-var array<class-string, array<string, object>>
* @var array<class-string, array<string, object>>
*/
private array $identityMap = [];
@@ -165,7 +164,7 @@ class UnitOfWork implements PropertyChangedListener
* This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
* Keys are object ids (spl_object_id).
*
* @psalm-var array<class-string, array<int, mixed>>
* @var array<class-string, array<int, mixed>>
*/
private array $scheduledForSynchronization = [];
@@ -290,7 +289,7 @@ class UnitOfWork implements PropertyChangedListener
/**
* Map of Entity Class-Names and corresponding IDs that should eager loaded when requested.
*
* @psalm-var array<class-string, array<string, mixed>>
* @var array<class-string, array<string, mixed>>
*/
private array $eagerLoadingEntities = [];
@@ -379,6 +378,8 @@ class UnitOfWork implements PropertyChangedListener
$conn = $this->em->getConnection();
$conn->beginTransaction();
$successful = false;
try {
// Collection deletions (deletions of complete collections)
foreach ($this->collectionDeletions as $collectionToDelete) {
@@ -436,16 +437,18 @@ class UnitOfWork implements PropertyChangedListener
if ($commitFailed) {
throw new OptimisticLockException('Commit failed', null, $e ?? null);
}
} catch (Throwable $e) {
$this->em->close();
if ($conn->isTransactionActive()) {
$conn->rollBack();
$successful = true;
} finally {
if (! $successful) {
$this->em->close();
if ($conn->isTransactionActive()) {
$conn->rollBack();
}
$this->afterTransactionRolledBack();
}
$this->afterTransactionRolledBack();
throw $e;
}
$this->afterTransactionComplete();
@@ -2340,11 +2343,9 @@ class UnitOfWork implements PropertyChangedListener
*
* Internal note: Highly performance-sensitive method.
*
* @param string $className The name of the entity class.
* @param mixed[] $data The data for the entity.
* @param mixed[] $hints Any hints to account for during reconstitution/lookup of the entity.
* @psalm-param class-string $className
* @psalm-param array<string, mixed> $hints
* @param class-string $className The name of the entity class.
* @param mixed[] $data The data for the entity.
* @param array<string, mixed> $hints Any hints to account for during reconstitution/lookup of the entity.
*
* @return object The managed entity instance.
*
@@ -2467,10 +2468,7 @@ class UnitOfWork implements PropertyChangedListener
} else {
$associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue;
}
} elseif (
$targetClass->containsForeignIdentifier
&& in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, true)
) {
} elseif (in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, true)) {
// the missing key is part of target's entity primary key
$associatedId = [];
break;
@@ -2746,7 +2744,7 @@ class UnitOfWork implements PropertyChangedListener
/**
* Gets the identity map of the UnitOfWork.
*
* @psalm-return array<class-string, array<string, object>>
* @return array<class-string, array<string, object>>
*/
public function getIdentityMap(): array
{
@@ -2827,9 +2825,8 @@ class UnitOfWork implements PropertyChangedListener
* Tries to find an entity with the given identifier in the identity map of
* this UnitOfWork.
*
* @param mixed $id The entity identifier to look for.
* @param string $rootClassName The name of the root class of the mapped entity hierarchy.
* @psalm-param class-string $rootClassName
* @param mixed $id The entity identifier to look for.
* @param class-string $rootClassName The name of the root class of the mapped entity hierarchy.
*
* @return object|false Returns the entity with the specified identifier if it exists in
* this UnitOfWork, FALSE otherwise.
@@ -2873,7 +2870,7 @@ class UnitOfWork implements PropertyChangedListener
/**
* Gets the EntityPersister for an Entity.
*
* @psalm-param class-string $entityName
* @param class-string $entityName The name of the Entity.
*/
public function getEntityPersister(string $entityName): EntityPersister
{

View File

@@ -1,18 +0,0 @@
<?php
declare(strict_types=1);
namespace Doctrine\Performance;
use Doctrine\DBAL\Cache\ArrayResult;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\PDO\SQLite\Driver;
use Doctrine\DBAL\Result;
final class ArrayResultFactory
{
public static function createFromArray(array $resultSet): Result
{
return new Result(new ArrayResult($resultSet), new Connection([], new Driver()));
}
}

View File

@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Doctrine\Performance;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Cache\ArrayResult;
use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\PDO\SQLite\Driver;
@@ -17,6 +16,7 @@ use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Doctrine\ORM\Proxy\ProxyFactory;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\Tests\Mocks\ArrayResultFactory;
use Doctrine\Tests\TestUtil;
use function array_map;
@@ -67,7 +67,7 @@ final class EntityManagerFactory
/** {@inheritDoc} */
public function executeQuery(string $sql, array $params = [], $types = [], QueryCacheProfile|null $qcp = null): Result
{
return new Result(new ArrayResult([]), $this);
return ArrayResultFactory::createWrapperResultFromArray([], $this);
}
};

View File

@@ -7,8 +7,8 @@ namespace Doctrine\Performance\Hydration;
use Doctrine\DBAL\Result;
use Doctrine\ORM\Internal\Hydration\ArrayHydrator;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Performance\ArrayResultFactory;
use Doctrine\Performance\EntityManagerFactory;
use Doctrine\Tests\Mocks\ArrayResultFactory;
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
use Doctrine\Tests\Models\CMS\CmsUser;
use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods;
@@ -62,7 +62,7 @@ final class MixedQueryFetchJoinArrayHydrationPerformanceBench
];
}
$this->result = ArrayResultFactory::createFromArray($resultSet);
$this->result = ArrayResultFactory::createWrapperResultFromArray($resultSet);
$this->hydrator = new ArrayHydrator(EntityManagerFactory::getEntityManager([]));
$this->rsm = new ResultSetMapping();

View File

@@ -7,8 +7,8 @@ namespace Doctrine\Performance\Hydration;
use Doctrine\DBAL\Result;
use Doctrine\ORM\Internal\Hydration\ObjectHydrator;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Performance\ArrayResultFactory;
use Doctrine\Performance\EntityManagerFactory;
use Doctrine\Tests\Mocks\ArrayResultFactory;
use Doctrine\Tests\Models\CMS\CmsAddress;
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
use Doctrine\Tests\Models\CMS\CmsUser;
@@ -49,7 +49,7 @@ final class MixedQueryFetchJoinFullObjectHydrationPerformanceBench
];
}
$this->result = ArrayResultFactory::createFromArray($resultSet);
$this->result = ArrayResultFactory::createWrapperResultFromArray($resultSet);
$this->hydrator = new ObjectHydrator(EntityManagerFactory::getEntityManager([]));
$this->rsm = new ResultSetMapping();

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace Doctrine\Performance\Hydration;
use Doctrine\DBAL\Result;
use Doctrine\ORM\Internal\Hydration\ObjectHydrator;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Performance\EntityManagerFactory;
use Doctrine\Tests\Mocks\ArrayResultFactory;
use Doctrine\Tests\Models\CMS\CmsAddress;
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
use Doctrine\Tests\Models\CMS\CmsUser;
use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods;
/** @BeforeMethods({"init"}) */
final class MixedQueryFetchJoinPartialObjectHydrationPerformanceBench
{
private ObjectHydrator|null $hydrator = null;
private ResultSetMapping|null $rsm = null;
private Result|null $result = null;
public function init(): void
{
$resultSet = [
[
'u__id' => '1',
'u__status' => 'developer',
'u__username' => 'romanb',
'u__name' => 'Roman',
'sclr0' => 'ROMANB',
'p__phonenumber' => '42',
'a__id' => '1',
],
[
'u__id' => '1',
'u__status' => 'developer',
'u__username' => 'romanb',
'u__name' => 'Roman',
'sclr0' => 'ROMANB',
'p__phonenumber' => '43',
'a__id' => '1',
],
[
'u__id' => '2',
'u__status' => 'developer',
'u__username' => 'romanb',
'u__name' => 'Roman',
'sclr0' => 'JWAGE',
'p__phonenumber' => '91',
'a__id' => '1',
],
];
for ($i = 4; $i < 2000; ++$i) {
$resultSet[] = [
'u__id' => $i,
'u__status' => 'developer',
'u__username' => 'jwage',
'u__name' => 'Jonathan',
'sclr0' => 'JWAGE' . $i,
'p__phonenumber' => '91',
'a__id' => '1',
];
}
$this->result = ArrayResultFactory::createWrapperResultFromArray($resultSet);
$this->hydrator = new ObjectHydrator(EntityManagerFactory::getEntityManager([]));
$this->rsm = new ResultSetMapping();
$this->rsm->addEntityResult(CmsUser::class, 'u');
$this->rsm->addJoinedEntityResult(CmsPhonenumber::class, 'p', 'u', 'phonenumbers');
$this->rsm->addFieldResult('u', 'u__id', 'id');
$this->rsm->addFieldResult('u', 'u__status', 'status');
$this->rsm->addFieldResult('u', 'u__username', 'username');
$this->rsm->addFieldResult('u', 'u__name', 'name');
$this->rsm->addScalarResult('sclr0', 'nameUpper');
$this->rsm->addFieldResult('p', 'p__phonenumber', 'phonenumber');
$this->rsm->addJoinedEntityResult(CmsAddress::class, 'a', 'u', 'address');
$this->rsm->addFieldResult('a', 'a__id', 'id');
}
public function benchHydration(): void
{
$this->hydrator->hydrateAll($this->result, $this->rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]);
}
}

View File

@@ -7,8 +7,8 @@ namespace Doctrine\Performance\Hydration;
use Doctrine\DBAL\Result;
use Doctrine\ORM\Internal\Hydration\ArrayHydrator;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Performance\ArrayResultFactory;
use Doctrine\Performance\EntityManagerFactory;
use Doctrine\Tests\Mocks\ArrayResultFactory;
use Doctrine\Tests\Models\CMS\CmsUser;
use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods;
@@ -53,7 +53,7 @@ final class SimpleQueryArrayHydrationPerformanceBench
];
}
$this->result = ArrayResultFactory::createFromArray($resultSet);
$this->result = ArrayResultFactory::createWrapperResultFromArray($resultSet);
$this->hydrator = new ArrayHydrator(EntityManagerFactory::getEntityManager([]));
$this->rsm = new ResultSetMapping();

View File

@@ -7,8 +7,8 @@ namespace Doctrine\Performance\Hydration;
use Doctrine\DBAL\Result;
use Doctrine\ORM\Internal\Hydration\ObjectHydrator;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Performance\ArrayResultFactory;
use Doctrine\Performance\EntityManagerFactory;
use Doctrine\Tests\Mocks\ArrayResultFactory;
use Doctrine\Tests\Models\CMS\CmsAddress;
use Doctrine\Tests\Models\CMS\CmsUser;
use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods;
@@ -44,7 +44,7 @@ final class SimpleQueryFullObjectHydrationPerformanceBench
];
}
$this->result = ArrayResultFactory::createFromArray($resultSet);
$this->result = ArrayResultFactory::createWrapperResultFromArray($resultSet);
$this->hydrator = new ObjectHydrator(EntityManagerFactory::getEntityManager([]));
$this->rsm = new ResultSetMapping();

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Doctrine\Performance\Hydration;
use Doctrine\DBAL\Result;
use Doctrine\ORM\Internal\Hydration\ObjectHydrator;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Performance\EntityManagerFactory;
use Doctrine\Tests\Mocks\ArrayResultFactory;
use Doctrine\Tests\Models\CMS\CmsAddress;
use Doctrine\Tests\Models\CMS\CmsUser;
use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods;
/** @BeforeMethods({"init"}) */
final class SimpleQueryPartialObjectHydrationPerformanceBench
{
private ObjectHydrator|null $hydrator = null;
private ResultSetMapping|null $rsm = null;
private Result|null $result = null;
public function init(): void
{
$resultSet = [
[
'u__id' => '1',
'u__status' => 'developer',
'u__username' => 'romanb',
'u__name' => 'Roman',
'a__id' => '1',
],
[
'u__id' => '1',
'u__status' => 'developer',
'u__username' => 'romanb',
'u__name' => 'Roman',
'a__id' => '1',
],
[
'u__id' => '2',
'u__status' => 'developer',
'u__username' => 'romanb',
'u__name' => 'Roman',
'a__id' => '1',
],
];
for ($i = 4; $i < 10000; ++$i) {
$resultSet[] = [
'u__id' => $i,
'u__status' => 'developer',
'u__username' => 'jwage',
'u__name' => 'Jonathan',
'a__id' => '1',
];
}
$this->result = ArrayResultFactory::createWrapperResultFromArray($resultSet);
$this->hydrator = new ObjectHydrator(EntityManagerFactory::getEntityManager([]));
$this->rsm = new ResultSetMapping();
$this->rsm->addEntityResult(CmsUser::class, 'u');
$this->rsm->addFieldResult('u', 'u__id', 'id');
$this->rsm->addFieldResult('u', 'u__status', 'status');
$this->rsm->addFieldResult('u', 'u__username', 'username');
$this->rsm->addFieldResult('u', 'u__name', 'name');
$this->rsm->addJoinedEntityResult(CmsAddress::class, 'a', 'u', 'address');
$this->rsm->addFieldResult('a', 'a__id', 'id');
}
public function benchHydration(): void
{
$this->hydrator->hydrateAll($this->result, $this->rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]);
}
}

View File

@@ -7,8 +7,8 @@ namespace Doctrine\Performance\Hydration;
use Doctrine\DBAL\Result;
use Doctrine\ORM\Internal\Hydration\ScalarHydrator;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Performance\ArrayResultFactory;
use Doctrine\Performance\EntityManagerFactory;
use Doctrine\Tests\Mocks\ArrayResultFactory;
use Doctrine\Tests\Models\CMS\CmsUser;
use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods;
@@ -53,7 +53,7 @@ final class SimpleQueryScalarHydrationPerformanceBench
];
}
$this->result = ArrayResultFactory::createFromArray($resultSet);
$this->result = ArrayResultFactory::createWrapperResultFromArray($resultSet);
$this->hydrator = new ScalarHydrator(EntityManagerFactory::getEntityManager([]));
$this->rsm = new ResultSetMapping();

View File

@@ -10,9 +10,9 @@ use Doctrine\ORM\Mapping\ClassMetadata;
class MetadataGenerator
{
/**
* @psalm-param class-string<T> $entityName
* @param class-string<T> $entityName
*
* @psalm-return ClassMetadata<T>
* @return ClassMetadata<T>
*/
public function createMetadata(string $entityName): ClassMetadata
{

View File

@@ -15,15 +15,15 @@ use Doctrine\ORM\Mapping\ClassMetadata;
*/
abstract class GetMetadata
{
/** @psalm-param class-string|object $class */
/** @param class-string|object $class */
abstract public function getEntityManager(string|object $class): EntityManagerInterface;
/**
* @psalm-param class-string<TObject> $class
* @param class-string<TObject> $class
*
* @psalm-return ClassMetadata<TObject>
* @return ClassMetadata<TObject>
*
* @psalm-template TObject of object
* @template TObject of object
*/
public function __invoke(string $class): ClassMetadata
{

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Mocks;
use Doctrine\DBAL\Cache\ArrayResult;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\PDO\SQLite\Driver;
use Doctrine\DBAL\Result;
use ReflectionMethod;
use function array_keys;
use function array_map;
use function array_values;
final class ArrayResultFactory
{
/** @param list<array<string, mixed>> $resultSet */
public static function createDriverResultFromArray(array $resultSet): ArrayResult
{
if ((new ReflectionMethod(ArrayResult::class, '__construct'))->getNumberOfRequiredParameters() < 2) {
// DBAL < 4.2
return new ArrayResult($resultSet);
}
// DBAL 4.2+
return new ArrayResult(
array_keys($resultSet[0] ?? []),
array_map(array_values(...), $resultSet),
);
}
/** @param list<array<string, mixed>> $resultSet */
public static function createWrapperResultFromArray(array $resultSet, Connection|null $connection = null): Result
{
return new Result(
self::createDriverResultFromArray($resultSet),
$connection ?? new Connection([], new Driver()),
);
}
}

View File

@@ -7,12 +7,14 @@ namespace Doctrine\Tests\Mocks;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\Query\AST;
use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\Exec\PreparedExecutorFinalizer;
use Doctrine\ORM\Query\Exec\SqlFinalizer;
use Doctrine\ORM\Query\SqlOutputWalker;
/**
* SqlWalker implementation that does not produce SQL.
*/
final class NullSqlWalker extends SqlWalker
final class NullSqlWalker extends SqlOutputWalker
{
public function walkSelectStatement(AST\SelectStatement $selectStatement): string
{
@@ -29,13 +31,15 @@ final class NullSqlWalker extends SqlWalker
return '';
}
public function getExecutor(AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement $statement): AbstractSqlExecutor
public function getFinalizer(AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement $statement): SqlFinalizer
{
return new class extends AbstractSqlExecutor {
public function execute(Connection $conn, array $params, array $types): int
{
return 0;
}
};
return new PreparedExecutorFinalizer(
new class extends AbstractSqlExecutor {
public function execute(Connection $conn, array $params, array $types): int
{
return 0;
}
},
);
}
}

View File

@@ -6,7 +6,7 @@ namespace Doctrine\Tests\Models\CMS;
class CmsAddressDTO
{
public function __construct(public string|null $country = null, public string|null $city = null, public string|null $zip = null)
public function __construct(public string|null $country = null, public string|null $city = null, public string|null $zip = null, public CmsAddressDTO|string|null $address = null)
{
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\CMS;
class CmsAddressDTONamedArgs
{
public function __construct(
public string|null $country = null,
public string|null $city = null,
public string|null $zip = null,
public CmsAddressDTO|string|null $address = null,
) {
}
}

View File

@@ -6,7 +6,7 @@ namespace Doctrine\Tests\Models\CMS;
class CmsUserDTO
{
public function __construct(public string|null $name = null, public string|null $email = null, public string|null $address = null, public int|null $phonenumbers = null)
public function __construct(public string|null $name = null, public string|null $email = null, public CmsAddressDTO|string|null $address = null, public int|null $phonenumbers = null)
{
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\CMS;
class CmsUserDTONamedArgs
{
public function __construct(
public string|null $name = null,
public string|null $email = null,
public string|null $address = null,
public int|null $phonenumbers = null,
public CmsAddressDTO|null $addressDto = null,
public CmsAddressDTONamedArgs|null $addressDtoNamedArgs = null,
) {
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\CMS;
class CmsUserDTOVariadicArg
{
public string|null $name = null;
public string|null $email = null;
public string|null $address = null;
public int|null $phonenumbers = null;
public function __construct(...$args)
{
$this->name = $args['name'] ?? null;
$this->email = $args['email'] ?? null;
$this->phonenumbers = $args['phonenumbers'] ?? null;
$this->address = $args['address'] ?? null;
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\CompositeKeyRelations;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Id;
#[Entity]
class CustomerClass
{
#[Id]
#[Column(type: 'string')]
public string $companyCode;
#[Id]
#[Column(type: 'string')]
public string $code;
#[Column(type: 'string')]
public string $name;
}

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