Compare commits

...

135 Commits

Author SHA1 Message Date
Grégoire Paris
ccfc97c32f Merge pull request #11187 from jwage/remove-xml-validation-disabling-deprecation
Remove XML validation disabling deprecation.
2024-01-26 20:41:16 +01:00
Jonathan H. Wage
d386b43be3 Remove XML validation disabling deprecation. 2024-01-26 09:59:03 -06:00
Grégoire Paris
0970ce7072 Merge pull request #11186 from derrabus/chore/readme-versions
Update branches in README
2024-01-26 08:34:24 +01:00
Alexander M. Turek
624c56be72 Update branches in README 2024-01-26 00:52:35 +01:00
Grégoire Paris
fbc8e6741e Merge pull request #11176 from thePanz/fix-11173-get-name-on-null-non-backed-enum
Throw exception when trying to use non-backed enum types
2024-01-23 07:50:20 +01:00
thePanz
7151db3cb8 Throw exception when trying to use non-backed enum types 2024-01-22 13:04:58 +01:00
Grégoire Paris
ac24c11808 Modernize code in documentation (#11179)
Somehow, there still were code samples relying on annotations.
2024-01-20 21:53:48 +01:00
Grégoire Paris
dd478d8662 Merge pull request #11178 from greg0ire/remove-ref-jira
Remove references to JIRA
2024-01-20 14:06:15 +01:00
Grégoire Paris
0b3cd72609 Remove references to JIRA 2024-01-20 13:45:04 +01:00
Grégoire Paris
85034699cb Merge pull request #11171 from greg0ire/extract-class
Make Doctrine\Tests\ORM\Internal\Node autoloadable
2024-01-18 21:33:06 +01:00
Grégoire Paris
d98186e2c4 Make Doctrine\Tests\ORM\Internal\Node autoloadable
It is used in several tests.
2024-01-18 21:19:28 +01:00
Grégoire Paris
a0ed37954b Merge pull request #11167 from bobvandevijver/fix-eager-iterable-loading-test
Use foreach on iterable to prevent table locks during tests
2024-01-18 16:47:11 +01:00
Bob van de Vijver
4875f4c878 Use foreach on iterable to prevent table locks during tests 2024-01-18 10:24:40 +01:00
Grégoire Paris
398ab0547a Merge pull request #11162 from greg0ire/fix-libxml-compat
Remove redundant tags
2024-01-16 22:32:04 +01:00
Grégoire Paris
8f15337b03 Remove redundant tags
The "any" tags inside the definition for mapped superclasses and
embeddables duplicate what is already done for entities.

The other removed "any" tags are also redundant, as they duplicate
what's already done inside the grandparent "choice" tag.

Starting with version libxml 2.12, such redundant tags cause errors
about the content model not being "determinist".

Fixes #11117
2024-01-16 22:01:16 +01:00
Matthias Pigulla
a8632aca8f Keep the declared mapping information when using attribute overrides (#11135)
When using `AttributeOverride` to override mapping information inherited from a parent class (a mapped superclass), make sure to keep information about where the field was originally declared.

This is important for `private` fields: Without the correct `declared` information, it will lead to errors when cached mapping information is loaded, reflection wakes up and looks for the private field in the wrong class.
2024-01-13 00:06:34 +01:00
Matthias Pigulla
3dd3d38857 Fix @SequenceGeneratorDefinition inheritance, take 1 (#11050)
#10927 reported that #10455 broke the way how the default `@SequenceGeneratorDefinition` is created and inherited by subclasses for ID columns using `@GeneratedValue(strategy="SEQUENCE")`.

First, I had to understand how `@SequenceGeneratorDefinition` has been handled before #10455 when entity inheritance comes into play:

* Entity and mapped superclasses inherit the ID generator type (as given by `@GeneratedValue`) from their parent classes
* `@SequenceGeneratorDefinition`, however, is not generally inherited
* ... instead, a default sequence generator definition is created for every class when no explicit configuration is given. In this case, sequence names are based on the current class' table name.
* Once a root entity has been identified, all subclasses inherit its sequence generator definition unchanged.

#### Why did #10455 break this?

When I implemented #10455, I was mislead by two tests `BasicInheritanceMappingTest::testGeneratedValueFromMappedSuperclass` and `BasicInheritanceMappingTest::testMultipleMappedSuperclasses`.

These tests check the sequence generator definition that is inherited by an entity class from a mapped superclass, either directly or through an additional (intermediate) mapped superclass.

The tests expect the sequence generator definition on the entity _to be the same_ as on the base mapped superclass.

The reason why the tests worked before was the quirky behaviour of the annotation and attribute drivers that #10455 was aiming at: The drivers did not report the `@SequenceGeneratorDefinition` on the base mapped superclass where it was actually defined. Instead, they reported this `@SequenceGeneratorDefinition` for the entity class only.

This means the inheritance rules stated above did not take effect, since the ID field with the sequence generator was virtually pushed down to the entity class.

In #10455, I did not realize that these failing tests had to do with the quirky and changed mapping driver behaviour. Instead, I tried to "fix" the inheritance rules by passing along the sequence generator definition unchanged once the ID column had been defined.

#### Consequences of the change suggested here

This PR reverts the changes made to `@SequenceGeneratorDefinition` inheritance behaviour that were done in #10455.

This means that with the new "report fields where declared" driver mode (which is active in our functional tests) we can not expect the sequence generator definition to be inherited from mapped superclasses. The two test cases from `BasicInheritanceMappingTest` are removed.

I will leave a notice in #10455 to indicate that the new driver mode also affects sequence generator definitions.

The `GH10927Test` test case validates the sequence names generated in a few cases. In fact, I wrote this test against the `2.15.x` branch to make sure we get results that are consistent with the previous behaviour.

This also means `@SequenceGeneratorDefinition` on mapped superclasses is pointless: The mapped superclass does not make use of the definition itself (it has no table), and the setting is never inherited to child classes.
 
Fixes #10927. There is another implementation with slightly different inheritance semantics in #11052, in case the fix is not good enough and we'd need to review the topic later on.
2024-01-12 22:59:14 +01:00
Matthias Pigulla
c6b3509aa9 Include ON DELETE CASCADE associations in the delete order computation (#10913)
In order to resolve #10348, some changes were included in #10547 to improve the computed _delete_ order for entities. 

One assumption was that foreign key references with `ON DELETE SET NULL` or `... CASCADE` need not need to be taken into consideration when planning the deletion order, since the RDBMS would unset or cascade-delete such associations by itself when necessary. Only associations that do _not_ use RDBMS-level cascade handling would be sequenced, to make sure the referring entity is deleted before the referred-to one.

This assumption is wrong for `ON DELETE CASCADE`. The following examples give reasons why we need to also consider such associations, and in addition, we need to be able to deal with cycles formed by them.

In the following diagrams, `odc` means `ON DELETE CASCADE`, and `ref` is a regular foreign key with no extra `ON DELETE` semantics.

```mermaid
graph LR;
C-->|ref| B;
B-->|odc| A;
```

In this example, C must be removed before B and A. If we ignore the B->A dependency in the delete order computation, the result may not to be correct. ACB is not a working solution.

```mermaid
graph LR;
A-->|odc| B;
B-->|odc| A;
C-->|ref| B;
```

This is the situation in #10912. We have to deal with a cycle in the graph. C must be removed before A as well as B. If we ignore the B->A dependency (e.g. because we set it to "optional" to get away with the cycle), we might end up with an incorrect order ACB.

```mermaid
graph LR;
A-->|odc| B;
B-->|odc| A;
A-->|ref| C;
C-->|ref| B;
```

This example has no possible remove order. But, if we treat `odc` edges as optional, A -> C -> B would wrongly be deemed suitable.

```mermaid
graph LR;
A-->|ref| B;
B-->|odc| C;
C-->|odc| B;
D-->|ref| C;
```

Here, we must first remove A and D in any order; then, B and C in any order. If we treat one of the `odc` edges as optional, we might find the invalid solutions ABDC or DCAB.

#### Solution implemented in this PR

First, build a graph with a node for every to-be-removed entity, and edges for `ON DELETE CASCADE` associations between those entities. Then, use [Tarjan's algorithm](https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm) to find strongly connected components (SCCs) in this graph. The significance of SCCs is that whenever we remove one of the entities in a SCC from the database (no matter which one), the DBMS will immediately remove _all_ the other entities of that group as well.

For every SCC, pick one (arbitrary) entity from the group to represent all entities of that group. 

Then, build a second graph. Again we have nodes for all entities that are to be removed. This time, we insert edges for all regular (foreign key) associations and those with `ON DELETE CASCADE`. `ON DELETE SET NULL` can be left out. The edges are not added between the entities themselves, but between the entities representing the respective SCCs.

Also, for all non-trivial SCCs (those containing more than a single entity), add dependency edges to indicate that all entities of the SCC shall be processed _after_ the entity representing the group. This is to make sure we do not remove a SCC inadvertedly by removing one of its entities too early.

Run a topological sort on the second graph to get the actual delete order. Cycles in this second graph are a problem, there is no delete order.

Fixes #10912.
2024-01-12 22:44:07 +01:00
Grégoire Paris
a32578b7ea Merge pull request #11082 from bobvandevijver/eager-collection-iterable
Do not defer eager collection loading when in iteration context
2024-01-10 10:03:40 +01:00
Matthias Pigulla
e585a92763 Mention that `postRemove` may still see removed entities in in-memory collections (#11146)
... plus minor tweaks.
2024-01-02 21:31:28 +01:00
Grégoire Paris
26f47cb8d3 Merge pull request #11142 from greg0ire/remove-inheritance
Remove inheritance
2024-01-02 08:24:50 +01:00
Grégoire Paris
ebb101009c Remove inheritance
Spotted while trying to merge https://github.com/doctrine/orm/pull/11076
(among other things) up into 3.0.x. On that branch, it is no longer
possible for an entity to extend another entity without specifying an
inheritance mapping type.

I think the goal of that inheritance was just to reuse the identifier
anyway, so let's just duplicate the identifier declaration instead.
2023-12-28 19:59:02 +01:00
Grégoire Paris
f80ef66ffb Merge pull request #11134 from doctrine/no-private-fields-duplicate
Mention in the limitations that private field names cannot be reused
2023-12-22 17:35:27 +01:00
Matthias Pigulla
85d78f8b0d Mention in the limitations that private field names cannot be reused 2023-12-22 17:13:11 +01:00
Grégoire Paris
c2886478e8 Merge pull request #11086 from mpdude/11058-revisited
Avoid an inconsistency in topological sort result order
2023-12-21 22:51:39 +01:00
Matthias Pigulla
108fa30db2 Improve topological sort result order
This PR changes a detail in the commit order computation for depended-upon entities.

We have a parent-child relationship between two entity classes. The association is parent one-to-many children, with the child entities containing the (owning side) back-reference.

Cascade-persist is not used, so all entities have to be passed to `EntityManager::persist()`.

Before v2.16.0, two child entities C1 and C2 will be inserted in the same order in which they are passed to `persist()`, and that is regardless of whether the parent entity was passed to `persist()` before or after the child entities.

As of v2.16.0, passing the parent entity to `persist()` _after_ the child entities will lead to an insert order that is _reversed_ compared to the order of `persist()` calls.

This PR makes the order consistent in both cases, as it was before v2.16.0.

 #### Cause

When the parent is passed to `persist()` after the children, commit order computation has to re-arrange the entities. The parent must be inserted first since it is referred to by the children.

The implementation of the topological sort from #10547 processed entities in reverse `persist()` order and unshifted finished nodes to an array to obtain the final result. That leads to dependencies (parent → before c1, parent → before c2) showing up in the result in the reverse order of which they were added.

This PR changes the topological sort to produce a result in the opposite order ("all edges pointing left"), which helps to avoid the duplicate array order reversal.

 #### Discussion

* This PR _does not_ change semantics of the `persist()` so that entities would (under all ciscumstances) be inserted in the order of `persist()` calls.
* It fixes an unnecessary inconsistency between versions before 2.16.0 and after. In particular, it may be surprising that the insert order for the child entities depends on whether another referred-to entity (the parent) was added before or after them.
* _Both_ orders (c1 before or after c2) are technically and logically correct with regard to the agreement that `commit()` is free to arrange entities in a way that allows for efficient insertion into the database.

Fixes #11058.
2023-12-21 16:26:20 +01:00
Alexander M. Turek
393679a479 Allow to skip property type validation (#11130) 2023-12-20 22:47:52 +01:00
Grégoire Paris
e50ae06fe7 Merge pull request #11122 from yceruto/enum_with_interface
Fix enum mapping validation
2023-12-15 08:12:07 +01:00
Yonel Ceruto
05ef1f4f96 Fixed enum mapping validation 2023-12-14 16:53:12 -05:00
Grégoire Paris
2b91edc525 Merge pull request #11115 from localheinz/fix/typo
Fix: Typo
2023-12-12 16:42:52 +01:00
Andreas Möller
6af7f9f7bf Fix: Typo 2023-12-12 16:33:54 +01:00
flaushi
46cb9a980b Added a note parameter type for the INSTANCE OF DQL expression (#7963)
Co-authored-by: flaushi <flaushi@users.noreply.github.com>
2023-12-12 14:51:31 +01:00
flack
ed1df148c2 Fix method name in code example (#11104) 2023-12-04 21:07:27 +01:00
Tomas Norkūnas
44e943e100 Fix JSON mapping linting against subset of builtin types (#11076) 2023-12-02 11:32:08 +01:00
Cliff Odijk
23d36c0d52 Add compatibility with the Symfony 4.4 VarExporter (#10948) 2023-12-01 19:23:51 +01:00
Alexander M. Turek
212edaa80b PHPStan 5.16.0, Symfony 7.0 (#11095) 2023-11-29 14:35:05 +01:00
Bob van de Vijver
e5ab18ff80 Do not defer eager loading when iterable hint is set 2023-11-23 13:04:13 +01:00
Bob van de Vijver
665ccf1376 Add failing test
This test show that eager collections are broken when used in conjuction
with iterating over a result.
2023-11-23 12:42:38 +01:00
Grégoire Paris
1a4fe6e0bb Merge pull request #11065 from kerbert101/fix-sleep-buhh
AbstractSqlExecutor::__sleep should return property names
2023-11-17 07:25:40 +01:00
Grégoire Paris
0a7b939623 Merge pull request #11039 from yceruto/enum_validation
Adds metadata field type and enumType validation against Entity property type
2023-11-16 23:45:38 +01:00
Albert Bakker
6be65ebc70 fix: return property names in AbstractSqlExecutor::__sleep
Property names as returned by a cast to array are mangled, and that
mangling is not documented. Returning unprefixed produces the same
result, and is more likely to be supported by external tools relying on
the documented possible return values of __sleep.

For instance symfony/var-exporter does not support mangled names, which
leads to issues when caching query parsing results in Symfony
applications.
2023-11-16 19:18:25 +01:00
Alexander M. Turek
e8afa9f80c Prepare 2.17.0 (#11059) 2023-11-16 00:04:41 +01:00
Grégoire Paris
1267f482ef Merge pull request #8391 from beberlei/GH-1569-SubselectFetchMode
[GH-1569] Optimize eager fetch for collections to batch query
2023-11-14 08:16:00 +01:00
Grégoire Paris
b41de2a39d Merge pull request #11056 from derrabus/deprecate/named-queries
Deprecate annotation classes for named queries
2023-11-11 12:30:09 +01:00
Alexander M. Turek
6a48b0741b Deprecate annotation classes for named queries 2023-11-09 16:01:17 +01:00
Grégoire Paris
0e95838787 Merge pull request #11044 from greg0ire/identity-on-dbal4
Recommend SEQUENCE until doctrine/dbal 4 is released
2023-11-07 08:14:32 +01:00
Grégoire Paris
be62f72b38 Merge pull request #11048 from greg0ire/restore-bc
Restore backward compatibility with previous format
2023-11-06 20:08:18 +01:00
Yonel Ceruto
7613f25d57 Adds metadata field type and enumType validation against Entity property type 2023-11-06 08:23:58 -05:00
Grégoire Paris
c075154e1e Restore backward compatibility with previous format
When unserializing from a cache entry in the previous format, the
sqlStatements need to be copied from the legacy property to the new
property before the reference is created.
2023-11-06 07:52:27 +01:00
Grégoire Paris
f08159eb87 Merge pull request #11027 from greg0ire/fw-compat-serialization
Make serialized SQL executors forward compatible
2023-11-05 23:27:48 +01:00
Grégoire Paris
300cffb942 Recommend SEQUENCE until doctrine/dbal 4 is released
Using IDENTITY with doctrine/dbal 3 results in SERIAL, which is not
recommended.
2023-11-05 20:59:24 +01:00
Grégoire Paris
2a9390d426 Make serialized SQL executors forward compatible
The idea here is that instead of having a backward compatibility layer
in the next major branch, we can have a forward compatibility layer in
this branch.
2023-11-05 19:56:16 +01:00
Benjamin Eberlei
ec74c83845 Fix typos 2023-11-05 19:26:35 +01:00
Benjamin Eberlei
4b2b4860fb Housekeeping: Revert change to AbstractExporter, not needed without subselect fetch. 2023-10-22 20:11:36 +02:00
Benjamin Eberlei
609e10df36 Address review comments. 2023-10-22 20:08:18 +02:00
Benjamin Eberlei
d03aed1265 Explain internals of eager loading in a bit more detail and how its configured. 2023-10-22 20:08:06 +02:00
Benjamin Eberlei
6993ad28ed 1:1 and M:1 associations also use fetch batch size configuration now. 2023-10-22 20:07:04 +02:00
Grégoire Paris
16028e4fd3 Merge pull request #11023 from doctrine/2.16.x
Merge 2.16.x up into 2.17.x
2023-10-21 19:52:04 +02:00
Grégoire Paris
609647a51a Merge pull request #11015 from greg0ire/phase2-optim
Make phpdoc accurate
2023-10-21 19:35:18 +02:00
Grégoire Paris
293299a314 Make phpdoc accurate
When transforming these phpdoc types into native types, things break
down. They are correct according to the EBNF, but in practice, there are
so-called phase 2 optimizations that allow using ConditionalPrimary,
ConditionalFactor and ConditionalTerm instances in places where
ConditionalExpression is used.
2023-10-21 19:15:03 +02:00
Grégoire Paris
0b7fe1862e Merge pull request #11018 from stof/fix_result_set_builder_enum
Fix the support for enum types in the ResultSetMappingBuilder
2023-10-18 22:27:01 +02:00
Christophe Coevoet
866283d1a7 Fix the support for enum types in the ResultSetMappingBuilder 2023-10-18 10:04:20 +02:00
Grégoire Paris
3676e3c571 Merge pull request #11007 from greg0ire/refresh-archi-docs
Address split of doctrine/common
2023-10-17 21:41:52 +02:00
Grégoire Paris
d84f607487 Address split of doctrine/common
doctrine/common has been split in several packages. A lot of what was
true about doctrine/common is true about doctrine/persistence today, so
let us simply reuse the existing paragraphs and mention persistence
instead of common.
2023-10-17 20:02:34 +02:00
Grégoire Paris
edd962e385 Merge pull request #11000 from greg0ire/copy-debug
Copy Debug class from doctrine/common
2023-10-14 23:00:13 +02:00
Grégoire Paris
a33a3813b2 Copy Debug class from doctrine/common
This reduces our dependency to this shared library that now holds very
little code we use.
The class has not been copied verbatim:
- Unused parameters and methods have been removed.
- The class is final and internal.
- Coding standards have been enforced, including enabling strict_types,
  which lead to casting a variable to string before feeding it to
  explode().
- A bug found by static analysis has been addressed, where an INI
  setting obtained with ini_get() was compared with true, which is never
  returned by that function.
- Tests are improved to run on all PHP versions
2023-10-14 21:21:51 +02:00
Grégoire Paris
bf69d0ac4e Implement proxy name resolver (#11009)
It is important to have the same implementation as used in
doctrine/persistence without relying on copy/paste.
2023-10-14 19:48:11 +02:00
Benjamin Eberlei
3f2fa309d4 Add another testcase for DQL based fetch eager of collection. 2023-10-14 15:56:42 +02:00
Benjamin Eberlei
8057b51f85 last violation hopefully 2023-10-14 15:38:26 +02:00
Benjamin Eberlei
c09660ac50 Merge remote-tracking branch 'origin/2.17.x' into GH-1569-SubselectFetchMode 2023-10-14 15:29:11 +02:00
Benjamin Eberlei
8ec599bb17 Static analysis 2023-10-14 15:28:57 +02:00
Benjamin Eberlei
bf74b434b8 Housekeeping: phpcs 2023-10-14 14:23:20 +02:00
Benjamin Eberlei
cd54c56278 Directly load many to many collections, batching not supported yet. fix tests. 2023-10-14 14:21:15 +02:00
Benjamin Eberlei
76fd34f766 Avoid new fetch mode, use this strategy with fetch=EAGER for collections. 2023-10-14 14:04:30 +02:00
David Buchmann
1cec0b82bd Remove useless check (#11006) 2023-10-13 18:57:12 +02:00
Grégoire Paris
0e74a180c4 Merge pull request #10999 from greg0ire/prepare-common-severance 2023-10-13 11:25:16 +02:00
Grégoire Paris
fdfca0f0e7 Undeprecate Autoloader class
We plan to sunset doctrine/common, and should move the Autoloader class
to doctrine/orm
2023-10-13 09:02:36 +02:00
Alexander M. Turek
42af7cabb7 Cover calling AbstractQuery::setParameter() with an array parameter (#10996) 2023-10-11 16:04:47 +02:00
Grégoire Paris
c5137da90e Merge pull request #8931 from greg0ire/gh-8893 2023-10-11 10:38:15 +02:00
Grégoire Paris
e89b680a28 Deprecate reliance on non-optimal defaults
What was optimal 10 years ago no longer is, and things might change in
the future. Using AUTO is still the best solution in most cases, and it
should be easy to make it mean something else when it is not.
2023-10-11 10:19:39 +02:00
Grégoire Paris
07b0917505 Merge pull request #10989 from greg0ire/improve-exceptions
Add method name in exception
2023-10-10 21:22:05 +02:00
Alexander M. Turek
143ee25697 Allow creating mocks of the Query class (#10990) 2023-10-10 17:16:01 +02:00
Grégoire Paris
52853c2e9c Merge pull request #10988 from greg0ire/add-missing-annotation 2023-10-10 16:42:50 +02:00
Benjamin Eberlei
40bfe07172 Make sure to many assocatinos are also respecting AbstractQuery::setFetchMode 2023-10-10 16:29:00 +02:00
Grégoire Paris
6983f48490 Merge pull request #10987 from greg0ire/deprecate-partialreference 2023-10-10 15:32:11 +02:00
Grégoire Paris
194f5062bb Add method name in exception
When we assert a given exception should be thrown, and get this instead,
it is hard to figure out what went wrong.
2023-10-10 15:19:16 +02:00
Grégoire Paris
922365d2c5 Add missing "deprecated" annotation on the annotation driver 2023-10-10 15:14:11 +02:00
Grégoire Paris
a1e055b608 Deprecate EntityManager*::getPartialReference()
Partial objects have been deprecated, so it makes no sense to still have
this way of getting some.
2023-10-10 14:39:08 +02:00
Alexander M. Turek
0e3489b240 Don't assert that BIGINTs are stored as strings (#10980) 2023-10-10 11:35:09 +02:00
Benjamin Eberlei
ff28ba8080 Disallow use of fetch=SUBSELECT on to-one associations. 2023-10-10 08:25:04 +02:00
Benjamin Eberlei
41410e6be1 Go through Persister API instead of indirectly through repository. 2023-10-10 08:17:25 +02:00
Benjamin Eberlei
b9e55bad4d Introduce configuration option for subselect batch size. 2023-10-10 07:59:29 +02:00
Benjamin Eberlei
47952c3228 Houskeeping: phpcs 2023-10-10 07:51:13 +02:00
Benjamin Eberlei
fdceb82454 Disallow WITH keyword on fetch joined associatiosn via subselect. 2023-10-10 07:49:38 +02:00
Benjamin Eberlei
dc899e26cf [GH-1569] Add new SUBSELECT fetch mode for OneToMany associations.
Co-authored-by: Wouter M. van Vliet <wouter@interpotential.com>
2023-10-10 07:26:17 +02:00
Alexander M. Turek
48edb33b3f Merge branch '2.16.x' into 2.17.x
* 2.16.x:
  Test against php 8.3 (#10963)
  update checkout version to version 4
2023-10-09 17:30:21 +02:00
Grégoire Paris
9c2e49a665 Merge pull request #10970 from goetas/distinct-limit 2023-10-09 16:43:20 +02:00
Grégoire Paris
8ef5c80148 Merge pull request #10974 from beberlei/RemovePartialObjectExpressionUsage 2023-10-09 16:28:45 +02:00
Benjamin Eberlei
083b1f98c1 Housekeeping: phpcs 2023-10-09 15:42:41 +02:00
Matthias Pigulla
3ff67c3e2f Merge pull request #10967 from greg0ire/remove-commit-order-calculator-from-2
Remove CommitOrderCalculator and related classes
2023-10-09 15:37:05 +02:00
Benjamin Eberlei
84a762e12e Adjust tests for new sql generation 2023-10-09 15:35:48 +02:00
Benjamin Eberlei
ef2123bd0f Remove use of PartialObjectExyxpression for LimitSubqueryOutputWalker to prepare for removal. 2023-10-09 15:30:10 +02:00
Asmir Mustafic
55699a9129 document Paginator::HINT_ENABLE_DISTINCT 2023-10-09 14:05:01 +02:00
Asmir Mustafic
68fc3b6458 allow to disable "DISTINCT" added to the sql query by the limit subquery walker 2023-10-09 14:05:01 +02:00
Serhii Petrov
32192c7b01 Test against php 8.3 (#10963) 2023-10-09 12:43:52 +02:00
Grégoire Paris
77843e45f3 Merge pull request #10972 from salehhashemi1992/ci/update-checkout-to-v4
update actions/checkout to v4
2023-10-08 18:44:13 +02:00
salehhashemi1992
1919eea0a9 update checkout version to version 4 2023-10-08 11:04:22 +03:30
Grégoire Paris
925631878f Remove CommitOrderCalculator and related classes
They are unused, and since they are internal, it should be fine to
remove them without a deprecation.
2023-10-07 11:57:26 +02:00
Alexander M. Turek
db2791001c Merge branch '2.16.x' into 2.17.x
* 2.16.x:
  PHPStan 1.10.35, Psalm 5.15.0 (#10958)
  docs: in text, refer to attributes when talking about metadata (#10956)
  Fix bullet list layout (#10951)
  docs[query-builder]: fix rendering of `Doctrine\DBAL\ParameterType::*` (#10945)
  tests[ORMSetupTest]: testCacheNamespaceShouldBeGeneratedForApcu requires enabled apc (#10940)
  docs: use modern named arguments syntax
  Ignore "Unknown directive" error
  Use a stable release
  Remove output directory argument
  tutorials[getting-started]: example fix bug id type definition
  Verify UnitOfWork::HINT_DEFEREAGERLOAD exists and is true
2023-09-29 08:57:56 +02:00
Alexander M. Turek
62ed63bbbe PHPStan 1.10.35, Psalm 5.15.0 (#10958) 2023-09-29 08:56:45 +02:00
Danny van Kooten
081ec2ad26 docs: in text, refer to attributes when talking about metadata (#10956)
Co-authored-by: Danny van Kooten <dannyvankooten@users.noreply.github.com>
2023-09-28 21:49:15 +02:00
Grégoire Paris
633ce41460 Merge pull request #10946 from greg0ire/improved-validation
Adds metadata field type validation against Entity property type
2023-09-23 12:12:18 +02:00
Adrien Crivelli
e9537f4cde Fix bullet list layout (#10951) 2023-09-19 21:01:37 +02:00
DavideBicego
0f67ba2176 Adds metadata field type validation against Entity property type
This avoid situations where you might map a decimal to a float, when it
should really be mapped to a string. This is a big pitfall because in
that situation, the ORM will consider the field changed every time.
2023-09-15 11:19:58 +02:00
Marko Kaznovac
38ad3925e2 docs[query-builder]: fix rendering of Doctrine\DBAL\ParameterType::* (#10945) 2023-09-15 00:17:48 +02:00
Marko Kaznovac
858b01f85e tests[ORMSetupTest]: testCacheNamespaceShouldBeGeneratedForApcu requires enabled apc (#10940) 2023-09-10 22:57:36 +02:00
Grégoire Paris
9f555ea8fb Merge pull request #10933 from kaznovac/patch-2
docs: use modern named arguments syntax
2023-09-08 22:02:11 +02:00
Marko Kaznovac
1f8c02f345 docs: use modern named arguments syntax
use official named arguments syntax in example instead of pre php 8 codestyle for 'named' arguments
2023-09-08 13:47:11 +02:00
Grégoire Paris
d81afdb6e3 Merge pull request #10930 from greg0ire/improve-doc-job
Improve doc job
2023-09-02 23:15:56 +02:00
Grégoire Paris
0628204b43 Ignore "Unknown directive" error
We have a lot of errors about "Unknown directive" that we should make
known when implementing guides for Doctrine, but cannot address by
modifying the docs.

The unknown directives are:

- configuration-block
- toc
- tocheader
- sectionauthor
2023-09-02 19:29:05 +02:00
Grégoire Paris
816ecc6d6b Use a stable release
0.1.0 has been published 3 weeks ago. This means we no longer need to
  use dev stability
2023-09-02 19:02:59 +02:00
Grégoire Paris
f66263d859 Remove output directory argument
It is no actually necessary at all.
2023-09-02 19:01:46 +02:00
Grégoire Paris
8aa5aa2f57 Merge pull request #10929 from kaznovac/patch-1
tutorials[getting-started]: example fix bug id type definition
2023-09-02 18:52:43 +02:00
Marko Kaznovac
96e31a3b30 tutorials[getting-started]: example fix bug id type definition 2023-09-02 17:48:29 +02:00
Grégoire Paris
a60a273423 Merge pull request #10808 from oscmarb/verifiy-hint-defer-eager-load-is-true
Verify UnitOfWork::HINT_DEFEREAGERLOAD exists and is true
2023-09-01 07:52:11 +02:00
Alexander M. Turek
a2d2e173c2 Merge release 2.16.2 into 2.17.x (#10924) 2023-08-28 09:50:37 +02:00
Óscar Martínez
7986fc64dd Verify UnitOfWork::HINT_DEFEREAGERLOAD exists and is true 2023-08-25 09:51:02 +02:00
Alexander M. Turek
aa333e2f1d Support Symfony 7 by adding return types conditionally (#10919) 2023-08-24 10:36:04 +02:00
Alexander M. Turek
b6441b4f26 Merge branch '2.16.x' into 2.17.x
* 2.16.x:
  Use required classes for Lifecycle Callback examples (#10916)
  Add space before backquote (#10918)
  Add back throws annotation to getSingleScalarResult (#10907)
  Fix link on known issues docs (#10904)
2023-08-23 23:47:29 +02:00
Alexander M. Turek
9647d0e2ae Merge release 2.16.1 into 2.17.x (#10896) 2023-08-09 15:17:34 +02:00
Alexander M. Turek
368eb01ac3 Merge 2.16.x into 2.17.x (#10894) 2023-08-09 11:46:12 +02:00
Nicolas Grekas
47cf50bcd5 Add note about not-enabling lazy-ghosts (#10887) 2023-08-07 15:20:38 +02:00
Alexander M. Turek
58df4078fc Merge branch '2.16.x' into 2.17.x
* 2.16.x:
  Turn identity map collisions from exception to deprecation notice (#10878)
  Add possibility to set reportFieldsWhereDeclared to true in ORMSetup (#10865)
  Fix UnitOfWork->originalEntityData is missing not-modified collections after computeChangeSet  (#9301)
  Add an UPGRADE notice about the potential changes in commit order (#10866)
  Update branch metadata (#10862)
2023-08-07 15:17:36 +02:00
Nicolas Grekas
eda1909c75 Deprecate not-enabling lazy-ghosts and decouple from doctrine/common's proxies (#10837) 2023-08-07 14:43:38 +02:00
Alexander M. Turek
ab542e97df Allow symfony/console 7 (#10724) 2023-08-01 14:25:03 +02:00
158 changed files with 4154 additions and 1233 deletions

View File

@@ -11,21 +11,23 @@
"slug": "latest",
"upcoming": true
},
{
"name": "2.18",
"branchName": "2.18.x",
"slug": "2.18",
"upcoming": true
},
{
"name": "2.17",
"branchName": "2.17.x",
"slug": "2.17",
"upcoming": true
"current": true
},
{
"name": "2.16",
"branchName": "2.16.x",
"slug": "2.16",
"current": true,
"aliases": [
"current",
"stable"
]
"maintained": false
},
{
"name": "2.15",

View File

@@ -39,6 +39,7 @@ jobs:
- "8.0"
- "8.1"
- "8.2"
- "8.3"
dbal-version:
- "default"
extension:
@@ -62,7 +63,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"
with:
fetch-depth: 2
@@ -111,6 +112,7 @@ jobs:
matrix:
php-version:
- "8.2"
- "8.3"
dbal-version:
- "default"
- "3@dev"
@@ -143,7 +145,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"
with:
fetch-depth: 2
@@ -183,6 +185,7 @@ jobs:
matrix:
php-version:
- "8.2"
- "8.3"
dbal-version:
- "default"
- "3@dev"
@@ -212,7 +215,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"
with:
fetch-depth: 2
@@ -252,6 +255,7 @@ jobs:
matrix:
php-version:
- "8.2"
- "8.3"
dbal-version:
- "default"
- "3@dev"
@@ -281,7 +285,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"
with:
fetch-depth: 2
@@ -333,7 +337,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"
with:
fetch-depth: 2
@@ -363,7 +367,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"
with:
fetch-depth: 2

View File

@@ -21,7 +21,7 @@ jobs:
steps:
- name: "Checkout code"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
@@ -33,10 +33,7 @@ jobs:
run: "rm composer.json"
- name: "Require phpdocumentor/guides-cli"
run: "composer require --dev phpdocumentor/guides-cli dev-main@dev --no-update"
- name: "Configure minimum stability"
run: "composer config minimum-stability dev"
run: "composer require --dev phpdocumentor/guides-cli --no-update"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
@@ -48,4 +45,4 @@ jobs:
printf '%s\n%s\n\n%s\n' "Dummy title" "===========" "$(cat docs/en/sidebar.rst)" > docs/en/sidebar.rst
- name: "Run guides-cli"
run: "vendor/bin/guides -vvv --no-progress docs/en /tmp/test 2>&1 | ( ! grep WARNING )"
run: "vendor/bin/guides -vvv --no-progress docs/en 2>&1 | grep -v 'Unknown directive' | ( ! grep WARNING )"

View File

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

View File

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

View File

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

View File

@@ -13,6 +13,5 @@ understand the assumptions we make.
- [DBAL Security Page](https://www.doctrine-project.org/projects/doctrine-dbal/en/stable/reference/security.html)
- [ORM Security Page](https://www.doctrine-project.org/projects/doctrine-orm/en/stable/reference/security.html)
If you find a Security bug in Doctrine, please report it on Jira and change the
Security Level to "Security Issues". It will be visible to Doctrine Core
developers and you only.
If you find a Security bug in Doctrine, please follow our
[Security reporting guidelines](https://www.doctrine-project.org/policies/security.html#reporting).

View File

@@ -1,9 +1,67 @@
# Upgrade to 2.17
## Deprecate annotations classes for named queries
The following classes have been deprecated:
* `Doctrine\ORM\Mapping\NamedNativeQueries`
* `Doctrine\ORM\Mapping\NamedNativeQuery`
* `Doctrine\ORM\Mapping\NamedQueries`
* `Doctrine\ORM\Mapping\NamedQuery`
## Deprecate `Doctrine\ORM\Query\Exec\AbstractSqlExecutor::_sqlStatements`
Use `Doctrine\ORM\Query\Exec\AbstractSqlExecutor::sqlStatements` instead.
## Undeprecate `Doctrine\ORM\Proxy\Autoloader`
It will be a full-fledged class, no longer extending
`Doctrine\Common\Proxy\Autoloader` in 3.0.x.
## Deprecated: reliance on the non-optimal defaults that come with the `AUTO` identifier generation strategy
When the `AUTO` identifier generation strategy was introduced, the best
strategy at the time was selected for each database platform.
A lot of time has passed since then, and with ORM 3.0.0 and DBAL 4.0.0, support
for better strategies will be added.
Because of that, it is now deprecated to rely on the historical defaults when
they differ from what we will be recommended in the future.
Instead, you should pick a strategy for each database platform you use, and it
will be used when using `AUTO`. As of now, only PostgreSQL is affected by this.
It is recommended that PostgreSQL users configure their existing and new
applications to use `SEQUENCE` until `doctrine/dbal` 4.0.0 is released:
```php
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\ORM\Configuration;
assert($configuration instanceof Configuration);
$configuration->setIdentityGenerationPreferences([
PostgreSQLPlatform::CLASS => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
]);
```
When DBAL 4 is released, `AUTO` will result in `IDENTITY`, and the above
configuration should be removed to migrate to it.
## Deprecate `EntityManagerInterface::getPartialReference()`
This method does not have a replacement and will be removed in 3.0.
## Deprecate not-enabling lazy-ghosts
Not enabling lazy ghost objects is deprecated. In ORM 3.0, they will be always enabled.
Ensure `Doctrine\ORM\Configuration::setLazyGhostObjectEnabled(true)` is called to enable them.
# Upgrade to 2.16
## Deprecated accepting duplicate IDs in the identity map
For any given entity class and ID value, there should be only one object instance
representing the entity.
representing the entity.
In https://github.com/doctrine/orm/pull/10785, a check was added that will guard this
in the identity map. The most probable cause for violations of this rule are collisions
@@ -26,13 +84,6 @@ avoided.
When using database-provided, auto-incrementing IDs, this may lead to IDs being assigned
to entities in a different order than it was previously the case.
## Deprecated `\Doctrine\ORM\Internal\CommitOrderCalculator` and related classes
With changes made to the commit order computation, the internal classes
`\Doctrine\ORM\Internal\CommitOrderCalculator`, `\Doctrine\ORM\Internal\CommitOrder\Edge`,
`\Doctrine\ORM\Internal\CommitOrder\Vertex` and `\Doctrine\ORM\Internal\CommitOrder\VertexState`
have been deprecated and will be removed in ORM 3.0.
## Deprecated returning post insert IDs from `EntityPersister::executeInserts()`
Persisters implementing `\Doctrine\ORM\Persisters\Entity\EntityPersister` should no longer
@@ -44,12 +95,12 @@ persister call `Doctrine\ORM\UnitOfWork::assignPostInsertId()` instead.
In ORM 3.0, a change will be made regarding how the `AttributeDriver` reports field mappings.
This change is necessary to be able to detect (and reject) some invalid mapping configurations.
To avoid surprises during 2.x upgrades, the new mode is opt-in. It can be activated on the
To avoid surprises during 2.x upgrades, the new mode is opt-in. It can be activated on the
`AttributeDriver` and `AnnotationDriver` by setting the `$reportFieldsWhereDeclared`
constructor parameter to `true`. It will cause `MappingException`s to be thrown when invalid
configurations are detected.
Not enabling the new mode will cause a deprecation notice to be raised. In ORM 3.0,
Not enabling the new mode will cause a deprecation notice to be raised. In ORM 3.0,
only the new mode will be available.
# Upgrade to 2.15
@@ -69,7 +120,7 @@ and will be an error in 3.0.
## Deprecated undeclared entity inheritance
As soon as an entity class inherits from another entity class, inheritance has to
As soon as an entity class inherits from another entity class, inheritance has to
be declared by adding the appropriate configuration for the root entity.
## Deprecated stubs for "concrete table inheritance"

View File

@@ -34,7 +34,7 @@
"doctrine/lexer": "^2",
"doctrine/persistence": "^2.4 || ^3",
"psr/cache": "^1 || ^2 || ^3",
"symfony/console": "^4.2 || ^5.0 || ^6.0",
"symfony/console": "^4.2 || ^5.0 || ^6.0 || ^7.0",
"symfony/polyfill-php72": "^1.23",
"symfony/polyfill-php80": "^1.16"
},
@@ -42,14 +42,14 @@
"doctrine/annotations": "^1.13 || ^2",
"doctrine/coding-standard": "^9.0.2 || ^12.0",
"phpbench/phpbench": "^0.16.10 || ^1.0",
"phpstan/phpstan": "~1.4.10 || 1.10.28",
"phpstan/phpstan": "~1.4.10 || 1.10.35",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6",
"psr/log": "^1 || ^2 || ^3",
"squizlabs/php_codesniffer": "3.7.2",
"symfony/cache": "^4.4 || ^5.4 || ^6.0",
"symfony/var-exporter": "^4.4 || ^5.4 || ^6.2",
"symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0",
"vimeo/psalm": "4.30.0 || 5.14.1"
"symfony/cache": "^4.4 || ^5.4 || ^6.4 || ^7.0",
"symfony/var-exporter": "^4.4 || ^5.4 || ^6.2 || ^7.0",
"symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0",
"vimeo/psalm": "4.30.0 || 5.16.0"
},
"conflict": {
"doctrine/annotations": "<1.13 || >= 3.0"

View File

@@ -36,71 +36,50 @@ Our entities look like:
namespace Bank\Entities;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
#[ORM\Entity]
class Account
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id;
/**
* @ORM\Column(type="string", unique=true)
*/
private string $no;
/**
* @ORM\OneToMany(targetEntity="Entry", mappedBy="account", cascade={"persist"})
*/
private array $entries;
/**
* @ORM\Column(type="integer")
*/
private int $maxCredit = 0;
public function __construct(string $no, int $maxCredit = 0)
{
$this->no = $no;
$this->maxCredit = $maxCredit;
$this->entries = new \Doctrine\Common\Collections\ArrayCollection();
#[ORM\OneToMany(targetEntity: Entry::class, mappedBy: 'account', cascade: ['persist'])]
private Collection $entries;
public function __construct(
#[ORM\Column(type: 'string', unique: true)]
private string $no,
#[ORM\Column(type: 'integer')]
private int $maxCredit = 0,
) {
$this->entries = new ArrayCollection();
}
}
/**
* @ORM\Entity
*/
#[ORM\Entity]
class Entry
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id;
/**
* @ORM\ManyToOne(targetEntity="Account", inversedBy="entries")
*/
private Account $account;
/**
* @ORM\Column(type="integer")
*/
private int $amount;
public function __construct(Account $account, int $amount)
{
$this->account = $account;
$this->amount = $amount;
public function __construct(
#[ORM\ManyToOne(targetEntity: Account::class, inversedBy: 'entries')]
private Account $account,
#[ORM\Column(type: 'integer')]
private int $amount,
) {
// more stuff here, from/to whom, stated reason, execution date and such
}
public function getAmount(): Amount
{
return $this->amount;
@@ -193,9 +172,8 @@ relation with this method:
public function addEntry(int $amount): void
{
$this->assertAcceptEntryAllowed($amount);
$e = new Entry($this, $amount);
$this->entries[] = $e;
$this->entries[] = new Entry($this, $amount);
}
}
@@ -211,20 +189,20 @@ Now look at the following test-code for our entities:
{
public function testAddEntry()
{
$account = new Account("123456", $maxCredit = 200);
$account = new Account("123456", maxCredit: 200);
$this->assertEquals(0, $account->getBalance());
$account->addEntry(500);
$this->assertEquals(500, $account->getBalance());
$account->addEntry(-700);
$this->assertEquals(-200, $account->getBalance());
}
public function testExceedMaxLimit()
{
$account = new Account("123456", $maxCredit = 200);
$account = new Account("123456", maxCredit: 200);
$this->expectException(Exception::class);
$account->addEntry(-1000);
}
@@ -285,22 +263,19 @@ entries collection) we want to add an aggregate field called
<?php
class Account
{
/**
* @ORM\Column(type="integer")
*/
#[ORM\Column(type: 'integer')]
private int $balance = 0;
public function getBalance(): int
{
return $this->balance;
}
public function addEntry(int $amount): void
{
$this->assertAcceptEntryAllowed($amount);
$e = new Entry($this, $amount);
$this->entries[] = $e;
$this->entries[] = new Entry($this, $amount);
$this->balance += $amount;
}
}
@@ -331,13 +306,13 @@ potentially lead to inconsistent state. See this example:
// The Account $accId has a balance of 0 and a max credit limit of 200:
// request 1 account
$account1 = $em->find(Account::class, $accId);
// request 2 account
$account2 = $em->find(Account::class, $accId);
$account1->addEntry(-200);
$account2->addEntry(-200);
// now request 1 and 2 both flush the changes.
The aggregate field ``Account::$balance`` is now -200, however the
@@ -357,10 +332,8 @@ Optimistic locking is as easy as adding a version column:
class Account
{
/**
* @ORM\Column(type="integer")
* @ORM\Version
*/
#[ORM\Column(type: 'integer')]
#[ORM\Version]
private int $version;
}

View File

@@ -47,10 +47,8 @@ A Customer entity
use Acme\CustomerModule\Entity\Customer as BaseCustomer;
use Acme\InvoiceModule\Model\InvoiceSubjectInterface;
/**
* @ORM\Entity
* @ORM\Table(name="customer")
*/
#[ORM\Entity]
#[ORM\Table(name: 'customer')]
class Customer extends BaseCustomer implements InvoiceSubjectInterface
{
// In our example, any methods defined in the InvoiceSubjectInterface
@@ -69,19 +67,12 @@ An Invoice entity
use Doctrine\ORM\Mapping AS ORM;
use Acme\InvoiceModule\Model\InvoiceSubjectInterface;
/**
* Represents an Invoice.
*
* @ORM\Entity
* @ORM\Table(name="invoice")
*/
#[ORM\Entity]
#[ORM\Table(name: 'invoice')]
class Invoice
{
/**
* @ORM\ManyToOne(targetEntity="Acme\InvoiceModule\Model\InvoiceSubjectInterface")
* @var InvoiceSubjectInterface
*/
protected $subject;
#[ORM\ManyToOne(targetEntity: InvoiceSubjectInterface::class)]
protected InvoiceSubjectInterface $subject;
}
An InvoiceSubjectInterface
@@ -127,7 +118,7 @@ the targetEntity resolution will occur reliably:
// Add the ResolveTargetEntityListener
$evm->addEventListener(Doctrine\ORM\Events::loadClassMetadata, $rtel);
$connection = \Doctrine\DBAL\DriverManager::createConnection($connectionOptions, $config, $evm);
$connection = \Doctrine\DBAL\DriverManager::getConnection($connectionOptions, $config, $evm);
$em = new \Doctrine\ORM\EntityManager($connection, $config, $evm);
Final Thoughts

View File

@@ -408,7 +408,7 @@ means that you have to register a special autoloader for these classes:
.. code-block:: php
<?php
use Doctrine\Common\Proxy\Autoloader;
use Doctrine\ORM\Proxy\Autoloader;
$proxyDir = "/path/to/proxies";
$proxyNamespace = "MyProxies";

View File

@@ -24,28 +24,34 @@ performance it is also recommended that you use APC with PHP.
Doctrine ORM Packages
-------------------
Doctrine ORM is divided into three main packages.
Doctrine ORM is divided into four main packages.
- Common
- DBAL (includes Common)
- ORM (includes DBAL+Common)
- `Collections <https://www.doctrine-project.org/projects/doctrine-collections/en/stable/index.html>`_
- `Event Manager <https://www.doctrine-project.org/projects/doctrine-event-manager/en/stable/index.html>`_
- `Persistence <https://www.doctrine-project.org/projects/doctrine-persistence/en/stable/index.html>`_
- `DBAL <https://www.doctrine-project.org/projects/doctrine-dbal/en/stable/index.html>`_
- ORM (depends on DBAL+Persistence+Collections)
This manual mainly covers the ORM package, sometimes touching parts
of the underlying DBAL and Common packages. The Doctrine code base
of the underlying DBAL and Persistence packages. The Doctrine code base
is split in to these packages for a few reasons and they are to...
- ...make things more maintainable and decoupled
- ...allow you to use the code in Doctrine Common without the ORM
or DBAL
- ...allow you to use the code in Doctrine Persistence and Collections
without the ORM or DBAL
- ...allow you to use the DBAL without the ORM
The Common Package
~~~~~~~~~~~~~~~~~~
Collection, Event Manager and Persistence
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The Common package contains highly reusable components that have no
dependencies beyond the package itself (and PHP, of course). The
root namespace of the Common package is ``Doctrine\Common``.
The Collection, Event Manager and Persistence packages contain highly
reusable components that have no dependencies beyond the packages
themselves (and PHP, of course). The root namespace of the Persistence
package is ``Doctrine\Persistence``. The root namespace of the
Collection package is ``Doctrine\Common\Collections``, for historical
reasons. The root namespace of the Event Manager package is just
``Doctrine\Common``, also for historical reasons.
The DBAL Package
~~~~~~~~~~~~~~~~
@@ -199,5 +205,3 @@ typical implementation of the
to keep track of all the things that need to be done the next time
``flush`` is invoked. You usually do not directly interact with a
``UnitOfWork`` but with the ``EntityManager`` instead.

View File

@@ -422,9 +422,11 @@ the field that serves as the identifier with the ``#[Id]`` attribute.
# fields here
In most cases using the automatic generator strategy (``#[GeneratedValue]``) is
what you want. It defaults to the identifier generation mechanism your current
database vendor prefers: AUTO_INCREMENT with MySQL, sequences with PostgreSQL
and Oracle and so on.
what you want, but for backwards-compatibility reasons it might not. It
defaults to the identifier generation mechanism your current database
vendor preferred at the time that strategy was introduced:
``AUTO_INCREMENT`` with MySQL, sequences with PostgreSQL and Oracle and
so on.
.. _identifier-generation-strategies:
@@ -441,17 +443,18 @@ Here is the list of possible generation strategies:
- ``AUTO`` (default): Tells Doctrine to pick the strategy that is
preferred by the used database platform. The preferred strategies
are IDENTITY for MySQL, SQLite, MsSQL and SQL Anywhere and SEQUENCE
for Oracle and PostgreSQL. This strategy provides full portability.
- ``SEQUENCE``: Tells Doctrine to use a database sequence for ID
generation. This strategy does currently not provide full
portability. Sequences are supported by Oracle, PostgreSql and
SQL Anywhere.
are ``IDENTITY`` for MySQL, SQLite, MsSQL and SQL Anywhere and, for
historical reasons, ``SEQUENCE`` for Oracle and PostgreSQL. This
strategy provides full portability.
- ``IDENTITY``: Tells Doctrine to use special identity columns in
the database that generate a value on insertion of a row. This
strategy does currently not provide full portability and is
supported by the following platforms: MySQL/SQLite/SQL Anywhere
(AUTO\_INCREMENT), MSSQL (IDENTITY) and PostgreSQL (SERIAL).
(``AUTO_INCREMENT``), MSSQL (``IDENTITY``) and PostgreSQL (``SERIAL``).
- ``SEQUENCE``: Tells Doctrine to use a database sequence for ID
generation. This strategy does currently not provide full
portability. Sequences are supported by Oracle, PostgreSql and
SQL Anywhere.
- ``UUID`` (deprecated): Tells Doctrine to use the built-in Universally
Unique Identifier generator. This strategy provides full portability.
- ``NONE``: Tells Doctrine that the identifiers are assigned (and

View File

@@ -464,6 +464,11 @@ hierarchies:
$query = $em->createQuery('SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF Doctrine\Tests\Models\Company\CompanyEmployee');
$query = $em->createQuery('SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF ?1');
$query = $em->createQuery('SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u NOT INSTANCE OF ?1');
$query->setParameter(0, $em->getClassMetadata(CompanyEmployee::class));
.. note::
To use a class as parameter, you have to bind its class metadata:
``$query->setParameter(0, $em->getClassMetadata(CompanyEmployee::class);``.
Get all users visible on a given website that have chosen certain gender:
@@ -1381,7 +1386,7 @@ Result Cache API:
$query->setResultCacheDriver(new ApcCache());
$query->useResultCache(true)
->setResultCacheLifeTime($seconds = 3600);
->setResultCacheLifeTime(3600);
$result = $query->getResult(); // cache miss
@@ -1392,7 +1397,7 @@ Result Cache API:
$result = $query->getResult(); // saved in given result cache id.
// or call useResultCache() with all parameters:
$query->useResultCache(true, $seconds = 3600, 'my_query_result');
$query->useResultCache(true, 3600, 'my_query_result');
$result = $query->getResult(); // cache hit!
// Introspection
@@ -1457,7 +1462,7 @@ several methods to interact with it:
- ``Query::setQueryCacheDriver($driver)`` - Allows to set a Cache
instance
- ``Query::setQueryCacheLifeTime($seconds = 3600)`` - Set lifetime
- ``Query::setQueryCacheLifeTime($seconds)`` - Set lifetime
of the query caching.
- ``Query::expireQueryCache($bool)`` - Enforce the expiring of the
query cache if set to true.

View File

@@ -173,6 +173,19 @@ Events Overview
| :ref:`onClear<reference-events-on-clear>` | ``$em->clear()`` | No | `OnClearEventArgs`_ |
+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
.. warning::
Making changes to entities and calling ``EntityManager::flush()`` from within
event handlers dispatched by ``EntityManager::flush()`` itself is strongly
discouraged, and might be deprecated and eventually prevented in the future.
The reason is that it causes re-entrance into ``UnitOfWork::commit()`` while a commit
is currently being processed. The ``UnitOfWork`` was never designed to support this,
and its behavior in this situation is not covered by any tests.
This may lead to entity or collection updates being missed, applied only in parts and
changes being lost at the end of the commit phase.
Naming convention
~~~~~~~~~~~~~~~~~
@@ -457,13 +470,12 @@ prePersist
There are two ways for the ``prePersist`` event to be triggered:
- One is obviously when you call ``EntityManager::persist()``. The
event is also called for all :ref:`cascaded associations<transitive-persistence>`.
- The other is inside the
``flush()`` method when changes to associations are computed and
this association is marked as :ref:`cascade: persist<transitive-persistence>`. Any new entity found
during this operation is also persisted and ``prePersist`` called
on it. This is called :ref:`persistence by reachability<persistence-by-reachability>`.
- One is when you call ``EntityManager::persist()``. The
event is also called for all :ref:`cascaded associations<transitive-persistence>`.
- The other is inside the ``flush()`` method when changes to associations are computed and
this association is marked as :ref:`cascade: persist<transitive-persistence>`. Any new entity found
during this operation is also persisted and ``prePersist`` called
on it. This is called :ref:`persistence by reachability<persistence-by-reachability>`.
In both cases you get passed a ``PrePersistEventArgs`` instance
which has access to the entity and the entity manager.
@@ -700,30 +712,33 @@ Restrictions for this event:
postUpdate, postRemove, postPersist
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
These three post* events are called inside ``EntityManager::flush()``.
These three ``post*`` events are called inside ``EntityManager::flush()``.
Changes in here are not relevant to the persistence in the
database, but you can use these events to alter non-persistable items,
like non-mapped fields, logging or even associated classes that are
not directly mapped by Doctrine.
- The ``postUpdate`` event occurs after the database
update operations to entity data. It is not called for a DQL
``UPDATE`` statement.
update operations to entity data, but before the database transaction
has been committed. It is not called for a DQL ``UPDATE`` statement.
- The ``postPersist`` event occurs for an entity after the entity has
been made persistent. It will be invoked after all database insert
operations for new entities have been performed. Generated primary
key values will be available for all entities at the time this
event is triggered.
operations for new entities have been performed, but before the database
transaction has been committed. Generated primary key values will be
available for all entities at the time this event is triggered.
- The ``postRemove`` event occurs for an entity after the
entity has been deleted. It will be invoked after all database
delete operations for entity rows have been executed. This event is
not called for a DQL ``DELETE`` statement.
delete operations for entity rows have been executed, but before the
database transaction has been committed. This event is not called for
a DQL ``DELETE`` statement.
.. note::
At the time ``postPersist`` is called, there may still be collection and/or
"extra" updates pending. The database may not yet be completely in
sync with the entity states in memory, not even for the new entities.
sync with the entity states in memory, not even for the new entities. Similarly,
also at the time ``postUpdate`` and ``postRemove`` are called, in-memory collections
may still be in a "dirty" state or still contain removed entities.
.. warning::
@@ -732,19 +747,6 @@ not directly mapped by Doctrine.
cascade remove relations. In this case, you should load yourself the proxy in
the associated ``pre*`` event.
.. warning::
Making changes to entities and calling ``EntityManager::flush()`` from within
``post*`` event handlers is strongly discouraged, and might be deprecated and
eventually prevented in the future.
The reason is that it causes re-entrance into ``UnitOfWork::commit()`` while a commit
is currently being processed. The ``UnitOfWork`` was never designed to support this,
and its behavior in this situation is not covered by any tests.
This may lead to entity or collection updates being missed, applied only in parts and
changes being lost at the end of the commit phase.
.. _reference-events-post-load:
postLoad

View File

@@ -1,10 +1,10 @@
Limitations and Known Issues
============================
We try to make using Doctrine2 a very pleasant experience.
We try to make using Doctrine ORM a very pleasant experience.
Therefore we think it is very important to be honest about the
current limitations to our users. Much like every other piece of
software Doctrine2 is not perfect and far from feature complete.
software the ORM is not perfect and far from feature complete.
This section should give you an overview of current limitations of
Doctrine ORM as well as critical known issues that you should know
about.
@@ -175,6 +175,18 @@ due to complexity.
XML mapping configuration probably needs to completely re-configure or otherwise
copy-and-paste configuration for fields used from traits.
Mapping multiple private fields of the same name
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When two classes, say a mapped superclass and an entity inheriting from it,
both contain a ``private`` field of the same name, this will lead to a ``MappingException``.
Since the fields are ``private``, both are technically separate and can contain
different values at the same time. However, the ``ClassMetadata`` configuration used
internally by the ORM currently refers to fields by their name only, without taking the
class containing the field into consideration. This makes it impossible to keep separate
mapping configuration for both fields.
Known Issues
------------

View File

@@ -253,7 +253,7 @@ Calling ``setParameter()`` automatically infers which type you are setting as
value. This works for integers, arrays of strings/integers, DateTime instances
and for managed entities. If you want to set a type explicitly you can call
the third argument to ``setParameter()`` explicitly. It accepts either a DBAL
Doctrine\DBAL\ParameterType::* or a DBAL Type name for conversion.
``Doctrine\DBAL\ParameterType::*`` or a DBAL Type name for conversion.
.. note::

View File

@@ -12,9 +12,8 @@ page only handles Security issues in the ORM.
- `DBAL Security Page <https://www.doctrine-project.org/projects/doctrine-dbal/en/current/reference/security.html>`
If you find a Security bug in Doctrine, please report it on Jira and change the
Security Level to "Security Issues". It will be visible to Doctrine Core
developers and you only.
If you find a Security bug in Doctrine, please follow our
`Security reporting guidelines <https://www.doctrine-project.org/policies/security.html#reporting>`_.
User input and Doctrine ORM
---------------------------

View File

@@ -782,6 +782,23 @@ and these associations are mapped as EAGER, they will automatically
be loaded together with the entity being queried and is thus
immediately available to your application.
Eager Loading can also be configured at runtime through
``AbstractQuery::setFetchMode`` in DQL or Native Queries.
Eager loading for many-to-one and one-to-one associations is using either a
LEFT JOIN or a second query for fetching the related entity eagerly.
Eager loading for many-to-one associations uses a second query to load
the collections for several entities at the same time.
When many-to-many, one-to-one or one-to-many associations are eagerly loaded,
then the global batch size configuration is used to avoid IN(?) queries with
too many arguments. The default batch size is 100 and can be changed with
``Configuration::setEagerFetchBatchSize()``.
For eagerly loaded Many-To-Many associations one query has to be made for each
collection.
By Lazy Loading
~~~~~~~~~~~~~~~

View File

@@ -735,7 +735,7 @@ classes. We'll store them in ``src/Bug.php`` and ``src/User.php``, respectively.
#[ORM\Id]
#[ORM\Column(type: 'integer')]
#[ORM\GeneratedValue]
private int $id;
private int|null $id;
#[ORM\Column(type: 'string')]
private string $description;
@@ -1199,21 +1199,21 @@ which translates the YYYY-mm-dd HH:mm:ss database format
into a PHP DateTime instance and back.
After the field definitions, the two qualified references to the
user entity are defined. They are created by the ``many-to-one``
tag. The class name of the related entity has to be specified with
the ``target-entity`` attribute, which is enough information for
the database mapper to access the foreign-table. Since
user entity are defined. They are created by the ``ManyToOne``
attribute. The class name of the related entity has to be specified with
the ``targetEntity`` parameter, which is enough information for
the database mapper to access the foreign table. Since
``reporter`` and ``engineer`` are on the owning side of a
bi-directional relation, we also have to specify the ``inversed-by``
attribute. They have to point to the field names on the inverse
side of the relationship. We will see in the next example that the ``inversed-by``
attribute has a counterpart ``mapped-by`` which makes that
bi-directional relation, we also have to specify the ``inversedBy``
parameter. They have to point to the field names on the inverse
side of the relationship. We will see in the next example that the ``inversedBy``
parameter has a counterpart ``mappedBy`` which makes that
the inverse side.
The last definition is for the ``Bug#products`` collection. It
holds all products where the specific bug occurs. Again
you have to define the ``target-entity`` and ``field`` attributes
on the ``many-to-many`` tag.
you have to define the ``targetEntity`` and ``field`` parameters
on the ``ManyToMany`` attribute.
Finally, we'll add metadata mappings for the ``User`` entity.
@@ -1336,7 +1336,7 @@ Finally, we'll add metadata mappings for the ``User`` entity.
targetEntity: Bug
mappedBy: engineer
Here are some new things to mention about the ``one-to-many`` tags.
Here are some new things to mention about the ``OneToMany`` attribute.
Remember that we discussed about the inverse and owning side. Now
both reportedBugs and assignedBugs are inverse relations, which
means the join details have already been defined on the owning

View File

@@ -15,7 +15,7 @@ has a very simple API and implements the SPL interfaces ``Countable`` and
->setFirstResult(0)
->setMaxResults(100);
$paginator = new Paginator($query, $fetchJoinCollection = true);
$paginator = new Paginator($query, fetchJoinCollection: true);
$c = count($paginator);
foreach ($paginator as $post) {
@@ -36,10 +36,25 @@ correct result:
This behavior is only necessary if you actually fetch join a to-many
collection. You can disable this behavior by setting the
``$fetchJoinCollection`` flag to ``false``; in that case only 2 instead of the 3 queries
``fetchJoinCollection`` argument to ``false``; in that case only 2 instead of the 3 queries
described are executed. We hope to automate the detection for this in
the future.
.. note::
``$fetchJoinCollection`` flag set to ``true`` might affect results if you use aggregations in your query.
``fetchJoinCollection`` argument set to ``true`` might affect results if you use aggregations in your query.
By using the ``Paginator::HINT_ENABLE_DISTINCT`` you can instruct doctrine that the query to be executed
will not produce "duplicate" rows (only to-one relations are joined), thus the SQL limit will work as expected.
In this way the `DISTINCT` keyword will be omitted and can bring important performance improvements.
.. code-block:: php
<?php
use Doctrine\ORM\Tools\Pagination\Paginator;
$dql = "SELECT u, p FROM User u JOIN u.mainPicture p";
$query = $entityManager->createQuery($dql)
->setHint(Paginator::HINT_ENABLE_DISTINCT, false)
->setFirstResult(0)
->setMaxResults(100);

View File

@@ -148,7 +148,6 @@
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="entity-result" type="orm:entity-result"/>
<xs:element name="column-result" type="orm:column-result"/>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
@@ -226,22 +225,13 @@
<xs:complexType name="mapped-superclass" >
<xs:complexContent>
<xs:extension base="orm:entity">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:anyAttribute namespace="##other"/>
</xs:extension>
<xs:extension base="orm:entity"/>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="embeddable">
<xs:complexContent>
<xs:extension base="orm:entity">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
</xs:extension>
<xs:extension base="orm:entity"/>
</xs:complexContent>
</xs:complexType>
@@ -565,7 +555,6 @@
<xs:choice minOccurs="0" maxOccurs="1">
<xs:element name="join-column" type="orm:join-column"/>
<xs:element name="join-columns" type="orm:join-columns"/>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
@@ -583,7 +572,6 @@
<xs:choice minOccurs="0" maxOccurs="1">
<xs:element name="join-column" type="orm:join-column"/>
<xs:element name="join-columns" type="orm:join-columns"/>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>

View File

@@ -10,7 +10,6 @@ use Doctrine\Common\Cache\Psr6\CacheAdapter;
use Doctrine\Common\Cache\Psr6\DoctrineProvider;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\Result;
use Doctrine\Deprecations\Deprecation;
@@ -20,6 +19,7 @@ use Doctrine\ORM\Cache\QueryCacheKey;
use Doctrine\ORM\Cache\TimestampCacheKey;
use Doctrine\ORM\Internal\Hydration\IterableResult;
use Doctrine\ORM\Mapping\MappingException as ORMMappingException;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Query\Parameter;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Query\ResultSetMapping;
@@ -430,7 +430,7 @@ abstract class AbstractQuery
}
try {
$class = ClassUtils::getClass($value);
$class = DefaultProxyClassNameResolver::getClass($value);
$value = $this->_em->getUnitOfWork()->getSingleIdentifierValue($value);
if ($value === null) {

View File

@@ -4,12 +4,12 @@ declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\Persister\CachedPersister;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\ORMInvalidArgumentException;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\UnitOfWork;
use function is_array;
@@ -293,7 +293,7 @@ class DefaultCache implements Cache
private function toIdentifierArray(ClassMetadata $metadata, $identifier): array
{
if (is_object($identifier)) {
$class = ClassUtils::getClass($identifier);
$class = DefaultProxyClassNameResolver::getClass($identifier);
if ($this->em->getMetadataFactory()->hasMetadataFor($class)) {
$identifier = $this->uow->getSingleIdentifierValue($identifier);

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Query;
use Doctrine\ORM\UnitOfWork;
use Doctrine\ORM\Utility\IdentifierFlattener;
@@ -112,7 +112,7 @@ class DefaultEntityHydrator implements EntityHydrator
}
if (! isset($assoc['id'])) {
$targetClass = ClassUtils::getClass($data[$name]);
$targetClass = DefaultProxyClassNameResolver::getClass($data[$name]);
$targetId = $this->uow->getEntityIdentifier($data[$name]);
$data[$name] = new AssociationCacheEntry($targetClass, $targetId);

View File

@@ -6,7 +6,6 @@ namespace Doctrine\ORM\Cache\Persister\Collection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Cache\CollectionCacheKey;
use Doctrine\ORM\Cache\CollectionHydrator;
@@ -19,6 +18,7 @@ use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\UnitOfWork;
use function array_values;
@@ -148,7 +148,7 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister
}
$class = $this->targetEntity;
$className = ClassUtils::getClass($elements[$index]);
$className = DefaultProxyClassNameResolver::getClass($elements[$index]);
if ($className !== $this->targetEntity->name) {
$class = $this->metadataFactory->getMetadataFor($className);

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace Doctrine\ORM\Cache\Persister\Collection;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\ORM\Cache\Exception\CannotUpdateReadOnlyCollection;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
class ReadOnlyCachedCollectionPersister extends NonStrictReadWriteCachedCollectionPersister
{
@@ -17,7 +17,7 @@ class ReadOnlyCachedCollectionPersister extends NonStrictReadWriteCachedCollecti
{
if ($collection->isDirty() && $collection->getSnapshot()) {
throw CannotUpdateReadOnlyCollection::fromEntityAndField(
ClassUtils::getClass($collection->getOwner()),
DefaultProxyClassNameResolver::getClass($collection->getOwner()),
$this->association['fieldName']
);
}

View File

@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Doctrine\ORM\Cache\Persister\Entity;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\CollectionCacheKey;
use Doctrine\ORM\Cache\EntityCacheKey;
@@ -21,6 +20,7 @@ use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Persisters\Entity\EntityPersister;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\UnitOfWork;
use function array_merge;
@@ -190,7 +190,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
public function storeEntityCache($entity, EntityCacheKey $key)
{
$class = $this->class;
$className = ClassUtils::getClass($entity);
$className = DefaultProxyClassNameResolver::getClass($entity);
if ($className !== $this->class->name) {
$class = $this->metadataFactory->getMetadataFor($className);
@@ -438,7 +438,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
}
$class = $this->class;
$className = ClassUtils::getClass($entity);
$className = DefaultProxyClassNameResolver::getClass($entity);
if ($className !== $this->class->name) {
$class = $this->metadataFactory->getMetadataFor($className);

View File

@@ -4,8 +4,8 @@ declare(strict_types=1);
namespace Doctrine\ORM\Cache\Persister\Entity;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\ORM\Cache\Exception\CannotUpdateReadOnlyEntity;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
/**
* Specific read-only region entity persister
@@ -17,6 +17,6 @@ class ReadOnlyCachedEntityPersister extends NonStrictReadWriteCachedEntityPersis
*/
public function update($entity)
{
throw CannotUpdateReadOnlyEntity::fromEntity(ClassUtils::getClass($entity));
throw CannotUpdateReadOnlyEntity::fromEntity(DefaultProxyClassNameResolver::getClass($entity));
}
}

View File

@@ -13,6 +13,7 @@ use Doctrine\Common\Cache\Cache as CacheDriver;
use Doctrine\Common\Cache\Psr6\CacheAdapter;
use Doctrine\Common\Cache\Psr6\DoctrineProvider;
use Doctrine\Common\Persistence\PersistentObject;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Cache\CacheConfiguration;
use Doctrine\ORM\Cache\Exception\CacheException;
@@ -27,6 +28,7 @@ use Doctrine\ORM\Exception\NotSupported;
use Doctrine\ORM\Exception\ProxyClassesAlwaysRegenerating;
use Doctrine\ORM\Exception\UnknownEntityNamespace;
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\Mapping\DefaultEntityListenerResolver;
use Doctrine\ORM\Mapping\DefaultNamingStrategy;
@@ -68,6 +70,21 @@ class Configuration extends \Doctrine\DBAL\Configuration
/** @var mixed[] */
protected $_attributes = [];
/** @psalm-var array<class-string<AbstractPlatform>, ClassMetadata::GENERATOR_TYPE_*> */
private $identityGenerationPreferences = [];
/** @psalm-param array<class-string<AbstractPlatform>, ClassMetadata::GENERATOR_TYPE_*> $value */
public function setIdentityGenerationPreferences(array $value): void
{
$this->identityGenerationPreferences = $value;
}
/** @psalm-return array<class-string<AbstractPlatform>, ClassMetadata::GENERATOR_TYPE_*> $value */
public function getIdentityGenerationPreferences(): array
{
return $this->identityGenerationPreferences;
}
/**
* Sets the directory where Doctrine generates any necessary proxy class files.
*
@@ -1127,4 +1144,14 @@ class Configuration extends \Doctrine\DBAL\Configuration
{
return $this->_attributes['rejectIdCollisionInIdentityMap'] ?? false;
}
public function setEagerFetchBatchSize(int $batchSize = 100): void
{
$this->_attributes['fetchModeSubselectBatchSize'] = $batchSize;
}
public function getEagerFetchBatchSize(): int
{
return $this->_attributes['fetchModeSubselectBatchSize'] ?? 100;
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Decorator;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\ResultSetMapping;
@@ -170,6 +171,13 @@ abstract class EntityManagerDecorator extends ObjectManagerDecorator implements
*/
public function getPartialReference($entityName, $identifier)
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10987',
'Method %s is deprecated and will be removed in 3.0.',
__METHOD__
);
return $this->wrapped->getPartialReference($entityName, $identifier);
}

View File

@@ -9,7 +9,6 @@ use BadMethodCallException;
use Doctrine\Common\Cache\Psr6\CacheAdapter;
use Doctrine\Common\EventManager;
use Doctrine\Common\Persistence\PersistentObject;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\LockMode;
@@ -24,6 +23,7 @@ use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\Exception\UnrecognizedIdentifierFields;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Proxy\ProxyFactory;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\Query\FilterCollection;
@@ -444,7 +444,7 @@ class EntityManager implements EntityManagerInterface
foreach ($id as $i => $value) {
if (is_object($value)) {
$className = ClassUtils::getClass($value);
$className = DefaultProxyClassNameResolver::getClass($value);
if ($this->metadataFactory->hasMetadataFor($className)) {
$id[$i] = $this->unitOfWork->getSingleIdentifierValue($value);
@@ -571,6 +571,12 @@ class EntityManager implements EntityManagerInterface
*/
public function getPartialReference($entityName, $identifier)
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10987',
'Method %s is deprecated and will be removed in 3.0.',
__METHOD__
);
$class = $this->metadataFactory->getMetadataFor(ltrim($entityName, '\\'));
$entity = $this->unitOfWork->tryGetById($identifier, $class->rootEntityName);
@@ -981,7 +987,7 @@ class EntityManager implements EntityManagerInterface
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/9961',
'%s() is deprecated. To boostrap a DBAL connection, call %s::getConnection() instead. Use the constructor to create an instance of %s.',
'%s() is deprecated. To bootstrap a DBAL connection, call %s::getConnection() instead. Use the constructor to create an instance of %s.',
__METHOD__,
DriverManager::class,
self::class

View File

@@ -200,6 +200,8 @@ interface EntityManagerInterface extends ObjectManager
* never be visible to the application (especially not event listeners) as it will
* never be loaded in the first place.
*
* @deprecated 2.7 This method is being removed from the ORM and won't have any replacement
*
* @param string $entityName The name of the entity type.
* @param mixed $identifier The entity identifier.
* @psalm-param class-string<T> $entityName

View File

@@ -1,46 +0,0 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Internal\CommitOrder;
use Doctrine\Deprecations\Deprecation;
/**
* @internal
* @deprecated
*/
final class Edge
{
/**
* @var string
* @readonly
*/
public $from;
/**
* @var string
* @readonly
*/
public $to;
/**
* @var int
* @readonly
*/
public $weight;
public function __construct(string $from, string $to, int $weight)
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10547',
'The %s class is deprecated and will be removed in ORM 3.0',
self::class
);
$this->from = $from;
$this->to = $to;
$this->weight = $weight;
}
}

View File

@@ -1,49 +0,0 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Internal\CommitOrder;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Mapping\ClassMetadata;
/**
* @internal
* @deprecated
*/
final class Vertex
{
/**
* @var string
* @readonly
*/
public $hash;
/**
* @var int
* @psalm-var VertexState::*
*/
public $state = VertexState::NOT_VISITED;
/**
* @var ClassMetadata
* @readonly
*/
public $value;
/** @var array<string, Edge> */
public $dependencyList = [];
public function __construct(string $hash, ClassMetadata $value)
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10547',
'The %s class is deprecated and will be removed in ORM 3.0',
self::class
);
$this->hash = $hash;
$this->value = $value;
}
}

View File

@@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Internal\CommitOrder;
use Doctrine\Deprecations\Deprecation;
/**
* @internal
* @deprecated
*/
final class VertexState
{
public const NOT_VISITED = 0;
public const IN_PROGRESS = 1;
public const VISITED = 2;
private function __construct()
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10547',
'The %s class is deprecated and will be removed in ORM 3.0',
self::class
);
}
}

View File

@@ -1,177 +0,0 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Internal;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Internal\CommitOrder\Edge;
use Doctrine\ORM\Internal\CommitOrder\Vertex;
use Doctrine\ORM\Internal\CommitOrder\VertexState;
use Doctrine\ORM\Mapping\ClassMetadata;
use function array_reverse;
/**
* CommitOrderCalculator implements topological sorting, which is an ordering
* algorithm for directed graphs (DG) and/or directed acyclic graphs (DAG) by
* using a depth-first searching (DFS) to traverse the graph built in memory.
* This algorithm have a linear running time based on nodes (V) and dependency
* between the nodes (E), resulting in a computational complexity of O(V + E).
*
* @deprecated
*/
class CommitOrderCalculator
{
/** @deprecated */
public const NOT_VISITED = VertexState::NOT_VISITED;
/** @deprecated */
public const IN_PROGRESS = VertexState::IN_PROGRESS;
/** @deprecated */
public const VISITED = VertexState::VISITED;
/**
* Matrix of nodes (aka. vertex).
*
* Keys are provided hashes and values are the node definition objects.
*
* @var array<string, Vertex>
*/
private $nodeList = [];
/**
* Volatile variable holding calculated nodes during sorting process.
*
* @psalm-var list<ClassMetadata>
*/
private $sortedNodeList = [];
public function __construct()
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10547',
'The %s class is deprecated and will be removed in ORM 3.0',
self::class
);
}
/**
* Checks for node (vertex) existence in graph.
*
* @param string $hash
*
* @return bool
*/
public function hasNode($hash)
{
return isset($this->nodeList[$hash]);
}
/**
* Adds a new node (vertex) to the graph, assigning its hash and value.
*
* @param string $hash
* @param ClassMetadata $node
*
* @return void
*/
public function addNode($hash, $node)
{
$this->nodeList[$hash] = new Vertex($hash, $node);
}
/**
* Adds a new dependency (edge) to the graph using their hashes.
*
* @param string $fromHash
* @param string $toHash
* @param int $weight
*
* @return void
*/
public function addDependency($fromHash, $toHash, $weight)
{
$this->nodeList[$fromHash]->dependencyList[$toHash]
= new Edge($fromHash, $toHash, $weight);
}
/**
* Return a valid order list of all current nodes.
* The desired topological sorting is the reverse post order of these searches.
*
* {@internal Highly performance-sensitive method.}
*
* @psalm-return list<ClassMetadata>
*/
public function sort()
{
foreach ($this->nodeList as $vertex) {
if ($vertex->state !== VertexState::NOT_VISITED) {
continue;
}
$this->visit($vertex);
}
$sortedList = $this->sortedNodeList;
$this->nodeList = [];
$this->sortedNodeList = [];
return array_reverse($sortedList);
}
/**
* Visit a given node definition for reordering.
*
* {@internal Highly performance-sensitive method.}
*/
private function visit(Vertex $vertex): void
{
$vertex->state = VertexState::IN_PROGRESS;
foreach ($vertex->dependencyList as $edge) {
$adjacentVertex = $this->nodeList[$edge->to];
switch ($adjacentVertex->state) {
case VertexState::VISITED:
// Do nothing, since node was already visited
break;
case VertexState::IN_PROGRESS:
if (
isset($adjacentVertex->dependencyList[$vertex->hash]) &&
$adjacentVertex->dependencyList[$vertex->hash]->weight < $edge->weight
) {
// If we have some non-visited dependencies in the in-progress dependency, we
// need to visit them before adding the node.
foreach ($adjacentVertex->dependencyList as $adjacentEdge) {
$adjacentEdgeVertex = $this->nodeList[$adjacentEdge->to];
if ($adjacentEdgeVertex->state === VertexState::NOT_VISITED) {
$this->visit($adjacentEdgeVertex);
}
}
$adjacentVertex->state = VertexState::VISITED;
$this->sortedNodeList[] = $adjacentVertex->value;
}
break;
case VertexState::NOT_VISITED:
$this->visit($adjacentVertex);
}
}
if ($vertex->state !== VertexState::VISITED) {
$vertex->state = VertexState::VISITED;
$this->sortedNodeList[] = $vertex->value;
}
}
}

View File

@@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Internal;
use InvalidArgumentException;
use function array_keys;
use function array_pop;
use function array_push;
use function min;
use function spl_object_id;
/**
* StronglyConnectedComponents implements Tarjan's algorithm to find strongly connected
* components (SCC) in a directed graph. This algorithm has a linear running time based on
* nodes (V) and edges between the nodes (E), resulting in a computational complexity
* of O(V + E).
*
* See https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
* for an explanation and the meaning of the DFS and lowlink numbers.
*
* @internal
*/
final class StronglyConnectedComponents
{
private const NOT_VISITED = 1;
private const IN_PROGRESS = 2;
private const VISITED = 3;
/**
* Array of all nodes, indexed by object ids.
*
* @var array<int, object>
*/
private $nodes = [];
/**
* DFS state for the different nodes, indexed by node object id and using one of
* this class' constants as value.
*
* @var array<int, self::*>
*/
private $states = [];
/**
* Edges between the nodes. The first-level key is the object id of the outgoing
* node; the second array maps the destination node by object id as key.
*
* @var array<int, array<int, bool>>
*/
private $edges = [];
/**
* DFS numbers, by object ID
*
* @var array<int, int>
*/
private $dfs = [];
/**
* lowlink numbers, by object ID
*
* @var array<int, int>
*/
private $lowlink = [];
/** @var int */
private $maxdfs = 0;
/**
* Nodes representing the SCC another node is in, indexed by lookup-node object ID
*
* @var array<int, object>
*/
private $representingNodes = [];
/**
* Stack with OIDs of nodes visited in the current state of the DFS
*
* @var list<int>
*/
private $stack = [];
/** @param object $node */
public function addNode($node): void
{
$id = spl_object_id($node);
$this->nodes[$id] = $node;
$this->states[$id] = self::NOT_VISITED;
$this->edges[$id] = [];
}
/** @param object $node */
public function hasNode($node): bool
{
return isset($this->nodes[spl_object_id($node)]);
}
/**
* Adds a new edge between two nodes to the graph
*
* @param object $from
* @param object $to
*/
public function addEdge($from, $to): void
{
$fromId = spl_object_id($from);
$toId = spl_object_id($to);
$this->edges[$fromId][$toId] = true;
}
public function findStronglyConnectedComponents(): void
{
foreach (array_keys($this->nodes) as $oid) {
if ($this->states[$oid] === self::NOT_VISITED) {
$this->tarjan($oid);
}
}
}
private function tarjan(int $oid): void
{
$this->dfs[$oid] = $this->lowlink[$oid] = $this->maxdfs++;
$this->states[$oid] = self::IN_PROGRESS;
array_push($this->stack, $oid);
foreach ($this->edges[$oid] as $adjacentId => $ignored) {
if ($this->states[$adjacentId] === self::NOT_VISITED) {
$this->tarjan($adjacentId);
$this->lowlink[$oid] = min($this->lowlink[$oid], $this->lowlink[$adjacentId]);
} elseif ($this->states[$adjacentId] === self::IN_PROGRESS) {
$this->lowlink[$oid] = min($this->lowlink[$oid], $this->dfs[$adjacentId]);
}
}
$lowlink = $this->lowlink[$oid];
if ($lowlink === $this->dfs[$oid]) {
$representingNode = null;
do {
$unwindOid = array_pop($this->stack);
if (! $representingNode) {
$representingNode = $this->nodes[$unwindOid];
}
$this->representingNodes[$unwindOid] = $representingNode;
$this->states[$unwindOid] = self::VISITED;
} while ($unwindOid !== $oid);
}
}
/**
* @param object $node
*
* @return object
*/
public function getNodeRepresentingStronglyConnectedComponent($node)
{
$oid = spl_object_id($node);
if (! isset($this->representingNodes[$oid])) {
throw new InvalidArgumentException('unknown node');
}
return $this->representingNodes[$oid];
}
}

View File

@@ -7,8 +7,6 @@ namespace Doctrine\ORM\Internal;
use Doctrine\ORM\Internal\TopologicalSort\CycleDetectedException;
use function array_keys;
use function array_reverse;
use function array_unshift;
use function spl_object_id;
/**
@@ -93,18 +91,14 @@ final class TopologicalSort
/**
* Returns a topological sort of all nodes. When we have an edge A->B between two nodes
* A and B, then A will be listed before B in the result.
* A and B, then B will be listed before A in the result. Visually speaking, when ordering
* the nodes in the result order from left to right, all edges point to the left.
*
* @return list<object>
*/
public function sort(): array
{
/*
* When possible, keep objects in the result in the same order in which they were added as nodes.
* Since nodes are unshifted into $this->>sortResult (see the visit() method), that means we
* need to work them in array_reverse order here.
*/
foreach (array_reverse(array_keys($this->nodes)) as $oid) {
foreach (array_keys($this->nodes) as $oid) {
if ($this->states[$oid] === self::NOT_VISITED) {
$this->visit($oid);
}
@@ -147,7 +141,7 @@ final class TopologicalSort
}
// We have found a cycle and cannot break it at $edge. Best we can do
// is to retreat from the current vertex, hoping that somewhere up the
// is to backtrack from the current vertex, hoping that somewhere up the
// stack this can be salvaged.
$this->states[$oid] = self::NOT_VISITED;
$exception->addToCycle($this->nodes[$oid]);
@@ -160,6 +154,6 @@ final class TopologicalSort
// So we're done with this vertex as well.
$this->states[$oid] = self::VISITED;
array_unshift($this->sortResult, $this->nodes[$oid]);
$this->sortResult[] = $this->nodes[$oid];
}
}

View File

@@ -24,6 +24,7 @@ use Doctrine\ORM\Id\UuidGenerator;
use Doctrine\ORM\Mapping\Exception\CannotGenerateIds;
use Doctrine\ORM\Mapping\Exception\InvalidCustomGenerator;
use Doctrine\ORM\Mapping\Exception\UnknownGeneratorType;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory;
use Doctrine\Persistence\Mapping\ClassMetadata as ClassMetadataInterface;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
@@ -38,6 +39,7 @@ use function end;
use function explode;
use function get_class;
use function in_array;
use function is_a;
use function is_subclass_of;
use function str_contains;
use function strlen;
@@ -71,9 +73,17 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
/** @var mixed[] */
private $embeddablesActiveNesting = [];
private const NON_IDENTITY_DEFAULT_STRATEGY = [
'Doctrine\DBAL\Platforms\PostgreSqlPlatform' => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
Platforms\OraclePlatform::class => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
Platforms\PostgreSQLPlatform::class => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
];
/** @return void */
public function setEntityManager(EntityManagerInterface $em)
{
parent::setProxyClassNameResolver(new DefaultProxyClassNameResolver());
$this->em = $em;
}
@@ -113,7 +123,7 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
if ($parent) {
$class->setInheritanceType($parent->inheritanceType);
$class->setDiscriminatorColumn($parent->discriminatorColumn);
$this->inheritIdGeneratorMapping($class, $parent);
$class->setIdGeneratorType($parent->generatorType);
$this->addInheritedFields($class, $parent);
$this->addInheritedRelations($class, $parent);
$this->addInheritedEmbeddedClasses($class, $parent);
@@ -141,8 +151,12 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
throw MappingException::reflectionFailure($class->getName(), $e);
}
// Complete id generator mapping when the generator was declared/added in this class
if ($class->identifier && (! $parent || ! $parent->identifier)) {
// If this class has a parent the id generator strategy is inherited.
// However this is only true if the hierarchy of parents contains the root entity,
// if it consists of mapped superclasses these don't necessarily include the id field.
if ($parent && $rootEntityFound) {
$this->inheritIdGeneratorMapping($class, $parent);
} else {
$this->completeIdGeneratorMapping($class);
}
@@ -630,7 +644,7 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
'https://github.com/doctrine/orm/issues/8850',
<<<'DEPRECATION'
Context: Loading metadata for class %s
Problem: Using the IDENTITY generator strategy with platform "%s" is deprecated and will not be possible in Doctrine ORM 3.0.
Problem: Using identity columns emulated with a sequence is deprecated and will not be possible in Doctrine ORM 3.0.
Solution: Use the SEQUENCE generator strategy instead.
DEPRECATION
,
@@ -725,14 +739,40 @@ DEPRECATION
}
}
/** @psalm-return ClassMetadata::GENERATOR_TYPE_SEQUENCE|ClassMetadata::GENERATOR_TYPE_IDENTITY */
/** @psalm-return ClassMetadata::GENERATOR_TYPE_* */
private function determineIdGeneratorStrategy(AbstractPlatform $platform): int
{
if (
$platform instanceof Platforms\OraclePlatform
|| $platform instanceof Platforms\PostgreSQLPlatform
) {
return ClassMetadata::GENERATOR_TYPE_SEQUENCE;
assert($this->em !== null);
foreach ($this->em->getConfiguration()->getIdentityGenerationPreferences() as $platformFamily => $strategy) {
if (is_a($platform, $platformFamily)) {
return $strategy;
}
}
foreach (self::NON_IDENTITY_DEFAULT_STRATEGY as $platformFamily => $strategy) {
if (is_a($platform, $platformFamily)) {
if ($platform instanceof Platforms\PostgreSQLPlatform || is_a($platform, 'Doctrine\DBAL\Platforms\PostgreSqlPlatform')) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/8893',
<<<'DEPRECATION'
Relying on non-optimal defaults for ID generation is deprecated, and IDENTITY
results in SERIAL, which is not recommended.
Instead, configure identifier generation strategies explicitly through
configuration.
We currently recommend "SEQUENCE" for "%s", so you should use
$configuration->setIdentityGenerationPreferences([
"%s" => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
]);
DEPRECATION
,
$platformFamily,
$platformFamily
);
}
return $strategy;
}
}
if ($platform->supportsIdentityColumns()) {

View File

@@ -2558,6 +2558,10 @@ class ClassMetadataInfo implements ClassMetadata
$overrideMapping['id'] = $mapping['id'];
}
if (isset($mapping['declared'])) {
$overrideMapping['declared'] = $mapping['declared'];
}
if (! isset($overrideMapping['type'])) {
$overrideMapping['type'] = $mapping['type'];
}

View File

@@ -57,7 +57,15 @@ final class DefaultTypedFieldMapper implements TypedFieldMapper
$mapping['enumType'] = $type->getName();
$reflection = new ReflectionEnum($type->getName());
$type = $reflection->getBackingType();
if (! $reflection->isBacked()) {
throw MappingException::backedEnumTypeRequired(
$field->class,
$mapping['fieldName'],
$mapping['enumType']
);
}
$type = $reflection->getBackingType();
assert($type instanceof ReflectionNamedType);
}

View File

@@ -30,6 +30,8 @@ use function is_numeric;
/**
* The AnnotationDriver reads the mapping metadata from docblock annotations.
*
* @deprecated This class will be removed in 3.0 without replacement.
*/
class AnnotationDriver extends CompatibilityAnnotationDriver
{

View File

@@ -69,8 +69,7 @@ final class AttributeReader
));
}
return $this->getPropertyAttributes($property)[$attributeName]
?? ($this->isRepeatable($attributeName) ? new RepeatableAttributeCollection() : null);
return $this->getPropertyAttributes($property)[$attributeName] ?? null;
}
/**

View File

@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Driver;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\MappingException;
@@ -56,15 +55,6 @@ class XmlDriver extends FileDriver
);
}
if (! $isXsdValidationEnabled) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/6728',
'Using XML mapping driver with XSD validation disabled is deprecated'
. ' and will not be supported in Doctrine ORM 3.0.'
);
}
if ($isXsdValidationEnabled && ! extension_loaded('dom')) {
throw new LogicException(
'XSD validation cannot be enabled because the DOM extension is missing.'
@@ -128,7 +118,7 @@ class XmlDriver extends FileDriver
// Evaluate named queries
if (isset($xmlRoot->{'named-queries'})) {
foreach ($xmlRoot->{'named-queries'}->{'named-query'} as $namedQueryElement) {
foreach ($xmlRoot->{'named-queries'}->{'named-query'} ?? [] as $namedQueryElement) {
$metadata->addNamedQuery(
[
'name' => (string) $namedQueryElement['name'],
@@ -140,7 +130,7 @@ class XmlDriver extends FileDriver
// Evaluate native named queries
if (isset($xmlRoot->{'named-native-queries'})) {
foreach ($xmlRoot->{'named-native-queries'}->{'named-native-query'} as $nativeQueryElement) {
foreach ($xmlRoot->{'named-native-queries'}->{'named-native-query'} ?? [] as $nativeQueryElement) {
$metadata->addNamedNativeQuery(
[
'name' => isset($nativeQueryElement['name']) ? (string) $nativeQueryElement['name'] : null,
@@ -154,7 +144,7 @@ class XmlDriver extends FileDriver
// Evaluate sql result set mapping
if (isset($xmlRoot->{'sql-result-set-mappings'})) {
foreach ($xmlRoot->{'sql-result-set-mappings'}->{'sql-result-set-mapping'} as $rsmElement) {
foreach ($xmlRoot->{'sql-result-set-mappings'}->{'sql-result-set-mapping'} ?? [] as $rsmElement) {
$entities = [];
$columns = [];
foreach ($rsmElement as $entityElement) {
@@ -240,7 +230,7 @@ class XmlDriver extends FileDriver
// Evaluate <indexes...>
if (isset($xmlRoot->indexes)) {
$metadata->table['indexes'] = [];
foreach ($xmlRoot->indexes->index as $indexXml) {
foreach ($xmlRoot->indexes->index ?? [] as $indexXml) {
$index = [];
if (isset($indexXml['columns']) && ! empty($indexXml['columns'])) {
@@ -283,7 +273,7 @@ class XmlDriver extends FileDriver
// Evaluate <unique-constraints..>
if (isset($xmlRoot->{'unique-constraints'})) {
$metadata->table['uniqueConstraints'] = [];
foreach ($xmlRoot->{'unique-constraints'}->{'unique-constraint'} as $uniqueXml) {
foreach ($xmlRoot->{'unique-constraints'}->{'unique-constraint'} ?? [] as $uniqueXml) {
$unique = [];
if (isset($uniqueXml['columns']) && ! empty($uniqueXml['columns'])) {
@@ -370,7 +360,7 @@ class XmlDriver extends FileDriver
// Evaluate <id ...> mappings
$associationIds = [];
foreach ($xmlRoot->id as $idElement) {
foreach ($xmlRoot->id ?? [] as $idElement) {
if (isset($idElement['association-key']) && $this->evaluateBoolean($idElement['association-key'])) {
$associationIds[(string) $idElement['name']] = true;
continue;
@@ -439,7 +429,7 @@ class XmlDriver extends FileDriver
if (isset($oneToOneElement->{'join-column'})) {
$joinColumns[] = $this->joinColumnToArray($oneToOneElement->{'join-column'});
} elseif (isset($oneToOneElement->{'join-columns'})) {
foreach ($oneToOneElement->{'join-columns'}->{'join-column'} as $joinColumnElement) {
foreach ($oneToOneElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
$joinColumns[] = $this->joinColumnToArray($joinColumnElement);
}
}
@@ -490,7 +480,7 @@ class XmlDriver extends FileDriver
if (isset($oneToManyElement->{'order-by'})) {
$orderBy = [];
foreach ($oneToManyElement->{'order-by'}->{'order-by-field'} as $orderByField) {
foreach ($oneToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) {
$orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
? (string) $orderByField['direction']
: Criteria::ASC;
@@ -542,7 +532,7 @@ class XmlDriver extends FileDriver
if (isset($manyToOneElement->{'join-column'})) {
$joinColumns[] = $this->joinColumnToArray($manyToOneElement->{'join-column'});
} elseif (isset($manyToOneElement->{'join-columns'})) {
foreach ($manyToOneElement->{'join-columns'}->{'join-column'} as $joinColumnElement) {
foreach ($manyToOneElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
$joinColumns[] = $this->joinColumnToArray($joinColumnElement);
}
}
@@ -601,11 +591,11 @@ class XmlDriver extends FileDriver
$joinTable['options'] = $this->parseOptions($joinTableElement->options->children());
}
foreach ($joinTableElement->{'join-columns'}->{'join-column'} as $joinColumnElement) {
foreach ($joinTableElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
$joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumnElement);
}
foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} as $joinColumnElement) {
foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
$joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumnElement);
}
@@ -618,7 +608,7 @@ class XmlDriver extends FileDriver
if (isset($manyToManyElement->{'order-by'})) {
$orderBy = [];
foreach ($manyToManyElement->{'order-by'}->{'order-by-field'} as $orderByField) {
foreach ($manyToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) {
$orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
? (string) $orderByField['direction']
: Criteria::ASC;
@@ -644,9 +634,9 @@ class XmlDriver extends FileDriver
// Evaluate association-overrides
if (isset($xmlRoot->{'attribute-overrides'})) {
foreach ($xmlRoot->{'attribute-overrides'}->{'attribute-override'} as $overrideElement) {
foreach ($xmlRoot->{'attribute-overrides'}->{'attribute-override'} ?? [] as $overrideElement) {
$fieldName = (string) $overrideElement['name'];
foreach ($overrideElement->field as $field) {
foreach ($overrideElement->field ?? [] as $field) {
$mapping = $this->columnToArray($field);
$mapping['fieldName'] = $fieldName;
$metadata->setAttributeOverride($fieldName, $mapping);
@@ -656,14 +646,14 @@ class XmlDriver extends FileDriver
// Evaluate association-overrides
if (isset($xmlRoot->{'association-overrides'})) {
foreach ($xmlRoot->{'association-overrides'}->{'association-override'} as $overrideElement) {
foreach ($xmlRoot->{'association-overrides'}->{'association-override'} ?? [] as $overrideElement) {
$fieldName = (string) $overrideElement['name'];
$override = [];
// Check for join-columns
if (isset($overrideElement->{'join-columns'})) {
$joinColumns = [];
foreach ($overrideElement->{'join-columns'}->{'join-column'} as $joinColumnElement) {
foreach ($overrideElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
$joinColumns[] = $this->joinColumnToArray($joinColumnElement);
}
@@ -685,13 +675,13 @@ class XmlDriver extends FileDriver
}
if (isset($joinTableElement->{'join-columns'})) {
foreach ($joinTableElement->{'join-columns'}->{'join-column'} as $joinColumnElement) {
foreach ($joinTableElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
$joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumnElement);
}
}
if (isset($joinTableElement->{'inverse-join-columns'})) {
foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} as $joinColumnElement) {
foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
$joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumnElement);
}
}
@@ -715,14 +705,14 @@ class XmlDriver extends FileDriver
// Evaluate <lifecycle-callbacks...>
if (isset($xmlRoot->{'lifecycle-callbacks'})) {
foreach ($xmlRoot->{'lifecycle-callbacks'}->{'lifecycle-callback'} as $lifecycleCallback) {
foreach ($xmlRoot->{'lifecycle-callbacks'}->{'lifecycle-callback'} ?? [] as $lifecycleCallback) {
$metadata->addLifecycleCallback((string) $lifecycleCallback['method'], constant('Doctrine\ORM\Events::' . (string) $lifecycleCallback['type']));
}
}
// Evaluate entity listener
if (isset($xmlRoot->{'entity-listeners'})) {
foreach ($xmlRoot->{'entity-listeners'}->{'entity-listener'} as $listenerElement) {
foreach ($xmlRoot->{'entity-listeners'}->{'entity-listener'} ?? [] as $listenerElement) {
$className = (string) $listenerElement['class'];
// Evaluate the listener using naming convention.
if ($listenerElement->count() === 0) {
@@ -744,16 +734,14 @@ class XmlDriver extends FileDriver
/**
* Parses (nested) option elements.
*
* @param SimpleXMLElement $options The XML element.
*
* @return mixed[] The options array.
* @psalm-return array<int|string, array<int|string, mixed|string>|bool|string>
*/
private function parseOptions(SimpleXMLElement $options): array
private function parseOptions(?SimpleXMLElement $options): array
{
$array = [];
foreach ($options as $option) {
foreach ($options ?? [] as $option) {
if ($option->count()) {
$value = $this->parseOptions($option->children());
} else {
@@ -816,7 +804,7 @@ class XmlDriver extends FileDriver
}
if (isset($joinColumnElement['options'])) {
$joinColumn['options'] = $this->parseOptions($joinColumnElement['options']->children());
$joinColumn['options'] = $this->parseOptions($joinColumnElement['options'] ? $joinColumnElement['options']->children() : null);
}
return $joinColumn;
@@ -972,19 +960,19 @@ class XmlDriver extends FileDriver
if (isset($xmlElement->entity)) {
foreach ($xmlElement->entity as $entityElement) {
/** @psalm-var class-string */
/** @psalm-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 */
/** @psalm-var class-string $className */
$className = (string) $mappedSuperClass['name'];
$result[$className] = $mappedSuperClass;
}
} elseif (isset($xmlElement->embeddable)) {
foreach ($xmlElement->embeddable as $embeddableElement) {
/** @psalm-var class-string */
/** @psalm-var class-string $embeddableName */
$embeddableName = (string) $embeddableElement['name'];
$result[$embeddableName] = $embeddableElement;
}

View File

@@ -954,6 +954,16 @@ class MappingException extends ORMException
return new self(sprintf('Enum types require PHP 8.1 in %s::$%s', $className, $fieldName));
}
public static function backedEnumTypeRequired(string $className, string $fieldName, string $enumType): self
{
return new self(sprintf(
'Attempting to map a non-backed enum type %s in entity %s::$%s. Please use backed enums only',
$enumType,
$className,
$fieldName
));
}
public static function nonEnumTypeMapped(string $className, string $fieldName, string $enumType): self
{
return new self(sprintf(

View File

@@ -8,6 +8,8 @@ namespace Doctrine\ORM\Mapping;
* Is used to specify an array of native SQL named queries.
* The NamedNativeQueries annotation can be applied to an entity or mapped superclass.
*
* @deprecated Named queries won't be supported in ORM 3.
*
* @Annotation
* @Target("CLASS")
*/

View File

@@ -8,6 +8,8 @@ namespace Doctrine\ORM\Mapping;
* Is used to specify a native SQL named query.
* The NamedNativeQuery annotation can be applied to an entity or mapped superclass.
*
* @deprecated Named queries won't be supported in ORM 3.
*
* @Annotation
* @Target("ANNOTATION")
*/

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
/**
* @deprecated Named queries won't be supported in ORM 3.
*
* @Annotation
* @Target("CLASS")
*/

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
/**
* @deprecated Named queries won't be supported in ORM 3.
*
* @Annotation
* @Target("ANNOTATION")
*/

View File

@@ -11,8 +11,10 @@ use function ksort;
/**
* Represents a native SQL query.
*
* @final
*/
final class NativeQuery extends AbstractQuery
class NativeQuery extends AbstractQuery
{
/** @var string */
private $sql;

View File

@@ -15,6 +15,7 @@ use function func_num_args;
use function get_debug_type;
use function gettype;
use function implode;
use function is_scalar;
use function method_exists;
use function reset;
use function spl_object_id;
@@ -261,6 +262,32 @@ EXCEPTION
return new self(sprintf('Entity name must be a string, %s given', get_debug_type($entityName)));
}
/** @param mixed $value */
public static function invalidAutoGenerateMode($value): self
{
return new self(sprintf('Invalid auto generate mode "%s" given.', is_scalar($value) ? (string) $value : get_debug_type($value)));
}
public static function missingPrimaryKeyValue(string $className, string $idField): self
{
return new self(sprintf('Missing value for primary key %s on %s', $idField, $className));
}
public static function proxyDirectoryRequired(): self
{
return new self('You must configure a proxy directory. See docs for details');
}
public static function proxyNamespaceRequired(): self
{
return new self('You must configure a proxy namespace');
}
public static function proxyDirectoryNotWritable(string $proxyDirectory): self
{
return new self(sprintf('Your proxy directory "%s" must be writable', $proxyDirectory));
}
/**
* Helper method to show an object as string.
*

View File

@@ -7,7 +7,6 @@ namespace Doctrine\ORM\Persisters\Entity;
use BackedEnum;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Expr\Comparison;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\LockMode;
use Doctrine\DBAL\Platforms\AbstractPlatform;
@@ -26,6 +25,7 @@ use Doctrine\ORM\Persisters\Exception\InvalidOrientation;
use Doctrine\ORM\Persisters\Exception\UnrecognizedField;
use Doctrine\ORM\Persisters\SqlExpressionVisitor;
use Doctrine\ORM\Persisters\SqlValueVisitor;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Repository\Exception\InvalidFindByCall;
@@ -1264,7 +1264,7 @@ class BasicEntityPersister implements EntityPersister
}
$isAssocToOneInverseSide = $assoc['type'] & ClassMetadata::TO_ONE && ! $assoc['isOwningSide'];
$isAssocFromOneEager = $assoc['type'] !== ClassMetadata::MANY_TO_MANY && $assoc['fetch'] === ClassMetadata::FETCH_EAGER;
$isAssocFromOneEager = $assoc['type'] & ClassMetadata::TO_ONE && $assoc['fetch'] === ClassMetadata::FETCH_EAGER;
if (! ($isAssocFromOneEager || $isAssocToOneInverseSide)) {
continue;
@@ -2028,7 +2028,7 @@ class BasicEntityPersister implements EntityPersister
return [$value->value];
}
$valueClass = ClassUtils::getClass($value);
$valueClass = DefaultProxyClassNameResolver::getClass($value);
if ($this->em->getMetadataFactory()->isTransient($valueClass)) {
return [$value];

View File

@@ -6,7 +6,6 @@ namespace Doctrine\ORM\Proxy;
use Doctrine\Common\Proxy\Autoloader as BaseAutoloader;
/** @deprecated use \Doctrine\Common\Proxy\Autoloader instead */
class Autoloader extends BaseAutoloader
{
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Proxy;
use Doctrine\Persistence\Mapping\ProxyClassNameResolver;
use Doctrine\Persistence\Proxy;
use function get_class;
use function strrpos;
use function substr;
/**
* Class-related functionality for objects that might or not be proxy objects
* at the moment.
*/
final class DefaultProxyClassNameResolver implements ProxyClassNameResolver
{
public function resolveClassName(string $className): string
{
$pos = strrpos($className, '\\' . Proxy::MARKER . '\\');
if ($pos === false) {
return $className;
}
return substr($className, $pos + Proxy::MARKER_LENGTH + 2);
}
/**
* @param object $object
*
* @return class-string
*/
public static function getClass($object): string
{
return (new self())->resolveClassName(get_class($object));
}
}

View File

@@ -9,30 +9,94 @@ use Doctrine\Common\Proxy\AbstractProxyFactory;
use Doctrine\Common\Proxy\Proxy as CommonProxy;
use Doctrine\Common\Proxy\ProxyDefinition;
use Doctrine\Common\Proxy\ProxyGenerator;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityNotFoundException;
use Doctrine\ORM\ORMInvalidArgumentException;
use Doctrine\ORM\Persisters\Entity\EntityPersister;
use Doctrine\ORM\Proxy\Proxy as LegacyProxy;
use Doctrine\ORM\UnitOfWork;
use Doctrine\ORM\Utility\IdentifierFlattener;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\Proxy;
use ReflectionProperty;
use Symfony\Component\VarExporter\ProxyHelper;
use Symfony\Component\VarExporter\VarExporter;
use Throwable;
use function array_combine;
use function array_flip;
use function array_intersect_key;
use function bin2hex;
use function chmod;
use function class_exists;
use function dirname;
use function file_exists;
use function file_put_contents;
use function filemtime;
use function is_bool;
use function is_dir;
use function is_int;
use function is_writable;
use function ltrim;
use function mkdir;
use function preg_match_all;
use function random_bytes;
use function rename;
use function rtrim;
use function str_replace;
use function strpos;
use function strrpos;
use function strtr;
use function substr;
use function uksort;
use function ucfirst;
use const DIRECTORY_SEPARATOR;
use const PHP_VERSION_ID;
/**
* This factory is used to create proxy objects for entities at runtime.
*/
class ProxyFactory extends AbstractProxyFactory
{
/**
* Never autogenerate a proxy and rely that it was generated by some
* process before deployment.
*/
public const AUTOGENERATE_NEVER = 0;
/**
* Always generates a new proxy in every request.
*
* This is only sane during development.
*/
public const AUTOGENERATE_ALWAYS = 1;
/**
* Autogenerate the proxy class when the proxy file does not exist.
*
* This strategy causes a file_exists() call whenever any proxy is used the
* first time in a request.
*/
public const AUTOGENERATE_FILE_NOT_EXISTS = 2;
/**
* Generate the proxy classes using eval().
*
* This strategy is only sane for development, and even then it gives me
* the creeps a little.
*/
public const AUTOGENERATE_EVAL = 3;
/**
* Autogenerate the proxy class when the proxy file does not exist or
* when the proxied file changed.
*
* This strategy causes a file_exists() call whenever any proxy is used the
* first time in a request. When the proxied file is changed, the proxy will
* be updated.
*/
public const AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED = 4;
private const PROXY_CLASS_TEMPLATE = <<<'EOPHP'
<?php
@@ -45,15 +109,6 @@ class <proxyShortClassName> extends \<className> implements \<baseProxyInterface
{
<useLazyGhostTrait>
public function __construct(?\Closure $initializer = null, ?\Closure $cloner = null)
{
if ($cloner !== null) {
return;
}
self::createLazyGhost($initializer, <skippedProperties>, $this);
}
public function __isInitialized(): bool
{
return isset($this->lazyObjectState) && $this->isLazyObjectInitialized();
@@ -73,9 +128,15 @@ EOPHP;
/** @var UnitOfWork The UnitOfWork this factory uses to retrieve persisters */
private $uow;
/** @var string */
private $proxyDir;
/** @var string */
private $proxyNs;
/** @var self::AUTOGENERATE_* */
private $autoGenerate;
/**
* The IdentifierFlattener used for manipulating identifiers
*
@@ -83,8 +144,11 @@ EOPHP;
*/
private $identifierFlattener;
/** @var ProxyDefinition[] */
private $definitions = [];
/** @var array<class-string, Closure> */
private $proxyFactories = [];
/** @var bool */
private $isLazyGhostObjectEnabled = true;
/**
* Initializes a new instance of the <tt>ProxyFactory</tt> class that is
@@ -97,23 +161,40 @@ EOPHP;
*/
public function __construct(EntityManagerInterface $em, $proxyDir, $proxyNs, $autoGenerate = self::AUTOGENERATE_NEVER)
{
$proxyGenerator = new ProxyGenerator($proxyDir, $proxyNs);
if (! $em->getConfiguration()->isLazyGhostObjectEnabled()) {
if (PHP_VERSION_ID >= 80100) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10837/',
'Not enabling lazy ghost objects is deprecated and will not be supported in Doctrine ORM 3.0. Ensure Doctrine\ORM\Configuration::setLazyGhostObjectEnabled(true) is called to enable them.'
);
}
if ($em->getConfiguration()->isLazyGhostObjectEnabled()) {
$proxyGenerator->setPlaceholder('baseProxyInterface', InternalProxy::class);
$proxyGenerator->setPlaceholder('useLazyGhostTrait', Closure::fromCallable([$this, 'generateUseLazyGhostTrait']));
$proxyGenerator->setPlaceholder('skippedProperties', Closure::fromCallable([$this, 'generateSkippedProperties']));
$proxyGenerator->setPlaceholder('serializeImpl', Closure::fromCallable([$this, 'generateSerializeImpl']));
$proxyGenerator->setProxyClassTemplate(self::PROXY_CLASS_TEMPLATE);
} else {
$this->isLazyGhostObjectEnabled = false;
$proxyGenerator = new ProxyGenerator($proxyDir, $proxyNs);
$proxyGenerator->setPlaceholder('baseProxyInterface', LegacyProxy::class);
parent::__construct($proxyGenerator, $em->getMetadataFactory(), $autoGenerate);
}
parent::__construct($proxyGenerator, $em->getMetadataFactory(), $autoGenerate);
if (! $proxyDir) {
throw ORMInvalidArgumentException::proxyDirectoryRequired();
}
if (! $proxyNs) {
throw ORMInvalidArgumentException::proxyNamespaceRequired();
}
if (is_int($autoGenerate) ? $autoGenerate < 0 || $autoGenerate > 4 : ! is_bool($autoGenerate)) {
throw ORMInvalidArgumentException::invalidAutoGenerateMode($autoGenerate);
}
$this->em = $em;
$this->uow = $em->getUnitOfWork();
$this->proxyDir = $proxyDir;
$this->proxyNs = $proxyNs;
$this->autoGenerate = (int) $autoGenerate;
$this->identifierFlattener = new IdentifierFlattener($this->uow, $em->getMetadataFactory());
}
@@ -122,19 +203,57 @@ EOPHP;
*/
public function getProxy($className, array $identifier)
{
$proxy = parent::getProxy($className, $identifier);
if (! $this->em->getConfiguration()->isLazyGhostObjectEnabled()) {
return $proxy;
if (! $this->isLazyGhostObjectEnabled) {
return parent::getProxy($className, $identifier);
}
$initializer = $this->definitions[$className]->initializer;
$proxyFactory = $this->proxyFactories[$className] ?? $this->getProxyFactory($className);
$proxy->__construct(static function (InternalProxy $object) use ($initializer, $proxy): void {
$initializer($object, $proxy);
});
return $proxyFactory($identifier);
}
return $proxy;
/**
* Generates proxy classes for all given classes.
*
* @param ClassMetadata[] $classes The classes (ClassMetadata instances) for which to generate proxies.
* @param string|null $proxyDir The target directory of the proxy classes. If not specified, the
* directory configured on the Configuration of the EntityManager used
* by this factory is used.
*
* @return int Number of generated proxies.
*/
public function generateProxyClasses(array $classes, $proxyDir = null)
{
if (! $this->isLazyGhostObjectEnabled) {
return parent::generateProxyClasses($classes, $proxyDir);
}
$generated = 0;
foreach ($classes as $class) {
if ($this->skipClass($class)) {
continue;
}
$proxyFileName = $this->getProxyFileName($class->getName(), $proxyDir ?: $this->proxyDir);
$proxyClassName = self::generateProxyClassName($class->getName(), $this->proxyNs);
$this->generateProxyClass($class, $proxyFileName, $proxyClassName);
++$generated;
}
return $generated;
}
/**
* {@inheritDoc}
*
* @deprecated ProxyFactory::resetUninitializedProxy() is deprecated and will be removed in version 3.0 of doctrine/orm.
*/
public function resetUninitializedProxy(CommonProxy $proxy)
{
return parent::resetUninitializedProxy($proxy);
}
/**
@@ -149,23 +268,19 @@ EOPHP;
/**
* {@inheritDoc}
*
* @deprecated ProxyFactory::createProxyDefinition() is deprecated and will be removed in version 3.0 of doctrine/orm.
*/
protected function createProxyDefinition($className)
{
$classMetadata = $this->em->getClassMetadata($className);
$entityPersister = $this->uow->getEntityPersister($className);
if ($this->em->getConfiguration()->isLazyGhostObjectEnabled()) {
$initializer = $this->createLazyInitializer($classMetadata, $entityPersister);
$cloner = static function (): void {
};
} else {
$initializer = $this->createInitializer($classMetadata, $entityPersister);
$cloner = $this->createCloner($classMetadata, $entityPersister);
}
$initializer = $this->createInitializer($classMetadata, $entityPersister);
$cloner = $this->createCloner($classMetadata, $entityPersister);
return $this->definitions[$className] = new ProxyDefinition(
ClassUtils::generateProxyClassName($className, $this->proxyNs),
return new ProxyDefinition(
self::generateProxyClassName($className, $this->proxyNs),
$classMetadata->getIdentifierFieldNames(),
$classMetadata->getReflectionProperties(),
$initializer,
@@ -176,6 +291,8 @@ EOPHP;
/**
* Creates a closure capable of initializing a proxy
*
* @deprecated ProxyFactory::createInitializer() is deprecated and will be removed in version 3.0 of doctrine/orm.
*
* @psalm-return Closure(CommonProxy):void
*
* @throws EntityNotFoundException
@@ -241,16 +358,16 @@ EOPHP;
*
* @throws EntityNotFoundException
*/
private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister): Closure
private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure
{
return function (InternalProxy $proxy, InternalProxy $original) use ($entityPersister, $classMetadata): void {
return static function (InternalProxy $proxy, InternalProxy $original) use ($entityPersister, $classMetadata, $identifierFlattener): void {
$identifier = $classMetadata->getIdentifierValues($original);
$entity = $entityPersister->loadById($identifier, $original);
if ($entity === null) {
throw EntityNotFoundException::fromClassNameAndIdentifier(
$classMetadata->getName(),
$this->identifierFlattener->flattenIdentifier($classMetadata, $identifier)
$identifierFlattener->flattenIdentifier($classMetadata, $identifier)
);
}
@@ -265,7 +382,6 @@ EOPHP;
continue;
}
$property->setAccessible(true);
$property->setValue($proxy, $property->getValue($entity));
}
};
@@ -274,6 +390,8 @@ EOPHP;
/**
* Creates a closure capable of finalizing state a cloned proxy
*
* @deprecated ProxyFactory::createCloner() is deprecated and will be removed in version 3.0 of doctrine/orm.
*
* @psalm-return Closure(CommonProxy):void
*
* @throws EntityNotFoundException
@@ -310,25 +428,18 @@ EOPHP;
};
}
private function generateUseLazyGhostTrait(ClassMetadata $class): string
private function getProxyFileName(string $className, string $baseDirectory): string
{
$code = ProxyHelper::generateLazyGhost($class->getReflectionClass());
$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;
setLazyObjectAsInitialized as public __setInitialized;
isLazyObjectInitialized as private;
createLazyGhost as private;
resetLazyObject as private;
}'), $code);
$baseDirectory = $baseDirectory ?: $this->proxyDir;
return $code;
return rtrim($baseDirectory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . InternalProxy::MARKER
. str_replace('\\', '', $className) . '.php';
}
private function generateSkippedProperties(ClassMetadata $class): string
private function getProxyFactory(string $className): Closure
{
$skippedProperties = [];
$class = $this->em->getClassMetadata($className);
$identifiers = array_flip($class->getIdentifierFieldNames());
$filter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE;
$reflector = $class->getReflectionClass();
@@ -350,11 +461,123 @@ EOPHP;
$reflector = $reflector->getParentClass();
}
uksort($skippedProperties, 'strnatcmp');
$className = $class->getName(); // aliases and case sensitivity
$entityPersister = $this->uow->getEntityPersister($className);
$initializer = $this->createLazyInitializer($class, $entityPersister, $this->identifierFlattener);
$proxyClassName = $this->loadProxyClass($class);
$identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers);
$code = VarExporter::export($skippedProperties);
$code = str_replace(VarExporter::export($class->getName()), 'parent::class', $code);
$code = str_replace("\n", "\n ", $code);
$proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy {
$proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, &$proxy): void {
$initializer($object, $proxy);
}, $skippedProperties);
foreach ($identifierFields as $idField => $reflector) {
if (! isset($identifier[$idField])) {
throw ORMInvalidArgumentException::missingPrimaryKeyValue($className, $idField);
}
$reflector->setValue($proxy, $identifier[$idField]);
}
return $proxy;
}, null, $proxyClassName);
return $this->proxyFactories[$className] = $proxyFactory;
}
private function loadProxyClass(ClassMetadata $class): string
{
$proxyClassName = self::generateProxyClassName($class->getName(), $this->proxyNs);
if (class_exists($proxyClassName, false)) {
return $proxyClassName;
}
if ($this->autoGenerate === self::AUTOGENERATE_EVAL) {
$this->generateProxyClass($class, null, $proxyClassName);
return $proxyClassName;
}
$fileName = $this->getProxyFileName($class->getName(), $this->proxyDir);
switch ($this->autoGenerate) {
case self::AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED:
if (file_exists($fileName) && filemtime($fileName) >= filemtime($class->getReflectionClass()->getFileName())) {
break;
}
// no break
case self::AUTOGENERATE_FILE_NOT_EXISTS:
if (file_exists($fileName)) {
break;
}
// no break
case self::AUTOGENERATE_ALWAYS:
$this->generateProxyClass($class, $fileName, $proxyClassName);
break;
}
require $fileName;
return $proxyClassName;
}
private function generateProxyClass(ClassMetadata $class, ?string $fileName, string $proxyClassName): void
{
$i = strrpos($proxyClassName, '\\');
$placeholders = [
'<className>' => $class->getName(),
'<namespace>' => substr($proxyClassName, 0, $i),
'<proxyShortClassName>' => substr($proxyClassName, 1 + $i),
'<baseProxyInterface>' => InternalProxy::class,
];
preg_match_all('(<([a-zA-Z]+)>)', self::PROXY_CLASS_TEMPLATE, $placeholderMatches);
foreach (array_combine($placeholderMatches[0], $placeholderMatches[1]) as $placeholder => $name) {
$placeholders[$placeholder] ?? $placeholders[$placeholder] = $this->{'generate' . ucfirst($name)}($class);
}
$proxyCode = strtr(self::PROXY_CLASS_TEMPLATE, $placeholders);
if (! $fileName) {
if (! class_exists($proxyClassName)) {
eval(substr($proxyCode, 5));
}
return;
}
$parentDirectory = dirname($fileName);
if (! is_dir($parentDirectory) && ! @mkdir($parentDirectory, 0775, true)) {
throw ORMInvalidArgumentException::proxyDirectoryNotWritable($this->proxyDir);
}
if (! is_writable($parentDirectory)) {
throw ORMInvalidArgumentException::proxyDirectoryNotWritable($this->proxyDir);
}
$tmpFileName = $fileName . '.' . bin2hex(random_bytes(12));
file_put_contents($tmpFileName, $proxyCode);
@chmod($tmpFileName, 0664);
rename($tmpFileName, $fileName);
}
private function generateUseLazyGhostTrait(ClassMetadata $class): string
{
$code = ProxyHelper::generateLazyGhost($class->getReflectionClass());
$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;
setLazyObjectAsInitialized as public __setInitialized;
isLazyObjectInitialized as private;
createLazyGhost as private;
resetLazyObject as private;
}'), $code);
return $code;
}
@@ -365,7 +588,7 @@ EOPHP;
$properties = $reflector->hasMethod('__serialize') ? 'parent::__serialize()' : '(array) $this';
$code = '$properties = ' . $properties . ';
unset($properties["\0" . self::class . "\0lazyObjectState"], $properties[\'__isCloning\']);
unset($properties["\0" . self::class . "\0lazyObjectState"]);
';
@@ -387,4 +610,9 @@ EOPHP;
return $data;';
}
private static function generateProxyClassName(string $className, string $proxyNamespace): string
{
return rtrim($proxyNamespace, '\\') . '\\' . Proxy::MARKER . '\\' . ltrim($className, '\\');
}
}

View File

@@ -44,8 +44,10 @@ use function stripos;
/**
* A Query object represents a DQL query.
*
* @final
*/
final class Query extends AbstractQuery
class Query extends AbstractQuery
{
/**
* A query object is in CLEAN state when it has NO unparsed/unprocessed DQL parts.

View File

@@ -9,7 +9,7 @@ namespace Doctrine\ORM\Query\AST;
*
* @link www.doctrine-project.org
*/
class ConditionalFactor extends Node
class ConditionalFactor extends Node implements Phase2OptimizableConditional
{
/** @var bool */
public $not = false;

View File

@@ -9,12 +9,12 @@ namespace Doctrine\ORM\Query\AST;
*
* @link www.doctrine-project.org
*/
class ConditionalPrimary extends Node
class ConditionalPrimary extends Node implements Phase2OptimizableConditional
{
/** @var Node|null */
public $simpleConditionalExpression;
/** @var ConditionalExpression|null */
/** @var ConditionalExpression|Phase2OptimizableConditional|null */
public $conditionalExpression;
/** @return bool */

View File

@@ -9,7 +9,7 @@ namespace Doctrine\ORM\Query\AST;
*
* @link www.doctrine-project.org
*/
class ConditionalTerm extends Node
class ConditionalTerm extends Node implements Phase2OptimizableConditional
{
/** @var mixed[] */
public $conditionalFactors = [];

View File

@@ -6,10 +6,10 @@ namespace Doctrine\ORM\Query\AST;
class HavingClause extends Node
{
/** @var ConditionalExpression */
/** @var ConditionalExpression|Phase2OptimizableConditional */
public $conditionalExpression;
/** @param ConditionalExpression $conditionalExpression */
/** @param ConditionalExpression|Phase2OptimizableConditional $conditionalExpression */
public function __construct($conditionalExpression)
{
$this->conditionalExpression = $conditionalExpression;

View File

@@ -25,7 +25,7 @@ class Join extends Node
/** @var Node|null */
public $joinAssociationDeclaration = null;
/** @var ConditionalExpression|null */
/** @var ConditionalExpression|Phase2OptimizableConditional|null */
public $conditionalExpression = null;
/**

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Query\AST;
/**
* Marks types that can be used in place of a ConditionalExpression as a phase
* 2 optimization.
*
* @internal
*
* @psalm-inheritors ConditionalPrimary|ConditionalFactor|ConditionalTerm
*/
interface Phase2OptimizableConditional
{
}

View File

@@ -11,15 +11,15 @@ namespace Doctrine\ORM\Query\AST;
*/
class WhenClause extends Node
{
/** @var ConditionalExpression */
/** @var ConditionalExpression|Phase2OptimizableConditional */
public $caseConditionExpression;
/** @var mixed */
public $thenScalarExpression = null;
/**
* @param ConditionalExpression $caseConditionExpression
* @param mixed $thenScalarExpression
* @param ConditionalExpression|Phase2OptimizableConditional $caseConditionExpression
* @param mixed $thenScalarExpression
*/
public function __construct($caseConditionExpression, $thenScalarExpression)
{

View File

@@ -11,10 +11,10 @@ namespace Doctrine\ORM\Query\AST;
*/
class WhereClause extends Node
{
/** @var ConditionalExpression|ConditionalTerm */
/** @var ConditionalExpression|Phase2OptimizableConditional */
public $conditionalExpression;
/** @param ConditionalExpression $conditionalExpression */
/** @param ConditionalExpression|Phase2OptimizableConditional $conditionalExpression */
public function __construct($conditionalExpression)
{
$this->conditionalExpression = $conditionalExpression;

View File

@@ -9,6 +9,12 @@ use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Result;
use Doctrine\DBAL\Types\Type;
use function array_diff;
use function array_keys;
use function array_map;
use function array_values;
use function str_replace;
/**
* Base class for SQL statement executors.
*
@@ -18,12 +24,24 @@ use Doctrine\DBAL\Types\Type;
*/
abstract class AbstractSqlExecutor
{
/** @var list<string>|string */
/**
* @deprecated use $sqlStatements instead
*
* @var list<string>|string
*/
protected $_sqlStatements;
/** @var list<string>|string */
protected $sqlStatements;
/** @var QueryCacheProfile */
protected $queryCacheProfile;
public function __construct()
{
$this->_sqlStatements = &$this->sqlStatements;
}
/**
* Gets the SQL statements that are executed by the executor.
*
@@ -31,21 +49,18 @@ abstract class AbstractSqlExecutor
*/
public function getSqlStatements()
{
return $this->_sqlStatements;
return $this->sqlStatements;
}
/** @return void */
public function setQueryCacheProfile(QueryCacheProfile $qcp)
public function setQueryCacheProfile(QueryCacheProfile $qcp): void
{
$this->queryCacheProfile = $qcp;
}
/**
* Do not use query cache
*
* @return void
*/
public function removeQueryCacheProfile()
public function removeQueryCacheProfile(): void
{
$this->queryCacheProfile = null;
}
@@ -60,4 +75,28 @@ abstract class AbstractSqlExecutor
* @return Result|int
*/
abstract public function execute(Connection $conn, array $params, array $types);
/** @return list<string> */
public function __sleep(): array
{
/* Two reasons for this:
- we do not need to serialize the deprecated property, we can
rebuild the reference to the new property in __wakeup()
- not having the legacy property in the serialized data means the
serialized representation becomes compatible with 3.0.x, meaning
there will not be a deprecation warning about a missing property
when unserializing data */
return array_values(array_diff(array_map(static function (string $prop): string {
return str_replace("\0*\0", '', $prop);
}, array_keys((array) $this)), ['_sqlStatements']));
}
public function __wakeup(): void
{
if ($this->_sqlStatements !== null && $this->sqlStatements === null) {
$this->sqlStatements = $this->_sqlStatements;
}
$this->_sqlStatements = &$this->sqlStatements;
}
}

View File

@@ -45,6 +45,8 @@ class MultiTableDeleteExecutor extends AbstractSqlExecutor
*/
public function __construct(AST\Node $AST, $sqlWalker)
{
parent::__construct();
$em = $sqlWalker->getEntityManager();
$conn = $em->getConnection();
$platform = $conn->getDatabasePlatform();
@@ -83,8 +85,8 @@ class MultiTableDeleteExecutor extends AbstractSqlExecutor
// 3. Create and store DELETE statements
$classNames = array_merge($primaryClass->parentClasses, [$primaryClass->name], $primaryClass->subClasses);
foreach (array_reverse($classNames) as $className) {
$tableName = $quoteStrategy->getTableName($em->getClassMetadata($className), $platform);
$this->_sqlStatements[] = 'DELETE FROM ' . $tableName
$tableName = $quoteStrategy->getTableName($em->getClassMetadata($className), $platform);
$this->sqlStatements[] = 'DELETE FROM ' . $tableName
. ' WHERE (' . $idColumnList . ') IN (' . $idSubselect . ')';
}
@@ -117,7 +119,7 @@ class MultiTableDeleteExecutor extends AbstractSqlExecutor
$numDeleted = $conn->executeStatement($this->insertSql, $params, $types);
// Execute DELETE statements
foreach ($this->_sqlStatements as $sql) {
foreach ($this->sqlStatements as $sql) {
$conn->executeStatement($sql);
}
} catch (Throwable $exception) {

View File

@@ -50,6 +50,8 @@ class MultiTableUpdateExecutor extends AbstractSqlExecutor
*/
public function __construct(AST\Node $AST, $sqlWalker)
{
parent::__construct();
$em = $sqlWalker->getEntityManager();
$conn = $em->getConnection();
$platform = $conn->getDatabasePlatform();
@@ -119,7 +121,7 @@ class MultiTableUpdateExecutor extends AbstractSqlExecutor
}
if ($affected) {
$this->_sqlStatements[$i] = $updateSql . ' WHERE (' . $idColumnList . ') IN (' . $idSubselect . ')';
$this->sqlStatements[$i] = $updateSql . ' WHERE (' . $idColumnList . ') IN (' . $idSubselect . ')';
}
}
@@ -163,7 +165,7 @@ class MultiTableUpdateExecutor extends AbstractSqlExecutor
);
// Execute UPDATE statements
foreach ($this->_sqlStatements as $key => $statement) {
foreach ($this->sqlStatements as $key => $statement) {
$paramValues = [];
$paramTypes = [];

View File

@@ -18,7 +18,9 @@ class SingleSelectExecutor extends AbstractSqlExecutor
{
public function __construct(SelectStatement $AST, SqlWalker $sqlWalker)
{
$this->_sqlStatements = $sqlWalker->walkSelectStatement($AST);
parent::__construct();
$this->sqlStatements = $sqlWalker->walkSelectStatement($AST);
}
/**
@@ -28,6 +30,6 @@ class SingleSelectExecutor extends AbstractSqlExecutor
*/
public function execute(Connection $conn, array $params, array $types)
{
return $conn->executeQuery($this->_sqlStatements, $params, $types, $this->queryCacheProfile);
return $conn->executeQuery($this->sqlStatements, $params, $types, $this->queryCacheProfile);
}
}

View File

@@ -22,10 +22,12 @@ class SingleTableDeleteUpdateExecutor extends AbstractSqlExecutor
/** @param SqlWalker $sqlWalker */
public function __construct(AST\Node $AST, $sqlWalker)
{
parent::__construct();
if ($AST instanceof AST\UpdateStatement) {
$this->_sqlStatements = $sqlWalker->walkUpdateStatement($AST);
$this->sqlStatements = $sqlWalker->walkUpdateStatement($AST);
} elseif ($AST instanceof AST\DeleteStatement) {
$this->_sqlStatements = $sqlWalker->walkDeleteStatement($AST);
$this->sqlStatements = $sqlWalker->walkDeleteStatement($AST);
}
}
@@ -40,6 +42,6 @@ class SingleTableDeleteUpdateExecutor extends AbstractSqlExecutor
$conn->ensureConnectedToPrimary();
}
return $conn->executeStatement($this->_sqlStatements, $params, $types);
return $conn->executeStatement($this->sqlStatements, $params, $types);
}
}

View File

@@ -141,7 +141,9 @@ class ParserResult
{
foreach (self::LEGACY_PROPERTY_MAPPING as $property => $legacyProperty) {
$this->$property = $data[sprintf("\0%s\0%s", self::class, $legacyProperty)]
?? $data[self::class][$legacyProperty]
?? $data[sprintf("\0%s\0%s", self::class, $property)]
?? $data[self::class][$property]
?? $this->$property
?? null;
}

View File

@@ -204,6 +204,14 @@ class QueryException extends ORMException
);
}
public static function eagerFetchJoinWithNotAllowed(string $sourceEntity, string $fieldName): QueryException
{
return new self(
'Associations with fetch-mode=EAGER may not be using WITH conditions in
"' . $sourceEntity . '#' . $fieldName . '".'
);
}
public static function iterateWithMixedResultNotAllowed(): QueryException
{
return new self('Iterating a query with mixed results (using scalars) is not supported.');

View File

@@ -154,6 +154,11 @@ class ResultSetMappingBuilder extends ResultSetMapping
}
$this->addFieldResult($alias, $columnAlias, $propertyName);
$enumType = $classMetadata->getFieldMapping($propertyName)['enumType'] ?? null;
if (! empty($enumType)) {
$this->addEnumResult($columnAlias, $enumType);
}
}
foreach ($classMetadata->associationMappings as $associationMapping) {

View File

@@ -1010,9 +1010,9 @@ class SqlWalker implements TreeWalker
/**
* Walks down a JoinAssociationDeclaration AST node, thereby generating the appropriate SQL.
*
* @param AST\JoinAssociationDeclaration $joinAssociationDeclaration
* @param int $joinType
* @param AST\ConditionalExpression $condExpr
* @param AST\JoinAssociationDeclaration $joinAssociationDeclaration
* @param int $joinType
* @param AST\ConditionalExpression|AST\Phase2OptimizableConditional $condExpr
* @psalm-param AST\Join::JOIN_TYPE_* $joinType
*
* @return string
@@ -1047,7 +1047,9 @@ class SqlWalker implements TreeWalker
}
}
$targetTableJoin = null;
if ($relation['fetch'] === ClassMetadata::FETCH_EAGER && $condExpr !== null) {
throw QueryException::eagerFetchJoinWithNotAllowed($assoc['sourceEntity'], $assoc['fieldName']);
}
// This condition is not checking ClassMetadata::MANY_TO_ONE, because by definition it cannot
// be the owning side and previously we ensured that $assoc is always the owning side of the associations.
@@ -2048,7 +2050,7 @@ class SqlWalker implements TreeWalker
/**
* Walk down a ConditionalExpression AST node, thereby generating the appropriate SQL.
*
* @param AST\ConditionalExpression $condExpr
* @param AST\ConditionalExpression|AST\Phase2OptimizableConditional $condExpr
*
* @return string
*
@@ -2068,7 +2070,7 @@ class SqlWalker implements TreeWalker
/**
* Walks down a ConditionalTerm AST node, thereby generating the appropriate SQL.
*
* @param AST\ConditionalTerm $condTerm
* @param AST\ConditionalTerm|AST\ConditionalFactor|AST\ConditionalPrimary $condTerm
*
* @return string
*
@@ -2088,7 +2090,7 @@ class SqlWalker implements TreeWalker
/**
* Walks down a ConditionalFactor AST node, thereby generating the appropriate SQL.
*
* @param AST\ConditionalFactor $factor
* @param AST\ConditionalFactor|AST\ConditionalPrimary $factor
*
* @return string The SQL.
*

View File

@@ -6,6 +6,7 @@ namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -20,6 +21,8 @@ use function sprintf;
*/
class CollectionRegionCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -58,12 +61,7 @@ EOT
);
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();

View File

@@ -6,6 +6,7 @@ namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -20,6 +21,8 @@ use function sprintf;
*/
class EntityRegionCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -57,12 +60,7 @@ EOT
);
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use InvalidArgumentException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -18,6 +19,8 @@ use Symfony\Component\Console\Style\SymfonyStyle;
*/
class MetadataCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -31,12 +34,7 @@ EOT
);
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();

View File

@@ -9,6 +9,7 @@ use Doctrine\Common\Cache\ClearableCache;
use Doctrine\Common\Cache\FlushableCache;
use Doctrine\Common\Cache\XcacheCache;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use InvalidArgumentException;
use LogicException;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
@@ -28,6 +29,8 @@ use function sprintf;
*/
class QueryCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -55,12 +58,7 @@ EOT
);
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();

View File

@@ -6,6 +6,7 @@ namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -20,6 +21,8 @@ use function sprintf;
*/
class QueryRegionCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -56,12 +59,7 @@ EOT
);
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();

View File

@@ -10,6 +10,7 @@ use Doctrine\Common\Cache\FlushableCache;
use Doctrine\Common\Cache\XcacheCache;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use InvalidArgumentException;
use LogicException;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
@@ -29,6 +30,8 @@ use function sprintf;
*/
class ResultCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -56,12 +59,7 @@ EOT
);
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\ConvertDoctrine1Schema;
use Doctrine\ORM\Tools\EntityGenerator;
use Doctrine\ORM\Tools\Export\ClassMetadataExporter;
@@ -34,6 +35,8 @@ use const PHP_EOL;
*/
class ConvertDoctrine1SchemaCommand extends Command
{
use CommandCompatibility;
/** @var EntityGenerator|null */
private $entityGenerator = null;
@@ -87,12 +90,7 @@ class ConvertDoctrine1SchemaCommand extends Command
->setHelp('Converts Doctrine 1.x schema into a Doctrine 2.x schema.');
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = new SymfonyStyle($input, $output);
$ui->getErrorStyle()->warning('Command ' . $this->getName() . ' is deprecated and will be removed in Doctrine ORM 3.0.');

View File

@@ -6,6 +6,7 @@ namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\Mapping\Driver\DatabaseDriver;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\Console\MetadataFilter;
use Doctrine\ORM\Tools\DisconnectedClassMetadataFactory;
use Doctrine\ORM\Tools\EntityGenerator;
@@ -37,6 +38,8 @@ use function strtolower;
*/
class ConvertMappingCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -84,12 +87,7 @@ EOT
);
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = new SymfonyStyle($input, $output);
$ui->getErrorStyle()->warning('Command ' . $this->getName() . ' is deprecated and will be removed in Doctrine ORM 3.0.');

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@@ -19,6 +20,8 @@ use Throwable;
*/
class EnsureProductionSettingsCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -29,12 +32,7 @@ class EnsureProductionSettingsCommand extends AbstractEntityManagerCommand
->setHelp('Verify that Doctrine is properly configured for a production environment.');
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$ui->warning('This console command has been deprecated and will be removed in a future version of Doctrine ORM.');

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\Console\MetadataFilter;
use Doctrine\ORM\Tools\DisconnectedClassMetadataFactory;
use Doctrine\ORM\Tools\EntityGenerator;
@@ -28,6 +29,8 @@ use function sprintf;
*/
class GenerateEntitiesCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -67,12 +70,7 @@ EOT
);
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$ui->warning('Command ' . $this->getName() . ' is deprecated and will be removed in Doctrine ORM 3.0.');

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\Console\MetadataFilter;
use InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
@@ -26,6 +27,8 @@ use function sprintf;
*/
class GenerateProxiesCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -38,12 +41,7 @@ class GenerateProxiesCommand extends AbstractEntityManagerCommand
->setHelp('Generates proxy classes for entity classes.');
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\Console\MetadataFilter;
use Doctrine\ORM\Tools\EntityRepositoryGenerator;
use InvalidArgumentException;
@@ -27,6 +28,8 @@ use function sprintf;
*/
class GenerateRepositoriesCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -39,12 +42,7 @@ class GenerateRepositoriesCommand extends AbstractEntityManagerCommand
->setHelp('Generate repository classes from your mapping information.');
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$ui->warning('Command ' . $this->getName() . ' is deprecated and will be removed in Doctrine ORM 3.0.');

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@@ -20,6 +21,8 @@ use function sprintf;
*/
class InfoCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -34,12 +37,7 @@ EOT
);
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();

View File

@@ -4,7 +4,8 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\Common\Util\Debug;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\Debug;
use LogicException;
use RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
@@ -27,6 +28,8 @@ use function strtoupper;
*/
class RunDqlCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -58,12 +61,7 @@ EOT
);
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = new SymfonyStyle($input, $output);
@@ -118,7 +116,7 @@ EOT
$resultSet = $query->execute([], constant($hydrationMode));
$ui->text(Debug::dump($resultSet, (int) $input->getOption('depth'), true, false));
$ui->text(Debug::dump($resultSet, (int) $input->getOption('depth')));
return 0;
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command\SchemaTool;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\SchemaTool;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@@ -17,6 +18,8 @@ use Symfony\Component\Console\Style\SymfonyStyle;
*/
abstract class AbstractCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/**
* @param mixed[] $metadatas
*
@@ -24,12 +27,7 @@ abstract class AbstractCommand extends AbstractEntityManagerCommand
*/
abstract protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui);
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = new SymfonyStyle($input, $output);

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\SchemaValidator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -20,6 +21,8 @@ use function sprintf;
*/
class ValidateSchemaCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -28,20 +31,16 @@ class ValidateSchemaCommand extends AbstractEntityManagerCommand
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
->addOption('skip-mapping', null, InputOption::VALUE_NONE, 'Skip the mapping validation check')
->addOption('skip-sync', null, InputOption::VALUE_NONE, 'Skip checking if the mapping is in sync with the database')
->addOption('skip-property-types', null, InputOption::VALUE_NONE, 'Skip checking if property types match the Doctrine types')
->setHelp('Validate that the mapping files are correct and in sync with the database.');
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$em = $this->getEntityManager($input);
$validator = new SchemaValidator($em);
$validator = new SchemaValidator($em, ! $input->getOption('skip-property-types'));
$exit = 0;
$ui->section('Mapping');

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console;
use ReflectionMethod;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
if ((new ReflectionMethod(Command::class, 'execute'))->hasReturnType()) {
/** @internal */
trait CommandCompatibility
{
protected function execute(InputInterface $input, OutputInterface $output): int
{
return $this->doExecute($input, $output);
}
}
} else {
/** @internal */
trait CommandCompatibility
{
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
return $this->doExecute($input, $output);
}
}
}

View File

@@ -6,7 +6,34 @@ namespace Doctrine\ORM\Tools\Console\Helper;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use ReflectionMethod;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Helper\HelperInterface;
if ((new ReflectionMethod(HelperInterface::class, 'getName'))->hasReturnType()) {
/** @internal */
trait EntityManagerHelperCompatibility
{
public function getName(): string
{
return 'entityManager';
}
}
} else {
/** @internal */
trait EntityManagerHelperCompatibility
{
/**
* {@inheritDoc}
*
* @return string
*/
public function getName()
{
return 'entityManager';
}
}
}
/**
* Doctrine CLI Connection Helper.
@@ -15,6 +42,8 @@ use Symfony\Component\Console\Helper\Helper;
*/
class EntityManagerHelper extends Helper
{
use EntityManagerHelperCompatibility;
/**
* Doctrine ORM EntityManagerInterface.
*
@@ -43,14 +72,4 @@ class EntityManagerHelper extends Helper
{
return $this->_em;
}
/**
* {@inheritDoc}
*
* @return string
*/
public function getName()
{
return 'entityManager';
}
}

View File

@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools;
use ArrayIterator;
use ArrayObject;
use DateTimeInterface;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\Persistence\Proxy;
use stdClass;
use function array_keys;
use function count;
use function end;
use function explode;
use function extension_loaded;
use function get_class;
use function html_entity_decode;
use function ini_get;
use function ini_set;
use function is_array;
use function is_object;
use function ob_end_clean;
use function ob_get_contents;
use function ob_start;
use function strip_tags;
use function var_dump;
/**
* Static class containing most used debug methods.
*
* @internal
*
* @link www.doctrine-project.org
*/
final class Debug
{
/**
* Private constructor (prevents instantiation).
*/
private function __construct()
{
}
/**
* Prints a dump of the public, protected and private properties of $var.
*
* @link https://xdebug.org/
*
* @param mixed $var The variable to dump.
* @param int $maxDepth The maximum nesting level for object properties.
*/
public static function dump($var, int $maxDepth = 2): string
{
$html = ini_get('html_errors');
if ($html !== '1') {
ini_set('html_errors', 'on');
}
if (extension_loaded('xdebug')) {
$previousDepth = ini_get('xdebug.var_display_max_depth');
ini_set('xdebug.var_display_max_depth', (string) $maxDepth);
}
try {
$var = self::export($var, $maxDepth);
ob_start();
var_dump($var);
$dump = ob_get_contents();
ob_end_clean();
$dumpText = strip_tags(html_entity_decode($dump));
} finally {
ini_set('html_errors', $html);
if (isset($previousDepth)) {
ini_set('xdebug.var_display_max_depth', $previousDepth);
}
}
return $dumpText;
}
/**
* @param mixed $var
*
* @return mixed
*/
public static function export($var, int $maxDepth)
{
if ($var instanceof Collection) {
$var = $var->toArray();
}
if (! $maxDepth) {
return is_object($var) ? get_class($var)
: (is_array($var) ? 'Array(' . count($var) . ')' : $var);
}
if (is_array($var)) {
$return = [];
foreach ($var as $k => $v) {
$return[$k] = self::export($v, $maxDepth - 1);
}
return $return;
}
if (! is_object($var)) {
return $var;
}
$return = new stdClass();
if ($var instanceof DateTimeInterface) {
$return->__CLASS__ = get_class($var);
$return->date = $var->format('c');
$return->timezone = $var->getTimezone()->getName();
return $return;
}
$return->__CLASS__ = DefaultProxyClassNameResolver::getClass($var);
if ($var instanceof Proxy) {
$return->__IS_PROXY__ = true;
$return->__PROXY_INITIALIZED__ = $var->__isInitialized();
}
if ($var instanceof ArrayObject || $var instanceof ArrayIterator) {
$return->__STORAGE__ = self::export($var->getArrayCopy(), $maxDepth - 1);
}
return self::fillReturnWithClassAttributes($var, $return, $maxDepth);
}
/**
* Fill the $return variable with class attributes
* Based on obj2array function from {@see https://secure.php.net/manual/en/function.get-object-vars.php#47075}
*
* @param object $var
*
* @return mixed
*/
private static function fillReturnWithClassAttributes($var, stdClass $return, int $maxDepth)
{
$clone = (array) $var;
foreach (array_keys($clone) as $key) {
$aux = explode("\0", (string) $key);
$name = end($aux);
if ($aux[0] === '') {
$name .= ':' . ($aux[1] === '*' ? 'protected' : $aux[1] . ':private');
}
$return->$name = self::export($clone[$key], $maxDepth - 1);
}
return $return;
}
}

View File

@@ -15,7 +15,6 @@ use Doctrine\ORM\Mapping\QuoteStrategy;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\AST\OrderByClause;
use Doctrine\ORM\Query\AST\PartialObjectExpression;
use Doctrine\ORM\Query\AST\PathExpression;
use Doctrine\ORM\Query\AST\SelectExpression;
use Doctrine\ORM\Query\AST\SelectStatement;
@@ -258,7 +257,7 @@ class LimitSubqueryOutputWalker extends SqlWalker
$innerSql
);
// http://www.doctrine-project.org/jira/browse/DDC-1958
// https://github.com/doctrine/orm/issues/2630
$sql = $this->preserveSqlOrdering($sqlIdentifier, $innerSql, $sql, $orderByClause);
// Apply the limit and offset.
@@ -335,7 +334,7 @@ class LimitSubqueryOutputWalker extends SqlWalker
// Add select items which were not excluded to the AST's select clause.
foreach ($selects as $idVar => $fields) {
$AST->selectClause->selectExpressions[] = new SelectExpression(new PartialObjectExpression($idVar, array_keys($fields)), null, true);
$AST->selectClause->selectExpressions[] = new SelectExpression($idVar, null, true);
}
}
@@ -374,8 +373,7 @@ class LimitSubqueryOutputWalker extends SqlWalker
string $innerSql
): string {
[$searchPatterns, $replacements] = $this->generateSqlAliasReplacements();
$orderByItems = [];
$orderByItems = [];
foreach ($orderByClause->orderByItems as $orderByItem) {
// Walk order by item to get string representation of it and

View File

@@ -50,12 +50,14 @@ class LimitSubqueryWalker extends TreeWalkerAdapter
throw new RuntimeException('Paginating an entity with foreign key as identifier only works when using the Output Walkers. Call Paginator#setUseOutputWalkers(true) before iterating the paginator.');
}
$this->_getQuery()->setHint(
$query = $this->_getQuery();
$query->setHint(
self::IDENTIFIER_TYPE,
Type::getType($rootClass->fieldMappings[$identifier]['type'])
);
$this->_getQuery()->setHint(self::FORCE_DBAL_TYPE_CONVERSION, true);
$query->setHint(self::FORCE_DBAL_TYPE_CONVERSION, true);
$pathExpression = new PathExpression(
PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION,
@@ -66,7 +68,7 @@ class LimitSubqueryWalker extends TreeWalkerAdapter
$pathExpression->type = PathExpression::TYPE_STATE_FIELD;
$AST->selectClause->selectExpressions = [new SelectExpression($pathExpression, '_dctrn_id')];
$AST->selectClause->isDistinct = true;
$AST->selectClause->isDistinct = ($query->getHints()[Paginator::HINT_ENABLE_DISTINCT] ?? true) === true;
if (! isset($AST->orderByClause)) {
return;

View File

@@ -34,6 +34,8 @@ class Paginator implements Countable, IteratorAggregate
{
use SQLResultCasing;
public const HINT_ENABLE_DISTINCT = 'paginator.distinct.enable';
/** @var Query */
private $query;

View File

@@ -6,7 +6,6 @@ namespace Doctrine\ORM\Tools\Pagination;
use Doctrine\ORM\Query\AST\ArithmeticExpression;
use Doctrine\ORM\Query\AST\ConditionalExpression;
use Doctrine\ORM\Query\AST\ConditionalFactor;
use Doctrine\ORM\Query\AST\ConditionalPrimary;
use Doctrine\ORM\Query\AST\ConditionalTerm;
use Doctrine\ORM\Query\AST\InListExpression;
@@ -96,10 +95,7 @@ class WhereInWalker extends TreeWalkerAdapter
),
]
);
} elseif (
$AST->whereClause->conditionalExpression instanceof ConditionalExpression
|| $AST->whereClause->conditionalExpression instanceof ConditionalFactor
) {
} else {
$tmpPrimary = new ConditionalPrimary();
$tmpPrimary->conditionalExpression = $AST->whereClause->conditionalExpression;
$AST->whereClause->conditionalExpression = new ConditionalTerm(

View File

@@ -4,36 +4,84 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools;
use BackedEnum;
use Doctrine\DBAL\Types\AsciiStringType;
use Doctrine\DBAL\Types\BigIntType;
use Doctrine\DBAL\Types\BooleanType;
use Doctrine\DBAL\Types\DecimalType;
use Doctrine\DBAL\Types\FloatType;
use Doctrine\DBAL\Types\GuidType;
use Doctrine\DBAL\Types\IntegerType;
use Doctrine\DBAL\Types\JsonType;
use Doctrine\DBAL\Types\SimpleArrayType;
use Doctrine\DBAL\Types\SmallIntType;
use Doctrine\DBAL\Types\StringType;
use Doctrine\DBAL\Types\TextType;
use Doctrine\DBAL\Types\Type;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use ReflectionEnum;
use ReflectionNamedType;
use function array_diff;
use function array_filter;
use function array_key_exists;
use function array_map;
use function array_push;
use function array_search;
use function array_values;
use function assert;
use function class_exists;
use function class_parents;
use function count;
use function get_class;
use function implode;
use function in_array;
use function interface_exists;
use function is_a;
use function sprintf;
use const PHP_VERSION_ID;
/**
* Performs strict validation of the mapping schema
*
* @link www.doctrine-project.com
*
* @psalm-import-type FieldMapping from ClassMetadata
*/
class SchemaValidator
{
/** @var EntityManagerInterface */
private $em;
public function __construct(EntityManagerInterface $em)
/** @var bool */
private $validatePropertyTypes;
/**
* It maps built-in Doctrine types to PHP types
*/
private const BUILTIN_TYPES_MAP = [
AsciiStringType::class => 'string',
BigIntType::class => 'string',
BooleanType::class => 'bool',
DecimalType::class => 'string',
FloatType::class => 'float',
GuidType::class => 'string',
IntegerType::class => 'int',
JsonType::class => 'array',
SimpleArrayType::class => 'array',
SmallIntType::class => 'int',
StringType::class => 'string',
TextType::class => 'string',
];
public function __construct(EntityManagerInterface $em, bool $validatePropertyTypes = true)
{
$this->em = $em;
$this->em = $em;
$this->validatePropertyTypes = $validatePropertyTypes;
}
/**
@@ -92,6 +140,11 @@ class SchemaValidator
}
}
// PHP 7.4 introduces the ability to type properties, so we can't validate them in previous versions
if (PHP_VERSION_ID >= 70400 && $this->validatePropertyTypes) {
array_push($ce, ...$this->validatePropertiesTypes($class));
}
if ($class->isEmbeddedClass && count($class->associationMappings) > 0) {
$ce[] = "Embeddable '" . $class->name . "' does not support associations";
@@ -304,4 +357,122 @@ class SchemaValidator
return $schemaTool->getUpdateSchemaSql($allMetadata, true);
}
/** @return list<string> containing the found issues */
private function validatePropertiesTypes(ClassMetadataInfo $class): array
{
return array_values(
array_filter(
array_map(
/** @param FieldMapping $fieldMapping */
function (array $fieldMapping) use ($class): ?string {
$fieldName = $fieldMapping['fieldName'];
assert(isset($class->reflFields[$fieldName]));
$propertyType = $class->reflFields[$fieldName]->getType();
// If the field type is not a built-in type, we cannot check it
if (! Type::hasType($fieldMapping['type'])) {
return null;
}
// If the property type is not a named type, we cannot check it
if (! ($propertyType instanceof ReflectionNamedType) || $propertyType->getName() === 'mixed') {
return null;
}
$metadataFieldType = $this->findBuiltInType(Type::getType($fieldMapping['type']));
//If the metadata field type is not a mapped built-in type, we cannot check it
if ($metadataFieldType === null) {
return null;
}
$propertyType = $propertyType->getName();
// If the property type is the same as the metadata field type, we are ok
if ($propertyType === $metadataFieldType) {
return null;
}
if (is_a($propertyType, BackedEnum::class, true)) {
$backingType = (string) (new ReflectionEnum($propertyType))->getBackingType();
if ($metadataFieldType !== $backingType) {
return sprintf(
"The field '%s#%s' has the property type '%s' with a backing type of '%s' that differs from the metadata field type '%s'.",
$class->name,
$fieldName,
$propertyType,
$backingType,
$metadataFieldType
);
}
if (! isset($fieldMapping['enumType']) || $propertyType === $fieldMapping['enumType']) {
return null;
}
return sprintf(
"The field '%s#%s' has the property type '%s' that differs from the metadata enumType '%s'.",
$class->name,
$fieldName,
$propertyType,
$fieldMapping['enumType']
);
}
if (
isset($fieldMapping['enumType'])
&& $propertyType !== $fieldMapping['enumType']
&& interface_exists($propertyType)
&& is_a($fieldMapping['enumType'], $propertyType, true)
) {
$backingType = (string) (new ReflectionEnum($fieldMapping['enumType']))->getBackingType();
if ($metadataFieldType === $backingType) {
return null;
}
return sprintf(
"The field '%s#%s' has the metadata enumType '%s' with a backing type of '%s' that differs from the metadata field type '%s'.",
$class->name,
$fieldName,
$fieldMapping['enumType'],
$backingType,
$metadataFieldType
);
}
if (
$fieldMapping['type'] === 'json'
&& in_array($propertyType, ['string', 'int', 'float', 'bool', 'true', 'false', 'null'], true)
) {
return null;
}
return sprintf(
"The field '%s#%s' has the property type '%s' that differs from the metadata field type '%s' returned by the '%s' DBAL type.",
$class->name,
$fieldName,
$propertyType,
$metadataFieldType,
$fieldMapping['type']
);
},
$class->fieldMappings
)
)
);
}
/**
* The exact DBAL type must be used (no subclasses), since consumers of doctrine/orm may have their own
* customization around field types.
*/
private function findBuiltInType(Type $type): ?string
{
$typeName = get_class($type);
return self::BUILTIN_TYPES_MAP[$typeName] ?? null;
}
}

View File

@@ -27,8 +27,8 @@ use Doctrine\ORM\Exception\EntityIdentityCollisionException;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\Exception\UnexpectedAssociationValue;
use Doctrine\ORM\Id\AssignedGenerator;
use Doctrine\ORM\Internal\CommitOrderCalculator;
use Doctrine\ORM\Internal\HydrationCompleteHandler;
use Doctrine\ORM\Internal\StronglyConnectedComponents;
use Doctrine\ORM\Internal\TopologicalSort;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\MappingException;
@@ -52,6 +52,7 @@ use RuntimeException;
use Throwable;
use UnexpectedValueException;
use function array_chunk;
use function array_combine;
use function array_diff_key;
use function array_filter;
@@ -315,6 +316,9 @@ class UnitOfWork implements PropertyChangedListener
*/
private $eagerLoadingEntities = [];
/** @var array<string, array<string, mixed>> */
private $eagerLoadingCollections = [];
/** @var bool */
protected $hasCache = false;
@@ -1375,9 +1379,10 @@ class UnitOfWork implements PropertyChangedListener
$joinColumns = reset($assoc['joinColumns']);
$isNullable = ! isset($joinColumns['nullable']) || $joinColumns['nullable'];
// Add dependency. The dependency direction implies that "$targetEntity has to go before $entity",
// so we can work through the topo sort result from left to right (with all edges pointing right).
$sort->addEdge($targetEntity, $entity, $isNullable);
// Add dependency. The dependency direction implies that "$entity depends on $targetEntity". The
// topological sort result will output the depended-upon nodes first, which means we can insert
// entities in that order.
$sort->addEdge($entity, $targetEntity, $isNullable);
}
}
@@ -1387,14 +1392,19 @@ class UnitOfWork implements PropertyChangedListener
/** @return list<object> */
private function computeDeleteExecutionOrder(): array
{
$sort = new TopologicalSort();
$stronglyConnectedComponents = new StronglyConnectedComponents();
$sort = new TopologicalSort();
// First make sure we have all the nodes
foreach ($this->entityDeletions as $entity) {
$stronglyConnectedComponents->addNode($entity);
$sort->addNode($entity);
}
// Now add edges
// First, consider only "on delete cascade" associations between entities
// and find strongly connected groups. Once we delete any one of the entities
// in such a group, _all_ of the other entities will be removed as well. So,
// we need to treat those groups like a single entity when performing delete
// order topological sorting.
foreach ($this->entityDeletions as $entity) {
$class = $this->em->getClassMetadata(get_class($entity));
@@ -1406,16 +1416,65 @@ class UnitOfWork implements PropertyChangedListener
continue;
}
// For associations that implement a database-level cascade/set null operation,
assert(isset($assoc['joinColumns']));
$joinColumns = reset($assoc['joinColumns']);
if (! isset($joinColumns['onDelete'])) {
continue;
}
$onDeleteOption = strtolower($joinColumns['onDelete']);
if ($onDeleteOption !== 'cascade') {
continue;
}
$targetEntity = $class->getFieldValue($entity, $assoc['fieldName']);
// If the association does not refer to another entity or that entity
// is not to be deleted, there is no ordering problem and we can
// skip this particular association.
if ($targetEntity === null || ! $stronglyConnectedComponents->hasNode($targetEntity)) {
continue;
}
$stronglyConnectedComponents->addEdge($entity, $targetEntity);
}
}
$stronglyConnectedComponents->findStronglyConnectedComponents();
// Now do the actual topological sorting to find the delete order.
foreach ($this->entityDeletions as $entity) {
$class = $this->em->getClassMetadata(get_class($entity));
// Get the entities representing the SCC
$entityComponent = $stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($entity);
// When $entity is part of a non-trivial strongly connected component group
// (a group containing not only those entities alone), make sure we process it _after_ the
// entity representing the group.
// The dependency direction implies that "$entity depends on $entityComponent
// being deleted first". The topological sort will output the depended-upon nodes first.
if ($entityComponent !== $entity) {
$sort->addEdge($entity, $entityComponent, false);
}
foreach ($class->associationMappings as $assoc) {
// We only need to consider the owning sides of to-one associations,
// since many-to-many associations can always be (and have already been)
// deleted in a preceding step.
if (! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
continue;
}
// For associations that implement a database-level set null operation,
// we do not have to follow a particular order: If the referred-to entity is
// deleted first, the DBMS will either delete the current $entity right away
// (CASCADE) or temporarily set the foreign key to NULL (SET NULL).
// Either way, we can skip it in the computation.
// deleted first, the DBMS will temporarily set the foreign key to NULL (SET NULL).
// So, we can skip it in the computation.
assert(isset($assoc['joinColumns']));
$joinColumns = reset($assoc['joinColumns']);
if (isset($joinColumns['onDelete'])) {
$onDeleteOption = strtolower($joinColumns['onDelete']);
if ($onDeleteOption === 'cascade' || $onDeleteOption === 'set null') {
if ($onDeleteOption === 'set null') {
continue;
}
}
@@ -1429,9 +1488,17 @@ class UnitOfWork implements PropertyChangedListener
continue;
}
// Add dependency. The dependency direction implies that "$entity has to be removed before $targetEntity",
// so we can work through the topo sort result from left to right (with all edges pointing right).
$sort->addEdge($entity, $targetEntity, false);
// Get the entities representing the SCC
$targetEntityComponent = $stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($targetEntity);
// When we have a dependency between two different groups of strongly connected nodes,
// add it to the computation.
// The dependency direction implies that "$targetEntityComponent depends on $entityComponent
// being deleted first". The topological sort will output the depended-upon nodes first,
// so we can work through the result in the returned order.
if ($targetEntityComponent !== $entityComponent) {
$sort->addEdge($targetEntityComponent, $entityComponent, false);
}
}
}
@@ -1682,11 +1749,11 @@ IDs should uniquely map to entity object instances. This problem may occur if:
clearing the EntityManager;
- you might have been using EntityManager#getReference() to create a reference
for a nonexistent ID that was subsequently (by the RDBMS) assigned to another
entity.
entity.
Otherwise, it might be an ORM-internal inconsistency, please report it.
To opt-in to the new exception, call
To opt-in to the new exception, call
\Doctrine\ORM\Configuration::setRejectIdCollisionInIdentityMap on the entity
manager's configuration.
EXCEPTION
@@ -2721,16 +2788,6 @@ EXCEPTION
}
}
/**
* Gets the CommitOrderCalculator used by the UnitOfWork to order commits.
*
* @return CommitOrderCalculator
*/
public function getCommitOrderCalculator()
{
return new Internal\CommitOrderCalculator();
}
/**
* Clears the UnitOfWork.
*
@@ -2760,6 +2817,7 @@ EXCEPTION
$this->pendingCollectionElementRemovals =
$this->visitedCollections =
$this->eagerLoadingEntities =
$this->eagerLoadingCollections =
$this->orphanRemovals = [];
} else {
Deprecation::triggerIfCalledFromOutside(
@@ -2949,6 +3007,10 @@ EXCEPTION
continue;
}
if (! isset($hints['fetchMode'][$class->name][$field])) {
$hints['fetchMode'][$class->name][$field] = $assoc['fetch'];
}
$targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
switch (true) {
@@ -3012,10 +3074,6 @@ EXCEPTION
break;
}
if (! isset($hints['fetchMode'][$class->name][$field])) {
$hints['fetchMode'][$class->name][$field] = $assoc['fetch'];
}
// Foreign key is set
// Check identity map first
// FIXME: Can break easily with composite keys if join column values are in
@@ -3058,7 +3116,9 @@ EXCEPTION
break;
// Deferred eager load only works for single identifier classes
case isset($hints[self::HINT_DEFEREAGERLOAD]) && ! $targetClass->isIdentifierComposite:
case isset($hints[self::HINT_DEFEREAGERLOAD]) &&
$hints[self::HINT_DEFEREAGERLOAD] &&
! $targetClass->isIdentifierComposite:
// TODO: Is there a faster approach?
$this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($normalizedAssociatedId);
@@ -3107,9 +3167,14 @@ EXCEPTION
$reflField = $class->reflFields[$field];
$reflField->setValue($entity, $pColl);
if ($assoc['fetch'] === ClassMetadata::FETCH_EAGER) {
$this->loadCollection($pColl);
$pColl->takeSnapshot();
if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER) {
$isIteration = isset($hints[Query::HINT_INTERNAL_ITERATION]) && $hints[Query::HINT_INTERNAL_ITERATION];
if (! $isIteration && $assoc['type'] === ClassMetadata::ONE_TO_MANY) {
$this->scheduleCollectionForBatchLoading($pColl, $class);
} elseif (($isIteration && $assoc['type'] === ClassMetadata::ONE_TO_MANY) || $assoc['type'] === ClassMetadata::MANY_TO_MANY) {
$this->loadCollection($pColl);
$pColl->takeSnapshot();
}
}
$this->originalEntityData[$oid][$field] = $pColl;
@@ -3126,7 +3191,7 @@ EXCEPTION
/** @return void */
public function triggerEagerLoads()
{
if (! $this->eagerLoadingEntities) {
if (! $this->eagerLoadingEntities && ! $this->eagerLoadingCollections) {
return;
}
@@ -3139,11 +3204,69 @@ EXCEPTION
continue;
}
$class = $this->em->getClassMetadata($entityName);
$class = $this->em->getClassMetadata($entityName);
$batches = array_chunk($ids, $this->em->getConfiguration()->getEagerFetchBatchSize());
$this->getEntityPersister($entityName)->loadAll(
array_combine($class->identifier, [array_values($ids)])
);
foreach ($batches as $batchedIds) {
$this->getEntityPersister($entityName)->loadAll(
array_combine($class->identifier, [$batchedIds])
);
}
}
$eagerLoadingCollections = $this->eagerLoadingCollections; // avoid recursion
$this->eagerLoadingCollections = [];
foreach ($eagerLoadingCollections as $group) {
$this->eagerLoadCollections($group['items'], $group['mapping']);
}
}
/**
* Load all data into the given collections, according to the specified mapping
*
* @param PersistentCollection[] $collections
* @param array<string, mixed> $mapping
* @psalm-param array{targetEntity: class-string, sourceEntity: class-string, mappedBy: string, indexBy: string|null} $mapping
*/
private function eagerLoadCollections(array $collections, array $mapping): void
{
$targetEntity = $mapping['targetEntity'];
$class = $this->em->getClassMetadata($mapping['sourceEntity']);
$mappedBy = $mapping['mappedBy'];
$batches = array_chunk($collections, $this->em->getConfiguration()->getEagerFetchBatchSize(), true);
foreach ($batches as $collectionBatch) {
$entities = [];
foreach ($collectionBatch as $collection) {
$entities[] = $collection->getOwner();
}
$found = $this->getEntityPersister($targetEntity)->loadAll([$mappedBy => $entities]);
$targetClass = $this->em->getClassMetadata($targetEntity);
$targetProperty = $targetClass->getReflectionProperty($mappedBy);
foreach ($found as $targetValue) {
$sourceEntity = $targetProperty->getValue($targetValue);
$id = $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($sourceEntity));
$idHash = implode(' ', $id);
if (isset($mapping['indexBy'])) {
$indexByProperty = $targetClass->getReflectionProperty($mapping['indexBy']);
$collectionBatch[$idHash]->hydrateSet($indexByProperty->getValue($targetValue), $targetValue);
} else {
$collectionBatch[$idHash]->add($targetValue);
}
}
}
foreach ($collections as $association) {
$association->setInitialized(true);
$association->takeSnapshot();
}
}
@@ -3174,6 +3297,33 @@ EXCEPTION
$collection->setInitialized(true);
}
/**
* Schedule this collection for batch loading at the end of the UnitOfWork
*/
private function scheduleCollectionForBatchLoading(PersistentCollection $collection, ClassMetadata $sourceClass): void
{
$mapping = $collection->getMapping();
$name = $mapping['sourceEntity'] . '#' . $mapping['fieldName'];
if (! isset($this->eagerLoadingCollections[$name])) {
$this->eagerLoadingCollections[$name] = [
'items' => [],
'mapping' => $mapping,
];
}
$owner = $collection->getOwner();
assert($owner !== null);
$id = $this->identifierFlattener->flattenIdentifier(
$sourceClass,
$sourceClass->getIdentifierValues($owner)
);
$idHash = implode(' ', $id);
$this->eagerLoadingCollections[$name]['items'][$idHash] = $collection;
}
/**
* Gets the identity map of the UnitOfWork.
*

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