Compare commits

...

86 Commits

Author SHA1 Message Date
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
115 changed files with 4074 additions and 618 deletions

View File

@@ -1,3 +1,31 @@
# Upgrade to 2.16
## Deprecated `\Doctrine\ORM\Internal\CommitOrderCalculator` and related classes
With changes made to the commit order computation, the internal classes
`\Doctrine\ORM\Internal\CommitOrderCalculator`, `\Doctrine\ORM\Internal\CommitOrder\Edge`,
`\Doctrine\ORM\Internal\CommitOrder\Vertex` and `\Doctrine\ORM\Internal\CommitOrder\VertexState`
have been deprecated and will be removed in ORM 3.0.
## Deprecated returning post insert IDs from `EntityPersister::executeInserts()`
Persisters implementing `\Doctrine\ORM\Persisters\Entity\EntityPersister` should no longer
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

View File

@@ -707,8 +707,8 @@ not directly mapped by Doctrine.
``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.
database insert operation for that entity. A generated primary key value for
the entity will be available in the postPersist event.
- 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.

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

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

View File

@@ -1010,7 +1010,7 @@ abstract class AbstractQuery
*
* Alias for getSingleResult(HYDRATE_SINGLE_SCALAR).
*
* @return mixed The scalar result.
* @return bool|float|int|string The scalar result.
*
* @throws NoResultException If the query returned no result.
* @throws NonUniqueResultException If the query result is not unique.

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

@@ -23,6 +23,7 @@ use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Persisters\Entity\EntityPersister;
use Doctrine\ORM\UnitOfWork;
use function array_merge;
use function assert;
use function serialize;
use function sha1;
@@ -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();
}

View File

@@ -62,8 +62,6 @@ 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
{
@@ -95,8 +93,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 +103,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 +159,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 +198,8 @@ class Configuration extends \Doctrine\DBAL\Configuration
return new AnnotationDriver(
$reader,
(array) $paths
(array) $paths,
$reportFieldsWhereDeclared
);
}

View File

@@ -953,6 +953,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.
*

View File

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

View File

