Compare commits

...

239 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
Grégoire Paris
17500f56ea Merge pull request #10923 from kaznovac/patch-1
basic-mapping: fix new-line rendered in output
2023-08-27 20:21:56 +02:00
Marko Kaznovac
fc2f724e2d basic-mapping: fix new-line rendered in output 2023-08-27 19:17:04 +02:00
Óscar Martínez
7986fc64dd Verify UnitOfWork::HINT_DEFEREAGERLOAD exists and is true 2023-08-25 09:51:02 +02:00
Grégoire Paris
2f9e98754b Merge pull request #10915 from mpdude/post-events-later
Mitigate problems with `EntityManager::flush()` reentrance since 2.16.0 (Take 2)
2023-08-25 07:47:25 +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
Sergii Dolgushev
bb5524099c Use required classes for Lifecycle Callback examples (#10916)
* Use required classes for Lifecycle Callback examples

* Coding Style fixes

---------

Co-authored-by: Sergii Dolgushev <Sergii.Dolgushev@secondwaveds.com>
2023-08-23 22:47:15 +02:00
Grégoire Paris
3a8cafe228 Add space before backquote (#10918)
According to the RST docs,

> [inline markup] it must be separated from surrounding text by non-word
> characters. Use a backslash escaped space to work around that: thisis\ *one*\ word.

Because we were missing a space before backquotes here, the links were
not rendered. Escaping the space allow not to actually produce a space
in the output.

See https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#inline-markup
2023-08-23 21:42:35 +02:00
Matthias Pigulla
8259a16681 Mitigate problems with EntityManager::flush() reentrance since 2.16.0 (Take 2)
The changes from #10547, which landed in 2.16.0, cause problems for users calling `EntityManager::flush()` from within `postPersist` event listeners.

* When `UnitOfWork::commit()` is re-entered, the "inner" (reentrant) call will start working through all changesets. Eventually, it finishes with all insertions being performed and `UoW::$entityInsertions` being empty. After return, the entity insertion order, an array computed at the beginning of `UnitOfWork::executeInserts()`, still contains entities that now have been processed already. This leads to a strange-looking SQL error where the number of parameters used does not match the number of parameters bound. This has been reported as #10869.

* The fixes made to the commit order computation may lead to a different entity insertion order than previously. `postPersist` listener code may be affected by this when accessing generated IDs for other entities than the one the event has been dispatched for. This ID may not yet be available when the insertion order is different from the one that was used before 2.16. This has been mentioned in https://github.com/doctrine/orm/pull/10906#issuecomment-1682417987.

This PR suggests to address both issues by dispatching the `postPersist` event only after _all_ new entities have their rows inserted into the database. Likewise, dispatch `postRemove` only after _all_ deletions have been executed.

This solves the first issue because the sequence of insertions or deletions has been processed completely _before_ we start calling event listeners. This way, potential changes made by listeners will no longer be relevant.

Regarding the second issue, I think deferring `postPersist` a bit until _all_ entities have been inserted does not violate any promises given, hence is not a BC break. In 2.15, this event was raised after all insertions _for a particular class_ had been processed - so, it was never an "immediate" event for every single entity. #10547 moved the event handling to directly after every single insertion. Now, this PR moves it back a bit to after _all_ insertions.
2023-08-23 07:55:21 +02:00
David Arenas
5577d51c44 Add back throws annotation to getSingleScalarResult (#10907)
Fix regression introduced in #10870

`$result = $this->execute(null, $hydrationMode);` in `getSingleResult` can still throw NoResultException exception.
2023-08-13 13:01:30 +02:00
Eduardo Rocha
d1922a3065 Fix link on known issues docs (#10904) 2023-08-10 21:41:31 +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
597a63a86c PHPStan 1.10.28, Psalm 5.14.1 (#10895) 2023-08-09 15:05:08 +02:00
Alexander M. Turek
368eb01ac3 Merge 2.16.x into 2.17.x (#10894) 2023-08-09 11:46:12 +02:00
Bei Xiao
6b220e3c90 Fix return type of getSingleScalarResult (#10870) 2023-08-09 11:42:00 +02:00
Matthias Pigulla
6de4b68705 Use a dedicated exception for the check added in #10785 (#10881)
This adds a dedicated exception for the case that objects with colliding identities are to be put into the identity map.

Implements #10872.
2023-08-09 11:38:35 +02:00
Matthias Pigulla
16c0151831 Document more clearly that the insert order is an implementation detail (#10883) 2023-08-09 11:36:05 +02:00
Matthias Pigulla
440b244ebc Fix broken changeset computation for entities loaded through fetch=EAGER + using inheritance (#10884)
#10880 reports a case where the changes from #10785 cause entity updates to be missed.

Upon closer inspection, this change seems to be causing it:

https://github.com/doctrine/orm/pull/10785/files#diff-55a900494fc8033ab498c53929716caf0aa39d6bdd7058e7d256787a24412ee4L2990-L3003

The code was changed to use `registerManaged()` instead, which basically does the same things, but (since #10785) also includes an additional check against duplicate entity instances.

But, one detail slipped through tests and reviews: `registerManaged()` also updates `\Doctrine\ORM\UnitOfWork::$originalEntityData`, which is used to compute entity changesets. An empty array `[]` was passed for $data here.

This will make the changeset computation assume that a partial object was loaded and effectively ignore all field updates here:

a616914887/lib/Doctrine/ORM/UnitOfWork.php (L762-L764)

I think that, effectively, it is sufficient to call `registerManaged()` only in the two cases where a proxy was created.

Calling `registerManaged()` with `[]` as data for a proxy object is consistent with e. g. `\Doctrine\ORM\EntityManager::getReference()`.

In the case that a full entity has to be loaded, we need not call `registerManaged()` at all, since that will already happen inside `EntityManager::find()` (or, more specifically, `UnitOfWork::createEntity()` called inside it).

Note that the test case has to make some provisions so that we actually reach this case:
* Load an entity that uses `fetch="EAGER"` on a to-one association
* That association being against a class that uses inheritance (why's that?)
2023-08-09 11:34:53 +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
Matthias Pigulla
a616914887 Turn identity map collisions from exception to deprecation notice (#10878)
In #10785, a check was added that prevents entity instances from getting into the identity map when another object for the same ID is already being tracked.

This caused regressions for users that work with application-provided IDs and expect this condition to fail with `UniqueConstraintViolationExceptions` when flushing to the database.

Thus, this PR turns the exception into a deprecation notice. Users can opt-in to the new behavior. In 3.0, the exception will be used.

Implements #10871.
2023-08-04 14:06:02 +02:00
Dieter Beck
fd0bdc69b0 Add possibility to set reportFieldsWhereDeclared to true in ORMSetup (#10865)
Otherwise it is impossible to avoid a deprecation warning when using ORMSetup::createAttributeMetadataConfiguration()
2023-08-02 14:34:13 +02:00
Michael Olšavský
f50803ccb9 Fix UnitOfWork->originalEntityData is missing not-modified collections after computeChangeSet (#9301)
* Fix original data incomplete after flush

* Apply suggestions from code review

Co-authored-by: Alexander M. Turek <me@derrabus.de>

---------

Co-authored-by: Alexander M. Turek <me@derrabus.de>
2023-08-02 13:44:15 +02:00
Matthias Pigulla
eeefc6bc0f Add an UPGRADE notice about the potential changes in commit order (#10866) 2023-08-02 13:42:49 +02:00
Grégoire Paris
710dde83aa Update branch metadata (#10862) 2023-08-01 14:56:34 +02:00
Alexander M. Turek
ab542e97df Allow symfony/console 7 (#10724) 2023-08-01 14:25:03 +02:00
Grégoire Paris
495cd06b9a Merge pull request #10727 from derrabus/revert/symfony-7
Revert "Allow symfony/console 7"
2023-08-01 14:07:04 +02:00
Grégoire Paris
fd7a14ad22 Merge pull request #10547 from mpdude/commit-order-entity-level
Compute the commit order (inserts/deletes) on the entity level
2023-08-01 09:21:09 +02:00
Matthias Pigulla
0d3ce5d4f8 Exclude deprecated classes from Psalm checks (until 3.0) 2023-08-01 08:53:56 +02:00
Matthias Pigulla
f01d107edc Fix commit order computation for self-referencing entities with application-provided IDs
This excludes such associations from the commit order computation, since the foreign key constraint will be satisfied when inserting the row.

See https://github.com/doctrine/orm/pull/10735/ for more details about this edge case.
2023-08-01 08:43:16 +02:00
Alexander M. Turek
3cc30c4024 Merge branch '2.15.x' into 2.16.x
* 2.15.x:
  Fix static analysis
  Other solution
  Avoid self deprecation
  fix: use platform options instead of deprecated custom options (#10855)
2023-07-28 16:26:45 +02:00
Alexander M. Turek
d2de4ec03c Revert "Introduce FilterCollection#restore method (#10537)"
This reverts commit 8e20e1598e.
2023-07-28 16:08:17 +02:00
Vincent Langlet
64ee76e94e Introduce FilterCollection#restore method (#10537)
* Introduce FilterCollection#restore method

* Add suspend method instead

* Add more tests
2023-07-28 16:08:06 +02:00
Vincent Langlet
8e20e1598e Introduce FilterCollection#restore method (#10537)
* Introduce FilterCollection#restore method

* Add suspend method instead

* Add more tests
2023-07-28 16:05:51 +02:00
Grégoire Paris
24df74d61d Merge pull request #10856 from VincentLanglet/fixDeprecation
Fix/Self deprecation with getQueryCacheImpl
2023-07-26 23:41:39 +02:00
Vincent Langlet
442f073d25 Fix static analysis 2023-07-26 17:42:20 +02:00
Vincent Langlet
a5161e9485 Other solution 2023-07-26 16:00:37 +02:00
Vincent Langlet
ddc7d953b9 Avoid self deprecation 2023-07-26 12:37:42 +02:00
Kévin Dunglas
db51ed4f4c fix: use platform options instead of deprecated custom options (#10855) 2023-07-25 16:23:46 +02:00
Alexander M. Turek
6d27797b2e Merge release 2.15.4 into 2.16.x (#10850) 2023-07-23 23:49:30 +02:00
Nicolas Grekas
89250b8ca2 Use properties instead of getters to read property/class names via reflection (#10848) 2023-07-20 20:37:52 +02:00
Alexander M. Turek
bc61d7d21e Merge branch '2.15.x' into 2.16.x
* 2.15.x:
  PHPStan 1.10.25, Psalm 5.13.1 (#10842)
2023-07-16 23:40:09 +02:00
Nicolas Grekas
dca7ddf969 Decouple public API from Doctrine\Persistence\Proxy (#10832) 2023-07-15 10:55:35 +02:00
Grégoire Paris
e781639812 Merge pull request #10841 from doctrine/2.15.x 2023-07-12 15:54:17 +02:00
Grégoire Paris
b5987ad29a Merge pull request #10598 from opsway/fix-generated-for-joined-inheritance
Support not Insertable/Updateable columns for entities with `JOINED` inheritance type
2023-07-11 23:48:10 +02:00
Nicolas Grekas
8c513a6523 Cleanup psalm-type AutogenerateMode (#10833) 2023-07-11 23:01:02 +02:00
Alexandr Vronskiy
3b3056f910 mistake on final property 2023-07-11 09:38:34 +03:00
Alexandr Vronskiy
fa5c37e972 Add final to protected
Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>
2023-07-11 09:38:34 +03:00
Yevhen Vilkhovchenko
f3e36debfe Fix persist notInsertable|notUpdatable fields of root entity of joined inheritance type
1. Inherit ClassMetadataInfo::requiresFetchAfterChange flag from root entity when process parent columns mapping (see ClassMetadataInfo::addInheritedFieldMapping(), it uses same condition as ClassMetadataInfo::mapField()) so JoinedSubclassPersister::assignDefaultVersionAndUpsertableValues() to be called in JoinedSubclassPersister::executeInserts().
2. Override JoinedSubclassPersister::fetchVersionAndNotUpsertableValues() to fetch all parent tables (see $this->getJoinSql() call) generated columns. So make protected BasicEntityPersister::identifierFlattener stateless service (use it flattenIdentifier() method) and BasicEntityPersister::extractIdentifierTypes() (to avoid copy-paste).
3. JoinedSubclassPersister::fetchVersionAndNotUpsertableValues() doesnt check empty $columnNames because it would be an error if ClassMetadataInfo::requiresFetchAfterChange is true while no generated columns in inheritance hierarchy.
4. Initialize JoinedInheritanceRoot not-nullable string properties with insertable=false attribute to avoid attempt to insert default null data which cause error:
    PDOException: SQLSTATE[23502]: Not null violation: 7 ERROR:  null value in column "rootwritablecontent" of relation "joined_inheritance_root" violates not-null constraint
    DETAIL:  Failing row contains (5, null, dbDefault, dbDefault, , nonUpdatable).
  while $rootTableStmt->executeStatement() because JoinedSubclassPersister::getInsertColumnList() have no $insertData (prepared later) to decide is generated column provided by client code or not (so need to skip column)
2023-07-11 09:38:34 +03:00
Yevhen Vilkhovchenko
ca7abd04a2 Fix persist notInsertable|notUpdatable columns of entity with joined inheritance type
1. Postgres gives error when insert root entity ($rootTableStmt->executeStatement()) in JoinedSubclassPersister::executeInserts():
   PDOException: SQLSTATE[08P01]: <<Unknown error>>: 7 ERROR:  bind message supplies 4 parameters, but prepared statement "" requires 6
  so exclude notInsertable columns from JoinedSubclassPersister::getInsertColumnList() like it done in parent::prepareInsertData() which call BasicEntityPersister::prepareUpdateData(isInsert: true) where we have condition:
    if ($isInsert && isset($fieldMapping['notInsertable']))
2. Try to get generated (notInsertable|notUpdatable) column value on flush() with JoinedSubclassPersister::executeInserts() also fails:
    Unexpected empty result for database query.
  because method it calls $this->assignDefaultVersionAndUpsertableValues() after insert root entity row, while generated columns in child-entity table, so move call just after insert child row
3. Use option['default'] = 'dbDefault' in functional test entities, to emulate generated value on insert, but declare as generated = 'ALWAYS' for tests purpose (correctness of JoinedSubclassPersister::fetchVersionAndNotUpsertableValues() sql-query)
4. Use JoinedInheritanceRoot::rootField to skip JoinedSubclassPersister::update() optimization for empty changeset in updatable:false columns tests
2023-07-11 09:38:34 +03:00
Matthias Pigulla
a555626150 Improve and add test to set to-one and to-many associations with reference objects (#10799) 2023-07-11 00:07:02 +02:00
Matthias Pigulla
f26946b477 Add @deprecated annotations in addition to runtime deprecation notices 2023-07-08 16:22:55 +02:00
Matthias Pigulla
efc83bce8e Make it possible to have non-NULLable self-referencing associations when using application-provided IDs (#10735)
This change improves scheduling of extra updates in the `BasicEntityPersister`.

Extra updates can be avoided when
* the referred-to entity has already been inserted during the current insert batch/transaction
* we have a self-referencing entity with application-provided ID values (the `NONE` generator strategy).

As a corollary, with this change applications that provide their own IDs can define self-referencing associations as not NULLable.

I am considering this a bugfix since the ORM previously executed additional queries that were not strictly necessary, and that required users to work with NULLable columns where conceptually a non-NULLable column would be valid and more expressive.

One caveat, though:

In the absence of entity-level commit ordering (#10547), it is not guaranteed that entities with self-references (at the class level) will be inserted in a suitable order. The order depends on the sequence in which the entities were added with `persist()`.

Fixes #7877, closes #7882.

Co-authored-by: Sylvain Fabre <sylvain.fabre@assoconnect.com>
2023-07-08 15:53:54 +02:00
Grégoire Paris
81ddeb426c Merge pull request #10823 from nicolas-grekas/mergeup 2023-07-07 13:09:11 +02:00
Nicolas Grekas
42e63bf358 Merge branch 2.15.x into 2.16.x
* 2.15.x: (23 commits)
  Fix cloning entities when using lazy-ghost proxies
  Fixes recomputation of single entity change set when entity contains enum attributes. Due to the fact that originalEntityData contains enum objects and ReflectionEnumProperty::getValue() returns value of enum, comparison of values are always falsy, resulting to update columns value even though it has not changes.
  Fix code style issues
  Use absolute references
  Avoid colon followed by double colon
  Use correct syntax for references
  Fix invalid reference syntax
  Use rst syntax
  Use internal link
  Escape pipes
  Introduce new workflow to test docs
  Remove lone dash (#10812)
  Treat id field proprites same as regular field
  Move three "Ticket/"-style tests to the right namespace
  Follow recommendation about multiline type
  Fix unserialize() errors when running tests on PHP 8.3 (#10803)
  Explain `EntityManager::getReference()` peculiarities (#10800)
  Upgrade to Psalm 5.13
  test: assert `postLoad` has data first
  distinct() updates QueryBuilder state correctly
  ...
2023-07-07 12:55:31 +02:00
Matthias Pigulla
bb21865cba Deprecate classes related to old commit order computation 2023-07-04 14:30:29 +02:00
Matthias Pigulla
606da9280d Un-prefix simple generics like list<> and array<>
... as suggested in GH review.
2023-07-04 14:17:14 +02:00
Grégoire Paris
21708e43c4 Merge pull request #10785 from mpdude/guard-duplicate-identity-map-entries 2023-07-04 10:03:04 +02:00
Grégoire Paris
8eb69922e6 Merge pull request #10809 from mpdude/show-trouble-delete-before-insert
Add test to show why delete-before-insert may be challenging
2023-06-30 08:08:57 +02:00
Matthias Pigulla
e9b6fd89a4 Add test to show why delete-before-insert may be challenging
There are a few requests (#5742, #5368, #5109, #6776) that ask to change the order of operations in the UnitOfWork to perform "deletes before inserts", or where such a switch appears to solve a reported problem.

I don't want to say that this is not doable. But this PR at least adds two tricky examples where INSERTs need to be done before an UPDATE can refer to new database rows; and where the UPDATE needs to happen to release foreign key references to other entities before those can be DELETEd.

So, at least as long as all operations of a certain type are to be executed in blocks, this example allows no other order of operations than the current one.
2023-06-29 14:32:08 +02:00
Matthias Pigulla
01a14327d2 Add a safeguard against multiple objects competing for the same identity map entry
While trying to understand #3037, I found that it may happen that we have more entries in `\Doctrine\ORM\UnitOfWork::$entityIdentifiers` than in `\Doctrine\ORM\UnitOfWork::$identityMap`.

The former is a mapping from `spl_object_id` values to ID hashes, the latter an array first of entity class names and then from ID hash to entity object instances.

(Basically, "ID hash" is a concatenation of all field values making up the `@Id` for a given entity.)

This means that at some point, we must have _different_ objects representing the same entity, or at least over time different objects are used for the same entity without the UoW properly updating its `::$entityIdentifiers` structure.

I don't think it makes sense to overwrite an entity in the identity map, since that means a currently `MANAGED` entity is replaced with something else.

If it makes sense at all to _replace_ an entity, that should happen through dedicated management methods to first detach the old entity before persisting, merging or otherwise adding the new one. This way we could make sure the internal structures remain consistent.
2023-06-28 22:50:17 +02:00
Matthias Pigulla
2df1071e7a Remove references to the temporary branch in workflow definitions 2023-06-28 17:10:20 +02:00
Matthias Pigulla
7fc359c2bb Avoid unnecessarily passing entity lists into executeDeletions/executeInserts 2023-06-28 14:46:22 +02:00
Matthias Pigulla
853b9f75ae Merge remote-tracking branch 'origin/2.16.x' into commit-order-entity-level 2023-06-28 14:43:54 +02:00
Grégoire Paris
44d2a83e09 Merge pull request #10532 from mpdude/entity-persister-must-not-batch 2023-06-28 11:45:39 +02:00
Grégoire Paris
5f079c2061 Merge pull request #10531 from mpdude/commit-order-must-be-entity-level
Add test: Commit order calculation must happen on the entity level
2023-06-27 23:52:33 +02:00
Grégoire Paris
7ef4afc688 Merge pull request #10743 from mpdude/post-insert-id-early
Make EntityPersisters tell the UoW about post insert IDs early
2023-06-25 23:51:15 +02:00
Grégoire Paris
70bcff7410 Merge pull request #10794 from doctrine/2.15.x
Merge 2.15.x up into 2.16.x
2023-06-23 23:22:31 +02:00
Grégoire Paris
f778d8cf98 Merge pull request #10792 from greg0ire/entity-level-commit-order
Entity level commit order
2023-06-23 22:37:10 +02:00
Grégoire Paris
18b32ab9db Merge remote-tracking branch 'origin/2.15.x' into entity-level-commit-order 2023-06-23 22:21:53 +02:00
Matthias Pigulla
aa3ff458c7 Add test: Commit order calculation must happen on the entity level
Add tests for entity insertion and deletion that require the commit order calculation to happen on the entity level. This demonstrates the necessity for the changes in #10547.

This PR contains two tests with carefully constructed entity relationships, where we have a non-nullable `parent` foreign key relationships between entities stored in the same table.

Class diagram:

```mermaid
classDiagram
    direction LR
    class A
    class B
    A --> B : parent
    B --|> A
```

Object graph:

```mermaid
graph LR;
    b1 --> b2;
    b2 --> a;
    b3 --> b2;
```

 #### Situation before #10547

The commit order is computed by looking at the associations at the _table_ (= _class_) level. Once the ordering of _tables_ has been found, entities being mapped to the same table will be processed in the order they were given to `persist()` or `remove()`.

That means only a particular ordering of `persist()` or `remove()` calls (see comment in the test) works:

For inserts, the order must be `$a, $b2, $b1, $b3` (or `... $b3, $b1`), for deletions `$b1, $b3, $b2, $a`.

 #### Situation with entity-level commit order computation (as in #10547)

The ORM computes the commit order by considering associations at the _entity_ level. It will be able to find a working order by itself.
2023-06-23 22:14:04 +02:00
Matthias Pigulla
8bc74c624a Make EntityPersisters tell the UoW about post insert IDs early
This refactoring does two things:

* We can avoid collecting the post insert IDs in a cumbersome array structure that will be returned by the EntityPersisters and processed by the UoW right after. Instead, use a more expressive API: Make the EntityPersisters tell the UoW about the IDs immediately.
* IDs will be available in inserted entities a tad sooner. That may help to resolve #10735, where we can use the IDs to skip extra updates.
2023-06-23 09:08:07 +02:00
Grégoire Paris
6c0a5ecbf9 Merge pull request #10787 from doctrine/2.15.x-merge-up-into-2.16.x_XV3tWpNu 2023-06-22 16:01:49 +02:00
Grégoire Paris
5f6501f842 Merge remote-tracking branch 'origin/2.15.x' into 2.15.x-merge-up-into-2.16.x_XV3tWpNu 2023-06-22 14:44:27 +02:00
Grégoire Paris
41f704cd96 Merge pull request #10775 from doctrine/2.15.x
Merge 2.15.x up into 2.16.x
2023-06-13 20:10:31 +02:00
Alexander M. Turek
5c74795893 Merge 2.15.x into 2.16.x (#10765) 2023-06-06 11:30:27 +02:00
Grégoire Paris
8961bfe90c Merge pull request #10756 from doctrine/2.15.x
Merge 2.15.x up into 2.16.x
2023-06-05 09:11:52 +02:00
Grégoire Paris
15e3a7e861 Merge pull request #10751 from mpdude/7180-fixed
Add test to show #7180 has been fixed
2023-06-05 08:35:40 +02:00
Grégoire Paris
1280e005b6 Merge pull request #10755 from mpdude/9192-fixed
Add test to show #9192 has been fixed
2023-06-04 22:08:17 +02:00
Matthias Pigulla
79f53d5dae Add test to show #9192 has been fixed
This test implements the situation described in #9192. The commit order computation will be fixed by #10547.
2023-06-04 19:46:16 +00:00
Matthias Pigulla
bf2937e63a Add test: Entity insertions must not happen table-wise
Add tests for entity insertion and deletion that require writes to different tables in an interleaved fashion, and that have to re-visit a particular table.

 #### Background

In #10531, I've given an example where it is necessary to compute the commit order on the entity (instead of table) level.

Taking a closer look at the UoW to see how this could be achieved, I noticed that the current, table-level commit order manifests itself also in the API between the UoW and `EntityPersister`s.

 #### Current situation

The UoW computes the commit order on the table level. All entity insertions for a particular table are passed through `EntityPersister::addInsert()` and finally written through `EntityPersister::executeInserts()`.

 #### Suggested change

The test in this PR contains a carefully constructed set of four entities. Two of them are of the same class (are written to the same table), but require other entities to be processed first.

In order to be able to insert this set of entities, the ORM must be able to perform inserts for a given table repeatedly, interleaved with writing other entities to their respective tables.
2023-06-03 13:07:41 +00:00
Grégoire Paris
ed212ab924 Merge pull request #10749 from mpdude/6499-fixed
Add tests to show #6499 has been fixed
2023-06-02 23:36:44 +02:00
Matthias Pigulla
dd0e02e912 Add test to show #7180 has been fixed
Tests suggested in https://github.com/doctrine/orm/pull/7180#issuecomment-380841413 and https://github.com/doctrine/orm/pull/7180#issuecomment-381067448 by @arnaud-lb.

Co-authored-by: Arnaud Le Blanc <arnaud.lb@gmail.com>
2023-06-02 21:28:11 +00:00
Matthias Pigulla
aad875eea1 Add tests to show #6499 has been fixed
@frikkle was the first to propose these tests in #6533.
@rvanlaak followed up in #8703, making some adjustments.

Co-authored-by: Gabe van der Weijde <gabe.vanderweijde@triasinformatica.nl>
Co-authored-by: Richard van Laak <rvanlaak@gmail.com>
Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>
2023-06-02 21:14:17 +00:00
Grégoire Paris
84ab535e56 Merge pull request #10750 from mpdude/7006-fixed
Add test to show #7006 has been fixed
2023-06-02 22:07:47 +02:00
Matthias Pigulla
a72a0c3597 Update tests/Doctrine/Tests/ORM/Functional/Ticket/GH7006Test.php
Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>
2023-06-02 22:00:20 +02:00
Matthias Pigulla
6217285544 Add test to show #7006 has been fixed 2023-06-02 17:52:51 +00:00
Grégoire Paris
330c0bc67e Merge pull request #10689 from mpdude/insert-entity-level-topo-sort 2023-06-02 15:21:08 +02:00
Grégoire Paris
b5595ca041 Merge pull request #10732 from mpdude/10348-fixed
Add a test case to show #10348 has been fixed
2023-06-02 07:50:03 +02:00
Grégoire Paris
b779b112f3 Merge pull request #10738 from doctrine/2.15.x-merge-up-into-2.16.x_PvK1bbO1 2023-06-01 11:45:08 +02:00
Matthias Pigulla
04e08640fb Add a test case to show #10348 has been fixed by #10566
This is part of the series of issues fixed by #10547. In particular, the changes from #10566 were relevant.

See #10348 for the bug description.

Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>
2023-05-31 21:38:42 +00:00
Matthias Pigulla
aa27b3a35f Remove the now unused UnitOfWork::getCommitOrder() method 2023-05-31 07:31:56 +00:00
Matthias Pigulla
d76fc4ebf6 Compute entity-level commit order for entity insertions
This is the third step to break https://github.com/doctrine/orm/pull/10547 into smaller PRs suitable for reviewing. It uses the new topological sort implementation from #10592 and the refactoring from #10651 to compute the UoW's commit order for entity insertions not on the entity class level, but for single entities and their actual dependencies instead.

 #### Current situation

`UnitOfWork::getCommitOrder()` would compute the entity sequence on the class level with the following code:

70477d81e9/lib/Doctrine/ORM/UnitOfWork.php (L1310-L1325)

 #### Suggested change

* Instead of considering the classes of all entities that need to be inserted, updated or deleted, consider the new (inserted) entities only. We only need to find a sequence in situations where there are foreign key relationships between two _new_ entities.
* In the dependency graph, add edges for all to-one association target entities.
* Make edges "optional" when the association is nullable.

 #### Test changes

I have not tried to fully understand the few changes necessary to fix the tests. My guess is that those are edge cases where the insert order changed and we need to consider this during clean-up.

Keep in mind that many of the functional tests we have assume that entities have IDs assigned in the order that they were added to the EntityManager. That does not change – so the order of entities is generally stable, equal to the previous implementation.
2023-05-31 07:16:16 +00:00
Matthias Pigulla
ae60cf005f Commit order for removals has to consider SET NULL, not nullable (#10566)
When computing the commit order for entity removals, we have to look out for `@ORM\JoinColumn(onDelete="SET NULL")` to find places where cyclic associations can be broken.

 #### Background

The UoW computes a "commit order" to find the sequence in which tables shall be processed when inserting entities into the database or performing delete operations.

For the insert case, the ORM is able to schedule _extra updates_ that will be performed after all entities have been inserted. Associations which are configured as `@ORM\JoinColumn(nullable=true, ...)` can be left as `NULL` in the database when performing the initial `INSERT` statements, and will be updated once all new entities have been written to the database. This can be used to break cyclic associations between entity instances.

For removals, the ORM does not currently implement up-front `UPDATE` statements to `NULL` out associations before `DELETE` statements are executed. That means when associations form a cycle, users have to configure `@ORM\JoinColumn(onDelete="SET NULL", ...)` on one of the associations involved. This transfers responsibility to the DBMS to break the cycle at that place.

_But_, we still have to perform the delete statements in an order that makes this happen early enough. This may be a _different_ order than the one required for the insert case. We can find it _only_ by looking at the `onDelete` behaviour. We must ignore the `nullable` property, which is irrelevant, since we do not even try to `NULL` anything.

 #### Example

Assume three entity classes `A`, `B`, `C`. There are unidirectional one-to-one associations `A -> B`, `B -> C`, `C -> A`. All those associations are `nullable= true`.

Three entities `$a`, `$b`, `$c` are created from these respective classes and associations are set up.

All operations `cascade` at the ORM level. So we can test what happens when we start the operations at the three individual entities, but in the end, they will always involve all three of them.

_Any_ insert order will work, so the improvements necessary to solve #10531 or #10532 are not needed here. Since all associations are between different tables, the current table-level computation is good enough.

For the removal case, only the `A -> B` association has `onDelete="SET NULL"`. So, the only possible execution order is `$b`, `$c`, `$a`. We have to find that regardless of where we start the cascade operation.

The DBMS will set the `A -> B` association on `$a` to `NULL` when we remove `$b`. We can then remove `$c` since it is no longer being referred to, then `$a`.

 #### Related cases

These cases ask for the ORM to perform the extra update before the delete by itself, without DBMS-level support:
* #5665
* #10548
2023-05-30 22:58:36 +02:00
Alexander M. Turek
ddd3066bc4 Revert "Allow symfony/console 7 (#10724)"
This reverts commit 6662195936.
2023-05-25 08:48:53 +02:00
Alexander M. Turek
6662195936 Allow symfony/console 7 (#10724) 2023-05-25 08:47:35 +02:00
Grégoire Paris
d951aa05b9 Merge pull request #10721 from phansys/query_single_result 2023-05-23 11:33:49 +02:00
Javier Spagnoletti
be297b9fd3 Narrow return types for AbstractQuery::getSingleScalarResult() 2023-05-23 05:47:39 -03:00
Grégoire Paris
4e137f77a5 Merge pull request #10704 from greg0ire/drop-useless-block
Remove unreachable piece of code
2023-05-16 08:19:42 +02:00
Grégoire Paris
a33aa15c79 Remove unreachable piece of code
mappedBy is never defined on an inverse relationship.
Bi-directional self-referencing should IMO still result in 2 separate
associations, on 2 separate fields of the same class, like mentor or
mentee.
2023-05-15 14:10:22 +02:00
Grégoire Paris
255ce51526 Merge pull request #10703 from doctrine/2.15.x 2023-05-15 12:05:48 +02:00
Grégoire Paris
38e47fdeab Merge pull request #10455 from mpdude/fix-mapping-driver-load
Make Annotations/Attribute mapping drivers report fields for the classes where they are declared
2023-05-08 15:46:54 +02:00
Matthias Pigulla
9ac063d879 Add a new topological sort implementation (#10592)
This is the first chunk to break #10547 into smaller PRs suitable for reviewing. It adds a new topological sort implementation.

 #### Background

Topological sort is an algorithm that sorts the vertices of a directed acyclic graph (DAG) in a linear order such that for every directed edge from vertex A to vertex B, vertex A comes before vertex B in the ordering. This ordering is called a topological order.

Ultimately (beyond the scope of this PR), in the ORM we'll need this to find an order in which we can insert new entities into the database. When one entity needs to refer to another one by means of a foreign key, the referred-to entity must be inserted before the referring entity. Deleting entities is similar.

A topological sorting can be obtained by running a depth first search (DFS) on the graph. The order in which the DFS finishes on the vertices is a topological order. The DFS is possible iif there are no cycles in the graph. When there are cycles, the DFS will find them.

For more information about topological sorting, as well as a description of an DFS-based topological sorting algorithm, see https://en.wikipedia.org/wiki/Topological_sorting.

 #### Current situation

There is a DFS-based topological sorting implemented in the `CommitOrderCalculator`. This implementation has two kinks:

1. It does not detect cycles

When there is a cycle in the DAG that cannot be resolved, we need to know about it. Ultimately, this means we will not be able to insert entities into the database in any order that allows all foreign keys constraints to be satisfied.

If you look at `CommitOrderCalculator`, you'll see that there is no code dealing with this situation.

2. It has an obscure concept of edge "weights"

To me, it is not clear how those are supposed to work. The weights are related to whether a foreign key is nullable or not, but can (could) be arbitrary integers. An edge will be ignored if it has a higher (lower) weight than another, already processed edge... 🤷🏻?

 #### Suggested change

In fact, when inserting entities into the database, we have two kinds of foreign key relationships: Those that are `nullable`, and those that are not.

Non-nullable foreign keys are hard requirements: Referred-to entities must be inserted first, no matter what. These are "non-optional" edges in the dependency graph.

Nullable foreign keys can be set to `NULL` when first inserting an entity, and then coming back and updating the foreign key value after the referred-to (related) entity has been inserted into the database. This is already implemented in `\Doctrine\ORM\UnitOfWork::scheduleExtraUpdate`, at the expense of performing one extra `UPDATE` query after all the `INSERT`s have been processed. These edges are "optional".

When finding a cycle that consists of non-optional edges only, treat it as a failure. We won't be able to insert entities with a circular dependency when all foreign keys are non-NULLable.

When a cycle contains at least one optional edge, we can use it to break the cycle: Use backtracking to go back to the point before traversing the last _optional_ edge. This omits the edge from the topological sort order, but will cost one extra UPDATE later on.

To make the transition easier, the new implementation is provided as a separate class, which is marked as `@internal`.

 #### Outlook

Backtracking to the last optional edge is the simplest solution for now. In general, it might be better to find _another_ (optional) edge that would also break the cycle, if that edge is also part of _other_ cycles.

Remember, every optional edge skipped costs an extra UPDATE query later on. The underlying problem is known as the "minimum feedback arc set" problem, and is not easily/efficiently solvable. Thus, for the time being, picking the nearest optional edge seems to be reasonable.
2023-05-08 13:30:07 +02:00
Matthias Pigulla
b52a8f8b9e Make Annotations/Attribute mapping drivers report fields for the classes where they are declared
This PR will make the annotations and attribute mapping drivers report mapping configuration for the classes where it is declared, instead of omitting it and reporting it for subclasses only. This is necessary to be able to catch mis-configurations in `ClassMetadataFactory`.

Fixes #10417, closes #10450, closes #10454.

#### ⚠️ Summary for users getting `MappingExceptions` with the new mode

When you set the `$reportFieldsWhereDeclared` constructor parameters to `true` for the AnnotationDriver and/or AttributesDriver and get `MappingExceptions`, you may be doing one of the following:

* Using `private` fields with the same name in different classes of an entity inheritance hierarchy (see #10450)
* Redeclaring/overwriting mapped properties inherited from mapped superclasses and/or other entities (see #10454)

As explained in these two PRs, the ORM cannot (or at least, was not designed to) support such configurations. Unfortunately, due to the old – now deprecated – driver behaviour, the misconfigurations could not be detected, and due to previously missing tests, this in turn was not noticed.

#### Current situation

The annotations mapping driver has the following condition to skip properties that are reported by the PHP reflection API:

69c7791ba2/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php (L345-L357)

This code has been there basically unchanged since the initial 2.0 release. The same condition can be found in the attribute driver, probably it has been copied when attributes were added.

I _think_ what the driver tries to do here is to deal with the fact that Reflection will also report `public`/`protected` properties inherited from parent classes. This is supported by the observation (see #5744) that e. g. YAML and XML drivers do not contain this logic.

The conditions are not precise enough for edge cases. They lead to some fields and configuration values not even being reported by the driver. 

Only since the fields would be "discovered" again when reflecting on subclasses, they eventually end up in class metadata structures for the subclasses. In one case of inherited ID generator mappings, the `ClassMetadataFactory` would also rely on this behaviour.

Two potential bugs that can result from this are demonstrated in #10450 and #10454.

#### Suggested solution

In order to find a more reliable way of separating properties that are merely reported again in subclasses from those that are actual re-declarations, use the information already available in `ClassMetadata`. In particular, `declared` tells us in which non-transient class a "field" was first seen.

Make the mapping driver skip only those properties for which we already know that they have been declared in parent classes, and skip them only when the observed declaring class matches the expectation. 

For all other properties, report them to `ClassMetadataFactory` and let that deal with consistency checking/error handling.

#10450 and #10454 are merged into this PR to show that they pass now.

#### Soft deprecation strategy

To avoid throwing new/surprising exceptions (even for misconfigurations) during a minor version upgrade, the new driver mode is opt-in.

Users will have to set the `$reportFieldsWhereDeclared` constructor parameters to `true` for the `AnnotationDriver` and/or `AttributesDriver`. Unless they do so, a deprecation warning will be raised.

In 3.0, the "new" mode will become the default. The constructor parameter can be deprecated (as of ORM 3.1, probably) and is a no-op.

We need to follow up in other places (DoctrineBundle, ... – what else?) to make this driver parameter an easy-to-change configuration setting.
2023-05-08 11:23:28 +00:00
Grégoire Paris
1b2771f964 Merge pull request #10685 from doctrine/2.15.x-merge-up-into-2.16.x_vNvlgHpG
Merge release 2.15.1 into 2.16.x
2023-05-07 21:06:56 +02:00
Grégoire Paris
9766b6b03e Merge pull request #10651 from mpdude/unit-of-work-schedule-single-entities
Prepare entity-level commit order computation in the `UnitOfWork`
2023-05-05 20:43:23 +02:00
Grégoire Paris
37c8953015 Merge pull request #10678 from doctrine/2.15.x
Merge 2.15.x up into 2.16.x
2023-05-05 08:25:43 +02:00
Grégoire Paris
a199ca3002 Merge pull request #10676 from doctrine/2.15.x
Merge 2.15.x up into 2.16.x
2023-05-04 19:35:13 +02:00
Matthias Pigulla
ed34327941 More precisely state conditions for the postPersist event 2023-04-25 09:48:41 +02:00
Matthias Pigulla
b42cf99402 Prepare entity-level commit order computation in the UnitOfWork
This is the second chunk to break #10547 into smaller PRs suitable for reviewing. It prepares the `UnitOfWork` to work with a commit order computed on the entity level, as opposed to a class-based ordering as before.

#### Background

#10531 and #10532 show that it is not always possible to run `UnitOfWork::commit()` with a commit order given in terms of  entity _classes_. Instead it is necessary to process entities in an order computed on the _object_ level. That may include

* a particular order for multiple entities of the _same_ class
* a particular order for entities of _different_ classes, possibly even going back and forth (entity `$a1` of class `A`, then `$b` of class `B`, then `$a2` of class `A` – revisiting that class).

This PR is about preparing the `UnitOfWork` so that its methods will be able to perform inserts and deletions on that level of granularity. Subsequent PRs will deal with implementing that particular order computation.

#### Suggested change

Change the private `executeInserts` and `executeDeletions` methods so that they do not take a `ClassMetadata` identifying the class of entities that shall be processed, but pass them the list of entities directly.

The lists of entities are built in two dedicated methods. That happens basically as previously, by iterating over `$this->entityInsertions` or `$this->entityDeletions` and filtering those by class.

#### Potential (BC breaking?) changes that need review scrutiny

* `\Doctrine\ORM\Persisters\Entity\EntityPersister::addInsert()` was previously called multiple times, before the insert would be performed by a call to `\Doctrine\ORM\Persisters\Entity\EntityPersister::executeInserts()`. With the changes here, this batching effectively no longer takes place. `executeInserts()` will always see one entity/insert only, and there may be multiple `executeInserts()` calls during a single `UoW::commit()` phase.
* The caching in `\Doctrine\ORM\Cache\Persister\Entity\AbstractEntityPersister` previously would cache entities from the last executed insert batch only. Now it will collect entities across multiple batches. I don't know if that poses a problem.
* Overhead in `\Doctrine\ORM\Persisters\Entity\BasicEntityPersister::executeInserts` is incurred multiple times; that may, however, only be about SQL statement preparation and might be salvageable.
* The `postPersist` event previously was not dispatched before _all_ instances of a particular entity class had been written to the database. Now, it will be dispatched immediately after every single entity that has been inserted.
2023-04-24 13:32:09 +02:00
Grégoire Paris
33a19f1bf3 Merge pull request #10593 from mpdude/workflows
Run workflows also for the `entity-level-commit-order` branch
2023-03-23 09:15:33 +01:00
Matthias Pigulla
b6669746b7 Run workflows also for the entity-level-commit-order branch 2023-03-23 08:01:15 +00:00
263 changed files with 8743 additions and 1744 deletions

View File

@@ -11,21 +11,29 @@
"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",
"current": true
},
{
"name": "2.16",
"branchName": "2.16.x",
"slug": "2.16",
"upcoming": true
"maintained": false
},
{
"name": "2.15",
"branchName": "2.15.x",
"slug": "2.15",
"current": true,
"aliases": [
"current",
"stable"
]
"maintained": false
},
{
"name": "2.14",

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,3 +1,108 @@
# 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.
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
of application-provided IDs.
In ORM 2.16.0, the check was added by throwing an exception. In ORM 2.16.1, this will be
changed to a deprecation notice. ORM 3.0 will make it an exception again. Use
`\Doctrine\ORM\Configuration::setRejectIdCollisionInIdentityMap()` if you want to opt-in
to the new mode.
## Potential changes to the order in which `INSERT`s are executed
In https://github.com/doctrine/orm/pull/10547, the commit order computation was improved
to fix a series of bugs where a correct (working) commit order was previously not found.
Also, the new computation may get away with fewer queries being executed: By inserting
referred-to entities first and using their ID values for foreign key fields in subsequent
`INSERT` statements, additional `UPDATE` statements that were previously necessary can be
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 returning post insert IDs from `EntityPersister::executeInserts()`
Persisters implementing `\Doctrine\ORM\Persisters\Entity\EntityPersister` should no longer
return an array of post insert IDs from their `::executeInserts()` method. Make the
persister call `Doctrine\ORM\UnitOfWork::assignPostInsertId()` instead.
## Changing the way how reflection-based mapping drivers report fields, deprecated the "old" mode
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
`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,
only the new mode will be available.
# Upgrade to 2.15
## Deprecated configuring `JoinColumn` on the inverse side of one-to-one associations
@@ -15,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.25",
"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.13.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

@@ -13,7 +13,7 @@ for all our domain objects.
.. note::
The notify change tracking policy is deprecated and will be removed in ORM 3.0.
(`Details <https://github.com/doctrine/orm/issues/8383>`_)
(\ `Details <https://github.com/doctrine/orm/issues/8383>`_)
Implementing NotifyPropertyChanged
----------------------------------

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

@@ -58,6 +58,10 @@ First Attributes:
.. code-block:: php
<?php
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\HasLifecycleCallbacks;
use Doctrine\ORM\Mapping\PrePersist;
use Doctrine\ORM\Mapping\PreUpdate;
#[Entity]
#[HasLifecycleCallbacks]

View File

@@ -29,7 +29,7 @@ steps of configuration.
$config = new Configuration;
$config->setMetadataCache($metadataCache);
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities']);
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true);
$config->setMetadataDriverImpl($driverImpl);
$config->setQueryCache($queryCache);
$config->setProxyDir('/path/to/myproject/lib/MyProject/Proxies');
@@ -134,7 +134,7 @@ The attribute driver can be injected in the ``Doctrine\ORM\Configuration``:
<?php
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities']);
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true);
$config->setMetadataDriverImpl($driverImpl);
The path information to the entities is required for the attribute
@@ -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

@@ -47,8 +47,7 @@ mapping metadata:
- :doc:`Attributes <attributes-reference>`
- :doc:`XML <xml-mapping>`
- :doc:`PHP code <php-mapping>`
- :doc:`Docblock Annotations <annotations-reference>` (deprecated and
will be removed in ``doctrine/orm`` 3.0)
- :doc:`Docblock Annotations <annotations-reference>` (deprecated and will be removed in ``doctrine/orm`` 3.0)
- :doc:`YAML <yaml-mapping>` (deprecated and will be removed in ``doctrine/orm`` 3.0.)
This manual will usually show mapping metadata via attributes, though
@@ -423,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:
@@ -442,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

@@ -63,7 +63,7 @@ Notify
.. note::
The notify change tracking policy is deprecated and will be removed in ORM 3.0.
(`Details <https://github.com/doctrine/orm/issues/8383>`_)
(\ `Details <https://github.com/doctrine/orm/issues/8383>`_)
This policy is based on the assumption that the entities notify
interested listeners of changes to their properties. For that

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
@@ -1425,7 +1430,7 @@ userland:
reloading this data. Partially loaded objects have to be passed to
``EntityManager::refresh()`` if they are to be reloaded fully from
the database. This query hint is deprecated and will be removed
in the future (`Details <https://github.com/doctrine/orm/issues/8471>`_)
in the future (\ `Details <https://github.com/doctrine/orm/issues/8471>`_)
- ``Query::HINT_REFRESH`` - This query is used internally by
``EntityManager::refresh()`` and can be used in userland as well.
If you specify this hint and a query returns the data for an entity
@@ -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
~~~~~~~~~~~~~~~~~
@@ -215,6 +228,10 @@ specific to a particular entity class's lifecycle.
<?php
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Event\PrePersistEventArgs;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\HasLifecycleCallbacks;
use Doctrine\ORM\Mapping\PrePersist;
use Doctrine\ORM\Mapping\PreUpdate;
#[Entity]
#[HasLifecycleCallbacks]
@@ -453,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.
@@ -696,22 +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.
- The ``postPersist`` event occurs for an entity after
the entity has been made persistent. It will be invoked after the
database insert operations. Generated primary key values are
available in the postPersist event.
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, 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 the database
delete operations. It is not called for a DQL ``DELETE`` statement.
entity has been deleted. It will be invoked after all database
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. 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::

View File

@@ -93,3 +93,34 @@ object.
want to refresh or reload an object after having modified a filter or the
FilterCollection, then you should clear the EntityManager and re-fetch your
entities, having the new rules for filtering applied.
Suspending/Restoring Filters
----------------------------
When a filter is disabled, the instance is fully deleted and all the filter
parameters previously set are lost. Then, if you enable it again, a new filter
is created without the previous filter parameters. If you want to keep a filter
(in order to use it later) but temporary disable it, you'll need to use the
``FilterCollection#suspend($name)`` and ``FilterCollection#restore($name)``
methods instead.
.. code-block:: php
<?php
$filter = $em->getFilters()->enable("locale");
$filter->setParameter('locale', 'en');
// Temporary suspend the filter
$filter = $em->getFilters()->suspend("locale");
// Do things
// Then restore it, the locale parameter will still be set
$filter = $em->getFilters()->restore("locale");
.. warning::
If you enable a previously disabled filter, doctrine will create a new
one without keeping any of the previously parameter set with
``SQLFilter#setParameter()`` or ``SQLFilter#getParameterList()``. If you
want to restore the previously disabled filter instead, you must use the
``FilterCollection#restore($name)`` method.

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.
@@ -167,7 +167,7 @@ have produced, this is probably fine.
However, to mention known limitations, it is currently not possible to use "class"
level `annotations <https://github.com/doctrine/orm/pull/1517>`_ or
`attributes <https://github.com/doctrine/orm/issues/8868>` on traits, and attempts to
`attributes <https://github.com/doctrine/orm/issues/8868>`_ on traits, and attempts to
improve parser support for traits as `here <https://github.com/doctrine/annotations/pull/102>`_
or `there <https://github.com/doctrine/annotations/pull/63>`_ have been abandoned
due to complexity.
@@ -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

@@ -6,7 +6,7 @@ Partial Objects
Creating Partial Objects through DQL is deprecated and
will be removed in the future, use data transfer object
support in DQL instead. (`Details
support in DQL instead. (\ `Details
<https://github.com/doctrine/orm/issues/8471>`_)
A partial object is an object whose state is not fully initialized

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

@@ -37,8 +37,8 @@ will still end up with the same reference:
public function testIdentityMapReference(): void
{
$objectA = $this->entityManager->getReference('EntityName', 1);
// check for proxyinterface
$this->assertInstanceOf('Doctrine\Persistence\Proxy', $objectA);
// check entity is not initialized
$this->assertTrue($this->entityManager->isUninitializedObject($objectA));
$objectB = $this->entityManager->find('EntityName', 1);
@@ -137,7 +137,7 @@ optimize the performance of the Flush Operation:
.. note::
Flush only a single entity with ``$entityManager->flush($entity)`` is deprecated and will be removed in ORM 3.0.
(`Details <https://github.com/doctrine/orm/issues/8459>`_)
(\ `Details <https://github.com/doctrine/orm/issues/8459>`_)
Query Internals
---------------

View File

@@ -192,6 +192,11 @@ be properly synchronized with the database when
database in the most efficient way and a single, short transaction,
taking care of maintaining referential integrity.
.. note::
Do not make any assumptions in your code about the number of queries
it takes to flush changes, about the ordering of ``INSERT``, ``UPDATE``
and ``DELETE`` queries or the order in which entities will be processed.
Example:
@@ -777,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

@@ -18,7 +18,7 @@ before. There are some prerequisites for the tutorial that have to be
installed:
- PHP (latest stable version)
- Composer Package Manager (`Install Composer
- Composer Package Manager (\ `Install Composer
<https://getcomposer.org/doc/00-intro.md>`_)
The code of this tutorial is `available on Github <https://github.com/doctrine/doctrine2-orm-tutorial>`_.
@@ -321,7 +321,7 @@ data in your storage, and later in your application when the data is loaded agai
.. note::
This method, although very common, is inappropriate for Domain Driven
Design (`DDD <https://en.wikipedia.org/wiki/Domain-driven_design>`_)
Design (\ `DDD <https://en.wikipedia.org/wiki/Domain-driven_design>`_)
where methods should represent real business operations and not simple
property change, And business invariants should be maintained both in the
application state (entities in this case) and in the database, with no
@@ -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) {
@@ -1010,7 +1010,7 @@ abstract class AbstractQuery
*
* Alias for getSingleResult(HYDRATE_SINGLE_SCALAR).
*
* @return mixed The scalar result.
* @return bool|float|int|string|null The scalar result.
*
* @throws NoResultException If the query returned no result.
* @throws NonUniqueResultException If the query result is not unique.

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

@@ -16,7 +16,6 @@ use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\UnitOfWork;
use Doctrine\Persistence\Proxy;
use function array_map;
use function array_shift;
@@ -345,7 +344,7 @@ class DefaultQueryCache implements QueryCache
$assocIdentifier = $this->uow->getEntityIdentifier($assocValue);
$entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier);
if (! $assocValue instanceof Proxy && ($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
if (! $this->uow->isUninitializedObject($assocValue) && ($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
// Entity put fail
if (! $assocPersister->storeEntityCache($assocValue, $entityKey)) {
return null;

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,8 +20,10 @@ 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;
use function assert;
use function serialize;
use function sha1;
@@ -189,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);
@@ -314,7 +315,13 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
*/
public function executeInserts()
{
$this->queuedCache['insert'] = $this->persister->getInserts();
// The commit order/foreign key relationships may make it necessary that multiple calls to executeInsert()
// are performed, so collect all the new entities.
$newInserts = $this->persister->getInserts();
if ($newInserts) {
$this->queuedCache['insert'] = array_merge($this->queuedCache['insert'] ?? [], $newInserts);
}
return $this->persister->executeInserts();
}
@@ -431,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;
@@ -62,14 +64,27 @@ use function trim;
* It combines all configuration options from DBAL & ORM.
*
* Internal note: When adding a new configuration option just write a getter/setter pair.
*
* @psalm-import-type AutogenerateMode from ProxyFactory
*/
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.
*
@@ -95,8 +110,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
/**
* Gets the strategy for automatically generating proxy classes.
*
* @return int Possible values are constants of Doctrine\ORM\Proxy\ProxyFactory.
* @psalm-return AutogenerateMode
* @return ProxyFactory::AUTOGENERATE_*
*/
public function getAutoGenerateProxyClasses()
{
@@ -106,9 +120,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
/**
* Sets the strategy for automatically generating proxy classes.
*
* @param bool|int $autoGenerate Possible values are constants of Doctrine\ORM\Proxy\ProxyFactory.
* @psalm-param bool|AutogenerateMode $autoGenerate
* True is converted to AUTOGENERATE_ALWAYS, false to AUTOGENERATE_NEVER.
* @param bool|ProxyFactory::AUTOGENERATE_* $autoGenerate True is converted to AUTOGENERATE_ALWAYS, false to AUTOGENERATE_NEVER.
*
* @return void
*/
@@ -164,7 +176,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
*
* @return AnnotationDriver
*/
public function newDefaultAnnotationDriver($paths = [], $useSimpleAnnotationReader = true)
public function newDefaultAnnotationDriver($paths = [], $useSimpleAnnotationReader = true, bool $reportFieldsWhereDeclared = false)
{
Deprecation::trigger(
'doctrine/orm',
@@ -203,7 +215,8 @@ class Configuration extends \Doctrine\DBAL\Configuration
return new AnnotationDriver(
$reader,
(array) $paths
(array) $paths,
$reportFieldsWhereDeclared
);
}
@@ -1121,4 +1134,24 @@ class Configuration extends \Doctrine\DBAL\Configuration
$this->_attributes['isLazyGhostObjectEnabled'] = $flag;
}
public function setRejectIdCollisionInIdentityMap(bool $flag): void
{
$this->_attributes['rejectIdCollisionInIdentityMap'] = $flag;
}
public function isRejectIdCollisionInIdentityMapEnabled(): bool
{
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);
@@ -953,6 +959,14 @@ class EntityManager implements EntityManagerInterface
$this->unitOfWork->initializeObject($obj);
}
/**
* {@inheritDoc}
*/
public function isUninitializedObject($obj): bool
{
return $this->unitOfWork->isUninitializedObject($obj);
}
/**
* Factory method to create EntityManager instances.
*
@@ -973,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

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use function get_class;
use function sprintf;
final class EntityIdentityCollisionException extends ORMException
{
/**
* @param object $existingEntity
* @param object $newEntity
*/
public static function create($existingEntity, $newEntity, string $idHash): self
{
return new self(
sprintf(
<<<'EXCEPTION'
While adding an entity of class %s with an ID hash of "%s" to the identity map,
another object of class %s was already present for the same ID. This exception
is a safeguard against an internal inconsistency - IDs should uniquely map to
entity object instances. This problem may occur if:
- you use application-provided IDs and reuse ID values;
- database-provided IDs are reassigned after truncating the database without
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.
Otherwise, it might be an ORM-internal inconsistency, please report it.
EXCEPTION
,
get_class($newEntity),
$idHash,
get_class($existingEntity)
)
);
}
}

View File

@@ -1,34 +0,0 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Internal\CommitOrder;
/** @internal */
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)
{
$this->from = $from;
$this->to = $to;
$this->weight = $weight;
}
}

View File

@@ -1,38 +0,0 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Internal\CommitOrder;
use Doctrine\ORM\Mapping\ClassMetadata;
/** @internal */
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)
{
$this->hash = $hash;
$this->value = $value;
}
}

View File

@@ -1,17 +0,0 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Internal\CommitOrder;
/** @internal */
final class VertexState
{
public const NOT_VISITED = 0;
public const IN_PROGRESS = 1;
public const VISITED = 2;
private function __construct()
{
}
}

View File

@@ -1,164 +0,0 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Internal;
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).
*/
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 = [];
/**
* 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

@@ -10,7 +10,6 @@ use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Query;
use Doctrine\ORM\UnitOfWork;
use Doctrine\Persistence\Proxy;
use function array_fill_keys;
use function array_keys;
@@ -439,7 +438,7 @@ class ObjectHydrator extends AbstractHydrator
// PATH B: Single-valued association
$reflFieldValue = $reflField->getValue($parentObject);
if (! $reflFieldValue || isset($this->_hints[Query::HINT_REFRESH]) || ($reflFieldValue instanceof Proxy && ! $reflFieldValue->__isInitialized())) {
if (! $reflFieldValue || isset($this->_hints[Query::HINT_REFRESH]) || $this->_uow->isUninitializedObject($reflFieldValue)) {
// we only need to take action if this value is null,
// we refresh the entity or its an uninitialized proxy.
if (isset($nonemptyComponents[$dqlAlias])) {
@@ -457,9 +456,6 @@ class ObjectHydrator extends AbstractHydrator
$targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($element, $parentObject);
$this->_uow->setOriginalEntityProperty(spl_object_id($element), $inverseAssoc['fieldName'], $parentObject);
}
} elseif ($parentClass === $targetClass && $relation['mappedBy']) {
// Special case: bi-directional self-referencing one-one on the same class
$targetClass->reflFields[$relationField]->setValue($element, $parentObject);
}
} else {
// For sure bidirectional, as there is no inverse side in unidirectional mappings

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

@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Internal;
use Doctrine\ORM\Internal\TopologicalSort\CycleDetectedException;
use function array_keys;
use function spl_object_id;
/**
* TopologicalSort implements topological sorting, which is an ordering
* algorithm for directed graphs (DG) using a depth-first searching (DFS)
* to traverse the graph built in memory.
* 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).
*
* @internal
*/
final class TopologicalSort
{
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. The final
* boolean value indicates whether the edge is optional or not.
*
* @var array<int, array<int, bool>>
*/
private $edges = [];
/**
* Builds up the result during the DFS.
*
* @var list<object>
*/
private $sortResult = [];
/** @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
* @param bool $optional This indicates whether the edge may be ignored during the topological sort if it is necessary to break cycles.
*/
public function addEdge($from, $to, bool $optional): void
{
$fromId = spl_object_id($from);
$toId = spl_object_id($to);
if (isset($this->edges[$fromId][$toId]) && $this->edges[$fromId][$toId] === false) {
return; // we already know about this dependency, and it is not optional
}
$this->edges[$fromId][$toId] = $optional;
}
/**
* Returns a topological sort of all nodes. When we have an edge A->B between two nodes
* 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
{
foreach (array_keys($this->nodes) as $oid) {
if ($this->states[$oid] === self::NOT_VISITED) {
$this->visit($oid);
}
}
return $this->sortResult;
}
private function visit(int $oid): void
{
if ($this->states[$oid] === self::IN_PROGRESS) {
// This node is already on the current DFS stack. We've found a cycle!
throw new CycleDetectedException($this->nodes[$oid]);
}
if ($this->states[$oid] === self::VISITED) {
// We've reached a node that we've already seen, including all
// other nodes that are reachable from here. We're done here, return.
return;
}
$this->states[$oid] = self::IN_PROGRESS;
// Continue the DFS downwards the edge list
foreach ($this->edges[$oid] as $adjacentId => $optional) {
try {
$this->visit($adjacentId);
} catch (CycleDetectedException $exception) {
if ($exception->isCycleCollected()) {
// There is a complete cycle downstream of the current node. We cannot
// do anything about that anymore.
throw $exception;
}
if ($optional) {
// The current edge is part of a cycle, but it is optional and the closest
// such edge while backtracking. Break the cycle here by skipping the edge
// and continuing with the next one.
continue;
}
// We have found a cycle and cannot break it at $edge. Best we can do
// 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]);
throw $exception;
}
}
// We have traversed all edges and visited all other nodes reachable from here.
// So we're done with this vertex as well.
$this->states[$oid] = self::VISITED;
$this->sortResult[] = $this->nodes[$oid];
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Internal\TopologicalSort;
use RuntimeException;
use function array_unshift;
class CycleDetectedException extends RuntimeException
{
/** @var list<object> */
private $cycle;
/** @var object */
private $startNode;
/**
* Do we have the complete cycle collected?
*
* @var bool
*/
private $cycleCollected = false;
/** @param object $startNode */
public function __construct($startNode)
{
parent::__construct('A cycle has been detected, so a topological sort is not possible. The getCycle() method provides the list of nodes that form the cycle.');
$this->startNode = $startNode;
$this->cycle = [$startNode];
}
/** @return list<object> */
public function getCycle(): array
{
return $this->cycle;
}
/** @param object $node */
public function addToCycle($node): void
{
array_unshift($this->cycle, $node);
if ($node === $this->startNode) {
$this->cycleCollected = true;
}
}
public function isCycleCollected(): bool
{
return $this->cycleCollected;
}
}

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;
}
@@ -634,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
,
@@ -729,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

@@ -1176,7 +1176,7 @@ class ClassMetadataInfo implements ClassMetadata
$this->namespace = $reflService->getClassNamespace($this->name);
if ($this->reflClass) {
$this->name = $this->rootEntityName = $this->reflClass->getName();
$this->name = $this->rootEntityName = $this->reflClass->name;
}
$this->table['name'] = $this->namingStrategy->classToTableName($this->name);
@@ -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'];
}
@@ -2764,6 +2768,10 @@ class ClassMetadataInfo implements ClassMetadata
$this->fieldMappings[$fieldMapping['fieldName']] = $fieldMapping;
$this->columnNames[$fieldMapping['fieldName']] = $fieldMapping['columnName'];
$this->fieldNames[$fieldMapping['columnName']] = $fieldMapping['fieldName'];
if (isset($fieldMapping['generated'])) {
$this->requiresFetchAfterChange = true;
}
}
/**
@@ -3862,7 +3870,7 @@ class ClassMetadataInfo implements ClassMetadata
{
$reflectionProperty = $reflService->getAccessibleProperty($class, $field);
if ($reflectionProperty !== null && PHP_VERSION_ID >= 80100 && $reflectionProperty->isReadOnly()) {
$declaringClass = $reflectionProperty->getDeclaringClass()->name;
$declaringClass = $reflectionProperty->class;
if ($declaringClass !== $class) {
$reflectionProperty = $reflService->getAccessibleProperty($declaringClass, $field);
}

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,10 +30,13 @@ 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
{
use ColocatedMappingDriver;
use ReflectionBasedDriver;
/**
* The annotation reader.
@@ -60,7 +63,7 @@ class AnnotationDriver extends CompatibilityAnnotationDriver
* @param Reader $reader The AnnotationReader to use
* @param string|string[]|null $paths One or multiple paths where mapping classes can be found.
*/
public function __construct($reader, $paths = null)
public function __construct($reader, $paths = null, bool $reportFieldsWhereDeclared = false)
{
Deprecation::trigger(
'doctrine/orm',
@@ -70,6 +73,17 @@ class AnnotationDriver extends CompatibilityAnnotationDriver
$this->reader = $reader;
$this->addPaths((array) $paths);
if (! $reportFieldsWhereDeclared) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10455',
'In ORM 3.0, the AttributeDriver will report fields for the classes where they are declared. This may uncover invalid mapping configurations. To opt into the new mode also with the AnnotationDriver today, set the "reportFieldsWhereDeclared" constructor parameter to true.',
self::class
);
}
$this->reportFieldsWhereDeclared = $reportFieldsWhereDeclared;
}
/**
@@ -348,20 +362,12 @@ class AnnotationDriver extends CompatibilityAnnotationDriver
// Evaluate annotations on properties/fields
foreach ($class->getProperties() as $property) {
if (
$metadata->isMappedSuperclass && ! $property->isPrivate()
||
$metadata->isInheritedField($property->name)
||
$metadata->isInheritedAssociation($property->name)
||
$metadata->isInheritedEmbeddedClass($property->name)
) {
if ($this->isRepeatedPropertyDeclaration($property, $metadata)) {
continue;
}
$mapping = [];
$mapping['fieldName'] = $property->getName();
$mapping['fieldName'] = $property->name;
// Evaluate @Cache annotation
$cacheAnnot = $this->reader->getPropertyAnnotation($property, Mapping\Cache::class);
@@ -394,7 +400,7 @@ class AnnotationDriver extends CompatibilityAnnotationDriver
// @Column, @OneToOne, @OneToMany, @ManyToOne, @ManyToMany
$columnAnnot = $this->reader->getPropertyAnnotation($property, Mapping\Column::class);
if ($columnAnnot) {
$mapping = $this->columnToArray($property->getName(), $columnAnnot);
$mapping = $this->columnToArray($property->name, $columnAnnot);
$idAnnot = $this->reader->getPropertyAnnotation($property, Mapping\Id::class);
if ($idAnnot) {

View File

@@ -29,6 +29,7 @@ use const PHP_VERSION_ID;
class AttributeDriver extends CompatibilityAnnotationDriver
{
use ColocatedMappingDriver;
use ReflectionBasedDriver;
private const ENTITY_ATTRIBUTE_CLASSES = [
Mapping\Entity::class => 1,
@@ -52,7 +53,7 @@ class AttributeDriver extends CompatibilityAnnotationDriver
protected $reader;
/** @param array<string> $paths */
public function __construct(array $paths)
public function __construct(array $paths, bool $reportFieldsWhereDeclared = false)
{
if (PHP_VERSION_ID < 80000) {
throw new LogicException(
@@ -72,6 +73,17 @@ class AttributeDriver extends CompatibilityAnnotationDriver
self::class
);
}
if (! $reportFieldsWhereDeclared) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10455',
'In ORM 3.0, the AttributeDriver will report fields for the classes where they are declared. This may uncover invalid mapping configurations. To opt into the new mode today, set the "reportFieldsWhereDeclared" constructor parameter to true.',
self::class
);
}
$this->reportFieldsWhereDeclared = $reportFieldsWhereDeclared;
}
/**
@@ -297,20 +309,13 @@ class AttributeDriver extends CompatibilityAnnotationDriver
foreach ($reflectionClass->getProperties() as $property) {
assert($property instanceof ReflectionProperty);
if (
$metadata->isMappedSuperclass && ! $property->isPrivate()
||
$metadata->isInheritedField($property->name)
||
$metadata->isInheritedAssociation($property->name)
||
$metadata->isInheritedEmbeddedClass($property->name)
) {
if ($this->isRepeatedPropertyDeclaration($property, $metadata)) {
continue;
}
$mapping = [];
$mapping['fieldName'] = $property->getName();
$mapping['fieldName'] = $property->name;
// Evaluate #[Cache] attribute
$cacheAttribute = $this->reader->getPropertyAttribute($property, Mapping\Cache::class);
@@ -345,7 +350,7 @@ class AttributeDriver extends CompatibilityAnnotationDriver
$embeddedAttribute = $this->reader->getPropertyAttribute($property, Mapping\Embedded::class);
if ($columnAttribute !== null) {
$mapping = $this->columnToArray($property->getName(), $columnAttribute);
$mapping = $this->columnToArray($property->name, $columnAttribute);
if ($this->reader->getPropertyAttribute($property, Mapping\Id::class)) {
$mapping['id'] = true;

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

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Driver;
use Doctrine\ORM\Mapping\ClassMetadata;
use ReflectionProperty;
/** @internal */
trait ReflectionBasedDriver
{
/** @var bool */
private $reportFieldsWhereDeclared = false;
/**
* Helps to deal with the case that reflection may report properties inherited from parent classes.
* When we know about the fields already (inheritance has been anticipated in ClassMetadataFactory),
* the driver must skip them.
*
* The declaring classes may mismatch when there are private properties: The same property name may be
* reported multiple times, but since it is private, it is in fact multiple (different) properties in
* different classes. In that case, report the property as an individual field. (ClassMetadataFactory will
* probably fail in that case, though.)
*/
private function isRepeatedPropertyDeclaration(ReflectionProperty $property, ClassMetadata $metadata): bool
{
if (! $this->reportFieldsWhereDeclared) {
return $metadata->isMappedSuperclass && ! $property->isPrivate()
|| $metadata->isInheritedField($property->name)
|| $metadata->isInheritedAssociation($property->name)
|| $metadata->isInheritedEmbeddedClass($property->name);
}
$declaringClass = $property->class;
if (
isset($metadata->fieldMappings[$property->name]['declared'])
&& $metadata->fieldMappings[$property->name]['declared'] === $declaringClass
) {
return true;
}
if (
isset($metadata->associationMappings[$property->name]['declared'])
&& $metadata->associationMappings[$property->name]['declared'] === $declaringClass
) {
return true;
}
return isset($metadata->embeddedClasses[$property->name]['declared'])
&& $metadata->embeddedClasses[$property->name]['declared'] === $declaringClass;
}
}

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;
@@ -944,7 +932,10 @@ class XmlDriver extends FileDriver
private function getCascadeMappings(SimpleXMLElement $cascadeElement): array
{
$cascades = [];
foreach ($cascadeElement->children() as $action) {
$children = $cascadeElement->children();
assert($children !== null);
foreach ($children as $action) {
// According to the JPA specifications, XML uses "cascade-persist"
// instead of "persist". Here, both variations
// are supported because YAML, Annotation and Attribute use "persist"
@@ -969,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

@@ -73,7 +73,7 @@ final class ReflectionPropertiesGetter
$parentClass = $currentClass->getParentClass();
if ($parentClass) {
$parentClassName = $parentClass->getName();
$parentClassName = $parentClass->name;
}
}
@@ -111,14 +111,14 @@ final class ReflectionPropertiesGetter
private function getAccessibleProperty(ReflectionProperty $property): ?ReflectionProperty
{
return $this->reflectionService->getAccessibleProperty(
$property->getDeclaringClass()->getName(),
$property->getName()
$property->class,
$property->name
);
}
private function getLogicalName(ReflectionProperty $property): string
{
$propertyName = $property->getName();
$propertyName = $property->name;
if ($property->isPublic()) {
return $propertyName;
@@ -128,6 +128,6 @@ final class ReflectionPropertiesGetter
return "\0*\0" . $propertyName;
}
return "\0" . $property->getDeclaringClass()->getName() . "\0" . $propertyName;
return "\0" . $property->class . "\0" . $propertyName;
}
}

View File

@@ -38,7 +38,7 @@ class ReflectionEmbeddedProperty extends ReflectionProperty
$this->childProperty = $childProperty;
$this->embeddedClass = (string) $embeddedClass;
parent::__construct($childProperty->getDeclaringClass()->getName(), $childProperty->getName());
parent::__construct($childProperty->class, $childProperty->name);
}
/**

View File

@@ -28,8 +28,8 @@ class ReflectionEnumProperty extends ReflectionProperty
$this->enumType = $enumType;
parent::__construct(
$originalReflectionProperty->getDeclaringClass()->getName(),
$originalReflectionProperty->getName()
$originalReflectionProperty->class,
$originalReflectionProperty->name
);
}
@@ -98,7 +98,7 @@ class ReflectionEnumProperty extends ReflectionProperty
} catch (ValueError $e) {
throw MappingException::invalidEnumValue(
get_class($object),
$this->originalReflectionProperty->getName(),
$this->originalReflectionProperty->name,
(string) $value,
$enumType,
$e

View File

@@ -30,7 +30,10 @@ class UnderscoreNamingStrategy implements NamingStrategy
/** @var int */
private $case;
/** @var string */
/**
* @var string
* @psalm-var non-empty-string
*/
private $pattern;
/**

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

@@ -62,7 +62,8 @@ final class ORMSetup
*/
public static function createDefaultAnnotationDriver(
array $paths = [],
?CacheItemPoolInterface $cache = null
?CacheItemPoolInterface $cache = null,
bool $reportFieldsWhereDeclared = false
): AnnotationDriver {
Deprecation::trigger(
'doctrine/orm',
@@ -88,7 +89,7 @@ final class ORMSetup
$reader = new PsrCachedReader($reader, $cache);
}
return new AnnotationDriver($reader, $paths);
return new AnnotationDriver($reader, $paths, $reportFieldsWhereDeclared);
}
/**
@@ -100,10 +101,11 @@ final class ORMSetup
array $paths,
bool $isDevMode = false,
?string $proxyDir = null,
?CacheItemPoolInterface $cache = null
?CacheItemPoolInterface $cache = null,
bool $reportFieldsWhereDeclared = false
): Configuration {
$config = self::createConfiguration($isDevMode, $proxyDir, $cache);
$config->setMetadataDriverImpl(new AttributeDriver($paths));
$config->setMetadataDriverImpl(new AttributeDriver($paths, $reportFieldsWhereDeclared));
return $config;
}

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;
@@ -183,7 +183,7 @@ class BasicEntityPersister implements EntityPersister
*
* @var IdentifierFlattener
*/
private $identifierFlattener;
protected $identifierFlattener;
/** @var CachedPersisterContext */
protected $currentPersisterContext;
@@ -256,17 +256,17 @@ class BasicEntityPersister implements EntityPersister
public function executeInserts()
{
if (! $this->queuedInserts) {
return [];
return;
}
$postInsertIds = [];
$uow = $this->em->getUnitOfWork();
$idGenerator = $this->class->idGenerator;
$isPostInsertId = $idGenerator->isPostInsertGenerator();
$stmt = $this->conn->prepare($this->getInsertSQL());
$tableName = $this->class->getTableName();
foreach ($this->queuedInserts as $entity) {
foreach ($this->queuedInserts as $key => $entity) {
$insertData = $this->prepareInsertData($entity);
if (isset($insertData[$tableName])) {
@@ -280,12 +280,10 @@ class BasicEntityPersister implements EntityPersister
$stmt->executeStatement();
if ($isPostInsertId) {
$generatedId = $idGenerator->generateId($this->em, $entity);
$id = [$this->class->identifier[0] => $generatedId];
$postInsertIds[] = [
'generatedId' => $generatedId,
'entity' => $entity,
];
$generatedId = $idGenerator->generateId($this->em, $entity);
$id = [$this->class->identifier[0] => $generatedId];
$uow->assignPostInsertId($entity, $generatedId);
} else {
$id = $this->class->getIdentifierValues($entity);
}
@@ -293,11 +291,16 @@ class BasicEntityPersister implements EntityPersister
if ($this->class->requiresFetchAfterChange) {
$this->assignDefaultVersionAndUpsertableValues($entity, $id);
}
// Unset this queued insert, so that the prepareUpdateData() method knows right away
// (for the next entity already) that the current entity has been written to the database
// and no extra updates need to be scheduled to refer to it.
//
// In \Doctrine\ORM\UnitOfWork::executeInserts(), the UoW already removed entities
// from its own list (\Doctrine\ORM\UnitOfWork::$entityInsertions) right after they
// were given to our addInsert() method.
unset($this->queuedInserts[$key]);
}
$this->queuedInserts = [];
return $postInsertIds;
}
/**
@@ -376,7 +379,7 @@ class BasicEntityPersister implements EntityPersister
* @return int[]|null[]|string[]
* @psalm-return list<int|string|null>
*/
private function extractIdentifierTypes(array $id, ClassMetadata $versionedClass): array
final protected function extractIdentifierTypes(array $id, ClassMetadata $versionedClass): array
{
$types = [];
@@ -675,10 +678,30 @@ class BasicEntityPersister implements EntityPersister
if ($newVal !== null) {
$oid = spl_object_id($newVal);
if (isset($this->queuedInserts[$oid]) || $uow->isScheduledForInsert($newVal)) {
// The associated entity $newVal is not yet persisted, so we must
// set $newVal = null, in order to insert a null value and schedule an
// extra update on the UnitOfWork.
// If the associated entity $newVal is not yet persisted and/or does not yet have
// an ID assigned, we must set $newVal = null. This will insert a null value and
// schedule an extra update on the UnitOfWork.
//
// This gives us extra time to a) possibly obtain a database-generated identifier
// value for $newVal, and b) insert $newVal into the database before the foreign
// key reference is being made.
//
// When looking at $this->queuedInserts and $uow->isScheduledForInsert, be aware
// of the implementation details that our own executeInserts() method will remove
// entities from the former as soon as the insert statement has been executed and
// a post-insert ID has been assigned (if necessary), and that the UnitOfWork has
// already removed entities from its own list at the time they were passed to our
// addInsert() method.
//
// Then, there is one extra exception we can make: An entity that references back to itself
// _and_ uses an application-provided ID (the "NONE" generator strategy) also does not
// need the extra update, although it is still in the list of insertions itself.
// This looks like a minor optimization at first, but is the capstone for being able to
// use non-NULLable, self-referencing associations in applications that provide IDs (like UUIDs).
if (
(isset($this->queuedInserts[$oid]) || $uow->isScheduledForInsert($newVal))
&& ! ($newVal === $entity && $this->class->isIdentifierNatural())
) {
$uow->scheduleExtraUpdate($entity, [$field => [null, $newVal]]);
$newVal = null;
@@ -1241,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;
@@ -2005,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

@@ -109,17 +109,15 @@ interface EntityPersister
public function addInsert($entity);
/**
* Executes all queued entity insertions and returns any generated post-insert
* identifiers that were created as a result of the insertions.
* Executes all queued entity insertions.
*
* If no inserts are queued, invoking this method is a NOOP.
*
* @psalm-return list<array{
* @psalm-return void|list<array{
* generatedId: int,
* entity: object
* }> An array of any generated post-insert IDs. This will be
* an empty array if the entity class does not use the
* IDENTITY generation strategy.
* }> Returning an array of generated post-insert IDs is deprecated, implementations
* should call UnitOfWork::assignPostInsertId() and return void.
*/
public function executeInserts();

View File

@@ -11,8 +11,11 @@ use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Internal\SQLResultCasing;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Utility\PersisterHelper;
use LengthException;
use function array_combine;
use function array_keys;
use function array_values;
use function implode;
/**
@@ -109,10 +112,10 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
public function executeInserts()
{
if (! $this->queuedInserts) {
return [];
return;
}
$postInsertIds = [];
$uow = $this->em->getUnitOfWork();
$idGenerator = $this->class->idGenerator;
$isPostInsertId = $idGenerator->isPostInsertGenerator();
$rootClass = $this->class->name !== $this->class->rootEntityName
@@ -157,20 +160,14 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
$rootTableStmt->executeStatement();
if ($isPostInsertId) {
$generatedId = $idGenerator->generateId($this->em, $entity);
$id = [$this->class->identifier[0] => $generatedId];
$postInsertIds[] = [
'generatedId' => $generatedId,
'entity' => $entity,
];
$generatedId = $idGenerator->generateId($this->em, $entity);
$id = [$this->class->identifier[0] => $generatedId];
$uow->assignPostInsertId($entity, $generatedId);
} else {
$id = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
}
if ($this->class->requiresFetchAfterChange) {
$this->assignDefaultVersionAndUpsertableValues($entity, $id);
}
// Execute inserts on subtables.
// The order doesn't matter because all child tables link to the root table via FK.
foreach ($subTableStmts as $tableName => $stmt) {
@@ -191,11 +188,13 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
$stmt->executeStatement();
}
if ($this->class->requiresFetchAfterChange) {
$this->assignDefaultVersionAndUpsertableValues($entity, $id);
}
}
$this->queuedInserts = [];
return $postInsertIds;
}
/**
@@ -514,6 +513,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
|| isset($this->class->associationMappings[$name]['inherited'])
|| ($this->class->isVersioned && $this->class->versionField === $name)
|| isset($this->class->embeddedClasses[$name])
|| isset($this->class->fieldMappings[$name]['notInsertable'])
) {
continue;
}
@@ -556,6 +556,60 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
}
}
/**
* {@inheritDoc}
*/
protected function fetchVersionAndNotUpsertableValues($versionedClass, array $id)
{
$columnNames = [];
foreach ($this->class->fieldMappings as $key => $column) {
$class = null;
if ($this->class->isVersioned && $key === $versionedClass->versionField) {
$class = $versionedClass;
} elseif (isset($column['generated'])) {
$class = isset($column['inherited'])
? $this->em->getClassMetadata($column['inherited'])
: $this->class;
} else {
continue;
}
$columnNames[$key] = $this->getSelectColumnSQL($key, $class);
}
$tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform);
$baseTableAlias = $this->getSQLTableAlias($this->class->name);
$joinSql = $this->getJoinSql($baseTableAlias);
$identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform);
foreach ($identifier as $i => $idValue) {
$identifier[$i] = $baseTableAlias . '.' . $idValue;
}
$sql = 'SELECT ' . implode(', ', $columnNames)
. ' FROM ' . $tableName . ' ' . $baseTableAlias
. $joinSql
. ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';
$flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id);
$values = $this->conn->fetchNumeric(
$sql,
array_values($flatId),
$this->extractIdentifierTypes($id, $versionedClass)
);
if ($values === false) {
throw new LengthException('Unexpected empty result for database query.');
}
$values = array_combine(array_keys($columnNames), $values);
if (! $values) {
throw new LengthException('Unexpected number of database columns.');
}
return $values;
}
private function getJoinSql(string $baseTableAlias): string
{
$joinSql = '';

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

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Proxy;
use Doctrine\Persistence\Proxy;
/**
* @internal
*
* @template T of object
* @template-extends Proxy<T>
*
* @method void __setInitialized(bool $initialized)
*/
interface InternalProxy extends Proxy
{
}

View File

@@ -10,7 +10,11 @@ use Doctrine\Common\Proxy\Proxy as BaseProxy;
* Interface for proxy classes.
*
* @deprecated 2.14. Use \Doctrine\Persistence\Proxy instead
*
* @template T of object
* @template-extends BaseProxy<T>
* @template-extends InternalProxy<T>
*/
interface Proxy extends BaseProxy
interface Proxy extends BaseProxy, InternalProxy
{
}

View File

@@ -9,9 +9,10 @@ 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;
@@ -20,22 +21,82 @@ 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.
*
* @psalm-type AutogenerateMode = ProxyFactory::AUTOGENERATE_NEVER|ProxyFactory::AUTOGENERATE_ALWAYS|ProxyFactory::AUTOGENERATE_FILE_NOT_EXISTS|ProxyFactory::AUTOGENERATE_EVAL|ProxyFactory::AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED
*/
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
@@ -48,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();
@@ -76,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
*
@@ -86,39 +144,57 @@ 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
* connected to the given <tt>EntityManager</tt>.
*
* @param EntityManagerInterface $em The EntityManager the new factory works for.
* @param string $proxyDir The directory to use for the proxy classes. It must exist.
* @param string $proxyNs The namespace to use for the proxy classes.
* @param bool|int $autoGenerate The strategy for automatically generating proxy classes. Possible
* values are constants of {@see ProxyFactory::AUTOGENERATE_*}.
* @psalm-param bool|AutogenerateMode $autoGenerate
* @param EntityManagerInterface $em The EntityManager the new factory works for.
* @param string $proxyDir The directory to use for the proxy classes. It must exist.
* @param string $proxyNs The namespace to use for the proxy classes.
* @param bool|self::AUTOGENERATE_* $autoGenerate The strategy for automatically generating proxy classes.
*/
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', Proxy::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());
}
@@ -127,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 (Proxy $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);
}
/**
@@ -154,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,
@@ -181,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
@@ -242,20 +354,20 @@ EOPHP;
/**
* Creates a closure capable of initializing a proxy
*
* @return Closure(Proxy, Proxy):void
* @return Closure(InternalProxy, InternalProxy):void
*
* @throws EntityNotFoundException
*/
private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister): Closure
private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure
{
return function (Proxy $proxy, Proxy $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)
);
}
@@ -270,7 +382,6 @@ EOPHP;
continue;
}
$property->setAccessible(true);
$property->setValue($proxy, $property->getValue($entity));
}
};
@@ -279,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
@@ -315,6 +428,144 @@ EOPHP;
};
}
private function getProxyFileName(string $className, string $baseDirectory): string
{
$baseDirectory = $baseDirectory ?: $this->proxyDir;
return rtrim($baseDirectory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . InternalProxy::MARKER
. str_replace('\\', '', $className) . '.php';
}
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();
while ($reflector) {
foreach ($reflector->getProperties($filter) as $property) {
$name = $property->name;
if ($property->isStatic() || (($class->hasField($name) || $class->hasAssociation($name)) && ! isset($identifiers[$name]))) {
continue;
}
$prefix = $property->isPrivate() ? "\0" . $property->class . "\0" : ($property->isProtected() ? "\0*\0" : '');
$skippedProperties[$prefix . $name] = true;
}
$filter = ReflectionProperty::IS_PRIVATE;
$reflector = $reflector->getParentClass();
}
$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);
$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());
@@ -331,46 +582,13 @@ EOPHP;
return $code;
}
private function generateSkippedProperties(ClassMetadata $class): string
{
$skippedProperties = [];
$identifiers = array_flip($class->getIdentifierFieldNames());
$filter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE;
$reflector = $class->getReflectionClass();
while ($reflector) {
foreach ($reflector->getProperties($filter) as $property) {
$name = $property->getName();
if ($property->isStatic() || (($class->hasField($name) || $class->hasAssociation($name)) && ! isset($identifiers[$name]))) {
continue;
}
$prefix = $property->isPrivate() ? "\0" . $property->getDeclaringClass()->getName() . "\0" : ($property->isProtected() ? "\0*\0" : '');
$skippedProperties[$prefix . $name] = true;
}
$filter = ReflectionProperty::IS_PRIVATE;
$reflector = $reflector->getParentClass();
}
uksort($skippedProperties, 'strnatcmp');
$code = VarExporter::export($skippedProperties);
$code = str_replace(VarExporter::export($class->getName()), 'parent::class', $code);
$code = str_replace("\n", "\n ", $code);
return $code;
}
private function generateSerializeImpl(ClassMetadata $class): string
{
$reflector = $class->getReflectionClass();
$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"]);
';
@@ -381,7 +599,7 @@ EOPHP;
return $code . '$data = [];
foreach (parent::__sleep() as $name) {
$value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0' . $reflector->getName() . '\0$name"] ?? $k = null;
$value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0' . $reflector->name . '\0$name"] ?? $k = null;
if (null === $k) {
trigger_error(sprintf(\'serialize(): "%s" returned as member variable from __sleep() but does not exist\', $name), \E_USER_NOTICE);
@@ -392,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

@@ -51,6 +51,14 @@ class FilterCollection
*/
private $enabledFilters = [];
/**
* Instances of suspended filters.
*
* @var SQLFilter[]
* @psalm-var array<string, SQLFilter>
*/
private $suspendedFilters = [];
/**
* The filter hash from the last time the query was parsed.
*
@@ -83,6 +91,17 @@ class FilterCollection
return $this->enabledFilters;
}
/**
* Gets all the suspended filters.
*
* @return SQLFilter[] The suspended filters.
* @psalm-return array<string, SQLFilter>
*/
public function getSuspendedFilters(): array
{
return $this->suspendedFilters;
}
/**
* Enables a filter from the collection.
*
@@ -105,6 +124,9 @@ class FilterCollection
$this->enabledFilters[$name] = new $filterClass($this->em);
// In case a suspended filter with the same name was forgotten
unset($this->suspendedFilters[$name]);
// Keep the enabled filters sorted for the hash
ksort($this->enabledFilters);
@@ -135,6 +157,54 @@ class FilterCollection
return $filter;
}
/**
* Suspend a filter.
*
* @param string $name Name of the filter.
*
* @return SQLFilter The suspended filter.
*
* @throws InvalidArgumentException If the filter does not exist.
*/
public function suspend(string $name): SQLFilter
{
// Get the filter to return it
$filter = $this->getFilter($name);
$this->suspendedFilters[$name] = $filter;
unset($this->enabledFilters[$name]);
$this->setFiltersStateDirty();
return $filter;
}
/**
* Restore a disabled filter from the collection.
*
* @param string $name Name of the filter.
*
* @return SQLFilter The restored filter.
*
* @throws InvalidArgumentException If the filter does not exist.
*/
public function restore(string $name): SQLFilter
{
if (! $this->isSuspended($name)) {
throw new InvalidArgumentException("Filter '" . $name . "' is not suspended.");
}
$this->enabledFilters[$name] = $this->suspendedFilters[$name];
unset($this->suspendedFilters[$name]);
// Keep the enabled filters sorted for the hash
ksort($this->enabledFilters);
$this->setFiltersStateDirty();
return $this->enabledFilters[$name];
}
/**
* Gets an enabled filter from the collection.
*
@@ -177,6 +247,18 @@ class FilterCollection
return isset($this->enabledFilters[$name]);
}
/**
* Checks if a filter is suspended.
*
* @param string $name Name of the filter.
*
* @return bool True if the filter is suspended, false otherwise.
*/
public function isSuspended(string $name): bool
{
return isset($this->suspendedFilters[$name]);
}
/**
* Checks if the filter collection is clean.
*

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();

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