Compare commits

...

68 Commits

Author SHA1 Message Date
Alexander M. Turek e8afa9f80c Prepare 2.17.0 (#11059) 2023-11-16 00:04:41 +01:00
Grégoire Paris 1267f482ef Merge pull request #8391 from beberlei/GH-1569-SubselectFetchMode
[GH-1569] Optimize eager fetch for collections to batch query
2023-11-14 08:16:00 +01:00
Grégoire Paris b41de2a39d Merge pull request #11056 from derrabus/deprecate/named-queries
Deprecate annotation classes for named queries
2023-11-11 12:30:09 +01:00
Alexander M. Turek 6a48b0741b Deprecate annotation classes for named queries 2023-11-09 16:01:17 +01:00
Grégoire Paris 0e95838787 Merge pull request #11044 from greg0ire/identity-on-dbal4
Recommend SEQUENCE until doctrine/dbal 4 is released
2023-11-07 08:14:32 +01:00
Grégoire Paris be62f72b38 Merge pull request #11048 from greg0ire/restore-bc
Restore backward compatibility with previous format
2023-11-06 20:08:18 +01:00
Grégoire Paris c075154e1e Restore backward compatibility with previous format
When unserializing from a cache entry in the previous format, the
sqlStatements need to be copied from the legacy property to the new
property before the reference is created.
2023-11-06 07:52:27 +01:00
Grégoire Paris f08159eb87 Merge pull request #11027 from greg0ire/fw-compat-serialization
Make serialized SQL executors forward compatible
2023-11-05 23:27:48 +01:00
Grégoire Paris 300cffb942 Recommend SEQUENCE until doctrine/dbal 4 is released
Using IDENTITY with doctrine/dbal 3 results in SERIAL, which is not
recommended.
2023-11-05 20:59:24 +01:00
Grégoire Paris 2a9390d426 Make serialized SQL executors forward compatible
The idea here is that instead of having a backward compatibility layer
in the next major branch, we can have a forward compatibility layer in
this branch.
2023-11-05 19:56:16 +01:00
Benjamin Eberlei ec74c83845 Fix typos 2023-11-05 19:26:35 +01:00
Benjamin Eberlei 4b2b4860fb Housekeeping: Revert change to AbstractExporter, not needed without subselect fetch. 2023-10-22 20:11:36 +02:00
Benjamin Eberlei 609e10df36 Address review comments. 2023-10-22 20:08:18 +02:00
Benjamin Eberlei d03aed1265 Explain internals of eager loading in a bit more detail and how its configured. 2023-10-22 20:08:06 +02:00
Benjamin Eberlei 6993ad28ed 1:1 and M:1 associations also use fetch batch size configuration now. 2023-10-22 20:07:04 +02:00
Grégoire Paris 16028e4fd3 Merge pull request #11023 from doctrine/2.16.x
Merge 2.16.x up into 2.17.x
2023-10-21 19:52:04 +02:00
Grégoire Paris edd962e385 Merge pull request #11000 from greg0ire/copy-debug
Copy Debug class from doctrine/common
2023-10-14 23:00:13 +02:00
Grégoire Paris a33a3813b2 Copy Debug class from doctrine/common
This reduces our dependency to this shared library that now holds very
little code we use.
The class has not been copied verbatim:
- Unused parameters and methods have been removed.
- The class is final and internal.
- Coding standards have been enforced, including enabling strict_types,
  which lead to casting a variable to string before feeding it to
  explode().
- A bug found by static analysis has been addressed, where an INI
  setting obtained with ini_get() was compared with true, which is never
  returned by that function.
- Tests are improved to run on all PHP versions
2023-10-14 21:21:51 +02:00
Grégoire Paris bf69d0ac4e Implement proxy name resolver (#11009)
It is important to have the same implementation as used in
doctrine/persistence without relying on copy/paste.
2023-10-14 19:48:11 +02:00
Benjamin Eberlei 3f2fa309d4 Add another testcase for DQL based fetch eager of collection. 2023-10-14 15:56:42 +02:00
Benjamin Eberlei 8057b51f85 last violation hopefully 2023-10-14 15:38:26 +02:00
Benjamin Eberlei c09660ac50 Merge remote-tracking branch 'origin/2.17.x' into GH-1569-SubselectFetchMode 2023-10-14 15:29:11 +02:00
Benjamin Eberlei 8ec599bb17 Static analysis 2023-10-14 15:28:57 +02:00
Benjamin Eberlei bf74b434b8 Housekeeping: phpcs 2023-10-14 14:23:20 +02:00
Benjamin Eberlei cd54c56278 Directly load many to many collections, batching not supported yet. fix tests. 2023-10-14 14:21:15 +02:00
Benjamin Eberlei 76fd34f766 Avoid new fetch mode, use this strategy with fetch=EAGER for collections. 2023-10-14 14:04:30 +02:00
David Buchmann 1cec0b82bd Remove useless check (#11006) 2023-10-13 18:57:12 +02:00
Grégoire Paris 0e74a180c4 Merge pull request #10999 from greg0ire/prepare-common-severance 2023-10-13 11:25:16 +02:00
Grégoire Paris fdfca0f0e7 Undeprecate Autoloader class
We plan to sunset doctrine/common, and should move the Autoloader class
to doctrine/orm
2023-10-13 09:02:36 +02:00
Grégoire Paris c5137da90e Merge pull request #8931 from greg0ire/gh-8893 2023-10-11 10:38:15 +02:00
Grégoire Paris e89b680a28 Deprecate reliance on non-optimal defaults
What was optimal 10 years ago no longer is, and things might change in
the future. Using AUTO is still the best solution in most cases, and it
should be easy to make it mean something else when it is not.
2023-10-11 10:19:39 +02:00
Grégoire Paris 07b0917505 Merge pull request #10989 from greg0ire/improve-exceptions
Add method name in exception
2023-10-10 21:22:05 +02:00
Alexander M. Turek 143ee25697 Allow creating mocks of the Query class (#10990) 2023-10-10 17:16:01 +02:00
Grégoire Paris 52853c2e9c Merge pull request #10988 from greg0ire/add-missing-annotation 2023-10-10 16:42:50 +02:00
Benjamin Eberlei 40bfe07172 Make sure to many assocatinos are also respecting AbstractQuery::setFetchMode 2023-10-10 16:29:00 +02:00
Grégoire Paris 6983f48490 Merge pull request #10987 from greg0ire/deprecate-partialreference 2023-10-10 15:32:11 +02:00
Grégoire Paris 194f5062bb Add method name in exception
When we assert a given exception should be thrown, and get this instead,
it is hard to figure out what went wrong.
2023-10-10 15:19:16 +02:00
Grégoire Paris 922365d2c5 Add missing "deprecated" annotation on the annotation driver 2023-10-10 15:14:11 +02:00
Grégoire Paris a1e055b608 Deprecate EntityManager*::getPartialReference()
Partial objects have been deprecated, so it makes no sense to still have
this way of getting some.
2023-10-10 14:39:08 +02:00
Alexander M. Turek 0e3489b240 Don't assert that BIGINTs are stored as strings (#10980) 2023-10-10 11:35:09 +02:00
Benjamin Eberlei ff28ba8080 Disallow use of fetch=SUBSELECT on to-one associations. 2023-10-10 08:25:04 +02:00
Benjamin Eberlei 41410e6be1 Go through Persister API instead of indirectly through repository. 2023-10-10 08:17:25 +02:00
Benjamin Eberlei b9e55bad4d Introduce configuration option for subselect batch size. 2023-10-10 07:59:29 +02:00
Benjamin Eberlei 47952c3228 Houskeeping: phpcs 2023-10-10 07:51:13 +02:00
Benjamin Eberlei fdceb82454 Disallow WITH keyword on fetch joined associatiosn via subselect. 2023-10-10 07:49:38 +02:00
Benjamin Eberlei dc899e26cf [GH-1569] Add new SUBSELECT fetch mode for OneToMany associations.
Co-authored-by: Wouter M. van Vliet <wouter@interpotential.com>
2023-10-10 07:26:17 +02:00
Alexander M. Turek 48edb33b3f Merge branch '2.16.x' into 2.17.x
* 2.16.x:
  Test against php 8.3 (#10963)
  update checkout version to version 4
2023-10-09 17:30:21 +02:00
Grégoire Paris 9c2e49a665 Merge pull request #10970 from goetas/distinct-limit 2023-10-09 16:43:20 +02:00
Grégoire Paris 8ef5c80148 Merge pull request #10974 from beberlei/RemovePartialObjectExpressionUsage 2023-10-09 16:28:45 +02:00
Benjamin Eberlei 083b1f98c1 Housekeeping: phpcs 2023-10-09 15:42:41 +02:00
Matthias Pigulla 3ff67c3e2f Merge pull request #10967 from greg0ire/remove-commit-order-calculator-from-2
Remove CommitOrderCalculator and related classes
2023-10-09 15:37:05 +02:00
Benjamin Eberlei 84a762e12e Adjust tests for new sql generation 2023-10-09 15:35:48 +02:00
Benjamin Eberlei ef2123bd0f Remove use of PartialObjectExyxpression for LimitSubqueryOutputWalker to prepare for removal. 2023-10-09 15:30:10 +02:00
Asmir Mustafic 55699a9129 document Paginator::HINT_ENABLE_DISTINCT 2023-10-09 14:05:01 +02:00
Asmir Mustafic 68fc3b6458 allow to disable "DISTINCT" added to the sql query by the limit subquery walker 2023-10-09 14:05:01 +02:00
Grégoire Paris 925631878f Remove CommitOrderCalculator and related classes
They are unused, and since they are internal, it should be fine to
remove them without a deprecation.
2023-10-07 11:57:26 +02:00
Alexander M. Turek db2791001c Merge branch '2.16.x' into 2.17.x
* 2.16.x:
  PHPStan 1.10.35, Psalm 5.15.0 (#10958)
  docs: in text, refer to attributes when talking about metadata (#10956)
  Fix bullet list layout (#10951)
  docs[query-builder]: fix rendering of `Doctrine\DBAL\ParameterType::*` (#10945)
  tests[ORMSetupTest]: testCacheNamespaceShouldBeGeneratedForApcu requires enabled apc (#10940)
  docs: use modern named arguments syntax
  Ignore "Unknown directive" error
  Use a stable release
  Remove output directory argument
  tutorials[getting-started]: example fix bug id type definition
  Verify UnitOfWork::HINT_DEFEREAGERLOAD exists and is true
2023-09-29 08:57:56 +02:00
Grégoire Paris 633ce41460 Merge pull request #10946 from greg0ire/improved-validation
Adds metadata field type validation against Entity property type
2023-09-23 12:12:18 +02:00
DavideBicego 0f67ba2176 Adds metadata field type validation against Entity property type
This avoid situations where you might map a decimal to a float, when it
should really be mapped to a string. This is a big pitfall because in
that situation, the ORM will consider the field changed every time.
2023-09-15 11:19:58 +02:00
Alexander M. Turek a2d2e173c2 Merge release 2.16.2 into 2.17.x (#10924) 2023-08-28 09:50:37 +02:00
Alexander M. Turek aa333e2f1d Support Symfony 7 by adding return types conditionally (#10919) 2023-08-24 10:36:04 +02:00
Alexander M. Turek b6441b4f26 Merge branch '2.16.x' into 2.17.x
* 2.16.x:
  Use required classes for Lifecycle Callback examples (#10916)
  Add space before backquote (#10918)
  Add back throws annotation to getSingleScalarResult (#10907)
  Fix link on known issues docs (#10904)
2023-08-23 23:47:29 +02:00
Alexander M. Turek 9647d0e2ae Merge release 2.16.1 into 2.17.x (#10896) 2023-08-09 15:17:34 +02:00
Alexander M. Turek 368eb01ac3 Merge 2.16.x into 2.17.x (#10894) 2023-08-09 11:46:12 +02:00
Nicolas Grekas 47cf50bcd5 Add note about not-enabling lazy-ghosts (#10887) 2023-08-07 15:20:38 +02:00
Alexander M. Turek 58df4078fc Merge branch '2.16.x' into 2.17.x
* 2.16.x:
  Turn identity map collisions from exception to deprecation notice (#10878)
  Add possibility to set reportFieldsWhereDeclared to true in ORMSetup (#10865)
  Fix UnitOfWork->originalEntityData is missing not-modified collections after computeChangeSet  (#9301)
  Add an UPGRADE notice about the potential changes in commit order (#10866)
  Update branch metadata (#10862)
2023-08-07 15:17:36 +02:00
Nicolas Grekas eda1909c75 Deprecate not-enabling lazy-ghosts and decouple from doctrine/common's proxies (#10837) 2023-08-07 14:43:38 +02:00
Alexander M. Turek ab542e97df Allow symfony/console 7 (#10724) 2023-08-01 14:25:03 +02:00
97 changed files with 1953 additions and 768 deletions
+8 -6
View File
@@ -11,21 +11,23 @@
"slug": "latest",
"upcoming": true
},
{
"name": "2.18",
"branchName": "2.18.x",
"slug": "2.18",
"upcoming": true
},
{
"name": "2.17",
"branchName": "2.17.x",
"slug": "2.17",
"upcoming": true
"current": true
},
{
"name": "2.16",
"branchName": "2.16.x",
"slug": "2.16",
"current": true,
"aliases": [
"current",
"stable"
]
"maintained": false
},
{
"name": "2.15",
+62 -11
View File
@@ -1,9 +1,67 @@
# Upgrade to 2.17
## Deprecate annotations classes for named queries
The following classes have been deprecated:
* `Doctrine\ORM\Mapping\NamedNativeQueries`
* `Doctrine\ORM\Mapping\NamedNativeQuery`
* `Doctrine\ORM\Mapping\NamedQueries`
* `Doctrine\ORM\Mapping\NamedQuery`
## Deprecate `Doctrine\ORM\Query\Exec\AbstractSqlExecutor::_sqlStatements`
Use `Doctrine\ORM\Query\Exec\AbstractSqlExecutor::sqlStatements` instead.
## Undeprecate `Doctrine\ORM\Proxy\Autoloader`
It will be a full-fledged class, no longer extending
`Doctrine\Common\Proxy\Autoloader` in 3.0.x.
## Deprecated: reliance on the non-optimal defaults that come with the `AUTO` identifier generation strategy
When the `AUTO` identifier generation strategy was introduced, the best
strategy at the time was selected for each database platform.
A lot of time has passed since then, and with ORM 3.0.0 and DBAL 4.0.0, support
for better strategies will be added.
Because of that, it is now deprecated to rely on the historical defaults when
they differ from what we will be recommended in the future.
Instead, you should pick a strategy for each database platform you use, and it
will be used when using `AUTO`. As of now, only PostgreSQL is affected by this.
It is recommended that PostgreSQL users configure their existing and new
applications to use `SEQUENCE` until `doctrine/dbal` 4.0.0 is released:
```php
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\ORM\Configuration;
assert($configuration instanceof Configuration);
$configuration->setIdentityGenerationPreferences([
PostgreSQLPlatform::CLASS => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
]);
```
When DBAL 4 is released, `AUTO` will result in `IDENTITY`, and the above
configuration should be removed to migrate to it.
## Deprecate `EntityManagerInterface::getPartialReference()`
This method does not have a replacement and will be removed in 3.0.
## Deprecate not-enabling lazy-ghosts
Not enabling lazy ghost objects is deprecated. In ORM 3.0, they will be always enabled.
Ensure `Doctrine\ORM\Configuration::setLazyGhostObjectEnabled(true)` is called to enable them.
# Upgrade to 2.16
## Deprecated accepting duplicate IDs in the identity map
For any given entity class and ID value, there should be only one object instance
representing the entity.
representing the entity.
In https://github.com/doctrine/orm/pull/10785, a check was added that will guard this
in the identity map. The most probable cause for violations of this rule are collisions
@@ -26,13 +84,6 @@ avoided.
When using database-provided, auto-incrementing IDs, this may lead to IDs being assigned
to entities in a different order than it was previously the case.
## Deprecated `\Doctrine\ORM\Internal\CommitOrderCalculator` and related classes
With changes made to the commit order computation, the internal classes
`\Doctrine\ORM\Internal\CommitOrderCalculator`, `\Doctrine\ORM\Internal\CommitOrder\Edge`,
`\Doctrine\ORM\Internal\CommitOrder\Vertex` and `\Doctrine\ORM\Internal\CommitOrder\VertexState`
have been deprecated and will be removed in ORM 3.0.
## Deprecated returning post insert IDs from `EntityPersister::executeInserts()`
Persisters implementing `\Doctrine\ORM\Persisters\Entity\EntityPersister` should no longer
@@ -44,12 +95,12 @@ persister call `Doctrine\ORM\UnitOfWork::assignPostInsertId()` instead.
In ORM 3.0, a change will be made regarding how the `AttributeDriver` reports field mappings.
This change is necessary to be able to detect (and reject) some invalid mapping configurations.
To avoid surprises during 2.x upgrades, the new mode is opt-in. It can be activated on the
To avoid surprises during 2.x upgrades, the new mode is opt-in. It can be activated on the
`AttributeDriver` and `AnnotationDriver` by setting the `$reportFieldsWhereDeclared`
constructor parameter to `true`. It will cause `MappingException`s to be thrown when invalid
configurations are detected.
Not enabling the new mode will cause a deprecation notice to be raised. In ORM 3.0,
Not enabling the new mode will cause a deprecation notice to be raised. In ORM 3.0,
only the new mode will be available.
# Upgrade to 2.15
@@ -69,7 +120,7 @@ and will be an error in 3.0.
## Deprecated undeclared entity inheritance
As soon as an entity class inherits from another entity class, inheritance has to
As soon as an entity class inherits from another entity class, inheritance has to
be declared by adding the appropriate configuration for the root entity.
## Deprecated stubs for "concrete table inheritance"
+1 -1
View File
@@ -34,7 +34,7 @@
"doctrine/lexer": "^2",
"doctrine/persistence": "^2.4 || ^3",
"psr/cache": "^1 || ^2 || ^3",
"symfony/console": "^4.2 || ^5.0 || ^6.0",
"symfony/console": "^4.2 || ^5.0 || ^6.0 || ^7.0",
"symfony/polyfill-php72": "^1.23",
"symfony/polyfill-php80": "^1.16"
},
+1 -1
View File
@@ -408,7 +408,7 @@ means that you have to register a special autoloader for these classes:
.. code-block:: php
<?php
use Doctrine\Common\Proxy\Autoloader;
use Doctrine\ORM\Proxy\Autoloader;
$proxyDir = "/path/to/proxies";
$proxyNamespace = "MyProxies";
+13 -10
View File
@@ -422,9 +422,11 @@ the field that serves as the identifier with the ``#[Id]`` attribute.
# fields here
In most cases using the automatic generator strategy (``#[GeneratedValue]``) is
what you want. It defaults to the identifier generation mechanism your current
database vendor prefers: AUTO_INCREMENT with MySQL, sequences with PostgreSQL
and Oracle and so on.
what you want, but for backwards-compatibility reasons it might not. It
defaults to the identifier generation mechanism your current database
vendor preferred at the time that strategy was introduced:
``AUTO_INCREMENT`` with MySQL, sequences with PostgreSQL and Oracle and
so on.
.. _identifier-generation-strategies:
@@ -441,17 +443,18 @@ Here is the list of possible generation strategies:
- ``AUTO`` (default): Tells Doctrine to pick the strategy that is
preferred by the used database platform. The preferred strategies
are IDENTITY for MySQL, SQLite, MsSQL and SQL Anywhere and SEQUENCE
for Oracle and PostgreSQL. This strategy provides full portability.
- ``SEQUENCE``: Tells Doctrine to use a database sequence for ID
generation. This strategy does currently not provide full
portability. Sequences are supported by Oracle, PostgreSql and
SQL Anywhere.
are ``IDENTITY`` for MySQL, SQLite, MsSQL and SQL Anywhere and, for
historical reasons, ``SEQUENCE`` for Oracle and PostgreSQL. This
strategy provides full portability.
- ``IDENTITY``: Tells Doctrine to use special identity columns in
the database that generate a value on insertion of a row. This
strategy does currently not provide full portability and is
supported by the following platforms: MySQL/SQLite/SQL Anywhere
(AUTO\_INCREMENT), MSSQL (IDENTITY) and PostgreSQL (SERIAL).
(``AUTO_INCREMENT``), MSSQL (``IDENTITY``) and PostgreSQL (``SERIAL``).
- ``SEQUENCE``: Tells Doctrine to use a database sequence for ID
generation. This strategy does currently not provide full
portability. Sequences are supported by Oracle, PostgreSql and
SQL Anywhere.
- ``UUID`` (deprecated): Tells Doctrine to use the built-in Universally
Unique Identifier generator. This strategy provides full portability.
- ``NONE``: Tells Doctrine that the identifiers are assigned (and
@@ -782,6 +782,23 @@ and these associations are mapped as EAGER, they will automatically
be loaded together with the entity being queried and is thus
immediately available to your application.
Eager Loading can also be configured at runtime through
``AbstractQuery::setFetchMode`` in DQL or Native Queries.
Eager loading for many-to-one and one-to-one associations is using either a
LEFT JOIN or a second query for fetching the related entity eagerly.
Eager loading for many-to-one associations uses a second query to load
the collections for several entities at the same time.
When many-to-many, one-to-one or one-to-many associations are eagerly loaded,
then the global batch size configuration is used to avoid IN(?) queries with
too many arguments. The default batch size is 100 and can be changed with
``Configuration::setEagerFetchBatchSize()``.
For eagerly loaded Many-To-Many associations one query has to be made for each
collection.
By Lazy Loading
~~~~~~~~~~~~~~~
+15
View File
@@ -43,3 +43,18 @@ the future.
.. note::
``fetchJoinCollection`` argument set to ``true`` might affect results if you use aggregations in your query.
By using the ``Paginator::HINT_ENABLE_DISTINCT`` you can instruct doctrine that the query to be executed
will not produce "duplicate" rows (only to-one relations are joined), thus the SQL limit will work as expected.
In this way the `DISTINCT` keyword will be omitted and can bring important performance improvements.
.. code-block:: php
<?php
use Doctrine\ORM\Tools\Pagination\Paginator;
$dql = "SELECT u, p FROM User u JOIN u.mainPicture p";
$query = $entityManager->createQuery($dql)
->setHint(Paginator::HINT_ENABLE_DISTINCT, false)
->setFirstResult(0)
->setMaxResults(100);
+2 -2
View File
@@ -10,7 +10,6 @@ use Doctrine\Common\Cache\Psr6\CacheAdapter;
use Doctrine\Common\Cache\Psr6\DoctrineProvider;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\Result;
use Doctrine\Deprecations\Deprecation;
@@ -20,6 +19,7 @@ use Doctrine\ORM\Cache\QueryCacheKey;
use Doctrine\ORM\Cache\TimestampCacheKey;
use Doctrine\ORM\Internal\Hydration\IterableResult;
use Doctrine\ORM\Mapping\MappingException as ORMMappingException;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Query\Parameter;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Query\ResultSetMapping;
@@ -430,7 +430,7 @@ abstract class AbstractQuery
}
try {
$class = ClassUtils::getClass($value);
$class = DefaultProxyClassNameResolver::getClass($value);
$value = $this->_em->getUnitOfWork()->getSingleIdentifierValue($value);
if ($value === null) {
+2 -2
View File
@@ -4,12 +4,12 @@ declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\Persister\CachedPersister;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\ORMInvalidArgumentException;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\UnitOfWork;
use function is_array;
@@ -293,7 +293,7 @@ class DefaultCache implements Cache
private function toIdentifierArray(ClassMetadata $metadata, $identifier): array
{
if (is_object($identifier)) {
$class = ClassUtils::getClass($identifier);
$class = DefaultProxyClassNameResolver::getClass($identifier);
if ($this->em->getMetadataFactory()->hasMetadataFor($class)) {
$identifier = $this->uow->getSingleIdentifierValue($identifier);
@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Query;
use Doctrine\ORM\UnitOfWork;
use Doctrine\ORM\Utility\IdentifierFlattener;
@@ -112,7 +112,7 @@ class DefaultEntityHydrator implements EntityHydrator
}
if (! isset($assoc['id'])) {
$targetClass = ClassUtils::getClass($data[$name]);
$targetClass = DefaultProxyClassNameResolver::getClass($data[$name]);
$targetId = $this->uow->getEntityIdentifier($data[$name]);
$data[$name] = new AssociationCacheEntry($targetClass, $targetId);
@@ -6,7 +6,6 @@ namespace Doctrine\ORM\Cache\Persister\Collection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Cache\CollectionCacheKey;
use Doctrine\ORM\Cache\CollectionHydrator;
@@ -19,6 +18,7 @@ use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\UnitOfWork;
use function array_values;
@@ -148,7 +148,7 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister
}
$class = $this->targetEntity;
$className = ClassUtils::getClass($elements[$index]);
$className = DefaultProxyClassNameResolver::getClass($elements[$index]);
if ($className !== $this->targetEntity->name) {
$class = $this->metadataFactory->getMetadataFor($className);
@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace Doctrine\ORM\Cache\Persister\Collection;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\ORM\Cache\Exception\CannotUpdateReadOnlyCollection;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
class ReadOnlyCachedCollectionPersister extends NonStrictReadWriteCachedCollectionPersister
{
@@ -17,7 +17,7 @@ class ReadOnlyCachedCollectionPersister extends NonStrictReadWriteCachedCollecti
{
if ($collection->isDirty() && $collection->getSnapshot()) {
throw CannotUpdateReadOnlyCollection::fromEntityAndField(
ClassUtils::getClass($collection->getOwner()),
DefaultProxyClassNameResolver::getClass($collection->getOwner()),
$this->association['fieldName']
);
}
@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Doctrine\ORM\Cache\Persister\Entity;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\CollectionCacheKey;
use Doctrine\ORM\Cache\EntityCacheKey;
@@ -21,6 +20,7 @@ use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Persisters\Entity\EntityPersister;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\UnitOfWork;
use function array_merge;
@@ -190,7 +190,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
public function storeEntityCache($entity, EntityCacheKey $key)
{
$class = $this->class;
$className = ClassUtils::getClass($entity);
$className = DefaultProxyClassNameResolver::getClass($entity);
if ($className !== $this->class->name) {
$class = $this->metadataFactory->getMetadataFor($className);
@@ -438,7 +438,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
}
$class = $this->class;
$className = ClassUtils::getClass($entity);
$className = DefaultProxyClassNameResolver::getClass($entity);
if ($className !== $this->class->name) {
$class = $this->metadataFactory->getMetadataFor($className);
@@ -4,8 +4,8 @@ declare(strict_types=1);
namespace Doctrine\ORM\Cache\Persister\Entity;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\ORM\Cache\Exception\CannotUpdateReadOnlyEntity;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
/**
* Specific read-only region entity persister
@@ -17,6 +17,6 @@ class ReadOnlyCachedEntityPersister extends NonStrictReadWriteCachedEntityPersis
*/
public function update($entity)
{
throw CannotUpdateReadOnlyEntity::fromEntity(ClassUtils::getClass($entity));
throw CannotUpdateReadOnlyEntity::fromEntity(DefaultProxyClassNameResolver::getClass($entity));
}
}
+27
View File
@@ -13,6 +13,7 @@ use Doctrine\Common\Cache\Cache as CacheDriver;
use Doctrine\Common\Cache\Psr6\CacheAdapter;
use Doctrine\Common\Cache\Psr6\DoctrineProvider;
use Doctrine\Common\Persistence\PersistentObject;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Cache\CacheConfiguration;
use Doctrine\ORM\Cache\Exception\CacheException;
@@ -27,6 +28,7 @@ use Doctrine\ORM\Exception\NotSupported;
use Doctrine\ORM\Exception\ProxyClassesAlwaysRegenerating;
use Doctrine\ORM\Exception\UnknownEntityNamespace;
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\Mapping\DefaultEntityListenerResolver;
use Doctrine\ORM\Mapping\DefaultNamingStrategy;
@@ -68,6 +70,21 @@ class Configuration extends \Doctrine\DBAL\Configuration
/** @var mixed[] */
protected $_attributes = [];
/** @psalm-var array<class-string<AbstractPlatform>, ClassMetadata::GENERATOR_TYPE_*> */
private $identityGenerationPreferences = [];
/** @psalm-param array<class-string<AbstractPlatform>, ClassMetadata::GENERATOR_TYPE_*> $value */
public function setIdentityGenerationPreferences(array $value): void
{
$this->identityGenerationPreferences = $value;
}
/** @psalm-return array<class-string<AbstractPlatform>, ClassMetadata::GENERATOR_TYPE_*> $value */
public function getIdentityGenerationPreferences(): array
{
return $this->identityGenerationPreferences;
}
/**
* Sets the directory where Doctrine generates any necessary proxy class files.
*
@@ -1127,4 +1144,14 @@ class Configuration extends \Doctrine\DBAL\Configuration
{
return $this->_attributes['rejectIdCollisionInIdentityMap'] ?? false;
}
public function setEagerFetchBatchSize(int $batchSize = 100): void
{
$this->_attributes['fetchModeSubselectBatchSize'] = $batchSize;
}
public function getEagerFetchBatchSize(): int
{
return $this->_attributes['fetchModeSubselectBatchSize'] ?? 100;
}
}
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Decorator;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\ResultSetMapping;
@@ -170,6 +171,13 @@ abstract class EntityManagerDecorator extends ObjectManagerDecorator implements
*/
public function getPartialReference($entityName, $identifier)
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10987',
'Method %s is deprecated and will be removed in 3.0.',
__METHOD__
);
return $this->wrapped->getPartialReference($entityName, $identifier);
}
+8 -2
View File
@@ -9,7 +9,6 @@ use BadMethodCallException;
use Doctrine\Common\Cache\Psr6\CacheAdapter;
use Doctrine\Common\EventManager;
use Doctrine\Common\Persistence\PersistentObject;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\LockMode;
@@ -24,6 +23,7 @@ use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\Exception\UnrecognizedIdentifierFields;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Proxy\ProxyFactory;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\Query\FilterCollection;
@@ -444,7 +444,7 @@ class EntityManager implements EntityManagerInterface
foreach ($id as $i => $value) {
if (is_object($value)) {
$className = ClassUtils::getClass($value);
$className = DefaultProxyClassNameResolver::getClass($value);
if ($this->metadataFactory->hasMetadataFor($className)) {
$id[$i] = $this->unitOfWork->getSingleIdentifierValue($value);
@@ -571,6 +571,12 @@ class EntityManager implements EntityManagerInterface
*/
public function getPartialReference($entityName, $identifier)
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10987',
'Method %s is deprecated and will be removed in 3.0.',
__METHOD__
);
$class = $this->metadataFactory->getMetadataFor(ltrim($entityName, '\\'));
$entity = $this->unitOfWork->tryGetById($identifier, $class->rootEntityName);
@@ -200,6 +200,8 @@ interface EntityManagerInterface extends ObjectManager
* never be visible to the application (especially not event listeners) as it will
* never be loaded in the first place.
*
* @deprecated 2.7 This method is being removed from the ORM and won't have any replacement
*
* @param string $entityName The name of the entity type.
* @param mixed $identifier The entity identifier.
* @psalm-param class-string<T> $entityName
@@ -1,46 +0,0 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Internal\CommitOrder;
use Doctrine\Deprecations\Deprecation;
/**
* @internal
* @deprecated
*/
final class Edge
{
/**
* @var string
* @readonly
*/
public $from;
/**
* @var string
* @readonly
*/
public $to;
/**
* @var int
* @readonly
*/
public $weight;
public function __construct(string $from, string $to, int $weight)
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10547',
'The %s class is deprecated and will be removed in ORM 3.0',
self::class
);
$this->from = $from;
$this->to = $to;
$this->weight = $weight;
}
}
@@ -1,49 +0,0 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Internal\CommitOrder;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Mapping\ClassMetadata;
/**
* @internal
* @deprecated
*/
final class Vertex
{
/**
* @var string
* @readonly
*/
public $hash;
/**
* @var int
* @psalm-var VertexState::*
*/
public $state = VertexState::NOT_VISITED;
/**
* @var ClassMetadata
* @readonly
*/
public $value;
/** @var array<string, Edge> */
public $dependencyList = [];
public function __construct(string $hash, ClassMetadata $value)
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10547',
'The %s class is deprecated and will be removed in ORM 3.0',
self::class
);
$this->hash = $hash;
$this->value = $value;
}
}
@@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Internal\CommitOrder;
use Doctrine\Deprecations\Deprecation;
/**
* @internal
* @deprecated
*/
final class VertexState
{
public const NOT_VISITED = 0;
public const IN_PROGRESS = 1;
public const VISITED = 2;
private function __construct()
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10547',
'The %s class is deprecated and will be removed in ORM 3.0',
self::class
);
}
}
@@ -1,177 +0,0 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Internal;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Internal\CommitOrder\Edge;
use Doctrine\ORM\Internal\CommitOrder\Vertex;
use Doctrine\ORM\Internal\CommitOrder\VertexState;
use Doctrine\ORM\Mapping\ClassMetadata;
use function array_reverse;
/**
* CommitOrderCalculator implements topological sorting, which is an ordering
* algorithm for directed graphs (DG) and/or directed acyclic graphs (DAG) by
* using a depth-first searching (DFS) to traverse the graph built in memory.
* This algorithm have a linear running time based on nodes (V) and dependency
* between the nodes (E), resulting in a computational complexity of O(V + E).
*
* @deprecated
*/
class CommitOrderCalculator
{
/** @deprecated */
public const NOT_VISITED = VertexState::NOT_VISITED;
/** @deprecated */
public const IN_PROGRESS = VertexState::IN_PROGRESS;
/** @deprecated */
public const VISITED = VertexState::VISITED;
/**
* Matrix of nodes (aka. vertex).
*
* Keys are provided hashes and values are the node definition objects.
*
* @var array<string, Vertex>
*/
private $nodeList = [];
/**
* Volatile variable holding calculated nodes during sorting process.
*
* @psalm-var list<ClassMetadata>
*/
private $sortedNodeList = [];
public function __construct()
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10547',
'The %s class is deprecated and will be removed in ORM 3.0',
self::class
);
}
/**
* Checks for node (vertex) existence in graph.
*
* @param string $hash
*
* @return bool
*/
public function hasNode($hash)
{
return isset($this->nodeList[$hash]);
}
/**
* Adds a new node (vertex) to the graph, assigning its hash and value.
*
* @param string $hash
* @param ClassMetadata $node
*
* @return void
*/
public function addNode($hash, $node)
{
$this->nodeList[$hash] = new Vertex($hash, $node);
}
/**
* Adds a new dependency (edge) to the graph using their hashes.
*
* @param string $fromHash
* @param string $toHash
* @param int $weight
*
* @return void
*/
public function addDependency($fromHash, $toHash, $weight)
{
$this->nodeList[$fromHash]->dependencyList[$toHash]
= new Edge($fromHash, $toHash, $weight);
}
/**
* Return a valid order list of all current nodes.
* The desired topological sorting is the reverse post order of these searches.
*
* {@internal Highly performance-sensitive method.}
*
* @psalm-return list<ClassMetadata>
*/
public function sort()
{
foreach ($this->nodeList as $vertex) {
if ($vertex->state !== VertexState::NOT_VISITED) {
continue;
}
$this->visit($vertex);
}
$sortedList = $this->sortedNodeList;
$this->nodeList = [];
$this->sortedNodeList = [];
return array_reverse($sortedList);
}
/**
* Visit a given node definition for reordering.
*
* {@internal Highly performance-sensitive method.}
*/
private function visit(Vertex $vertex): void
{
$vertex->state = VertexState::IN_PROGRESS;
foreach ($vertex->dependencyList as $edge) {
$adjacentVertex = $this->nodeList[$edge->to];
switch ($adjacentVertex->state) {
case VertexState::VISITED:
// Do nothing, since node was already visited
break;
case VertexState::IN_PROGRESS:
if (
isset($adjacentVertex->dependencyList[$vertex->hash]) &&
$adjacentVertex->dependencyList[$vertex->hash]->weight < $edge->weight
) {
// If we have some non-visited dependencies in the in-progress dependency, we
// need to visit them before adding the node.
foreach ($adjacentVertex->dependencyList as $adjacentEdge) {
$adjacentEdgeVertex = $this->nodeList[$adjacentEdge->to];
if ($adjacentEdgeVertex->state === VertexState::NOT_VISITED) {
$this->visit($adjacentEdgeVertex);
}
}
$adjacentVertex->state = VertexState::VISITED;
$this->sortedNodeList[] = $adjacentVertex->value;
}
break;
case VertexState::NOT_VISITED:
$this->visit($adjacentVertex);
}
}
if ($vertex->state !== VertexState::VISITED) {
$vertex->state = VertexState::VISITED;
$this->sortedNodeList[] = $vertex->value;
}
}
}
@@ -24,6 +24,7 @@ use Doctrine\ORM\Id\UuidGenerator;
use Doctrine\ORM\Mapping\Exception\CannotGenerateIds;
use Doctrine\ORM\Mapping\Exception\InvalidCustomGenerator;
use Doctrine\ORM\Mapping\Exception\UnknownGeneratorType;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory;
use Doctrine\Persistence\Mapping\ClassMetadata as ClassMetadataInterface;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
@@ -38,6 +39,7 @@ use function end;
use function explode;
use function get_class;
use function in_array;
use function is_a;
use function is_subclass_of;
use function str_contains;
use function strlen;
@@ -71,9 +73,17 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
/** @var mixed[] */
private $embeddablesActiveNesting = [];
private const NON_IDENTITY_DEFAULT_STRATEGY = [
'Doctrine\DBAL\Platforms\PostgreSqlPlatform' => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
Platforms\OraclePlatform::class => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
Platforms\PostgreSQLPlatform::class => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
];
/** @return void */
public function setEntityManager(EntityManagerInterface $em)
{
parent::setProxyClassNameResolver(new DefaultProxyClassNameResolver());
$this->em = $em;
}
@@ -630,7 +640,7 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
'https://github.com/doctrine/orm/issues/8850',
<<<'DEPRECATION'
Context: Loading metadata for class %s
Problem: Using the IDENTITY generator strategy with platform "%s" is deprecated and will not be possible in Doctrine ORM 3.0.
Problem: Using identity columns emulated with a sequence is deprecated and will not be possible in Doctrine ORM 3.0.
Solution: Use the SEQUENCE generator strategy instead.
DEPRECATION
,
@@ -725,14 +735,40 @@ DEPRECATION
}
}
/** @psalm-return ClassMetadata::GENERATOR_TYPE_SEQUENCE|ClassMetadata::GENERATOR_TYPE_IDENTITY */
/** @psalm-return ClassMetadata::GENERATOR_TYPE_* */
private function determineIdGeneratorStrategy(AbstractPlatform $platform): int
{
if (
$platform instanceof Platforms\OraclePlatform
|| $platform instanceof Platforms\PostgreSQLPlatform
) {
return ClassMetadata::GENERATOR_TYPE_SEQUENCE;
assert($this->em !== null);
foreach ($this->em->getConfiguration()->getIdentityGenerationPreferences() as $platformFamily => $strategy) {
if (is_a($platform, $platformFamily)) {
return $strategy;
}
}
foreach (self::NON_IDENTITY_DEFAULT_STRATEGY as $platformFamily => $strategy) {
if (is_a($platform, $platformFamily)) {
if ($platform instanceof Platforms\PostgreSQLPlatform || is_a($platform, 'Doctrine\DBAL\Platforms\PostgreSqlPlatform')) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/8893',
<<<'DEPRECATION'
Relying on non-optimal defaults for ID generation is deprecated, and IDENTITY
results in SERIAL, which is not recommended.
Instead, configure identifier generation strategies explicitly through
configuration.
We currently recommend "SEQUENCE" for "%s", so you should use
$configuration->setIdentityGenerationPreferences([
"%s" => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
]);
DEPRECATION
,
$platformFamily,
$platformFamily
);
}
return $strategy;
}
}
if ($platform->supportsIdentityColumns()) {
@@ -30,6 +30,8 @@ use function is_numeric;
/**
* The AnnotationDriver reads the mapping metadata from docblock annotations.
*
* @deprecated This class will be removed in 3.0 without replacement.
*/
class AnnotationDriver extends CompatibilityAnnotationDriver
{
@@ -69,8 +69,7 @@ final class AttributeReader
));
}
return $this->getPropertyAttributes($property)[$attributeName]
?? ($this->isRepeatable($attributeName) ? new RepeatableAttributeCollection() : null);
return $this->getPropertyAttributes($property)[$attributeName] ?? null;
}
/**
@@ -8,6 +8,8 @@ namespace Doctrine\ORM\Mapping;
* Is used to specify an array of native SQL named queries.
* The NamedNativeQueries annotation can be applied to an entity or mapped superclass.
*
* @deprecated Named queries won't be supported in ORM 3.
*
* @Annotation
* @Target("CLASS")
*/
@@ -8,6 +8,8 @@ namespace Doctrine\ORM\Mapping;
* Is used to specify a native SQL named query.
* The NamedNativeQuery annotation can be applied to an entity or mapped superclass.
*
* @deprecated Named queries won't be supported in ORM 3.
*
* @Annotation
* @Target("ANNOTATION")
*/
@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
/**
* @deprecated Named queries won't be supported in ORM 3.
*
* @Annotation
* @Target("CLASS")
*/
+2
View File
@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
/**
* @deprecated Named queries won't be supported in ORM 3.
*
* @Annotation
* @Target("ANNOTATION")
*/
+3 -1
View File
@@ -11,8 +11,10 @@ use function ksort;
/**
* Represents a native SQL query.
*
* @final
*/
final class NativeQuery extends AbstractQuery
class NativeQuery extends AbstractQuery
{
/** @var string */
private $sql;
@@ -15,6 +15,7 @@ use function func_num_args;
use function get_debug_type;
use function gettype;
use function implode;
use function is_scalar;
use function method_exists;
use function reset;
use function spl_object_id;
@@ -261,6 +262,32 @@ EXCEPTION
return new self(sprintf('Entity name must be a string, %s given', get_debug_type($entityName)));
}
/** @param mixed $value */
public static function invalidAutoGenerateMode($value): self
{
return new self(sprintf('Invalid auto generate mode "%s" given.', is_scalar($value) ? (string) $value : get_debug_type($value)));
}
public static function missingPrimaryKeyValue(string $className, string $idField): self
{
return new self(sprintf('Missing value for primary key %s on %s', $idField, $className));
}
public static function proxyDirectoryRequired(): self
{
return new self('You must configure a proxy directory. See docs for details');
}
public static function proxyNamespaceRequired(): self
{
return new self('You must configure a proxy namespace');
}
public static function proxyDirectoryNotWritable(string $proxyDirectory): self
{
return new self(sprintf('Your proxy directory "%s" must be writable', $proxyDirectory));
}
/**
* Helper method to show an object as string.
*
@@ -7,7 +7,6 @@ namespace Doctrine\ORM\Persisters\Entity;
use BackedEnum;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Expr\Comparison;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\LockMode;
use Doctrine\DBAL\Platforms\AbstractPlatform;
@@ -26,6 +25,7 @@ use Doctrine\ORM\Persisters\Exception\InvalidOrientation;
use Doctrine\ORM\Persisters\Exception\UnrecognizedField;
use Doctrine\ORM\Persisters\SqlExpressionVisitor;
use Doctrine\ORM\Persisters\SqlValueVisitor;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Repository\Exception\InvalidFindByCall;
@@ -1264,7 +1264,7 @@ class BasicEntityPersister implements EntityPersister
}
$isAssocToOneInverseSide = $assoc['type'] & ClassMetadata::TO_ONE && ! $assoc['isOwningSide'];
$isAssocFromOneEager = $assoc['type'] !== ClassMetadata::MANY_TO_MANY && $assoc['fetch'] === ClassMetadata::FETCH_EAGER;
$isAssocFromOneEager = $assoc['type'] & ClassMetadata::TO_ONE && $assoc['fetch'] === ClassMetadata::FETCH_EAGER;
if (! ($isAssocFromOneEager || $isAssocToOneInverseSide)) {
continue;
@@ -2028,7 +2028,7 @@ class BasicEntityPersister implements EntityPersister
return [$value->value];
}
$valueClass = ClassUtils::getClass($value);
$valueClass = DefaultProxyClassNameResolver::getClass($value);
if ($this->em->getMetadataFactory()->isTransient($valueClass)) {
return [$value];
-1
View File
@@ -6,7 +6,6 @@ namespace Doctrine\ORM\Proxy;
use Doctrine\Common\Proxy\Autoloader as BaseAutoloader;
/** @deprecated use \Doctrine\Common\Proxy\Autoloader instead */
class Autoloader extends BaseAutoloader
{
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Proxy;
use Doctrine\Persistence\Mapping\ProxyClassNameResolver;
use Doctrine\Persistence\Proxy;
use function get_class;
use function strrpos;
use function substr;
/**
* Class-related functionality for objects that might or not be proxy objects
* at the moment.
*/
final class DefaultProxyClassNameResolver implements ProxyClassNameResolver
{
public function resolveClassName(string $className): string
{
$pos = strrpos($className, '\\' . Proxy::MARKER . '\\');
if ($pos === false) {
return $className;
}
return substr($className, $pos + Proxy::MARKER_LENGTH + 2);
}
/**
* @param object $object
*
* @return class-string
*/
public static function getClass($object): string
{
return (new self())->resolveClassName(get_class($object));
}
}
+292 -64
View File
@@ -9,30 +9,94 @@ use Doctrine\Common\Proxy\AbstractProxyFactory;
use Doctrine\Common\Proxy\Proxy as CommonProxy;
use Doctrine\Common\Proxy\ProxyDefinition;
use Doctrine\Common\Proxy\ProxyGenerator;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityNotFoundException;
use Doctrine\ORM\ORMInvalidArgumentException;
use Doctrine\ORM\Persisters\Entity\EntityPersister;
use Doctrine\ORM\Proxy\Proxy as LegacyProxy;
use Doctrine\ORM\UnitOfWork;
use Doctrine\ORM\Utility\IdentifierFlattener;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\Proxy;
use ReflectionProperty;
use Symfony\Component\VarExporter\ProxyHelper;
use Symfony\Component\VarExporter\VarExporter;
use Throwable;
use function array_combine;
use function array_flip;
use function array_intersect_key;
use function bin2hex;
use function chmod;
use function class_exists;
use function dirname;
use function file_exists;
use function file_put_contents;
use function filemtime;
use function is_bool;
use function is_dir;
use function is_int;
use function is_writable;
use function ltrim;
use function mkdir;
use function preg_match_all;
use function random_bytes;
use function rename;
use function rtrim;
use function str_replace;
use function strpos;
use function strrpos;
use function strtr;
use function substr;
use function uksort;
use function ucfirst;
use const DIRECTORY_SEPARATOR;
use const PHP_VERSION_ID;
/**
* This factory is used to create proxy objects for entities at runtime.
*/
class ProxyFactory extends AbstractProxyFactory
{
/**
* Never autogenerate a proxy and rely that it was generated by some
* process before deployment.
*/
public const AUTOGENERATE_NEVER = 0;
/**
* Always generates a new proxy in every request.
*
* This is only sane during development.
*/
public const AUTOGENERATE_ALWAYS = 1;
/**
* Autogenerate the proxy class when the proxy file does not exist.
*
* This strategy causes a file_exists() call whenever any proxy is used the
* first time in a request.
*/
public const AUTOGENERATE_FILE_NOT_EXISTS = 2;
/**
* Generate the proxy classes using eval().
*
* This strategy is only sane for development, and even then it gives me
* the creeps a little.
*/
public const AUTOGENERATE_EVAL = 3;
/**
* Autogenerate the proxy class when the proxy file does not exist or
* when the proxied file changed.
*
* This strategy causes a file_exists() call whenever any proxy is used the
* first time in a request. When the proxied file is changed, the proxy will
* be updated.
*/
public const AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED = 4;
private const PROXY_CLASS_TEMPLATE = <<<'EOPHP'
<?php
@@ -45,15 +109,6 @@ class <proxyShortClassName> extends \<className> implements \<baseProxyInterface
{
<useLazyGhostTrait>
public function __construct(?\Closure $initializer = null, ?\Closure $cloner = null)
{
if ($cloner !== null) {
return;
}
self::createLazyGhost($initializer, <skippedProperties>, $this);
}
public function __isInitialized(): bool
{
return isset($this->lazyObjectState) && $this->isLazyObjectInitialized();
@@ -73,9 +128,15 @@ EOPHP;
/** @var UnitOfWork The UnitOfWork this factory uses to retrieve persisters */
private $uow;
/** @var string */
private $proxyDir;
/** @var string */
private $proxyNs;
/** @var self::AUTOGENERATE_* */
private $autoGenerate;
/**
* The IdentifierFlattener used for manipulating identifiers
*
@@ -83,8 +144,11 @@ EOPHP;
*/
private $identifierFlattener;
/** @var ProxyDefinition[] */
private $definitions = [];
/** @var array<class-string, Closure> */
private $proxyFactories = [];
/** @var bool */
private $isLazyGhostObjectEnabled = true;
/**
* Initializes a new instance of the <tt>ProxyFactory</tt> class that is
@@ -97,23 +161,40 @@ EOPHP;
*/
public function __construct(EntityManagerInterface $em, $proxyDir, $proxyNs, $autoGenerate = self::AUTOGENERATE_NEVER)
{
$proxyGenerator = new ProxyGenerator($proxyDir, $proxyNs);
if (! $em->getConfiguration()->isLazyGhostObjectEnabled()) {
if (PHP_VERSION_ID >= 80100) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10837/',
'Not enabling lazy ghost objects is deprecated and will not be supported in Doctrine ORM 3.0. Ensure Doctrine\ORM\Configuration::setLazyGhostObjectEnabled(true) is called to enable them.'
);
}
if ($em->getConfiguration()->isLazyGhostObjectEnabled()) {
$proxyGenerator->setPlaceholder('baseProxyInterface', InternalProxy::class);
$proxyGenerator->setPlaceholder('useLazyGhostTrait', Closure::fromCallable([$this, 'generateUseLazyGhostTrait']));
$proxyGenerator->setPlaceholder('skippedProperties', Closure::fromCallable([$this, 'generateSkippedProperties']));
$proxyGenerator->setPlaceholder('serializeImpl', Closure::fromCallable([$this, 'generateSerializeImpl']));
$proxyGenerator->setProxyClassTemplate(self::PROXY_CLASS_TEMPLATE);
} else {
$this->isLazyGhostObjectEnabled = false;
$proxyGenerator = new ProxyGenerator($proxyDir, $proxyNs);
$proxyGenerator->setPlaceholder('baseProxyInterface', LegacyProxy::class);
parent::__construct($proxyGenerator, $em->getMetadataFactory(), $autoGenerate);
}
parent::__construct($proxyGenerator, $em->getMetadataFactory(), $autoGenerate);
if (! $proxyDir) {
throw ORMInvalidArgumentException::proxyDirectoryRequired();
}
if (! $proxyNs) {
throw ORMInvalidArgumentException::proxyNamespaceRequired();
}
if (is_int($autoGenerate) ? $autoGenerate < 0 || $autoGenerate > 4 : ! is_bool($autoGenerate)) {
throw ORMInvalidArgumentException::invalidAutoGenerateMode($autoGenerate);
}
$this->em = $em;
$this->uow = $em->getUnitOfWork();
$this->proxyDir = $proxyDir;
$this->proxyNs = $proxyNs;
$this->autoGenerate = (int) $autoGenerate;
$this->identifierFlattener = new IdentifierFlattener($this->uow, $em->getMetadataFactory());
}
@@ -122,19 +203,57 @@ EOPHP;
*/
public function getProxy($className, array $identifier)
{
$proxy = parent::getProxy($className, $identifier);
if (! $this->em->getConfiguration()->isLazyGhostObjectEnabled()) {
return $proxy;
if (! $this->isLazyGhostObjectEnabled) {
return parent::getProxy($className, $identifier);
}
$initializer = $this->definitions[$className]->initializer;
$proxyFactory = $this->proxyFactories[$className] ?? $this->getProxyFactory($className);
$proxy->__construct(static function (InternalProxy $object) use ($initializer, $proxy): void {
$initializer($object, $proxy);
});
return $proxyFactory($identifier);
}
return $proxy;
/**
* Generates proxy classes for all given classes.
*
* @param ClassMetadata[] $classes The classes (ClassMetadata instances) for which to generate proxies.
* @param string|null $proxyDir The target directory of the proxy classes. If not specified, the
* directory configured on the Configuration of the EntityManager used
* by this factory is used.
*
* @return int Number of generated proxies.
*/
public function generateProxyClasses(array $classes, $proxyDir = null)
{
if (! $this->isLazyGhostObjectEnabled) {
return parent::generateProxyClasses($classes, $proxyDir);
}
$generated = 0;
foreach ($classes as $class) {
if ($this->skipClass($class)) {
continue;
}
$proxyFileName = $this->getProxyFileName($class->getName(), $proxyDir ?: $this->proxyDir);
$proxyClassName = self::generateProxyClassName($class->getName(), $this->proxyNs);
$this->generateProxyClass($class, $proxyFileName, $proxyClassName);
++$generated;
}
return $generated;
}
/**
* {@inheritDoc}
*
* @deprecated ProxyFactory::resetUninitializedProxy() is deprecated and will be removed in version 3.0 of doctrine/orm.
*/
public function resetUninitializedProxy(CommonProxy $proxy)
{
return parent::resetUninitializedProxy($proxy);
}
/**
@@ -149,23 +268,19 @@ EOPHP;
/**
* {@inheritDoc}
*
* @deprecated ProxyFactory::createProxyDefinition() is deprecated and will be removed in version 3.0 of doctrine/orm.
*/
protected function createProxyDefinition($className)
{
$classMetadata = $this->em->getClassMetadata($className);
$entityPersister = $this->uow->getEntityPersister($className);
if ($this->em->getConfiguration()->isLazyGhostObjectEnabled()) {
$initializer = $this->createLazyInitializer($classMetadata, $entityPersister);
$cloner = static function (): void {
};
} else {
$initializer = $this->createInitializer($classMetadata, $entityPersister);
$cloner = $this->createCloner($classMetadata, $entityPersister);
}
$initializer = $this->createInitializer($classMetadata, $entityPersister);
$cloner = $this->createCloner($classMetadata, $entityPersister);
return $this->definitions[$className] = new ProxyDefinition(
ClassUtils::generateProxyClassName($className, $this->proxyNs),
return new ProxyDefinition(
self::generateProxyClassName($className, $this->proxyNs),
$classMetadata->getIdentifierFieldNames(),
$classMetadata->getReflectionProperties(),
$initializer,
@@ -176,6 +291,8 @@ EOPHP;
/**
* Creates a closure capable of initializing a proxy
*
* @deprecated ProxyFactory::createInitializer() is deprecated and will be removed in version 3.0 of doctrine/orm.
*
* @psalm-return Closure(CommonProxy):void
*
* @throws EntityNotFoundException
@@ -241,16 +358,16 @@ EOPHP;
*
* @throws EntityNotFoundException
*/
private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister): Closure
private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure
{
return function (InternalProxy $proxy, InternalProxy $original) use ($entityPersister, $classMetadata): void {
return static function (InternalProxy $proxy, InternalProxy $original) use ($entityPersister, $classMetadata, $identifierFlattener): void {
$identifier = $classMetadata->getIdentifierValues($original);
$entity = $entityPersister->loadById($identifier, $original);
if ($entity === null) {
throw EntityNotFoundException::fromClassNameAndIdentifier(
$classMetadata->getName(),
$this->identifierFlattener->flattenIdentifier($classMetadata, $identifier)
$identifierFlattener->flattenIdentifier($classMetadata, $identifier)
);
}
@@ -265,7 +382,6 @@ EOPHP;
continue;
}
$property->setAccessible(true);
$property->setValue($proxy, $property->getValue($entity));
}
};
@@ -274,6 +390,8 @@ EOPHP;
/**
* Creates a closure capable of finalizing state a cloned proxy
*
* @deprecated ProxyFactory::createCloner() is deprecated and will be removed in version 3.0 of doctrine/orm.
*
* @psalm-return Closure(CommonProxy):void
*
* @throws EntityNotFoundException
@@ -310,25 +428,18 @@ EOPHP;
};
}
private function generateUseLazyGhostTrait(ClassMetadata $class): string
private function getProxyFileName(string $className, string $baseDirectory): string
{
$code = ProxyHelper::generateLazyGhost($class->getReflectionClass());
$code = substr($code, 7 + (int) strpos($code, "\n{"));
$code = substr($code, 0, (int) strpos($code, "\n}"));
$code = str_replace('LazyGhostTrait;', str_replace("\n ", "\n", 'LazyGhostTrait {
initializeLazyObject as __load;
setLazyObjectAsInitialized as public __setInitialized;
isLazyObjectInitialized as private;
createLazyGhost as private;
resetLazyObject as private;
}'), $code);
$baseDirectory = $baseDirectory ?: $this->proxyDir;
return $code;
return rtrim($baseDirectory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . InternalProxy::MARKER
. str_replace('\\', '', $className) . '.php';
}
private function generateSkippedProperties(ClassMetadata $class): string
private function getProxyFactory(string $className): Closure
{
$skippedProperties = [];
$class = $this->em->getClassMetadata($className);
$identifiers = array_flip($class->getIdentifierFieldNames());
$filter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE;
$reflector = $class->getReflectionClass();
@@ -350,11 +461,123 @@ EOPHP;
$reflector = $reflector->getParentClass();
}
uksort($skippedProperties, 'strnatcmp');
$className = $class->getName(); // aliases and case sensitivity
$entityPersister = $this->uow->getEntityPersister($className);
$initializer = $this->createLazyInitializer($class, $entityPersister, $this->identifierFlattener);
$proxyClassName = $this->loadProxyClass($class);
$identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers);
$code = VarExporter::export($skippedProperties);
$code = str_replace(VarExporter::export($class->getName()), 'parent::class', $code);
$code = str_replace("\n", "\n ", $code);
$proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy {
$proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, &$proxy): void {
$initializer($object, $proxy);
}, $skippedProperties);
foreach ($identifierFields as $idField => $reflector) {
if (! isset($identifier[$idField])) {
throw ORMInvalidArgumentException::missingPrimaryKeyValue($className, $idField);
}
$reflector->setValue($proxy, $identifier[$idField]);
}
return $proxy;
}, null, $proxyClassName);
return $this->proxyFactories[$className] = $proxyFactory;
}
private function loadProxyClass(ClassMetadata $class): string
{
$proxyClassName = self::generateProxyClassName($class->getName(), $this->proxyNs);
if (class_exists($proxyClassName, false)) {
return $proxyClassName;
}
if ($this->autoGenerate === self::AUTOGENERATE_EVAL) {
$this->generateProxyClass($class, null, $proxyClassName);
return $proxyClassName;
}
$fileName = $this->getProxyFileName($class->getName(), $this->proxyDir);
switch ($this->autoGenerate) {
case self::AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED:
if (file_exists($fileName) && filemtime($fileName) >= filemtime($class->getReflectionClass()->getFileName())) {
break;
}
// no break
case self::AUTOGENERATE_FILE_NOT_EXISTS:
if (file_exists($fileName)) {
break;
}
// no break
case self::AUTOGENERATE_ALWAYS:
$this->generateProxyClass($class, $fileName, $proxyClassName);
break;
}
require $fileName;
return $proxyClassName;
}
private function generateProxyClass(ClassMetadata $class, ?string $fileName, string $proxyClassName): void
{
$i = strrpos($proxyClassName, '\\');
$placeholders = [
'<className>' => $class->getName(),
'<namespace>' => substr($proxyClassName, 0, $i),
'<proxyShortClassName>' => substr($proxyClassName, 1 + $i),
'<baseProxyInterface>' => InternalProxy::class,
];
preg_match_all('(<([a-zA-Z]+)>)', self::PROXY_CLASS_TEMPLATE, $placeholderMatches);
foreach (array_combine($placeholderMatches[0], $placeholderMatches[1]) as $placeholder => $name) {
$placeholders[$placeholder] ?? $placeholders[$placeholder] = $this->{'generate' . ucfirst($name)}($class);
}
$proxyCode = strtr(self::PROXY_CLASS_TEMPLATE, $placeholders);
if (! $fileName) {
if (! class_exists($proxyClassName)) {
eval(substr($proxyCode, 5));
}
return;
}
$parentDirectory = dirname($fileName);
if (! is_dir($parentDirectory) && ! @mkdir($parentDirectory, 0775, true)) {
throw ORMInvalidArgumentException::proxyDirectoryNotWritable($this->proxyDir);
}
if (! is_writable($parentDirectory)) {
throw ORMInvalidArgumentException::proxyDirectoryNotWritable($this->proxyDir);
}
$tmpFileName = $fileName . '.' . bin2hex(random_bytes(12));
file_put_contents($tmpFileName, $proxyCode);
@chmod($tmpFileName, 0664);
rename($tmpFileName, $fileName);
}
private function generateUseLazyGhostTrait(ClassMetadata $class): string
{
$code = ProxyHelper::generateLazyGhost($class->getReflectionClass());
$code = substr($code, 7 + (int) strpos($code, "\n{"));
$code = substr($code, 0, (int) strpos($code, "\n}"));
$code = str_replace('LazyGhostTrait;', str_replace("\n ", "\n", 'LazyGhostTrait {
initializeLazyObject as __load;
setLazyObjectAsInitialized as public __setInitialized;
isLazyObjectInitialized as private;
createLazyGhost as private;
resetLazyObject as private;
}'), $code);
return $code;
}
@@ -365,7 +588,7 @@ EOPHP;
$properties = $reflector->hasMethod('__serialize') ? 'parent::__serialize()' : '(array) $this';
$code = '$properties = ' . $properties . ';
unset($properties["\0" . self::class . "\0lazyObjectState"], $properties[\'__isCloning\']);
unset($properties["\0" . self::class . "\0lazyObjectState"]);
';
@@ -387,4 +610,9 @@ EOPHP;
return $data;';
}
private static function generateProxyClassName(string $className, string $proxyNamespace): string
{
return rtrim($proxyNamespace, '\\') . '\\' . Proxy::MARKER . '\\' . ltrim($className, '\\');
}
}
+3 -1
View File
@@ -44,8 +44,10 @@ use function stripos;
/**
* A Query object represents a DQL query.
*
* @final
*/
final class Query extends AbstractQuery
class Query extends AbstractQuery
{
/**
* A query object is in CLEAN state when it has NO unparsed/unprocessed DQL parts.
@@ -9,6 +9,10 @@ use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Result;
use Doctrine\DBAL\Types\Type;
use function array_diff;
use function array_keys;
use function array_values;
/**
* Base class for SQL statement executors.
*
@@ -18,12 +22,24 @@ use Doctrine\DBAL\Types\Type;
*/
abstract class AbstractSqlExecutor
{
/** @var list<string>|string */
/**
* @deprecated use $sqlStatements instead
*
* @var list<string>|string
*/
protected $_sqlStatements;
/** @var list<string>|string */
protected $sqlStatements;
/** @var QueryCacheProfile */
protected $queryCacheProfile;
public function __construct()
{
$this->_sqlStatements = &$this->sqlStatements;
}
/**
* Gets the SQL statements that are executed by the executor.
*
@@ -31,21 +47,18 @@ abstract class AbstractSqlExecutor
*/
public function getSqlStatements()
{
return $this->_sqlStatements;
return $this->sqlStatements;
}
/** @return void */
public function setQueryCacheProfile(QueryCacheProfile $qcp)
public function setQueryCacheProfile(QueryCacheProfile $qcp): void
{
$this->queryCacheProfile = $qcp;
}
/**
* Do not use query cache
*
* @return void
*/
public function removeQueryCacheProfile()
public function removeQueryCacheProfile(): void
{
$this->queryCacheProfile = null;
}
@@ -60,4 +73,26 @@ abstract class AbstractSqlExecutor
* @return Result|int
*/
abstract public function execute(Connection $conn, array $params, array $types);
/** @return list<string> */
public function __sleep(): array
{
/* Two reasons for this:
- we do not need to serialize the deprecated property, we can
rebuild the reference to the new property in __wakeup()
- not having the legacy property in the serialized data means the
serialized representation becomes compatible with 3.0.x, meaning
there will not be a deprecation warning about a missing property
when unserializing data */
return array_values(array_diff(array_keys((array) $this), ["\0*\0_sqlStatements"]));
}
public function __wakeup(): void
{
if ($this->_sqlStatements !== null && $this->sqlStatements === null) {
$this->sqlStatements = $this->_sqlStatements;
}
$this->_sqlStatements = &$this->sqlStatements;
}
}
@@ -45,6 +45,8 @@ class MultiTableDeleteExecutor extends AbstractSqlExecutor
*/
public function __construct(AST\Node $AST, $sqlWalker)
{
parent::__construct();
$em = $sqlWalker->getEntityManager();
$conn = $em->getConnection();
$platform = $conn->getDatabasePlatform();
@@ -83,8 +85,8 @@ class MultiTableDeleteExecutor extends AbstractSqlExecutor
// 3. Create and store DELETE statements
$classNames = array_merge($primaryClass->parentClasses, [$primaryClass->name], $primaryClass->subClasses);
foreach (array_reverse($classNames) as $className) {
$tableName = $quoteStrategy->getTableName($em->getClassMetadata($className), $platform);
$this->_sqlStatements[] = 'DELETE FROM ' . $tableName
$tableName = $quoteStrategy->getTableName($em->getClassMetadata($className), $platform);
$this->sqlStatements[] = 'DELETE FROM ' . $tableName
. ' WHERE (' . $idColumnList . ') IN (' . $idSubselect . ')';
}
@@ -117,7 +119,7 @@ class MultiTableDeleteExecutor extends AbstractSqlExecutor
$numDeleted = $conn->executeStatement($this->insertSql, $params, $types);
// Execute DELETE statements
foreach ($this->_sqlStatements as $sql) {
foreach ($this->sqlStatements as $sql) {
$conn->executeStatement($sql);
}
} catch (Throwable $exception) {
@@ -50,6 +50,8 @@ class MultiTableUpdateExecutor extends AbstractSqlExecutor
*/
public function __construct(AST\Node $AST, $sqlWalker)
{
parent::__construct();
$em = $sqlWalker->getEntityManager();
$conn = $em->getConnection();
$platform = $conn->getDatabasePlatform();
@@ -119,7 +121,7 @@ class MultiTableUpdateExecutor extends AbstractSqlExecutor
}
if ($affected) {
$this->_sqlStatements[$i] = $updateSql . ' WHERE (' . $idColumnList . ') IN (' . $idSubselect . ')';
$this->sqlStatements[$i] = $updateSql . ' WHERE (' . $idColumnList . ') IN (' . $idSubselect . ')';
}
}
@@ -163,7 +165,7 @@ class MultiTableUpdateExecutor extends AbstractSqlExecutor
);
// Execute UPDATE statements
foreach ($this->_sqlStatements as $key => $statement) {
foreach ($this->sqlStatements as $key => $statement) {
$paramValues = [];
$paramTypes = [];
@@ -18,7 +18,9 @@ class SingleSelectExecutor extends AbstractSqlExecutor
{
public function __construct(SelectStatement $AST, SqlWalker $sqlWalker)
{
$this->_sqlStatements = $sqlWalker->walkSelectStatement($AST);
parent::__construct();
$this->sqlStatements = $sqlWalker->walkSelectStatement($AST);
}
/**
@@ -28,6 +30,6 @@ class SingleSelectExecutor extends AbstractSqlExecutor
*/
public function execute(Connection $conn, array $params, array $types)
{
return $conn->executeQuery($this->_sqlStatements, $params, $types, $this->queryCacheProfile);
return $conn->executeQuery($this->sqlStatements, $params, $types, $this->queryCacheProfile);
}
}
@@ -22,10 +22,12 @@ class SingleTableDeleteUpdateExecutor extends AbstractSqlExecutor
/** @param SqlWalker $sqlWalker */
public function __construct(AST\Node $AST, $sqlWalker)
{
parent::__construct();
if ($AST instanceof AST\UpdateStatement) {
$this->_sqlStatements = $sqlWalker->walkUpdateStatement($AST);
$this->sqlStatements = $sqlWalker->walkUpdateStatement($AST);
} elseif ($AST instanceof AST\DeleteStatement) {
$this->_sqlStatements = $sqlWalker->walkDeleteStatement($AST);
$this->sqlStatements = $sqlWalker->walkDeleteStatement($AST);
}
}
@@ -40,6 +42,6 @@ class SingleTableDeleteUpdateExecutor extends AbstractSqlExecutor
$conn->ensureConnectedToPrimary();
}
return $conn->executeStatement($this->_sqlStatements, $params, $types);
return $conn->executeStatement($this->sqlStatements, $params, $types);
}
}
@@ -204,6 +204,14 @@ class QueryException extends ORMException
);
}
public static function eagerFetchJoinWithNotAllowed(string $sourceEntity, string $fieldName): QueryException
{
return new self(
'Associations with fetch-mode=EAGER may not be using WITH conditions in
"' . $sourceEntity . '#' . $fieldName . '".'
);
}
public static function iterateWithMixedResultNotAllowed(): QueryException
{
return new self('Iterating a query with mixed results (using scalars) is not supported.');
+3 -1
View File
@@ -1047,7 +1047,9 @@ class SqlWalker implements TreeWalker
}
}
$targetTableJoin = null;
if ($relation['fetch'] === ClassMetadata::FETCH_EAGER && $condExpr !== null) {
throw QueryException::eagerFetchJoinWithNotAllowed($assoc['sourceEntity'], $assoc['fieldName']);
}
// This condition is not checking ClassMetadata::MANY_TO_ONE, because by definition it cannot
// be the owning side and previously we ensured that $assoc is always the owning side of the associations.
@@ -6,6 +6,7 @@ namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -20,6 +21,8 @@ use function sprintf;
*/
class CollectionRegionCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -58,12 +61,7 @@ EOT
);
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
@@ -6,6 +6,7 @@ namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -20,6 +21,8 @@ use function sprintf;
*/
class EntityRegionCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -57,12 +60,7 @@ EOT
);
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use InvalidArgumentException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -18,6 +19,8 @@ use Symfony\Component\Console\Style\SymfonyStyle;
*/
class MetadataCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -31,12 +34,7 @@ EOT
);
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
@@ -9,6 +9,7 @@ use Doctrine\Common\Cache\ClearableCache;
use Doctrine\Common\Cache\FlushableCache;
use Doctrine\Common\Cache\XcacheCache;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use InvalidArgumentException;
use LogicException;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
@@ -28,6 +29,8 @@ use function sprintf;
*/
class QueryCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -55,12 +58,7 @@ EOT
);
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
@@ -6,6 +6,7 @@ namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -20,6 +21,8 @@ use function sprintf;
*/
class QueryRegionCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -56,12 +59,7 @@ EOT
);
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
@@ -10,6 +10,7 @@ use Doctrine\Common\Cache\FlushableCache;
use Doctrine\Common\Cache\XcacheCache;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use InvalidArgumentException;
use LogicException;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
@@ -29,6 +30,8 @@ use function sprintf;
*/
class ResultCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -56,12 +59,7 @@ EOT
);
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\ConvertDoctrine1Schema;
use Doctrine\ORM\Tools\EntityGenerator;
use Doctrine\ORM\Tools\Export\ClassMetadataExporter;
@@ -34,6 +35,8 @@ use const PHP_EOL;
*/
class ConvertDoctrine1SchemaCommand extends Command
{
use CommandCompatibility;
/** @var EntityGenerator|null */
private $entityGenerator = null;
@@ -87,12 +90,7 @@ class ConvertDoctrine1SchemaCommand extends Command
->setHelp('Converts Doctrine 1.x schema into a Doctrine 2.x schema.');
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = new SymfonyStyle($input, $output);
$ui->getErrorStyle()->warning('Command ' . $this->getName() . ' is deprecated and will be removed in Doctrine ORM 3.0.');
@@ -6,6 +6,7 @@ namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\Mapping\Driver\DatabaseDriver;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\Console\MetadataFilter;
use Doctrine\ORM\Tools\DisconnectedClassMetadataFactory;
use Doctrine\ORM\Tools\EntityGenerator;
@@ -37,6 +38,8 @@ use function strtolower;
*/
class ConvertMappingCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -84,12 +87,7 @@ EOT
);
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = new SymfonyStyle($input, $output);
$ui->getErrorStyle()->warning('Command ' . $this->getName() . ' is deprecated and will be removed in Doctrine ORM 3.0.');
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@@ -19,6 +20,8 @@ use Throwable;
*/
class EnsureProductionSettingsCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -29,12 +32,7 @@ class EnsureProductionSettingsCommand extends AbstractEntityManagerCommand
->setHelp('Verify that Doctrine is properly configured for a production environment.');
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$ui->warning('This console command has been deprecated and will be removed in a future version of Doctrine ORM.');
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\Console\MetadataFilter;
use Doctrine\ORM\Tools\DisconnectedClassMetadataFactory;
use Doctrine\ORM\Tools\EntityGenerator;
@@ -28,6 +29,8 @@ use function sprintf;
*/
class GenerateEntitiesCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -67,12 +70,7 @@ EOT
);
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$ui->warning('Command ' . $this->getName() . ' is deprecated and will be removed in Doctrine ORM 3.0.');
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\Console\MetadataFilter;
use InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
@@ -26,6 +27,8 @@ use function sprintf;
*/
class GenerateProxiesCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -38,12 +41,7 @@ class GenerateProxiesCommand extends AbstractEntityManagerCommand
->setHelp('Generates proxy classes for entity classes.');
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\Console\MetadataFilter;
use Doctrine\ORM\Tools\EntityRepositoryGenerator;
use InvalidArgumentException;
@@ -27,6 +28,8 @@ use function sprintf;
*/
class GenerateRepositoriesCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -39,12 +42,7 @@ class GenerateRepositoriesCommand extends AbstractEntityManagerCommand
->setHelp('Generate repository classes from your mapping information.');
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$ui->warning('Command ' . $this->getName() . ' is deprecated and will be removed in Doctrine ORM 3.0.');
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@@ -20,6 +21,8 @@ use function sprintf;
*/
class InfoCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -34,12 +37,7 @@ EOT
);
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
@@ -4,7 +4,8 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\Common\Util\Debug;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\Debug;
use LogicException;
use RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
@@ -27,6 +28,8 @@ use function strtoupper;
*/
class RunDqlCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -58,12 +61,7 @@ EOT
);
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = new SymfonyStyle($input, $output);
@@ -118,7 +116,7 @@ EOT
$resultSet = $query->execute([], constant($hydrationMode));
$ui->text(Debug::dump($resultSet, (int) $input->getOption('depth'), true, false));
$ui->text(Debug::dump($resultSet, (int) $input->getOption('depth')));
return 0;
}
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command\SchemaTool;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\SchemaTool;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@@ -17,6 +18,8 @@ use Symfony\Component\Console\Style\SymfonyStyle;
*/
abstract class AbstractCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/**
* @param mixed[] $metadatas
*
@@ -24,12 +27,7 @@ abstract class AbstractCommand extends AbstractEntityManagerCommand
*/
abstract protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui);
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = new SymfonyStyle($input, $output);
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\SchemaValidator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -20,6 +21,8 @@ use function sprintf;
*/
class ValidateSchemaCommand extends AbstractEntityManagerCommand
{
use CommandCompatibility;
/** @return void */
protected function configure()
{
@@ -31,12 +34,7 @@ class ValidateSchemaCommand extends AbstractEntityManagerCommand
->setHelp('Validate that the mapping files are correct and in sync with the database.');
}
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
private function doExecute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console;
use ReflectionMethod;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
if ((new ReflectionMethod(Command::class, 'execute'))->hasReturnType()) {
/** @internal */
trait CommandCompatibility
{
protected function execute(InputInterface $input, OutputInterface $output): int
{
return $this->doExecute($input, $output);
}
}
} else {
/** @internal */
trait CommandCompatibility
{
/**
* {@inheritDoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
return $this->doExecute($input, $output);
}
}
}
@@ -6,7 +6,34 @@ namespace Doctrine\ORM\Tools\Console\Helper;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use ReflectionMethod;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Helper\HelperInterface;
if ((new ReflectionMethod(HelperInterface::class, 'getName'))->hasReturnType()) {
/** @internal */
trait EntityManagerHelperCompatibility
{
public function getName(): string
{
return 'entityManager';
}
}
} else {
/** @internal */
trait EntityManagerHelperCompatibility
{
/**
* {@inheritDoc}
*
* @return string
*/
public function getName()
{
return 'entityManager';
}
}
}
/**
* Doctrine CLI Connection Helper.
@@ -15,6 +42,8 @@ use Symfony\Component\Console\Helper\Helper;
*/
class EntityManagerHelper extends Helper
{
use EntityManagerHelperCompatibility;
/**
* Doctrine ORM EntityManagerInterface.
*
@@ -43,14 +72,4 @@ class EntityManagerHelper extends Helper
{
return $this->_em;
}
/**
* {@inheritDoc}
*
* @return string
*/
public function getName()
{
return 'entityManager';
}
}
+168
View File
@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools;
use ArrayIterator;
use ArrayObject;
use DateTimeInterface;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\Persistence\Proxy;
use stdClass;
use function array_keys;
use function count;
use function end;
use function explode;
use function extension_loaded;
use function get_class;
use function html_entity_decode;
use function ini_get;
use function ini_set;
use function is_array;
use function is_object;
use function ob_end_clean;
use function ob_get_contents;
use function ob_start;
use function strip_tags;
use function var_dump;
/**
* Static class containing most used debug methods.
*
* @internal
*
* @link www.doctrine-project.org
*/
final class Debug
{
/**
* Private constructor (prevents instantiation).
*/
private function __construct()
{
}
/**
* Prints a dump of the public, protected and private properties of $var.
*
* @link https://xdebug.org/
*
* @param mixed $var The variable to dump.
* @param int $maxDepth The maximum nesting level for object properties.
*/
public static function dump($var, int $maxDepth = 2): string
{
$html = ini_get('html_errors');
if ($html !== '1') {
ini_set('html_errors', 'on');
}
if (extension_loaded('xdebug')) {
$previousDepth = ini_get('xdebug.var_display_max_depth');
ini_set('xdebug.var_display_max_depth', (string) $maxDepth);
}
try {
$var = self::export($var, $maxDepth);
ob_start();
var_dump($var);
$dump = ob_get_contents();
ob_end_clean();
$dumpText = strip_tags(html_entity_decode($dump));
} finally {
ini_set('html_errors', $html);
if (isset($previousDepth)) {
ini_set('xdebug.var_display_max_depth', $previousDepth);
}
}
return $dumpText;
}
/**
* @param mixed $var
*
* @return mixed
*/
public static function export($var, int $maxDepth)
{
if ($var instanceof Collection) {
$var = $var->toArray();
}
if (! $maxDepth) {
return is_object($var) ? get_class($var)
: (is_array($var) ? 'Array(' . count($var) . ')' : $var);
}
if (is_array($var)) {
$return = [];
foreach ($var as $k => $v) {
$return[$k] = self::export($v, $maxDepth - 1);
}
return $return;
}
if (! is_object($var)) {
return $var;
}
$return = new stdClass();
if ($var instanceof DateTimeInterface) {
$return->__CLASS__ = get_class($var);
$return->date = $var->format('c');
$return->timezone = $var->getTimezone()->getName();
return $return;
}
$return->__CLASS__ = DefaultProxyClassNameResolver::getClass($var);
if ($var instanceof Proxy) {
$return->__IS_PROXY__ = true;
$return->__PROXY_INITIALIZED__ = $var->__isInitialized();
}
if ($var instanceof ArrayObject || $var instanceof ArrayIterator) {
$return->__STORAGE__ = self::export($var->getArrayCopy(), $maxDepth - 1);
}
return self::fillReturnWithClassAttributes($var, $return, $maxDepth);
}
/**
* Fill the $return variable with class attributes
* Based on obj2array function from {@see https://secure.php.net/manual/en/function.get-object-vars.php#47075}
*
* @param object $var
*
* @return mixed
*/
private static function fillReturnWithClassAttributes($var, stdClass $return, int $maxDepth)
{
$clone = (array) $var;
foreach (array_keys($clone) as $key) {
$aux = explode("\0", (string) $key);
$name = end($aux);
if ($aux[0] === '') {
$name .= ':' . ($aux[1] === '*' ? 'protected' : $aux[1] . ':private');
}
$return->$name = self::export($clone[$key], $maxDepth - 1);
}
return $return;
}
}
@@ -15,7 +15,6 @@ use Doctrine\ORM\Mapping\QuoteStrategy;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\AST\OrderByClause;
use Doctrine\ORM\Query\AST\PartialObjectExpression;
use Doctrine\ORM\Query\AST\PathExpression;
use Doctrine\ORM\Query\AST\SelectExpression;
use Doctrine\ORM\Query\AST\SelectStatement;
@@ -335,7 +334,7 @@ class LimitSubqueryOutputWalker extends SqlWalker
// Add select items which were not excluded to the AST's select clause.
foreach ($selects as $idVar => $fields) {
$AST->selectClause->selectExpressions[] = new SelectExpression(new PartialObjectExpression($idVar, array_keys($fields)), null, true);
$AST->selectClause->selectExpressions[] = new SelectExpression($idVar, null, true);
}
}
@@ -374,8 +373,7 @@ class LimitSubqueryOutputWalker extends SqlWalker
string $innerSql
): string {
[$searchPatterns, $replacements] = $this->generateSqlAliasReplacements();
$orderByItems = [];
$orderByItems = [];
foreach ($orderByClause->orderByItems as $orderByItem) {
// Walk order by item to get string representation of it and
@@ -50,12 +50,14 @@ class LimitSubqueryWalker extends TreeWalkerAdapter
throw new RuntimeException('Paginating an entity with foreign key as identifier only works when using the Output Walkers. Call Paginator#setUseOutputWalkers(true) before iterating the paginator.');
}
$this->_getQuery()->setHint(
$query = $this->_getQuery();
$query->setHint(
self::IDENTIFIER_TYPE,
Type::getType($rootClass->fieldMappings[$identifier]['type'])
);
$this->_getQuery()->setHint(self::FORCE_DBAL_TYPE_CONVERSION, true);
$query->setHint(self::FORCE_DBAL_TYPE_CONVERSION, true);
$pathExpression = new PathExpression(
PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION,
@@ -66,7 +68,7 @@ class LimitSubqueryWalker extends TreeWalkerAdapter
$pathExpression->type = PathExpression::TYPE_STATE_FIELD;
$AST->selectClause->selectExpressions = [new SelectExpression($pathExpression, '_dctrn_id')];
$AST->selectClause->isDistinct = true;
$AST->selectClause->isDistinct = ($query->getHints()[Paginator::HINT_ENABLE_DISTINCT] ?? true) === true;
if (! isset($AST->orderByClause)) {
return;
@@ -34,6 +34,8 @@ class Paginator implements Countable, IteratorAggregate
{
use SQLResultCasing;
public const HINT_ENABLE_DISTINCT = 'paginator.distinct.enable';
/** @var Query */
private $query;
+107
View File
@@ -4,33 +4,73 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools;
use Doctrine\DBAL\Types\AsciiStringType;
use Doctrine\DBAL\Types\BigIntType;
use Doctrine\DBAL\Types\BooleanType;
use Doctrine\DBAL\Types\DecimalType;
use Doctrine\DBAL\Types\FloatType;
use Doctrine\DBAL\Types\GuidType;
use Doctrine\DBAL\Types\IntegerType;
use Doctrine\DBAL\Types\JsonType;
use Doctrine\DBAL\Types\SimpleArrayType;
use Doctrine\DBAL\Types\SmallIntType;
use Doctrine\DBAL\Types\StringType;
use Doctrine\DBAL\Types\TextType;
use Doctrine\DBAL\Types\Type;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use ReflectionNamedType;
use function array_diff;
use function array_filter;
use function array_key_exists;
use function array_map;
use function array_push;
use function array_search;
use function array_values;
use function assert;
use function class_exists;
use function class_parents;
use function count;
use function get_class;
use function implode;
use function in_array;
use function sprintf;
use const PHP_VERSION_ID;
/**
* Performs strict validation of the mapping schema
*
* @link www.doctrine-project.com
*
* @psalm-import-type FieldMapping from ClassMetadata
*/
class SchemaValidator
{
/** @var EntityManagerInterface */
private $em;
/**
* It maps built-in Doctrine types to PHP types
*/
private const BUILTIN_TYPES_MAP = [
AsciiStringType::class => 'string',
BigIntType::class => 'string',
BooleanType::class => 'bool',
DecimalType::class => 'string',
FloatType::class => 'float',
GuidType::class => 'string',
IntegerType::class => 'int',
JsonType::class => 'array',
SimpleArrayType::class => 'array',
SmallIntType::class => 'int',
StringType::class => 'string',
TextType::class => 'string',
];
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
@@ -92,6 +132,11 @@ class SchemaValidator
}
}
// PHP 7.4 introduces the ability to type properties, so we can't validate them in previous versions
if (PHP_VERSION_ID >= 70400) {
array_push($ce, ...$this->validatePropertiesTypes($class));
}
if ($class->isEmbeddedClass && count($class->associationMappings) > 0) {
$ce[] = "Embeddable '" . $class->name . "' does not support associations";
@@ -304,4 +349,66 @@ class SchemaValidator
return $schemaTool->getUpdateSchemaSql($allMetadata, true);
}
/** @return list<string> containing the found issues */
private function validatePropertiesTypes(ClassMetadataInfo $class): array
{
return array_values(
array_filter(
array_map(
/** @param FieldMapping $fieldMapping */
function (array $fieldMapping) use ($class): ?string {
$fieldName = $fieldMapping['fieldName'];
assert(isset($class->reflFields[$fieldName]));
$propertyType = $class->reflFields[$fieldName]->getType();
// If the field type is not a built-in type, we cannot check it
if (! Type::hasType($fieldMapping['type'])) {
return null;
}
// If the property type is not a named type, we cannot check it
if (! ($propertyType instanceof ReflectionNamedType)) {
return null;
}
$metadataFieldType = $this->findBuiltInType(Type::getType($fieldMapping['type']));
//If the metadata field type is not a mapped built-in type, we cannot check it
if ($metadataFieldType === null) {
return null;
}
$propertyType = $propertyType->getName();
// If the property type is the same as the metadata field type, we are ok
if ($propertyType === $metadataFieldType) {
return null;
}
return sprintf(
"The field '%s#%s' has the property type '%s' that differs from the metadata field type '%s' returned by the '%s' DBAL type.",
$class->name,
$fieldName,
$propertyType,
$metadataFieldType,
$fieldMapping['type']
);
},
$class->fieldMappings
)
)
);
}
/**
* The exact DBAL type must be used (no subclasses), since consumers of doctrine/orm may have their own
* customization around field types.
*/
private function findBuiltInType(Type $type): ?string
{
$typeName = get_class($type);
return self::BUILTIN_TYPES_MAP[$typeName] ?? null;
}
}
+108 -25
View File
@@ -27,7 +27,6 @@ use Doctrine\ORM\Exception\EntityIdentityCollisionException;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\Exception\UnexpectedAssociationValue;
use Doctrine\ORM\Id\AssignedGenerator;
use Doctrine\ORM\Internal\CommitOrderCalculator;
use Doctrine\ORM\Internal\HydrationCompleteHandler;
use Doctrine\ORM\Internal\TopologicalSort;
use Doctrine\ORM\Mapping\ClassMetadata;
@@ -52,6 +51,7 @@ use RuntimeException;
use Throwable;
use UnexpectedValueException;
use function array_chunk;
use function array_combine;
use function array_diff_key;
use function array_filter;
@@ -315,6 +315,9 @@ class UnitOfWork implements PropertyChangedListener
*/
private $eagerLoadingEntities = [];
/** @var array<string, array<string, mixed>> */
private $eagerLoadingCollections = [];
/** @var bool */
protected $hasCache = false;
@@ -1682,11 +1685,11 @@ IDs should uniquely map to entity object instances. This problem may occur if:
clearing the EntityManager;
- you might have been using EntityManager#getReference() to create a reference
for a nonexistent ID that was subsequently (by the RDBMS) assigned to another
entity.
entity.
Otherwise, it might be an ORM-internal inconsistency, please report it.
To opt-in to the new exception, call
To opt-in to the new exception, call
\Doctrine\ORM\Configuration::setRejectIdCollisionInIdentityMap on the entity
manager's configuration.
EXCEPTION
@@ -2721,16 +2724,6 @@ EXCEPTION
}
}
/**
* Gets the CommitOrderCalculator used by the UnitOfWork to order commits.
*
* @return CommitOrderCalculator
*/
public function getCommitOrderCalculator()
{
return new Internal\CommitOrderCalculator();
}
/**
* Clears the UnitOfWork.
*
@@ -2760,6 +2753,7 @@ EXCEPTION
$this->pendingCollectionElementRemovals =
$this->visitedCollections =
$this->eagerLoadingEntities =
$this->eagerLoadingCollections =
$this->orphanRemovals = [];
} else {
Deprecation::triggerIfCalledFromOutside(
@@ -2949,6 +2943,10 @@ EXCEPTION
continue;
}
if (! isset($hints['fetchMode'][$class->name][$field])) {
$hints['fetchMode'][$class->name][$field] = $assoc['fetch'];
}
$targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
switch (true) {
@@ -3012,10 +3010,6 @@ EXCEPTION
break;
}
if (! isset($hints['fetchMode'][$class->name][$field])) {
$hints['fetchMode'][$class->name][$field] = $assoc['fetch'];
}
// Foreign key is set
// Check identity map first
// FIXME: Can break easily with composite keys if join column values are in
@@ -3109,9 +3103,13 @@ EXCEPTION
$reflField = $class->reflFields[$field];
$reflField->setValue($entity, $pColl);
if ($assoc['fetch'] === ClassMetadata::FETCH_EAGER) {
$this->loadCollection($pColl);
$pColl->takeSnapshot();
if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER) {
if ($assoc['type'] === ClassMetadata::ONE_TO_MANY) {
$this->scheduleCollectionForBatchLoading($pColl, $class);
} elseif ($assoc['type'] === ClassMetadata::MANY_TO_MANY) {
$this->loadCollection($pColl);
$pColl->takeSnapshot();
}
}
$this->originalEntityData[$oid][$field] = $pColl;
@@ -3128,7 +3126,7 @@ EXCEPTION
/** @return void */
public function triggerEagerLoads()
{
if (! $this->eagerLoadingEntities) {
if (! $this->eagerLoadingEntities && ! $this->eagerLoadingCollections) {
return;
}
@@ -3141,11 +3139,69 @@ EXCEPTION
continue;
}
$class = $this->em->getClassMetadata($entityName);
$class = $this->em->getClassMetadata($entityName);
$batches = array_chunk($ids, $this->em->getConfiguration()->getEagerFetchBatchSize());
$this->getEntityPersister($entityName)->loadAll(
array_combine($class->identifier, [array_values($ids)])
);
foreach ($batches as $batchedIds) {
$this->getEntityPersister($entityName)->loadAll(
array_combine($class->identifier, [$batchedIds])
);
}
}
$eagerLoadingCollections = $this->eagerLoadingCollections; // avoid recursion
$this->eagerLoadingCollections = [];
foreach ($eagerLoadingCollections as $group) {
$this->eagerLoadCollections($group['items'], $group['mapping']);
}
}
/**
* Load all data into the given collections, according to the specified mapping
*
* @param PersistentCollection[] $collections
* @param array<string, mixed> $mapping
* @psalm-param array{targetEntity: class-string, sourceEntity: class-string, mappedBy: string, indexBy: string|null} $mapping
*/
private function eagerLoadCollections(array $collections, array $mapping): void
{
$targetEntity = $mapping['targetEntity'];
$class = $this->em->getClassMetadata($mapping['sourceEntity']);
$mappedBy = $mapping['mappedBy'];
$batches = array_chunk($collections, $this->em->getConfiguration()->getEagerFetchBatchSize(), true);
foreach ($batches as $collectionBatch) {
$entities = [];
foreach ($collectionBatch as $collection) {
$entities[] = $collection->getOwner();
}
$found = $this->getEntityPersister($targetEntity)->loadAll([$mappedBy => $entities]);
$targetClass = $this->em->getClassMetadata($targetEntity);
$targetProperty = $targetClass->getReflectionProperty($mappedBy);
foreach ($found as $targetValue) {
$sourceEntity = $targetProperty->getValue($targetValue);
$id = $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($sourceEntity));
$idHash = implode(' ', $id);
if (isset($mapping['indexBy'])) {
$indexByProperty = $targetClass->getReflectionProperty($mapping['indexBy']);
$collectionBatch[$idHash]->hydrateSet($indexByProperty->getValue($targetValue), $targetValue);
} else {
$collectionBatch[$idHash]->add($targetValue);
}
}
}
foreach ($collections as $association) {
$association->setInitialized(true);
$association->takeSnapshot();
}
}
@@ -3176,6 +3232,33 @@ EXCEPTION
$collection->setInitialized(true);
}
/**
* Schedule this collection for batch loading at the end of the UnitOfWork
*/
private function scheduleCollectionForBatchLoading(PersistentCollection $collection, ClassMetadata $sourceClass): void
{
$mapping = $collection->getMapping();
$name = $mapping['sourceEntity'] . '#' . $mapping['fieldName'];
if (! isset($this->eagerLoadingCollections[$name])) {
$this->eagerLoadingCollections[$name] = [
'items' => [],
'mapping' => $mapping,
];
}
$owner = $collection->getOwner();
assert($owner !== null);
$id = $this->identifierFlattener->flattenIdentifier(
$sourceClass,
$sourceClass->getIdentifierValues($owner)
);
$idHash = implode(' ', $id);
$this->eagerLoadingCollections[$name]['items'][$idHash] = $collection;
}
/**
* Gets the identity map of the UnitOfWork.
*
+14 -1
View File
@@ -49,11 +49,19 @@
<rule ref="PSR1.Classes.ClassDeclaration.MultipleClasses">
<exclude-pattern>lib/Doctrine/ORM/Mapping/Driver/CompatibilityAnnotationDriver.php</exclude-pattern>
<exclude-pattern>lib/Doctrine/ORM/Tools/Console/CommandCompatibility.php</exclude-pattern>
<exclude-pattern>lib/Doctrine/ORM/Tools/Console/Helper/EntityManagerHelper.php</exclude-pattern>
<exclude-pattern>tests/*</exclude-pattern>
</rule>
<rule ref="Squiz.Classes.ClassFileName.NoMatch">
<exclude-pattern>*/tests/*</exclude-pattern>
<exclude-pattern>lib/Doctrine/ORM/Tools/Console/Helper/EntityManagerHelper.php</exclude-pattern>
<exclude-pattern>tests/*</exclude-pattern>
</rule>
<rule ref="Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps">
<exclude-pattern>lib/Doctrine/ORM/Tools/Debug.php</exclude-pattern>
<exclude-pattern>tests/Doctrine/Tests/ORM/Tools/DebugTest.php</exclude-pattern>
</rule>
<rule ref="Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase">
@@ -192,6 +200,11 @@
<exclude-pattern>tests/Doctrine/Tests/ORM/Functional/Ticket/DDC832Test.php</exclude-pattern>
</rule>
<rule ref="Squiz.Classes.ValidClassName.NotCamelCaps">
<!-- we need to test what happens with an stdClass proxy -->
<exclude-pattern>tests/Doctrine/Tests/Proxy/DefaultProxyClassNameResolverTest.php</exclude-pattern>
</rule>
<rule ref="Squiz.Commenting.FunctionComment.WrongStyle">
<!-- https://github.com/squizlabs/PHP_CodeSniffer/issues/1961 -->
<exclude-pattern>tests/Doctrine/Tests/Mocks/DatabasePlatformMock.php</exclude-pattern>
+17 -2
View File
@@ -286,12 +286,22 @@ parameters:
path: lib/Doctrine/ORM/Proxy/ProxyFactory.php
-
message: "#^Call to an undefined method Doctrine\\\\Common\\\\Proxy\\\\Proxy\\:\\:__construct\\(\\)\\.$#"
message: "#^Call to an undefined method Doctrine\\\\Common\\\\Proxy\\\\Proxy\\:\\:__wakeup\\(\\)\\.$#"
count: 1
path: lib/Doctrine/ORM/Proxy/ProxyFactory.php
-
message: "#^Call to an undefined method Doctrine\\\\Common\\\\Proxy\\\\Proxy\\:\\:__wakeup\\(\\)\\.$#"
message: "#^Call to an undefined static method Doctrine\\\\ORM\\\\Proxy\\\\ProxyFactory\\:\\:createLazyGhost\\(\\)\\.$#"
count: 1
path: lib/Doctrine/ORM/Proxy/ProxyFactory.php
-
message: "#^Comparison operation \"\\<\" between 0\\|1\\|2\\|3\\|4 and 0 is always false\\.$#"
count: 1
path: lib/Doctrine/ORM/Proxy/ProxyFactory.php
-
message: "#^Comparison operation \"\\>\" between 0\\|1\\|2\\|3\\|4 and 4 is always false\\.$#"
count: 1
path: lib/Doctrine/ORM/Proxy/ProxyFactory.php
@@ -300,6 +310,11 @@ parameters:
count: 3
path: lib/Doctrine/ORM/Proxy/ProxyFactory.php
-
message: "#^Result of \\|\\| is always false\\.$#"
count: 1
path: lib/Doctrine/ORM/Proxy/ProxyFactory.php
-
message: "#^Parameter \\#2 \\$sqlParams of method Doctrine\\\\ORM\\\\Query\\:\\:evictResultSetCache\\(\\) expects array\\<string, mixed\\>, array\\<int, mixed\\> given\\.$#"
count: 1
+5
View File
@@ -54,6 +54,11 @@ parameters:
count: 1
path: lib/Doctrine/ORM/Query/AST/Functions/SubstringFunction.php
-
message: '#^Class Doctrine\\DBAL\\Platforms\\MySQLPlatform not found\.$#'
count: 2
path: lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
# Symfony cache supports passing a key prefix to the clear method.
- '/^Method Psr\\Cache\\CacheItemPoolInterface\:\:clear\(\) invoked with 1 parameter, 0 required\.$/'
+77 -30
View File
@@ -400,12 +400,6 @@
<code>getTableHiLoUpdateNextValSql</code>
</UndefinedMethod>
</file>
<file src="lib/Doctrine/ORM/Internal/CommitOrderCalculator.php">
<RedundantCondition>
<code><![CDATA[$vertex->state !== VertexState::VISITED]]></code>
<code><![CDATA[$vertex->state !== VertexState::VISITED]]></code>
</RedundantCondition>
</file>
<file src="lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php">
<DeprecatedClass>
<code>IterableResult</code>
@@ -542,6 +536,8 @@
<ArgumentTypeCoercion>
<code>$class</code>
<code>$class</code>
<code>$platformFamily</code>
<code><![CDATA['Doctrine\DBAL\Platforms\PostgreSqlPlatform']]></code>
<code><![CDATA[new $definition['class']()]]></code>
</ArgumentTypeCoercion>
<DeprecatedClass>
@@ -1371,38 +1367,74 @@
<code>$columnList</code>
</PossiblyUndefinedVariable>
</file>
<file src="lib/Doctrine/ORM/Proxy/DefaultProxyClassNameResolver.php">
<LessSpecificReturnStatement>
<code>$className</code>
<code>substr($className, $pos + Proxy::MARKER_LENGTH + 2)</code>
</LessSpecificReturnStatement>
<MoreSpecificReturnType>
<code>string</code>
</MoreSpecificReturnType>
</file>
<file src="lib/Doctrine/ORM/Proxy/ProxyFactory.php">
<ArgumentTypeCoercion>
<code>$classMetadata</code>
<code>$classMetadata</code>
<code>$classMetadata</code>
</ArgumentTypeCoercion>
<DirectConstructorCall>
<code><![CDATA[$proxy->__construct(static function (InternalProxy $object) use ($initializer, $proxy): void {
$initializer($object, $proxy);
})]]></code>
</DirectConstructorCall>
<DeprecatedMethod>
<code>createCloner</code>
<code>createInitializer</code>
</DeprecatedMethod>
<InvalidArgument>
<code><![CDATA[$classMetadata->getReflectionProperties()]]></code>
<code><![CDATA[$em->getMetadataFactory()]]></code>
<code><![CDATA[$em->getMetadataFactory()]]></code>
</InvalidArgument>
<InvalidNullableReturnType>
<code>Closure</code>
</InvalidNullableReturnType>
<InvalidPropertyAssignmentValue>
<code><![CDATA[$this->proxyFactories]]></code>
</InvalidPropertyAssignmentValue>
<NoInterfaceProperties>
<code><![CDATA[$metadata->isEmbeddedClass]]></code>
<code><![CDATA[$metadata->isMappedSuperclass]]></code>
</NoInterfaceProperties>
<NullableReturnStatement>
<code><![CDATA[$this->proxyFactories[$className] = $proxyFactory]]></code>
</NullableReturnStatement>
<PossiblyFalseArgument>
<code>$i</code>
</PossiblyFalseArgument>
<PossiblyFalseOperand>
<code>$i</code>
</PossiblyFalseOperand>
<PossiblyNullPropertyFetch>
<code><![CDATA[$property->name]]></code>
<code><![CDATA[$property->name]]></code>
</PossiblyNullPropertyFetch>
<PossiblyNullReference>
<code>getValue</code>
<code>setAccessible</code>
<code>setAccessible</code>
<code>setValue</code>
<code>setValue</code>
</PossiblyNullReference>
<TypeDoesNotContainType>
<code><![CDATA[$autoGenerate < 0]]></code>
<code><![CDATA[$autoGenerate > 4]]></code>
</TypeDoesNotContainType>
<UndefinedInterfaceMethod>
<code>__construct</code>
<code>__wakeup</code>
</UndefinedInterfaceMethod>
<UndefinedMethod>
<code><![CDATA[self::createLazyGhost(static function (InternalProxy $object) use ($initializer, &$proxy): void {
$initializer($object, $proxy);
}, $skippedProperties)]]></code>
</UndefinedMethod>
<UnresolvableInclude>
<code>require $fileName</code>
</UnresolvableInclude>
</file>
<file src="lib/Doctrine/ORM/Query.php">
<DeprecatedClass>
@@ -1813,9 +1845,32 @@
</ParamNameMismatch>
</file>
<file src="lib/Doctrine/ORM/Query/Exec/AbstractSqlExecutor.php">
<DeprecatedProperty>
<code><![CDATA[$this->_sqlStatements]]></code>
<code><![CDATA[$this->_sqlStatements]]></code>
</DeprecatedProperty>
<DocblockTypeContradiction>
<code><![CDATA[$this->_sqlStatements !== null && $this->sqlStatements === null]]></code>
<code><![CDATA[$this->sqlStatements === null]]></code>
</DocblockTypeContradiction>
<PossiblyNullPropertyAssignmentValue>
<code>null</code>
</PossiblyNullPropertyAssignmentValue>
<PropertyNotSetInConstructor>
<code>$_sqlStatements</code>
<code>$queryCacheProfile</code>
<code>$sqlStatements</code>
</PropertyNotSetInConstructor>
<RedundantConditionGivenDocblockType>
<code><![CDATA[$this->_sqlStatements !== null]]></code>
</RedundantConditionGivenDocblockType>
<UninitializedProperty>
<code><![CDATA[$this->sqlStatements]]></code>
</UninitializedProperty>
<UnsupportedPropertyReferenceUsage>
<code><![CDATA[$this->_sqlStatements = &$this->sqlStatements]]></code>
<code><![CDATA[$this->_sqlStatements = &$this->sqlStatements]]></code>
</UnsupportedPropertyReferenceUsage>
</file>
<file src="lib/Doctrine/ORM/Query/Exec/MultiTableDeleteExecutor.php">
<InvalidReturnStatement>
@@ -1825,15 +1880,12 @@
<code>int</code>
</InvalidReturnType>
<PossiblyInvalidIterator>
<code><![CDATA[$this->_sqlStatements]]></code>
<code><![CDATA[$this->sqlStatements]]></code>
</PossiblyInvalidIterator>
<PropertyNotSetInConstructor>
<code>MultiTableDeleteExecutor</code>
<code>MultiTableDeleteExecutor</code>
</PropertyNotSetInConstructor>
<UninitializedProperty>
<code><![CDATA[$this->_sqlStatements]]></code>
</UninitializedProperty>
</file>
<file src="lib/Doctrine/ORM/Query/Exec/MultiTableUpdateExecutor.php">
<InvalidReturnStatement>
@@ -1843,40 +1895,40 @@
<code>int</code>
</InvalidReturnType>
<PossiblyInvalidIterator>
<code><![CDATA[$this->_sqlStatements]]></code>
<code><![CDATA[$this->sqlStatements]]></code>
</PossiblyInvalidIterator>
<PropertyNotSetInConstructor>
<code>MultiTableUpdateExecutor</code>
<code>MultiTableUpdateExecutor</code>
<code>MultiTableUpdateExecutor</code>
</PropertyNotSetInConstructor>
<PropertyTypeCoercion>
<code><![CDATA[$this->_sqlStatements]]></code>
<code><![CDATA[$this->sqlStatements]]></code>
</PropertyTypeCoercion>
<UninitializedProperty>
<code><![CDATA[$this->_sqlStatements]]></code>
</UninitializedProperty>
</file>
<file src="lib/Doctrine/ORM/Query/Exec/SingleSelectExecutor.php">
<PossiblyInvalidArgument>
<code><![CDATA[$this->_sqlStatements]]></code>
<code><![CDATA[$this->sqlStatements]]></code>
</PossiblyInvalidArgument>
<PropertyNotSetInConstructor>
<code>SingleSelectExecutor</code>
<code>SingleSelectExecutor</code>
</PropertyNotSetInConstructor>
</file>
<file src="lib/Doctrine/ORM/Query/Exec/SingleTableDeleteUpdateExecutor.php">
<InvalidReturnStatement>
<code><![CDATA[$conn->executeStatement($this->_sqlStatements, $params, $types)]]></code>
<code><![CDATA[$conn->executeStatement($this->sqlStatements, $params, $types)]]></code>
</InvalidReturnStatement>
<InvalidReturnType>
<code>int</code>
</InvalidReturnType>
<PossiblyInvalidArgument>
<code><![CDATA[$this->_sqlStatements]]></code>
<code><![CDATA[$this->sqlStatements]]></code>
</PossiblyInvalidArgument>
<PropertyNotSetInConstructor>
<code>SingleTableDeleteUpdateExecutor</code>
<code>SingleTableDeleteUpdateExecutor</code>
<code>SingleTableDeleteUpdateExecutor</code>
</PropertyNotSetInConstructor>
</file>
<file src="lib/Doctrine/ORM/Query/Expr.php">
@@ -2372,11 +2424,6 @@
<code>getAllClassNames</code>
</PossiblyNullReference>
</file>
<file src="lib/Doctrine/ORM/Tools/Console/Command/RunDqlCommand.php">
<DeprecatedClass>
<code><![CDATA[Debug::dump($resultSet, (int) $input->getOption('depth'), true, false)]]></code>
</DeprecatedClass>
</file>
<file src="lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/AbstractCommand.php">
<InvalidNullableReturnType>
<code>int</code>
+17 -4
View File
@@ -36,7 +36,12 @@
<referencedClass name="Doctrine\DBAL\Schema\Visitor\RemoveNamespacedAssets"/>
<referencedClass name="Doctrine\ORM\Event\LifecycleEventArgs"/>
<referencedClass name="Doctrine\ORM\Exception\UnknownEntityNamespace"/>
<referencedClass name="Doctrine\ORM\Mapping\Driver\AnnotationDriver"/>
<referencedClass name="Doctrine\ORM\Mapping\Driver\YamlDriver"/>
<referencedClass name="Doctrine\ORM\Mapping\NamedNativeQueries"/>
<referencedClass name="Doctrine\ORM\Mapping\NamedNativeQuery"/>
<referencedClass name="Doctrine\ORM\Mapping\NamedQueries"/>
<referencedClass name="Doctrine\ORM\Mapping\NamedQuery"/>
<referencedClass name="Doctrine\ORM\Query\AST\InExpression"/>
<referencedClass name="Doctrine\ORM\Tools\Console\Command\ConvertDoctrine1SchemaCommand"/>
<referencedClass name="Doctrine\ORM\Tools\Console\Command\ConvertMappingCommand"/>
@@ -45,10 +50,6 @@
<referencedClass name="Doctrine\ORM\Tools\Console\Command\GenerateRepositoriesCommand"/>
<referencedClass name="Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper"/>
<referencedClass name="Doctrine\ORM\Tools\Console\EntityManagerProvider\HelperSetManagerProvider"/>
<referencedClass name="Doctrine\ORM\Internal\CommitOrder\Edge"/>
<referencedClass name="Doctrine\ORM\Internal\CommitOrder\Vertex"/>
<referencedClass name="Doctrine\ORM\Internal\CommitOrder\VertexState"/>
<referencedClass name="Doctrine\ORM\Internal\CommitOrderCalculator"/>
</errorLevel>
</DeprecatedClass>
<DeprecatedConstant>
@@ -94,6 +95,7 @@
<referencedMethod name="Doctrine\ORM\Configuration::ensureProductionSettings"/>
<referencedMethod name="Doctrine\ORM\Configuration::newDefaultAnnotationDriver"/>
<referencedMethod name="Doctrine\ORM\EntityManager::createConnection"/>
<referencedMethod name="Doctrine\ORM\EntityManagerInterface::getPartialReference"/>
<referencedMethod name="Doctrine\ORM\Id\AbstractIdGenerator::generate"/>
<referencedMethod name="Doctrine\ORM\ORMInvalidArgumentException::invalidEntityName"/>
<referencedMethod name="Doctrine\ORM\ORMSetup::createDefaultAnnotationDriver"/>
@@ -121,6 +123,17 @@
<file name="lib/Doctrine/ORM/PersistentCollection.php"/>
</errorLevel>
</DocblockTypeContradiction>
<DuplicateClass>
<errorLevel type="suppress">
<file name="lib/Doctrine/ORM/Tools/Console/CommandCompatibility.php"/>
<file name="lib/Doctrine/ORM/Tools/Console/Helper/EntityManagerHelper.php"/>
</errorLevel>
</DuplicateClass>
<ForbiddenCode>
<errorLevel type="suppress">
<file name="lib/Doctrine/ORM/Tools/Debug.php"/>
</errorLevel>
</ForbiddenCode>
<InvalidArgument>
<errorLevel type="suppress">
<!-- Argument type changes in DBAL 3.2 -->
@@ -87,12 +87,18 @@ class ConnectionMock extends Connection
*/
public function fetchColumn($statement, array $params = [], $colunm = 0, array $types = [])
{
throw new BadMethodCallException('Call to deprecated method.');
throw new BadMethodCallException(sprintf(
'Call to deprecated method %s().',
__METHOD__
));
}
public function query(?string $sql = null): Result
{
throw new BadMethodCallException('Call to deprecated method.');
throw new BadMethodCallException(sprintf(
'Call to deprecated method %s().',
__METHOD__
));
}
/**
@@ -8,6 +8,8 @@ use BadMethodCallException;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use function sprintf;
/**
* Mock class for DatabasePlatform.
*/
@@ -15,7 +17,10 @@ class DatabasePlatformMock extends AbstractPlatform
{
public function prefersIdentityColumns(): bool
{
throw new BadMethodCallException('Call to deprecated method.');
throw new BadMethodCallException(sprintf(
'Call to deprecated method %s().',
__METHOD__
));
}
public function supportsIdentityColumns(): bool
@@ -25,7 +30,10 @@ class DatabasePlatformMock extends AbstractPlatform
public function prefersSequences(): bool
{
throw new BadMethodCallException('Call to deprecated method.');
throw new BadMethodCallException(sprintf(
'Call to deprecated method %s().',
__METHOD__
));
}
public function supportsSequences(): bool
@@ -94,7 +102,10 @@ class DatabasePlatformMock extends AbstractPlatform
public function getName(): string
{
throw new BadMethodCallException('Call to deprecated method.');
throw new BadMethodCallException(sprintf(
'Call to deprecated method %s().',
__METHOD__
));
}
/**
+6 -1
View File
@@ -12,6 +12,8 @@ use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Exception;
use function sprintf;
/**
* Mock class for Driver.
*/
@@ -70,7 +72,10 @@ class DriverMock implements Driver
*/
public function getName()
{
throw new BadMethodCallException('Call to deprecated method.');
throw new BadMethodCallException(sprintf(
'Call to deprecated method %s().',
__METHOD__
));
}
/**
@@ -1,122 +0,0 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM;
use Doctrine\ORM\Internal\CommitOrderCalculator;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\Tests\OrmTestCase;
/**
* Tests of the commit order calculation.
*
* IMPORTANT: When writing tests here consider that a lot of graph constellations
* can have many valid orderings, so you may want to build a graph that has only
* 1 valid order to simplify your tests.
*/
class CommitOrderCalculatorTest extends OrmTestCase
{
/** @var CommitOrderCalculator */
private $_calc;
protected function setUp(): void
{
$this->_calc = new CommitOrderCalculator();
}
public function testCommitOrdering1(): void
{
$class1 = new ClassMetadata(NodeClass1::class);
$class2 = new ClassMetadata(NodeClass2::class);
$class3 = new ClassMetadata(NodeClass3::class);
$class4 = new ClassMetadata(NodeClass4::class);
$class5 = new ClassMetadata(NodeClass5::class);
$this->_calc->addNode($class1->name, $class1);
$this->_calc->addNode($class2->name, $class2);
$this->_calc->addNode($class3->name, $class3);
$this->_calc->addNode($class4->name, $class4);
$this->_calc->addNode($class5->name, $class5);
$this->_calc->addDependency($class1->name, $class2->name, 1);
$this->_calc->addDependency($class2->name, $class3->name, 1);
$this->_calc->addDependency($class3->name, $class4->name, 1);
$this->_calc->addDependency($class5->name, $class1->name, 1);
$sorted = $this->_calc->sort();
// There is only 1 valid ordering for this constellation
$correctOrder = [$class5, $class1, $class2, $class3, $class4];
self::assertSame($correctOrder, $sorted);
}
public function testCommitOrdering2(): void
{
$class1 = new ClassMetadata(NodeClass1::class);
$class2 = new ClassMetadata(NodeClass2::class);
$this->_calc->addNode($class1->name, $class1);
$this->_calc->addNode($class2->name, $class2);
$this->_calc->addDependency($class1->name, $class2->name, 0);
$this->_calc->addDependency($class2->name, $class1->name, 1);
$sorted = $this->_calc->sort();
// There is only 1 valid ordering for this constellation
$correctOrder = [$class2, $class1];
self::assertSame($correctOrder, $sorted);
}
public function testCommitOrdering3(): void
{
// this test corresponds to the GH7259Test::testPersistFileBeforeVersion functional test
$class1 = new ClassMetadata(NodeClass1::class);
$class2 = new ClassMetadata(NodeClass2::class);
$class3 = new ClassMetadata(NodeClass3::class);
$class4 = new ClassMetadata(NodeClass4::class);
$this->_calc->addNode($class1->name, $class1);
$this->_calc->addNode($class2->name, $class2);
$this->_calc->addNode($class3->name, $class3);
$this->_calc->addNode($class4->name, $class4);
$this->_calc->addDependency($class4->name, $class1->name, 1);
$this->_calc->addDependency($class1->name, $class2->name, 1);
$this->_calc->addDependency($class4->name, $class3->name, 1);
$this->_calc->addDependency($class1->name, $class4->name, 0);
$sorted = $this->_calc->sort();
// There is only multiple valid ordering for this constellation, but
// the class4, class1, class2 ordering is important to break the cycle
// on the nullable link.
$correctOrders = [
[$class4, $class1, $class2, $class3],
[$class4, $class1, $class3, $class2],
[$class4, $class3, $class1, $class2],
];
// We want to perform a strict comparison of the array
self::assertContains($sorted, $correctOrders, '', false, true);
}
}
class NodeClass1
{
}
class NodeClass2
{
}
class NodeClass3
{
}
class NodeClass4
{
}
class NodeClass5
{
}
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Decorator;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\Decorator\EntityManagerDecorator;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\ResultSetMapping;
@@ -22,6 +23,8 @@ use function sprintf;
class EntityManagerDecoratorTest extends TestCase
{
use VerifyDeprecations;
public const VOID_METHODS = [
'persist',
'remove',
@@ -122,4 +125,12 @@ class EntityManagerDecoratorTest extends TestCase
self::assertSame($return, $decorator->$method(...$parameters));
}
public function testGetPartialReferenceIsDeprecated(): void
{
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/10987');
$decorator = new class ($this->wrapped) extends EntityManagerDecorator {
};
$decorator->getPartialReference(stdClass::class, 1);
}
}
@@ -131,6 +131,7 @@ class EntityManagerTest extends OrmTestCase
public function testGetPartialReference(): void
{
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/10987');
$user = $this->entityManager->getPartialReference(CmsUser::class, 42);
self::assertTrue($this->entityManager->contains($user));
self::assertEquals(42, $user->id);
@@ -0,0 +1,152 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Query\QueryException;
use Doctrine\Tests\OrmFunctionalTestCase;
use function count;
class EagerFetchCollectionTest extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(EagerFetchOwner::class, EagerFetchChild::class);
}
public function testEagerFetchMode(): void
{
$owner = $this->createOwnerWithChildren(2);
$owner2 = $this->createOwnerWithChildren(3);
$this->_em->flush();
$this->_em->clear();
$owner = $this->_em->find(EagerFetchOwner::class, $owner->id);
$afterQueryCount = count($this->getQueryLog()->queries);
$this->assertCount(2, $owner->children);
$this->assertQueryCount($afterQueryCount, 'The $owner->children collection should already be initialized by find EagerFetchOwner before.');
$this->assertCount(3, $this->_em->find(EagerFetchOwner::class, $owner2->id)->children);
$this->_em->clear();
$beforeQueryCount = count($this->getQueryLog()->queries);
$owners = $this->_em->getRepository(EagerFetchOwner::class)->findAll();
$this->assertQueryCount($beforeQueryCount + 2, 'the findAll() + 1 subselect loading both collections of the two returned $owners');
$this->assertCount(2, $owners[0]->children);
$this->assertCount(3, $owners[1]->children);
$this->assertQueryCount($beforeQueryCount + 2, 'both collections are already initialized and counting them does not make a difference in total query count');
}
public function testEagerFetchModeWithDQL(): void
{
$owner = $this->createOwnerWithChildren(2);
$owner2 = $this->createOwnerWithChildren(3);
$this->_em->flush();
$this->_em->clear();
$query = $this->_em->createQuery('SELECT o FROM ' . EagerFetchOwner::class . ' o');
$query->setFetchMode(EagerFetchOwner::class, 'children', ORM\ClassMetadata::FETCH_EAGER);
$beforeQueryCount = count($this->getQueryLog()->queries);
$owners = $query->getResult();
$afterQueryCount = count($this->getQueryLog()->queries);
$this->assertEquals($beforeQueryCount + 2, $afterQueryCount);
$owners[0]->children->count();
$owners[1]->children->count();
$anotherQueryCount = count($this->getQueryLog()->queries);
$this->assertEquals($anotherQueryCount, $afterQueryCount);
}
public function testSubselectFetchJoinWithNotAllowed(): void
{
$this->expectException(QueryException::class);
$this->expectExceptionMessage('Associations with fetch-mode=EAGER may not be using WITH conditions');
$query = $this->_em->createQuery('SELECT o, c FROM ' . EagerFetchOwner::class . ' o JOIN o.children c WITH c.id = 1');
$query->getResult();
}
protected function createOwnerWithChildren(int $children): EagerFetchOwner
{
$owner = new EagerFetchOwner();
$this->_em->persist($owner);
for ($i = 0; $i < $children; $i++) {
$child = new EagerFetchChild();
$child->owner = $owner;
$owner->children->add($child);
$this->_em->persist($child);
}
return $owner;
}
}
/**
* @ORM\Entity
*/
class EagerFetchOwner
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue()
*
* @var int
*/
public $id;
/**
* @ORM\OneToMany(targetEntity="EagerFetchChild", mappedBy="owner", fetch="EAGER")
*
* @var ArrayCollection
*/
public $children;
public function __construct()
{
$this->children = new ArrayCollection();
}
}
/**
* @ORM\Entity
*/
class EagerFetchChild
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue()
*
* @var int
*/
public $id;
/**
* @ORM\ManyToOne(targetEntity="EagerFetchOwner", inversedBy="children")
*
* @var EagerFetchOwner
*/
public $owner;
}
@@ -11,6 +11,7 @@ use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Tests\OrmFunctionalTestCase;
use Generator;
use ReflectionMethod;
use ReflectionProperty;
use function file_get_contents;
use function rtrim;
@@ -41,6 +42,30 @@ class ParserResultSerializationTest extends OrmFunctionalTestCase
$this->assertInstanceOf(SingleSelectExecutor::class, $unserialized->getSqlExecutor());
}
public function testItSerializesParserResultWithAForwardCompatibleFormat(): void
{
$query = $this->_em
->createQuery('SELECT u FROM Doctrine\Tests\Models\Company\CompanyEmployee u WHERE u.name = :name');
$parserResult = self::parseQuery($query);
$serialized = serialize($parserResult);
$this->assertStringNotContainsString(
'_sqlStatements',
$serialized,
'ParserResult should not contain any reference to _sqlStatements, which is a legacy property.'
);
$unserialized = unserialize($serialized);
$r = new ReflectionProperty($unserialized->getSqlExecutor(), '_sqlStatements');
$r->setAccessible(true);
$this->assertSame(
$r->getValue($unserialized->getSqlExecutor()),
$unserialized->getSqlExecutor()->getSqlStatements(),
'The legacy property should be populated with the same value as the new one.'
);
}
/**
* @dataProvider provideSerializedSingleSelectResults
*/
@@ -52,6 +77,7 @@ class ParserResultSerializationTest extends OrmFunctionalTestCase
$this->assertInstanceOf(ResultSetMapping::class, $unserialized->getResultSetMapping());
$this->assertEquals(['name' => [0]], $unserialized->getParameterMappings());
$this->assertInstanceOf(SingleSelectExecutor::class, $unserialized->getSqlExecutor());
$this->assertIsString($unserialized->getSqlExecutor()->getSqlStatements());
}
/** @return Generator<string, array{string}> */
@@ -59,6 +85,7 @@ class ParserResultSerializationTest extends OrmFunctionalTestCase
{
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")];
yield '2.17.0' => [rtrim(file_get_contents(__DIR__ . '/ParserResults/single_select_2_17_0.txt'), "\n")];
}
private static function parseQuery(Query $query): ParserResult
@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\ORM\Event\PostLoadEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\Tests\Models\CMS\CmsAddress;
use Doctrine\Tests\Models\CMS\CmsEmail;
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
@@ -302,7 +302,7 @@ class PostLoadListenerLoadEntityInEventHandler
public function postLoad(PostLoadEventArgs $event): void
{
$object = $event->getObject();
$class = ClassUtils::getClass($object);
$class = DefaultProxyClassNameResolver::getClass($object);
if (! isset($this->firedByClasses[$class])) {
$this->firedByClasses[$class] = 1;
} else {
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Common\Proxy\Proxy as CommonProxy;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Proxy\InternalProxy;
use Doctrine\Tests\Models\Company\CompanyAuction;
use Doctrine\Tests\Models\ECommerce\ECommerceProduct;
@@ -227,7 +227,7 @@ class ReferenceProxyTest extends OrmFunctionalTestCase
$entity = $this->_em->getReference(ECommerceProduct::class, $id);
assert($entity instanceof ECommerceProduct);
$className = ClassUtils::getClass($entity);
$className = DefaultProxyClassNameResolver::getClass($entity);
self::assertInstanceOf(InternalProxy::class, $entity);
self::assertTrue($this->isUninitializedObject($entity));
@@ -44,11 +44,11 @@ class DDC2350Test extends OrmFunctionalTestCase
$this->getQueryLog()->reset()->enable();
$user = $this->_em->find(DDC2350User::class, $user->id);
$this->assertQueryCount(1);
$this->assertQueryCount(2);
self::assertCount(2, $user->reportedBugs);
$this->assertQueryCount(1);
$this->assertQueryCount(2);
}
}
@@ -20,6 +20,7 @@ use Doctrine\ORM\Mapping\InheritanceType;
use Doctrine\Tests\OrmFunctionalTestCase;
use function debug_backtrace;
use function sprintf;
use const PHP_INT_MAX;
@@ -60,24 +61,24 @@ class DDC3634Test extends OrmFunctionalTestCase
self::assertSame($veryLargeId, $entity->id);
}
public function testSavesIntegerAutoGeneratedValueAsString(): void
public function testSavesIntegerAutoGeneratedValue(): void
{
$entity = new DDC3634Entity();
$this->_em->persist($entity);
$this->_em->flush();
self::assertIsString($entity->id);
self::assertNotNull($entity->id);
}
public function testSavesIntegerAutoGeneratedValueAsStringWithJoinedInheritance(): void
public function testSavesIntegerAutoGeneratedValueWithJoinedInheritance(): void
{
$entity = new DDC3634JTIChildEntity();
$this->_em->persist($entity);
$this->_em->flush();
self::assertIsString($entity->id);
self::assertNotNull($entity->id);
}
}
@@ -347,7 +348,10 @@ class DDC3634LastInsertIdMockingConnection extends Connection
/** {@inheritDoc} */
public function executeUpdate($query, array $params = [], array $types = []): int
{
throw new BadMethodCallException('Call to deprecated method.');
throw new BadMethodCallException(sprintf(
'Call to deprecated method %s().',
__METHOD__
));
}
/** {@inheritDoc} */
@@ -49,7 +49,7 @@ class DDC440Test extends OrmFunctionalTestCase
$phone->setClient($client);
$phone2 = new DDC440Phone();
$phone->setId(2);
$phone2->setId(2);
$phone2->setNumber('418 222-2222');
$phone2->setClient($client);
@@ -88,10 +88,10 @@ class DDC440Test extends OrmFunctionalTestCase
class DDC440Phone
{
/**
* @var int
* @Column(name="id", type="integer")
* @Id
* @GeneratedValue(strategy="AUTO")
* @var int
*/
protected $id;
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH10661;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\SchemaValidator;
use Doctrine\Tests\OrmTestCase;
/**
* @requires PHP >= 7.4
*/
final class GH10661Test extends OrmTestCase
{
/** @var EntityManagerInterface */
private $em;
/** @var SchemaValidator */
private $validator;
protected function setUp(): void
{
$this->em = $this->getTestEntityManager();
$this->validator = new SchemaValidator($this->em);
}
public function testMetadataFieldTypeNotCoherentWithEntityPropertyType(): void
{
$class = $this->em->getClassMetadata(InvalidEntity::class);
$ce = $this->validator->validateClass($class);
self::assertEquals(
["The field 'Doctrine\Tests\ORM\Functional\Ticket\GH10661\InvalidEntity#property1' has the property type 'float' that differs from the metadata field type 'string' returned by the 'decimal' DBAL type."],
$ce
);
}
public function testMetadataFieldTypeNotCoherentWithEntityPropertyTypeWithInheritance(): void
{
$class = $this->em->getClassMetadata(InvalidChildEntity::class);
$ce = $this->validator->validateClass($class);
self::assertEquals(
[
"The field 'Doctrine\Tests\ORM\Functional\Ticket\GH10661\InvalidChildEntity#property1' has the property type 'float' that differs from the metadata field type 'string' returned by the 'decimal' DBAL type.",
"The field 'Doctrine\Tests\ORM\Functional\Ticket\GH10661\InvalidChildEntity#property2' has the property type 'int' that differs from the metadata field type 'string' returned by the 'string' DBAL type.",
"The field 'Doctrine\Tests\ORM\Functional\Ticket\GH10661\InvalidChildEntity#anotherProperty' has the property type 'string' that differs from the metadata field type 'bool' returned by the 'boolean' DBAL type.",
],
$ce
);
}
}
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH10661;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
/** @Entity */
class InvalidChildEntity extends InvalidEntity
{
/** @Column(type="string") */
protected int $property2;
/**
* @Column(type="boolean")
*/
private string $anotherProperty;
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH10661;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Id;
/** @Entity */
class InvalidEntity
{
/**
* @var int
* @Id
* @Column
*/
protected $key;
/**
* @Column(type="decimal")
*/
protected float $property1;
}
@@ -39,8 +39,7 @@ class GH10808Test extends OrmFunctionalTestCase
$query = $this->_em->createQuery(
'SELECT appointment from Doctrine\Tests\ORM\Functional\Ticket\GH10808Appointment appointment
JOIN appointment.child appointment_child
WITH appointment_child.id = 1'
JOIN appointment.child appointment_child'
);
// By default, UnitOfWork::HINT_DEFEREAGERLOAD is set to 'true'
@@ -9,6 +9,8 @@ use Doctrine\Common\Persistence\PersistentObject;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\OraclePlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManagerInterface;
@@ -151,6 +153,60 @@ class ClassMetadataFactoryTest extends OrmTestCase
self::assertInstanceOf(CustomIdGenerator::class, $actual->idGenerator);
}
/** @param array<class-string<AbstractPlatform>, ClassMetadata::GENERATOR_TYPE_*> $preferences */
private function setUpCmfForPlatform(AbstractPlatform $platform, array $preferences = []): ClassMetadataFactoryTestSubject
{
$cmf = new ClassMetadataFactoryTestSubject();
$driver = $this->createMock(Driver::class);
$driver->method('getDatabasePlatform')
->willReturn($platform);
$entityManager = $this->createEntityManager(
new MetadataDriverMock(),
new Connection([], $driver)
);
$cmf->setEntityManager($entityManager);
$entityManager->getConfiguration()->setIdentityGenerationPreferences($preferences);
return $cmf;
}
public function testRelyingOnLegacyIdGenerationDefaultsIsOKIfItResultsInTheCurrentlyRecommendedStrategyBeingUsed(): void
{
$cm = $this->createValidClassMetadata();
$cm->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_AUTO);
$cmf = $this->setUpCmfForPlatform(new OraclePlatform());
$cmf->setMetadataForClass($cm->name, $cm);
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/8893');
$cmf->getMetadataFor($cm->name);
}
public function testRelyingOnLegacyIdGenerationDefaultsIsDeprecatedIfItResultsInADefaultThatWillChange(): void
{
$cm = $this->createValidClassMetadata();
$cm->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_AUTO);
$cmf = $this->setUpCmfForPlatform(new PostgreSQLPlatform());
$cmf->setMetadataForClass($cm->name, $cm);
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/8893');
$cmf->getMetadataFor($cm->name);
}
public function testSpecifyingIdGenerationStrategyThroughConfigurationFixesTheDeprecation(): void
{
$cm = $this->createValidClassMetadata();
$cm->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_AUTO);
$cmf = $this->setUpCmfForPlatform(new PostgreSQLPlatform(), [
PostgreSQLPlatform::class => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
]);
$cmf->setMetadataForClass($cm->name, $cm);
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/8893');
$cmf->getMetadataFor($cm->name);
}
public function testGetMetadataForThrowsExceptionOnUnknownCustomGeneratorClass(): void
{
$cm1 = $this->createValidClassMetadata();
@@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Tools;
use ArrayIterator;
use ArrayObject;
use DateTime;
use DateTimeImmutable;
use DateTimeZone;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Tools\Debug;
use Doctrine\Tests\DoctrineTestCase;
use stdClass;
use function print_r;
use function strpos;
use function substr;
use function version_compare;
use const PHP_VERSION;
class DebugTest extends DoctrineTestCase
{
public function testExportObject(): void
{
$obj = new stdClass();
$obj->foo = 'bar';
$obj->bar = 1234;
$var = Debug::export($obj, 2);
self::assertEquals('stdClass', $var->__CLASS__);
}
public function testExportObjectWithReference(): void
{
$foo = 'bar';
$bar = ['foo' => & $foo];
$baz = (object) $bar;
$var = Debug::export($baz, 2);
$baz->foo = 'tab';
self::assertEquals('bar', $var->foo);
self::assertEquals('tab', $bar['foo']);
}
public function testExportArray(): void
{
$array = ['a' => 'b', 'b' => ['c', 'd' => ['e', 'f']]];
$var = Debug::export($array, 2);
$expected = $array;
$expected['b']['d'] = 'Array(2)';
self::assertEquals($expected, $var);
}
public function testExportDateTime(): void
{
$obj = new DateTime('2010-10-10 10:10:10', new DateTimeZone('UTC'));
$var = Debug::export($obj, 2);
self::assertEquals('DateTime', $var->__CLASS__);
self::assertEquals('2010-10-10T10:10:10+00:00', $var->date);
}
public function testExportDateTimeImmutable(): void
{
$obj = new DateTimeImmutable('2010-10-10 10:10:10', new DateTimeZone('UTC'));
$var = Debug::export($obj, 2);
self::assertEquals('DateTimeImmutable', $var->__CLASS__);
self::assertEquals('2010-10-10T10:10:10+00:00', $var->date);
}
public function testExportDateTimeZone(): void
{
$obj = new DateTimeImmutable('2010-10-10 12:34:56', new DateTimeZone('Europe/Rome'));
$var = Debug::export($obj, 2);
self::assertEquals('DateTimeImmutable', $var->__CLASS__);
self::assertEquals('2010-10-10T12:34:56+02:00', $var->date);
}
public function testExportArrayTraversable(): void
{
$obj = new ArrayObject(['foobar']);
$var = Debug::export($obj, 2);
self::assertContains('foobar', $var->__STORAGE__);
$it = new ArrayIterator(['foobar']);
$var = Debug::export($it, 5);
self::assertContains('foobar', $var->__STORAGE__);
}
/**
* @param array<string, int> $expected
*
* @dataProvider provideAttributesCases
*/
public function testExportParentAttributes(TestAsset\ParentClass $class, array $expected): void
{
$actualRepresentation = print_r($class, true);
$expectedRepresentation = print_r($expected, true);
$actualRepresentation = substr($actualRepresentation, strpos($actualRepresentation, '('));
$expectedRepresentation = substr($expectedRepresentation, strpos($expectedRepresentation, '('));
self::assertSame($expectedRepresentation, $actualRepresentation);
$var = Debug::export($class, 3);
$var = (array) $var;
unset($var['__CLASS__']);
self::assertSame($expected, $var);
}
public function testCollectionsAreCastIntoArrays(): void
{
$collection = new ArrayCollection();
$collection->add('foo');
$collection->add('bar');
$var = Debug::export($collection, 2);
self::assertEquals(['foo', 'bar'], $var);
}
/**
* @psalm-return array<string, array{TestAsset\ParentClass, mixed[]}>
*/
public function provideAttributesCases(): iterable
{
return [
'different-attributes' => [
new TestAsset\ChildClass(),
version_compare(PHP_VERSION, '8.1', '<') ?
[
'childPublicAttribute' => 4,
'childProtectedAttribute:protected' => 5,
'childPrivateAttribute:Doctrine\Tests\ORM\Tools\TestAsset\ChildClass:private' => 6,
'parentPublicAttribute' => 1,
'parentProtectedAttribute:protected' => 2,
'parentPrivateAttribute:Doctrine\Tests\ORM\Tools\TestAsset\ParentClass:private' => 3,
] :
[
'parentPublicAttribute' => 1,
'parentProtectedAttribute:protected' => 2,
'parentPrivateAttribute:Doctrine\Tests\ORM\Tools\TestAsset\ParentClass:private' => 3,
'childPublicAttribute' => 4,
'childProtectedAttribute:protected' => 5,
'childPrivateAttribute:Doctrine\Tests\ORM\Tools\TestAsset\ChildClass:private' => 6,
],
],
'same-attributes' => [
new TestAsset\ChildWithSameAttributesClass(),
version_compare(PHP_VERSION, '8.1', '<') ?
[
'parentPublicAttribute' => 4,
'parentProtectedAttribute:protected' => 5,
'parentPrivateAttribute:Doctrine\Tests\ORM\Tools\TestAsset\ChildWithSameAttributesClass:private' => 6,
'parentPrivateAttribute:Doctrine\Tests\ORM\Tools\TestAsset\ParentClass:private' => 3,
] :
[
'parentPublicAttribute' => 4,
'parentProtectedAttribute:protected' => 5,
'parentPrivateAttribute:Doctrine\Tests\ORM\Tools\TestAsset\ParentClass:private' => 3,
'parentPrivateAttribute:Doctrine\Tests\ORM\Tools\TestAsset\ChildWithSameAttributesClass:private' => 6,
],
],
];
}
}
@@ -229,7 +229,7 @@ final class LimitSubqueryOutputWalkerTest extends PaginationTestCase
);
}
public function testCountQueryWithComplexScalarOrderByItem(): void
public function testCountQueryWithComplexScalarOrderByItemWithoutJoin(): void
{
$query = $this->entityManager->createQuery(
'SELECT a FROM Doctrine\Tests\ORM\Tools\Pagination\Avatar a ORDER BY a.imageHeight * a.imageWidth DESC'
@@ -244,7 +244,7 @@ final class LimitSubqueryOutputWalkerTest extends PaginationTestCase
);
}
public function testCountQueryWithComplexScalarOrderByItemJoined(): void
public function testCountQueryWithComplexScalarOrderByItemJoinedWithoutPartial(): void
{
$query = $this->entityManager->createQuery(
'SELECT u FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.avatar a ORDER BY a.imageHeight * a.imageWidth DESC'
@@ -254,7 +254,7 @@ final class LimitSubqueryOutputWalkerTest extends PaginationTestCase
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
self::assertSame(
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, imageHeight_1 * imageWidth_2 FROM (SELECT u0_.id AS id_0, a1_.imageHeight AS imageHeight_1, a1_.imageWidth AS imageWidth_2, a1_.user_id AS user_id_3 FROM User u0_ INNER JOIN Avatar a1_ ON u0_.id = a1_.user_id) dctrn_result_inner ORDER BY imageHeight_1 * imageWidth_2 DESC) dctrn_result',
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, imageHeight_3 * imageWidth_4 FROM (SELECT u0_.id AS id_0, a1_.id AS id_1, a1_.image AS image_2, a1_.imageHeight AS imageHeight_3, a1_.imageWidth AS imageWidth_4, a1_.imageAltDesc AS imageAltDesc_5, a1_.user_id AS user_id_6 FROM User u0_ INNER JOIN Avatar a1_ ON u0_.id = a1_.user_id) dctrn_result_inner ORDER BY imageHeight_3 * imageWidth_4 DESC) dctrn_result',
$query->getSQL()
);
}
@@ -269,7 +269,7 @@ final class LimitSubqueryOutputWalkerTest extends PaginationTestCase
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
self::assertSame(
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, imageHeight_3 * imageWidth_4 FROM (SELECT u0_.id AS id_0, a1_.id AS id_1, a1_.imageAltDesc AS imageAltDesc_2, a1_.imageHeight AS imageHeight_3, a1_.imageWidth AS imageWidth_4, a1_.user_id AS user_id_5 FROM User u0_ INNER JOIN Avatar a1_ ON u0_.id = a1_.user_id) dctrn_result_inner ORDER BY imageHeight_3 * imageWidth_4 DESC) dctrn_result',
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, imageHeight_5 * imageWidth_6 FROM (SELECT u0_.id AS id_0, a1_.id AS id_1, a1_.imageAltDesc AS imageAltDesc_2, a1_.id AS id_3, a1_.image AS image_4, a1_.imageHeight AS imageHeight_5, a1_.imageWidth AS imageWidth_6, a1_.imageAltDesc AS imageAltDesc_7, a1_.user_id AS user_id_8 FROM User u0_ INNER JOIN Avatar a1_ ON u0_.id = a1_.user_id) dctrn_result_inner ORDER BY imageHeight_5 * imageWidth_6 DESC) dctrn_result',
$query->getSQL()
);
}
@@ -328,7 +328,7 @@ final class LimitSubqueryOutputWalkerTest extends PaginationTestCase
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
self::assertSame(
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, name_1 FROM (SELECT b0_.id AS id_0, a1_.name AS name_1, b0_.author_id AS author_id_2, b0_.category_id AS category_id_3 FROM BlogPost b0_ INNER JOIN Author a1_ ON b0_.author_id = a1_.id) dctrn_result_inner ORDER BY name_1 ASC) dctrn_result',
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, name_2 FROM (SELECT b0_.id AS id_0, a1_.id AS id_1, a1_.name AS name_2, b0_.author_id AS author_id_3, b0_.category_id AS category_id_4 FROM BlogPost b0_ INNER JOIN Author a1_ ON b0_.author_id = a1_.id) dctrn_result_inner ORDER BY name_2 ASC) dctrn_result',
$query->getSQL()
);
}
@@ -6,6 +6,7 @@ namespace Doctrine\Tests\ORM\Tools\Pagination;
use Doctrine\ORM\Query;
use Doctrine\ORM\Tools\Pagination\LimitSubqueryWalker;
use Doctrine\ORM\Tools\Pagination\Paginator;
/** @group DDC-1613 */
class LimitSubqueryWalkerTest extends PaginationTestCase
@@ -24,6 +25,21 @@ class LimitSubqueryWalkerTest extends PaginationTestCase
);
}
public function testHintCanDisableDistinct(): void
{
$dql = 'SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\MyBlogPost p JOIN p.category c JOIN p.author a';
$query = $this->entityManager->createQuery($dql);
$limitQuery = clone $query;
$limitQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [LimitSubqueryWalker::class]);
$limitQuery->setHint(Paginator::HINT_ENABLE_DISTINCT, false);
self::assertEquals(
'SELECT m0_.id AS id_0 FROM MyBlogPost m0_ INNER JOIN Category c1_ ON m0_.category_id = c1_.id INNER JOIN Author a2_ ON m0_.author_id = a2_.id',
$limitQuery->getSQL()
);
}
public function testLimitSubqueryWithSort(): void
{
$dql = 'SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\MyBlogPost p JOIN p.category c JOIN p.author a ORDER BY p.title';
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Tools\TestAsset;
final class ChildClass extends ParentClass
{
/** @var int */
public $childPublicAttribute = 4;
/** @var int */
protected $childProtectedAttribute = 5;
/** @var int */
private $childPrivateAttribute = 6;
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Tools\TestAsset;
final class ChildWithSameAttributesClass extends ParentClass
{
/** @var int */
public $parentPublicAttribute = 4;
/** @var int */
protected $parentProtectedAttribute = 5;
/** @var int */
private $parentPrivateAttribute = 6;
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Tools\TestAsset;
abstract class ParentClass
{
/** @var int */
public $parentPublicAttribute = 1;
/** @var int */
protected $parentProtectedAttribute = 2;
/** @var int */
private $parentPrivateAttribute = 3;
}