Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:

jobs:
tests:
name: P:${{ matrix.php }}, S:${{ matrix.symfony }}, D:${{ matrix.database }}, PU:${{ matrix.phpunit }}${{ matrix.deps == 'lowest' && ' (lowest)' || '' }}${{ matrix.use-phpunit-extension == 1 && ' (phpunit extension)' || '' }}
name: P:${{ matrix.php }}, S:${{ matrix.symfony }}, D:${{ matrix.database }}, PU:${{ matrix.phpunit }}${{ matrix.deps == 'lowest' && ' (lowest)' || '' }}${{ matrix.use-phpunit-extension == 1 && ' (phpunit extension)' || '' }}${{ (matrix.use-php-84-lazy-objects != 1 || matrix.php != '8.4') && ' (legacy proxy)' || '' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand Down
3 changes: 0 additions & 3 deletions UPGRADE-2.7.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,6 @@ You'll also need to update the PHPDoc `@extends` annotation to use the new class
- `_assertPersisted()` => `\Zenstruck\Foundry\Persistence\assert_persisted()`
- `_assertNotPersisted()` => `\Zenstruck\Foundry\Persistence\assert_not_persisted()`

> [!NOTE]
> Calls to `Proxy::refresh()` also need to be replaced, but they are not covered by a Rector rule.

- Remove `Proxy` type for parameters in the prototype in methods and functions (covered by `ChangeProxyParamTypesRector`).
- Remove `Proxy` return type in methods and functions (covered by `ChangeProxyReturnTypesRector`).
- Remove all `Proxy` type hints in PHPDoc (covered by `RemovePhpDocProxyTypeHintRector`).
2 changes: 1 addition & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ parameters:
# we're currently running static analysis with PHP 8.3
- message: '#Call to an undefined method ReflectionClass\<(.*)\>::isUninitializedLazyObject\(\).#'
- message: '#Call to an undefined method ReflectionClass\<(.*)\>::initializeLazyObject\(\).#'
- message: '#Call to an undefined method ReflectionClass\<(.*)\>::resetAsLazyProxy\(\).#'
- message: '#Call to an undefined method ReflectionClass\<(.*)\>::resetAsLazyGhost\(\).#'

excludePaths:
- tests/Fixture/Maker/expected/can_create_factory_with_auto_activated_not_persisted_option.php
Expand Down
2 changes: 2 additions & 0 deletions phpunit-deprecation-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<line number="25" hash="c6af5d66288d0667e424978000f29571e4954b81">
<issue><![CDATA[Since symfony/framework-bundle 6.4: Not setting the "framework.php_errors.log" config option is deprecated. It will default to "true" in 7.0.]]></issue>
<issue><![CDATA[Since symfony/var-exporter 7.3: Generating lazy proxy for class "Zenstruck\Foundry\Tests\Integration\ForceFactoriesTraitUsage\SomeObject" is deprecated; leverage native lazy objects instead.]]></issue>
<issue><![CDATA[Since symfony/var-exporter 7.3: Using ProxyHelper::generateLazyGhost() is deprecated, use native lazy objects instead.]]></issue>
<issue><![CDATA[Since symfony/var-exporter 7.3: The "Symfony\Component\VarExporter\LazyGhostTrait" trait is deprecated, use native lazy objects instead.]]></issue>
<issue><![CDATA[Since zenstruck/foundry 2.7: Proxy usage is deprecated in PHP 8.4. Use directly PersistentObjectFactory, Foundry now leverages the native PHP lazy system to auto-refresh objects.]]></issue>
<issue><![CDATA[Since zenstruck/foundry 2.7: Proxy usage is deprecated in PHP 8.4. You should extend directly PersistentObjectFactory in your factories.
Foundry now leverages the native PHP lazy system to auto-refresh objects (it can be enabled with "zenstruck_foundry.enable_auto_refresh_with_lazy_objects" configuration).
Expand Down
2 changes: 2 additions & 0 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ public static function isBooted(): bool
/** @param \Closure():self|self $configuration */
public static function boot(\Closure|self $configuration): void
{
PersistedObjectsTracker::reset();
self::$instance = $configuration;
}

Expand All @@ -142,6 +143,7 @@ public static function bootForDataProvider(\Closure|self $configuration): void

public static function shutdown(): void
{
PersistedObjectsTracker::reset();
StoryRegistry::reset();
self::$instance = null;
}
Expand Down
23 changes: 23 additions & 0 deletions src/Object/Hydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,29 @@ public static function get(object $object, string $property): mixed
return self::accessibleProperty($object, $property)->getValue($object);
}

/**
* @template T of object
*
* @param T $object
* @param T $other
*/
public static function hydrateFromOtherObject(object $object, object $other): void
{
$classes = [$object::class, ...\array_values(\class_parents($object))];

$properties = [];
foreach ($classes as $class) {
$reflector = new \ReflectionClass($class);
foreach ($reflector->getProperties() as $property) {
$properties[$property->getName()] = $property->getName();
}
}

foreach ($properties as $property) {
self::set($object, $property, self::get($other, $property), catchErrors: true);
}
}
Comment on lines +150 to +165
Copy link
Copy Markdown
Member Author

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 😅


private static function accessibleProperty(object $object, string $name): \ReflectionProperty
{
$class = new \ReflectionClass($object);
Expand Down
24 changes: 24 additions & 0 deletions src/Persistence/Exception/ObjectHasUnsavedChanges.php
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)."
);
}
}
22 changes: 22 additions & 0 deletions src/Persistence/Exception/ObjectNoLongerExist.php
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...');
}
}
26 changes: 1 addition & 25 deletions src/Persistence/Exception/RefreshObjectFailed.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,6 @@

