diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 00000000000..242bf81fe82 --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,51 @@ +name: "Documentation" + +on: + pull_request: + branches: + - "*.x" + paths: + - .github/workflows/documentation.yml + - docs/** + push: + branches: + - "*.x" + paths: + - .github/workflows/documentation.yml + - docs/** + +jobs: + validate-with-guides: + name: "Validate documentation with phpDocumentor/guides" + runs-on: "ubuntu-22.04" + + steps: + - name: "Checkout code" + uses: "actions/checkout@v3" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.2" + + - name: "Remove existing composer file" + run: "rm composer.json" + + - name: "Require phpdocumentor/guides-cli" + run: "composer require --dev phpdocumentor/guides-cli dev-main@dev --no-update" + + - name: "Configure minimum stability" + run: "composer config minimum-stability dev" + + - name: "Install dependencies with Composer" + uses: "ramsey/composer-install@v2" + with: + dependency-versions: "highest" + + - name: "Add dummy title to the sidebar" + run: | + printf '%s\n%s\n\n%s\n' "Dummy title" "===========" "$(cat docs/en/sidebar.rst)" > docs/en/sidebar.rst + + - name: "Run guides-cli" + run: "vendor/bin/guides -vvv --no-progress docs/en /tmp/test 2>&1 | ( ! grep WARNING )" diff --git a/UPGRADE.md b/UPGRADE.md index a583a83fffc..ef703efa09d 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,5 +1,18 @@ # 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. diff --git a/composer.json b/composer.json index 4184b077f7a..a897bbbf8fd 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "doctrine/lexer": "^2", "doctrine/persistence": "^2.4 || ^3", "psr/cache": "^1 || ^2 || ^3", - "symfony/console": "^4.2 || ^5.0 || ^6.0 || ^7.0", + "symfony/console": "^4.2 || ^5.0 || ^6.0", "symfony/polyfill-php72": "^1.23", "symfony/polyfill-php80": "^1.16" }, @@ -42,14 +42,14 @@ "doctrine/annotations": "^1.13 || ^2", "doctrine/coding-standard": "^9.0.2 || ^12.0", "phpbench/phpbench": "^0.16.10 || ^1.0", - "phpstan/phpstan": "~1.4.10 || 1.10.18", + "phpstan/phpstan": "~1.4.10 || 1.10.25", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", "psr/log": "^1 || ^2 || ^3", "squizlabs/php_codesniffer": "3.7.2", "symfony/cache": "^4.4 || ^5.4 || ^6.0", "symfony/var-exporter": "^4.4 || ^5.4 || ^6.2", "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0", - "vimeo/psalm": "4.30.0 || 5.12.0" + "vimeo/psalm": "4.30.0 || 5.13.1" }, "conflict": { "doctrine/annotations": "<1.13 || >= 3.0" diff --git a/docs/en/index.rst b/docs/en/index.rst index f89c4ec2693..a7979cb1a70 100644 --- a/docs/en/index.rst +++ b/docs/en/index.rst @@ -18,8 +18,8 @@ Doctrine ORM don't panic. You can get help from different sources: - Report a bug on `GitHub `_. - On `StackOverflow `_ -If you need more structure over the different topics you can browse the :doc:`table -of contents `. +If you need more structure over the different topics you can browse the table +of contents. Getting Started --------------- @@ -34,32 +34,32 @@ Mapping Objects onto a Database ------------------------------- * **Mapping**: - :doc:`Objects ` | - :doc:`Associations ` | + :doc:`Objects ` \| + :doc:`Associations ` \| :doc:`Inheritance ` * **Drivers**: - :doc:`Docblock Annotations ` | - :doc:`Attributes ` | - :doc:`XML ` | - :doc:`YAML ` | + :doc:`Docblock Annotations ` \| + :doc:`Attributes ` \| + :doc:`XML ` \| + :doc:`YAML ` \| :doc:`PHP ` Working with Objects -------------------- * **Basic Reference**: - :doc:`Entities ` | - :doc:`Associations ` | + :doc:`Entities ` \| + :doc:`Associations ` \| :doc:`Events ` * **Query Reference**: - :doc:`DQL ` | - :doc:`QueryBuilder ` | + :doc:`DQL ` \| + :doc:`QueryBuilder ` \| :doc:`Native SQL ` * **Internals**: - :doc:`Internals explained ` | + :doc:`Internals explained ` \| :doc:`Associations ` Advanced Topics @@ -102,20 +102,20 @@ Cookbook -------- * **Patterns**: - :doc:`Aggregate Fields ` | - :doc:`Decorator Pattern ` | + :doc:`Aggregate Fields ` \| + :doc:`Decorator Pattern ` \| :doc:`Strategy Pattern ` * **DQL Extension Points**: - :doc:`DQL Custom Walkers ` | + :doc:`DQL Custom Walkers ` \| :doc:`DQL User-Defined-Functions ` * **Implementation**: - :doc:`Array Access ` | - :doc:`Notify ChangeTracking Example ` | - :doc:`Working with DateTime ` | - :doc:`Validation ` | - :doc:`Entities in the Session ` | + :doc:`Array Access ` \| + :doc:`Notify ChangeTracking Example ` \| + :doc:`Working with DateTime ` \| + :doc:`Validation ` \| + :doc:`Entities in the Session ` \| :doc:`Keeping your Modules independent ` * **Hidden Gems** @@ -124,5 +124,3 @@ Cookbook * **Custom Datatypes** :doc:`MySQL Enums ` :doc:`Advanced Field Value Conversion ` - -.. include:: toc.rst diff --git a/docs/en/reference/advanced-configuration.rst b/docs/en/reference/advanced-configuration.rst index 2973e2e9ebb..fdd42aeb6c5 100644 --- a/docs/en/reference/advanced-configuration.rst +++ b/docs/en/reference/advanced-configuration.rst @@ -311,10 +311,12 @@ Reference Proxies The method ``EntityManager#getReference($entityName, $identifier)`` lets you obtain a reference to an entity for which the identifier -is known, without loading that entity from the database. This is -useful, for example, as a performance enhancement, when you want to -establish an association to an entity for which you have the -identifier. You could simply do this: +is known, without necessarily loading that entity from the database. +This is useful, for example, as a performance enhancement, when you +want to establish an association to an entity for which you have the +identifier. + +Consider the following example: .. code-block:: php @@ -324,15 +326,33 @@ identifier. You could simply do this: $item = $em->getReference('MyProject\Model\Item', $itemId); $cart->addItem($item); -Here, we added an Item to a Cart without loading the Item from the -database. If you access any state that isn't yet available in the -Item instance, the proxying mechanism would fully initialize the -object's state transparently from the database. Here -$item is actually an instance of the proxy class that was generated -for the Item class but your code does not need to care. In fact it -**should not care**. Proxy objects should be transparent to your +Whether the object being returned from ``EntityManager#getReference()`` +is a proxy or a direct instance of the entity class may depend on different +factors, including whether the entity has already been loaded into memory +or entity inheritance being used. But your code does not need to care +and in fact it **should not care**. Proxy objects should be transparent to your code. +When using the ``EntityManager#getReference()`` method, you need to be aware +of a few peculiarities. + +At the best case, the ORM can avoid querying the database at all. But, that +also means that this method will not throw an exception when an invalid value +for the ``$identifier`` parameter is passed. ``$identifier`` values are +not checked and there is no guarantee that the requested entity instance even +exists – the method will still return a proxy object. + +Its only when the proxy has to be fully initialized or associations cannot +be written to the database that invalid ``$identifier`` values may lead to +exceptions. + +The ``EntityManager#getReference()`` is mostly useful when you only +need a reference to some entity to make an association, like in the example +above. In that case, it can save you from loading data from the database +that you don't need. But remember – as soon as you read any property values +besides those making up the ID, a database request will be made to initialize +all fields. + Association proxies ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/en/reference/architecture.rst b/docs/en/reference/architecture.rst index 67e1388d9ca..c9d16a69ff9 100644 --- a/docs/en/reference/architecture.rst +++ b/docs/en/reference/architecture.rst @@ -102,7 +102,7 @@ persistent entity state and mapping information for its subclasses, but which is not itself an entity. Mapped superclasses are explained in greater detail in the chapter -on :doc:`inheritance mapping `. +on :doc:`inheritance mapping `. Transient Classes ~~~~~~~~~~~~~~~~~ diff --git a/docs/en/reference/basic-mapping.rst b/docs/en/reference/basic-mapping.rst index 1d8fd856344..6004988a15a 100644 --- a/docs/en/reference/basic-mapping.rst +++ b/docs/en/reference/basic-mapping.rst @@ -460,7 +460,7 @@ Here is the list of possible generation strategies: a new entity is passed to ``EntityManager#persist``. NONE is the same as leaving off the ``#[GeneratedValue]`` entirely. - ``CUSTOM``: With this option, you can use the ``#[CustomIdGenerator]`` attribute. - It will allow you to pass a :doc:`class of your own to generate the identifiers.<_annref_customidgenerator>` + It will allow you to pass a :ref:`class of your own to generate the identifiers.` Sequence Generator ^^^^^^^^^^^^^^^^^^ diff --git a/docs/en/reference/configuration.rst b/docs/en/reference/configuration.rst index 831ae91a16a..a65ede69b28 100644 --- a/docs/en/reference/configuration.rst +++ b/docs/en/reference/configuration.rst @@ -104,7 +104,7 @@ Inside the ``ORMSetup`` methods several assumptions are made: In order to have ``ORMSetup`` configure the cache automatically, the library ``symfony/cache`` has to be installed as a dependency. -If you want to configure Doctrine in more detail, take a look at the :doc:`Advanced Configuration ` section. +If you want to configure Doctrine in more detail, take a look at the :doc:`Advanced Configuration ` section. .. note:: diff --git a/docs/en/reference/dql-doctrine-query-language.rst b/docs/en/reference/dql-doctrine-query-language.rst index 140d558b336..38c54024226 100644 --- a/docs/en/reference/dql-doctrine-query-language.rst +++ b/docs/en/reference/dql-doctrine-query-language.rst @@ -1336,8 +1336,8 @@ There are situations when a query you want to execute returns a very large result-set that needs to be processed. All the previously described hydration modes completely load a result-set into memory which might not be feasible with large result sets. See -the `Batch Processing `_ section on details how -to iterate large result sets. +the :doc:`Batch Processing ` section on +details how to iterate large result sets. Functions ~~~~~~~~~ diff --git a/docs/en/reference/events.rst b/docs/en/reference/events.rst index 97c3a6f339c..0307429b864 100644 --- a/docs/en/reference/events.rst +++ b/docs/en/reference/events.rst @@ -281,10 +281,10 @@ specific to a particular entity class's lifecycle. - + @@ -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. diff --git a/docs/en/reference/filters.rst b/docs/en/reference/filters.rst index bf4e733a7f6..58cabbc48fd 100644 --- a/docs/en/reference/filters.rst +++ b/docs/en/reference/filters.rst @@ -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 + + 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. diff --git a/docs/en/reference/improving-performance.rst b/docs/en/reference/improving-performance.rst index 79610644a7c..0022efdd271 100644 --- a/docs/en/reference/improving-performance.rst +++ b/docs/en/reference/improving-performance.rst @@ -91,7 +91,7 @@ Apply Best Practices A lot of the points mentioned in the Best Practices chapter will also positively affect the performance of Doctrine. -See :doc:`Best Practices ` +See :doc:`Best Practices ` Change Tracking policies ------------------------ diff --git a/docs/en/reference/inheritance-mapping.rst b/docs/en/reference/inheritance-mapping.rst index 1605cf5d3cd..0e23c8f7d87 100644 --- a/docs/en/reference/inheritance-mapping.rst +++ b/docs/en/reference/inheritance-mapping.rst @@ -35,7 +35,7 @@ have to be used. superclass, since they require the "many" side to hold the foreign key. - It is, however, possible to use the :doc:`ResolveTargetEntityListener ` + It is, however, possible to use the :doc:`ResolveTargetEntityListener ` to replace references to a mapped superclass with an entity class at runtime. As long as there is only one entity subclass inheriting from the mapped superclass and all references to the mapped superclass are resolved to that @@ -45,7 +45,7 @@ have to be used. .. warning:: At least when using attributes or annotations to specify your mapping, - it _seems_ as if you could inherit from a base class that is neither + it *seems* as if you could inherit from a base class that is neither an entity nor a mapped superclass, but has properties with mapping configuration on them that would also be used in the inheriting class. @@ -60,7 +60,7 @@ have to be used. You may be tempted to use traits to mix mapped fields or relationships into your entity classes to circumvent some of the limitations of mapped superclasses. Before doing that, please read the section on traits - in the :doc:`Limitations and Known Issues ` chapter. + in the :doc:`Limitations and Known Issues ` chapter. Example: @@ -380,7 +380,7 @@ It is not supported to use overrides in entity inheritance scenarios. .. note:: When using traits, make sure not to miss the warnings given in the - :doc:`Limitations and Known Issues` chapter. + :doc:`Limitations and Known Issues` chapter. Association Override diff --git a/docs/en/reference/installation.rst b/docs/en/reference/installation.rst index fec83787eff..dab1364f777 100644 --- a/docs/en/reference/installation.rst +++ b/docs/en/reference/installation.rst @@ -1,4 +1,4 @@ Installation ============ -The installation chapter has moved to :doc:`Installation and Configuration `_. +The installation chapter has moved to :doc:`Installation and Configuration `. diff --git a/docs/en/reference/limitations-and-known-issues.rst b/docs/en/reference/limitations-and-known-issues.rst index cf67c67e3a2..8a960f63d76 100644 --- a/docs/en/reference/limitations-and-known-issues.rst +++ b/docs/en/reference/limitations-and-known-issues.rst @@ -145,7 +145,7 @@ more than two years after the initial Doctrine 2 release and the time where core components were designed. In fact, this documentation mentions traits only in the context of -:doc:`overriding field association mappings in subclasses `. +:doc:`overriding field association mappings in subclasses `. Coverage of traits in test cases is practically nonexistent. Thus, you should at least be aware that when using traits in your entity and @@ -162,7 +162,7 @@ that, some precedence and conflict resolution rules apply. When it comes to loading mapping configuration, the annotation and attribute drivers rely on PHP reflection to inspect class properties including their docblocks. -As long as the results are consistent with what a solution _without_ traits would +As long as the results are consistent with what a solution *without* traits would have produced, this is probably fine. However, to mention known limitations, it is currently not possible to use "class" diff --git a/docs/en/reference/query-builder.rst b/docs/en/reference/query-builder.rst index 7e3e3fff363..99de6df8d08 100644 --- a/docs/en/reference/query-builder.rst +++ b/docs/en/reference/query-builder.rst @@ -578,8 +578,6 @@ of DQL. It takes 3 parameters: ``$dqlPartName``, ``$dqlPart`` and not (no effect on the ``where`` and ``having`` DQL query parts, which always override all previously defined items) -- - .. code-block:: php - + @@ -427,7 +430,10 @@ It caches the primary keys of association and cache each element will be cached .. code-block:: xml - + diff --git a/docs/en/reference/unitofwork.rst b/docs/en/reference/unitofwork.rst index fc8459cd2ff..4af7f16decf 100644 --- a/docs/en/reference/unitofwork.rst +++ b/docs/en/reference/unitofwork.rst @@ -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); diff --git a/docs/en/reference/xml-mapping.rst b/docs/en/reference/xml-mapping.rst index 67edc4a4965..c8c1abe51d4 100644 --- a/docs/en/reference/xml-mapping.rst +++ b/docs/en/reference/xml-mapping.rst @@ -16,9 +16,9 @@ setup for the latest code in trunk. .. code-block:: xml - ... @@ -102,9 +102,9 @@ of several common elements: // Doctrine.Tests.ORM.Mapping.User.dcm.xml - @@ -769,9 +769,9 @@ entity relationship. You can define this in XML with the "association-key" attri .. code-block:: xml - diff --git a/docs/en/toc.rst b/docs/en/toc.rst deleted file mode 100644 index fa92cf38021..00000000000 --- a/docs/en/toc.rst +++ /dev/null @@ -1,86 +0,0 @@ -Welcome to Doctrine 2 ORM's documentation! -========================================== - -Tutorials ---------- - -.. toctree:: - :maxdepth: 1 - - tutorials/getting-started - tutorials/getting-started-database - tutorials/getting-started-models - tutorials/working-with-indexed-associations - tutorials/extra-lazy-associations - tutorials/composite-primary-keys - tutorials/ordered-associations - tutorials/override-field-association-mappings-in-subclasses - tutorials/pagination.rst - tutorials/embeddables.rst - -Reference Guide ---------------- - -.. toctree:: - :maxdepth: 1 - :numbered: - - reference/architecture - reference/configuration.rst - reference/faq - reference/basic-mapping - reference/association-mapping - reference/inheritance-mapping - reference/working-with-objects - reference/working-with-associations - reference/events - reference/unitofwork - reference/unitofwork-associations - reference/transactions-and-concurrency - reference/batch-processing - reference/dql-doctrine-query-language - reference/query-builder - reference/native-sql - reference/change-tracking-policies - reference/partial-objects - reference/annotations-reference - reference/attributes-reference - reference/xml-mapping - reference/yaml-mapping - reference/php-mapping - reference/caching - reference/improving-performance - reference/tools - reference/metadata-drivers - reference/best-practices - reference/limitations-and-known-issues - tutorials/pagination - reference/filters - reference/namingstrategy - reference/advanced-configuration - reference/second-level-cache - reference/security - - -Cookbook --------- - -.. toctree:: - :maxdepth: 1 - - cookbook/aggregate-fields - cookbook/custom-mapping-types - cookbook/decorator-pattern - cookbook/dql-custom-walkers - cookbook/dql-user-defined-functions - cookbook/implementing-arrayaccess-for-domain-objects - cookbook/implementing-the-notify-changetracking-policy - cookbook/resolve-target-entity-listener - cookbook/sql-table-prefixes - cookbook/strategy-cookbook-introduction - cookbook/validation-of-entities - cookbook/working-with-datetime - cookbook/mysql-enums - cookbook/advanced-field-value-conversion-using-custom-mapping-types - cookbook/entities-in-session - diff --git a/docs/en/tutorials/composite-primary-keys.rst b/docs/en/tutorials/composite-primary-keys.rst index 62111cdd1e3..456adeaf5de 100644 --- a/docs/en/tutorials/composite-primary-keys.rst +++ b/docs/en/tutorials/composite-primary-keys.rst @@ -85,9 +85,9 @@ and year of production as primary keys: .. code-block:: xml - @@ -267,9 +267,9 @@ We keep up the example of an Article with arbitrary attributes, the mapping look .. code-block:: xml - diff --git a/docs/en/tutorials/extra-lazy-associations.rst b/docs/en/tutorials/extra-lazy-associations.rst index ca558f4530d..fbff96f428b 100644 --- a/docs/en/tutorials/extra-lazy-associations.rst +++ b/docs/en/tutorials/extra-lazy-associations.rst @@ -85,9 +85,9 @@ switch to extra lazy as shown in these examples: .. code-block:: xml - diff --git a/docs/en/tutorials/getting-started.rst b/docs/en/tutorials/getting-started.rst index 071486ee053..d6c4c816302 100644 --- a/docs/en/tutorials/getting-started.rst +++ b/docs/en/tutorials/getting-started.rst @@ -102,8 +102,7 @@ Install Doctrine using the Composer Dependency Management tool, by calling: This will install the packages Doctrine Common, Doctrine DBAL, Doctrine ORM, into the ``vendor`` directory. -Add the following directories: -:: +Add the following directories:: doctrine2-tutorial |-- config @@ -558,10 +557,10 @@ methods, but you only need to choose one. .. code-block:: xml - + @@ -1139,10 +1138,10 @@ the ``Product`` before: .. code-block:: xml - + @@ -1294,10 +1293,10 @@ Finally, we'll add metadata mappings for the ``User`` entity. .. code-block:: xml - + @@ -1344,8 +1343,7 @@ means the join details have already been defined on the owning side. Therefore we only have to specify the property on the Bug class that holds the owning sides. -Update your database schema by running: -:: +Update your database schema by running:: $ php bin/doctrine orm:schema-tool:update --force @@ -1819,9 +1817,9 @@ we have to adjust the metadata slightly. .. code-block:: xml - diff --git a/docs/en/tutorials/working-with-indexed-associations.rst b/docs/en/tutorials/working-with-indexed-associations.rst index b09da398249..c4099eb5589 100644 --- a/docs/en/tutorials/working-with-indexed-associations.rst +++ b/docs/en/tutorials/working-with-indexed-associations.rst @@ -161,9 +161,9 @@ The code and mappings for the Market entity looks like this: .. code-block:: xml - @@ -278,9 +278,9 @@ here are the code and mappings for it: .. code-block:: xml - diff --git a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php index ba806eac62a..1c11ebbb8dd 100644 --- a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php +++ b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php @@ -16,7 +16,6 @@ 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 @@ private function storeAssociationCache(QueryCacheKey $key, array $assoc, $assocV $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; diff --git a/lib/Doctrine/ORM/Cache/Persister/Entity/AbstractEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/Entity/AbstractEntityPersister.php index 2878db50e07..2ea4c132ba8 100644 --- a/lib/Doctrine/ORM/Cache/Persister/Entity/AbstractEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/Entity/AbstractEntityPersister.php @@ -23,6 +23,7 @@ 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 @@ public function getOwningTable($fieldName) */ 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(); } diff --git a/lib/Doctrine/ORM/Configuration.php b/lib/Doctrine/ORM/Configuration.php index cef4a9fff5e..fcde0c190fa 100644 --- a/lib/Doctrine/ORM/Configuration.php +++ b/lib/Doctrine/ORM/Configuration.php @@ -62,8 +62,6 @@ * 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 @@ public function getProxyDir() /** * 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 @@ public function getAutoGenerateProxyClasses() /** * 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 */ diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index 7722b15922d..94a609f0692 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -953,6 +953,14 @@ public function initializeObject($obj) $this->unitOfWork->initializeObject($obj); } + /** + * {@inheritDoc} + */ + public function isUninitializedObject($obj): bool + { + return $this->unitOfWork->isUninitializedObject($obj); + } + /** * Factory method to create EntityManager instances. * diff --git a/lib/Doctrine/ORM/Internal/CommitOrder/Edge.php b/lib/Doctrine/ORM/Internal/CommitOrder/Edge.php index f1457755ee1..98bb7378955 100644 --- a/lib/Doctrine/ORM/Internal/CommitOrder/Edge.php +++ b/lib/Doctrine/ORM/Internal/CommitOrder/Edge.php @@ -4,7 +4,12 @@ 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; diff --git a/lib/Doctrine/ORM/Internal/CommitOrder/Vertex.php b/lib/Doctrine/ORM/Internal/CommitOrder/Vertex.php index c4747e032d1..c748bd7eab0 100644 --- a/lib/Doctrine/ORM/Internal/CommitOrder/Vertex.php +++ b/lib/Doctrine/ORM/Internal/CommitOrder/Vertex.php @@ -4,9 +4,13 @@ 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; } diff --git a/lib/Doctrine/ORM/Internal/CommitOrder/VertexState.php b/lib/Doctrine/ORM/Internal/CommitOrder/VertexState.php index 395db58d554..24d2fb54ee1 100644 --- a/lib/Doctrine/ORM/Internal/CommitOrder/VertexState.php +++ b/lib/Doctrine/ORM/Internal/CommitOrder/VertexState.php @@ -4,7 +4,12 @@ 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 + ); } } diff --git a/lib/Doctrine/ORM/Internal/CommitOrderCalculator.php b/lib/Doctrine/ORM/Internal/CommitOrderCalculator.php index 22ee1ab13bd..f0f8112ba23 100644 --- a/lib/Doctrine/ORM/Internal/CommitOrderCalculator.php +++ b/lib/Doctrine/ORM/Internal/CommitOrderCalculator.php @@ -4,6 +4,7 @@ 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 @@ * 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. * diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index 9918936a3ff..a5d97d30966 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -10,7 +10,6 @@ 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 @@ protected function hydrateRowData(array $row, array &$result) // 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])) { diff --git a/lib/Doctrine/ORM/Internal/TopologicalSort.php b/lib/Doctrine/ORM/Internal/TopologicalSort.php new file mode 100644 index 00000000000..2bf1624ced8 --- /dev/null +++ b/lib/Doctrine/ORM/Internal/TopologicalSort.php @@ -0,0 +1,165 @@ + + */ + private $nodes = []; + + /** + * DFS state for the different nodes, indexed by node object id and using one of + * this class' constants as value. + * + * @var array + */ + 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> + */ + private $edges = []; + + /** + * Builds up the result during the DFS. + * + * @var list + */ + 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 + */ + 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]); + } +} diff --git a/lib/Doctrine/ORM/Internal/TopologicalSort/CycleDetectedException.php b/lib/Doctrine/ORM/Internal/TopologicalSort/CycleDetectedException.php new file mode 100644 index 00000000000..9b0bc49d257 --- /dev/null +++ b/lib/Doctrine/ORM/Internal/TopologicalSort/CycleDetectedException.php @@ -0,0 +1,55 @@ + */ + 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 */ + 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; + } +} diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 70e22aa048d..9d8f27cd1a8 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -1176,7 +1176,7 @@ public function initializeReflection($reflService) $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 @@ public function addInheritedFieldMapping(array $fieldMapping) $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 @@ private function getAccessibleProperty(ReflectionService $reflService, string $c { $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); } diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index 8fdb4495b0f..24ffa0d4168 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -365,7 +365,7 @@ public function loadMetadataForClass($className, PersistenceClassMetadata $metad } $mapping = []; - $mapping['fieldName'] = $property->getName(); + $mapping['fieldName'] = $property->name; // Evaluate @Cache annotation $cacheAnnot = $this->reader->getPropertyAnnotation($property, Mapping\Cache::class); @@ -398,7 +398,7 @@ public function loadMetadataForClass($className, PersistenceClassMetadata $metad // @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) { diff --git a/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php index d63e7b40edb..1e14836833e 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php @@ -23,7 +23,6 @@ use function constant; use function defined; use function get_class; -use function sprintf; use const PHP_VERSION_ID; @@ -57,10 +56,10 @@ class AttributeDriver extends CompatibilityAnnotationDriver public function __construct(array $paths, bool $reportFieldsWhereDeclared = false) { if (PHP_VERSION_ID < 80000) { - throw new LogicException(sprintf( + throw new LogicException( 'The attribute metadata driver cannot be enabled on PHP 7. Please upgrade to PHP 8 or choose a different' . ' metadata driver.' - )); + ); } $this->reader = new AttributeReader(); @@ -316,7 +315,7 @@ public function loadMetadataForClass($className, PersistenceClassMetadata $metad } $mapping = []; - $mapping['fieldName'] = $property->getName(); + $mapping['fieldName'] = $property->name; // Evaluate #[Cache] attribute $cacheAttribute = $this->reader->getPropertyAttribute($property, Mapping\Cache::class); @@ -351,7 +350,7 @@ public function loadMetadataForClass($className, PersistenceClassMetadata $metad $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; diff --git a/lib/Doctrine/ORM/Mapping/Driver/ReflectionBasedDriver.php b/lib/Doctrine/ORM/Mapping/Driver/ReflectionBasedDriver.php index e42aab7b9cc..a2900d13e01 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/ReflectionBasedDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/ReflectionBasedDriver.php @@ -32,7 +32,7 @@ private function isRepeatedPropertyDeclaration(ReflectionProperty $property, Cla || $metadata->isInheritedEmbeddedClass($property->name); } - $declaringClass = $property->getDeclaringClass()->getName(); + $declaringClass = $property->class; if ( isset($metadata->fieldMappings[$property->name]['declared']) diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php index f755ad5f9a7..abd94fd01e3 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -50,27 +50,25 @@ class XmlDriver extends FileDriver public function __construct($locator, $fileExtension = self::DEFAULT_FILE_EXTENSION, bool $isXsdValidationEnabled = false) { if (! extension_loaded('simplexml')) { - throw new LogicException(sprintf( + throw new LogicException( 'The XML metadata driver cannot be enabled because the SimpleXML PHP extension is missing.' . ' Please configure PHP with SimpleXML or choose a different metadata driver.' - )); + ); } if (! $isXsdValidationEnabled) { Deprecation::trigger( 'doctrine/orm', 'https://github.com/doctrine/orm/pull/6728', - sprintf( - 'Using XML mapping driver with XSD validation disabled is deprecated' - . ' and will not be supported in Doctrine ORM 3.0.' - ) + 'Using XML mapping driver with XSD validation disabled is deprecated' + . ' and will not be supported in Doctrine ORM 3.0.' ); } if ($isXsdValidationEnabled && ! extension_loaded('dom')) { - throw new LogicException(sprintf( + throw new LogicException( 'XSD validation cannot be enabled because the DOM extension is missing.' - )); + ); } $this->isXsdValidationEnabled = $isXsdValidationEnabled; @@ -378,30 +376,8 @@ public function loadMetadataForClass($className, PersistenceClassMetadata $metad continue; } - $mapping = [ - 'id' => true, - 'fieldName' => (string) $idElement['name'], - ]; - - if (isset($idElement['type'])) { - $mapping['type'] = (string) $idElement['type']; - } - - if (isset($idElement['length'])) { - $mapping['length'] = (int) $idElement['length']; - } - - if (isset($idElement['column'])) { - $mapping['columnName'] = (string) $idElement['column']; - } - - if (isset($idElement['column-definition'])) { - $mapping['columnDefinition'] = (string) $idElement['column-definition']; - } - - if (isset($idElement->options)) { - $mapping['options'] = $this->parseOptions($idElement->options->children()); - } + $mapping = $this->columnToArray($idElement); + $mapping['id'] = true; $metadata->mapField($mapping); diff --git a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php index 4a1882c0e16..c6ff83e0f17 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php @@ -48,11 +48,11 @@ public function __construct($locator, $fileExtension = self::DEFAULT_FILE_EXTENS ); if (! class_exists(Yaml::class)) { - throw new LogicException(sprintf( + throw new LogicException( 'The YAML metadata driver cannot be enabled because the "symfony/yaml" library' . ' is not installed. Please run "composer require symfony/yaml" or choose a different' . ' metadata driver.' - )); + ); } parent::__construct($locator, $fileExtension); diff --git a/lib/Doctrine/ORM/Mapping/Reflection/ReflectionPropertiesGetter.php b/lib/Doctrine/ORM/Mapping/Reflection/ReflectionPropertiesGetter.php index 11b41ea9332..b6a289d5e79 100644 --- a/lib/Doctrine/ORM/Mapping/Reflection/ReflectionPropertiesGetter.php +++ b/lib/Doctrine/ORM/Mapping/Reflection/ReflectionPropertiesGetter.php @@ -73,7 +73,7 @@ private function getHierarchyClasses(string $className): array $parentClass = $currentClass->getParentClass(); if ($parentClass) { - $parentClassName = $parentClass->getName(); + $parentClassName = $parentClass->name; } } @@ -111,14 +111,14 @@ private function isInstanceProperty(ReflectionProperty $reflectionProperty): boo 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 @@ private function getLogicalName(ReflectionProperty $property): string return "\0*\0" . $propertyName; } - return "\0" . $property->getDeclaringClass()->getName() . "\0" . $propertyName; + return "\0" . $property->class . "\0" . $propertyName; } } diff --git a/lib/Doctrine/ORM/Mapping/ReflectionEmbeddedProperty.php b/lib/Doctrine/ORM/Mapping/ReflectionEmbeddedProperty.php index 563a699480f..db4c1f868a3 100644 --- a/lib/Doctrine/ORM/Mapping/ReflectionEmbeddedProperty.php +++ b/lib/Doctrine/ORM/Mapping/ReflectionEmbeddedProperty.php @@ -38,7 +38,7 @@ public function __construct(ReflectionProperty $parentProperty, ReflectionProper $this->childProperty = $childProperty; $this->embeddedClass = (string) $embeddedClass; - parent::__construct($childProperty->getDeclaringClass()->getName(), $childProperty->getName()); + parent::__construct($childProperty->class, $childProperty->name); } /** diff --git a/lib/Doctrine/ORM/Mapping/ReflectionEnumProperty.php b/lib/Doctrine/ORM/Mapping/ReflectionEnumProperty.php index a162ebfd345..3283a54ab0d 100644 --- a/lib/Doctrine/ORM/Mapping/ReflectionEnumProperty.php +++ b/lib/Doctrine/ORM/Mapping/ReflectionEnumProperty.php @@ -28,8 +28,8 @@ public function __construct(ReflectionProperty $originalReflectionProperty, stri $this->enumType = $enumType; parent::__construct( - $originalReflectionProperty->getDeclaringClass()->getName(), - $originalReflectionProperty->getName() + $originalReflectionProperty->class, + $originalReflectionProperty->name ); } @@ -98,7 +98,7 @@ private function initializeEnumValue($object, $value): BackedEnum } catch (ValueError $e) { throw MappingException::invalidEnumValue( get_class($object), - $this->originalReflectionProperty->getName(), + $this->originalReflectionProperty->name, (string) $value, $enumType, $e diff --git a/lib/Doctrine/ORM/ORMSetup.php b/lib/Doctrine/ORM/ORMSetup.php index 1039ce82004..4b667628610 100644 --- a/lib/Doctrine/ORM/ORMSetup.php +++ b/lib/Doctrine/ORM/ORMSetup.php @@ -24,7 +24,6 @@ use function class_exists; use function extension_loaded; use function md5; -use function sprintf; use function sys_get_temp_dir; final class ORMSetup @@ -73,11 +72,11 @@ public static function createDefaultAnnotationDriver( __METHOD__ ); if (! class_exists(AnnotationReader::class)) { - throw new LogicException(sprintf( + throw new LogicException( 'The annotation metadata driver cannot be enabled because the "doctrine/annotations" library' . ' is not installed. Please run "composer require doctrine/annotations" or choose a different' . ' metadata driver.' - )); + ); } $reader = new AnnotationReader(); diff --git a/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php index 1f6d3e51271..32ac1d4f531 100644 --- a/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php @@ -183,7 +183,7 @@ class BasicEntityPersister implements EntityPersister * * @var IdentifierFlattener */ - private $identifierFlattener; + protected $identifierFlattener; /** @var CachedPersisterContext */ protected $currentPersisterContext; @@ -256,17 +256,17 @@ public function getInserts() 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 @@ public function executeInserts() $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 @@ public function executeInserts() if ($this->class->requiresFetchAfterChange) { $this->assignDefaultVersionAndUpsertableValues($entity, $id); } - } - - $this->queuedInserts = []; - return $postInsertIds; + // 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]); + } } /** @@ -376,7 +379,7 @@ protected function fetchVersionAndNotUpsertableValues($versionedClass, array $id * @return int[]|null[]|string[] * @psalm-return list */ - private function extractIdentifierTypes(array $id, ClassMetadata $versionedClass): array + final protected function extractIdentifierTypes(array $id, ClassMetadata $versionedClass): array { $types = []; @@ -675,10 +678,30 @@ protected function prepareUpdateData($entity, bool $isInsert = false) 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; diff --git a/lib/Doctrine/ORM/Persisters/Entity/EntityPersister.php b/lib/Doctrine/ORM/Persisters/Entity/EntityPersister.php index 7f9d54450bf..a69fbbdae12 100644 --- a/lib/Doctrine/ORM/Persisters/Entity/EntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/Entity/EntityPersister.php @@ -109,17 +109,15 @@ public function getSelectConditionStatementSQL($field, $value, $assoc = null, $c 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 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(); diff --git a/lib/Doctrine/ORM/Persisters/Entity/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/Entity/JoinedSubclassPersister.php index 4a84ce5786f..f286ac1d8e4 100644 --- a/lib/Doctrine/ORM/Persisters/Entity/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/Entity/JoinedSubclassPersister.php @@ -11,8 +11,11 @@ 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 @@ public function getOwningTable($fieldName) 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 @@ public function executeInserts() $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 @@ public function executeInserts() $stmt->executeStatement(); } + + if ($this->class->requiresFetchAfterChange) { + $this->assignDefaultVersionAndUpsertableValues($entity, $id); + } } $this->queuedInserts = []; - - return $postInsertIds; } /** @@ -514,6 +513,7 @@ protected function getInsertColumnList() || 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 @@ protected function assignDefaultVersionAndUpsertableValues($entity, array $id) } } + /** + * {@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 = ''; diff --git a/lib/Doctrine/ORM/Proxy/InternalProxy.php b/lib/Doctrine/ORM/Proxy/InternalProxy.php new file mode 100644 index 00000000000..baa5703f2a3 --- /dev/null +++ b/lib/Doctrine/ORM/Proxy/InternalProxy.php @@ -0,0 +1,19 @@ + + * + * @method void __setInitialized(bool $initialized) + */ +interface InternalProxy extends Proxy +{ +} diff --git a/lib/Doctrine/ORM/Proxy/Proxy.php b/lib/Doctrine/ORM/Proxy/Proxy.php index 51837a2b973..bc6015d97da 100644 --- a/lib/Doctrine/ORM/Proxy/Proxy.php +++ b/lib/Doctrine/ORM/Proxy/Proxy.php @@ -10,7 +10,11 @@ * Interface for proxy classes. * * @deprecated 2.14. Use \Doctrine\Persistence\Proxy instead + * + * @template T of object + * @template-extends BaseProxy + * @template-extends InternalProxy */ -interface Proxy extends BaseProxy +interface Proxy extends BaseProxy, InternalProxy { } diff --git a/lib/Doctrine/ORM/Proxy/ProxyFactory.php b/lib/Doctrine/ORM/Proxy/ProxyFactory.php index 2ba41caba72..63c925861c4 100644 --- a/lib/Doctrine/ORM/Proxy/ProxyFactory.php +++ b/lib/Doctrine/ORM/Proxy/ProxyFactory.php @@ -17,7 +17,6 @@ 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 @@ /** * 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 { @@ -48,13 +45,12 @@ class extends \ implements \ - /** - * @internal - */ - public bool $__isCloning = false; - - public function __construct(?\Closure $initializer = null) + public function __construct(?\Closure $initializer = null, ?\Closure $cloner = null) { + if ($cloner !== null) { + return; + } + self::createLazyGhost($initializer, , $this); } @@ -63,17 +59,6 @@ public function __isInitialized(): bool return isset($this->lazyObjectState) && $this->isLazyObjectInitialized(); } - public function __clone() - { - $this->__isCloning = true; - - try { - $this->__doClone(); - } finally { - $this->__isCloning = false; - } - } - public function __serialize(): array { @@ -98,23 +83,24 @@ public function __serialize(): array */ private $identifierFlattener; + /** @var ProxyDefinition[] */ + private $definitions = []; + /** * Initializes a new instance of the ProxyFactory class that is * connected to the given EntityManager. * - * @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'])); @@ -131,6 +117,26 @@ public function __construct(EntityManagerInterface $em, $proxyDir, $proxyNs, $au $this->identifierFlattener = new IdentifierFlattener($this->uow, $em->getMetadataFactory()); } + /** + * {@inheritDoc} + */ + public function getProxy($className, array $identifier) + { + $proxy = parent::getProxy($className, $identifier); + + if (! $this->em->getConfiguration()->isLazyGhostObjectEnabled()) { + return $proxy; + } + + $initializer = $this->definitions[$className]->initializer; + + $proxy->__construct(static function (InternalProxy $object) use ($initializer, $proxy): void { + $initializer($object, $proxy); + }); + + return $proxy; + } + /** * {@inheritDoc} */ @@ -158,7 +164,7 @@ protected function createProxyDefinition($className) $cloner = $this->createCloner($classMetadata, $entityPersister); } - return new ProxyDefinition( + return $this->definitions[$className] = new ProxyDefinition( ClassUtils::generateProxyClassName($className, $this->proxyNs), $classMetadata->getIdentifierFieldNames(), $classMetadata->getReflectionProperties(), @@ -231,15 +237,15 @@ private function createInitializer(ClassMetadata $classMetadata, EntityPersister /** * Creates a closure capable of initializing a proxy * - * @return Closure(Proxy):void + * @return Closure(InternalProxy, InternalProxy):void * * @throws EntityNotFoundException */ private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister): Closure { - return function (Proxy $proxy) use ($entityPersister, $classMetadata): void { - $identifier = $classMetadata->getIdentifierValues($proxy); - $entity = $entityPersister->loadById($identifier, $proxy->__isCloning ? null : $proxy); + return function (InternalProxy $proxy, InternalProxy $original) use ($entityPersister, $classMetadata): void { + $identifier = $classMetadata->getIdentifierValues($original); + $entity = $entityPersister->loadById($identifier, $original); if ($entity === null) { throw EntityNotFoundException::fromClassNameAndIdentifier( @@ -248,7 +254,7 @@ private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersi ); } - if (! $proxy->__isCloning) { + if ($proxy === $original) { return; } @@ -315,7 +321,6 @@ private function generateUseLazyGhostTrait(ClassMetadata $class): string isLazyObjectInitialized as private; createLazyGhost as private; resetLazyObject as private; - __clone as private __doClone; }'), $code); return $code; @@ -323,20 +328,20 @@ private function generateUseLazyGhostTrait(ClassMetadata $class): string private function generateSkippedProperties(ClassMetadata $class): string { - $skippedProperties = ['__isCloning' => true]; + $skippedProperties = []; $identifiers = array_flip($class->getIdentifierFieldNames()); $filter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE; $reflector = $class->getReflectionClass(); while ($reflector) { foreach ($reflector->getProperties($filter) as $property) { - $name = $property->getName(); + $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; } @@ -371,7 +376,7 @@ private function generateSerializeImpl(ClassMetadata $class): string 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); diff --git a/lib/Doctrine/ORM/Query/FilterCollection.php b/lib/Doctrine/ORM/Query/FilterCollection.php index 01c7e2c61c1..d8421baac6a 100644 --- a/lib/Doctrine/ORM/Query/FilterCollection.php +++ b/lib/Doctrine/ORM/Query/FilterCollection.php @@ -51,6 +51,14 @@ class FilterCollection */ private $enabledFilters = []; + /** + * Instances of suspended filters. + * + * @var SQLFilter[] + * @psalm-var array + */ + private $suspendedFilters = []; + /** * The filter hash from the last time the query was parsed. * @@ -83,6 +91,17 @@ public function getEnabledFilters() return $this->enabledFilters; } + /** + * Gets all the suspended filters. + * + * @return SQLFilter[] The suspended filters. + * @psalm-return array + */ + public function getSuspendedFilters(): array + { + return $this->suspendedFilters; + } + /** * Enables a filter from the collection. * @@ -105,6 +124,9 @@ public function enable($name) $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 @@ public function disable($name) 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 @@ public function isEnabled($name) 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. * diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index 4cfe233a1ed..7e69b4865d0 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -2566,8 +2566,7 @@ public function ConditionalPrimary() * EmptyCollectionComparisonExpression | CollectionMemberExpression | * InstanceOfExpression * - * @return AST\Node - * @psalm-return AST\BetweenExpression| + * @return (AST\BetweenExpression| * AST\CollectionMemberExpression| * AST\ComparisonExpression| * AST\EmptyCollectionComparisonExpression| @@ -2575,7 +2574,7 @@ public function ConditionalPrimary() * AST\InExpression| * AST\InstanceOfExpression| * AST\LikeExpression| - * AST\NullComparisonExpression + * AST\NullComparisonExpression) */ public function SimpleConditionalExpression() { diff --git a/lib/Doctrine/ORM/QueryBuilder.php b/lib/Doctrine/ORM/QueryBuilder.php index 115fddda58c..30ca64c41c2 100644 --- a/lib/Doctrine/ORM/QueryBuilder.php +++ b/lib/Doctrine/ORM/QueryBuilder.php @@ -809,7 +809,12 @@ public function select($select = null) */ public function distinct($flag = true) { - $this->dqlParts['distinct'] = (bool) $flag; + $flag = (bool) $flag; + + if ($this->dqlParts['distinct'] !== $flag) { + $this->dqlParts['distinct'] = $flag; + $this->state = self::STATE_DIRTY; + } return $this; } diff --git a/lib/Doctrine/ORM/Tools/AttachEntityListenersListener.php b/lib/Doctrine/ORM/Tools/AttachEntityListenersListener.php index 8b3e7977ad4..1193db40f38 100644 --- a/lib/Doctrine/ORM/Tools/AttachEntityListenersListener.php +++ b/lib/Doctrine/ORM/Tools/AttachEntityListenersListener.php @@ -56,7 +56,5 @@ public function loadClassMetadata(LoadClassMetadataEventArgs $event) $metadata->addEntityListener($listener['event'], $listener['class'], $listener['method']); } } - - unset($this->entityListeners[$metadata->name]); } } diff --git a/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryCommand.php index 2b2910410da..3ce2fb6cf1b 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryCommand.php @@ -17,6 +17,7 @@ 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 @@ protected function execute(InputInterface $input, OutputInterface $output) { $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) { + 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 ClearableCache)) { - throw new LogicException(sprintf( - 'Can only clear cache when ClearableCache interface is implemented, %s does not implement.', - get_debug_type($cacheDriver) - )); + 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 all 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) { diff --git a/lib/Doctrine/ORM/Tools/DebugUnitOfWorkListener.php b/lib/Doctrine/ORM/Tools/DebugUnitOfWorkListener.php index 699cf4f6e85..2b01d80af50 100644 --- a/lib/Doctrine/ORM/Tools/DebugUnitOfWorkListener.php +++ b/lib/Doctrine/ORM/Tools/DebugUnitOfWorkListener.php @@ -9,7 +9,6 @@ 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 @@ public function dumpIdentityMap(EntityManagerInterface $em) if ($value === null) { fwrite($fh, " NULL\n"); } else { - if ($value instanceof Proxy && ! $value->__isInitialized()) { + if ($uow->isUninitializedObject($value)) { fwrite($fh, '[PROXY] '); } diff --git a/lib/Doctrine/ORM/Tools/EntityGenerator.php b/lib/Doctrine/ORM/Tools/EntityGenerator.php index 34f4eba6258..5bf086d52b5 100644 --- a/lib/Doctrine/ORM/Tools/EntityGenerator.php +++ b/lib/Doctrine/ORM/Tools/EntityGenerator.php @@ -969,7 +969,7 @@ protected function getClassToExtendName() { $refl = new ReflectionClass($this->getClassToExtend()); - return '\\' . $refl->getName(); + return '\\' . $refl->name; } /** @return string */ diff --git a/lib/Doctrine/ORM/Tools/SchemaTool.php b/lib/Doctrine/ORM/Tools/SchemaTool.php index dc0fbc53e78..2e02279cc0c 100644 --- a/lib/Doctrine/ORM/Tools/SchemaTool.php +++ b/lib/Doctrine/ORM/Tools/SchemaTool.php @@ -821,8 +821,8 @@ private function gatherColumnOptions(array $mapping): array 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; } diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index ebb5f12a01b..5bf8a6b6085 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -28,6 +28,7 @@ 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\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_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 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 @@ public function commit($entity = null) $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 @@ public function commit($entity = null) } 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 @@ private function computeSingleEntityChangeSet($entity): void } // Ignore uninitialized proxy objects - if ($entity instanceof Proxy && ! $entity->__isInitialized()) { + if ($this->isUninitializedObject($entity)) { return; } @@ -906,7 +908,7 @@ public function computeChangeSets() foreach ($entitiesToProcess as $entity) { // Ignore uninitialized proxy objects - if ($entity instanceof Proxy && ! $entity->__isInitialized()) { + if ($this->isUninitializedObject($entity)) { continue; } @@ -931,7 +933,7 @@ public function computeChangeSets() */ private function computeAssociationChanges(array $assoc, $value): void { - if ($value instanceof Proxy && ! $value->__isInitialized()) { + if ($this->isUninitializedObject($value)) { return; } @@ -1124,6 +1126,20 @@ public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity) foreach ($actualData as $propName => $actualValue) { $orgValue = $originalData[$propName] ?? null; + if (isset($class->fieldMappings[$propName]['enumType'])) { + if (is_array($orgValue)) { + foreach ($orgValue as $id => $val) { + if ($val instanceof BackedEnum) { + $orgValue[$id] = $val->value; + } + } + } else { + if ($orgValue instanceof BackedEnum) { + $orgValue = $orgValue->value; + } + } + } + if ($orgValue !== $actualValue) { $changeSet[$propName] = [$orgValue, $actualValue]; } @@ -1142,64 +1158,46 @@ public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity) } /** - * 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); - - $insertionsForClass = []; + $entities = $this->computeInsertExecutionOrder(); - 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 ($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; + 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.' + ); - $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); + } } } @@ -1237,19 +1235,15 @@ private function addToEntityIdentifiersAndEntityMap( } /** - * 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); @@ -1270,18 +1264,17 @@ private function executeUpdates(ClassMetadata $class): void } /** - * 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); @@ -1305,73 +1298,116 @@ private function executeDeletions(ClassMetadata $class): void } } - /** - * Gets the commit order. - * - * @return list - */ - private function getCommitOrder(): array + /** @return list */ + 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 = []; + // First make sure we have all the nodes + foreach ($this->entityInsertions as $entity) { + $sort->addNode($entity); + } - foreach (array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions) as $entity) { + // Now add edges + foreach ($this->entityInsertions as $entity) { $class = $this->em->getClassMetadata(get_class($entity)); - if ($calc->hasNode($class->name)) { - continue; - } - - $calc->addNode($class->name, $class); - - $newNodes[] = $class; - } - - // Calculate dependencies for new nodes - while ($class = array_pop($newNodes)) { 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); + // 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; + } - $newNodes[] = $targetClass; + // 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']; - $calc->addDependency($targetClass->name, $class->name, (int) empty($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); + } + } - // If the target class has mapped subclasses, these share the same dependency. - if (! $targetClass->subClasses) { - continue; - } + return $sort->sort(); + } - foreach ($targetClass->subClasses as $subClassName) { - $targetSubClass = $this->em->getClassMetadata($subClassName); + /** @return list */ + private function computeDeleteExecutionOrder(): array + { + $sort = new TopologicalSort(); - if (! $calc->hasNode($subClassName)) { - $calc->addNode($targetSubClass->name, $targetSubClass); + // First make sure we have all the nodes + foreach ($this->entityDeletions as $entity) { + $sort->addNode($entity); + } - $newNodes[] = $targetSubClass; + // 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']); - $calc->addDependency($targetSubClass->name, $class->name, 1); + // 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 $calc->sort(); + return $sort->sort(); } /** @@ -1597,6 +1633,30 @@ public function addToIdentityMap($entity) $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; } @@ -2140,7 +2200,7 @@ private function ensureVersionMatch( $entity, $managedCopy ): void { - if (! ($class->isVersioned && $this->isLoaded($managedCopy) && $this->isLoaded($entity))) { + if (! ($class->isVersioned && ! $this->isUninitializedObject($managedCopy) && ! $this->isUninitializedObject($entity))) { return; } @@ -2158,16 +2218,6 @@ private function ensureVersionMatch( 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 * @@ -2463,7 +2513,7 @@ static function ($assoc) { */ 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; } @@ -2537,13 +2587,13 @@ static function ($assoc) { } ); + 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) { @@ -2599,9 +2649,7 @@ public function lock($entity, int $lockMode, $lockVersion = null): void return; } - if ($entity instanceof Proxy && ! $entity->__isInitialized()) { - $entity->__load(); - } + $this->initializeObject($entity); assert($class->versionField !== null); $entityVersion = $class->reflFields[$class->versionField]->getValue($entity); @@ -2791,7 +2839,6 @@ public function createEntity($className, array $data, &$hints = []) $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: @@ -2800,7 +2847,7 @@ public function createEntity($className, array $data, &$hints = []) } } - if ($entity instanceof Proxy && ! $entity->__isInitialized()) { + if ($this->isUninitializedObject($entity)) { $entity->__setInitialized(true); } else { if ( @@ -2817,25 +2864,20 @@ public function createEntity($className, array $data, &$hints = []) } $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); @@ -2951,8 +2993,7 @@ public function createEntity($className, array $data, &$hints = []) $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); } @@ -2993,20 +3034,7 @@ public function createEntity($className, array $data, &$hints = []) 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; } @@ -3363,7 +3391,7 @@ public function registerManaged($entity, array $id, array $data) $this->addToIdentityMap($entity); - if ($entity instanceof NotifyPropertyChanged && ( ! $entity instanceof Proxy || $entity->__isInitialized())) { + if ($entity instanceof NotifyPropertyChanged && ! $this->isUninitializedObject($entity)) { $entity->addPropertyChangedListener($this); } } @@ -3471,7 +3499,7 @@ public function getScheduledCollectionUpdates() */ public function initializeObject($obj) { - if ($obj instanceof Proxy) { + if ($obj instanceof InternalProxy) { $obj->__load(); return; @@ -3482,6 +3510,18 @@ public function initializeObject($obj) } } + /** + * 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. * @@ -3632,13 +3672,11 @@ private function assertThatThereAreNoUnintentionallyNonPersistedAssociations(): */ 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)); @@ -3659,7 +3697,7 @@ private function mergeEntityStateIntoManagedCopy($entity, $managedCopy): void 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; } @@ -3669,14 +3707,18 @@ private function mergeEntityStateIntoManagedCopy($entity, $managedCopy): void $targetClass = $this->em->getClassMetadata($assoc2['targetEntity']); $relatedId = $targetClass->getIdentifierValues($other); - if ($targetClass->subClasses) { - $other = $this->em->find($targetClass->name, $relatedId); - } else { - $other = $this->em->getProxyFactory()->getProxy( - $assoc2['targetEntity'], - $relatedId - ); - $this->registerManaged($other, $relatedId, []); + $other = $this->tryGetById($relatedId, $targetClass->name); + + if (! $other) { + if ($targetClass->subClasses) { + $other = $this->em->find($targetClass->name, $relatedId); + } else { + $other = $this->em->getProxyFactory()->getProxy( + $assoc2['targetEntity'], + $relatedId + ); + $this->registerManaged($other, $relatedId, []); + } } } @@ -3819,4 +3861,30 @@ private function normalizeIdentifier(ClassMetadata $targetClass, array $flatIden 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); + } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 42b40438d7d..87d9dc9837c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -286,7 +286,7 @@ parameters: path: lib/Doctrine/ORM/Proxy/ProxyFactory.php - - message: "#^Access to an undefined property Doctrine\\\\Persistence\\\\Proxy\\:\\:\\$__isCloning\\.$#" + message: "#^Call to an undefined method Doctrine\\\\Common\\\\Proxy\\\\Proxy\\:\\:__construct\\(\\)\\.$#" count: 1 path: lib/Doctrine/ORM/Proxy/ProxyFactory.php diff --git a/phpstan-persistence2.neon b/phpstan-persistence2.neon index 394498d9e89..41d9db7a30c 100644 --- a/phpstan-persistence2.neon +++ b/phpstan-persistence2.neon @@ -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\.$/' diff --git a/psalm-baseline.xml b/psalm-baseline.xml index b59efeac37b..2b887162e50 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + IterableResult @@ -213,11 +213,6 @@ CacheProvider - - - directory, self::LOCK_EXTENSION)]]> - - (int) $defaultLifetime @@ -1385,17 +1380,17 @@ $columnList - - - BaseProxy - - $classMetadata $classMetadata $classMetadata + + __construct(static function (InternalProxy $object) use ($initializer, $proxy): void { + $initializer($object, $proxy); + })]]> + getReflectionProperties()]]> getMetadataFactory()]]> @@ -1404,7 +1399,6 @@ isEmbeddedClass]]> isMappedSuperclass]]> - __isCloning]]> name]]> @@ -1415,6 +1409,7 @@ setAccessible + __construct __wakeup @@ -2012,19 +2007,10 @@ $factors[0] $primary $terms[0] - CollectionMemberExpression()]]> - ComparisonExpression()]]> - EmptyCollectionComparisonExpression()]]> - ExistsExpression()]]> - InExpression()]]> - InstanceOfExpression()]]> - LikeExpression()]]> - NullComparisonExpression()]]> AST\ArithmeticFactor AST\ArithmeticTerm - AST\BetweenExpression| AST\SimpleArithmeticExpression|AST\ArithmeticTerm @@ -2455,9 +2441,6 @@ - - - @@ -2810,7 +2793,6 @@ setValue - @@ -2819,10 +2801,6 @@ unwrap unwrap - - = 0 && $this->entityDeletions]]> - entityDeletions]]> - is_array($entity) diff --git a/psalm.xml b/psalm.xml index 262a6ed0aa6..9cb91ee7714 100644 --- a/psalm.xml +++ b/psalm.xml @@ -45,6 +45,10 @@ + + + + diff --git a/tests/Doctrine/Performance/LazyLoading/ProxyInitializationTimeBench.php b/tests/Doctrine/Performance/LazyLoading/ProxyInitializationTimeBench.php index 6504f8b151a..0befc9eb41e 100644 --- a/tests/Doctrine/Performance/LazyLoading/ProxyInitializationTimeBench.php +++ b/tests/Doctrine/Performance/LazyLoading/ProxyInitializationTimeBench.php @@ -4,9 +4,9 @@ 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; diff --git a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php index 8689dd5735f..7356a1bfe0b 100644 --- a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php @@ -8,17 +8,19 @@ 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 @@ public function testBasicOneToOne(): void // 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 @@ public function testInitializeCollectionWithNewObjectsRetainsNewObjects(): void 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); + $this->_em->flush(); + $this->_em->clear(); - $address = new CmsAddress(); - $address->country = 'Germany'; - $address->city = 'Berlin'; - $address->zip = '12345'; - $this->_em->persist($address); + // 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(); - $this->_em->clear(CmsAddress::class); - self::assertFalse($this->_em->contains($address)); - self::assertTrue($this->_em->contains($user)); + 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.articles a where u.username='gblanco'"); + $gblanco = $query->getSingleResult(); + + self::assertInstanceOf(CmsUser::class, $gblanco); + 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 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()); + // 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->setAddress($addressRef); // Ugh! Initializes address 'cause of $address->setUser($user)! + $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.address a where u.username='gblanco'"); + $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(CmsAddress::class, $gblanco->getAddress()); - self::assertEquals('Berlin', $gblanco->getAddress()->getCity()); + 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 @@ public function testQueryEntityByReference(): void ->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 @@ public function testManyToOneFetchModeQuery(): void ->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 @@ public function testWrongAssociationInstance(): void $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(); + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php b/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php index 520f8ddb081..489d9779e67 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php @@ -7,7 +7,6 @@ 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 @@ public function testLazyLoading2(): void $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 @@ public function testGetReferenceEntityWithSubclasses(): void $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 */ diff --git a/tests/Doctrine/Tests/ORM/Functional/DefaultValuesTest.php b/tests/Doctrine/Tests/ORM/Functional/DefaultValuesTest.php index 0c31b494823..313eaa0c30c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/DefaultValuesTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/DefaultValuesTest.php @@ -43,7 +43,7 @@ public function testSimpleDetachMerge(): void $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 @@ public function testSimpleDetachMerge(): void $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); diff --git a/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php b/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php index f1faf5b212f..1fef49dd952 100644 --- a/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php @@ -6,7 +6,6 @@ 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 @@ public function testUninitializedLazyAssociationsAreIgnoredOnMerge(): void $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 */ diff --git a/tests/Doctrine/Tests/ORM/Functional/EnumTest.php b/tests/Doctrine/Tests/ORM/Functional/EnumTest.php index f253266bc7a..3062c707ac6 100644 --- a/tests/Doctrine/Tests/ORM/Functional/EnumTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/EnumTest.php @@ -260,6 +260,78 @@ public function testEnumArrayInDtoHydration(): void self::assertEqualsCanonicalizing([Unit::Gram, Unit::Meter], $result[0]->supportedUnits); } + public function testEnumSingleEntityChangeSetsSimpleObjectHydrator(): void + { + $this->setUpEntitySchema([Card::class]); + + $card = new Card(); + $card->suit = Suit::Clubs; + + $this->_em->persist($card); + $this->_em->flush(); + $this->_em->clear(); + + $result = $this->_em->find(Card::class, $card->id); + + $this->_em->getUnitOfWork()->recomputeSingleEntityChangeSet( + $this->_em->getClassMetadata(Card::class), + $result + ); + + self::assertFalse($this->_em->getUnitOfWork()->isScheduledForUpdate($result)); + + $result->suit = Suit::Hearts; + + $this->_em->getUnitOfWork()->recomputeSingleEntityChangeSet( + $this->_em->getClassMetadata(Card::class), + $result + ); + + self::assertTrue($this->_em->getUnitOfWork()->isScheduledForUpdate($result)); + } + + public function testEnumSingleEntityChangeSetsObjectHydrator(): void + { + $this->setUpEntitySchema([Card::class]); + + $card = new Card(); + $card->suit = Suit::Clubs; + + $this->_em->persist($card); + $this->_em->flush(); + $this->_em->clear(); + + $result = $this->_em->find(Card::class, $card->id); + + $this->_em->getUnitOfWork()->recomputeSingleEntityChangeSet( + $this->_em->getClassMetadata(Card::class), + $result + ); + + self::assertFalse($this->_em->getUnitOfWork()->isScheduledForUpdate($result)); + } + + public function testEnumArraySingleEntityChangeSets(): void + { + $this->setUpEntitySchema([Scale::class]); + + $scale = new Scale(); + $scale->supportedUnits = [Unit::Gram]; + + $this->_em->persist($scale); + $this->_em->flush(); + $this->_em->clear(); + + $result = $this->_em->find(Scale::class, $scale->id); + + $this->_em->getUnitOfWork()->recomputeSingleEntityChangeSet( + $this->_em->getClassMetadata(Scale::class), + $result + ); + + self::assertFalse($this->_em->getUnitOfWork()->isScheduledForUpdate($result)); + } + public function testEnumChangeSetsSimpleObjectHydrator(): void { $this->setUpEntitySchema([Card::class]); diff --git a/tests/Doctrine/Tests/ORM/Functional/GH7877Test.php b/tests/Doctrine/Tests/ORM/Functional/GH7877Test.php new file mode 100644 index 00000000000..6461c686527 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/GH7877Test.php @@ -0,0 +1,135 @@ +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(); + } +} diff --git a/tests/Doctrine/Tests/ORM/Functional/ManyToManyBidirectionalAssociationTest.php b/tests/Doctrine/Tests/ORM/Functional/ManyToManyBidirectionalAssociationTest.php index 52bb08e47e3..2b5dcb953c4 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ManyToManyBidirectionalAssociationTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ManyToManyBidirectionalAssociationTest.php @@ -109,8 +109,7 @@ private function createLoadingFixture(): void /** @psalm-return list */ protected function findProducts(): array { - $query = $this->_em->createQuery('SELECT p, c FROM Doctrine\Tests\Models\ECommerce\ECommerceProduct p LEFT JOIN p.categories c ORDER BY p.id, c.id'); - //$query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true); + $query = $this->_em->createQuery('SELECT p, c FROM Doctrine\Tests\Models\ECommerce\ECommerceProduct p LEFT JOIN p.categories c ORDER BY p.id, c.id'); $result = $query->getResult(); self::assertCount(2, $result); $cats1 = $result[0]->getCategories(); @@ -126,8 +125,7 @@ protected function findProducts(): array /** @psalm-return list */ protected function findCategories(): array { - $query = $this->_em->createQuery('SELECT c, p FROM Doctrine\Tests\Models\ECommerce\ECommerceCategory c LEFT JOIN c.products p ORDER BY c.id, p.id'); - //$query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true); + $query = $this->_em->createQuery('SELECT c, p FROM Doctrine\Tests\Models\ECommerce\ECommerceCategory c LEFT JOIN c.products p ORDER BY c.id, p.id'); $result = $query->getResult(); self::assertCount(2, $result); self::assertInstanceOf(ECommerceCategory::class, $result[0]); diff --git a/tests/Doctrine/Tests/ORM/Functional/MappedSuperclassTest.php b/tests/Doctrine/Tests/ORM/Functional/MappedSuperclassTest.php index 7163931f30c..03c14f2e03f 100644 --- a/tests/Doctrine/Tests/ORM/Functional/MappedSuperclassTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/MappedSuperclassTest.php @@ -4,7 +4,6 @@ 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 @@ public function testCRUD(): void $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()); diff --git a/tests/Doctrine/Tests/ORM/Functional/MergeProxiesTest.php b/tests/Doctrine/Tests/ORM/Functional/MergeProxiesTest.php index 49e6425a5f3..312c5f975ca 100644 --- a/tests/Doctrine/Tests/ORM/Functional/MergeProxiesTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/MergeProxiesTest.php @@ -11,7 +11,6 @@ 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 @@ public function testMergeDetachedUnInitializedProxy(): void 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 @@ public function testMergeUnserializedUnInitializedProxy(): void $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 @@ public function testMergeManagedProxy(): void self::assertSame($managed, $this->_em->merge($managed)); - self::assertFalse($managed->__isInitialized()); + self::assertTrue($this->isUninitializedObject($managed)); } /** @@ -109,13 +108,12 @@ public function testMergeWithExistingUninitializedManagedProxy(): void $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 @@ public function testMergingProxyFromDifferentEntityManagerWithExistingManagedIns 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 @@ public function testMergingUnInitializedProxyDoesNotInitializeIt(): void $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, diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToManyBidirectionalAssociationTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToManyBidirectionalAssociationTest.php index 5b6db0f93c6..1f8695cd432 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToManyBidirectionalAssociationTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToManyBidirectionalAssociationTest.php @@ -6,7 +6,6 @@ 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 @@ public function testEagerLoadsOneToManyAssociation(): void $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 @@ public function testLazyLoadsObjectsOnTheInverseSide(): void $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 @@ public function testLazyLoadsObjectsOnTheInverseSide2(): void $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()); diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToOneBidirectionalAssociationTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToOneBidirectionalAssociationTest.php index fb7199fe4eb..b0896734d53 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToOneBidirectionalAssociationTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToOneBidirectionalAssociationTest.php @@ -5,7 +5,6 @@ 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 @@ public function testInverseSideIsNeverLazy(): void 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()); } diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php index 9e6c9e1df1e..94a68c19f32 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php @@ -14,7 +14,6 @@ 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 @@ public function testEagerLoadOneToOneOwningSide(): void $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 @@ public function testEagerLoadOneToOneNullOwningSide(): void $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 @@ public function testEagerLoadOneToOneInverseSide(): void $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 @@ public function testEagerLoadOneToOneNullInverseSide(): void $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 @@ public function testEagerLoadManyToOne(): void $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 */ diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToOneSelfReferentialAssociationTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToOneSelfReferentialAssociationTest.php index 6c211aa603c..93b89acd242 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToOneSelfReferentialAssociationTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToOneSelfReferentialAssociationTest.php @@ -11,7 +11,6 @@ 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 @@ public function testFind(): void $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); diff --git a/tests/Doctrine/Tests/ORM/Functional/ParserResultSerializationTest.php b/tests/Doctrine/Tests/ORM/Functional/ParserResultSerializationTest.php index f5718715e2a..a9d73fc7c98 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ParserResultSerializationTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ParserResultSerializationTest.php @@ -13,6 +13,7 @@ use ReflectionMethod; use function file_get_contents; +use function rtrim; use function serialize; use function unserialize; @@ -56,8 +57,8 @@ public function testUnserializeSingleSelectResult(string $serialized): void /** @return Generator */ public static function provideSerializedSingleSelectResults(): Generator { - yield '2.14.3' => [file_get_contents(__DIR__ . '/ParserResults/single_select_2_14_3.txt')]; - yield '2.15.0' => [file_get_contents(__DIR__ . '/ParserResults/single_select_2_15_0.txt')]; + yield '2.14.3' => [rtrim(file_get_contents(__DIR__ . '/ParserResults/single_select_2_14_3.txt'), "\n")]; + yield '2.15.0' => [rtrim(file_get_contents(__DIR__ . '/ParserResults/single_select_2_15_0.txt'), "\n")]; } private static function parseQuery(Query $query): ParserResult diff --git a/tests/Doctrine/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php b/tests/Doctrine/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php index c15306efc3a..01f82c8de7d 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php @@ -59,7 +59,7 @@ public function testPersistUpdate(): void { // 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 @@ public function testEntityWithIdentifier(): void $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 @@ public function testEntityWithIdentifier(): void */ 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 diff --git a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php index 894ab729dbc..791c5c31719 100644 --- a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php @@ -13,7 +13,6 @@ 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 @@ public function testEntityParameters(): void 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 @@ public function testEnableFetchEagerMode(): void self::assertCount(10, $articles); foreach ($articles as $article) { - self::assertNotInstanceOf(Proxy::class, $article); + self::assertFalse($this->isUninitializedObject($article)); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php b/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php index 1303099f9be..c0b147dfc56 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php @@ -6,7 +6,7 @@ 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 @@ public function testInitializeProxy(): void $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 @@ public function testDoNotInitializeProxyOnGettingTheIdentifier(): void $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 @@ public function testDoNotInitializeProxyOnGettingTheIdentifierDDC1625(): void $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 @@ public function testDoNotInitializeProxyOnGettingTheIdentifierAndReturnTheRightT $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 @@ public function testInitializeProxyOnGettingSomethingOtherThanTheIdentifier(): v $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 @@ public function testCommonPersistenceProxy(): void 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 @@ public function testCommonPersistenceProxy(): void self::assertTrue(file_exists($proxyFileName), 'Proxy file name cannot be found generically.'); $entity->__load(); - self::assertTrue($entity->__isInitialized()); + self::assertFalse($this->isUninitializedObject($entity)); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php index 32cac827f08..984cb3e3929 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php @@ -10,9 +10,9 @@ 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 @@ public function testResolveAssociationCacheEntry(): void 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 @@ public function testResolveAssociationCacheEntry(): void 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 @@ public function testResolveToManyAssociationCacheEntry(): void $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 @@ public function testResolveToManyAssociationCacheEntry(): void $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()); diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php index 6a1b5c142da..5ac7ab8242f 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php @@ -4,7 +4,7 @@ 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 @@ public function testRepositoryCacheFindAllToOneAssociation(): void 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 @@ public function testRepositoryCacheFindAllToOneAssociation(): void 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 @@ public function testRepositoryCacheFindAllToOneAssociation(): void 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 @@ public function testRepositoryCacheFindAllToOneAssociation(): void 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()); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php b/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php index 953c93e78ac..935b4dd6e43 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php @@ -7,7 +7,6 @@ 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 @@ public function testGetReferenceEntityWithSubclasses(): void $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 @@ public function testEagerLoadInheritanceHierarchy(): void ->setParameter(1, $this->fix->getId()) ->getSingleResult(); - self::assertNotInstanceOf(Proxy::class, $contract->getSalesPerson()); + self::assertFalse($this->isUninitializedObject($contract->getSalesPerson())); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1163Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1163Test.php index 1d1581011fd..dbff2863d4e 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1163Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1163Test.php @@ -15,7 +15,6 @@ 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 @@ private function setPropertyAndAssignTagToSpecialProduct(): void assert($specialProduct instanceof DDC1163SpecialProduct); self::assertInstanceOf(DDC1163SpecialProduct::class, $specialProduct); - self::assertInstanceOf(Proxy::class, $specialProduct); + self::assertTrue($this->isUninitializedObject($specialProduct)); $specialProduct->setSubclassProperty('foobar'); diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1193Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1193Test.php index fcfb7e45e34..cc7967710c4 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1193Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1193Test.php @@ -45,7 +45,7 @@ public function testIssue(): void $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); diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1228Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1228Test.php index f8eae7d19f5..d5a95261c12 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1228Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1228Test.php @@ -38,9 +38,9 @@ public function testOneToOnePersist(): void $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())); diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1238Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1238Test.php index 082b40f1967..bdb1d70231a 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1238Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1238Test.php @@ -56,7 +56,7 @@ public function testIssueProxyClear(): void $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'); diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1452Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1452Test.php index a95f9ea22b5..2cd3d1924bf 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1452Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1452Test.php @@ -12,7 +12,6 @@ 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 @@ public function testIssue(): void $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 @@ public function testFetchJoinOneToOneFromInverse(): void $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)); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1690Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1690Test.php index 20af53c3abe..c72081cbc61 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1690Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1690Test.php @@ -12,7 +12,6 @@ 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 @@ public function testChangeTracking(): void $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); diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1734Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1734Test.php index 86bc0adbdf2..35fbffdea88 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1734Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1734Test.php @@ -4,7 +4,7 @@ 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 @@ public function testMergeWorksOnNonSerializedProxies(): void $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 @@ public function testMergeWorksOnSerializedProxies(): void $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 @@ public function testMergeWorksOnSerializedProxies(): void } /** @param object $object */ - private function getProxy($object): Proxy + private function getProxy($object): InternalProxy { $metadataFactory = $this->_em->getMetadataFactory(); $className = get_class($object); diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2230Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2230Test.php index 93c14149a08..8557858ac92 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2230Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2230Test.php @@ -12,7 +12,6 @@ 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 @@ public function testNotifyTrackingNotCalledOnUninitializedProxies(): void $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 @@ public function testNotifyTrackingCalledOnProxyInitialization(): void $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); } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2231Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2231Test.php index 8ef946fabd2..1f8a3c604e7 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2231Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2231Test.php @@ -12,7 +12,6 @@ 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 @@ public function testInjectObjectManagerInProxyIfInitializedInUow(): void $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); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2306Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2306Test.php index 60acc5a59dc..0d251fe6ffb 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2306Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2306Test.php @@ -12,7 +12,6 @@ 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 @@ public function testIssue(): void $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, diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC237Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC237Test.php index 1899cbed5ca..ed5e7bff1c0 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC237Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC237Test.php @@ -11,7 +11,6 @@ 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 @@ public function testUninitializedProxyIsInitializedOnFetchJoin(): void $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 @@ public function testUninitializedProxyIsInitializedOnFetchJoin(): void self::assertNotSame($x, $x2); self::assertNotSame($z, $z2); self::assertSame($z2->y, $x2->y); - self::assertInstanceOf(Proxy::class, $z2->y); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2494Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2494Test.php index 6cf49a70670..b785a58a463 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2494Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2494Test.php @@ -15,7 +15,6 @@ 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 @@ public function testIssue(): void $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); } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2519Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2519Test.php index ee8bf3cfa08..0fa9401c728 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2519Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2519Test.php @@ -4,7 +4,6 @@ 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 @@ public function testIssue(): void 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()); diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC371Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC371Test.php index 828a7adcc2f..89a42dfbce7 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC371Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC371Test.php @@ -14,7 +14,6 @@ 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 @@ public function testIssue(): void ->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()); } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC522Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC522Test.php index 0616f96ce1c..7d9bdbba644 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC522Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC522Test.php @@ -10,7 +10,6 @@ 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 @@ public function testJoinColumnWithSameNameAsAssociationField(): void 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 @@ public function testJoinColumnWithSameNameAsAssociationField(): void $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)); } /** diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC531Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC531Test.php index e4981161331..5c9a4bfba92 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC531Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC531Test.php @@ -16,7 +16,6 @@ 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 @@ public function testIssue(): void // 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()); diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php index 827021926ab..a6d77040074 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php @@ -9,7 +9,6 @@ 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 @@ public function testOneToOneEager(): void $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 @@ public function testDQLDeferredEagerLoad(): void $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!'); } } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC6460Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC6460Test.php index 379e6f090e9..85680311845 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC6460Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC6460Test.php @@ -12,7 +12,6 @@ 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 @@ public function testInlineEmbeddableProxyInitialization(): void $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)); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC736Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC736Test.php index 02d0b69681a..b081651d66d 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC736Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC736Test.php @@ -8,7 +8,6 @@ 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 @@ public function testReorderEntityFetchJoinForHydration(): void 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 @@ public function testDqlTreeWalkerReordering(): void $cart2 = $result[0][0]; assert($cart2 instanceof ECommerceCart); - self::assertInstanceOf(Proxy::class, $cart2->getCustomer()); + self::assertTrue($this->isUninitializedObject($cart2->getCustomer())); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC881Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC881Test.php index 0d3c2a7592e..138c3c0914c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC881Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC881Test.php @@ -15,7 +15,6 @@ 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 @@ public function testIssue(): void $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(); diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10348Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10348Test.php new file mode 100644 index 00000000000..1e4f0c47c94 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10348Test.php @@ -0,0 +1,108 @@ +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; + } +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10531Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10531Test.php new file mode 100644 index 00000000000..2b0f4fa5c19 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10531Test.php @@ -0,0 +1,151 @@ +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; +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10532Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10532Test.php new file mode 100644 index 00000000000..b3762b068e4 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10532Test.php @@ -0,0 +1,186 @@ +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; +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10566Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10566Test.php new file mode 100644 index 00000000000..91b6174a560 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10566Test.php @@ -0,0 +1,177 @@ +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; +} diff --git a/tests/Doctrine/Tests/ORM/Functional/GH10747Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10747Test.php similarity index 96% rename from tests/Doctrine/Tests/ORM/Functional/GH10747Test.php rename to tests/Doctrine/Tests/ORM/Functional/Ticket/GH10747Test.php index 019449a4164..198341af789 100644 --- a/tests/Doctrine/Tests/ORM/Functional/GH10747Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10747Test.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Doctrine\Tests\ORM\Functional; +namespace Doctrine\Tests\ORM\Functional\Ticket; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -93,7 +93,7 @@ class GH10747Article { /** * @Id - * @Column(type="Doctrine\Tests\ORM\Functional\GH10747CustomIdObjectHashType") + * @Column(type="Doctrine\Tests\ORM\Functional\Ticket\GH10747CustomIdObjectHashType") * @var CustomIdObject */ public $id; diff --git a/tests/Doctrine/Tests/ORM/Functional/GH10752Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10752Test.php similarity index 98% rename from tests/Doctrine/Tests/ORM/Functional/GH10752Test.php rename to tests/Doctrine/Tests/ORM/Functional/Ticket/GH10752Test.php index c90d1995001..afeefa4121b 100644 --- a/tests/Doctrine/Tests/ORM/Functional/GH10752Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10752Test.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Doctrine\Tests\ORM\Functional; +namespace Doctrine\Tests\ORM\Functional\Ticket; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH5742Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH5742Test.php new file mode 100644 index 00000000000..ba62f2531bc --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH5742Test.php @@ -0,0 +1,158 @@ +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 + */ + 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; +} diff --git a/tests/Doctrine/Tests/ORM/Functional/GH5988Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH5988Test.php similarity index 95% rename from tests/Doctrine/Tests/ORM/Functional/GH5988Test.php rename to tests/Doctrine/Tests/ORM/Functional/Ticket/GH5988Test.php index bf47d315b42..4e731b54353 100644 --- a/tests/Doctrine/Tests/ORM/Functional/GH5988Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH5988Test.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Doctrine\Tests\ORM\Functional; +namespace Doctrine\Tests\ORM\Functional\Ticket; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\Type as DBALType; @@ -106,7 +106,7 @@ abstract class GH5988CustomIdObjectTypeParent { /** * @Id - * @Column(type="Doctrine\Tests\ORM\Functional\GH5988CustomIdObjectHashType", length=255) + * @Column(type="Doctrine\Tests\ORM\Functional\Ticket\GH5988CustomIdObjectHashType", length=255) * @var CustomIdObject */ public $id; diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH6362Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH6362Test.php index f756ce5e5db..b185a4b430b 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH6362Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH6362Test.php @@ -15,7 +15,6 @@ use Doctrine\ORM\Mapping\InheritanceType; use Doctrine\ORM\Mapping\ManyToOne; use Doctrine\ORM\Mapping\OneToMany; -use Doctrine\ORM\Query; use Doctrine\ORM\Query\ResultSetMapping; use Doctrine\Tests\Mocks\ArrayResultFactory; use Doctrine\Tests\OrmFunctionalTestCase; @@ -79,7 +78,7 @@ public function testInheritanceJoinAlias(): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->_em); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertInstanceOf(GH6362Start::class, $result[0]['base']); self::assertInstanceOf(GH6362Child::class, $result[1][0]); diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH6499OneToManyRelationshipTest.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH6499OneToManyRelationshipTest.php new file mode 100644 index 00000000000..f638e79f013 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH6499OneToManyRelationshipTest.php @@ -0,0 +1,155 @@ +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; + } +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH6499OneToOneRelationshipTest.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH6499OneToOneRelationshipTest.php new file mode 100644 index 00000000000..2922c674733 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH6499OneToOneRelationshipTest.php @@ -0,0 +1,78 @@ +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; +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH6499Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH6499Test.php new file mode 100644 index 00000000000..a6672801ca4 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH6499Test.php @@ -0,0 +1,103 @@ +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; +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7006Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7006Test.php new file mode 100644 index 00000000000..e4be3ed83b8 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7006Test.php @@ -0,0 +1,135 @@ +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 + */ + 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); + } +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7180Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7180Test.php new file mode 100644 index 00000000000..46431647f75 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7180Test.php @@ -0,0 +1,223 @@ +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; +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7407Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7407Test.php new file mode 100644 index 00000000000..c12d3da3279 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7407Test.php @@ -0,0 +1,88 @@ +useModelSet('cms'); + + parent::setUp(); + } + + public function testMergingEntitiesDoesNotCreateUnmanagedProxyReferences(): void + { + // 1. Create an article with a user; persist, flush and clear the entity manager + $user = new CmsUser(); + $user->username = 'Test'; + $user->name = 'Test'; + $this->_em->persist($user); + + $article = new CmsArticle(); + $article->topic = 'Test'; + $article->text = 'Test'; + $article->setAuthor($user); + $this->_em->persist($article); + + $this->_em->flush(); + $this->_em->clear(); + + // 2. Merge the user object back in: + // We get a new (different) entity object that represents the user instance + // which is now (through this object instance) managed by the EM/UoW + $mergedUser = $this->_em->merge($user); + $mergedUserOid = spl_object_id($mergedUser); + + // 3. Merge the article object back in, + // the returned entity object is the article instance as it is managed by the EM/UoW + $mergedArticle = $this->_em->merge($article); + $mergedArticleOid = spl_object_id($mergedArticle); + + self::assertSame($mergedUser, $mergedArticle->user, 'The $mergedArticle\'s #user property should hold the $mergedUser we obtained previously, since that\'s the only legitimate object instance representing the user from the UoW\'s point of view.'); + + // Inspect internal UoW state + $uow = $this->_em->getUnitOfWork(); + $entityIdentifiers = $this->grabProperty('entityIdentifiers', $uow); + $identityMap = $this->grabProperty('identityMap', $uow); + $entityStates = $this->grabProperty('entityStates', $uow); + + self::assertCount(2, $entityIdentifiers, 'UoW#entityIdentifiers contains exactly two OID -> ID value mapping entries one for the article, one for the user object'); + self::assertArrayHasKey($mergedArticleOid, $entityIdentifiers); + self::assertArrayHasKey($mergedUserOid, $entityIdentifiers); + + self::assertSame([ + $mergedUserOid => UnitOfWork::STATE_MANAGED, + $mergedArticleOid => UnitOfWork::STATE_MANAGED, + ], $entityStates, 'UoW#entityStates contains two OID -> state entries, one for the article, one for the user object'); + + self::assertCount(2, $entityIdentifiers); + self::assertArrayHasKey($mergedArticleOid, $entityIdentifiers); + self::assertArrayHasKey($mergedUserOid, $entityIdentifiers); + + self::assertSame([ + CmsUser::class => [$user->id => $mergedUser], + CmsArticle::class => [$article->id => $mergedArticle], + ], $identityMap, 'The identity map contains exactly two objects, the article and the user.'); + } + + /** @return mixed */ + private function grabProperty(string $name, UnitOfWork $uow) + { + $reflection = new ReflectionClass($uow); + $property = $reflection->getProperty($name); + $property->setAccessible(true); + + return $property->getValue($uow); + } +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7869Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7869Test.php index 11a5eec69e2..534bb6c1c37 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7869Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7869Test.php @@ -55,7 +55,7 @@ public function getClassMetadata($className): ClassMetadata $uow->clear(); $uow->triggerEagerLoads(); - self::assertSame(2, $em->getClassMetadataCalls); + self::assertSame(4, $em->getClassMetadataCalls); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH9192Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH9192Test.php new file mode 100644 index 00000000000..216c4f2dff9 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH9192Test.php @@ -0,0 +1,136 @@ +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 + */ + 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 + */ + 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; +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH9467/GH9467Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH9467/GH9467Test.php new file mode 100644 index 00000000000..01a6acae51e --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH9467/GH9467Test.php @@ -0,0 +1,261 @@ +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); + } +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH9467/JoinedInheritanceChild.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH9467/JoinedInheritanceChild.php new file mode 100644 index 00000000000..0b30c6972a7 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH9467/JoinedInheritanceChild.php @@ -0,0 +1,18 @@ + 'dbDefault'], generated: 'ALWAYS')] + public $nonInsertableContent; +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH9467/JoinedInheritanceNonUpdatableColumn.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH9467/JoinedInheritanceNonUpdatableColumn.php new file mode 100644 index 00000000000..c8b1c671f6e --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH9467/JoinedInheritanceNonUpdatableColumn.php @@ -0,0 +1,25 @@ + 'dbDefault'], generated: 'ALWAYS')] + public $nonUpdatableContent; +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH9467/JoinedInheritanceNonWritableColumn.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH9467/JoinedInheritanceNonWritableColumn.php new file mode 100644 index 00000000000..2f78f6b5e5f --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH9467/JoinedInheritanceNonWritableColumn.php @@ -0,0 +1,25 @@ + 'dbDefault'], generated: 'ALWAYS')] + public $nonWritableContent; +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH9467/JoinedInheritanceRoot.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH9467/JoinedInheritanceRoot.php new file mode 100644 index 00000000000..ba7f39b7f6e --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH9467/JoinedInheritanceRoot.php @@ -0,0 +1,81 @@ + JoinedInheritanceChild::class, 'writable' => JoinedInheritanceWritableColumn::class, 'nonWritable' => JoinedInheritanceNonWritableColumn::class, 'nonInsertable' => JoinedInheritanceNonInsertableColumn::class, 'nonUpdatable' => JoinedInheritanceNonUpdatableColumn::class])] +class JoinedInheritanceRoot +{ + /** + * @var int + * @Id + * @GeneratedValue + * @Column(type="integer") + */ + #[Id] + #[GeneratedValue] + #[Column(type: 'integer')] + public $id; + + /** + * @var string + * @Column(type="string") + */ + #[Column(type: 'string')] + public $rootField = ''; + + /** + * @var string + * @Column(type="string", insertable=true, updatable=true, options={"default": "dbDefault"}, generated="ALWAYS") + */ + #[Column(type: 'string', insertable: true, updatable: true, options: ['default' => 'dbDefault'], generated: 'ALWAYS')] + public $rootWritableContent = ''; + + /** + * @var string + * @Column(type="string", insertable=false, updatable=false, options={"default": "dbDefault"}, generated="ALWAYS") + */ + #[Column(type: 'string', insertable: false, updatable: false, options: ['default' => 'dbDefault'], generated: 'ALWAYS')] + public $rootNonWritableContent; + + /** + * @var string + * @Column(type="string", insertable=false, updatable=true, options={"default": "dbDefault"}, generated="ALWAYS") + */ + #[Column(type: 'string', insertable: false, updatable: true, options: ['default' => 'dbDefault'], generated: 'ALWAYS')] + public $rootNonInsertableContent; + + /** + * @var string + * @Column(type="string", insertable=true, updatable=false, options={"default": "dbDefault"}, generated="ALWAYS") + */ + #[Column(type: 'string', insertable: true, updatable: false, options: ['default' => 'dbDefault'], generated: 'ALWAYS')] + public $rootNonUpdatableContent = ''; +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH9467/JoinedInheritanceWritableColumn.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH9467/JoinedInheritanceWritableColumn.php new file mode 100644 index 00000000000..874df49e6b6 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH9467/JoinedInheritanceWritableColumn.php @@ -0,0 +1,25 @@ + 'dbDefault'], generated: 'ALWAYS')] + public $writableContent; +} diff --git a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php index 8352ddc46bd..37c36d18a8e 100644 --- a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php +++ b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php @@ -8,7 +8,6 @@ use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\PersistentCollection; use Doctrine\ORM\Proxy\ProxyFactory; -use Doctrine\ORM\Query; use Doctrine\ORM\Query\ResultSetMapping; use Doctrine\Tests\Mocks\ArrayResultFactory; use Doctrine\Tests\Models\CMS\CmsAddress; @@ -65,7 +64,7 @@ public static function provideDataForProductEntityResult(): array } /** - * SELECT PARTIAL u.{id,name} + * SELECT u * FROM Doctrine\Tests\Models\CMS\CmsUser u */ public function testSimpleEntityQuery(): void @@ -89,7 +88,7 @@ public function testSimpleEntityQuery(): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(2, count($result)); @@ -104,7 +103,7 @@ public function testSimpleEntityQuery(): void } /** - * SELECT PARTIAL u.{id,name} AS user + * SELECT u AS user * FROM Doctrine\Tests\Models\CMS\CmsUser u */ public function testSimpleEntityQueryWithAliasedUserEntity(): void @@ -128,7 +127,7 @@ public function testSimpleEntityQueryWithAliasedUserEntity(): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(2, count($result)); @@ -146,7 +145,7 @@ public function testSimpleEntityQueryWithAliasedUserEntity(): void } /** - * SELECT PARTIAL u.{id, name}, PARTIAL a.{id, topic} + * SELECT u, a * FROM Doctrine\Tests\Models\CMS\CmsUser u, Doctrine\Tests\Models\CMS\CmsArticle a */ public function testSimpleMultipleRootEntityQuery(): void @@ -177,7 +176,7 @@ public function testSimpleMultipleRootEntityQuery(): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(4, count($result)); @@ -200,7 +199,7 @@ public function testSimpleMultipleRootEntityQuery(): void } /** - * SELECT PARTIAL u.{id, name} AS user, PARTIAL a.{id, topic} + * SELECT u AS user, a * FROM Doctrine\Tests\Models\CMS\CmsUser u, Doctrine\Tests\Models\CMS\CmsArticle a */ public function testSimpleMultipleRootEntityQueryWithAliasedUserEntity(): void @@ -231,7 +230,7 @@ public function testSimpleMultipleRootEntityQueryWithAliasedUserEntity(): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(4, count($result)); @@ -261,7 +260,7 @@ public function testSimpleMultipleRootEntityQueryWithAliasedUserEntity(): void } /** - * SELECT PARTIAL u.{id, name}, PARTIAL a.{id, topic} AS article + * SELECT u, a AS article * FROM Doctrine\Tests\Models\CMS\CmsUser u, Doctrine\Tests\Models\CMS\CmsArticle a */ public function testSimpleMultipleRootEntityQueryWithAliasedArticleEntity(): void @@ -292,7 +291,7 @@ public function testSimpleMultipleRootEntityQueryWithAliasedArticleEntity(): voi $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(4, count($result)); @@ -322,7 +321,7 @@ public function testSimpleMultipleRootEntityQueryWithAliasedArticleEntity(): voi } /** - * SELECT PARTIAL u.{id, name} AS user, PARTIAL a.{id, topic} AS article + * SELECT u AS user, a AS article * FROM Doctrine\Tests\Models\CMS\CmsUser u, Doctrine\Tests\Models\CMS\CmsArticle a */ public function testSimpleMultipleRootEntityQueryWithAliasedEntities(): void @@ -353,7 +352,7 @@ public function testSimpleMultipleRootEntityQueryWithAliasedEntities(): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(4, count($result)); @@ -383,7 +382,7 @@ public function testSimpleMultipleRootEntityQueryWithAliasedEntities(): void } /** - * SELECT PARTIAL u.{id, status}, COUNT(p.phonenumber) numPhones + * SELECT u, COUNT(p.phonenumber) numPhones * FROM User u * JOIN u.phonenumbers p * GROUP BY u.id @@ -415,7 +414,7 @@ public function testMixedQueryNormalJoin($userEntityKey): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(2, count($result)); @@ -433,7 +432,7 @@ public function testMixedQueryNormalJoin($userEntityKey): void } /** - * SELECT PARTIAL u.{id, status}, PARTIAL p.{phonenumber}, UPPER(u.name) nameUpper + * SELECT u, p, UPPER(u.name) nameUpper * FROM Doctrine\Tests\Models\CMS\CmsUser u * JOIN u.phonenumbers p * @@ -479,7 +478,7 @@ public function testMixedQueryFetchJoin($userEntityKey): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(2, count($result)); @@ -559,7 +558,7 @@ public function testMixedQueryFetchJoinCustomIndex($userEntityKey): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(2, count($result)); @@ -673,7 +672,7 @@ public function testMixedQueryMultipleFetchJoin($userEntityKey): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(2, count($result)); @@ -803,7 +802,7 @@ public function testMixedQueryMultipleDeepMixedFetchJoin($userEntityKey): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(2, count($result)); @@ -916,7 +915,7 @@ public function testEntityQueryCustomResultSetOrder(): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(2, count($result)); @@ -936,7 +935,7 @@ public function testEntityQueryCustomResultSetOrder(): void } /** - * SELECT PARTIAL u.{id,name} + * SELECT u * FROM Doctrine\Tests\Models\CMS\CmsUser u * * @group DDC-644 @@ -959,7 +958,7 @@ public function testSkipUnknownColumns(): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(1, count($result)); self::assertInstanceOf(CmsUser::class, $result[0]); @@ -992,7 +991,7 @@ public function testScalarQueryWithoutResultVariables($userEntityKey): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(2, count($result)); @@ -1104,7 +1103,7 @@ public function testCreatesProxyForLazyLoadingWithForeignKeysWithAliasedProductE } /** - * SELECT PARTIAL u.{id, status}, PARTIAL a.{id, topic}, PARTIAL c.{id, topic} + * SELECT u, a, c * FROM Doctrine\Tests\Models\CMS\CmsUser u * LEFT JOIN u.articles a * LEFT JOIN a.comments c @@ -1155,7 +1154,7 @@ public function testChainedJoinWithEmptyCollections(): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(2, count($result)); @@ -1167,7 +1166,7 @@ public function testChainedJoinWithEmptyCollections(): void } /** - * SELECT PARTIAL u.{id, status} AS user, PARTIAL a.{id, topic}, PARTIAL c.{id, topic} + * SELECT u AS user, a, c * FROM Doctrine\Tests\Models\CMS\CmsUser u * LEFT JOIN u.articles a * LEFT JOIN a.comments c @@ -1218,7 +1217,7 @@ public function testChainedJoinWithEmptyCollectionsWithAliasedUserEntity(): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(2, count($result)); @@ -1233,7 +1232,7 @@ public function testChainedJoinWithEmptyCollectionsWithAliasedUserEntity(): void } /** - * SELECT PARTIAL u.{id, name} + * SELECT u * FROM Doctrine\Tests\Models\CMS\CmsUser u */ public function testResultIteration(): void @@ -1259,8 +1258,7 @@ public function testResultIteration(): void $iterableResult = $hydrator->iterate( ArrayResultFactory::createFromArray($resultSet), - $rsm, - [Query::HINT_FORCE_PARTIAL_LOAD => true] + $rsm ); $rowNum = 0; @@ -1283,8 +1281,7 @@ public function testResultIteration(): void $iterableResult = $hydrator->toIterable( ArrayResultFactory::createFromArray($resultSet), - $rsm, - [Query::HINT_FORCE_PARTIAL_LOAD => true] + $rsm ); $rowNum = 0; @@ -1308,7 +1305,7 @@ public function testResultIteration(): void } /** - * SELECT PARTIAL u.{id, name} + * SELECT u * FROM Doctrine\Tests\Models\CMS\CmsUser u */ public function testResultIterationWithAliasedUserEntity(): void @@ -1334,8 +1331,7 @@ public function testResultIterationWithAliasedUserEntity(): void $rowNum = 0; $iterableResult = $hydrator->iterate( ArrayResultFactory::createFromArray($resultSet), - $rsm, - [Query::HINT_FORCE_PARTIAL_LOAD => true] + $rsm ); while (($row = $iterableResult->next()) !== false) { @@ -1360,8 +1356,7 @@ public function testResultIterationWithAliasedUserEntity(): void $rowNum = 0; $iterableResult = $hydrator->toIterable( ArrayResultFactory::createFromArray($resultSet), - $rsm, - [Query::HINT_FORCE_PARTIAL_LOAD => true] + $rsm ); foreach ($iterableResult as $row) { @@ -1388,7 +1383,7 @@ public function testResultIterationWithAliasedUserEntity(): void /** * Checks if multiple joined multiple-valued collections is hydrated correctly. * - * SELECT PARTIAL u.{id, status}, PARTIAL g.{id, name}, PARTIAL p.{phonenumber} + * SELECT u, g, p * FROM Doctrine\Tests\Models\CMS\CmsUser u * * @group DDC-809 @@ -1495,7 +1490,7 @@ public function testManyToManyHydration(): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(2, count($result)); @@ -1511,7 +1506,7 @@ public function testManyToManyHydration(): void /** * Checks if multiple joined multiple-valued collections is hydrated correctly. * - * SELECT PARTIAL u.{id, status} As user, PARTIAL g.{id, name}, PARTIAL p.{phonenumber} + * SELECT u As user, g, p * FROM Doctrine\Tests\Models\CMS\CmsUser u * * @group DDC-809 @@ -1618,7 +1613,7 @@ public function testManyToManyHydrationWithAliasedUserEntity(): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(2, count($result)); @@ -1635,7 +1630,7 @@ public function testManyToManyHydrationWithAliasedUserEntity(): void } /** - * SELECT PARTIAL u.{id, status}, UPPER(u.name) as nameUpper + * SELECT u, UPPER(u.name) as nameUpper * FROM Doctrine\Tests\Models\CMS\CmsUser u * * @group DDC-1358 @@ -1676,7 +1671,7 @@ public function testMissingIdForRootEntity($userEntityKey): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(4, count($result), 'Should hydrate four results.'); @@ -1693,7 +1688,7 @@ public function testMissingIdForRootEntity($userEntityKey): void } /** - * SELECT PARTIAL u.{id, status}, PARTIAL p.{phonenumber}, UPPER(u.name) AS nameUpper + * SELECT u, p, UPPER(u.name) AS nameUpper * FROM Doctrine\Tests\Models\CMS\CmsUser u * LEFT JOIN u.phonenumbers u * @@ -1746,7 +1741,7 @@ public function testMissingIdForCollectionValuedChildEntity($userEntityKey): voi $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(2, count($result)); @@ -1755,7 +1750,7 @@ public function testMissingIdForCollectionValuedChildEntity($userEntityKey): voi } /** - * SELECT PARTIAL u.{id, status}, PARTIAL a.{id, city}, UPPER(u.name) AS nameUpper + * SELECT u, a, UPPER(u.name) AS nameUpper * FROM Doctrine\Tests\Models\CMS\CmsUser u * JOIN u.address a * @@ -1800,7 +1795,7 @@ public function testMissingIdForSingleValuedChildEntity($userEntityKey): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(2, count($result)); @@ -1809,7 +1804,7 @@ public function testMissingIdForSingleValuedChildEntity($userEntityKey): void } /** - * SELECT PARTIAL u.{id, status}, UPPER(u.name) AS nameUpper + * SELECT u, UPPER(u.name) AS nameUpper * FROM Doctrine\Tests\Models\CMS\CmsUser u * INDEX BY u.id * @@ -1842,7 +1837,7 @@ public function testIndexByAndMixedResult($userEntityKey): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals(2, count($result)); @@ -1876,7 +1871,7 @@ public function testIndexByScalarsOnly($userEntityKey): void $stmt = ArrayResultFactory::createFromArray($resultSet); $hydrator = new ObjectHydrator($this->entityManager); - $result = $hydrator->hydrateAll($stmt, $rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals( [ diff --git a/tests/Doctrine/Tests/ORM/Internal/TopologicalSortTest.php b/tests/Doctrine/Tests/ORM/Internal/TopologicalSortTest.php new file mode 100644 index 00000000000..bba00d2d44e --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Internal/TopologicalSortTest.php @@ -0,0 +1,248 @@ + */ + private $nodes = []; + + /** @var TopologicalSort */ + private $topologicalSort; + + protected function setUp(): void + { + $this->topologicalSort = new TopologicalSort(); + } + + public function testSimpleOrdering(): void + { + $this->addNodes('C', 'B', 'A', 'E'); + + $this->addEdge('A', 'B'); + $this->addEdge('B', 'C'); + $this->addEdge('E', 'A'); + + // There is only 1 valid ordering for this constellation + self::assertSame(['E', 'A', 'B', 'C'], $this->computeResult()); + } + + public function testSkipOptionalEdgeToBreakCycle(): void + { + $this->addNodes('A', 'B'); + + $this->addEdge('A', 'B', true); + $this->addEdge('B', 'A', false); + + self::assertSame(['B', 'A'], $this->computeResult()); + } + + public function testBreakCycleByBacktracking(): void + { + $this->addNodes('A', 'B', 'C', 'D'); + + $this->addEdge('A', 'B'); + $this->addEdge('B', 'C', true); + $this->addEdge('C', 'D'); + $this->addEdge('D', 'A'); // closes the cycle + + // We can only break B -> C, so the result must be C -> D -> A -> B + self::assertSame(['C', 'D', 'A', 'B'], $this->computeResult()); + } + + public function testCycleRemovedByEliminatingLastOptionalEdge(): void + { + // The cycle-breaking algorithm is currently very naive. It breaks the cycle + // at the last optional edge while it backtracks. In this example, we might + // get away with one extra update if we'd break A->B; instead, we break up + // B->C and B->D. + + $this->addNodes('A', 'B', 'C', 'D'); + + $this->addEdge('A', 'B', true); + $this->addEdge('B', 'C', true); + $this->addEdge('C', 'A'); + $this->addEdge('B', 'D', true); + $this->addEdge('D', 'A'); + + self::assertSame(['C', 'D', 'A', 'B'], $this->computeResult()); + } + + public function testGH7180Example(): void + { + // Example given in https://github.com/doctrine/orm/pull/7180#issuecomment-381341943 + + $this->addNodes('E', 'F', 'D', 'G'); + + $this->addEdge('D', 'G'); + $this->addEdge('D', 'F', true); + $this->addEdge('F', 'E'); + $this->addEdge('E', 'D'); + + self::assertSame(['F', 'E', 'D', 'G'], $this->computeResult()); + } + + public function testCommitOrderingFromGH7259Test(): void + { + // this test corresponds to the GH7259Test::testPersistFileBeforeVersion functional test + $this->addNodes('A', 'B', 'C', 'D'); + + $this->addEdge('D', 'A'); + $this->addEdge('A', 'B'); + $this->addEdge('D', 'C'); + $this->addEdge('A', 'D', true); + + // There is only multiple valid ordering for this constellation, but + // the D -> A -> B ordering is important to break the cycle + // on the nullable link. + $correctOrders = [ + ['D', 'A', 'B', 'C'], + ['D', 'A', 'C', 'B'], + ['D', 'C', 'A', 'B'], + ]; + + self::assertContains($this->computeResult(), $correctOrders); + } + + public function testCommitOrderingFromGH8349Case1Test(): void + { + $this->addNodes('A', 'B', 'C', 'D'); + + $this->addEdge('D', 'A'); + $this->addEdge('A', 'B', true); + $this->addEdge('B', 'D', true); + $this->addEdge('B', 'C', true); + $this->addEdge('C', 'D', true); + + // Many orderings are possible here, but the bottom line is D must be before A (it's the only hard requirement). + $result = $this->computeResult(); + + $indexA = array_search('A', $result, true); + $indexD = array_search('D', $result, true); + self::assertTrue($indexD < $indexA); + } + + public function testCommitOrderingFromGH8349Case2Test(): void + { + $this->addNodes('A', 'B'); + + $this->addEdge('B', 'A'); + $this->addEdge('B', 'A', true); // interesting: We have two edges in that direction + $this->addEdge('A', 'B', true); + + // The B -> A requirement determines the result here + self::assertSame(['B', 'A'], $this->computeResult()); + } + + public function testNodesMaintainOrderWhenNoDepencency(): void + { + $this->addNodes('A', 'B', 'C'); + + // Nodes that are not constrained by dependencies shall maintain the order + // in which they were added + self::assertSame(['A', 'B', 'C'], $this->computeResult()); + } + + public function testDetectSmallCycle(): void + { + $this->addNodes('A', 'B'); + + $this->addEdge('A', 'B'); + $this->addEdge('B', 'A'); + + $this->expectException(CycleDetectedException::class); + $this->computeResult(); + } + + public function testMultipleEdges(): void + { + // There may be more than one association between two given entities. + // For the commit order, we only need to track this once, since the + // result is the same (one entity must be processed before the other). + // + // In case one of the associations is optional and the other one is not, + // we must honor the non-optional one, regardless of the order in which + // they were declared. + + $this->addNodes('A', 'B'); + + $this->addEdge('A', 'B', true); // optional comes first + $this->addEdge('A', 'B', false); + $this->addEdge('B', 'A', false); + $this->addEdge('B', 'A', true); // optional comes last + + // Both edges A -> B and B -> A are non-optional, so this is a cycle + // that cannot be broken. + + $this->expectException(CycleDetectedException::class); + $this->computeResult(); + } + + public function testDetectLargerCycleNotIncludingStartNode(): void + { + $this->addNodes('A', 'B', 'C', 'D'); + + $this->addEdge('A', 'B'); + $this->addEdge('B', 'C'); + $this->addEdge('C', 'D'); + $this->addEdge('D', 'B'); + + // The sort has to start with the last node being added to make it possible that + // the result is in the order the nodes were added (if permitted by edges). + // That means the cycle will be detected when starting at D, so it is D -> B -> C -> D. + + try { + $this->computeResult(); + } catch (CycleDetectedException $exception) { + self::assertEquals( + [$this->nodes['D'], $this->nodes['B'], $this->nodes['C'], $this->nodes['D']], + $exception->getCycle() + ); + } + } + + private function addNodes(string ...$names): void + { + foreach ($names as $name) { + $node = new Node($name); + $this->nodes[$name] = $node; + $this->topologicalSort->addNode($node); + } + } + + private function addEdge(string $from, string $to, bool $optional = false): void + { + $this->topologicalSort->addEdge($this->nodes[$from], $this->nodes[$to], $optional); + } + + /** + * @return list + */ + private function computeResult(): array + { + return array_map(static function (Node $n): string { + return $n->name; + }, array_values($this->topologicalSort->sort())); + } +} + +class Node +{ + /** @var string */ + public $name; + + public function __construct(string $name) + { + $this->name = $name; + } +} diff --git a/tests/Doctrine/Tests/ORM/Persisters/JoinedSubclassPersisterTest.php b/tests/Doctrine/Tests/ORM/Persisters/JoinedSubclassPersisterTest.php deleted file mode 100644 index 896a35a9a62..00000000000 --- a/tests/Doctrine/Tests/ORM/Persisters/JoinedSubclassPersisterTest.php +++ /dev/null @@ -1,38 +0,0 @@ -em = $this->getTestEntityManager(); - $this->persister = new JoinedSubclassPersister($this->em, $this->em->getClassMetadata(RootClass::class)); - } - - /** @group DDC-3470 */ - public function testExecuteInsertsWillReturnEmptySetWithNoQueuedInserts(): void - { - self::assertSame([], $this->persister->executeInserts()); - } -} diff --git a/tests/Doctrine/Tests/ORM/Proxy/ProxyFactoryTest.php b/tests/Doctrine/Tests/ORM/Proxy/ProxyFactoryTest.php index 804cbbe040e..3739aaf4dd5 100644 --- a/tests/Doctrine/Tests/ORM/Proxy/ProxyFactoryTest.php +++ b/tests/Doctrine/Tests/ORM/Proxy/ProxyFactoryTest.php @@ -5,7 +5,6 @@ namespace Doctrine\Tests\ORM\Proxy; use Doctrine\Common\EventManager; -use Doctrine\Common\Proxy\Proxy as CommonProxy; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\ORM\EntityNotFoundException; @@ -227,21 +226,12 @@ public function testProxyClonesParentFields(): void ->expects(self::atLeastOnce()) ->method('loadById'); - if ($proxy instanceof CommonProxy) { - $loadByIdMock->willReturn($companyEmployee); + $loadByIdMock->willReturn($companyEmployee); - $persister - ->expects(self::atLeastOnce()) - ->method('getClassMetadata') - ->willReturn($classMetaData); - } else { - $loadByIdMock->willReturnCallback(static function (array $id, CompanyEmployee $companyEmployee) { - $companyEmployee->setSalary(1000); // A property on the CompanyEmployee - $companyEmployee->setName('Bob'); // A property on the parent class, CompanyPerson - - return $companyEmployee; - }); - } + $persister + ->expects(self::atLeastOnce()) + ->method('getClassMetadata') + ->willReturn($classMetaData); $cloned = clone $proxy; assert($cloned instanceof CompanyEmployee); diff --git a/tests/Doctrine/Tests/ORM/Query/FilterCollectionTest.php b/tests/Doctrine/Tests/ORM/Query/FilterCollectionTest.php index c750dc74ce5..a4c0f998b47 100644 --- a/tests/Doctrine/Tests/ORM/Query/FilterCollectionTest.php +++ b/tests/Doctrine/Tests/ORM/Query/FilterCollectionTest.php @@ -30,15 +30,54 @@ public function testEnable(): void self::assertCount(0, $filterCollection->getEnabledFilters()); - $filterCollection->enable('testFilter'); + $filter1 = $filterCollection->enable('testFilter'); $enabledFilters = $filterCollection->getEnabledFilters(); self::assertCount(1, $enabledFilters); self::assertContainsOnly(MyFilter::class, $enabledFilters); - $filterCollection->disable('testFilter'); + $filter2 = $filterCollection->disable('testFilter'); self::assertCount(0, $filterCollection->getEnabledFilters()); + self::assertSame($filter1, $filter2); + + $filter3 = $filterCollection->enable('testFilter'); + self::assertNotSame($filter1, $filter3); + + $filter4 = $filterCollection->suspend('testFilter'); + self::assertSame($filter3, $filter4); + + $filter5 = $filterCollection->enable('testFilter'); + self::assertNotSame($filter4, $filter5); + + self::assertCount(1, $enabledFilters); + self::assertContainsOnly(MyFilter::class, $enabledFilters); + } + + public function testSuspend(): void + { + $filterCollection = $this->em->getFilters(); + + self::assertCount(0, $filterCollection->getEnabledFilters()); + + $filter1 = $filterCollection->enable('testFilter'); + self::assertCount(1, $filterCollection->getEnabledFilters()); + + $filter2 = $filterCollection->suspend('testFilter'); + self::assertSame($filter1, $filter2); + self::assertCount(0, $filterCollection->getEnabledFilters()); + + $filter3 = $filterCollection->restore('testFilter'); + self::assertSame($filter1, $filter3); + self::assertCount(1, $filterCollection->getEnabledFilters()); + } + + public function testRestoreFailure(): void + { + $filterCollection = $this->em->getFilters(); + + $this->expectException(InvalidArgumentException::class); + $filterCollection->suspend('testFilter'); } public function testHasFilter(): void @@ -49,7 +88,6 @@ public function testHasFilter(): void self::assertFalse($filterCollection->has('fakeFilter')); } - /** @depends testEnable */ public function testIsEnabled(): void { $filterCollection = $this->em->getFilters(); @@ -59,6 +97,41 @@ public function testIsEnabled(): void $filterCollection->enable('testFilter'); self::assertTrue($filterCollection->isEnabled('testFilter')); + + $filterCollection->suspend('testFilter'); + + self::assertFalse($filterCollection->isEnabled('testFilter')); + + $filterCollection->restore('testFilter'); + + self::assertTrue($filterCollection->isEnabled('testFilter')); + + self::assertFalse($filterCollection->isEnabled('wrongFilter')); + } + + public function testIsSuspended(): void + { + $filterCollection = $this->em->getFilters(); + + self::assertFalse($filterCollection->isSuspended('testFilter')); + + $filterCollection->enable('testFilter'); + + self::assertFalse($filterCollection->isSuspended('testFilter')); + + $filterCollection->suspend('testFilter'); + + self::assertTrue($filterCollection->isSuspended('testFilter')); + + $filterCollection->restore('testFilter'); + + self::assertFalse($filterCollection->isSuspended('testFilter')); + + $filterCollection->disable('testFilter'); + + self::assertFalse($filterCollection->isSuspended('testFilter')); + + self::assertFalse($filterCollection->isSuspended('wrongFilter')); } public function testGetFilterInvalidArgument(): void @@ -74,6 +147,11 @@ public function testGetFilter(): void $filterCollection->enable('testFilter'); self::assertInstanceOf(MyFilter::class, $filterCollection->getFilter('testFilter')); + + $filterCollection->suspend('testFilter'); + + $this->expectException(InvalidArgumentException::class); + $filterCollection->getFilter('testFilter'); } public function testHashing(): void @@ -99,6 +177,18 @@ public function testHashing(): void self::assertTrue($filterCollection->isClean()); self::assertSame($hash, $filterCollection->getHash()); + $filterCollection->suspend('testFilter'); + + self::assertFalse($filterCollection->isClean()); + self::assertSame($oldHash, $filterCollection->getHash()); + self::assertTrue($filterCollection->isClean()); + + $filterCollection->restore('testFilter'); + + self::assertFalse($filterCollection->isClean()); + self::assertSame($hash, $filterCollection->getHash()); + self::assertTrue($filterCollection->isClean()); + $filterCollection->disable('testFilter'); self::assertFalse($filterCollection->isClean()); diff --git a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php index 7695bd60054..4d1972593f3 100644 --- a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php +++ b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php @@ -42,7 +42,6 @@ public function assertInvalidDQL(string $dql): void public function parseDql(string $dql, array $hints = []): ParserResult { $query = $this->entityManager->createQuery($dql); - $query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true); $query->setDQL($dql); foreach ($hints as $key => $value) { diff --git a/tests/Doctrine/Tests/ORM/QueryBuilderTest.php b/tests/Doctrine/Tests/ORM/QueryBuilderTest.php index b6fe839bbb8..28774189a29 100644 --- a/tests/Doctrine/Tests/ORM/QueryBuilderTest.php +++ b/tests/Doctrine/Tests/ORM/QueryBuilderTest.php @@ -1087,6 +1087,18 @@ public function testAddDistinct(): void self::assertEquals('SELECT DISTINCT u FROM Doctrine\Tests\Models\CMS\CmsUser u', $qb->getDQL()); } + public function testDistinctUpdatesState(): void + { + $qb = $this->entityManager->createQueryBuilder() + ->select('u') + ->from(CmsUser::class, 'u'); + + $qb->getDQL(); + $qb->distinct(); + + self::assertEquals('SELECT DISTINCT u FROM Doctrine\Tests\Models\CMS\CmsUser u', $qb->getDQL()); + } + /** @group DDC-2192 */ public function testWhereAppend(): void { diff --git a/tests/Doctrine/Tests/ORM/Tools/AttachEntityListenersListenerTest.php b/tests/Doctrine/Tests/ORM/Tools/AttachEntityListenersListenerTest.php index 485794e331a..31223ee9262 100644 --- a/tests/Doctrine/Tests/ORM/Tools/AttachEntityListenersListenerTest.php +++ b/tests/Doctrine/Tests/ORM/Tools/AttachEntityListenersListenerTest.php @@ -56,6 +56,17 @@ public function testAttachEntityListeners(): void self::assertCount(1, $metadata->entityListeners['postLoad']); self::assertEquals('postLoadHandler', $metadata->entityListeners['postLoad'][0]['method']); self::assertEquals(AttachEntityListenersListenerTestListener::class, $metadata->entityListeners['postLoad'][0]['class']); + + // Can reattach entity listeners even class metadata factory recreated. + $factory2 = new ClassMetadataFactory(); + $factory2->setEntityManager($this->em); + + $metadata2 = $factory2->getMetadataFor(AttachEntityListenersListenerTestFooEntity::class); + + self::assertArrayHasKey('postLoad', $metadata2->entityListeners); + self::assertCount(1, $metadata2->entityListeners['postLoad']); + self::assertEquals('postLoadHandler', $metadata2->entityListeners['postLoad'][0]['method']); + self::assertEquals(AttachEntityListenersListenerTestListener::class, $metadata2->entityListeners['postLoad'][0]['class']); } public function testAttachToExistingEntityListeners(): void diff --git a/tests/Doctrine/Tests/ORM/Tools/SchemaToolTest.php b/tests/Doctrine/Tests/ORM/Tools/SchemaToolTest.php index 08305b243dd..83f70e85965 100644 --- a/tests/Doctrine/Tests/ORM/Tools/SchemaToolTest.php +++ b/tests/Doctrine/Tests/ORM/Tools/SchemaToolTest.php @@ -77,7 +77,7 @@ public function testAnnotationOptionsAttribute(): void ); $table = $schema->getTable('TestEntityWithAnnotationOptionsAttribute'); - foreach ([$table->getOptions(), $table->getColumn('test')->getCustomSchemaOptions()] as $options) { + foreach ([$table->getOptions(), $table->getColumn('test')->getPlatformOptions()] as $options) { self::assertArrayHasKey('foo', $options); self::assertSame('bar', $options['foo']); self::assertArrayHasKey('baz', $options); @@ -139,7 +139,7 @@ public function testPassColumnOptionsToJoinColumn(): void self::assertEquals( ['collation' => 'latin1_bin', 'foo' => 'bar'], - $tableBoard->getColumn('category_id')->getCustomSchemaOptions() + $tableBoard->getColumn('category_id')->getPlatformOptions() ); } @@ -192,13 +192,13 @@ public function testEnumTypeAddedToCustomSchemaOptions(): void $em = $this->getTestEntityManager(); $schemaTool = new SchemaTool($em); - $customSchemaOptions = $schemaTool->getSchemaFromMetadata([$em->getClassMetadata(Card::class)]) + $platformOptions = $schemaTool->getSchemaFromMetadata([$em->getClassMetadata(Card::class)]) ->getTable('Card') ->getColumn('suit') - ->getCustomSchemaOptions(); + ->getPlatformOptions(); - self::assertArrayHasKey('enumType', $customSchemaOptions); - self::assertSame(Suit::class, $customSchemaOptions['enumType']); + self::assertArrayHasKey('enumType', $platformOptions); + self::assertSame(Suit::class, $platformOptions['enumType']); } /** @group DDC-3671 */ diff --git a/tests/Doctrine/Tests/ORM/UnitOfWorkTest.php b/tests/Doctrine/Tests/ORM/UnitOfWorkTest.php index 5e5fc29b7f9..e47af9d57df 100644 --- a/tests/Doctrine/Tests/ORM/UnitOfWorkTest.php +++ b/tests/Doctrine/Tests/ORM/UnitOfWorkTest.php @@ -41,6 +41,7 @@ use Doctrine\Tests\OrmTestCase; use Doctrine\Tests\PHPUnitCompatibility\MockBuilderCompatibilityTools; use PHPUnit\Framework\MockObject\MockObject; +use RuntimeException; use stdClass; use function assert; @@ -108,6 +109,8 @@ protected function setUp(): void $driverConnection = $this->createMock(Driver\Connection::class); $driverConnection->method('prepare') ->willReturn($driverStatement); + $driverConnection->method('lastInsertId') + ->willReturnOnConsecutiveCalls(1, 2, 3, 4, 5, 6); $driver = $this->createMock(Driver::class); $driver->method('getDatabasePlatform') @@ -923,6 +926,25 @@ public function testRemovedEntityIsRemovedFromOneToManyCollection(): void self::assertFalse($user->phonenumbers->isDirty()); self::assertEmpty($user->phonenumbers->getSnapshot()); } + + public function testItThrowsWhenApplicationProvidedIdsCollide(): void + { + // We're using application-provided IDs and assign the same ID twice + // Note this is about colliding IDs in the identity map in memory. + // Duplicate database-level IDs would be spotted when the EM is flushed. + + $phone1 = new CmsPhonenumber(); + $phone1->phonenumber = '1234'; + $this->_unitOfWork->persist($phone1); + + $phone2 = new CmsPhonenumber(); + $phone2->phonenumber = '1234'; + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/another object .* was already present for the same ID/'); + + $this->_unitOfWork->persist($phone2); + } } /** @Entity */ diff --git a/tests/Doctrine/Tests/OrmFunctionalTestCase.php b/tests/Doctrine/Tests/OrmFunctionalTestCase.php index 368e2b6f71b..07bf94eb654 100644 --- a/tests/Doctrine/Tests/OrmFunctionalTestCase.php +++ b/tests/Doctrine/Tests/OrmFunctionalTestCase.php @@ -421,6 +421,7 @@ protected function tearDown(): void $conn->executeStatement('DELETE FROM ecommerce_products_categories'); $conn->executeStatement('DELETE FROM ecommerce_products_related'); $conn->executeStatement('DELETE FROM ecommerce_carts'); + $conn->executeStatement('DELETE FROM ecommerce_customers WHERE mentor_id IS NOT NULL'); $conn->executeStatement('DELETE FROM ecommerce_customers'); $conn->executeStatement('DELETE FROM ecommerce_features'); $conn->executeStatement('DELETE FROM ecommerce_products'); @@ -967,4 +968,10 @@ protected function dropAndCreateTable(Table $table): void $this->dropTableIfExists($tableName); $schemaManager->createTable($table); } + + /** @param object $entity */ + final protected function isUninitializedObject($entity): bool + { + return $this->_em->getUnitOfWork()->isUninitializedObject($entity); + } }