-
-
Notifications
You must be signed in to change notification settings - Fork 111
feat: use ghost objects for auto refresh mechanism #967
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /* | ||
| * This file is part of the zenstruck/foundry package. | ||
| * | ||
| * (c) Kevin Bond <[email protected]> | ||
| * | ||
| * For the full copyright and license information, please view the LICENSE | ||
| * file that was distributed with this source code. | ||
| */ | ||
|
|
||
| namespace Zenstruck\Foundry\Persistence\Exception; | ||
|
|
||
| final class ObjectHasUnsavedChanges extends RefreshObjectFailed | ||
| { | ||
| public function __construct(string $objectClass) | ||
| { | ||
| parent::__construct( | ||
| "Cannot auto refresh \"{$objectClass}\" 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)." | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /* | ||
| * This file is part of the zenstruck/foundry package. | ||
| * | ||
| * (c) Kevin Bond <[email protected]> | ||
| * | ||
| * For the full copyright and license information, please view the LICENSE | ||
| * file that was distributed with this source code. | ||
| */ | ||
|
|
||
| namespace Zenstruck\Foundry\Persistence\Exception; | ||
|
|
||
| final class ObjectNoLongerExist extends RefreshObjectFailed | ||
| { | ||
| public function __construct(public readonly object $originalObject) | ||
| { | ||
| parent::__construct('object no longer exists...'); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,7 +16,7 @@ | |
| use Zenstruck\Foundry\Configuration; | ||
| use Zenstruck\Foundry\Exception\PersistenceNotAvailable; | ||
| use Zenstruck\Foundry\Object\Hydrator; | ||
| use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed; | ||
| use Zenstruck\Foundry\Persistence\Exception\ObjectNoLongerExist; | ||
|
|
||
| /** | ||
| * @author Kevin Bond <[email protected]> | ||
|
|
@@ -152,10 +152,7 @@ private function _autoRefresh(): void | |
| // we don't want that "transparent" calls to _refresh() to trigger a PersistenceNotAvailable exception | ||
| // or a RefreshObjectFailed exception when the object was deleted | ||
| $this->_refresh(); | ||
| } catch (PersistenceNotAvailable|RefreshObjectFailed $e) { | ||
| if ($e instanceof RefreshObjectFailed && false === $e->objectWasDeleted()) { | ||
| throw $e; | ||
| } | ||
| } catch (PersistenceNotAvailable|ObjectNoLongerExist) { | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,8 +16,11 @@ | |
| use Doctrine\Persistence\ObjectRepository; | ||
| use Zenstruck\Foundry\Configuration; | ||
| use Zenstruck\Foundry\Exception\PersistenceNotAvailable; | ||
| use Zenstruck\Foundry\Object\Hydrator; | ||
| use Zenstruck\Foundry\ORM\AbstractORMPersistenceStrategy; | ||
| use Zenstruck\Foundry\Persistence\Exception\NoPersistenceStrategy; | ||
| use Zenstruck\Foundry\Persistence\Exception\ObjectHasUnsavedChanges; | ||
| use Zenstruck\Foundry\Persistence\Exception\ObjectNoLongerExist; | ||
| use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed; | ||
| use Zenstruck\Foundry\Persistence\Relationship\RelationshipMetadata; | ||
| use Zenstruck\Foundry\Persistence\ResetDatabase\ResetDatabaseManager; | ||
|
|
@@ -140,14 +143,53 @@ public function flush(ObjectManager $om): void | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * @template T of object | ||
| * | ||
| * @param T $object | ||
| */ | ||
| public function autorefresh(object $object, object $clone): void | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I decided to split "refresh" and "autorefresh" mechanisms, because they does not involve the same problems, and the autorefresh should not manipulate the object reference (otherwise, we're exposed to identity problems) |
||
| { | ||
| $strategy = $this->strategyFor($object::class); | ||
| $om = $strategy->objectManagerFor($object::class); | ||
|
|
||
| $id = $strategy->getIdentifierValues($clone); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we need the clone here, because it still has is "identifier fields" hydrated |
||
|
|
||
| if ($id) { | ||
| try { | ||
| $om->refresh($object); | ||
|
|
||
| if ($strategy->getIdentifierValues($object)) { | ||
| // no identifier values means the object no longer exists | ||
| return; | ||
| } | ||
| } catch (\Throwable $e) { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really don't like to do this, but, this was the only way to have something working in all situations, without being to tight with Doctrine |
||
| } | ||
|
|
||
| // let's detach the object, in order to prevent Doctrine cache | ||
| $om->detach($object); | ||
| if ($refreshedObject = $om->find($object::class, $id)) { | ||
| Hydrator::hydrateFromOtherObject($object, $refreshedObject); | ||
|
|
||
| return; | ||
| } | ||
| } | ||
|
|
||
| // the object no longer exists | ||
| Hydrator::hydrateFromOtherObject($object, $clone); | ||
| $om->detach($object); | ||
|
Comment on lines
+178
to
+180
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if the object no longer exist, we still need to return something, otherwise, the lazy ghost will be re-initialized as an empty shell. For this purpose we're using a clone of the object |
||
| } | ||
|
|
||
| /** | ||
| * @template T of object | ||
| * | ||
| * @param T $object | ||
| * | ||
| * @return T | ||
| * | ||
| * @throws RefreshObjectFailed | ||
| */ | ||
| public function refresh(object &$object, bool $force = false, bool $allowRefreshDeletedObject = false): object | ||
| public function refresh(object &$object, bool $force = false): object | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've actually reset the |
||
| { | ||
| if (!$this->flush && !$force) { | ||
| return $object; | ||
|
|
@@ -167,45 +209,36 @@ public function refresh(object &$object, bool $force = false, bool $allowRefresh | |
|
|
||
| $strategy = $this->strategyFor($object::class); | ||
|
|
||
| if ($strategy->hasChanges($object)) { | ||
| throw RefreshObjectFailed::objectHasUnsavedChanges($object::class); | ||
| } | ||
|
|
||
| $om = $strategy->objectManagerFor($object::class); | ||
|
|
||
| if ($strategy->isEmbeddable($object)) { | ||
| return $object; | ||
| } | ||
|
|
||
| if (!$strategy->contains($object)) { | ||
| $objectFromDb = null; | ||
| if ($strategy->hasChanges($object)) { | ||
| throw new ObjectHasUnsavedChanges($object::class); | ||
| } | ||
|
|
||
| $id = $om->getClassMetadata($object::class)->getIdentifierValues($object); | ||
| $om = $strategy->objectManagerFor($object::class); | ||
|
|
||
| if ($id) { | ||
| // "merge" object if it is not managed | ||
| $objectFromDb = $om->find($object::class, $id); | ||
| if ($strategy->contains($object)) { | ||
| try { | ||
| $om->refresh($object); | ||
| } catch (\LogicException|\Error) { | ||
| // prevent entities/documents with readonly properties to create an error | ||
| // LogicException is for ORM / Error is for ODM | ||
| // @see https://github.com/doctrine/orm/issues/9505 | ||
| } | ||
|
|
||
| if ($objectFromDb) { | ||
| $object = $objectFromDb; | ||
| } else { | ||
| if ($allowRefreshDeletedObject) { | ||
| return $object; | ||
| } | ||
|
|
||
| throw RefreshObjectFailed::objectNoLongExists(); | ||
| } | ||
| return $object; | ||
| } | ||
|
|
||
| try { | ||
| $om->refresh($object); | ||
| } catch (\LogicException|\Error) { | ||
| // prevent entities/documents with readonly properties to create an error | ||
| // LogicException is for ORM / Error is for ODM | ||
| // @see https://github.com/doctrine/orm/issues/9505 | ||
| $id = $strategy->getIdentifierValues($object); | ||
|
|
||
| if (!$id || !($objectFromDB = $om->find($object::class, $id))) { | ||
| throw new ObjectNoLongerExist($object); | ||
| } | ||
|
|
||
| $object = $objectFromDB; | ||
|
|
||
| return $object; | ||
| } | ||
|
|
||
|
|
@@ -223,17 +256,19 @@ public function isPersisted(object $object): bool | |
| $object = $reflector->initializeLazyObject($object); | ||
| } | ||
|
|
||
| $persistenceStrategy = $this->strategyFor($object::class); | ||
|
|
||
| // prevents doctrine to use its cache and think the object is persisted | ||
| if ($this->strategyFor($object::class)->isScheduledForInsert($object)) { | ||
| if ($persistenceStrategy->isScheduledForInsert($object)) { | ||
| return false; | ||
| } | ||
|
|
||
| if ($object instanceof Proxy) { | ||
| $object = ProxyGenerator::unwrap($object); | ||
| } | ||
|
|
||
| $om = $this->strategyFor($object::class)->objectManagerFor($object::class); | ||
| $id = $om->getClassMetadata($object::class)->getIdentifierValues($object); | ||
| $om = $persistenceStrategy->objectManagerFor($object::class); | ||
| $id = $persistenceStrategy->getIdentifierValues($object); | ||
|
|
||
| return $id && null !== $om->find($object::class, $id); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hope this code won't break too much things...
actually if it breaks, it means that the entity/object has a design problem 😅