<?php
declare(strict_types=1);
namespace Doctrine\ORM\Proxy;
use Closure;
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\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityNotFoundException;
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 ReflectionProperty;
use Symfony\Component\VarExporter\ProxyHelper;
use Symfony\Component\VarExporter\VarExporter;
use Throwable;
use function array_flip;
use function str_replace;
use function strpos;
use function substr;
use function uksort;
/**
* This factory is used to create proxy objects for entities at runtime.
*/
class ProxyFactory extends AbstractProxyFactory
{
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 __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();
}
public function __serialize(): array
{
<serializeImpl>
}
}
EOPHP;
/** @var EntityManagerInterface The EntityManager this factory is bound to. */
private $em;
/** @var UnitOfWork The UnitOfWork this factory uses to retrieve persisters */
private $uow;
/** @var string */
private $proxyNs;
/**
* The IdentifierFlattener used for manipulating identifiers
*
* @var IdentifierFlattener
*/
private $identifierFlattener;
/** @var ProxyDefinition[] */
private $definitions = [];
/**
* Initializes a new instance of the <tt>ProxyFactory</tt> class that is
* connected to the given <tt>EntityManager</tt>.
*
* @param EntityManagerInterface $em The EntityManager the new factory works for.
* @param string $proxyDir The directory to use for the proxy classes. It must exist.
* @param string $proxyNs The namespace to use for the proxy classes.
* @param bool|self::AUTOGENERATE_* $autoGenerate The strategy for automatically generating proxy classes.
*/
public function __construct(EntityManagerInterface $em, $proxyDir, $proxyNs, $autoGenerate = self::AUTOGENERATE_NEVER)
{
$proxyGenerator = new ProxyGenerator($proxyDir, $proxyNs);
if ($em->getConfiguration()->isLazyGhostObjectEnabled()) {
$proxyGenerator->setPlaceholder('baseProxyInterface', 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 {
$proxyGenerator->setPlaceholder('baseProxyInterface', LegacyProxy::class);
}
parent::__construct($proxyGenerator, $em->getMetadataFactory(), $autoGenerate);
$this->em = $em;
$this->uow = $em->getUnitOfWork();
$this->proxyNs = $proxyNs;
$this->identifierFlattener = new IdentifierFlattener($this->uow, $em->getMetadataFactory());
}
/**
* {@inheritDoc}
*/
public function getProxy($className, array $identifier)
{
$proxy = parent::getProxy($className, $identifier);
if (! $this->em->getConfiguration()->isLazyGhostObjectEnabled()) {
return $proxy;
}
$initializer = $this->definitions[$className]->initializer;
$proxy->__construct(static function (InternalProxy $object) use ($initializer, $proxy): void {
$initializer($object, $proxy);
});
return $proxy;
}
/**
* {@inheritDoc}
*/
protected function skipClass(ClassMetadata $metadata)
{
return $metadata->isMappedSuperclass
|| $metadata->isEmbeddedClass
|| $metadata->getReflectionClass()->isAbstract();
}
/**
* {@inheritDoc}
*/
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);
}
return $this->definitions[$className] = new ProxyDefinition(
ClassUtils::generateProxyClassName($className, $this->proxyNs),
$classMetadata->getIdentifierFieldNames(),
$classMetadata->getReflectionProperties(),
$initializer,
$cloner
);
}
/**
* Creates a closure capable of initializing a proxy
*
* @psalm-return Closure(CommonProxy):void
*
* @throws EntityNotFoundException
*/
private function createInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister): Closure
{
$wakeupProxy = $classMetadata->getReflectionClass()->hasMethod('__wakeup');
return function (CommonProxy $proxy) use ($entityPersister, $classMetadata, $wakeupProxy): void {
$initializer = $proxy->__getInitializer();
$cloner = $proxy->__getCloner();
$proxy->__setInitializer(null);
$proxy->__setCloner(null);
if ($proxy->__isInitialized()) {
return;
}
$properties = $proxy->__getLazyProperties();
foreach ($properties as $propertyName => $property) {
if (! isset($proxy->$propertyName)) {
$proxy->$propertyName = $properties[$propertyName];
}
}
$proxy->__setInitialized(true);
if ($wakeupProxy) {
$proxy->__wakeup();
}
$identifier = $classMetadata->getIdentifierValues($proxy);
try {
$entity = $entityPersister->loadById($identifier, $proxy);
} catch (Throwable $exception) {
$proxy->__setInitializer($initializer);
$proxy->__setCloner($cloner);
$proxy->__setInitialized(false);
throw $exception;
}
if ($entity === null) {
$proxy->__setInitializer($initializer);
$proxy->__setCloner($cloner);
$proxy->__setInitialized(false);
throw EntityNotFoundException::fromClassNameAndIdentifier(
$classMetadata->getName(),
$this->identifierFlattener->flattenIdentifier($classMetadata, $identifier)
);
}
};
}
/**
* Creates a closure capable of initializing a proxy
*
* @return Closure(InternalProxy, InternalProxy):void
*
* @throws EntityNotFoundException
*/
private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister): Closure
{
return function (InternalProxy $proxy, InternalProxy $original) use ($entityPersister, $classMetadata): void {
$identifier = $classMetadata->getIdentifierValues($original);
$entity = $entityPersister->loadById($identifier, $original);
if ($entity === null) {
throw EntityNotFoundException::fromClassNameAndIdentifier(
$classMetadata->getName(),
$this->identifierFlattener->flattenIdentifier($classMetadata, $identifier)
);
}
if ($proxy === $original) {
return;
}
$class = $entityPersister->getClassMetadata();
foreach ($class->getReflectionProperties() as $property) {
if (! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) {
continue;
}
$property->setAccessible(true);
$property->setValue($proxy, $property->getValue($entity));
}
};
}
/**
* Creates a closure capable of finalizing state a cloned proxy
*
* @psalm-return Closure(CommonProxy):void
*
* @throws EntityNotFoundException
*/
private function createCloner(ClassMetadata $classMetadata, EntityPersister $entityPersister): Closure
{
return function (CommonProxy $proxy) use ($entityPersister, $classMetadata): void {
if ($proxy->__isInitialized()) {
return;
}
$proxy->__setInitialized(true);
$proxy->__setInitializer(null);
$class = $entityPersister->getClassMetadata();
$identifier = $classMetadata->getIdentifierValues($proxy);
$original = $entityPersister->loadById($identifier);
if ($original === null) {
throw EntityNotFoundException::fromClassNameAndIdentifier(
$classMetadata->getName(),
$this->identifierFlattener->flattenIdentifier($classMetadata, $identifier)
);
}
foreach ($class->getReflectionProperties() as $property) {
if (! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) {
continue;
}
$property->setAccessible(true);
$property->setValue($proxy, $property->getValue($original));
}
};
}
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 generateSkippedProperties(ClassMetadata $class): string
{
$skippedProperties = [];
$identifiers = array_flip($class->getIdentifierFieldNames());
$filter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE;
$reflector = $class->getReflectionClass();
while ($reflector) {
foreach ($reflector->getProperties($filter) as $property) {
$name = $property->name;
if ($property->isStatic() || (($class->hasField($name) || $class->hasAssociation($name)) && ! isset($identifiers[$name]))) {
continue;
}
$prefix = $property->isPrivate() ? "\0" . $property->class . "\0" : ($property->isProtected() ? "\0*\0" : '');
$skippedProperties[$prefix . $name] = true;
}
$filter = ReflectionProperty::IS_PRIVATE;
$reflector = $reflector->getParentClass();
}
uksort($skippedProperties, 'strnatcmp');
$code = VarExporter::export($skippedProperties);
$code = str_replace(VarExporter::export($class->getName()), 'parent::class', $code);
$code = str_replace("\n", "\n ", $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"], $properties[\'__isCloning\']);
';
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;';
}
}