From fdfca0f0e7e0963cbfdd91de3b02a7a33eb6ecd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Wed, 11 Oct 2023 19:44:07 +0200 Subject: [PATCH 1/4] Undeprecate Autoloader class We plan to sunset doctrine/common, and should move the Autoloader class to doctrine/orm --- UPGRADE.md | 5 +++++ docs/en/reference/advanced-configuration.rst | 2 +- lib/Doctrine/ORM/Proxy/Autoloader.php | 1 - 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index 6d2947f6e68..1db52439499 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,5 +1,10 @@ # Upgrade to 2.17 +## 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 diff --git a/docs/en/reference/advanced-configuration.rst b/docs/en/reference/advanced-configuration.rst index 6761ac1b974..42a0833aca1 100644 --- a/docs/en/reference/advanced-configuration.rst +++ b/docs/en/reference/advanced-configuration.rst @@ -408,7 +408,7 @@ means that you have to register a special autoloader for these classes: .. code-block:: php Date: Fri, 13 Oct 2023 18:57:12 +0200 Subject: [PATCH 2/4] Remove useless check (#11006) --- lib/Doctrine/ORM/Mapping/Driver/AttributeReader.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/Mapping/Driver/AttributeReader.php b/lib/Doctrine/ORM/Mapping/Driver/AttributeReader.php index 42b4cbaf85c..cc344bd0970 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AttributeReader.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AttributeReader.php @@ -69,8 +69,7 @@ public function getPropertyAttribute(ReflectionProperty $property, $attributeNam )); } - return $this->getPropertyAttributes($property)[$attributeName] - ?? ($this->isRepeatable($attributeName) ? new RepeatableAttributeCollection() : null); + return $this->getPropertyAttributes($property)[$attributeName] ?? null; } /** From bf69d0ac4ea6ebf69bef772fd5ef33e51a9f9584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Sat, 14 Oct 2023 19:48:11 +0200 Subject: [PATCH 3/4] Implement proxy name resolver (#11009) It is important to have the same implementation as used in doctrine/persistence without relying on copy/paste. --- lib/Doctrine/ORM/AbstractQuery.php | 4 +- lib/Doctrine/ORM/Cache/DefaultCache.php | 4 +- .../ORM/Cache/DefaultEntityHydrator.php | 4 +- .../AbstractCollectionPersister.php | 4 +- .../ReadOnlyCachedCollectionPersister.php | 4 +- .../Entity/AbstractEntityPersister.php | 6 +-- .../Entity/ReadOnlyCachedEntityPersister.php | 4 +- lib/Doctrine/ORM/EntityManager.php | 4 +- .../ORM/Mapping/ClassMetadataFactory.php | 3 ++ .../Entity/BasicEntityPersister.php | 4 +- .../Proxy/DefaultProxyClassNameResolver.php | 40 +++++++++++++++++++ phpcs.xml.dist | 5 +++ psalm-baseline.xml | 9 +++++ .../ORM/Functional/PostLoadEventTest.php | 4 +- .../ORM/Functional/ReferenceProxyTest.php | 4 +- 15 files changed, 80 insertions(+), 23 deletions(-) create mode 100644 lib/Doctrine/ORM/Proxy/DefaultProxyClassNameResolver.php diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index 6cfc76d7ed0..7532681580c 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -10,7 +10,6 @@ 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\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 @@ public function processParameterValue($value) } try { - $class = ClassUtils::getClass($value); + $class = DefaultProxyClassNameResolver::getClass($value); $value = $this->_em->getUnitOfWork()->getSingleIdentifierValue($value); if ($value === null) { diff --git a/lib/Doctrine/ORM/Cache/DefaultCache.php b/lib/Doctrine/ORM/Cache/DefaultCache.php index bf67186bd9b..ff9b60ba402 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCache.php +++ b/lib/Doctrine/ORM/Cache/DefaultCache.php @@ -4,12 +4,12 @@ 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 @@ private function buildCollectionCacheKey( 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); diff --git a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php index 32170f41094..d97d805a393 100644 --- a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php +++ b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php @@ -4,9 +4,9 @@ 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 @@ public function buildCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, $e } 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); diff --git a/lib/Doctrine/ORM/Cache/Persister/Collection/AbstractCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/Collection/AbstractCollectionPersister.php index 9667cbc4252..ad81823b84c 100644 --- a/lib/Doctrine/ORM/Cache/Persister/Collection/AbstractCollectionPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/Collection/AbstractCollectionPersister.php @@ -6,7 +6,6 @@ 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\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 @@ public function storeCollectionCache(CollectionCacheKey $key, $elements) } $class = $this->targetEntity; - $className = ClassUtils::getClass($elements[$index]); + $className = DefaultProxyClassNameResolver::getClass($elements[$index]); if ($className !== $this->targetEntity->name) { $class = $this->metadataFactory->getMetadataFor($className); diff --git a/lib/Doctrine/ORM/Cache/Persister/Collection/ReadOnlyCachedCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/Collection/ReadOnlyCachedCollectionPersister.php index ca927eed848..5722172d0ab 100644 --- a/lib/Doctrine/ORM/Cache/Persister/Collection/ReadOnlyCachedCollectionPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/Collection/ReadOnlyCachedCollectionPersister.php @@ -4,9 +4,9 @@ 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 @@ public function update(PersistentCollection $collection) { if ($collection->isDirty() && $collection->getSnapshot()) { throw CannotUpdateReadOnlyCollection::fromEntityAndField( - ClassUtils::getClass($collection->getOwner()), + DefaultProxyClassNameResolver::getClass($collection->getOwner()), $this->association['fieldName'] ); } diff --git a/lib/Doctrine/ORM/Cache/Persister/Entity/AbstractEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/Entity/AbstractEntityPersister.php index 2ea4c132ba8..5831af99816 100644 --- a/lib/Doctrine/ORM/Cache/Persister/Entity/AbstractEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/Entity/AbstractEntityPersister.php @@ -5,7 +5,6 @@ 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\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 @@ public function getEntityHydrator() 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 @@ public function loadById(array $identifier, $entity = null) } $class = $this->class; - $className = ClassUtils::getClass($entity); + $className = DefaultProxyClassNameResolver::getClass($entity); if ($className !== $this->class->name) { $class = $this->metadataFactory->getMetadataFor($className); diff --git a/lib/Doctrine/ORM/Cache/Persister/Entity/ReadOnlyCachedEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/Entity/ReadOnlyCachedEntityPersister.php index 3e7e32af654..497815950e5 100644 --- a/lib/Doctrine/ORM/Cache/Persister/Entity/ReadOnlyCachedEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/Entity/ReadOnlyCachedEntityPersister.php @@ -4,8 +4,8 @@ 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)); } } diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index 6838a787c77..21d2c7d3421 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -9,7 +9,6 @@ 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\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 @@ public function find($className, $id, $lockMode = null, $lockVersion = null) 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); diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php index 34c98d37d56..2ad71382e85 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php @@ -24,6 +24,7 @@ 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; @@ -98,6 +99,8 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory /** @return void */ public function setEntityManager(EntityManagerInterface $em) { + parent::setProxyClassNameResolver(new DefaultProxyClassNameResolver()); + $this->em = $em; } diff --git a/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php index 32ac1d4f531..82d3eb3763f 100644 --- a/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php @@ -7,7 +7,6 @@ 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\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; @@ -2028,7 +2028,7 @@ private function getIndividualValue($value): array return [$value->value]; } - $valueClass = ClassUtils::getClass($value); + $valueClass = DefaultProxyClassNameResolver::getClass($value); if ($this->em->getMetadataFactory()->isTransient($valueClass)) { return [$value]; diff --git a/lib/Doctrine/ORM/Proxy/DefaultProxyClassNameResolver.php b/lib/Doctrine/ORM/Proxy/DefaultProxyClassNameResolver.php new file mode 100644 index 00000000000..a9d0a3dba93 --- /dev/null +++ b/lib/Doctrine/ORM/Proxy/DefaultProxyClassNameResolver.php @@ -0,0 +1,40 @@ +resolveClassName(get_class($object)); + } +} diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 420e6f91c1a..b3c2ab6149e 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -195,6 +195,11 @@ tests/Doctrine/Tests/ORM/Functional/Ticket/DDC832Test.php + + + tests/Doctrine/Tests/Proxy/DefaultProxyClassNameResolverTest.php + + tests/Doctrine/Tests/Mocks/DatabasePlatformMock.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 38a29728619..c9c79e0f4a0 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1366,6 +1366,15 @@ $columnList + + + $className + substr($className, $pos + Proxy::MARKER_LENGTH + 2) + + + string + + $classMetadata diff --git a/tests/Doctrine/Tests/ORM/Functional/PostLoadEventTest.php b/tests/Doctrine/Tests/ORM/Functional/PostLoadEventTest.php index 286494493f0..74518f79a1c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/PostLoadEventTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/PostLoadEventTest.php @@ -4,9 +4,9 @@ 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 { diff --git a/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php b/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php index c0b147dfc56..88c14253e20 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php @@ -5,7 +5,7 @@ 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 @@ public function testCommonPersistenceProxy(): void $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)); From a33a3813b2b8546e5c614451f707c0a07ae1cefa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Thu, 12 Oct 2023 13:09:49 +0200 Subject: [PATCH 4/4] 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 --- .../Tools/Console/Command/RunDqlCommand.php | 4 +- lib/Doctrine/ORM/Tools/Debug.php | 168 +++++++++++++++++ phpcs.xml.dist | 5 + psalm-baseline.xml | 7 +- psalm.xml | 5 + tests/Doctrine/Tests/ORM/Tools/DebugTest.php | 174 ++++++++++++++++++ .../Tests/ORM/Tools/TestAsset/ChildClass.php | 15 ++ .../ChildWithSameAttributesClass.php | 15 ++ .../Tests/ORM/Tools/TestAsset/ParentClass.php | 15 ++ 9 files changed, 400 insertions(+), 8 deletions(-) create mode 100644 lib/Doctrine/ORM/Tools/Debug.php create mode 100644 tests/Doctrine/Tests/ORM/Tools/DebugTest.php create mode 100644 tests/Doctrine/Tests/ORM/Tools/TestAsset/ChildClass.php create mode 100644 tests/Doctrine/Tests/ORM/Tools/TestAsset/ChildWithSameAttributesClass.php create mode 100644 tests/Doctrine/Tests/ORM/Tools/TestAsset/ParentClass.php diff --git a/lib/Doctrine/ORM/Tools/Console/Command/RunDqlCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/RunDqlCommand.php index f8611da0b11..61f1eef78f1 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/RunDqlCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/RunDqlCommand.php @@ -4,8 +4,8 @@ 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; @@ -116,7 +116,7 @@ private function doExecute(InputInterface $input, OutputInterface $output): int $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; } diff --git a/lib/Doctrine/ORM/Tools/Debug.php b/lib/Doctrine/ORM/Tools/Debug.php new file mode 100644 index 00000000000..153abac4a44 --- /dev/null +++ b/lib/Doctrine/ORM/Tools/Debug.php @@ -0,0 +1,168 @@ +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; + } +} diff --git a/phpcs.xml.dist b/phpcs.xml.dist index b3c2ab6149e..9f7dba86050 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -59,6 +59,11 @@ tests/* + + lib/Doctrine/ORM/Tools/Debug.php + tests/Doctrine/Tests/ORM/Tools/DebugTest.php + + lib/Doctrine/ORM/Events.php lib/Doctrine/ORM/Tools/ToolEvents.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index c9c79e0f4a0..72069f0765a 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -536,8 +536,8 @@ $class $class - $platformFamily + new UuidGenerator() @@ -2415,11 +2415,6 @@ getAllClassNames - - - getOption('depth'), true, false)]]> - - int diff --git a/psalm.xml b/psalm.xml index 81d8fe8476b..8905a3c4086 100644 --- a/psalm.xml +++ b/psalm.xml @@ -125,6 +125,11 @@ + + + + + diff --git a/tests/Doctrine/Tests/ORM/Tools/DebugTest.php b/tests/Doctrine/Tests/ORM/Tools/DebugTest.php new file mode 100644 index 00000000000..413c577fa66 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Tools/DebugTest.php @@ -0,0 +1,174 @@ +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 $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 + */ + 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, + ], + ], + ]; + } +} diff --git a/tests/Doctrine/Tests/ORM/Tools/TestAsset/ChildClass.php b/tests/Doctrine/Tests/ORM/Tools/TestAsset/ChildClass.php new file mode 100644 index 00000000000..35e11456da1 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Tools/TestAsset/ChildClass.php @@ -0,0 +1,15 @@ +