@@ -4,9 +4,13 @@ declare(strict_types=1);
namespace Doctrine\ORM\Internal\CommitOrder;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Mapping\ClassMetadata;
/** @internal */
/**
* @internal
* @deprecated
*/
final class Vertex
{
/**
@@ -32,6 +36,13 @@ final class Vertex
public function __construct(string $hash, ClassMetadata $value)
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10547',
'The %s class is deprecated and will be removed in ORM 3.0',
self::class
);
$this->hash = $hash;
$this->value = $value;
}

View File

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

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Internal;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Internal\CommitOrder\Edge;
use Doctrine\ORM\Internal\CommitOrder\Vertex;
use Doctrine\ORM\Internal\CommitOrder\VertexState;
@@ -17,6 +18,8 @@ use function array_reverse;
* using a depth-first searching (DFS) to traverse the graph built in memory.
* This algorithm have a linear running time based on nodes (V) and dependency
* between the nodes (E), resulting in a computational complexity of O(V + E).
*
* @deprecated
*/
class CommitOrderCalculator
{
@@ -45,6 +48,16 @@ class CommitOrderCalculator
*/
private $sortedNodeList = [];
public function __construct()
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10547',
'The %s class is deprecated and will be removed in ORM 3.0',
self::class
);
}
/**
* Checks for node (vertex) existence in graph.
*

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,165 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Internal;
use Doctrine\ORM\Internal\TopologicalSort\CycleDetectedException;
use function array_keys;
use function array_reverse;
use function array_unshift;
use function spl_object_id;
/**
* 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 A will be listed before B in the result.
*
* @return list<object>
*/
public function sort(): array
{
/*
* When possible, keep objects in the result in the same order in which they were added as nodes.
* Since nodes are unshifted into $this->>sortResult (see the visit() method), that means we
* need to work them in array_reverse order here.
*/
foreach (array_reverse(array_keys($this->nodes)) as $oid) {
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 retreat 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;
array_unshift($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

@@ -113,7 +113,7 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
if ($parent) {
$class->setInheritanceType($parent->inheritanceType);
$class->setDiscriminatorColumn($parent->discriminatorColumn);
$class->setIdGeneratorType($parent->generatorType);
$this->inheritIdGeneratorMapping($class, $parent);
$this->addInheritedFields($class, $parent);
$this->addInheritedRelations($class, $parent);
$this->addInheritedEmbeddedClasses($class, $parent);
@@ -141,12 +141,8 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
throw MappingException::reflectionFailure($class->getName(), $e);
}
// If this class has a parent the id generator strategy is inherited.
// However this is only true if the hierarchy of parents contains the root entity,
// if it consists of mapped superclasses these don't necessarily include the id field.
if ($parent && $rootEntityFound) {
$this->inheritIdGeneratorMapping($class, $parent);
} else {
// Complete id generator mapping when the generator was declared/added in this class
if ($class->identifier && (! $parent || ! $parent->identifier)) {
$this->completeIdGeneratorMapping($class);
}

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);
@@ -2764,6 +2764,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 +3866,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

@@ -34,6 +34,7 @@ use function is_numeric;
class AnnotationDriver extends CompatibilityAnnotationDriver
{
use ColocatedMappingDriver;
use ReflectionBasedDriver;
/**
* The annotation reader.
@@ -60,7 +61,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 +71,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 +360,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 +398,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

@@ -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

@@ -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

@@ -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);
}
/**

View File

@@ -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;

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

@@ -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

@@ -17,7 +17,6 @@ use Doctrine\ORM\Proxy\Proxy as LegacyProxy;
use Doctrine\ORM\UnitOfWork;
use Doctrine\ORM\Utility\IdentifierFlattener;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\Proxy;
use ReflectionProperty;
use Symfony\Component\VarExporter\ProxyHelper;
use Symfony\Component\VarExporter\VarExporter;
@@ -31,8 +30,6 @@ use function uksort;
/**
* 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
{
@@ -93,19 +90,17 @@ EOPHP;
* 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()) {
$proxyGenerator->setPlaceholder('baseProxyInterface', Proxy::class);
$proxyGenerator->setPlaceholder('baseProxyInterface', InternalProxy::class);
$proxyGenerator->setPlaceholder('useLazyGhostTrait', Closure::fromCallable([$this, 'generateUseLazyGhostTrait']));
$proxyGenerator->setPlaceholder('skippedProperties', Closure::fromCallable([$this, 'generateSkippedProperties']));
$proxyGenerator->setPlaceholder('serializeImpl', Closure::fromCallable([$this, 'generateSerializeImpl']));
@@ -135,7 +130,7 @@ EOPHP;
$initializer = $this->definitions[$className]->initializer;
$proxy->__construct(static function (Proxy $object) use ($initializer, $proxy): void {
$proxy->__construct(static function (InternalProxy $object) use ($initializer, $proxy): void {
$initializer($object, $proxy);
});
@@ -242,13 +237,13 @@ 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
{
return function (Proxy $proxy, Proxy $original) use ($entityPersister, $classMetadata): void {
return function (InternalProxy $proxy, InternalProxy $original) use ($entityPersister, $classMetadata): void {
$identifier = $classMetadata->getIdentifierValues($original);
$entity = $entityPersister->loadById($identifier, $original);
@@ -340,13 +335,13 @@ EOPHP;
while ($reflector) {
foreach ($reflector->getProperties($filter) as $property) {
$name = $property->getName();
$name = $property->name;
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" : '');
$prefix = $property->isPrivate() ? "\0" . $property->class . "\0" : ($property->isProtected() ? "\0*\0" : '');
$skippedProperties[$prefix . $name] = true;
}
@@ -381,7 +376,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);

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

@@ -17,6 +17,7 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function assert;
use function get_debug_type;
use function sprintf;
@@ -63,32 +64,46 @@ EOT
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$em = $this->getEntityManager($input);
$cache = $em->getConfiguration()->getQueryCache();
$cacheDriver = $em->getConfiguration()->getQueryCacheImpl();
$em = $this->getEntityManager($input);
$cache = $em->getConfiguration()->getQueryCache();
if (! $cacheDriver) {
throw new InvalidArgumentException('No Query cache driver is configured on given EntityManager.');
}
if ($cacheDriver instanceof ApcCache || $cache instanceof ApcuAdapter) {
if ($cache instanceof ApcuAdapter) {
throw new LogicException('Cannot clear APCu Cache from Console, it\'s shared in the Webserver memory and not accessible from the CLI.');
}
if ($cacheDriver instanceof XcacheCache) {
throw new LogicException('Cannot clear XCache Cache from Console, it\'s shared in the Webserver memory and not accessible from the CLI.');
}
$cacheDriver = null;
if (! $cache) {
$cacheDriver = $em->getConfiguration()->getQueryCacheImpl();
if (! ($cacheDriver instanceof ClearableCache)) {
throw new LogicException(sprintf(
'Can only clear cache when ClearableCache interface is implemented, %s does not implement.',
get_debug_type($cacheDriver)
));
if (! $cacheDriver) {
throw new InvalidArgumentException('No Query cache driver is configured on given EntityManager.');
}
if ($cacheDriver instanceof ApcCache) {
throw new LogicException('Cannot clear APCu Cache from Console, it\'s shared in the Webserver memory and not accessible from the CLI.');
}
if ($cacheDriver instanceof XcacheCache) {
throw new LogicException('Cannot clear XCache Cache from Console, it\'s shared in the Webserver memory and not accessible from the CLI.');
}
if (! ($cacheDriver instanceof ClearableCache)) {
throw new LogicException(sprintf(
'Can only clear cache when ClearableCache interface is implemented, %s does not implement.',
get_debug_type($cacheDriver)
));
}
}
$ui->comment('Clearing <info>all</info> Query cache entries');
$result = $cache ? $cache->clear() : $cacheDriver->deleteAll();
if ($cache) {
$result = $cache->clear();
} else {
assert($cacheDriver !== null);
$result = $cacheDriver->deleteAll();
}
$message = $result ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.';
if ($input->getOption('flush') === true && ! $cache) {

View File

@@ -9,7 +9,6 @@ use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\UnitOfWork;
use Doctrine\Persistence\Proxy;
use ReflectionObject;
use function count;
@@ -87,7 +86,7 @@ class DebugUnitOfWorkListener
if ($value === null) {
fwrite($fh, " NULL\n");
} else {
if ($value instanceof Proxy && ! $value->__isInitialized()) {
if ($uow->isUninitializedObject($value)) {
fwrite($fh, '[PROXY] ');
}

View File

@@ -969,7 +969,7 @@ public function __construct(<params>)
{
$refl = new ReflectionClass($this->getClassToExtend());
return '\\' . $refl->getName();
return '\\' . $refl->name;
}
/** @return string */

View File

@@ -821,8 +821,8 @@ class SchemaTool
return [];
}
$options = array_intersect_key($mappingOptions, array_flip(self::KNOWN_COLUMN_OPTIONS));
$options['customSchemaOptions'] = array_diff_key($mappingOptions, $options);
$options = array_intersect_key($mappingOptions, array_flip(self::KNOWN_COLUMN_OPTIONS));
$options['platformOptions'] = array_diff_key($mappingOptions, $options);
return $options;
}

View File

@@ -28,6 +28,7 @@ use Doctrine\ORM\Exception\UnexpectedAssociationValue;
use Doctrine\ORM\Id\AssignedGenerator;
use Doctrine\ORM\Internal\CommitOrderCalculator;
use Doctrine\ORM\Internal\HydrationCompleteHandler;
use Doctrine\ORM\Internal\TopologicalSort;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\Mapping\Reflection\ReflectionPropertiesGetter;
@@ -38,12 +39,12 @@ use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
use Doctrine\ORM\Persisters\Entity\EntityPersister;
use Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister;
use Doctrine\ORM\Persisters\Entity\SingleTablePersister;
use Doctrine\ORM\Proxy\InternalProxy;
use Doctrine\ORM\Utility\IdentifierFlattener;
use Doctrine\Persistence\Mapping\RuntimeReflectionService;
use Doctrine\Persistence\NotifyPropertyChanged;
use Doctrine\Persistence\ObjectManagerAware;
use Doctrine\Persistence\PropertyChangedListener;
use Doctrine\Persistence\Proxy;
use Exception;
use InvalidArgumentException;
use RuntimeException;
@@ -56,11 +57,9 @@ use function array_filter;
use function array_key_exists;
use function array_map;
use function array_merge;
use function array_pop;
use function array_sum;
use function array_values;
use function assert;
use function count;
use function current;
use function func_get_arg;
use function func_num_args;
@@ -74,6 +73,7 @@ use function method_exists;
use function reset;
use function spl_object_id;
use function sprintf;
use function strtolower;
/**
* The UnitOfWork is responsible for tracking changes to objects during an
@@ -419,9 +419,6 @@ class UnitOfWork implements PropertyChangedListener
$this->dispatchOnFlushEvent();
// Now we need a commit order to maintain referential integrity
$commitOrder = $this->getCommitOrder();
$conn = $this->em->getConnection();
$conn->beginTransaction();
@@ -437,32 +434,37 @@ class UnitOfWork implements PropertyChangedListener
}
if ($this->entityInsertions) {
foreach ($commitOrder as $class) {
$this->executeInserts($class);
}
// Perform entity insertions first, so that all new entities have their rows in the database
// and can be referred to by foreign keys. The commit order only needs to take new entities
// into account (new entities referring to other new entities), since all other types (entities
// with updates or scheduled deletions) are currently not a problem, since they are already
// in the database.
$this->executeInserts();
}
if ($this->entityUpdates) {
foreach ($commitOrder as $class) {
$this->executeUpdates($class);
}
// Updates do not need to follow a particular order
$this->executeUpdates();
}
// Extra updates that were requested by persisters.
// This may include foreign keys that could not be set when an entity was inserted,
// which may happen in the case of circular foreign key relationships.
if ($this->extraUpdates) {
$this->executeExtraUpdates();
}
// Collection updates (deleteRows, updateRows, insertRows)
// No particular order is necessary, since all entities themselves are already
// in the database
foreach ($this->collectionUpdates as $collectionToUpdate) {
$this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
}
// Entity deletions come last and need to be in reverse commit order
// Entity deletions come last. Their order only needs to take care of other deletions
// (first delete entities depending upon others, before deleting depended-upon entities).
if ($this->entityDeletions) {
for ($count = count($commitOrder), $i = $count - 1; $i >= 0 && $this->entityDeletions; --$i) {
$this->executeDeletions($commitOrder[$i]);
}
$this->executeDeletions();
}
// Commit failed silently
@@ -581,7 +583,7 @@ class UnitOfWork implements PropertyChangedListener
}
// Ignore uninitialized proxy objects
if ($entity instanceof Proxy && ! $entity->__isInitialized()) {
if ($this->isUninitializedObject($entity)) {
return;
}
@@ -906,7 +908,7 @@ class UnitOfWork implements PropertyChangedListener
foreach ($entitiesToProcess as $entity) {
// Ignore uninitialized proxy objects
if ($entity instanceof Proxy && ! $entity->__isInitialized()) {
if ($this->isUninitializedObject($entity)) {
continue;
}
@@ -931,7 +933,7 @@ class UnitOfWork implements PropertyChangedListener
*/
private function computeAssociationChanges(array $assoc, $value): void
{
if ($value instanceof Proxy && ! $value->__isInitialized()) {
if ($this->isUninitializedObject($value)) {
return;
}
@@ -1156,64 +1158,46 @@ class UnitOfWork implements PropertyChangedListener
}
/**
* Executes all entity insertions for entities of the specified type.
* Executes entity insertions
*/
private function executeInserts(ClassMetadata $class): void
private function executeInserts(): void
{
$entities = [];
$className = $class->name;
$persister = $this->getEntityPersister($className);
$invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
$entities = $this->computeInsertExecutionOrder();
$insertionsForClass = [];
foreach ($this->entityInsertions as $oid => $entity) {
if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
continue;
}
$insertionsForClass[$oid] = $entity;
foreach ($entities as $entity) {
$oid = spl_object_id($entity);
$class = $this->em->getClassMetadata(get_class($entity));
$persister = $this->getEntityPersister($class->name);
$invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
$persister->addInsert($entity);
unset($this->entityInsertions[$oid]);
if ($invoke !== ListenersInvoker::INVOKE_NONE) {
$entities[] = $entity;
}
}
$postInsertIds = $persister->executeInserts();
$postInsertIds = $persister->executeInserts();
if (is_array($postInsertIds)) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10743/',
'Returning post insert IDs from \Doctrine\ORM\Persisters\Entity\EntityPersister::executeInserts() is deprecated and will not be supported in Doctrine ORM 3.0. Make the persister call Doctrine\ORM\UnitOfWork::assignPostInsertId() instead.'
);
if ($postInsertIds) {
// Persister returned post-insert IDs
foreach ($postInsertIds as $postInsertId) {
$idField = $class->getSingleIdentifierFieldName();
$idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $postInsertId['generatedId']);
$entity = $postInsertId['entity'];
$oid = spl_object_id($entity);
$class->reflFields[$idField]->setValue($entity, $idValue);
$this->entityIdentifiers[$oid] = [$idField => $idValue];
$this->entityStates[$oid] = self::STATE_MANAGED;
$this->originalEntityData[$oid][$idField] = $idValue;
$this->addToIdentityMap($entity);
}
} else {
foreach ($insertionsForClass as $oid => $entity) {
if (! isset($this->entityIdentifiers[$oid])) {
//entity was not added to identity map because some identifiers are foreign keys to new entities.
//add it now
$this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
// Persister returned post-insert IDs
foreach ($postInsertIds as $postInsertId) {
$this->assignPostInsertId($postInsertId['entity'], $postInsertId['generatedId']);
}
}
}
foreach ($entities as $entity) {
$this->listenersInvoker->invoke($class, Events::postPersist, $entity, new PostPersistEventArgs($entity, $this->em), $invoke);
if (! isset($this->entityIdentifiers[$oid])) {
//entity was not added to identity map because some identifiers are foreign keys to new entities.
//add it now
$this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
}
if ($invoke !== ListenersInvoker::INVOKE_NONE) {
$this->listenersInvoker->invoke($class, Events::postPersist, $entity, new PostPersistEventArgs($entity, $this->em), $invoke);
}
}
}
@@ -1251,19 +1235,15 @@ class UnitOfWork implements PropertyChangedListener
}
/**
* Executes all entity updates for entities of the specified type.
* Executes all entity updates
*/
private function executeUpdates(ClassMetadata $class): void
private function executeUpdates(): void
{
$className = $class->name;
$persister = $this->getEntityPersister($className);
$preUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
$postUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
foreach ($this->entityUpdates as $oid => $entity) {
if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
continue;
}
$class = $this->em->getClassMetadata(get_class($entity));
$persister = $this->getEntityPersister($class->name);
$preUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
$postUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
if ($preUpdateInvoke !== ListenersInvoker::INVOKE_NONE) {
$this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
@@ -1284,18 +1264,17 @@ class UnitOfWork implements PropertyChangedListener
}
/**
* Executes all entity deletions for entities of the specified type.
* Executes all entity deletions
*/
private function executeDeletions(ClassMetadata $class): void
private function executeDeletions(): void
{
$className = $class->name;
$persister = $this->getEntityPersister($className);
$invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
$entities = $this->computeDeleteExecutionOrder();
foreach ($this->entityDeletions as $oid => $entity) {
if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
continue;
}
foreach ($entities as $entity) {
$oid = spl_object_id($entity);
$class = $this->em->getClassMetadata(get_class($entity));
$persister = $this->getEntityPersister($class->name);
$invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
$persister->delete($entity);
@@ -1319,73 +1298,116 @@ class UnitOfWork implements PropertyChangedListener
}
}
/**
* Gets the commit order.
*
* @return list<ClassMetadata>
*/
private function getCommitOrder(): array
/** @return list<object> */
private function computeInsertExecutionOrder(): array
{
$calc = $this->getCommitOrderCalculator();
$sort = new TopologicalSort();
// See if there are any new classes in the changeset, that are not in the
// commit order graph yet (don't have a node).
// We have to inspect changeSet to be able to correctly build dependencies.
// It is not possible to use IdentityMap here because post inserted ids
// are not yet available.
$newNodes = [];
foreach (array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions) as $entity) {
$class = $this->em->getClassMetadata(get_class($entity));
if ($calc->hasNode($class->name)) {
continue;
}
$calc->addNode($class->name, $class);
$newNodes[] = $class;
// First make sure we have all the nodes
foreach ($this->entityInsertions as $entity) {
$sort->addNode($entity);
}
// Calculate dependencies for new nodes
while ($class = array_pop($newNodes)) {
// Now add edges
foreach ($this->entityInsertions as $entity) {
$class = $this->em->getClassMetadata(get_class($entity));
foreach ($class->associationMappings as $assoc) {
// We only need to consider the owning sides of to-one associations,
// since many-to-many associations are persisted at a later step and
// have no insertion order problems (all entities already in the database
// at that time).
if (! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
continue;
}
$targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
$targetEntity = $class->getFieldValue($entity, $assoc['fieldName']);
if (! $calc->hasNode($targetClass->name)) {
$calc->addNode($targetClass->name, $targetClass);
$newNodes[] = $targetClass;
}
$joinColumns = reset($assoc['joinColumns']);
$calc->addDependency($targetClass->name, $class->name, (int) empty($joinColumns['nullable']));
// If the target class has mapped subclasses, these share the same dependency.
if (! $targetClass->subClasses) {
// If there is no entity that we need to refer to, or it is already in the
// database (i. e. does not have to be inserted), no need to consider it.
if ($targetEntity === null || ! $sort->hasNode($targetEntity)) {
continue;
}
foreach ($targetClass->subClasses as $subClassName) {
$targetSubClass = $this->em->getClassMetadata($subClassName);
if (! $calc->hasNode($subClassName)) {
$calc->addNode($targetSubClass->name, $targetSubClass);
$newNodes[] = $targetSubClass;
}
$calc->addDependency($targetSubClass->name, $class->name, 1);
// An entity that references back to itself _and_ uses an application-provided ID
// (the "NONE" generator strategy) can be exempted from commit order computation.
// See https://github.com/doctrine/orm/pull/10735/ for more details on this edge case.
// A non-NULLable self-reference would be a cycle in the graph.
if ($targetEntity === $entity && $class->isIdentifierNatural()) {
continue;
}
// According to https://www.doctrine-project.org/projects/doctrine-orm/en/2.14/reference/annotations-reference.html#annref_joincolumn,
// the default for "nullable" is true. Unfortunately, it seems this default is not applied at the metadata driver, factory or other
// level, but in fact we may have an undefined 'nullable' key here, so we must assume that default here as well.
//
// Same in \Doctrine\ORM\Tools\EntityGenerator::isAssociationIsNullable or \Doctrine\ORM\Persisters\Entity\BasicEntityPersister::getJoinSQLForJoinColumns,
// to give two examples.
assert(isset($assoc['joinColumns']));
$joinColumns = reset($assoc['joinColumns']);
$isNullable = ! isset($joinColumns['nullable']) || $joinColumns['nullable'];
// Add dependency. The dependency direction implies that "$targetEntity has to go before $entity",
// so we can work through the topo sort result from left to right (with all edges pointing right).
$sort->addEdge($targetEntity, $entity, $isNullable);
}
}
return $calc->sort();
return $sort->sort();
}
/** @return list<object> */
private function computeDeleteExecutionOrder(): array
{
$sort = new TopologicalSort();
// First make sure we have all the nodes
foreach ($this->entityDeletions as $entity) {
$sort->addNode($entity);
}
// Now add edges
foreach ($this->entityDeletions as $entity) {
$class = $this->em->getClassMetadata(get_class($entity));
foreach ($class->associationMappings as $assoc) {
// We only need to consider the owning sides of to-one associations,
// since many-to-many associations can always be (and have already been)
// deleted in a preceding step.
if (! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
continue;
}
// For associations that implement a database-level cascade/set null operation,
// we do not have to follow a particular order: If the referred-to entity is
// deleted first, the DBMS will either delete the current $entity right away
// (CASCADE) or temporarily set the foreign key to NULL (SET NULL).
// Either way, we can skip it in the computation.
assert(isset($assoc['joinColumns']));
$joinColumns = reset($assoc['joinColumns']);
if (isset($joinColumns['onDelete'])) {
$onDeleteOption = strtolower($joinColumns['onDelete']);
if ($onDeleteOption === 'cascade' || $onDeleteOption === 'set null') {
continue;
}
}
$targetEntity = $class->getFieldValue($entity, $assoc['fieldName']);
// If the association does not refer to another entity or that entity
// is not to be deleted, there is no ordering problem and we can
// skip this particular association.
if ($targetEntity === null || ! $sort->hasNode($targetEntity)) {
continue;
}
// Add dependency. The dependency direction implies that "$entity has to be removed before $targetEntity",
// so we can work through the topo sort result from left to right (with all edges pointing right).
$sort->addEdge($entity, $targetEntity, false);
}
}
return $sort->sort();
}
/**
@@ -1611,6 +1633,30 @@ class UnitOfWork implements PropertyChangedListener
$className = $classMetadata->rootEntityName;
if (isset($this->identityMap[$className][$idHash])) {
if ($this->identityMap[$className][$idHash] !== $entity) {
throw new RuntimeException(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($entity),
$idHash,
get_class($this->identityMap[$className][$idHash])
));
}
return false;
}
@@ -2154,7 +2200,7 @@ class UnitOfWork implements PropertyChangedListener
$entity,
$managedCopy
): void {
if (! ($class->isVersioned && $this->isLoaded($managedCopy) && $this->isLoaded($entity))) {
if (! ($class->isVersioned && ! $this->isUninitializedObject($managedCopy) && ! $this->isUninitializedObject($entity))) {
return;
}
@@ -2172,16 +2218,6 @@ class UnitOfWork implements PropertyChangedListener
throw OptimisticLockException::lockFailedVersionMismatch($entity, $entityVersion, $managedCopyVersion);
}
/**
* Tests if an entity is loaded - must either be a loaded proxy or not a proxy
*
* @param object $entity
*/
private function isLoaded($entity): bool
{
return ! ($entity instanceof Proxy) || $entity->__isInitialized();
}
/**
* Sets/adds associated managed copies into the previous entity's association field
*
@@ -2477,7 +2513,7 @@ class UnitOfWork implements PropertyChangedListener
*/
private function cascadePersist($entity, array &$visited): void
{
if ($entity instanceof Proxy && ! $entity->__isInitialized()) {
if ($this->isUninitializedObject($entity)) {
// nothing to do - proxy is not initialized, therefore we don't do anything with it
return;
}
@@ -2551,13 +2587,13 @@ class UnitOfWork implements PropertyChangedListener
}
);
if ($associationMappings) {
$this->initializeObject($entity);
}
$entitiesToCascade = [];
foreach ($associationMappings as $assoc) {
if ($entity instanceof Proxy && ! $entity->__isInitialized()) {
$entity->__load();
}
$relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
switch (true) {
@@ -2613,9 +2649,7 @@ class UnitOfWork implements PropertyChangedListener
return;
}
if ($entity instanceof Proxy && ! $entity->__isInitialized()) {
$entity->__load();
}
$this->initializeObject($entity);
assert($class->versionField !== null);
$entityVersion = $class->reflFields[$class->versionField]->getValue($entity);
@@ -2805,7 +2839,6 @@ class UnitOfWork implements PropertyChangedListener
$unmanagedProxy = $hints[Query::HINT_REFRESH_ENTITY];
if (
$unmanagedProxy !== $entity
&& $unmanagedProxy instanceof Proxy
&& $this->isIdentifierEquals($unmanagedProxy, $entity)
) {
// We will hydrate the given un-managed proxy anyway:
@@ -2814,7 +2847,7 @@ class UnitOfWork implements PropertyChangedListener
}
}
if ($entity instanceof Proxy && ! $entity->__isInitialized()) {
if ($this->isUninitializedObject($entity)) {
$entity->__setInitialized(true);
} else {
if (
@@ -2831,25 +2864,20 @@ class UnitOfWork implements PropertyChangedListener
}
$this->originalEntityData[$oid] = $data;
if ($entity instanceof NotifyPropertyChanged) {
$entity->addPropertyChangedListener($this);
}
} else {
$entity = $this->newInstance($class);
$oid = spl_object_id($entity);
$this->entityIdentifiers[$oid] = $id;
$this->entityStates[$oid] = self::STATE_MANAGED;
$this->originalEntityData[$oid] = $data;
$this->identityMap[$class->rootEntityName][$idHash] = $entity;
$this->registerManaged($entity, $id, $data);
if (isset($hints[Query::HINT_READ_ONLY])) {
$this->readOnlyObjects[$oid] = true;
}
}
if ($entity instanceof NotifyPropertyChanged) {
$entity->addPropertyChangedListener($this);
}
foreach ($data as $field => $value) {
if (isset($class->fieldMappings[$field])) {
$class->reflFields[$field]->setValue($entity, $value);
@@ -2965,8 +2993,7 @@ class UnitOfWork implements PropertyChangedListener
$hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER &&
isset($hints[self::HINT_DEFEREAGERLOAD]) &&
! $targetClass->isIdentifierComposite &&
$newValue instanceof Proxy &&
$newValue->__isInitialized() === false
$this->isUninitializedObject($newValue)
) {
$this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
}
@@ -3007,20 +3034,7 @@ class UnitOfWork implements PropertyChangedListener
break;
}
// PERF: Inlined & optimized code from UnitOfWork#registerManaged()
$newValueOid = spl_object_id($newValue);
$this->entityIdentifiers[$newValueOid] = $associatedId;
$this->identityMap[$targetClass->rootEntityName][$relatedIdHash] = $newValue;
if (
$newValue instanceof NotifyPropertyChanged &&
( ! $newValue instanceof Proxy || $newValue->__isInitialized())
) {
$newValue->addPropertyChangedListener($this);
}
$this->entityStates[$newValueOid] = self::STATE_MANAGED;
// make sure that when an proxy is then finally loaded, $this->originalEntityData is set also!
$this->registerManaged($newValue, $associatedId, []);
break;
}
@@ -3377,7 +3391,7 @@ class UnitOfWork implements PropertyChangedListener
$this->addToIdentityMap($entity);
if ($entity instanceof NotifyPropertyChanged && ( ! $entity instanceof Proxy || $entity->__isInitialized())) {
if ($entity instanceof NotifyPropertyChanged && ! $this->isUninitializedObject($entity)) {
$entity->addPropertyChangedListener($this);
}
}
@@ -3485,7 +3499,7 @@ class UnitOfWork implements PropertyChangedListener
*/
public function initializeObject($obj)
{
if ($obj instanceof Proxy) {
if ($obj instanceof InternalProxy) {
$obj->__load();
return;
@@ -3496,6 +3510,18 @@ class UnitOfWork implements PropertyChangedListener
}
}
/**
* Tests if a value is an uninitialized entity.
*
* @param mixed $obj
*
* @psalm-assert-if-true InternalProxy $obj
*/
public function isUninitializedObject($obj): bool
{
return $obj instanceof InternalProxy && ! $obj->__isInitialized();
}
/**
* Helper method to show an object as string.
*
@@ -3646,13 +3672,11 @@ class UnitOfWork implements PropertyChangedListener
*/
private function mergeEntityStateIntoManagedCopy($entity, $managedCopy): void
{
if (! $this->isLoaded($entity)) {
if ($this->isUninitializedObject($entity)) {
return;
}
if (! $this->isLoaded($managedCopy)) {
$managedCopy->__load();
}
$this->initializeObject($managedCopy);
$class = $this->em->getClassMetadata(get_class($entity));
@@ -3673,7 +3697,7 @@ class UnitOfWork implements PropertyChangedListener
if ($other === null) {
$prop->setValue($managedCopy, null);
} else {
if ($other instanceof Proxy && ! $other->__isInitialized()) {
if ($this->isUninitializedObject($other)) {
// do not merge fields marked lazy that have not been fetched.
continue;
}
@@ -3837,4 +3861,30 @@ class UnitOfWork implements PropertyChangedListener
return $normalizedAssociatedId;
}
/**
* Assign a post-insert generated ID to an entity
*
* This is used by EntityPersisters after they inserted entities into the database.
* It will place the assigned ID values in the entity's fields and start tracking
* the entity in the identity map.
*
* @param object $entity
* @param mixed $generatedId
*/
final public function assignPostInsertId($entity, $generatedId): void
{
$class = $this->em->getClassMetadata(get_class($entity));
$idField = $class->getSingleIdentifierFieldName();
$idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $generatedId);
$oid = spl_object_id($entity);
$class->reflFields[$idField]->setValue($entity, $idValue);
$this->entityIdentifiers[$oid] = [$idField => $idValue];
$this->entityStates[$oid] = self::STATE_MANAGED;
$this->originalEntityData[$oid][$idField] = $idValue;
$this->addToIdentityMap($entity);
}
}

View File

@@ -34,10 +34,5 @@ parameters:
count: 1
path: lib/Doctrine/ORM/Tools/Console/Command/ClearCache/ResultCommand.php
-
message: '/^Call to an undefined method Doctrine\\Persistence\\Proxy::__setInitialized\(\)\.$/'
count: 1
path: lib/Doctrine/ORM/UnitOfWork.php
# Symfony cache supports passing a key prefix to the clear method.
- '/^Method Psr\\Cache\\CacheItemPoolInterface\:\:clear\(\) invoked with 1 parameter, 0 required\.$/'

View File

@@ -488,7 +488,6 @@
<code>getValue</code>
<code>setValue</code>
<code>setValue</code>
<code>setValue</code>
</PossiblyNullReference>
<PossiblyUndefinedArrayOffset>
<code><![CDATA[$class->associationMappings[$class->identifier[0]]['joinColumns']]]></code>
@@ -1381,11 +1380,6 @@
<code>$columnList</code>
</PossiblyUndefinedVariable>
</file>
<file src="lib/Doctrine/ORM/Proxy/Proxy.php">
<MissingTemplateParam>
<code>BaseProxy</code>
</MissingTemplateParam>
</file>
<file src="lib/Doctrine/ORM/Proxy/ProxyFactory.php">
<ArgumentTypeCoercion>
<code>$classMetadata</code>
@@ -1393,7 +1387,7 @@
<code>$classMetadata</code>
</ArgumentTypeCoercion>
<DirectConstructorCall>
<code><![CDATA[$proxy->__construct(static function (Proxy $object) use ($initializer, $proxy): void {
<code><![CDATA[$proxy->__construct(static function (InternalProxy $object) use ($initializer, $proxy): void {
$initializer($object, $proxy);
})]]></code>
</DirectConstructorCall>
@@ -2799,7 +2793,6 @@
<code>setValue</code>
</PossiblyNullReference>
<PossiblyUndefinedArrayOffset>
<code><![CDATA[$assoc['joinColumns']]]></code>
<code><![CDATA[$assoc['orphanRemoval']]]></code>
<code><![CDATA[$assoc['targetToSourceKeyColumns']]]></code>
</PossiblyUndefinedArrayOffset>
@@ -2808,10 +2801,6 @@
<code>unwrap</code>
<code>unwrap</code>
</PossiblyUndefinedMethod>
<RedundantCondition>
<code><![CDATA[$i >= 0 && $this->entityDeletions]]></code>
<code><![CDATA[$this->entityDeletions]]></code>
</RedundantCondition>
<RedundantConditionGivenDocblockType>
<code>is_array($entity)</code>
</RedundantConditionGivenDocblockType>

View File

@@ -45,6 +45,10 @@
<referencedClass name="Doctrine\ORM\Tools\Console\Command\GenerateRepositoriesCommand"/>
<referencedClass name="Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper"/>
<referencedClass name="Doctrine\ORM\Tools\Console\EntityManagerProvider\HelperSetManagerProvider"/>
<referencedClass name="Doctrine\ORM\Internal\CommitOrder\Edge"/>
<referencedClass name="Doctrine\ORM\Internal\CommitOrder\Vertex"/>
<referencedClass name="Doctrine\ORM\Internal\CommitOrder\VertexState"/>
<referencedClass name="Doctrine\ORM\Internal\CommitOrderCalculator"/>
</errorLevel>
</DeprecatedClass>
<DeprecatedConstant>

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace Doctrine\Performance\LazyLoading;
use Doctrine\ORM\Proxy\InternalProxy as Proxy;
use Doctrine\Performance\EntityManagerFactory;
use Doctrine\Performance\Mock\NonProxyLoadingEntityManager;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\Models\CMS\CmsEmployee;
use Doctrine\Tests\Models\CMS\CmsUser;

View File

@@ -21,6 +21,7 @@ abstract class DoctrineTestCase extends TestCase
'assertMatchesRegularExpression' => 'assertRegExp', // can be removed when PHPUnit 9 is minimum
'assertDoesNotMatchRegularExpression' => 'assertNotRegExp', // can be removed when PHPUnit 9 is minimum
'assertFileDoesNotExist' => 'assertFileNotExists', // can be removed PHPUnit 9 is minimum
'expectExceptionMessageMatches' => 'expectExceptionMessageRegExp', // can be removed when PHPUnit 8 is minimum
];
/**

View File

@@ -11,8 +11,6 @@ use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\EntityResult;
use Doctrine\ORM\Mapping\FieldResult;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\JoinTable;
use Doctrine\ORM\Mapping\ManyToMany;
@@ -67,14 +65,6 @@ use Doctrine\ORM\Mapping\SqlResultSetMappings;
#[ORM\Entity]
class CompanyFlexContract extends CompanyContract
{
/**
* @Id
* @GeneratedValue
* @Column(type="integer")
* @var int
*/
public $id;
/**
* @Column(type="integer")
* @var int

View File

@@ -101,7 +101,7 @@ class ConfigurationTest extends DoctrineTestCase
$paths = [__DIR__];
$reflectionClass = new ReflectionClass(ConfigurationTestAnnotationReaderChecker::class);
$annotationDriver = $this->configuration->newDefaultAnnotationDriver($paths, false);
$annotationDriver = $this->configuration->newDefaultAnnotationDriver($paths, false, true);
$reader = $annotationDriver->getReader();
$annotation = $reader->getMethodAnnotation(
$reflectionClass->getMethod('namespacedAnnotationMethod'),

View File

@@ -8,17 +8,19 @@ use Doctrine\ORM\EntityNotFoundException;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\ORMInvalidArgumentException;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Proxy\InternalProxy;
use Doctrine\ORM\Query;
use Doctrine\ORM\UnitOfWork;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\IterableTester;
use Doctrine\Tests\Models\CMS\CmsAddress;
use Doctrine\Tests\Models\CMS\CmsArticle;
use Doctrine\Tests\Models\CMS\CmsComment;
use Doctrine\Tests\Models\CMS\CmsGroup;
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\Tests\OrmFunctionalTestCase;
use InvalidArgumentException;
use RuntimeException;
use function get_class;
@@ -144,7 +146,7 @@ class BasicFunctionalTest extends OrmFunctionalTestCase
// Address has been eager-loaded because it cant be lazy
self::assertInstanceOf(CmsAddress::class, $user2->address);
self::assertNotInstanceOf(Proxy::class, $user2->address);
self::assertFalse($this->isUninitializedObject($user2->address));
}
/** @group DDC-1230 */
@@ -515,42 +517,76 @@ class BasicFunctionalTest extends OrmFunctionalTestCase
self::assertEquals(4, $gblanco2->getPhonenumbers()->count());
}
public function testSetSetAssociationWithGetReference(): void
public function testSetToOneAssociationWithGetReference(): void
{
$user = new CmsUser();
$user->name = 'Guilherme';
$user->username = 'gblanco';
$user->status = 'developer';
$this->_em->persist($user);
$address = new CmsAddress();
$address->country = 'Germany';
$address->city = 'Berlin';
$address->zip = '12345';
$this->_em->persist($address);
$this->_em->flush();
$this->_em->clear(CmsAddress::class);
self::assertFalse($this->_em->contains($address));
self::assertTrue($this->_em->contains($user));
// Assume we only got the identifier of the address and now want to attach
// that address to the user without actually loading it, using getReference().
$addressRef = $this->_em->getReference(CmsAddress::class, $address->getId());
$user->setAddress($addressRef); // Ugh! Initializes address 'cause of $address->setUser($user)!
$this->_em->flush();
$this->_em->clear();
// Assume we only got the identifier of the user and now want to attach
// the article to the user without actually loading it, using getReference().
$userRef = $this->_em->getReference(CmsUser::class, $user->getId());
self::assertTrue($this->isUninitializedObject($userRef));
$article = new CmsArticle();
$article->topic = 'topic';
$article->text = 'text';
$article->setAuthor($userRef);
$this->_em->persist($article);
$this->_em->flush();
self::assertFalse($userRef->__isInitialized());
$this->_em->clear();
// Check with a fresh load that the association is indeed there
$query = $this->_em->createQuery("select u, a from Doctrine\Tests\Models\CMS\CmsUser u join u.address a where u.username='gblanco'");
$query = $this->_em->createQuery("select u, a from Doctrine\Tests\Models\CMS\CmsUser u join u.articles a where u.username='gblanco'");
$gblanco = $query->getSingleResult();
self::assertInstanceOf(CmsUser::class, $gblanco);
self::assertInstanceOf(CmsAddress::class, $gblanco->getAddress());
self::assertEquals('Berlin', $gblanco->getAddress()->getCity());
self::assertInstanceOf(CmsArticle::class, $gblanco->articles[0]);
self::assertSame($article->id, $gblanco->articles[0]->id);
self::assertSame('text', $gblanco->articles[0]->text);
}
public function testAddToToManyAssociationWithGetReference(): void
{
$group = new CmsGroup();
$group->name = 'admins';
$this->_em->persist($group);
$this->_em->flush();
$this->_em->clear();
// Assume we only got the identifier of the user and now want to attach
// the article to the user without actually loading it, using getReference().
$groupRef = $this->_em->getReference(CmsGroup::class, $group->id);
self::assertTrue($this->isUninitializedObject($groupRef));
$user = new CmsUser();
$user->name = 'Guilherme';
$user->username = 'gblanco';
$user->groups->add($groupRef);
$this->_em->persist($user);
$this->_em->flush();
self::assertFalse($groupRef->__isInitialized());
$this->_em->clear();
// Check with a fresh load that the association is indeed there
$query = $this->_em->createQuery("select u, a from Doctrine\Tests\Models\CMS\CmsUser u join u.groups a where u.username='gblanco'");
$gblanco = $query->getSingleResult();
self::assertInstanceOf(CmsUser::class, $gblanco);
self::assertInstanceOf(CmsGroup::class, $gblanco->groups[0]);
self::assertSame($group->id, $gblanco->groups[0]->id);
self::assertSame('admins', $gblanco->groups[0]->name);
}
public function testOneToManyCascadeRemove(): void
@@ -707,9 +743,8 @@ class BasicFunctionalTest extends OrmFunctionalTestCase
->setParameter('user', $userRef)
->getSingleResult();
self::assertInstanceOf(Proxy::class, $address2->getUser());
self::assertTrue($userRef === $address2->getUser());
self::assertFalse($userRef->__isInitialized());
self::assertTrue($this->isUninitializedObject($userRef));
self::assertEquals('Germany', $address2->country);
self::assertEquals('Berlin', $address2->city);
self::assertEquals('12345', $address2->zip);
@@ -1006,8 +1041,8 @@ class BasicFunctionalTest extends OrmFunctionalTestCase
->setParameter(1, $article->id)
->setFetchMode(CmsArticle::class, 'user', ClassMetadata::FETCH_EAGER)
->getSingleResult();
self::assertInstanceOf(Proxy::class, $article->user, 'It IS a proxy, ...');
self::assertTrue($article->user->__isInitialized(), '...but its initialized!');
self::assertInstanceOf(InternalProxy::class, $article->user, 'It IS a proxy, ...');
self::assertFalse($this->isUninitializedObject($article->user), '...but its initialized!');
$this->assertQueryCount(2);
}
@@ -1291,4 +1326,33 @@ class BasicFunctionalTest extends OrmFunctionalTestCase
$this->_em->flush();
}
public function testItThrowsWhenReferenceUsesIdAssignedByDatabase(): void
{
$user = new CmsUser();
$user->name = 'test';
$user->username = 'test';
$this->_em->persist($user);
$this->_em->flush();
// Obtain a reference object for the next ID. This is a user error - references
// should be fetched only for existing IDs
$ref = $this->_em->getReference(CmsUser::class, $user->id + 1);
$user2 = new CmsUser();
$user2->name = 'test2';
$user2->username = 'test2';
// Now the database will assign an ID to the $user2 entity, but that place
// in the identity map is already taken by user error.
$this->expectException(RuntimeException::class);
$this->expectExceptionMessageMatches('/another object .* was already present for the same ID/');
// depending on ID generation strategy, the ID may be asssigned already here
// and the entity be put in the identity map
$this->_em->persist($user2);
// post insert IDs will be assigned during flush
$this->_em->flush();
}
}

View File

@@ -7,7 +7,6 @@ namespace Doctrine\Tests\ORM\Functional;
use DateTime;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\PersistentCollection;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\IterableTester;
use Doctrine\Tests\Models\Company\CompanyAuction;
use Doctrine\Tests\Models\Company\CompanyEmployee;
@@ -300,7 +299,7 @@ class ClassTableInheritanceTest extends OrmFunctionalTestCase
$mainEvent = $result[0]->getMainEvent();
// mainEvent should have been loaded because it can't be lazy
self::assertInstanceOf(CompanyAuction::class, $mainEvent);
self::assertNotInstanceOf(Proxy::class, $mainEvent);
self::assertFalse($this->isUninitializedObject($mainEvent));
$this->_em->clear();
@@ -432,13 +431,13 @@ class ClassTableInheritanceTest extends OrmFunctionalTestCase
$this->_em->clear();
$ref = $this->_em->getReference(CompanyPerson::class, $manager->getId());
self::assertNotInstanceOf(Proxy::class, $ref, 'Cannot Request a proxy from a class that has subclasses.');
self::assertFalse($this->isUninitializedObject($ref), 'Cannot Request a proxy from a class that has subclasses.');
self::assertInstanceOf(CompanyPerson::class, $ref);
self::assertInstanceOf(CompanyEmployee::class, $ref, 'Direct fetch of the reference has to load the child class Employee directly.');
$this->_em->clear();
$ref = $this->_em->getReference(CompanyManager::class, $manager->getId());
self::assertInstanceOf(Proxy::class, $ref, 'A proxy can be generated only if no subclasses exists for the requested reference.');
self::assertTrue($this->isUninitializedObject($ref), 'A proxy can be generated only if no subclasses exists for the requested reference.');
}
/** @group DDC-992 */

View File

@@ -43,7 +43,7 @@ class DefaultValuesTest extends OrmFunctionalTestCase
$user2 = $this->_em->getReference(get_class($user), $userId);
$this->_em->flush();
self::assertFalse($user2->__isInitialized());
self::assertTrue($this->isUninitializedObject($user2));
$a = new DefaultValueAddress();
$a->country = 'de';
@@ -55,7 +55,7 @@ class DefaultValuesTest extends OrmFunctionalTestCase
$this->_em->persist($a);
$this->_em->flush();
self::assertFalse($user2->__isInitialized());
self::assertTrue($this->isUninitializedObject($user2));
$this->_em->clear();
$a2 = $this->_em->find(get_class($a), $a->id);

View File

@@ -6,7 +6,6 @@ namespace Doctrine\Tests\ORM\Functional;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\Models\CMS\CmsAddress;
use Doctrine\Tests\Models\CMS\CmsArticle;
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
@@ -150,16 +149,13 @@ class DetachedEntityTest extends OrmFunctionalTestCase
$this->_em->clear();
$address2 = $this->_em->find(get_class($address), $address->id);
self::assertInstanceOf(Proxy::class, $address2->user);
self::assertFalse($address2->user->__isInitialized());
self::assertTrue($this->isUninitializedObject($address2->user));
$detachedAddress2 = unserialize(serialize($address2));
self::assertInstanceOf(Proxy::class, $detachedAddress2->user);
self::assertFalse($detachedAddress2->user->__isInitialized());
self::assertTrue($this->isUninitializedObject($detachedAddress2->user));
$managedAddress2 = $this->_em->merge($detachedAddress2);
self::assertInstanceOf(Proxy::class, $managedAddress2->user);
self::assertFalse($managedAddress2->user === $detachedAddress2->user);
self::assertFalse($managedAddress2->user->__isInitialized());
self::assertTrue($this->isUninitializedObject($managedAddress2->user));
}
/** @group DDC-822 */

View File

@@ -36,7 +36,7 @@ class EnumTest extends OrmFunctionalTestCase
{
parent::setUp();
$this->_em = $this->getEntityManager(null, new AttributeDriver([dirname(__DIR__, 2) . '/Models/Enums']));
$this->_em = $this->getEntityManager(null, new AttributeDriver([dirname(__DIR__, 2) . '/Models/Enums'], true));
$this->_schemaTool = new SchemaTool($this->_em);
if ($this->isSecondLevelCacheEnabled) {

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
use function uniqid;
/**
* @group GH7877
*/
class GH7877Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
GH7877ApplicationGeneratedIdEntity::class,
GH7877EntityWithNullableAssociation::class
);
}
public function testSelfReferenceWithApplicationGeneratedIdMayBeNotNullable(): void
{
$entity = new GH7877ApplicationGeneratedIdEntity();
$entity->parent = $entity;
$this->expectNotToPerformAssertions();
$this->_em->persist($entity);
$this->_em->flush();
}
public function testCrossReferenceWithApplicationGeneratedIdMayBeNotNullable(): void
{
$entity1 = new GH7877ApplicationGeneratedIdEntity();
$entity1->parent = $entity1;
$entity2 = new GH7877ApplicationGeneratedIdEntity();
$entity2->parent = $entity1;
$this->expectNotToPerformAssertions();
// As long as we do not have entity-level commit order computation
// (see https://github.com/doctrine/orm/pull/10547),
// this only works when the UoW processes $entity1 before $entity2,
// so that the foreign key constraint E2 -> E1 can be satisfied.
$this->_em->persist($entity1);
$this->_em->persist($entity2);
$this->_em->flush();
}
public function testNullableForeignKeysMakeInsertOrderLessRelevant(): void
{
$entity1 = new GH7877EntityWithNullableAssociation();
$entity1->parent = $entity1;
$entity2 = new GH7877EntityWithNullableAssociation();
$entity2->parent = $entity1;
$this->expectNotToPerformAssertions();
// In contrast to the previous test, this case demonstrates that with NULLable
// associations, even without entity-level commit order computation
// (see https://github.com/doctrine/orm/pull/10547), we can get away with an
// insertion order of E2 before E1. That is because the UoW will schedule an extra
// update that saves the day - the foreign key reference will established only after
// all insertions have been performed.
$this->_em->persist($entity2);
$this->_em->persist($entity1);
$this->_em->flush();
}
}
/**
* @ORM\Entity
*/
class GH7877ApplicationGeneratedIdEntity
{
/**
* @ORM\Id
* @ORM\Column(type="string")
* @ORM\GeneratedValue(strategy="NONE")
*
* @var string
*/
public $id;
/**
* (!) Note this uses "nullable=false"
*
* @ORM\ManyToOne(targetEntity="GH7877ApplicationGeneratedIdEntity")
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=false)
*
* @var self
*/
public $parent;
public function __construct()
{
$this->id = uniqid();
}
}
/**
* @ORM\Entity
*/
class GH7877EntityWithNullableAssociation
{
/**
* @ORM\Id
* @ORM\Column(type="string")
* @ORM\GeneratedValue(strategy="NONE")
*
* @var string
*/
public $id;
/**
* @ORM\ManyToOne(targetEntity="GH7877EntityWithNullableAssociation")
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true)
*
* @var self
*/
public $parent;
public function __construct()
{
$this->id = uniqid();
}
}

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\Models\DirectoryTree\Directory;
use Doctrine\Tests\Models\DirectoryTree\File;
use Doctrine\Tests\OrmFunctionalTestCase;
@@ -46,7 +45,7 @@ class MappedSuperclassTest extends OrmFunctionalTestCase
$cleanFile = $this->_em->find(get_class($file), $file->getId());
self::assertInstanceOf(Directory::class, $cleanFile->getParent());
self::assertInstanceOf(Proxy::class, $cleanFile->getParent());
self::assertTrue($this->isUninitializedObject($cleanFile->getParent()));
self::assertEquals($directory->getId(), $cleanFile->getParent()->getId());
self::assertInstanceOf(Directory::class, $cleanFile->getParent()->getParent());
self::assertEquals($root->getId(), $cleanFile->getParent()->getParent()->getId());

View File

@@ -11,7 +11,6 @@ use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\ORMSetup;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\DbalExtensions\Connection;
use Doctrine\Tests\DbalExtensions\QueryLog;
use Doctrine\Tests\Models\Generic\DateTimeModel;
@@ -48,8 +47,8 @@ class MergeProxiesTest extends OrmFunctionalTestCase
self::assertSame($managed, $this->_em->merge($detachedUninitialized));
self::assertFalse($managed->__isInitialized());
self::assertFalse($detachedUninitialized->__isInitialized());
self::assertTrue($this->isUninitializedObject($managed));
self::assertTrue($this->isUninitializedObject($detachedUninitialized));
}
/**
@@ -71,8 +70,8 @@ class MergeProxiesTest extends OrmFunctionalTestCase
$this->_em->merge(unserialize(serialize($this->_em->merge($detachedUninitialized))))
);
self::assertFalse($managed->__isInitialized());
self::assertFalse($detachedUninitialized->__isInitialized());
self::assertTrue($this->isUninitializedObject($managed));
self::assertTrue($this->isUninitializedObject($detachedUninitialized));
}
/**
@@ -87,7 +86,7 @@ class MergeProxiesTest extends OrmFunctionalTestCase
self::assertSame($managed, $this->_em->merge($managed));
self::assertFalse($managed->__isInitialized());
self::assertTrue($this->isUninitializedObject($managed));
}
/**
@@ -109,13 +108,12 @@ class MergeProxiesTest extends OrmFunctionalTestCase
$managed = $this->_em->getReference(DateTimeModel::class, $date->id);
self::assertInstanceOf(Proxy::class, $managed);
self::assertFalse($managed->__isInitialized());
self::assertTrue($this->isUninitializedObject($managed));
$date->date = $dateTime = new DateTime();
self::assertSame($managed, $this->_em->merge($date));
self::assertTrue($managed->__isInitialized());
self::assertFalse($this->isUninitializedObject($managed));
self::assertSame($dateTime, $managed->date, 'Data was merged into the proxy after initialization');
}
@@ -150,8 +148,8 @@ class MergeProxiesTest extends OrmFunctionalTestCase
self::assertNotSame($proxy1, $merged2);
self::assertSame($proxy2, $merged2);
self::assertFalse($proxy1->__isInitialized());
self::assertFalse($proxy2->__isInitialized());
self::assertTrue($this->isUninitializedObject($proxy1));
self::assertTrue($this->isUninitializedObject($proxy2));
$proxy1->__load();
@@ -207,9 +205,8 @@ class MergeProxiesTest extends OrmFunctionalTestCase
$unManagedProxy = $em1->getReference(DateTimeModel::class, $file1->id);
$mergedInstance = $em2->merge($unManagedProxy);
self::assertNotInstanceOf(Proxy::class, $mergedInstance);
self::assertNotSame($unManagedProxy, $mergedInstance);
self::assertFalse($unManagedProxy->__isInitialized());
self::assertFalse($this->isUninitializedObject($mergedInstance));
self::assertTrue($this->isUninitializedObject($unManagedProxy));
self::assertCount(
0,
@@ -242,7 +239,9 @@ class MergeProxiesTest extends OrmFunctionalTestCase
TestUtil::configureProxies($config);
$config->setMetadataDriverImpl(ORMSetup::createDefaultAnnotationDriver(
[realpath(__DIR__ . '/../../Models/Cache')]
[realpath(__DIR__ . '/../../Models/Cache')],
null,
true
));
// always runs on sqlite to prevent multi-connection race-conditions with the test suite

View File

@@ -6,7 +6,6 @@ namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\Models\ECommerce\ECommerceFeature;
use Doctrine\Tests\Models\ECommerce\ECommerceProduct;
use Doctrine\Tests\OrmFunctionalTestCase;
@@ -90,12 +89,12 @@ class OneToManyBidirectionalAssociationTest extends OrmFunctionalTestCase
$features = $product->getFeatures();
self::assertInstanceOf(ECommerceFeature::class, $features[0]);
self::assertNotInstanceOf(Proxy::class, $features[0]->getProduct());
self::assertFalse($this->isUninitializedObject($features[0]->getProduct()));
self::assertSame($product, $features[0]->getProduct());
self::assertEquals('Model writing tutorial', $features[0]->getDescription());
self::assertInstanceOf(ECommerceFeature::class, $features[1]);
self::assertSame($product, $features[1]->getProduct());
self::assertNotInstanceOf(Proxy::class, $features[1]->getProduct());
self::assertFalse($this->isUninitializedObject($features[1]->getProduct()));
self::assertEquals('Annotations examples', $features[1]->getDescription());
}
@@ -126,11 +125,10 @@ class OneToManyBidirectionalAssociationTest extends OrmFunctionalTestCase
$features = $query->getResult();
$product = $features[0]->getProduct();
self::assertInstanceOf(Proxy::class, $product);
self::assertInstanceOf(ECommerceProduct::class, $product);
self::assertFalse($product->__isInitialized());
self::assertTrue($this->isUninitializedObject($product));
self::assertSame('Doctrine Cookbook', $product->getName());
self::assertTrue($product->__isInitialized());
self::assertFalse($this->isUninitializedObject($product));
}
public function testLazyLoadsObjectsOnTheInverseSide2(): void
@@ -141,7 +139,7 @@ class OneToManyBidirectionalAssociationTest extends OrmFunctionalTestCase
$features = $query->getResult();
$product = $features[0]->getProduct();
self::assertNotInstanceOf(Proxy::class, $product);
self::assertFalse($this->isUninitializedObject($product));
self::assertInstanceOf(ECommerceProduct::class, $product);
self::assertSame('Doctrine Cookbook', $product->getName());

View File

@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\Models\ECommerce\ECommerceCart;
use Doctrine\Tests\Models\ECommerce\ECommerceCustomer;
use Doctrine\Tests\OrmFunctionalTestCase;
@@ -100,7 +99,7 @@ class OneToOneBidirectionalAssociationTest extends OrmFunctionalTestCase
self::assertNull($customer->getMentor());
self::assertInstanceOf(ECommerceCart::class, $customer->getCart());
self::assertNotInstanceOf(Proxy::class, $customer->getCart());
self::assertFalse($this->isUninitializedObject($customer->getCart()));
self::assertEquals('paypal', $customer->getCart()->getPayment());
}

View File

@@ -14,7 +14,6 @@ use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Mapping\OneToOne;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\OrmFunctionalTestCase;
use function get_class;
@@ -52,7 +51,7 @@ class OneToOneEagerLoadingTest extends OrmFunctionalTestCase
$this->getQueryLog()->reset()->enable();
$train = $this->_em->find(get_class($train), $train->id);
self::assertNotInstanceOf(Proxy::class, $train->driver);
self::assertFalse($this->isUninitializedObject($train->driver));
self::assertEquals('Benjamin', $train->driver->name);
$this->assertQueryCount(1);
@@ -70,7 +69,6 @@ class OneToOneEagerLoadingTest extends OrmFunctionalTestCase
$this->getQueryLog()->reset()->enable();
$train = $this->_em->find(get_class($train), $train->id);
self::assertNotInstanceOf(Proxy::class, $train->driver);
self::assertNull($train->driver);
$this->assertQueryCount(1);
@@ -88,9 +86,9 @@ class OneToOneEagerLoadingTest extends OrmFunctionalTestCase
$this->getQueryLog()->reset()->enable();
$driver = $this->_em->find(get_class($owner), $owner->id);
self::assertNotInstanceOf(Proxy::class, $owner->train);
self::assertNotNull($owner->train);
$this->_em->find(get_class($owner), $owner->id);
self::assertFalse($this->isUninitializedObject($owner->train));
self::assertInstanceOf(Train::class, $owner->train);
$this->assertQueryCount(1);
}
@@ -109,7 +107,6 @@ class OneToOneEagerLoadingTest extends OrmFunctionalTestCase
$this->getQueryLog()->reset()->enable();
$driver = $this->_em->find(get_class($driver), $driver->id);
self::assertNotInstanceOf(Proxy::class, $driver->train);
self::assertNull($driver->train);
$this->assertQueryCount(1);
@@ -126,8 +123,8 @@ class OneToOneEagerLoadingTest extends OrmFunctionalTestCase
$this->_em->clear();
$waggon = $this->_em->find(get_class($waggon), $waggon->id);
self::assertNotInstanceOf(Proxy::class, $waggon->train);
self::assertNotNull($waggon->train);
self::assertFalse($this->isUninitializedObject($waggon->train));
self::assertInstanceOf(Train::class, $waggon->train);
}
/** @group non-cacheable */

View File

@@ -11,7 +11,6 @@ use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\OneToOne;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\Models\ECommerce\ECommerceCustomer;
use Doctrine\Tests\OrmFunctionalTestCase;
@@ -69,14 +68,16 @@ class OneToOneSelfReferentialAssociationTest extends OrmFunctionalTestCase
$id = $this->createFixture();
$customer = $this->_em->find(ECommerceCustomer::class, $id);
self::assertNotInstanceOf(Proxy::class, $customer->getMentor());
self::assertFalse($this->isUninitializedObject($customer->getMentor()));
}
public function testEagerLoadsAssociation(): void
{
$this->createFixture();
$customerId = $this->createFixture();
$query = $this->_em->createQuery('select c, m from Doctrine\Tests\Models\ECommerce\ECommerceCustomer c left join c.mentor m where c.id = :id');
$query->setParameter('id', $customerId);
$query = $this->_em->createQuery('select c, m from Doctrine\Tests\Models\ECommerce\ECommerceCustomer c left join c.mentor m order by c.id asc');
$result = $query->getResult();
$customer = $result[0];
$this->assertLoadingOfAssociation($customer);

View File

@@ -59,7 +59,7 @@ class ProxiesLikeEntitiesTest extends OrmFunctionalTestCase
{
// Considering case (a)
$proxy = $this->_em->getProxyFactory()->getProxy(CmsUser::class, ['id' => 123]);
$proxy->__setInitialized(true);
$proxy->id = null;
$proxy->username = 'ocra';
$proxy->name = 'Marco';
@@ -84,7 +84,7 @@ class ProxiesLikeEntitiesTest extends OrmFunctionalTestCase
$this->_em->persist($uninitializedProxy);
$this->_em->flush();
self::assertFalse($uninitializedProxy->__isInitialized(), 'Proxy didn\'t get initialized during flush operations');
self::assertTrue($this->isUninitializedObject($uninitializedProxy), 'Proxy didn\'t get initialized during flush operations');
self::assertEquals($userId, $uninitializedProxy->getId());
$this->_em->remove($uninitializedProxy);
$this->_em->flush();
@@ -95,7 +95,7 @@ class ProxiesLikeEntitiesTest extends OrmFunctionalTestCase
*/
public function testProxyAsDqlParameterPersist(): void
{
$proxy = $this->_em->getProxyFactory()->getProxy(CmsUser::class, ['id' => $this->user->getId()]);
$proxy = $this->_em->getReference(CmsUser::class, ['id' => $this->user->getId()]);
$proxy->id = $this->user->getId();
$result = $this
->_em

View File

@@ -13,7 +13,6 @@ use Doctrine\ORM\Query;
use Doctrine\ORM\Query\Parameter;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\UnexpectedResultException;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\IterableTester;
use Doctrine\Tests\Models\CMS\CmsArticle;
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
@@ -624,8 +623,7 @@ class QueryTest extends OrmFunctionalTestCase
self::assertEquals(1, count($result));
self::assertInstanceOf(CmsArticle::class, $result[0]);
self::assertEquals('dr. dolittle', $result[0]->topic);
self::assertInstanceOf(Proxy::class, $result[0]->user);
self::assertFalse($result[0]->user->__isInitialized());
self::assertTrue($this->isUninitializedObject($result[0]->user));
}
/** @group DDC-952 */
@@ -653,7 +651,7 @@ class QueryTest extends OrmFunctionalTestCase
self::assertCount(10, $articles);
foreach ($articles as $article) {
self::assertNotInstanceOf(Proxy::class, $article);
self::assertFalse($this->isUninitializedObject($article));
}
}

View File

@@ -26,7 +26,8 @@ class ReadonlyPropertiesTest extends OrmFunctionalTestCase
}
$this->_em = $this->getEntityManager(null, new AttributeDriver(
[dirname(__DIR__, 2) . '/Models/ReadonlyProperties']
[dirname(__DIR__, 2) . '/Models/ReadonlyProperties'],
true
));
$this->_schemaTool = new SchemaTool($this->_em);

View File

@@ -6,7 +6,7 @@ namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Common\Proxy\Proxy as CommonProxy;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\Persistence\Proxy;
use Doctrine\ORM\Proxy\InternalProxy;
use Doctrine\Tests\Models\Company\CompanyAuction;
use Doctrine\Tests\Models\ECommerce\ECommerceProduct;
use Doctrine\Tests\Models\ECommerce\ECommerceShipping;
@@ -120,9 +120,9 @@ class ReferenceProxyTest extends OrmFunctionalTestCase
$entity = $this->_em->getReference(ECommerceProduct::class, $id);
assert($entity instanceof ECommerceProduct);
self::assertFalse($entity->__isInitialized(), 'Pre-Condition: Object is unitialized proxy.');
self::assertTrue($this->isUninitializedObject($entity), 'Pre-Condition: Object is unitialized proxy.');
$this->_em->getUnitOfWork()->initializeObject($entity);
self::assertTrue($entity->__isInitialized(), 'Should be initialized after called UnitOfWork::initializeObject()');
self::assertFalse($this->isUninitializedObject($entity), 'Should be initialized after called UnitOfWork::initializeObject()');
}
/** @group DDC-1163 */
@@ -167,9 +167,9 @@ class ReferenceProxyTest extends OrmFunctionalTestCase
$entity = $this->_em->getReference(ECommerceProduct::class, $id);
assert($entity instanceof ECommerceProduct);
self::assertFalse($entity->__isInitialized(), 'Pre-Condition: Object is unitialized proxy.');
self::assertTrue($this->isUninitializedObject($entity), 'Pre-Condition: Object is unitialized proxy.');
self::assertEquals($id, $entity->getId());
self::assertFalse($entity->__isInitialized(), "Getting the identifier doesn't initialize the proxy.");
self::assertTrue($this->isUninitializedObject($entity), "Getting the identifier doesn't initialize the proxy.");
}
/** @group DDC-1625 */
@@ -180,9 +180,9 @@ class ReferenceProxyTest extends OrmFunctionalTestCase
$entity = $this->_em->getReference(CompanyAuction::class, $id);
assert($entity instanceof CompanyAuction);
self::assertFalse($entity->__isInitialized(), 'Pre-Condition: Object is unitialized proxy.');
self::assertTrue($this->isUninitializedObject($entity), 'Pre-Condition: Object is unitialized proxy.');
self::assertEquals($id, $entity->getId());
self::assertFalse($entity->__isInitialized(), "Getting the identifier doesn't initialize the proxy when extending.");
self::assertTrue($this->isUninitializedObject($entity), "Getting the identifier doesn't initialize the proxy when extending.");
}
public function testDoNotInitializeProxyOnGettingTheIdentifierAndReturnTheRightType(): void
@@ -202,10 +202,10 @@ class ReferenceProxyTest extends OrmFunctionalTestCase
$product = $this->_em->getRepository(ECommerceProduct::class)->find($product->getId());
$entity = $product->getShipping();
self::assertFalse($entity->__isInitialized(), 'Pre-Condition: Object is unitialized proxy.');
self::assertTrue($this->isUninitializedObject($entity), 'Pre-Condition: Object is unitialized proxy.');
self::assertEquals($id, $entity->getId());
self::assertSame($id, $entity->getId(), "Check that the id's are the same value, and type.");
self::assertFalse($entity->__isInitialized(), "Getting the identifier doesn't initialize the proxy.");
self::assertTrue($this->isUninitializedObject($entity), "Getting the identifier doesn't initialize the proxy.");
}
public function testInitializeProxyOnGettingSomethingOtherThanTheIdentifier(): void
@@ -215,9 +215,9 @@ class ReferenceProxyTest extends OrmFunctionalTestCase
$entity = $this->_em->getReference(ECommerceProduct::class, $id);
assert($entity instanceof ECommerceProduct);
self::assertFalse($entity->__isInitialized(), 'Pre-Condition: Object is unitialized proxy.');
self::assertTrue($this->isUninitializedObject($entity), 'Pre-Condition: Object is unitialized proxy.');
self::assertEquals('Doctrine Cookbook', $entity->getName());
self::assertTrue($entity->__isInitialized(), 'Getting something other than the identifier initializes the proxy.');
self::assertFalse($this->isUninitializedObject($entity), 'Getting something other than the identifier initializes the proxy.');
}
/** @group DDC-1604 */
@@ -229,8 +229,8 @@ class ReferenceProxyTest extends OrmFunctionalTestCase
assert($entity instanceof ECommerceProduct);
$className = ClassUtils::getClass($entity);
self::assertInstanceOf(Proxy::class, $entity);
self::assertFalse($entity->__isInitialized());
self::assertInstanceOf(InternalProxy::class, $entity);
self::assertTrue($this->isUninitializedObject($entity));
self::assertEquals(ECommerceProduct::class, $className);
$restName = str_replace($this->_em->getConfiguration()->getProxyNamespace(), '', get_class($entity));
@@ -239,6 +239,6 @@ class ReferenceProxyTest extends OrmFunctionalTestCase
self::assertTrue(file_exists($proxyFileName), 'Proxy file name cannot be found generically.');
$entity->__load();
self::assertTrue($entity->__isInitialized());
self::assertFalse($this->isUninitializedObject($entity));
}
}

