From b55647a4098265b3611e2c643afdfc860f025ef0 Mon Sep 17 00:00:00 2001 From: DPvic Date: Wed, 25 Feb 2026 21:02:11 +0100 Subject: [PATCH 1/3] Add support for native lazy objects on PHP 8.4+, refactor proxy handling, and improve compatibility by conditionally implementing `LazyObjectInterface`. --- composer.json | 2 +- src/Manager/SettingsCloner.php | 22 +++++++++-- src/Manager/SettingsManager.php | 36 +++++++++++++----- src/Proxy/ProxyFactory.php | 9 ++++- src/Proxy/SettingsProxyInterface.php | 26 ++++++++----- tests/Manager/SettingsManagerTest.php | 5 +-- .../EnvVarToSettingsMigratorTest.php | 1 - tests/Proxy/LazyObjectTestHelper.php | 20 +++++++--- tests/Proxy/ProxyFactoryTest.php | 3 +- .../config/packages/doctrine.php | 38 +++++++++++-------- 10 files changed, 109 insertions(+), 53 deletions(-) diff --git a/composer.json b/composer.json index 802bb2f..34973ab 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "symfony/translation-contracts": "^2.5|^3.0", "symfony/validator": "^6.4|^7.0|^8.0", "symfony/form": "^6.4|^7.0|^8.0", - "symfony/var-exporter": "^6.4|^7.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", "ergebnis/classy": "^1.6", "symfony/translation": "^7.0|^6.4|^8.0", "symfony/deprecation-contracts": "^3.4" diff --git a/src/Manager/SettingsCloner.php b/src/Manager/SettingsCloner.php index 2367415..ecd511f 100644 --- a/src/Manager/SettingsCloner.php +++ b/src/Manager/SettingsCloner.php @@ -37,7 +37,6 @@ use Jbtronics\SettingsBundle\Settings\CloneAndMergeAwareSettingsInterface; use Jbtronics\SettingsBundle\Settings\ResettableSettingsInterface; use PhpParser\Node\Param; -use Symfony\Component\VarExporter\LazyObjectInterface; /** * @internal @@ -138,7 +137,7 @@ public function mergeCopyInternal(object $copy, object $into, bool $recursive, a continue; } - if ($copyEmbedded instanceof SettingsProxyInterface && $copyEmbedded instanceof LazyObjectInterface && !$copyEmbedded->isLazyObjectInitialized()) { //Fallback for older PHP versions + if ($copyEmbedded instanceof SettingsProxyInterface && $this->isLegacyProxyUninitialized($copyEmbedded)) { //Fallback for older PHP versions continue; } @@ -198,6 +197,23 @@ private function shouldBeCloned(mixed $value, ParameterMetadata $parameterMetada return $parameterMetadata->isCloneable(); } + /** + * Checks if a legacy (pre-PHP 8.4) proxy is still uninitialized. + */ + private function isLegacyProxyUninitialized(object $instance): bool + { + if (!method_exists($instance, 'isLazyObjectInitialized')) { + return false; + } + + $method = new \ReflectionMethod($instance, 'isLazyObjectInitialized'); + if ($method->getNumberOfParameters() >= 1) { + return !$instance->isLazyObjectInitialized(false); + } + + return !$instance->isLazyObjectInitialized(); + } + /** * Clones the given data if needed and returns the cloned data * @param mixed $data @@ -218,4 +234,4 @@ private function cloneDataIfNeeded(mixed $data, ParameterMetadata $parameter): m return $data; } -} \ No newline at end of file +} diff --git a/src/Manager/SettingsManager.php b/src/Manager/SettingsManager.php index a22aca0..ca51b71 100644 --- a/src/Manager/SettingsManager.php +++ b/src/Manager/SettingsManager.php @@ -33,7 +33,6 @@ use Jbtronics\SettingsBundle\Metadata\ParameterMetadata; use Jbtronics\SettingsBundle\Proxy\ProxyFactoryInterface; use Jbtronics\SettingsBundle\Proxy\SettingsProxyInterface; -use Symfony\Component\VarExporter\LazyObjectInterface; use Symfony\Contracts\Service\ResetInterface; /** @@ -210,9 +209,9 @@ public function save(string|object|array|null $settings = null, bool $cascade = continue; } - if ($instance instanceof SettingsProxyInterface && $instance instanceof LazyObjectInterface && !$instance->isLazyObjectInitialized()) { //Fallback for older PHP versions - continue; - } + if ($instance instanceof SettingsProxyInterface && $this->isLegacyProxyUninitialized($instance)) { //Fallback for older PHP versions + continue; + } $this->settingsHydrator->persist($instance, $this->metadataManager->getSettingsMetadata($class)); } @@ -228,11 +227,28 @@ public function resetToDefaultValues(object|string $settings): void $this->settingsResetter->resetSettings($settings, $this->metadataManager->getSettingsMetadata($settings)); } - public function reset(): void - { - //Reset all cached settings classes, to trigger a reload on new requests - $this->settings_by_class = []; - } + public function reset(): void + { + //Reset all cached settings classes, to trigger a reload on new requests + $this->settings_by_class = []; + } + + /** + * Checks if a legacy (pre-PHP 8.4) proxy is still uninitialized. + */ + private function isLegacyProxyUninitialized(object $instance): bool + { + if (!method_exists($instance, 'isLazyObjectInitialized')) { + return false; + } + + $method = new \ReflectionMethod($instance, 'isLazyObjectInitialized'); + if ($method->getNumberOfParameters() >= 1) { + return !$instance->isLazyObjectInitialized(false); + } + + return !$instance->isLazyObjectInitialized(); + } public function isEnvVarOverwritten( object|string $settings, @@ -311,4 +327,4 @@ public function mergeTemporaryCopy(object|string $copy, bool $cascade = true): v //Use the cloner service to merge the temporary copy back to the original instance $this->settingsCloner->mergeCopy($copy, $original, $cascade); } -} \ No newline at end of file +} diff --git a/src/Proxy/ProxyFactory.php b/src/Proxy/ProxyFactory.php index 5cebd58..23bac6d 100644 --- a/src/Proxy/ProxyFactory.php +++ b/src/Proxy/ProxyFactory.php @@ -187,7 +187,12 @@ private function generateProxyClassFile(string $className): void */ protected function generateUseLazyGhostTrait(\ReflectionClass $reflClass): string { - $code = ProxyHelper::generateLazyGhost($reflClass); + $proxyHelper = new \ReflectionClass(ProxyHelper::class); + if (!$proxyHelper->hasMethod('generateLazyGhost')) { + throw new \RuntimeException('Lazy ghost proxy generation requires symfony/var-exporter < 8 or PHP 8.4+ native lazy objects.'); + } + + $code = (string) $proxyHelper->getMethod('generateLazyGhost')->invoke(null, $reflClass); $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 { @@ -221,4 +226,4 @@ public function getProxyClassName(string $class): string { return rtrim($this->proxyNamespace, '\\') . '\\' . SettingsProxyInterface::MARKER . '\\' . ltrim($class, '\\'); } -} \ No newline at end of file +} diff --git a/src/Proxy/SettingsProxyInterface.php b/src/Proxy/SettingsProxyInterface.php index 1fc02d8..1d9f7fe 100644 --- a/src/Proxy/SettingsProxyInterface.php +++ b/src/Proxy/SettingsProxyInterface.php @@ -28,16 +28,24 @@ namespace Jbtronics\SettingsBundle\Proxy; -use \Symfony\Component\VarExporter\LazyObjectInterface; - /** * This interface is implemented by proxies that lazy load settings. * @internal */ -interface SettingsProxyInterface extends LazyObjectInterface -{ - /** - * Marker for Proxy class names. - */ - public const MARKER = '__JB__'; -} \ No newline at end of file +if (interface_exists(\Symfony\Component\VarExporter\LazyObjectInterface::class)) { + interface SettingsProxyInterface extends \Symfony\Component\VarExporter\LazyObjectInterface + { + /** + * Marker for Proxy class names. + */ + public const MARKER = '__JB__'; + } +} else { + interface SettingsProxyInterface + { + /** + * Marker for Proxy class names. + */ + public const MARKER = '__JB__'; + } +} diff --git a/tests/Manager/SettingsManagerTest.php b/tests/Manager/SettingsManagerTest.php index 4ef00f5..ba762e6 100644 --- a/tests/Manager/SettingsManagerTest.php +++ b/tests/Manager/SettingsManagerTest.php @@ -37,7 +37,6 @@ use Jbtronics\SettingsBundle\Tests\TestApplication\Settings\ValidatableSettings; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Validator\DataCollector\ValidatorDataCollector; -use Symfony\Component\VarExporter\LazyObjectInterface; /** * The functional/integration test for the SettingsManager @@ -160,9 +159,7 @@ public function testGetEmbedded(): void $this->assertEquals('default', $settings->simpleSettings->getValue1()); - if ($settings->simpleSettings instanceof LazyObjectInterface) { - $this->assertTrue($settings->simpleSettings->isLazyObjectInitialized()); - } + $this->assertTrue(LazyObjectTestHelper::isLazyObjectInitialized($settings->simpleSettings)); } public function testGetEmbeddedCircular(): void diff --git a/tests/Migrations/EnvVarToSettingsMigratorTest.php b/tests/Migrations/EnvVarToSettingsMigratorTest.php index 3ab9519..6376515 100644 --- a/tests/Migrations/EnvVarToSettingsMigratorTest.php +++ b/tests/Migrations/EnvVarToSettingsMigratorTest.php @@ -44,7 +44,6 @@ use Jbtronics\SettingsBundle\Tests\TestApplication\Settings\ValidatableSettings; use Jbtronics\SettingsBundle\Tests\TestApplication\Settings\VersionedSettings; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Symfony\Component\VarExporter\LazyObjectInterface; class EnvVarToSettingsMigratorTest extends KernelTestCase { diff --git a/tests/Proxy/LazyObjectTestHelper.php b/tests/Proxy/LazyObjectTestHelper.php index 2298d3d..26d3804 100644 --- a/tests/Proxy/LazyObjectTestHelper.php +++ b/tests/Proxy/LazyObjectTestHelper.php @@ -5,7 +5,6 @@ namespace Jbtronics\SettingsBundle\Tests\Proxy; use ReflectionClass; -use Symfony\Component\VarExporter\LazyObjectInterface; final class LazyObjectTestHelper { @@ -16,7 +15,13 @@ final class LazyObjectTestHelper */ public static function isLazyObject(object $obj): bool { - if ($obj instanceof LazyObjectInterface){ + if (interface_exists(\Symfony\Component\VarExporter\LazyObjectInterface::class) + && $obj instanceof \Symfony\Component\VarExporter\LazyObjectInterface + ) { + return true; + } + + if (method_exists($obj, 'isLazyObjectInitialized')) { return true; } @@ -36,8 +41,13 @@ public static function isLazyObject(object $obj): bool */ public static function isLazyObjectInitialized(object $obj, bool $partial = false): bool { - if ($obj instanceof LazyObjectInterface){ - return $obj->isLazyObjectInitialized($partial); + if (method_exists($obj, 'isLazyObjectInitialized')) { + $method = new \ReflectionMethod($obj, 'isLazyObjectInitialized'); + if ($method->getNumberOfParameters() >= 1) { + return $obj->isLazyObjectInitialized($partial); + } + + return $obj->isLazyObjectInitialized(); } if (PHP_VERSION_ID >= 80400) { @@ -47,4 +57,4 @@ public static function isLazyObjectInitialized(object $obj, bool $partial = fals //If we reach here, the object is not a lazy object, so it is considered initialized return true; } -} \ No newline at end of file +} diff --git a/tests/Proxy/ProxyFactoryTest.php b/tests/Proxy/ProxyFactoryTest.php index 655e8bb..31c6fd9 100644 --- a/tests/Proxy/ProxyFactoryTest.php +++ b/tests/Proxy/ProxyFactoryTest.php @@ -30,7 +30,6 @@ use Jbtronics\SettingsBundle\Proxy\SettingsProxyInterface; use Jbtronics\SettingsBundle\Tests\TestApplication\Settings\SimpleSettings; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Symfony\Component\VarExporter\LazyObjectInterface; class ProxyFactoryTest extends KernelTestCase { @@ -64,7 +63,7 @@ public function testCreateProxy(): void $instance->setValue1('Initialized'); }; - /** @var LazyObjectInterface&SimpleSettings&SettingsProxyInterface $proxy */ + /** @var SimpleSettings&SettingsProxyInterface $proxy */ $proxy = $this->proxyFactory->createProxy(SimpleSettings::class, $initializer); $this->assertInstanceOf(SimpleSettings::class, $proxy); diff --git a/tests/TestApplication/config/packages/doctrine.php b/tests/TestApplication/config/packages/doctrine.php index a2e6866..e4800fd 100644 --- a/tests/TestApplication/config/packages/doctrine.php +++ b/tests/TestApplication/config/packages/doctrine.php @@ -26,24 +26,30 @@ declare(strict_types=1); +$ormConfig = [ + 'auto_generate_proxy_classes' => true, + 'naming_strategy' => 'doctrine.orm.naming_strategy.underscore_number_aware', + 'auto_mapping' => true, + 'mappings' => [ + 'TestEntities' => [ + 'is_bundle' => false, + 'type' => 'attribute', + 'dir' => '%kernel.project_dir%/src/Entity', + 'prefix' => 'Jbtronics\SettingsBundle\Tests\TestApplication\Entity', + 'alias' => 'app', + ], + ], +]; + +// Doctrine ORM supports native lazy objects on PHP 8.4+ via config. +if (PHP_VERSION_ID >= 80400) { + $ormConfig['enable_native_lazy_objects'] = true; +} + $container->loadFromExtension('doctrine', [ 'dbal' => [ 'driver' => 'pdo_sqlite', 'path' => '%kernel.cache_dir%/test_database.sqlite', ], - - 'orm' => [ - 'auto_generate_proxy_classes' => true, - 'naming_strategy' => 'doctrine.orm.naming_strategy.underscore_number_aware', - 'auto_mapping' => true, - 'mappings' => [ - 'TestEntities' => [ - 'is_bundle' => false, - 'type' => 'attribute', - 'dir' => '%kernel.project_dir%/src/Entity', - 'prefix' => 'Jbtronics\SettingsBundle\Tests\TestApplication\Entity', - 'alias' => 'app', - ], - ], - ], -]); \ No newline at end of file + 'orm' => $ormConfig, +]); From 18df85f7c7cf80b3a16f9c34972c6443d8e4b44e Mon Sep 17 00:00:00 2001 From: DPvic Date: Wed, 25 Feb 2026 21:06:05 +0100 Subject: [PATCH 2/3] Trigger workflow --- src/Proxy/ProxyFactory.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Proxy/ProxyFactory.php b/src/Proxy/ProxyFactory.php index 23bac6d..f4a1883 100644 --- a/src/Proxy/ProxyFactory.php +++ b/src/Proxy/ProxyFactory.php @@ -66,7 +66,6 @@ class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); */ private readonly bool $useNativeGhostObject; - public function __construct( private readonly string $proxyDir, private readonly string $proxyNamespace, From 550c7bc0e136a888f7712904e85973081e827095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 28 Feb 2026 17:17:35 +0100 Subject: [PATCH 3/3] Centralized isLegacyProxyUnitialized logic in a new class --- src/Manager/SettingsCloner.php | 20 ++---------------- src/Manager/SettingsManager.php | 37 ++++++++++----------------------- src/Proxy/LegacyProxyHelper.php | 32 ++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 44 deletions(-) create mode 100644 src/Proxy/LegacyProxyHelper.php diff --git a/src/Manager/SettingsCloner.php b/src/Manager/SettingsCloner.php index ecd511f..51f672d 100644 --- a/src/Manager/SettingsCloner.php +++ b/src/Manager/SettingsCloner.php @@ -32,6 +32,7 @@ use Jbtronics\SettingsBundle\Helper\PropertyAccessHelper; use Jbtronics\SettingsBundle\Metadata\MetadataManager; use Jbtronics\SettingsBundle\Metadata\ParameterMetadata; +use Jbtronics\SettingsBundle\Proxy\LegacyProxyHelper; use Jbtronics\SettingsBundle\Proxy\ProxyFactoryInterface; use Jbtronics\SettingsBundle\Proxy\SettingsProxyInterface; use Jbtronics\SettingsBundle\Settings\CloneAndMergeAwareSettingsInterface; @@ -137,7 +138,7 @@ public function mergeCopyInternal(object $copy, object $into, bool $recursive, a continue; } - if ($copyEmbedded instanceof SettingsProxyInterface && $this->isLegacyProxyUninitialized($copyEmbedded)) { //Fallback for older PHP versions + if ($copyEmbedded instanceof SettingsProxyInterface && LegacyProxyHelper::isLegacyProxyUninitialized($copyEmbedded)) { //Fallback for older PHP versions continue; } @@ -197,23 +198,6 @@ private function shouldBeCloned(mixed $value, ParameterMetadata $parameterMetada return $parameterMetadata->isCloneable(); } - /** - * Checks if a legacy (pre-PHP 8.4) proxy is still uninitialized. - */ - private function isLegacyProxyUninitialized(object $instance): bool - { - if (!method_exists($instance, 'isLazyObjectInitialized')) { - return false; - } - - $method = new \ReflectionMethod($instance, 'isLazyObjectInitialized'); - if ($method->getNumberOfParameters() >= 1) { - return !$instance->isLazyObjectInitialized(false); - } - - return !$instance->isLazyObjectInitialized(); - } - /** * Clones the given data if needed and returns the cloned data * @param mixed $data diff --git a/src/Manager/SettingsManager.php b/src/Manager/SettingsManager.php index ca51b71..7ee0fa9 100644 --- a/src/Manager/SettingsManager.php +++ b/src/Manager/SettingsManager.php @@ -31,6 +31,7 @@ use Jbtronics\SettingsBundle\Metadata\EnvVarMode; use Jbtronics\SettingsBundle\Metadata\MetadataManagerInterface; use Jbtronics\SettingsBundle\Metadata\ParameterMetadata; +use Jbtronics\SettingsBundle\Proxy\LegacyProxyHelper; use Jbtronics\SettingsBundle\Proxy\ProxyFactoryInterface; use Jbtronics\SettingsBundle\Proxy\SettingsProxyInterface; use Symfony\Contracts\Service\ResetInterface; @@ -209,9 +210,9 @@ public function save(string|object|array|null $settings = null, bool $cascade = continue; } - if ($instance instanceof SettingsProxyInterface && $this->isLegacyProxyUninitialized($instance)) { //Fallback for older PHP versions - continue; - } + if ($instance instanceof SettingsProxyInterface && LegacyProxyHelper::isLegacyProxyUninitialized($instance)) { //Fallback for older PHP versions + continue; + } $this->settingsHydrator->persist($instance, $this->metadataManager->getSettingsMetadata($class)); } @@ -227,28 +228,12 @@ public function resetToDefaultValues(object|string $settings): void $this->settingsResetter->resetSettings($settings, $this->metadataManager->getSettingsMetadata($settings)); } - public function reset(): void - { - //Reset all cached settings classes, to trigger a reload on new requests - $this->settings_by_class = []; - } - - /** - * Checks if a legacy (pre-PHP 8.4) proxy is still uninitialized. - */ - private function isLegacyProxyUninitialized(object $instance): bool - { - if (!method_exists($instance, 'isLazyObjectInitialized')) { - return false; - } - - $method = new \ReflectionMethod($instance, 'isLazyObjectInitialized'); - if ($method->getNumberOfParameters() >= 1) { - return !$instance->isLazyObjectInitialized(false); - } - - return !$instance->isLazyObjectInitialized(); - } + public function reset(): void + { + //Reset all cached settings classes, to trigger a reload on new requests + $this->settings_by_class = []; + } + public function isEnvVarOverwritten( object|string $settings, @@ -327,4 +312,4 @@ public function mergeTemporaryCopy(object|string $copy, bool $cascade = true): v //Use the cloner service to merge the temporary copy back to the original instance $this->settingsCloner->mergeCopy($copy, $original, $cascade); } -} +} diff --git a/src/Proxy/LegacyProxyHelper.php b/src/Proxy/LegacyProxyHelper.php new file mode 100644 index 0000000..b8a90aa --- /dev/null +++ b/src/Proxy/LegacyProxyHelper.php @@ -0,0 +1,32 @@ +getNumberOfParameters() >= 1) { + return !$instance->isLazyObjectInitialized(false); + } + + return !$instance->isLazyObjectInitialized(); + } +} \ No newline at end of file