Skip to content

Commit

Permalink
Use native proxies
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolas-grekas committed Nov 15, 2023
1 parent 21466a0 commit 3aaaac1
Show file tree
Hide file tree
Showing 17 changed files with 109 additions and 278 deletions.
3 changes: 1 addition & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@
"doctrine/lexer": "^3",
"doctrine/persistence": "^3.1.1",
"psr/cache": "^1 || ^2 || ^3",
"symfony/console": "^5.4 || ^6.0 || ^7.0",
"symfony/var-exporter": "~6.2.13 || ^6.3.2 || ^7.0"
"symfony/console": "^5.4 || ^6.0 || ^7.0"
},
"require-dev": {
"doctrine/coding-standard": "^12.0",
Expand Down
5 changes: 5 additions & 0 deletions lib/Doctrine/ORM/Mapping/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -2611,6 +2611,11 @@ public function getSequencePrefix(AbstractPlatform $platform): string
private function getAccessibleProperty(ReflectionService $reflService, string $class, string $field): ReflectionProperty|null
{
$reflectionProperty = $reflService->getAccessibleProperty($class, $field);

if ($reflectionProperty) {
$reflectionProperty = new ReflectionLazyProperty($reflectionProperty);
}

if ($reflectionProperty?->isReadOnly()) {
$declaringClass = $reflectionProperty->class;
if ($declaringClass !== $class) {
Expand Down
34 changes: 34 additions & 0 deletions lib/Doctrine/ORM/Mapping/ReflectionLazyProperty.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Doctrine\ORM\Mapping;

use ReflectionProperty;

final class ReflectionLazyProperty extends ReflectionProperty

Check failure on line 9 in lib/Doctrine/ORM/Mapping/ReflectionLazyProperty.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm (3.7)

PropertyNotSetInConstructor

lib/Doctrine/ORM/Mapping/ReflectionLazyProperty.php:9:13: PropertyNotSetInConstructor: Property Doctrine\ORM\Mapping\ReflectionLazyProperty::$name is not defined in constructor of Doctrine\ORM\Mapping\ReflectionLazyProperty or in any methods called in the constructor (see https://psalm.dev/074)

Check failure on line 9 in lib/Doctrine/ORM/Mapping/ReflectionLazyProperty.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm (3.7)

PropertyNotSetInConstructor

lib/Doctrine/ORM/Mapping/ReflectionLazyProperty.php:9:13: PropertyNotSetInConstructor: Property Doctrine\ORM\Mapping\ReflectionLazyProperty::$class is not defined in constructor of Doctrine\ORM\Mapping\ReflectionLazyProperty or in any methods called in the constructor (see https://psalm.dev/074)

Check failure on line 9 in lib/Doctrine/ORM/Mapping/ReflectionLazyProperty.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm (default)

PropertyNotSetInConstructor

lib/Doctrine/ORM/Mapping/ReflectionLazyProperty.php:9:13: PropertyNotSetInConstructor: Property Doctrine\ORM\Mapping\ReflectionLazyProperty::$name is not defined in constructor of Doctrine\ORM\Mapping\ReflectionLazyProperty or in any methods called in the constructor (see https://psalm.dev/074)

Check failure on line 9 in lib/Doctrine/ORM/Mapping/ReflectionLazyProperty.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm (default)

PropertyNotSetInConstructor

lib/Doctrine/ORM/Mapping/ReflectionLazyProperty.php:9:13: PropertyNotSetInConstructor: Property Doctrine\ORM\Mapping\ReflectionLazyProperty::$class is not defined in constructor of Doctrine\ORM\Mapping\ReflectionLazyProperty or in any methods called in the constructor (see https://psalm.dev/074)
{
public function __construct(
private readonly ReflectionProperty $wrappedProperty,
) {
parent::__construct($wrappedProperty->class, $wrappedProperty->name);
}

public function getValue(object|null $object = null): mixed
{
return $this->wrappedProperty->getValue($object);
}

public function setValue(object|null $object, mixed $value = null): void
{
if (\is_object($object)) {
$r = \ReflectionLazyObject::fromInstance($object);

Check failure on line 25 in lib/Doctrine/ORM/Mapping/ReflectionLazyProperty.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm (3.7)

UndefinedClass

lib/Doctrine/ORM/Mapping/ReflectionLazyProperty.php:25:18: UndefinedClass: Class, interface or enum named ReflectionLazyObject does not exist (see https://psalm.dev/019)

Check failure on line 25 in lib/Doctrine/ORM/Mapping/ReflectionLazyProperty.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm (default)

UndefinedClass

lib/Doctrine/ORM/Mapping/ReflectionLazyProperty.php:25:18: UndefinedClass: Class, interface or enum named ReflectionLazyObject does not exist (see https://psalm.dev/019)

Check failure on line 25 in lib/Doctrine/ORM/Mapping/ReflectionLazyProperty.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (3.7, phpstan-dbal3.neon)

Call to static method fromInstance() on an unknown class ReflectionLazyObject.

Check failure on line 25 in lib/Doctrine/ORM/Mapping/ReflectionLazyProperty.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (default, phpstan.neon)

Call to static method fromInstance() on an unknown class ReflectionLazyObject.

if ($r) {
$r->skipProperty($this->name, $this->class);
}
}

$this->wrappedProperty->setValue($object, $value);
}
}
18 changes: 0 additions & 18 deletions lib/Doctrine/ORM/Proxy/InternalProxy.php

This file was deleted.

213 changes: 18 additions & 195 deletions lib/Doctrine/ORM/Proxy/ProxyFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,37 +14,16 @@
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\Proxy;
use ReflectionProperty;
use Symfony\Component\VarExporter\ProxyHelper;

use function array_combine;
use function array_flip;
use function array_intersect_key;
use function assert;
use function bin2hex;
use function chmod;
use function class_alias;
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 ucfirst;

use const DIRECTORY_SEPARATOR;

/**
* This factory is used to create proxy objects for entities at runtime.
Expand Down Expand Up @@ -90,37 +69,9 @@ class ProxyFactory
*/
public const AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED = 4;

private const PROXY_CLASS_TEMPLATE = <<<'EOPHP'
<?php
namespace <namespace>;
/**
* DO NOT EDIT THIS FILE - IT WAS CREATED BY DOCTRINE'S PROXY GENERATOR
*/
class <proxyShortClassName> extends \<className> implements \<baseProxyInterface>
{
<useLazyGhostTrait>
public function __isInitialized(): bool
{
return isset($this->lazyObjectState) && $this->isLazyObjectInitialized();
}
public function __serialize(): array
{
<serializeImpl>
}
}

EOPHP;

/** The UnitOfWork this factory uses to retrieve persisters */
private readonly UnitOfWork $uow;

/** @var self::AUTOGENERATE_* */
private $autoGenerate;

/** The IdentifierFlattener used for manipulating identifiers */
private readonly IdentifierFlattener $identifierFlattener;

Expand Down Expand Up @@ -155,15 +106,14 @@ public function __construct(
}

$this->uow = $em->getUnitOfWork();
$this->autoGenerate = (int) $autoGenerate;
$this->identifierFlattener = new IdentifierFlattener($this->uow, $em->getMetadataFactory());
}

/**
* @param class-string $className
* @param array<mixed> $identifier
*/
public function getProxy(string $className, array $identifier): InternalProxy
public function getProxy(string $className, array $identifier): object
{
$proxyFactory = $this->proxyFactories[$className] ?? $this->getProxyFactory($className);

Expand All @@ -189,10 +139,7 @@ public function generateProxyClasses(array $classes, string|null $proxyDir = nul
continue;
}

$proxyFileName = $this->getProxyFileName($class->getName(), $proxyDir ?: $this->proxyDir);
$proxyClassName = self::generateProxyClassName($class->getName(), $this->proxyNs);

$this->generateProxyClass($class, $proxyFileName, $proxyClassName);
$this->loadProxyClass($class);

++$generated;
}
Expand All @@ -210,13 +157,13 @@ protected function skipClass(ClassMetadata $metadata): bool
/**
* Creates a closure capable of initializing a proxy
*
* @return Closure(InternalProxy, InternalProxy):void
* @return Closure(object, object):void
*
* @throws EntityNotFoundException
*/
private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure
{
return static function (InternalProxy $proxy, InternalProxy $original) use ($entityPersister, $classMetadata, $identifierFlattener): void {
return static function (object $proxy, object $original) use ($entityPersister, $classMetadata, $identifierFlattener): void {
$identifier = $classMetadata->getIdentifierValues($original);
$entity = $entityPersister->loadById($identifier, $original);

Expand All @@ -243,14 +190,6 @@ private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersi
};
}

private function getProxyFileName(string $className, string $baseDirectory): string
{
$baseDirectory = $baseDirectory ?: $this->proxyDir;

return rtrim($baseDirectory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . InternalProxy::MARKER
. str_replace('\\', '', $className) . '.php';
}

private function getProxyFactory(string $className): Closure
{
$skippedProperties = [];
Expand All @@ -267,9 +206,7 @@ private function getProxyFactory(string $className): Closure
continue;
}

$prefix = $property->isPrivate() ? "\0" . $property->class . "\0" : ($property->isProtected() ? "\0*\0" : '');

$skippedProperties[$prefix . $name] = true;
$skippedProperties[] = [$name, $property->class];
}

$filter = ReflectionProperty::IS_PRIVATE;
Expand All @@ -282,10 +219,15 @@ private function getProxyFactory(string $className): Closure
$proxyClassName = $this->loadProxyClass($class);
$identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers);

$proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy {
$proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, &$proxy): void {
$proxyFactory = static function (array $identifier) use ($proxyClassName, $initializer, $skippedProperties, $identifierFields, $className): object {
$proxy = (new \ReflectionClass($proxyClassName))->newInstanceWithoutConstructor();

Check failure on line 223 in lib/Doctrine/ORM/Proxy/ProxyFactory.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm (3.7)

ArgumentTypeCoercion

lib/Doctrine/ORM/Proxy/ProxyFactory.php:223:44: ArgumentTypeCoercion: Argument 1 of ReflectionClass::__construct expects class-string|object|trait-string, but parent type string provided (see https://psalm.dev/193)

Check failure on line 223 in lib/Doctrine/ORM/Proxy/ProxyFactory.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm (default)

ArgumentTypeCoercion

lib/Doctrine/ORM/Proxy/ProxyFactory.php:223:44: ArgumentTypeCoercion: Argument 1 of ReflectionClass::__construct expects class-string|object|trait-string, but parent type string provided (see https://psalm.dev/193)
$r = \ReflectionLazyObject::makeLazy($proxy, static function (object $object) use ($initializer, $proxy): void {

Check failure on line 224 in lib/Doctrine/ORM/Proxy/ProxyFactory.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm (3.7)

UndefinedClass

lib/Doctrine/ORM/Proxy/ProxyFactory.php:224:18: UndefinedClass: Class, interface or enum named ReflectionLazyObject does not exist (see https://psalm.dev/019)

Check failure on line 224 in lib/Doctrine/ORM/Proxy/ProxyFactory.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm (default)

UndefinedClass

lib/Doctrine/ORM/Proxy/ProxyFactory.php:224:18: UndefinedClass: Class, interface or enum named ReflectionLazyObject does not exist (see https://psalm.dev/019)

Check failure on line 224 in lib/Doctrine/ORM/Proxy/ProxyFactory.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (3.7, phpstan-dbal3.neon)

Call to static method makeLazy() on an unknown class ReflectionLazyObject.

Check failure on line 224 in lib/Doctrine/ORM/Proxy/ProxyFactory.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (default, phpstan.neon)

Call to static method makeLazy() on an unknown class ReflectionLazyObject.
$initializer($object, $proxy);
}, $skippedProperties);
}, \ReflectionLazyObject::SKIP_INITIALIZATION_ON_SERIALIZE);

Check failure on line 226 in lib/Doctrine/ORM/Proxy/ProxyFactory.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (3.7, phpstan-dbal3.neon)

Access to constant SKIP_INITIALIZATION_ON_SERIALIZE on an unknown class ReflectionLazyObject.

Check failure on line 226 in lib/Doctrine/ORM/Proxy/ProxyFactory.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (default, phpstan.neon)

Access to constant SKIP_INITIALIZATION_ON_SERIALIZE on an unknown class ReflectionLazyObject.

foreach ($skippedProperties as [$name, $class]) {
$r->skipProperty($name, $class);
}

foreach ($identifierFields as $idField => $reflector) {
if (! isset($identifier[$idField])) {
Expand All @@ -297,138 +239,19 @@ private function getProxyFactory(string $className): Closure
}

return $proxy;
}, null, $proxyClassName);
};

return $this->proxyFactories[$className] = $proxyFactory;
}

private function loadProxyClass(ClassMetadata $class): string
{
$proxyClassName = self::generateProxyClassName($class->getName(), $this->proxyNs);
$proxyClassName = rtrim($this->proxyNs, '\\') . '\\' . Proxy::MARKER . '\\' . ltrim($class->getName(), '\\');

if (class_exists($proxyClassName, false)) {
return $proxyClassName;
if (! class_exists($proxyClassName, false)) {
class_alias($class->getName(), $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|null $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;
}

private function generateSerializeImpl(ClassMetadata $class): string
{
$reflector = $class->getReflectionClass();
$properties = $reflector->hasMethod('__serialize') ? 'parent::__serialize()' : '(array) $this';

$code = '$properties = ' . $properties . ';
unset($properties["\0" . self::class . "\0lazyObjectState"]);
';

if ($reflector->hasMethod('__serialize') || ! $reflector->hasMethod('__sleep')) {
return $code . 'return $properties;';
}

return $code . '$data = [];
foreach (parent::__sleep() as $name) {
$value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0' . $reflector->name . '\0$name"] ?? $k = null;
if (null === $k) {
trigger_error(sprintf(\'serialize(): "%s" returned as member variable from __sleep() but does not exist\', $name), \E_USER_NOTICE);
} else {
$data[$k] = $value;
}
}
return $data;';
}

private static function generateProxyClassName(string $className, string $proxyNamespace): string
{
return rtrim($proxyNamespace, '\\') . '\\' . Proxy::MARKER . '\\' . ltrim($className, '\\');
}
}
Loading

0 comments on commit 3aaaac1

Please sign in to comment.