View File

@@ -10,9 +10,9 @@ use Doctrine\ORM\Cache\EntityCacheEntry;
use Doctrine\ORM\Cache\EntityCacheKey;
use Doctrine\ORM\Cache\Exception\CacheException;
use Doctrine\ORM\Cache\QueryCacheKey;
use Doctrine\ORM\Proxy\InternalProxy;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\Models\Cache\Attraction;
use Doctrine\Tests\Models\Cache\City;
use Doctrine\Tests\Models\Cache\Country;
@@ -938,7 +938,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheFunctionalTestCase
self::assertNotNull($state1->getCountry());
$this->assertQueryCount(1);
self::assertInstanceOf(State::class, $state1);
self::assertInstanceOf(Proxy::class, $state1->getCountry());
self::assertInstanceOf(InternalProxy::class, $state1->getCountry());
self::assertEquals($countryName, $state1->getCountry()->getName());
self::assertEquals($stateId, $state1->getId());
@@ -956,7 +956,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheFunctionalTestCase
self::assertNotNull($state2->getCountry());
$this->assertQueryCount(0);
self::assertInstanceOf(State::class, $state2);
self::assertInstanceOf(Proxy::class, $state2->getCountry());
self::assertInstanceOf(InternalProxy::class, $state2->getCountry());
self::assertEquals($countryName, $state2->getCountry()->getName());
self::assertEquals($stateId, $state2->getId());
}
@@ -1030,7 +1030,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheFunctionalTestCase
$this->assertQueryCount(1);
self::assertInstanceOf(State::class, $state1);
self::assertInstanceOf(Proxy::class, $state1->getCountry());
self::assertInstanceOf(InternalProxy::class, $state1->getCountry());
self::assertInstanceOf(City::class, $state1->getCities()->get(0));
self::assertInstanceOf(State::class, $state1->getCities()->get(0)->getState());
self::assertSame($state1, $state1->getCities()->get(0)->getState());
@@ -1047,7 +1047,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheFunctionalTestCase
$this->assertQueryCount(0);
self::assertInstanceOf(State::class, $state2);
self::assertInstanceOf(Proxy::class, $state2->getCountry());
self::assertInstanceOf(InternalProxy::class, $state2->getCountry());
self::assertInstanceOf(City::class, $state2->getCities()->get(0));
self::assertInstanceOf(State::class, $state2->getCities()->get(0)->getState());
self::assertSame($state2, $state2->getCities()->get(0)->getState());

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Persistence\Proxy;
use Doctrine\ORM\Proxy\InternalProxy;
use Doctrine\Tests\Models\Cache\Country;
use Doctrine\Tests\Models\Cache\State;
@@ -197,8 +197,8 @@ class SecondLevelCacheRepositoryTest extends SecondLevelCacheFunctionalTestCase
self::assertInstanceOf(State::class, $entities[1]);
self::assertInstanceOf(Country::class, $entities[0]->getCountry());
self::assertInstanceOf(Country::class, $entities[0]->getCountry());
self::assertInstanceOf(Proxy::class, $entities[0]->getCountry());
self::assertInstanceOf(Proxy::class, $entities[1]->getCountry());
self::assertInstanceOf(InternalProxy::class, $entities[0]->getCountry());
self::assertInstanceOf(InternalProxy::class, $entities[1]->getCountry());
// load from cache
$this->getQueryLog()->reset()->enable();
@@ -211,8 +211,8 @@ class SecondLevelCacheRepositoryTest extends SecondLevelCacheFunctionalTestCase
self::assertInstanceOf(State::class, $entities[1]);
self::assertInstanceOf(Country::class, $entities[0]->getCountry());
self::assertInstanceOf(Country::class, $entities[1]->getCountry());
self::assertInstanceOf(Proxy::class, $entities[0]->getCountry());
self::assertInstanceOf(Proxy::class, $entities[1]->getCountry());
self::assertInstanceOf(InternalProxy::class, $entities[0]->getCountry());
self::assertInstanceOf(InternalProxy::class, $entities[1]->getCountry());
// invalidate cache
$this->_em->persist(new State('foo', $this->_em->find(Country::class, $this->countries[0]->getId())));
@@ -230,8 +230,8 @@ class SecondLevelCacheRepositoryTest extends SecondLevelCacheFunctionalTestCase
self::assertInstanceOf(State::class, $entities[1]);
self::assertInstanceOf(Country::class, $entities[0]->getCountry());
self::assertInstanceOf(Country::class, $entities[1]->getCountry());
self::assertInstanceOf(Proxy::class, $entities[0]->getCountry());
self::assertInstanceOf(Proxy::class, $entities[1]->getCountry());
self::assertInstanceOf(InternalProxy::class, $entities[0]->getCountry());
self::assertInstanceOf(InternalProxy::class, $entities[1]->getCountry());
// load from cache
$this->getQueryLog()->reset()->enable();
@@ -244,7 +244,7 @@ class SecondLevelCacheRepositoryTest extends SecondLevelCacheFunctionalTestCase
self::assertInstanceOf(State::class, $entities[1]);
self::assertInstanceOf(Country::class, $entities[0]->getCountry());
self::assertInstanceOf(Country::class, $entities[1]->getCountry());
self::assertInstanceOf(Proxy::class, $entities[0]->getCountry());
self::assertInstanceOf(Proxy::class, $entities[1]->getCountry());
self::assertInstanceOf(InternalProxy::class, $entities[0]->getCountry());
self::assertInstanceOf(InternalProxy::class, $entities[1]->getCountry());
}
}

