diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index bad19ab6a..6bc2edcad 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -71,7 +71,12 @@ parameters: path: src/Persistence/RepositoryDecorator.php - - message: "#^If condition is always false\\.$#" + message: "#^Method Zenstruck\\\\Foundry\\\\Persistence\\\\RepositoryDecorator\\:\\:proxyResult\\(\\) should return array\\\\>\\|Zenstruck\\\\Foundry\\\\Persistence\\\\Proxy\\ but returns Zenstruck\\\\Foundry\\\\Proxy\\.$#" + count: 1 + path: src/Persistence/RepositoryDecorator.php + + - + message: "#^PHPDoc tag @return with type Zenstruck\\\\Foundry\\\\Persistence\\\\Proxy\\ is not subtype of native type Zenstruck\\\\Foundry\\\\Proxy\\.$#" count: 1 path: src/Proxy.php diff --git a/src/Factory.php b/src/Factory.php index 555996ed5..f8bf95ac7 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -20,6 +20,7 @@ use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Persistence\PostPersistCallback; use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Proxy as ProxyObject; /** * @template TObject of object @@ -156,17 +157,17 @@ public function create( } if (!$this->isPersisting()) { - return $noProxy ? $object : new Proxy($object); + return $noProxy ? $object : new ProxyObject($object); } - $proxy = new Proxy($object); + $proxy = new ProxyObject($object); if ($this->cascadePersist && !$postPersistCallbacks) { return $proxy; } $proxy->_save() - ->_withoutAutoRefresh(function(Proxy $proxy) use ($attributes, $postPersistCallbacks): void { + ->_withoutAutoRefresh(function(ProxyObject $proxy) use ($attributes, $postPersistCallbacks): void { $callbacks = [...$postPersistCallbacks, ...$this->afterPersist]; if (!$callbacks) { @@ -417,7 +418,7 @@ private function normalizeAttributes(array|callable $attributes): array private function normalizeAttribute(mixed $value, string $name): mixed { - if ($value instanceof Proxy) { + if ($value instanceof ProxyObject) { return $value->isPersisted(calledInternally: true) ? $value->_refresh()->_real() : $value->_real(); } @@ -489,7 +490,7 @@ private static function normalizeObject(object $object): object } try { - return Proxy::createFromPersisted($object)->_refresh()->_real(); + return ProxyObject::createFromPersisted($object)->_refresh()->_real(); } catch (\RuntimeException) { return $object; } diff --git a/src/Persistence/Proxy.php b/src/Persistence/Proxy.php index 9200d9bbf..25ffc4e4d 100644 --- a/src/Persistence/Proxy.php +++ b/src/Persistence/Proxy.php @@ -11,349 +11,38 @@ namespace Zenstruck\Foundry\Persistence; -use Doctrine\ODM\MongoDB\DocumentManager; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\Persistence\ObjectManager; -use Zenstruck\Assert; -use Zenstruck\Callback; -use Zenstruck\Callback\Parameter; -use Zenstruck\Foundry\Factory; -use Zenstruck\Foundry\Instantiator; - /** * @template TProxiedObject of object - * @mixin TProxiedObject * * @author Kevin Bond * * @final */ -class Proxy implements \Stringable +interface Proxy { - /** - * @phpstan-var class-string - */ - private string $class; - - private bool $autoRefresh; - - private bool $persisted = false; - - /** - * @internal - * - * @phpstan-param TProxiedObject $object - */ - public function __construct( - /** @param TProxiedObject $object */ - private object $object - ) { - if ((new \ReflectionClass($object::class))->isFinal()) { - trigger_deprecation( - 'zenstruck\foundry', '1.37.0', - \sprintf('Using a proxy factory with a final class is deprecated and will throw an error in Foundry 2.0. Use "%s" instead, or unfinalize "%s" class.', PersistentProxyObjectFactory::class, $object::class) - ); - } - - $this->class = $object::class; - $this->autoRefresh = Factory::configuration()->defaultProxyAutoRefresh(); - } - - public function __call(string $method, array $arguments) // @phpstan-ignore-line - { - return $this->_real()->{$method}(...$arguments); - } - - public function __get(string $name): mixed - { - return $this->_real()->{$name}; - } - - public function __set(string $name, mixed $value): void - { - $this->_real()->{$name} = $value; - } - - public function __unset(string $name): void - { - unset($this->_real()->{$name}); - } - - public function __isset(string $name): bool - { - return isset($this->_real()->{$name}); - } - - public function __toString(): string - { - $object = $this->_real(); - - if (!\method_exists($object, '__toString')) { - throw new \RuntimeException(\sprintf('Proxied object "%s" cannot be converted to a string.', $this->class)); - } - - return $object->__toString(); - } - - /** - * @internal - * - * @template TObject of object - * @phpstan-param TObject $object - * @phpstan-return Proxy - */ - public static function createFromPersisted(object $object): self - { - $proxy = new self($object); - $proxy->persisted = true; - - return $proxy; - } - - /** - * @deprecated - */ - public function isPersisted(bool $calledInternally = false): bool - { - if (!$calledInternally) { - trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0 without replacement.', __METHOD__)); - } - - return $this->persisted; - } - - /** - * @return TProxiedObject - * - * @deprecated - */ - public function object(): object - { - trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_real()" instead.', __METHOD__, self::class)); - - return $this->_real(); - } - /** * @return TProxiedObject */ - public function _real(): object - { - if (!$this->autoRefresh || !$this->persisted || !Factory::configuration()->isFlushingEnabled() || !Factory::configuration()->isPersistEnabled()) { - return $this->object; - } - - $om = $this->objectManager(); - - // only check for changes if the object is managed in the current om - if (($om instanceof EntityManagerInterface || $om instanceof DocumentManager) && $om->contains($this->object)) { - // cannot use UOW::recomputeSingleEntityChangeSet() here as it wrongly computes embedded objects as changed - $om->getUnitOfWork()->computeChangeSet($om->getClassMetadata($this->class), $this->object); - - if ( - ($om instanceof EntityManagerInterface && !empty($om->getUnitOfWork()->getEntityChangeSet($this->object))) - || ($om instanceof DocumentManager && !empty($om->getUnitOfWork()->getDocumentChangeSet($this->object)))) { - throw new \RuntimeException(\sprintf('Cannot auto refresh "%s" as there are unsaved changes. Be sure to call ->save() or disable auto refreshing (see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#auto-refresh for details).', $this->class)); - } - } - - $this->_refresh(); - - return $this->object; - } - - /** - * @deprecated - */ - public function save(): static - { - trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_save()" instead.', __METHOD__, self::class)); - - return $this->_save(); - } - - public function _save(): static - { - $this->objectManager()->persist($this->object); - - if (Factory::configuration()->isFlushingEnabled()) { - $this->objectManager()->flush(); - } + public function _real(): object; - $this->persisted = true; + public function _save(): static; - return $this; - } + public function _delete(): static; - /** - * @deprecated - */ - public function remove(): static - { - trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_delete()" instead.', __METHOD__, self::class)); + public function _refresh(): static; - return $this->_delete(); - } + public function _set(string $property, mixed $value): static; - public function _delete(): static - { - $this->objectManager()->remove($this->object); - $this->objectManager()->flush(); - $this->autoRefresh = false; - $this->persisted = false; - - return $this; - } + public function _get(string $property): mixed; /** - * @deprecated + * @return RepositoryDecorator */ - public function refresh(): static - { - trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_refresh()" instead.', __METHOD__, self::class)); - - return $this->_refresh(); - } - - public function _refresh(): static - { - if (!Factory::configuration()->isPersistEnabled()) { - return $this; - } - - if (!$this->persisted) { - throw new \RuntimeException(\sprintf('Cannot refresh unpersisted object (%s).', $this->class)); - } - - if ($this->objectManager()->contains($this->object)) { - $this->objectManager()->refresh($this->object); - - return $this; - } + public function _repository(): RepositoryDecorator; - if (!$object = $this->fetchObject()) { - throw new \RuntimeException('The object no longer exists.'); - } + public function _enableAutoRefresh(): static; - $this->object = $object; - - return $this; - } - - /** - * @deprecated - */ - public function forceSet(string $property, mixed $value): static - { - trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_set()" instead.', __METHOD__, self::class)); - - return $this->_set($property, $value); - } - - public function _set(string $property, mixed $value): static - { - $object = $this->_real(); - - Instantiator::forceSet($object, $property, $value); - - return $this; - } - - /** - * @deprecated - */ - public function forceSetAll(array $properties): static - { - trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0 without replacement.', __METHOD__)); - - $object = $this->_real(); - - foreach ($properties as $property => $value) { - Instantiator::forceSet($object, $property, $value); - } - - return $this; - } - - /** - * @deprecated - */ - public function get(string $property): mixed - { - trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_get()" instead.', __METHOD__, self::class)); - - return $this->_get($property); - } - - public function _get(string $property): mixed - { - return Instantiator::forceGet($this->_real(), $property); - } - - /** - * @deprecated - */ - public function repository(): RepositoryDecorator - { - trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_repository()" instead.', __METHOD__, self::class)); - - return $this->_repository(); - } - - public function _repository(): RepositoryDecorator - { - return Factory::configuration()->repositoryFor($this->class); - } - - /** - * @deprecated - */ - public function enableAutoRefresh(): static - { - trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_enableAutoRefresh()" instead.', __METHOD__, self::class)); - - return $this->_enableAutoRefresh(); - } - - public function _enableAutoRefresh(): static - { - if (!$this->persisted) { - throw new \RuntimeException(\sprintf('Cannot enable auto-refresh on unpersisted object (%s).', $this->class)); - } - - $this->autoRefresh = true; - - return $this; - } - - /** - * @deprecated - */ - public function disableAutoRefresh(): static - { - trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_disableAutoRefresh()" instead.', __METHOD__, self::class)); - - return $this->_disableAutoRefresh(); - } - - public function _disableAutoRefresh(): static - { - $this->autoRefresh = false; - - return $this; - } - - /** - * @param callable $callback (object|Proxy $object): void - * - * @deprecated - */ - public function withoutAutoRefresh(callable $callback): static - { - trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_withoutAutoRefresh()" instead.', __METHOD__, self::class)); - - return $this->_withoutAutoRefresh($callback); - } + public function _disableAutoRefresh(): static; /** * Ensures "autoRefresh" is disabled when executing $callback. Re-enables @@ -361,82 +50,5 @@ public function withoutAutoRefresh(callable $callback): static * * @param callable $callback (object|Proxy $object): void */ - public function _withoutAutoRefresh(callable $callback): static - { - $original = $this->autoRefresh; - $this->autoRefresh = false; - - $this->executeCallback($callback); - - $this->autoRefresh = $original; // set to original value (even if it was false) - - return $this; - } - - /** - * @deprecated - */ - public function assertPersisted(string $message = '{entity} is not persisted.'): self - { - trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0 without replacement.', __METHOD__)); - - Assert::that($this->fetchObject())->isNotEmpty($message, ['entity' => $this->class]); - - return $this; - } - - /** - * @deprecated - */ - public function assertNotPersisted(string $message = '{entity} is persisted but it should not be.'): self - { - trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0 without replacement.', __METHOD__)); - - Assert::that($this->fetchObject())->isEmpty($message, ['entity' => $this->class]); - - return $this; - } - - /** - * @internal - */ - public function executeCallback(callable $callback, mixed ...$arguments): void - { - Callback::createFor($callback)->invoke( - Parameter::union( - Parameter::untyped($this), - Parameter::typed(self::class, $this), - Parameter::typed($this->class, Parameter::factory(fn(): object => $this->_real())) - )->optional(), - ...$arguments - ); - } - - /** - * @phpstan-return TProxiedObject|null - */ - private function fetchObject(): ?object - { - $objectManager = $this->objectManager(); - - if ($objectManager instanceof DocumentManager) { - $classMetadata = $objectManager->getClassMetadata($this->class); - if (!$classMetadata->isEmbeddedDocument) { - $id = $classMetadata->getIdentifierValue($this->object); - } - } else { - $id = $objectManager->getClassMetadata($this->class)->getIdentifierValues($this->object); - } - - return empty($id) ? null : $objectManager->find($this->class, $id); - } - - private function objectManager(): ObjectManager - { - if (!Factory::configuration()->isPersistEnabled()) { - throw new \RuntimeException('Should not get doctrine\'s object manager when persist is disabled.'); - } - - return Factory::configuration()->objectManagerFor($this->class); - } + public function _withoutAutoRefresh(callable $callback): static; } diff --git a/src/Persistence/RepositoryDecorator.php b/src/Persistence/RepositoryDecorator.php index 1343b7eba..2d6ff5c44 100644 --- a/src/Persistence/RepositoryDecorator.php +++ b/src/Persistence/RepositoryDecorator.php @@ -24,6 +24,7 @@ use Doctrine\Persistence\ObjectRepository; use Symfony\Component\PropertyAccess\PropertyAccess; use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\Proxy as ProxyObject; /** * @mixin EntityRepository @@ -437,7 +438,7 @@ private function proxyResult(mixed $result) } if ($result && \is_a($result, $this->getClassName())) { - return Proxy::createFromPersisted($result); + return ProxyObject::createFromPersisted($result); } return $result; diff --git a/src/Proxy.php b/src/Proxy.php index 4c0d2db7f..658f04c73 100644 --- a/src/Proxy.php +++ b/src/Proxy.php @@ -11,20 +11,426 @@ namespace Zenstruck\Foundry; +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\ObjectManager; +use Zenstruck\Assert; +use Zenstruck\Callback; +use Zenstruck\Callback\Parameter; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Persistence\Proxy as ProxyBase; +use Zenstruck\Foundry\Persistence\RepositoryDecorator; -trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Class "%s" is deprecated and will be removed in version 2.0. Use "%s" instead.', Proxy::class, ProxyBase::class)); +/** + * @deprecated + * + * @template TProxiedObject of object + * @implements ProxyBase + * + * @mixin TProxiedObject + * + * @author Kevin Bond + */ +final class Proxy implements \Stringable, ProxyBase +{ + /** + * @phpstan-var class-string + */ + private string $class; + + private bool $autoRefresh; + + private bool $persisted = false; + + /** + * @internal + * + * @phpstan-param TProxiedObject $object + */ + public function __construct( + /** @param TProxiedObject $object */ + private object $object + ) { + if ((new \ReflectionClass($object::class))->isFinal()) { + trigger_deprecation( + 'zenstruck\foundry', '1.37.0', + \sprintf('Using a proxy factory with a final class is deprecated and will throw an error in Foundry 2.0. Use "%s" instead, or unfinalize "%s" class.', PersistentProxyObjectFactory::class, $object::class) + ); + } + + $this->class = $object::class; + $this->autoRefresh = Factory::configuration()->defaultProxyAutoRefresh(); + } + + public function __call(string $method, array $arguments) // @phpstan-ignore-line + { + return $this->_real()->{$method}(...$arguments); + } + + public function __get(string $name): mixed + { + return $this->_real()->{$name}; + } + + public function __set(string $name, mixed $value): void + { + $this->_real()->{$name} = $value; + } + + public function __unset(string $name): void + { + unset($this->_real()->{$name}); + } + + public function __isset(string $name): bool + { + return isset($this->_real()->{$name}); + } + + public function __toString(): string + { + $object = $this->_real(); + + if (!\method_exists($object, '__toString')) { + throw new \RuntimeException(\sprintf('Proxied object "%s" cannot be converted to a string.', $this->class)); + } + + return $object->__toString(); + } + + /** + * @internal + * + * @template TObject of object + * @phpstan-param TObject $object + * @phpstan-return ProxyBase + */ + public static function createFromPersisted(object $object): self + { + $proxy = new self($object); + $proxy->persisted = true; + + return $proxy; + } -if (false) { /** - * @template TProxiedObject of object - * @mixin TProxiedObject + * @deprecated + */ + public function isPersisted(bool $calledInternally = false): bool + { + if (!$calledInternally) { + trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0 without replacement.', __METHOD__)); + } + + return $this->persisted; + } + + /** + * @return TProxiedObject * * @deprecated + */ + public function object(): object + { + trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_real()" instead.', __METHOD__, self::class)); + + return $this->_real(); + } + + public function _real(): object + { + if (!$this->autoRefresh || !$this->persisted || !Factory::configuration()->isFlushingEnabled() || !Factory::configuration()->isPersistEnabled()) { + return $this->object; + } + + $om = $this->objectManager(); + + // only check for changes if the object is managed in the current om + if (($om instanceof EntityManagerInterface || $om instanceof DocumentManager) && $om->contains($this->object)) { + // cannot use UOW::recomputeSingleEntityChangeSet() here as it wrongly computes embedded objects as changed + $om->getUnitOfWork()->computeChangeSet($om->getClassMetadata($this->class), $this->object); + + if ( + ($om instanceof EntityManagerInterface && !empty($om->getUnitOfWork()->getEntityChangeSet($this->object))) + || ($om instanceof DocumentManager && !empty($om->getUnitOfWork()->getDocumentChangeSet($this->object)))) { + throw new \RuntimeException(\sprintf('Cannot auto refresh "%s" as there are unsaved changes. Be sure to call ->save() or disable auto refreshing (see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#auto-refresh for details).', $this->class)); + } + } + + $this->_refresh(); + + return $this->object; + } + + /** + * @deprecated + */ + public function save(): static + { + trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_save()" instead.', __METHOD__, self::class)); + + return $this->_save(); + } + + public function _save(): static + { + $this->objectManager()->persist($this->object); + + if (Factory::configuration()->isFlushingEnabled()) { + $this->objectManager()->flush(); + } + + $this->persisted = true; + + return $this; + } + + /** + * @deprecated + */ + public function remove(): static + { + trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_delete()" instead.', __METHOD__, self::class)); + + return $this->_delete(); + } + + public function _delete(): static + { + $this->objectManager()->remove($this->object); + $this->objectManager()->flush(); + $this->autoRefresh = false; + $this->persisted = false; + + return $this; + } + + /** + * @deprecated + */ + public function refresh(): static + { + trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_refresh()" instead.', __METHOD__, self::class)); + + return $this->_refresh(); + } + + public function _refresh(): static + { + if (!Factory::configuration()->isPersistEnabled()) { + return $this; + } + + if (!$this->persisted) { + throw new \RuntimeException(\sprintf('Cannot refresh unpersisted object (%s).', $this->class)); + } + + if ($this->objectManager()->contains($this->object)) { + $this->objectManager()->refresh($this->object); + + return $this; + } + + if (!$object = $this->fetchObject()) { + throw new \RuntimeException('The object no longer exists.'); + } + + $this->object = $object; + + return $this; + } + + /** + * @deprecated + */ + public function forceSet(string $property, mixed $value): static + { + trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_set()" instead.', __METHOD__, self::class)); + + return $this->_set($property, $value); + } + + public function _set(string $property, mixed $value): static + { + $object = $this->_real(); + + Instantiator::forceSet($object, $property, $value); + + return $this; + } + + /** + * @deprecated + */ + public function forceSetAll(array $properties): static + { + trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0 without replacement.', __METHOD__)); + + $object = $this->_real(); + + foreach ($properties as $property => $value) { + Instantiator::forceSet($object, $property, $value); + } + + return $this; + } + + /** + * @deprecated + */ + public function get(string $property): mixed + { + trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_get()" instead.', __METHOD__, self::class)); + + return $this->_get($property); + } + + public function _get(string $property): mixed + { + return Instantiator::forceGet($this->_real(), $property); + } + + /** + * @deprecated + */ + public function repository(): RepositoryDecorator + { + trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_repository()" instead.', __METHOD__, self::class)); + + return $this->_repository(); + } + + public function _repository(): RepositoryDecorator + { + return Factory::configuration()->repositoryFor($this->class); + } + + /** + * @deprecated + */ + public function enableAutoRefresh(): static + { + trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_enableAutoRefresh()" instead.', __METHOD__, self::class)); + + return $this->_enableAutoRefresh(); + } + + public function _enableAutoRefresh(): static + { + if (!$this->persisted) { + throw new \RuntimeException(\sprintf('Cannot enable auto-refresh on unpersisted object (%s).', $this->class)); + } + + $this->autoRefresh = true; + + return $this; + } + + /** + * @deprecated + */ + public function disableAutoRefresh(): static + { + trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_disableAutoRefresh()" instead.', __METHOD__, self::class)); + + return $this->_disableAutoRefresh(); + } + + public function _disableAutoRefresh(): static + { + $this->autoRefresh = false; + + return $this; + } + + /** + * @param callable $callback (object|Proxy $object): void * - * @author Kevin Bond + * @deprecated + */ + public function withoutAutoRefresh(callable $callback): static + { + trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_withoutAutoRefresh()" instead.', __METHOD__, self::class)); + + return $this->_withoutAutoRefresh($callback); + } + + public function _withoutAutoRefresh(callable $callback): static + { + $original = $this->autoRefresh; + $this->autoRefresh = false; + + $this->executeCallback($callback); + + $this->autoRefresh = $original; // set to original value (even if it was false) + + return $this; + } + + /** + * @deprecated */ - final class Proxy + public function assertPersisted(string $message = '{entity} is not persisted.'): self { + trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0 without replacement.', __METHOD__)); + + Assert::that($this->fetchObject())->isNotEmpty($message, ['entity' => $this->class]); + + return $this; + } + + /** + * @deprecated + */ + public function assertNotPersisted(string $message = '{entity} is persisted but it should not be.'): self + { + trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in 2.0 without replacement.', __METHOD__)); + + Assert::that($this->fetchObject())->isEmpty($message, ['entity' => $this->class]); + + return $this; + } + + /** + * @internal + */ + public function executeCallback(callable $callback, mixed ...$arguments): void + { + Callback::createFor($callback)->invoke( + Parameter::union( + Parameter::untyped($this), + Parameter::typed(self::class, $this), + Parameter::typed($this->class, Parameter::factory(fn(): object => $this->_real())) + )->optional(), + ...$arguments + ); + } + + /** + * @phpstan-return TProxiedObject|null + */ + private function fetchObject(): ?object + { + $objectManager = $this->objectManager(); + + if ($objectManager instanceof DocumentManager) { + $classMetadata = $objectManager->getClassMetadata($this->class); + if (!$classMetadata->isEmbeddedDocument) { + $id = $classMetadata->getIdentifierValue($this->object); + } + } else { + $id = $objectManager->getClassMetadata($this->class)->getIdentifierValues($this->object); + } + + return empty($id) ? null : $objectManager->find($this->class, $id); + } + + private function objectManager(): ObjectManager + { + if (!Factory::configuration()->isPersistEnabled()) { + throw new \RuntimeException('Should not get doctrine\'s object manager when persist is disabled.'); + } + + return Factory::configuration()->objectManagerFor($this->class); } } diff --git a/src/Story.php b/src/Story.php index 4e35f169c..3022ade96 100644 --- a/src/Story.php +++ b/src/Story.php @@ -12,6 +12,7 @@ namespace Zenstruck\Foundry; use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Proxy as ProxyObject; /** * @author Kevin Bond @@ -179,8 +180,8 @@ private static function normalizeObject(object $object): Proxy } // ensure objects are proxied - if (!$object instanceof Proxy) { - $object = new Proxy($object); + if (!$object instanceof ProxyObject) { + $object = new ProxyObject($object); } // ensure proxies are persisted diff --git a/src/deprecations.php b/src/deprecations.php index 9048a4bef..a37e20a30 100644 --- a/src/deprecations.php +++ b/src/deprecations.php @@ -14,4 +14,3 @@ \class_alias(\Zenstruck\Foundry\Persistence\RepositoryAssertions::class, \Zenstruck\Foundry\RepositoryAssertions::class); \class_alias(RepositoryDecorator::class, RepositoryProxy::class); -\class_alias(\Zenstruck\Foundry\Persistence\Proxy::class, \Zenstruck\Foundry\Proxy::class); diff --git a/src/functions.php b/src/functions.php index 48355b16a..4a778d56a 100644 --- a/src/functions.php +++ b/src/functions.php @@ -13,6 +13,7 @@ use Faker; use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Proxy as ProxyObject; use Zenstruck\Foundry\Persistence\RepositoryDecorator; use function Zenstruck\Foundry\Persistence\persist_proxy; @@ -104,7 +105,7 @@ function instantiate(string $class, array|callable $attributes = []): Proxy { trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Function "%s()" is deprecated and will be removed in Foundry 2.0. Use "%s::object()" instead.', __FUNCTION__, __NAMESPACE__)); - return new Proxy(object($class, $attributes)); + return new ProxyObject(object($class, $attributes)); } /** diff --git a/tests/Unit/FactoryTest.php b/tests/Unit/FactoryTest.php index 9c020d2e6..27274c348 100644 --- a/tests/Unit/FactoryTest.php +++ b/tests/Unit/FactoryTest.php @@ -19,6 +19,7 @@ use Zenstruck\Foundry\Factory; use Zenstruck\Foundry\LazyValue; use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Proxy as ProxyObject; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Tests\Fixtures\Entity\Category; use Zenstruck\Foundry\Tests\Fixtures\Entity\Post; @@ -286,7 +287,7 @@ public function instantiating_with_proxy_attribute_normalizes_to_underlying_obje $object = persistent_factory(Post::class)->create([ 'title' => 'title', 'body' => 'body', - 'category' => new Proxy(new Category()), + 'category' => new ProxyObject(new Category()), ]); $this->assertInstanceOf(Category::class, $object->getCategory()); diff --git a/tests/Unit/ProxyTest.php b/tests/Unit/ProxyTest.php index b80db628e..77af2ebc8 100644 --- a/tests/Unit/ProxyTest.php +++ b/tests/Unit/ProxyTest.php @@ -16,6 +16,7 @@ use PHPUnit\Framework\TestCase; use Zenstruck\Foundry\Factory; use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Proxy as ProxyObject; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Tests\Fixtures\Entity\Category; @@ -31,7 +32,7 @@ final class ProxyTest extends TestCase */ public function can_force_get_and_set_non_public_properties(): void { - $proxy = new Proxy(new Category()); + $proxy = new ProxyObject(new Category()); $this->assertNull($proxy->_get('name')); @@ -45,7 +46,7 @@ public function can_force_get_and_set_non_public_properties(): void */ public function can_access_wrapped_objects_properties(): void { - $proxy = new Proxy(new class() { + $proxy = new ProxyObject(new class() { public $property; }); @@ -70,7 +71,7 @@ public function cannot_refresh_unpersisted_proxy(): void $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Cannot refresh unpersisted object (Zenstruck\Foundry\Tests\Fixtures\Entity\Category).'); - (new Proxy(new Category()))->_refresh(); + (new ProxyObject(new Category()))->_refresh(); } /** @@ -88,7 +89,7 @@ public function saving_unpersisted_proxy_changes_it_to_a_persisted_proxy(): void Factory::configuration()->setManagerRegistry($registry)->enableDefaultProxyAutoRefresh(); - $category = new Proxy(new Category()); + $category = new ProxyObject(new Category()); $this->assertFalse($category->isPersisted()); @@ -102,7 +103,7 @@ public function saving_unpersisted_proxy_changes_it_to_a_persisted_proxy(): void */ public function can_use_without_auto_refresh_with_proxy_or_object_typehint(): void { - $proxy = new Proxy(new Category()); + $proxy = new ProxyObject(new Category()); $calls = 0; $proxy @@ -136,7 +137,7 @@ public function can_use_without_auto_refresh_with_proxy_or_object_typehint(): vo */ public function can_use_new_class_as_legacy_one(): void { - $proxy = new Proxy(new Category()); + $proxy = new ProxyObject(new Category()); self::assertInstanceOf(\Zenstruck\Foundry\Proxy::class, $proxy); self::assertInstanceOf(\Zenstruck\Foundry\Persistence\Proxy::class, $proxy);