namespace Zenstruck\Foundry\Persistence\Exception;

final class RefreshObjectFailed extends \RuntimeException
abstract class RefreshObjectFailed extends \RuntimeException
{
private function __construct(string $message, private bool $objectWasDeleted = false)
{
parent::__construct($message);
}

public static function objectNoLongExists(): static
{
return new self('object no longer exists...', objectWasDeleted: true);
}

/**
* @param class-string $objectClass
*/
public static function objectHasUnsavedChanges(string $objectClass): static
{
return new self(
"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)."
);
}

public function objectWasDeleted(): bool
{
return $this->objectWasDeleted;
}
}
7 changes: 2 additions & 5 deletions src/Persistence/IsProxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]>
Expand Down Expand Up @@ -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) {
}
}

Expand Down
97 changes: 66 additions & 31 deletions src/Persistence/PersistenceManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've actually reset the refresh() method to the same state than in 2.x

{
if (!$this->flush && !$force) {
return $object;
Expand All @@ -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;
}

Expand All @@ -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);
}
Expand Down
8 changes: 8 additions & 0 deletions src/Persistence/PersistenceStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ abstract public function contains(object $object): bool;

abstract public function truncate(string $class): void;

/**
* @return array<string, mixed>
*/
public function getIdentifierValues(object $object): array
{
return $this->classMetadata($object::class)->getIdentifierValues($object);
}

/**
* @return list<string>
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Persistence/PersistentObjectFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ protected function normalizeObject(string $field, object $object): object

try {
return $configuration->persistence()->refresh($object);
} catch (RefreshObjectFailed|VarExportLogicException) {
} catch (RefreshObjectFailed|VarExportLogicException) { // @phpstan-ignore catch.neverThrown (thrown by var exporter)
return $object;
}
}
Expand Down
8 changes: 3 additions & 5 deletions src/Persistence/Proxy/PersistedObjectsTracker.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public function add(object ...$objects): void
}
}

public function reset(): void
public static function reset(): void
{
self::$buffer = [];
}
Expand All @@ -63,10 +63,8 @@ static function(\WeakReference $weakRef) {
}

$clone = clone $object;
$reflector->resetAsLazyProxy($object, function() use ($clone, $reflector) {
Configuration::instance()->persistence()->refresh($clone, allowRefreshDeletedObject: true);

return $reflector->initializeLazyObject($clone);
$reflector->resetAsLazyGhost($object, function($object) use ($clone) {
Configuration::instance()->persistence()->autorefresh($object, $clone);
});

return \WeakReference::create($object);
Expand Down
Loading