View File

@@ -7,7 +7,6 @@ namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Persisters\MatchingAssociationFieldRequiresObject;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\Models\Company\CompanyContract;
use Doctrine\Tests\Models\Company\CompanyEmployee;
use Doctrine\Tests\Models\Company\CompanyFixContract;
@@ -397,13 +396,14 @@ class SingleTableInheritanceTest extends OrmFunctionalTestCase
$this->loadFullFixture();
$ref = $this->_em->getReference(CompanyContract::class, $this->fix->getId());
self::assertNotInstanceOf(Proxy::class, $ref, 'Cannot Request a proxy from a class that has subclasses.');
self::assertFalse($this->isUninitializedObject($ref), 'Cannot Request a proxy from a class that has subclasses.');
self::assertInstanceOf(CompanyContract::class, $ref);
self::assertInstanceOf(CompanyFixContract::class, $ref, 'Direct fetch of the reference has to load the child class Employee directly.');
$this->_em->clear();
$ref = $this->_em->getReference(CompanyFixContract::class, $this->fix->getId());
self::assertInstanceOf(Proxy::class, $ref, 'A proxy can be generated only if no subclasses exists for the requested reference.');
self::assertTrue($this->isUninitializedObject($ref), 'A proxy can be generated only if no subclasses exists for the requested reference.');
}
/** @group DDC-952 */
@@ -417,6 +417,6 @@ class SingleTableInheritanceTest extends OrmFunctionalTestCase
->setParameter(1, $this->fix->getId())
->getSingleResult();
self::assertNotInstanceOf(Proxy::class, $contract->getSalesPerson());
self::assertFalse($this->isUninitializedObject($contract->getSalesPerson()));
}
}

View File

@@ -15,7 +15,6 @@ use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\JoinColumns;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToOne;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\OrmFunctionalTestCase;
use function assert;
@@ -92,7 +91,7 @@ class DDC1163Test extends OrmFunctionalTestCase
assert($specialProduct instanceof DDC1163SpecialProduct);
self::assertInstanceOf(DDC1163SpecialProduct::class, $specialProduct);
self::assertInstanceOf(Proxy::class, $specialProduct);
self::assertTrue($this->isUninitializedObject($specialProduct));
$specialProduct->setSubclassProperty('foobar');

View File

@@ -45,7 +45,7 @@ class DDC1193Test extends OrmFunctionalTestCase
$company = $this->_em->find(get_class($company), $companyId);
self::assertTrue($this->_em->getUnitOfWork()->isInIdentityMap($company), 'Company is in identity map.');
self::assertFalse($company->member->__isInitialized(), 'Pre-Condition');
self::assertTrue($this->isUninitializedObject($company->member), 'Pre-Condition');
self::assertTrue($this->_em->getUnitOfWork()->isInIdentityMap($company->member), 'Member is in identity map.');
$this->_em->remove($company);

View File

@@ -38,9 +38,9 @@ class DDC1228Test extends OrmFunctionalTestCase
$user = $this->_em->find(DDC1228User::class, $user->id);
self::assertFalse($user->getProfile()->__isInitialized(), 'Proxy is not initialized');
self::assertTrue($this->isUninitializedObject($user->getProfile()), 'Proxy is not initialized');
$user->getProfile()->setName('Bar');
self::assertTrue($user->getProfile()->__isInitialized(), 'Proxy is not initialized');
self::assertFalse($this->isUninitializedObject($user->getProfile()), 'Proxy is not initialized');
self::assertEquals('Bar', $user->getProfile()->getName());
self::assertEquals(['id' => 1, 'name' => 'Foo'], $this->_em->getUnitOfWork()->getOriginalEntityData($user->getProfile()));

View File

@@ -56,7 +56,7 @@ class DDC1238Test extends OrmFunctionalTestCase
$user2 = $this->_em->getReference(DDC1238User::class, $userId);
$user->__load();
//$user->__load();
self::assertIsInt($user->getId(), 'Even if a proxy is detached, it should still have an identifier');

View File

@@ -12,7 +12,6 @@ use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\Models\CMS\CmsAddress;
use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\Tests\OrmFunctionalTestCase;
@@ -54,7 +53,7 @@ class DDC1452Test extends OrmFunctionalTestCase
$results = $this->_em->createQuery($dql)->setMaxResults(1)->getResult();
self::assertSame($results[0], $results[0]->entitiesB[0]->entityAFrom);
self::assertNotInstanceOf(Proxy::class, $results[0]->entitiesB[0]->entityATo);
self::assertFalse($this->isUninitializedObject($results[0]->entitiesB[0]->entityATo));
self::assertInstanceOf(Collection::class, $results[0]->entitiesB[0]->entityATo->getEntitiesB());
}
@@ -82,12 +81,12 @@ class DDC1452Test extends OrmFunctionalTestCase
$data = $this->_em->createQuery($dql)->getResult();
$this->_em->clear();
self::assertNotInstanceOf(Proxy::class, $data[0]->user);
self::assertFalse($this->isUninitializedObject($data[0]->user));
$dql = 'SELECT u, a FROM Doctrine\Tests\Models\CMS\CmsUser u INNER JOIN u.address a';
$data = $this->_em->createQuery($dql)->getResult();
self::assertNotInstanceOf(Proxy::class, $data[0]->address);
self::assertFalse($this->isUninitializedObject($data[0]->address));
}
}

View File

@@ -12,7 +12,6 @@ use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToOne;
use Doctrine\Persistence\NotifyPropertyChanged;
use Doctrine\Persistence\PropertyChangedListener;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\OrmFunctionalTestCase;
use function count;
@@ -57,9 +56,10 @@ class DDC1690Test extends OrmFunctionalTestCase
$child = $this->_em->find(DDC1690Child::class, $childId);
self::assertEquals(1, count($parent->listeners));
self::assertInstanceOf(Proxy::class, $child, 'Verifying that $child is a proxy before using proxy API');
self::assertCount(0, $child->listeners);
$child->__load();
$this->_em->getUnitOfWork()->initializeObject($child);
self::assertCount(1, $child->listeners);
unset($parent, $child);

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Persistence\Proxy;
use Doctrine\ORM\Proxy\InternalProxy;
use Doctrine\Tests\Models\CMS\CmsGroup;
use Doctrine\Tests\OrmFunctionalTestCase;
@@ -37,8 +37,7 @@ class DDC1734Test extends OrmFunctionalTestCase
$proxy = $this->getProxy($group);
self::assertInstanceOf(Proxy::class, $proxy);
self::assertFalse($proxy->__isInitialized());
self::assertTrue($this->isUninitializedObject($proxy));
$this->_em->detach($proxy);
$this->_em->clear();
@@ -67,8 +66,7 @@ class DDC1734Test extends OrmFunctionalTestCase
$proxy = $this->getProxy($group);
self::assertInstanceOf(Proxy::class, $proxy);
self::assertFalse($proxy->__isInitialized());
self::assertTrue($this->isUninitializedObject($proxy));
$this->_em->detach($proxy);
$serializedProxy = serialize($proxy);
@@ -79,7 +77,7 @@ class DDC1734Test extends OrmFunctionalTestCase
}
/** @param object $object */
private function getProxy($object): Proxy
private function getProxy($object): InternalProxy
{
$metadataFactory = $this->_em->getMetadataFactory();
$className = get_class($object);

View File

@@ -12,7 +12,6 @@ use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToOne;
use Doctrine\Persistence\NotifyPropertyChanged;
use Doctrine\Persistence\PropertyChangedListener;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\OrmFunctionalTestCase;
use function assert;
@@ -47,10 +46,8 @@ class DDC2230Test extends OrmFunctionalTestCase
$mergedUser = $this->_em->merge($user);
$address = $mergedUser->address;
assert($address instanceof Proxy);
self::assertInstanceOf(Proxy::class, $address);
self::assertFalse($address->__isInitialized());
self::assertTrue($this->isUninitializedObject($address));
}
public function testNotifyTrackingCalledOnProxyInitialization(): void
@@ -62,12 +59,12 @@ class DDC2230Test extends OrmFunctionalTestCase
$this->_em->clear();
$addressProxy = $this->_em->getReference(DDC2230Address::class, $insertedAddress->id);
assert($addressProxy instanceof Proxy || $addressProxy instanceof DDC2230Address);
assert($addressProxy instanceof DDC2230Address);
self::assertFalse($addressProxy->__isInitialized());
self::assertTrue($this->isUninitializedObject($addressProxy));
self::assertNull($addressProxy->listener);
$addressProxy->__load();
$this->_em->getUnitOfWork()->initializeObject($addressProxy);
self::assertSame($this->_em->getUnitOfWork(), $addressProxy->listener);
}

View File

@@ -12,7 +12,6 @@ use Doctrine\ORM\Mapping\Table;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\ObjectManager;
use Doctrine\Persistence\ObjectManagerAware;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\OrmFunctionalTestCase;
use function get_class;
@@ -43,12 +42,11 @@ class DDC2231Test extends OrmFunctionalTestCase
$y1ref = $this->_em->getReference(get_class($y1), $y1->id);
self::assertInstanceOf(Proxy::class, $y1ref);
self::assertFalse($y1ref->__isInitialized());
self::assertTrue($this->isUninitializedObject($y1ref));
$id = $y1ref->doSomething();
self::assertTrue($y1ref->__isInitialized());
self::assertFalse($this->isUninitializedObject($y1ref));
self::assertEquals($this->_em, $y1ref->om);
}
}

View File

@@ -12,7 +12,6 @@ use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\OrmFunctionalTestCase;
use function assert;
@@ -63,16 +62,15 @@ class DDC2306Test extends OrmFunctionalTestCase
$address = $this->_em->find(DDC2306Address::class, $address->id);
assert($address instanceof DDC2306Address);
$user = $address->users->first()->user;
assert($user instanceof DDC2306User || $user instanceof Proxy);
self::assertInstanceOf(Proxy::class, $user);
$this->assertTrue($this->isUninitializedObject($user));
self::assertInstanceOf(DDC2306User::class, $user);
$userId = $user->id;
self::assertNotNull($userId);
$user->__load();
$this->_em->getUnitOfWork()->initializeObject($user);
self::assertEquals(
$userId,

View File

@@ -11,7 +11,6 @@ use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\OneToOne;
use Doctrine\ORM\Mapping\Table;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\OrmFunctionalTestCase;
use function get_class;
@@ -50,16 +49,14 @@ class DDC237Test extends OrmFunctionalTestCase
$this->_em->clear();
$x2 = $this->_em->find(get_class($x), $x->id); // proxy injected for Y
self::assertInstanceOf(Proxy::class, $x2->y);
self::assertFalse($x2->y->__isInitialized());
self::assertTrue($this->isUninitializedObject($x2->y));
// proxy for Y is in identity map
$z2 = $this->_em->createQuery('select z,y from ' . get_class($z) . ' z join z.y y where z.id = ?1')
->setParameter(1, $z->id)
->getSingleResult();
self::assertInstanceOf(Proxy::class, $z2->y);
self::assertTrue($z2->y->__isInitialized());
self::assertFalse($this->isUninitializedObject($z2->y));
self::assertEquals('Y', $z2->y->data);
self::assertEquals($y->id, $z2->y->id);
@@ -69,7 +66,6 @@ class DDC237Test extends OrmFunctionalTestCase
self::assertNotSame($x, $x2);
self::assertNotSame($z, $z2);
self::assertSame($z2->y, $x2->y);
self::assertInstanceOf(Proxy::class, $z2->y);
}
}

View File

@@ -15,7 +15,6 @@ use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Mapping\Table;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\OrmFunctionalTestCase;
/**
@@ -61,21 +60,20 @@ class DDC2494Test extends OrmFunctionalTestCase
$this->getQueryLog()->reset()->enable();
self::assertInstanceOf(Proxy::class, $item->getCurrency());
self::assertFalse($item->getCurrency()->__isInitialized());
self::assertTrue($this->isUninitializedObject($item->getCurrency()));
self::assertArrayHasKey('convertToPHPValue', DDC2494TinyIntType::$calls);
self::assertCount(1, DDC2494TinyIntType::$calls['convertToPHPValue']);
self::assertIsInt($item->getCurrency()->getId());
self::assertCount(1, DDC2494TinyIntType::$calls['convertToPHPValue']);
self::assertFalse($item->getCurrency()->__isInitialized());
self::assertTrue($this->isUninitializedObject($item->getCurrency()));
$this->assertQueryCount(0);
self::assertIsInt($item->getCurrency()->getTemp());
self::assertCount(3, DDC2494TinyIntType::$calls['convertToPHPValue']);
self::assertTrue($item->getCurrency()->__isInitialized());
self::assertFalse($this->isUninitializedObject($item->getCurrency()));
$this->assertQueryCount(1);
}

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\Models\Legacy\LegacyUser;
use Doctrine\Tests\Models\Legacy\LegacyUserReference;
use Doctrine\Tests\OrmFunctionalTestCase;
@@ -36,15 +35,10 @@ class DDC2519Test extends OrmFunctionalTestCase
self::assertInstanceOf(LegacyUser::class, $result[1]->source());
self::assertInstanceOf(LegacyUser::class, $result[1]->target());
self::assertInstanceOf(Proxy::class, $result[0]->source());
self::assertInstanceOf(Proxy::class, $result[0]->target());
self::assertInstanceOf(Proxy::class, $result[1]->source());
self::assertInstanceOf(Proxy::class, $result[1]->target());
self::assertFalse($result[0]->target()->__isInitialized());
self::assertFalse($result[0]->source()->__isInitialized());
self::assertFalse($result[1]->target()->__isInitialized());
self::assertFalse($result[1]->source()->__isInitialized());
self::assertTrue($this->isUninitializedObject($result[0]->target()));
self::assertTrue($this->isUninitializedObject($result[0]->source()));
self::assertTrue($this->isUninitializedObject($result[1]->target()));
self::assertTrue($this->isUninitializedObject($result[1]->source()));
self::assertNotNull($result[0]->source()->getId());
self::assertNotNull($result[0]->target()->getId());

View File

@@ -14,7 +14,6 @@ use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Query;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\OrmFunctionalTestCase;
/** @group DDC-371 */
@@ -51,7 +50,7 @@ class DDC371Test extends OrmFunctionalTestCase
->getResult();
self::assertCount(1, $children);
self::assertNotInstanceOf(Proxy::class, $children[0]->parent);
self::assertFalse($this->isUninitializedObject($children[0]->parent));
self::assertFalse($children[0]->parent->children->isInitialized());
self::assertEquals(0, $children[0]->parent->children->unwrap()->count());
}

View File

@@ -10,7 +10,6 @@ use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\OneToOne;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\OrmFunctionalTestCase;
use function get_class;
@@ -52,7 +51,7 @@ class DDC522Test extends OrmFunctionalTestCase
self::assertInstanceOf(DDC522Cart::class, $r[0]);
self::assertInstanceOf(DDC522Customer::class, $r[0]->customer);
self::assertNotInstanceOf(Proxy::class, $r[0]->customer);
self::assertFalse($this->isUninitializedObject($r[0]->customer));
self::assertEquals('name', $r[0]->customer->name);
$fkt = new DDC522ForeignKeyTest();
@@ -64,8 +63,7 @@ class DDC522Test extends OrmFunctionalTestCase
$fkt2 = $this->_em->find(get_class($fkt), $fkt->id);
self::assertEquals($fkt->cart->id, $fkt2->cartId);
self::assertInstanceOf(Proxy::class, $fkt2->cart);
self::assertFalse($fkt2->cart->__isInitialized());
self::assertTrue($this->isUninitializedObject($fkt2->cart));
}
/**

View File

@@ -16,7 +16,6 @@ use Doctrine\ORM\Mapping\InheritanceType;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\OrmFunctionalTestCase;
class DDC531Test extends OrmFunctionalTestCase
@@ -46,7 +45,7 @@ class DDC531Test extends OrmFunctionalTestCase
// parent will already be loaded, cannot be lazy because it has mapped subclasses and we would not
// know which proxy type to put in.
self::assertInstanceOf(DDC531Item::class, $item3->parent);
self::assertNotInstanceOf(Proxy::class, $item3->parent);
self::assertFalse($this->isUninitializedObject($item3->parent));
$item4 = $this->_em->find(DDC531Item::class, $item1->id); // Load parent item (id 1)
self::assertNull($item4->parent);
self::assertNotNull($item4->getChildren());

View File

@@ -9,7 +9,6 @@ use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToOne;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\OrmFunctionalTestCase;
class DDC633Test extends OrmFunctionalTestCase
@@ -44,7 +43,7 @@ class DDC633Test extends OrmFunctionalTestCase
$eagerAppointment = $this->_em->find(DDC633Appointment::class, $app->id);
// Eager loading of one to one leads to fetch-join
self::assertNotInstanceOf(Proxy::class, $eagerAppointment->patient);
self::assertFalse($this->isUninitializedObject($eagerAppointment->patient));
self::assertTrue($this->_em->contains($eagerAppointment->patient));
}
@@ -70,8 +69,7 @@ class DDC633Test extends OrmFunctionalTestCase
$appointments = $this->_em->createQuery('SELECT a FROM ' . __NAMESPACE__ . '\DDC633Appointment a')->getResult();
foreach ($appointments as $eagerAppointment) {
self::assertInstanceOf(Proxy::class, $eagerAppointment->patient);
self::assertTrue($eagerAppointment->patient->__isInitialized(), 'Proxy should already be initialized due to eager loading!');
self::assertFalse($this->isUninitializedObject($eagerAppointment->patient), 'Proxy should already be initialized due to eager loading!');
}
}
}

View File

@@ -12,7 +12,6 @@ use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\OrmFunctionalTestCase;
class DDC6460Test extends OrmFunctionalTestCase
@@ -61,11 +60,10 @@ class DDC6460Test extends OrmFunctionalTestCase
$secondEntityWithLazyParameter = $this->_em->getRepository(DDC6460ParentEntity::class)->findOneById(1);
self::assertInstanceOf(Proxy::class, $secondEntityWithLazyParameter->lazyLoaded);
self::assertInstanceOf(DDC6460Entity::class, $secondEntityWithLazyParameter->lazyLoaded);
self::assertFalse($secondEntityWithLazyParameter->lazyLoaded->__isInitialized());
self::assertTrue($this->isUninitializedObject($secondEntityWithLazyParameter->lazyLoaded));
self::assertEquals($secondEntityWithLazyParameter->lazyLoaded->embedded, $entity->embedded);
self::assertTrue($secondEntityWithLazyParameter->lazyLoaded->__isInitialized());
self::assertFalse($this->isUninitializedObject($secondEntityWithLazyParameter->lazyLoaded));
}
}

View File

@@ -31,7 +31,7 @@ class DDC719Test extends OrmFunctionalTestCase
{
$q = $this->_em->createQuery('SELECT g, c FROM Doctrine\Tests\ORM\Functional\Ticket\DDC719Group g LEFT JOIN g.children c WHERE g.parents IS EMPTY');
$referenceSQL = 'SELECT g0_.name AS name_0, g0_.description AS description_1, g0_.id AS id_2, g1_.name AS name_3, g1_.description AS description_4, g1_.id AS id_5 FROM groups g0_ LEFT JOIN groups_groups g2_ ON g0_.id = g2_.parent_id LEFT JOIN groups g1_ ON g1_.id = g2_.child_id WHERE (SELECT COUNT(*) FROM groups_groups g3_ WHERE g3_.child_id = g0_.id) = 0';
$referenceSQL = 'SELECT g0_.id AS id_0, g0_.name AS name_1, g0_.description AS description_2, g1_.id as id_3, g1_.name AS name_4, g1_.description AS description_5 FROM groups g0_ LEFT JOIN groups_groups g2_ ON g0_.id = g2_.parent_id LEFT JOIN groups g1_ ON g1_.id = g2_.child_id WHERE (SELECT COUNT(*) FROM groups_groups g3_ WHERE g3_.child_id = g0_.id) = 0';
self::assertEquals(
strtolower($referenceSQL),

View File

@@ -8,7 +8,6 @@ use Doctrine\ORM\Query;
use Doctrine\ORM\Query\AST;
use Doctrine\ORM\Query\AST\SelectExpression;
use Doctrine\ORM\Query\TreeWalkerAdapter;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\Models\ECommerce\ECommerceCart;
use Doctrine\Tests\Models\ECommerce\ECommerceCustomer;
use Doctrine\Tests\OrmFunctionalTestCase;
@@ -46,7 +45,7 @@ class DDC736Test extends OrmFunctionalTestCase
unset($result[0]);
self::assertInstanceOf(ECommerceCart::class, $cart2);
self::assertNotInstanceOf(Proxy::class, $cart2->getCustomer());
self::assertFalse($this->isUninitializedObject($cart2->getCustomer()));
self::assertInstanceOf(ECommerceCustomer::class, $cart2->getCustomer());
self::assertEquals(['name' => 'roman', 'payment' => 'cash'], $result);
}
@@ -77,7 +76,7 @@ class DDC736Test extends OrmFunctionalTestCase
$cart2 = $result[0][0];
assert($cart2 instanceof ECommerceCart);
self::assertInstanceOf(Proxy::class, $cart2->getCustomer());
self::assertTrue($this->isUninitializedObject($cart2->getCustomer()));
}
}

View File

@@ -15,7 +15,6 @@ use Doctrine\ORM\Mapping\JoinColumns;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\PersistentCollection;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\OrmFunctionalTestCase;
class DDC881Test extends OrmFunctionalTestCase
@@ -90,8 +89,8 @@ class DDC881Test extends OrmFunctionalTestCase
$calls = $this->_em->createQuery($dql)->getResult();
self::assertCount(2, $calls);
self::assertNotInstanceOf(Proxy::class, $calls[0]->getPhoneNumber());
self::assertNotInstanceOf(Proxy::class, $calls[1]->getPhoneNumber());
self::assertFalse($this->isUninitializedObject($calls[0]->getPhoneNumber()));
self::assertFalse($this->isUninitializedObject($calls[1]->getPhoneNumber()));
$dql = 'SELECT p, c FROM ' . DDC881PhoneNumber::class . ' p JOIN p.calls c';
$numbers = $this->_em->createQuery($dql)->getResult();

View File

@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
final class GH10348Test extends OrmFunctionalTestCase
{
public function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
GH10348Person::class,
GH10348Company::class,
]);
}
public function testTheORMRemovesReferencedEmployeeBeforeReferencingEmployee(): void
{
$person1 = new GH10348Person();
$person2 = new GH10348Person();
$person2->mentor = $person1;
$company = new GH10348Company();
$company->addEmployee($person1)->addEmployee($person2);
$this->_em->persist($company);
$this->_em->flush();
$company = $this->_em->find(GH10348Company::class, $company->id);
$this->_em->remove($company);
$this->_em->flush();
self::assertEmpty($this->_em->createQuery('SELECT c FROM ' . GH10348Company::class . ' c')->getResult());
self::assertEmpty($this->_em->createQuery('SELECT p FROM ' . GH10348Person::class . ' p')->getResult());
}
}
/**
* @ORM\Entity
*/
class GH10348Person
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var ?int
*/
public $id = null;
/**
* @ORM\ManyToOne(targetEntity="GH10348Company", inversedBy="employees")
*
* @var ?GH10348Company
*/
public $employer = null;
/**
* @ORM\ManyToOne(targetEntity="GH10348Person", cascade={"remove"})
*
* @var ?GH10348Person
*/
public $mentor = null;
}
/**
* @ORM\Entity
*/
class GH10348Company
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var ?int
*/
public $id = null;
/**
* @ORM\OneToMany(targetEntity="GH10348Person", mappedBy="emplo", cascade={"persist", "remove"})
*
* @var Collection
*/
private $employees;
public function __construct()
{
$this->employees = new ArrayCollection();
}
public function addEmployee(GH10348Person $person): self
{
$person->employer = $this;
$this->employees->add($person);
return $this;
}
}

View File

@@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\Tests\OrmTestCase;
use Generator;
class GH10450Test extends OrmTestCase
{
/**
* @param class-string $className
*
* @dataProvider classesThatOverrideFieldNames
*/
public function testDuplicatePrivateFieldsShallBeRejected(string $className): void
{
$em = $this->getTestEntityManager();
$this->expectException(MappingException::class);
$em->getClassMetadata($className);
}
public function classesThatOverrideFieldNames(): Generator
{
yield 'Entity class that redeclares a private field inherited from a base entity' => [GH10450EntityChildPrivate::class];
yield 'Entity class that redeclares a private field inherited from a mapped superclass' => [GH10450MappedSuperclassChildPrivate::class];
yield 'Entity class that redeclares a protected field inherited from a base entity' => [GH10450EntityChildProtected::class];
yield 'Entity class that redeclares a protected field inherited from a mapped superclass' => [GH10450MappedSuperclassChildProtected::class];
}
}
/**
* @ORM\Entity
* @ORM\InheritanceType("JOINED")
* @ORM\DiscriminatorMap({ "base": "GH10450BaseEntityPrivate", "child": "GH10450EntityChildPrivate" })
* @ORM\DiscriminatorColumn(name="type")
*/
class GH10450BaseEntityPrivate
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*
* @var int
*/
private $id;
/**
* @ORM\Column(type="text", name="base")
*
* @var string
*/
private $field;
}
/**
* @ORM\Entity
*/
class GH10450EntityChildPrivate extends GH10450BaseEntityPrivate
{
/**
* @ORM\Column(type="text", name="child")
*
* @var string
*/
private $field;
}
/**
* @ORM\MappedSuperclass
*/
class GH10450BaseMappedSuperclassPrivate
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*
* @var int
*/
private $id;
/**
* @ORM\Column(type="text", name="base")
*
* @var string
*/
private $field;
}
/**
* @ORM\Entity
*/
class GH10450MappedSuperclassChildPrivate extends GH10450BaseMappedSuperclassPrivate
{
/**
* @ORM\Column(type="text", name="child")
*
* @var string
*/
private $field;
}
/**
* @ORM\Entity
* @ORM\InheritanceType("JOINED")
* @ORM\DiscriminatorMap({ "base": "GH10450BaseEntityProtected", "child": "GH10450EntityChildProtected" })
* @ORM\DiscriminatorColumn(name="type")
*/
class GH10450BaseEntityProtected
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*
* @var int
*/
protected $id;
/**
* @ORM\Column(type="text", name="base")
*
* @var string
*/
protected $field;
}
/**
* @ORM\Entity
*/
class GH10450EntityChildProtected extends GH10450BaseEntityProtected
{
/**
* @ORM\Column(type="text", name="child")
*
* @var string
*/
protected $field;
}
/**
* @ORM\MappedSuperclass
*/
class GH10450BaseMappedSuperclassProtected
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*
* @var int
*/
protected $id;
/**
* @ORM\Column(type="text", name="base")
*
* @var string
*/
protected $field;
}
/**
* @ORM\Entity
*/
class GH10450MappedSuperclassChildProtected extends GH10450BaseMappedSuperclassProtected
{
/**
* @ORM\Column(type="text", name="child")
*
* @var string
*/
protected $field;
}

View File

@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\Tests\OrmTestCase;
use Generator;
class GH10454Test extends OrmTestCase
{
/**
* @param class-string $className
*
* @dataProvider classesThatOverrideFieldNames
*/
public function testProtectedPropertyMustNotBeInheritedAndReconfigured(string $className): void
{
$em = $this->getTestEntityManager();
$this->expectException(MappingException::class);
$this->expectExceptionMessageMatches('/Property "field" .* was already declared, but it must be declared only once/');
$em->getClassMetadata($className);
}
public function classesThatOverrideFieldNames(): Generator
{
yield 'Entity class that redeclares a protected field inherited from a base entity' => [GH10454EntityChildProtected::class];
yield 'Entity class that redeclares a protected field inherited from a mapped superclass' => [GH10454MappedSuperclassChildProtected::class];
}
}
/**
* @ORM\Entity
* @ORM\InheritanceType("JOINED")
* @ORM\DiscriminatorMap({ "base": "GH10454BaseEntityProtected", "child": "GH10454EntityChildProtected" })
* @ORM\DiscriminatorColumn(name="type")
*/
class GH10454BaseEntityProtected
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*
* @var int
*/
protected $id;
/**
* @ORM\Column(type="text", name="base")
*
* @var string
*/
protected $field;
}
/**
* @ORM\Entity
*/
class GH10454EntityChildProtected extends GH10454BaseEntityProtected
{
/**
* @ORM\Column(type="text", name="child")
*
* @var string
*/
protected $field;
}
/**
* @ORM\MappedSuperclass
*/
class GH10454BaseMappedSuperclassProtected
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*
* @var int
*/
protected $id;
/**
* @ORM\Column(type="text", name="base")
*
* @var string
*/
protected $field;
}
/**
* @ORM\Entity
*/
class GH10454MappedSuperclassChildProtected extends GH10454BaseMappedSuperclassProtected
{
/**
* @ORM\Column(type="text", name="child")
*
* @var string
*/
protected $field;
}

View File

@@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH10531Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
GH10531A::class,
GH10531B::class
);
}
public function tearDown(): void
{
$conn = static::$sharedConn;
$conn->executeStatement('DELETE FROM gh10531_b');
$conn->executeStatement('DELETE FROM gh10531_a');
}
public function testInserts(): void
{
$a = new GH10531A();
$b1 = new GH10531B();
$b2 = new GH10531B();
$b3 = new GH10531B();
$b1->parent = $b2;
$b3->parent = $b2;
$b2->parent = $a;
/*
* The following would force a working commit order, but that's not what
* we want (the ORM shall sort this out internally).
*
* $this->_em->persist($a);
* $this->_em->persist($b2);
* $this->_em->flush();
* $this->_em->persist($b1);
* $this->_em->persist($b3);
* $this->_em->flush();
*/
// Pass $b2 to persist() between $b1 and $b3, so that any potential reliance upon the
// order of persist() calls is spotted: No matter if it is in the order that persist()
// was called or the other way round, in both cases there is an entity that will come
// "before" $b2 but depend on its primary key, so the ORM must re-order the inserts.
$this->_em->persist($a);
$this->_em->persist($b1);
$this->_em->persist($b2);
$this->_em->persist($b3);
$this->_em->flush();
self::assertNotNull($a->id);
self::assertNotNull($b1->id);
self::assertNotNull($b2->id);
self::assertNotNull($b3->id);
}
public function testDeletes(): void
{
$this->expectNotToPerformAssertions();
$con = $this->_em->getConnection();
// The "a" entity
$con->insert('gh10531_a', ['id' => 1, 'discr' => 'A']);
$a = $this->_em->find(GH10531A::class, 1);
// The "b2" entity
$con->insert('gh10531_a', ['id' => 2, 'discr' => 'B']);
$con->insert('gh10531_b', ['id' => 2, 'parent_id' => 1]);
$b2 = $this->_em->find(GH10531B::class, 2);
// The "b1" entity
$con->insert('gh10531_a', ['id' => 3, 'discr' => 'B']);
$con->insert('gh10531_b', ['id' => 3, 'parent_id' => 2]);
$b1 = $this->_em->find(GH10531B::class, 3);
// The "b3" entity
$con->insert('gh10531_a', ['id' => 4, 'discr' => 'B']);
$con->insert('gh10531_b', ['id' => 4, 'parent_id' => 2]);
$b3 = $this->_em->find(GH10531B::class, 4);
/*
* The following would make the deletions happen in an order
* where the not-nullable foreign key constraints would not be
* violated. But, we want the ORM to be able to sort this out
* internally.
*
* $this->_em->remove($b1);
* $this->_em->remove($b3);
* $this->_em->remove($b2);
*/
// As before, put $b2 in between $b1 and $b3 so that the order of the
// remove() calls alone (in either direction) does not solve the problem.
// The ORM will have to sort $b2 to be deleted last, after $b1 and $b3.
$this->_em->remove($b1);
$this->_em->remove($b2);
$this->_em->remove($b3);
$this->_em->flush();
}
}
/**
* @ORM\Entity
* @ORM\Table(name="gh10531_a")
* @ORM\DiscriminatorColumn(name="discr", type="string")
* @ORM\DiscriminatorMap({ "A": "GH10531A", "B": "GH10531B" })
* @ORM\InheritanceType("JOINED")
*
* We are using JTI here, since STI would relax the not-nullable constraint for the "parent"
* column (it has to be NULL when the row contains a GH10531A instance). Causes another error,
* but not the constraint violation I'd like to point out.
*/
class GH10531A
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
}
/**
* @ORM\Entity
* @ORM\Table(name="gh10531_b")
*/
class GH10531B extends GH10531A
{
/**
* @ORM\ManyToOne(targetEntity="GH10531A")
* @ORM\JoinColumn(nullable=false, name="parent_id")
*
* @var GH10531A
*/
public $parent;
}

View File

@@ -0,0 +1,186 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH10532Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
GH10532A::class,
GH10532B::class,
GH10532C::class,
GH10532X::class
);
}
public function tearDown(): void
{
$conn = static::$sharedConn;
$conn->executeStatement('DELETE FROM gh10532_c');
$conn->executeStatement('DELETE FROM gh10532_b');
$conn->executeStatement('DELETE FROM gh10532_a');
$conn->executeStatement('DELETE FROM gh10532_x');
}
public function testInserts(): void
{
// Dependencies are $a1 -> $b -> $a2 -> $c
$a1 = new GH10532A();
$b = new GH10532B();
$a2 = new GH10532A();
$c = new GH10532C();
$a1->x = $b;
$b->a = $a2;
$a2->x = $c;
/*
* The following would force a working commit order, but that's not what
* we want (the ORM shall sort this out internally).
*
* $this->_em->persist($c);
* $this->_em->persist($a2);
* $this->_em->flush();
* $this->_em->persist($b);
* $this->_em->persist($a1);
* $this->_em->flush();
*/
$this->_em->persist($a1);
$this->_em->persist($a2);
$this->_em->persist($b);
$this->_em->persist($c);
$this->_em->flush();
self::assertNotNull($a1->id);
self::assertNotNull($b->id);
self::assertNotNull($a2->id);
self::assertNotNull($c->id);
}
public function testDeletes(): void
{
// Dependencies are $a1 -> $b -> $a2 -> $c
$this->expectNotToPerformAssertions();
$con = $this->_em->getConnection();
// The "c" entity
$con->insert('gh10532_x', ['id' => 1, 'discr' => 'C']);
$con->insert('gh10532_c', ['id' => 1]);
$c = $this->_em->find(GH10532C::class, 1);
// The "a2" entity
$con->insert('gh10532_a', ['id' => 2, 'gh10532x_id' => 1]);
$a2 = $this->_em->find(GH10532A::class, 2);
// The "b" entity
$con->insert('gh10532_x', ['id' => 3, 'discr' => 'B']);
$con->insert('gh10532_b', ['id' => 3, 'gh10532a_id' => 2]);
$b = $this->_em->find(GH10532B::class, 3);
// The "a1" entity
$con->insert('gh10532_a', ['id' => 4, 'gh10532x_id' => 3]);
$a1 = $this->_em->find(GH10532A::class, 4);
/*
* The following would make the deletions happen in an order
* where the not-nullable foreign key constraints would not be
* violated. But, we want the ORM to be able to sort this out
* internally.
*
* $this->_em->remove($a1);
* $this->_em->flush();
* $this->_em->remove($b);
* $this->_em->flush();
* $this->_em->remove($a2);
* $this->_em->remove($c);
* $this->_em->flush();
*/
$this->_em->remove($a1);
$this->_em->remove($a2);
$this->_em->remove($b);
$this->_em->remove($c);
$this->_em->flush();
}
}
/**
* @ORM\Entity
* @ORM\Table(name="gh10532_x")
* @ORM\DiscriminatorColumn(name="discr", type="string")
* @ORM\DiscriminatorMap({ "B": "GH10532B", "C": "GH10532C" })
* @ORM\InheritanceType("JOINED")
*
* We are using JTI here, since STI would relax the not-nullable constraint for the "parent"
* column. Causes another error, but not the constraint violation I'd like to point out.
*/
abstract class GH10532X
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
}
/**
* @ORM\Entity
* @ORM\Table(name="gh10532_b")
*/
class GH10532B extends GH10532X
{
/**
* @ORM\ManyToOne(targetEntity="GH10532A")
* @ORM\JoinColumn(nullable=false, name="gh10532a_id")
*
* @var GH10532A
*/
public $a;
}
/**
* @ORM\Entity
* @ORM\Table(name="gh10532_c")
*/
class GH10532C extends GH10532X
{
}
/**
* @ORM\Entity
* @ORM\Table(name="gh10532_a")
*/
class GH10532A
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
/**
* @ORM\ManyToOne(targetEntity="GH10532X")
* @ORM\JoinColumn(nullable=false, name="gh10532x_id")
*
* @var GH10532X
*/
public $x;
}

View File

@@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
use Generator;
use function is_a;
class GH10566Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
GH10566A::class,
GH10566B::class,
GH10566C::class
);
}
/**
* @dataProvider provideEntityClasses
*/
public function testInsertion(string $startEntityClass): void
{
$a = new GH10566A();
$b = new GH10566B();
$c = new GH10566C();
$a->other = $b;
$b->other = $c;
$c->other = $a;
foreach ([$a, $b, $c] as $candidate) {
if (is_a($candidate, $startEntityClass)) {
$this->_em->persist($candidate);
}
}
// Since all associations are nullable, the ORM has no problem finding an insert order,
// it can always schedule "deferred updates" to fill missing foreign key values.
$this->_em->flush();
self::assertNotNull($a->id);
self::assertNotNull($b->id);
self::assertNotNull($c->id);
}
/**
* @dataProvider provideEntityClasses
*/
public function testRemoval(string $startEntityClass): void
{
$a = new GH10566A();
$b = new GH10566B();
$c = new GH10566C();
$a->other = $b;
$b->other = $c;
$c->other = $a;
$this->_em->persist($a);
$this->_em->flush();
$aId = $a->id;
$bId = $b->id;
$cId = $c->id;
// In the removal case, the ORM currently does not schedule "extra updates"
// to break association cycles before entities are removed. So, we must not
// look at "nullable" for associations to find a delete commit order.
//
// To make it work, the user needs to have a database-level "ON DELETE SET NULL"
// on an association. That's where the cycle can be broken. Commit order computation
// for the removal case needs to look at this property.
//
// In this example, only A -> B can be used to break the cycle. So, regardless which
// entity we start with, the ORM-level cascade will always remove all three entities,
// and the order of database deletes always has to be (can only be) from B, then C, then A.
foreach ([$a, $b, $c] as $candidate) {
if (is_a($candidate, $startEntityClass)) {
$this->_em->remove($candidate);
}
}
$this->_em->flush();
self::assertFalse($this->_em->getConnection()->fetchOne('SELECT id FROM gh10566_a WHERE id = ?', [$aId]));
self::assertFalse($this->_em->getConnection()->fetchOne('SELECT id FROM gh10566_b WHERE id = ?', [$bId]));
self::assertFalse($this->_em->getConnection()->fetchOne('SELECT id FROM gh10566_c WHERE id = ?', [$cId]));
}
public function provideEntityClasses(): Generator
{
yield [GH10566A::class];
yield [GH10566B::class];
yield [GH10566C::class];
}
}
/**
* @ORM\Entity
* @ORM\Table(name="gh10566_a")
*/
class GH10566A
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue()
*
* @var int
*/
public $id;
/**
* @ORM\OneToOne(targetEntity="GH10566B", cascade={"all"})
* @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*
* @var GH10566B
*/
public $other;
}
/**
* @ORM\Entity
* @ORM\Table(name="gh10566_b")
*/
class GH10566B
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue()
*
* @var int
*/
public $id;
/**
* @ORM\OneToOne(targetEntity="GH10566C", cascade={"all"})
* @ORM\JoinColumn(nullable=true)
*
* @var GH10566C
*/
public $other;
}
/**
* @ORM\Entity
* @ORM\Table(name="gh10566_c")
*/
class GH10566C
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue()
*
* @var int
*/
public $id;
/**
* @ORM\OneToOne(targetEntity="GH10566A", cascade={"all"})
* @ORM\JoinColumn(nullable=true)
*
* @var GH10566A
*/
public $other;
}

View File

@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH5742Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
GH5742Person::class,
GH5742Toothbrush::class,
GH5742ToothpasteBrand::class
);
}
public function testUpdateOneToOneToNewEntityBeforePreviousEntityCanBeRemoved(): void
{
$person = new GH5742Person();
$oldToothbrush = new GH5742Toothbrush();
$person->toothbrush = $oldToothbrush;
$this->_em->persist($person);
$this->_em->persist($oldToothbrush);
$this->_em->flush();
$oldToothbrushId = $oldToothbrush->id;
$newToothbrush = new GH5742Toothbrush();
$person->toothbrush = $newToothbrush;
$this->_em->remove($oldToothbrush);
$this->_em->persist($newToothbrush);
// The flush operation will have to make sure the new toothbrush
// has been written to the database
// _before_ the person can be updated to refer to it.
// Likewise, the update must have happened _before_ the old
// toothbrush can be removed (non-nullable FK constraint).
$this->_em->flush();
$this->_em->clear();
self::assertSame($newToothbrush->id, $this->_em->find(GH5742Person::class, $person->id)->toothbrush->id);
self::assertNull($this->_em->find(GH5742Toothbrush::class, $oldToothbrushId));
}
public function testManyToManyCollectionUpdateBeforeRemoval(): void
{
$person = new GH5742Person();
$person->toothbrush = new GH5742Toothbrush(); // to satisfy not-null constraint
$this->_em->persist($person);
$oldMice = new GH5742ToothpasteBrand();
$this->_em->persist($oldMice);
$person->preferredBrands->set(1, $oldMice);
$this->_em->flush();
$oldBrandId = $oldMice->id;
$newSpice = new GH5742ToothpasteBrand();
$this->_em->persist($newSpice);
$person->preferredBrands->set(1, $newSpice);
$this->_em->remove($oldMice);
// The flush operation will have to make sure the new brand
// has been written to the database _before_ it can be referred
// to from the m2m join table.
// Likewise, the old join table entry must have been removed
// _before_ the old brand can be removed.
$this->_em->flush();
$this->_em->clear();
self::assertCount(1, $this->_em->find(GH5742Person::class, $person->id)->preferredBrands);
self::assertNull($this->_em->find(GH5742ToothpasteBrand::class, $oldBrandId));
}
}
/**
* @ORM\Entity
*/
class GH5742Person
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
/**
* @ORM\OneToOne(targetEntity="GH5742Toothbrush", cascade={"persist"})
* @ORM\JoinColumn(nullable=false)
*
* @var GH5742Toothbrush
*/
public $toothbrush;
/**
* @ORM\ManyToMany(targetEntity="GH5742ToothpasteBrand")
* @ORM\JoinTable(name="gh5742person_gh5742toothpastebrand",
* joinColumns={@ORM\JoinColumn(name="person_id", referencedColumnName="id", onDelete="CASCADE")},
* inverseJoinColumns={@ORM\JoinColumn(name="brand_id", referencedColumnName="id")}
* )
*
* @var Collection<GH5742ToothpasteBrand>
*/
public $preferredBrands;
public function __construct()
{
$this->preferredBrands = new ArrayCollection();
}
}
/**
* @ORM\Entity
*/
class GH5742Toothbrush
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
}
/**
* @ORM\Entity
*/
class GH5742ToothpasteBrand
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
}

View File

@@ -48,6 +48,7 @@ class GH5998Test extends OrmFunctionalTestCase
$this->_em->persist($child->rel);
$this->_em->flush();
$this->_em->clear();
$id = $child->id;
// Test find by rel
$child = $this->_em->getRepository($className)->findOneBy(['rel' => $child->rel]);
@@ -55,7 +56,7 @@ class GH5998Test extends OrmFunctionalTestCase
$this->_em->clear();
// Test query by id with fetch join
$child = $this->_em->createQuery('SELECT t, r FROM ' . $className . ' t JOIN t.rel r WHERE t.id = 1')->getOneOrNullResult();
$child = $this->_em->createQuery('SELECT t, r FROM ' . $className . ' t JOIN t.rel r WHERE t.id = ?0')->setParameter(0, $id)->getOneOrNullResult();
self::assertNotNull($child);
// Test lock and update
@@ -65,14 +66,15 @@ class GH5998Test extends OrmFunctionalTestCase
$child->status = 0;
});
$this->_em->clear();
$child = $this->_em->getRepository($className)->find(1);
$child = $this->_em->getRepository($className)->find($id);
self::assertNotNull($child);
self::assertEquals($child->firstName, 'Bob');
self::assertEquals($child->status, 0);
// Test delete
$this->_em->remove($child);
$this->_em->flush();
$child = $this->_em->getRepository($className)->find(1);
$child = $this->_em->getRepository($className)->find($id);
self::assertNull($child);
}
}

View File

@@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
/**
* @group GH6499
*/
class GH6499OneToManyRelationshipTest extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(Application::class, Person::class, ApplicationPerson::class);
}
/**
* Test for the bug described in issue #6499.
*/
public function testIssue(): void
{
$person = new Person();
$this->_em->persist($person);
$application = new Application();
$this->_em->persist($application);
$applicationPerson = new ApplicationPerson($person, $application);
$this->_em->persist($applicationPerson);
$this->_em->flush();
$this->_em->clear();
$personFromDatabase = $this->_em->find(Person::class, $person->id);
$applicationFromDatabase = $this->_em->find(Application::class, $application->id);
self::assertEquals($personFromDatabase->id, $person->id, 'Issue #6499 will result in an integrity constraint violation before reaching this point.');
self::assertFalse($personFromDatabase->getApplicationPeople()->isEmpty());
self::assertEquals($applicationFromDatabase->id, $application->id, 'Issue #6499 will result in an integrity constraint violation before reaching this point.');
self::assertFalse($applicationFromDatabase->getApplicationPeople()->isEmpty());
}
}
/**
* @ORM\Entity
* @ORM\Table("GH6499OTM_application")
*/
class Application
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
/**
* @ORM\OneToMany(targetEntity=ApplicationPerson::class, mappedBy="application", orphanRemoval=true, cascade={"persist"})
* @ORM\JoinColumn(nullable=false)
*
* @var Collection
*/
private $applicationPeople;
public function __construct()
{
$this->applicationPeople = new ArrayCollection();
}
public function getApplicationPeople(): Collection
{
return $this->applicationPeople;
}
}
/**
* @ORM\Entity()
* @ORM\Table("GH6499OTM_person")
*/
class Person
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
/**
* @ORM\OneToMany(targetEntity=ApplicationPerson::class, mappedBy="person", orphanRemoval=true, cascade={"persist"})
* @ORM\JoinColumn(nullable=false)
*
* @var Collection
*/
private $applicationPeople;
public function __construct()
{
$this->applicationPeople = new ArrayCollection();
}
public function getApplicationPeople(): Collection
{
return $this->applicationPeople;
}
}
/**
* @ORM\Entity()
* @ORM\Table("GH6499OTM_application_person")
*/
class ApplicationPerson
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
/**
* @ORM\ManyToOne(targetEntity=Application::class, inversedBy="applicationPeople", cascade={"persist"})
* @ORM\JoinColumn(nullable=false)
*
* @var Application
*/
public $application;
/**
* @ORM\ManyToOne(targetEntity=Person::class, inversedBy="applicationPeople", cascade={"persist"})
* @ORM\JoinColumn(nullable=false)
*
* @var Person
*/
public $person;
public function __construct(Person $person, Application $application)
{
$this->person = $person;
$this->application = $application;
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
/**
* @group GH6499
*/
class GH6499OneToOneRelationshipTest extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(GH6499OTOA::class, GH6499OTOB::class);
}
/**
* Test for the bug described in issue #6499.
*/
public function testIssue(): void
{
$a = new GH6499OTOA();
$this->_em->persist($a);
$this->_em->flush();
$this->_em->clear();
self::assertEquals(
$this->_em->find(GH6499OTOA::class, $a->id)->b->id,
$a->b->id,
'Issue #6499 will result in an integrity constraint violation before reaching this point.'
);
}
}
/** @ORM\Entity */
class GH6499OTOA
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var int
*/
public $id;
/**
* @ORM\OneToOne(targetEntity="GH6499OTOB", cascade={"persist"})
* @ORM\JoinColumn(nullable=false)
*
* @var GH6499OTOB
*/
public $b;
public function __construct()
{
$this->b = new GH6499OTOB();
}
}
/** @ORM\Entity */
class GH6499OTOB
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var int
*/
public $id;
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
/**
* @group GH6499
*
* Specifically, GH6499B has a dependency on GH6499A, and GH6499A
* has a dependency on GH6499B. Since GH6499A#b is not nullable,
* the database row for GH6499B should be inserted first.
*/
class GH6499Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(GH6499A::class, GH6499B::class);
}
public function testIssue(): void
{
$b = new GH6499B();
$a = new GH6499A();
$this->_em->persist($a);
$a->b = $b;
$this->_em->persist($b);
$this->_em->flush();
self::assertIsInt($a->id);
self::assertIsInt($b->id);
}
public function testIssueReversed(): void
{
$b = new GH6499B();
$a = new GH6499A();
$a->b = $b;
$this->_em->persist($b);
$this->_em->persist($a);
$this->_em->flush();
self::assertIsInt($a->id);
self::assertIsInt($b->id);
}
}
/**
* @ORM\Entity
*/
class GH6499A
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var int
*/
public $id;
/**
* @ORM\JoinColumn(nullable=false)
* @ORM\OneToOne(targetEntity=GH6499B::class)
*
* @var GH6499B
*/
public $b;
}
/**
* @ORM\Entity
*/
class GH6499B
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var int
*/
public $id;
/**
* @ORM\ManyToOne(targetEntity=GH6499A::class)
*
* @var GH6499A
*/
private $a;
}

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH7006Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(GH7006Book::class, GH7006PCT::class, GH7006PCTFee::class);
}
public function testIssue(): void
{
$book = new GH7006Book();
$book->exchangeCode = 'first';
$this->_em->persist($book);
$book->exchangeCode = 'second'; // change sth.
$paymentCardTransaction = new GH7006PCT();
$paymentCardTransaction->book = $book;
$paymentCardTransactionFee = new GH7006PCTFee($paymentCardTransaction);
$this->_em->persist($paymentCardTransaction);
$this->_em->flush();
self::assertIsInt($book->id);
self::assertIsInt($paymentCardTransaction->id);
self::assertIsInt($paymentCardTransactionFee->id);
}
}
/**
* @ORM\Entity
*/
class GH7006Book
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*
* @var int
*/
public $id;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*
* @var string
*/
public $exchangeCode;
/**
* @ORM\OneToOne(targetEntity="GH7006PCT", cascade={"persist", "remove"})
* @ORM\JoinColumn(name="paymentCardTransactionId", referencedColumnName="id")
*
* @var GH7006PCT
*/
public $paymentCardTransaction;
}
/**
* @ORM\Entity
*/
class GH7006PCT
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*
* @var int
*/
public $id;
/**
* @ORM\ManyToOne(targetEntity="GH7006Book")
* @ORM\JoinColumn(name="bookingId", referencedColumnName="id", nullable=false)
*
* @var GH7006Book
*/
public $book;
/**
* @ORM\OneToMany(targetEntity="GH7006PCTFee", mappedBy="pct", cascade={"persist", "remove"})
* @ORM\OrderBy({"id" = "ASC"})
*
* @var Collection<int, GH7006PCTFee>
*/
public $fees;
public function __construct()
{
$this->fees = new ArrayCollection();
}
}
/**
* @ORM\Entity
*/
class GH7006PCTFee
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*
* @var int
*/
public $id;
/**
* @ORM\ManyToOne(targetEntity="GH7006PCT", inversedBy="fees")
* @ORM\JoinColumn(name="paymentCardTransactionId", referencedColumnName="id", nullable=false)
*
* @var GH7006PCT
*/
public $pct;
public function __construct(GH7006PCT $pct)
{
$this->pct = $pct;
$pct->fees->add($this);
}
}

View File

@@ -0,0 +1,223 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToOne;
use Doctrine\Tests\OrmFunctionalTestCase;
/**
* Tests suggested in https://github.com/doctrine/orm/pull/7180#issuecomment-380841413 and
* https://github.com/doctrine/orm/pull/7180#issuecomment-381067448.
*
* @group 7180
*/
final class GH7180Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([GH7180A::class, GH7180B::class, GH7180C::class, GH7180D::class, GH7180E::class, GH7180F::class, GH7180G::class]);
}
public function testIssue(): void
{
$a = new GH7180A();
$b = new GH7180B();
$c = new GH7180C();
$a->b = $b;
$b->a = $a;
$c->a = $a;
$this->_em->persist($a);
$this->_em->persist($b);
$this->_em->persist($c);
$this->_em->flush();
self::assertIsInt($a->id);
self::assertIsInt($b->id);
self::assertIsInt($c->id);
}
public function testIssue3NodeCycle(): void
{
$d = new GH7180D();
$e = new GH7180E();
$f = new GH7180F();
$g = new GH7180G();
$d->e = $e;
$e->f = $f;
$f->d = $d;
$g->d = $d;
$this->_em->persist($d);
$this->_em->persist($e);
$this->_em->persist($f);
$this->_em->persist($g);
$this->_em->flush();
self::assertIsInt($d->id);
self::assertIsInt($e->id);
self::assertIsInt($f->id);
self::assertIsInt($g->id);
}
}
/**
* @Entity
*/
class GH7180A
{
/**
* @GeneratedValue()
* @Id
* @Column(type="integer")
* @var int
*/
public $id;
/**
* @OneToOne(targetEntity=GH7180B::class, inversedBy="a")
* @JoinColumn(nullable=false)
* @var GH7180B
*/
public $b;
}
/**
* @Entity
*/
class GH7180B
{
/**
* @GeneratedValue()
* @Id
* @Column(type="integer")
* @var int
*/
public $id;
/**
* @OneToOne(targetEntity=GH7180A::class, mappedBy="b")
* @JoinColumn(nullable=true)
* @var GH7180A
*/
public $a;
}
/**
* @Entity
*/
class GH7180C
{
/**
* @GeneratedValue()
* @Id
* @Column(type="integer")
* @var int
*/
public $id;
/**
* @ManyToOne(targetEntity=GH7180A::class)
* @JoinColumn(nullable=false)
* @var GH7180A
*/
public $a;
}
/**
* @Entity
*/
class GH7180D
{
/**
* @GeneratedValue()
* @Id
* @Column(type="integer")
* @var int
*/
public $id;
/**
* @OneToOne(targetEntity=GH7180E::class)
* @JoinColumn(nullable=false)
* @var GH7180E
*/
public $e;
}
/**
* @Entity
*/
class GH7180E
{
/**
* @GeneratedValue()
* @Id
* @Column(type="integer")
* @var int
*/
public $id;
/**
* @OneToOne(targetEntity=GH7180F::class)
* @JoinColumn(nullable=false)
* @var GH7180F
*/
public $f;
}
/**
* @Entity
*/
class GH7180F
{
/**
* @GeneratedValue()
* @Id
* @Column(type="integer")
* @var int
*/
public $id;
/**
* @ManyToOne(targetEntity=GH7180D::class)
* @JoinColumn(nullable=true)
* @var GH7180D
*/
public $d;
}
/**
* @Entity
*/
class GH7180G
{
/**
* @GeneratedValue()
* @Id
* @Column(type="integer")
* @var int
*/
public $id;
/**
* @ManyToOne(targetEntity=GH7180D::class)
* @JoinColumn(nullable=false)
* @var GH7180D
*/
public $d;
}

View File

@@ -55,7 +55,7 @@ class GH7869Test extends OrmTestCase
$uow->clear();
$uow->triggerEagerLoads();
self::assertSame(2, $em->getClassMetadataCalls);
self::assertSame(4, $em->getClassMetadataCalls);
}
}

View File

@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH9192Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(GH9192A::class, GH9192B::class, GH9192C::class);
}
public function testIssue(): void
{
$a = new GH9192A();
$b = new GH9192B();
$b->a = $a;
$a->bs->add($b);
$c = new GH9192C();
$c->b = $b;
$b->cs->add($c);
$a->c = $c;
$this->_em->persist($a);
$this->_em->persist($b);
$this->_em->persist($c);
$this->_em->flush();
$this->expectNotToPerformAssertions();
$this->_em->remove($a);
$this->_em->flush();
}
}
/**
* @ORM\Entity
*/
class GH9192A
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*
* @var int
*/
public $id;
/**
* @ORM\OneToMany(targetEntity="GH9192B", mappedBy="a", cascade={"remove"})
*
* @var Collection<GH9192B>
*/
public $bs;
/**
* @ORM\OneToOne(targetEntity="GH9192C")
* @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*
* @var GH9192C
*/
public $c;
public function __construct()
{
$this->bs = new ArrayCollection();
}
}
/**
* @ORM\Entity
*/
class GH9192B
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*
* @var int
*/
public $id;
/**
* @ORM\OneToMany(targetEntity="GH9192C", mappedBy="b", cascade={"remove"})
*
* @var Collection<GH9192C>
*/
public $cs;
/**
* @ORM\ManyToOne(targetEntity="GH9192A", inversedBy="bs")
*
* @var GH9192A
*/
public $a;
public function __construct()
{
$this->cs = new ArrayCollection();
}
}
/**
* @ORM\Entity
*/
class GH9192C
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*
* @var int
*/
public $id;
/**
* @ORM\ManyToOne(targetEntity="GH9192B", inversedBy="cs")
*
* @var GH9192B
*/
public $b;
}

View File

@@ -0,0 +1,261 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH9467;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH9467Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
JoinedInheritanceRoot::class,
JoinedInheritanceChild::class,
JoinedInheritanceWritableColumn::class,
JoinedInheritanceNonWritableColumn::class,
JoinedInheritanceNonInsertableColumn::class,
JoinedInheritanceNonUpdatableColumn::class
);
}
public function testRootColumnsInsert(): int
{
$entity = new JoinedInheritanceChild();
$entity->rootWritableContent = 'foo';
$entity->rootNonWritableContent = 'foo';
$entity->rootNonInsertableContent = 'foo';
$entity->rootNonUpdatableContent = 'foo';
$this->_em->persist($entity);
$this->_em->flush();
// check INSERT query cause set database values into non-insertable entity properties
self::assertEquals('foo', $entity->rootWritableContent);
self::assertEquals('dbDefault', $entity->rootNonWritableContent);
self::assertEquals('dbDefault', $entity->rootNonInsertableContent);
self::assertEquals('foo', $entity->rootNonUpdatableContent);
// check other process get same state
$this->_em->clear();
$entity = $this->_em->find(JoinedInheritanceChild::class, $entity->id);
self::assertInstanceOf(JoinedInheritanceChild::class, $entity);
self::assertEquals('foo', $entity->rootWritableContent);
self::assertEquals('dbDefault', $entity->rootNonWritableContent);
self::assertEquals('dbDefault', $entity->rootNonInsertableContent);
self::assertEquals('foo', $entity->rootNonUpdatableContent);
return $entity->id;
}
/** @depends testRootColumnsInsert */
public function testRootColumnsUpdate(int $entityId): void
{
$entity = $this->_em->find(JoinedInheritanceChild::class, $entityId);
self::assertInstanceOf(JoinedInheritanceChild::class, $entity);
// update exist entity
$entity->rootWritableContent = 'bar';
$entity->rootNonInsertableContent = 'bar';
$entity->rootNonWritableContent = 'bar';
$entity->rootNonUpdatableContent = 'bar';
$this->_em->persist($entity);
$this->_em->flush();
// check UPDATE query cause set database values into non-insertable entity properties
self::assertEquals('bar', $entity->rootWritableContent);
self::assertEquals('dbDefault', $entity->rootNonWritableContent);
self::assertEquals('bar', $entity->rootNonInsertableContent);
self::assertEquals('foo', $entity->rootNonUpdatableContent);
// check other process get same state
$this->_em->clear();
$entity = $this->_em->find(JoinedInheritanceChild::class, $entity->id);
self::assertInstanceOf(JoinedInheritanceChild::class, $entity);
self::assertEquals('bar', $entity->rootWritableContent);
self::assertEquals('dbDefault', $entity->rootNonWritableContent);
self::assertEquals('bar', $entity->rootNonInsertableContent);
self::assertEquals('foo', $entity->rootNonUpdatableContent);
}
public function testChildWritableColumnInsert(): int
{
$entity = new JoinedInheritanceWritableColumn();
$entity->writableContent = 'foo';
$this->_em->persist($entity);
$this->_em->flush();
// check INSERT query doesn't change insertable entity property
self::assertEquals('foo', $entity->writableContent);
// check other process get same state
$this->_em->clear();
$entity = $this->_em->find(JoinedInheritanceWritableColumn::class, $entity->id);
self::assertInstanceOf(JoinedInheritanceWritableColumn::class, $entity);
self::assertEquals('foo', $entity->writableContent);
return $entity->id;
}
/** @depends testChildWritableColumnInsert */
public function testChildWritableColumnUpdate(int $entityId): void
{
$entity = $this->_em->find(JoinedInheritanceWritableColumn::class, $entityId);
self::assertInstanceOf(JoinedInheritanceWritableColumn::class, $entity);
// update exist entity
$entity->writableContent = 'bar';
$this->_em->persist($entity);
$this->_em->flush();
// check UPDATE query doesn't change updatable entity property
self::assertEquals('bar', $entity->writableContent);
// check other process get same state
$this->_em->clear();
$entity = $this->_em->find(JoinedInheritanceWritableColumn::class, $entity->id);
self::assertInstanceOf(JoinedInheritanceWritableColumn::class, $entity);
self::assertEquals('bar', $entity->writableContent);
}
public function testChildNonWritableColumnInsert(): int
{
$entity = new JoinedInheritanceNonWritableColumn();
$entity->nonWritableContent = 'foo';
$this->_em->persist($entity);
$this->_em->flush();
// check INSERT query cause set database value into non-insertable entity property
self::assertEquals('dbDefault', $entity->nonWritableContent);
// check other process get same state
$this->_em->clear();
$entity = $this->_em->find(JoinedInheritanceNonWritableColumn::class, $entity->id);
self::assertInstanceOf(JoinedInheritanceNonWritableColumn::class, $entity);
self::assertEquals('dbDefault', $entity->nonWritableContent);
return $entity->id;
}
/** @depends testChildNonWritableColumnInsert */
public function testChildNonWritableColumnUpdate(int $entityId): void
{
$entity = $this->_em->find(JoinedInheritanceNonWritableColumn::class, $entityId);
self::assertInstanceOf(JoinedInheritanceNonWritableColumn::class, $entity);
// update exist entity
$entity->nonWritableContent = 'bar';
// change some property to ensure UPDATE query will be done
self::assertNotEquals('bar', $entity->rootField);
$entity->rootField = 'bar';
$this->_em->persist($entity);
$this->_em->flush();
// check UPDATE query cause set database value into non-updatable entity property
self::assertEquals('dbDefault', $entity->nonWritableContent);
// check other process get same state
$this->_em->clear();
$entity = $this->_em->find(JoinedInheritanceNonWritableColumn::class, $entity->id);
self::assertInstanceOf(JoinedInheritanceNonWritableColumn::class, $entity);
self::assertEquals('bar', $entity->rootField); // check that UPDATE query done
self::assertEquals('dbDefault', $entity->nonWritableContent);
}
public function testChildNonInsertableColumnInsert(): int
{
$entity = new JoinedInheritanceNonInsertableColumn();
$entity->nonInsertableContent = 'foo';
$this->_em->persist($entity);
$this->_em->flush();
// check INSERT query cause set database value into non-insertable entity property
self::assertEquals('dbDefault', $entity->nonInsertableContent);
// check other process get same state
$this->_em->clear();
$entity = $this->_em->find(JoinedInheritanceNonInsertableColumn::class, $entity->id);
self::assertInstanceOf(JoinedInheritanceNonInsertableColumn::class, $entity);
self::assertEquals('dbDefault', $entity->nonInsertableContent);
return $entity->id;
}
/** @depends testChildNonInsertableColumnInsert */
public function testChildNonInsertableColumnUpdate(int $entityId): void
{
$entity = $this->_em->find(JoinedInheritanceNonInsertableColumn::class, $entityId);
self::assertInstanceOf(JoinedInheritanceNonInsertableColumn::class, $entity);
// update exist entity
$entity->nonInsertableContent = 'bar';
$this->_em->persist($entity);
$this->_em->flush();
// check UPDATE query doesn't change updatable entity property
self::assertEquals('bar', $entity->nonInsertableContent);
// check other process get same state
$this->_em->clear();
$entity = $this->_em->find(JoinedInheritanceNonInsertableColumn::class, $entity->id);
self::assertInstanceOf(JoinedInheritanceNonInsertableColumn::class, $entity);
self::assertEquals('bar', $entity->nonInsertableContent);
}
public function testChildNonUpdatableColumnInsert(): int
{
$entity = new JoinedInheritanceNonUpdatableColumn();
$entity->nonUpdatableContent = 'foo';
$this->_em->persist($entity);
$this->_em->flush();
// check INSERT query doesn't change insertable entity property
self::assertEquals('foo', $entity->nonUpdatableContent);
// check other process get same state
$this->_em->clear();
$entity = $this->_em->find(JoinedInheritanceNonUpdatableColumn::class, $entity->id);
self::assertInstanceOf(JoinedInheritanceNonUpdatableColumn::class, $entity);
self::assertEquals('foo', $entity->nonUpdatableContent);
return $entity->id;
}
/** @depends testChildNonUpdatableColumnInsert */
public function testChildNonUpdatableColumnUpdate(int $entityId): void
{
$entity = $this->_em->find(JoinedInheritanceNonUpdatableColumn::class, $entityId);
self::assertInstanceOf(JoinedInheritanceNonUpdatableColumn::class, $entity);
self::assertEquals('foo', $entity->nonUpdatableContent);
// update exist entity
$entity->nonUpdatableContent = 'bar';
// change some property to ensure UPDATE query will be done
self::assertNotEquals('bar', $entity->rootField);
$entity->rootField = 'bar';
$this->_em->persist($entity);
$this->_em->flush();
// check UPDATE query cause set database value into non-updatable entity property
self::assertEquals('foo', $entity->nonUpdatableContent);
// check other process get same state
$this->_em->clear();
$entity = $this->_em->find(JoinedInheritanceNonUpdatableColumn::class, $entity->id);
self::assertInstanceOf(JoinedInheritanceNonUpdatableColumn::class, $entity);
self::assertEquals('bar', $entity->rootField); // check that UPDATE query done
self::assertEquals('foo', $entity->nonUpdatableContent);
}
}

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