Skip to content

Commit ffccbc3

Browse files
committed
[JsonEncoder] Add native lazyghost support
1 parent 4ababf2 commit ffccbc3

18 files changed

+202
-221
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2032,6 +2032,10 @@ private function registerJsonEncoderConfiguration(array $config, ContainerBuilde
20322032
foreach ($config['paths'] as $namespace => $path) {
20332033
$loader->registerClasses($encodableDefinition, $namespace, $path);
20342034
}
2035+
2036+
if (\PHP_VERSION_ID >= 80400) {
2037+
$container->removeDefinition('.json_encoder.cache_warmer.lazy_ghost');
2038+
}
20352039
}
20362040

20372041
private function registerPropertyInfoConfiguration(ContainerBuilder $container, PhpFileLoader $loader): void

src/Symfony/Component/JsonEncoder/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ CHANGELOG
55
---
66

77
* Introduce the component as experimental
8+
* Add native PHP lazy ghost support

src/Symfony/Component/JsonEncoder/Decode/LazyInstantiator.php

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,23 @@
1212
namespace Symfony\Component\JsonEncoder\Decode;
1313

1414
use Symfony\Component\Filesystem\Filesystem;
15+
use Symfony\Component\JsonEncoder\Exception\InvalidArgumentException;
1516
use Symfony\Component\JsonEncoder\Exception\RuntimeException;
1617
use Symfony\Component\VarExporter\ProxyHelper;
1718

1819
/**
1920
* Instantiates a new $className lazy ghost {@see \Symfony\Component\VarExporter\LazyGhostTrait}.
2021
*
21-
* The $className class must not be final.
22-
*
23-
* A property must be a callable that returns the actual value when being called.
22+
* Prior to PHP 8.4, the "$className" argument class must not be final.
23+
* The $initializer must be a callable that sets the actual object values when being called.
2424
*
2525
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
2626
*
2727
* @internal
2828
*/
2929
final class LazyInstantiator
3030
{
31-
private Filesystem $fs;
31+
private ?Filesystem $fs = null;
3232

3333
/**
3434
* @var array{reflection: array<class-string, \ReflectionClass<object>>, lazy_class_name: array<class-string, class-string>}
@@ -44,34 +44,37 @@ final class LazyInstantiator
4444
private static array $lazyClassesLoaded = [];
4545

4646
public function __construct(
47-
private string $lazyGhostsDir,
47+
private ?string $lazyGhostsDir = null,
4848
) {
49-
$this->fs = new Filesystem();
49+
if (null === $this->lazyGhostsDir && \PHP_VERSION_ID < 80400) {
50+
throw new InvalidArgumentException('The "$lazyGhostsDir" argument cannot be null when using PHP < 8.4.');
51+
}
5052
}
5153

5254
/**
5355
* @template T of object
5456
*
55-
* @param class-string<T> $className
56-
* @param array<string, callable(): mixed> $propertiesCallables
57+
* @param class-string<T> $className
58+
* @param callable(T): void $initializer
5759
*
5860
* @return T
5961
*/
60-
public function instantiate(string $className, array $propertiesCallables): object
62+
public function instantiate(string $className, callable $initializer): object
6163
{
6264
try {
6365
$classReflection = self::$cache['reflection'][$className] ??= new \ReflectionClass($className);
6466
} catch (\ReflectionException $e) {
6567
throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
6668
}
6769

68-
$lazyClassName = self::$cache['lazy_class_name'][$className] ??= \sprintf('%sGhost', preg_replace('/\\\\/', '', $className));
70+
// use native lazy ghosts if available
71+
if (\PHP_VERSION_ID >= 80400) {
72+
return $classReflection->newLazyGhost($initializer);
73+
}
6974

70-
$initializer = function (object $object) use ($propertiesCallables) {
71-
foreach ($propertiesCallables as $name => $propertyCallable) {
72-
$object->{$name} = $propertyCallable();
73-
}
74-
};
75+
$this->fs ??= new Filesystem();
76+
77+
$lazyClassName = self::$cache['lazy_class_name'][$className] ??= \sprintf('%sGhost', preg_replace('/\\\\/', '', $className));
7578

7679
if (isset(self::$lazyClassesLoaded[$className]) && class_exists($lazyClassName)) {
7780
return $lazyClassName::createLazyGhost($initializer);

src/Symfony/Component/JsonEncoder/Decode/PhpAstBuilder.php

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@
5353
use Symfony\Component\TypeInfo\Type\BackedEnumType;
5454
use Symfony\Component\TypeInfo\Type\CollectionType;
5555
use Symfony\Component\TypeInfo\Type\ObjectType;
56-
use Symfony\Component\TypeInfo\TypeIdentifier;
5756
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
57+
use Symfony\Component\TypeInfo\TypeIdentifier;
5858

5959
/**
6060
* Builds a PHP syntax tree that decodes JSON.
@@ -445,21 +445,8 @@ private function buildObjectNodeStatements(ObjectNode $node, bool $decodeFromStr
445445
);
446446

447447
$streamPropertiesValuesStmts[] = new MatchArm([$this->builder->val($encodedName)], new Assign(
448-
new ArrayDimFetch($this->builder->var('properties'), $this->builder->val($property['name'])),
449-
new Closure([
450-
'static' => true,
451-
'uses' => [
452-
new ClosureUse($this->builder->var('stream')),
453-
new ClosureUse($this->builder->var('v')),
454-
new ClosureUse($this->builder->var('options')),
455-
new ClosureUse($this->builder->var('denormalizers')),
456-
new ClosureUse($this->builder->var('instantiator')),
457-
new ClosureUse($this->builder->var('providers'), byRef: true),
458-
],
459-
'stmts' => [
460-
new Return_($property['accessor'](new PhpExprDataAccessor($propertyValueStmt))->toPhpExpr()),
461-
],
462-
]),
448+
$this->builder->propertyFetch($this->builder->var('object'), $property['name']),
449+
$property['accessor'](new PhpExprDataAccessor($propertyValueStmt))->toPhpExpr(),
463450
));
464451
} else {
465452
$propertyValueStmt = $this->nodeOnlyNeedsDecode($property['value'], $decodeFromStream)
@@ -494,17 +481,29 @@ private function buildObjectNodeStatements(ObjectNode $node, bool $decodeFromStr
494481

495482
if ($decodeFromStream) {
496483
$instantiateStmts = [
497-
new Expression(new Assign($this->builder->var('properties'), new Array_([], ['kind' => Array_::KIND_SHORT]))),
498-
new Foreach_($this->builder->var('data'), $this->builder->var('v'), [
499-
'keyVar' => $this->builder->var('k'),
500-
'stmts' => [new Expression(new Match_(
501-
$this->builder->var('k'),
502-
[...$streamPropertiesValuesStmts, new MatchArm(null, $this->builder->val(null))],
503-
))],
504-
]),
505484
new Return_($this->builder->methodCall($this->builder->var('instantiator'), 'instantiate', [
506485
new ClassConstFetch(new FullyQualified($node->getType()->getClassName()), 'class'),
507-
$this->builder->var('properties'),
486+
new Closure([
487+
'static' => true,
488+
'params' => [new Param($this->builder->var('object'))],
489+
'uses' => [
490+
new ClosureUse($this->builder->var('stream')),
491+
new ClosureUse($this->builder->var('data')),
492+
new ClosureUse($this->builder->var('options')),
493+
new ClosureUse($this->builder->var('denormalizers')),
494+
new ClosureUse($this->builder->var('instantiator')),
495+
new ClosureUse($this->builder->var('providers'), byRef: true),
496+
],
497+
'stmts' => [
498+
new Foreach_($this->builder->var('data'), $this->builder->var('v'), [
499+
'keyVar' => $this->builder->var('k'),
500+
'stmts' => [new Expression(new Match_(
501+
$this->builder->var('k'),
502+
[...$streamPropertiesValuesStmts, new MatchArm(null, $this->builder->val(null))],
503+
))],
504+
]),
505+
],
506+
]),
508507
])),
509508
];
510509
} else {

src/Symfony/Component/JsonEncoder/Encode/PhpAstBuilder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@
4545
use Symfony\Component\JsonEncoder\Exception\RuntimeException;
4646
use Symfony\Component\JsonEncoder\Exception\UnexpectedValueException;
4747
use Symfony\Component\TypeInfo\Type\ObjectType;
48-
use Symfony\Component\TypeInfo\TypeIdentifier;
4948
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
49+
use Symfony\Component\TypeInfo\TypeIdentifier;
5050

5151
/**
5252
* Builds a PHP syntax tree that encodes data to JSON.

src/Symfony/Component/JsonEncoder/Mapping/GenericTypePropertyMetadataLoader.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
use Symfony\Component\TypeInfo\Type\IntersectionType;
1919
use Symfony\Component\TypeInfo\Type\ObjectType;
2020
use Symfony\Component\TypeInfo\Type\UnionType;
21-
use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
2221
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
22+
use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
2323

2424
/**
2525
* Enhances properties encoding/decoding metadata based on properties' generic type.

src/Symfony/Component/JsonEncoder/Tests/Decode/LazyInstantiatorTest.php

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\JsonEncoder\Decode\LazyInstantiator;
16+
use Symfony\Component\JsonEncoder\Exception\InvalidArgumentException;
1617
use Symfony\Component\JsonEncoder\Tests\Fixtures\Model\ClassicDummy;
1718
use Symfony\Component\JsonEncoder\Tests\Fixtures\Model\DummyWithNormalizerAttributes;
1819

@@ -32,17 +33,46 @@ protected function setUp(): void
3233
}
3334
}
3435

35-
public function testCreateLazyGhost()
36+
/**
37+
* @requires PHP < 8.4
38+
*/
39+
public function testCreateLazyGhostUsingVarExporter()
3640
{
37-
$ghost = (new LazyInstantiator($this->lazyGhostsDir))->instantiate(ClassicDummy::class, []);
41+
$ghost = (new LazyInstantiator($this->lazyGhostsDir))->instantiate(ClassicDummy::class, function (ClassicDummy $object): void {
42+
$object->id = 123;
43+
});
3844

39-
$this->assertArrayHasKey(\sprintf("\0%sGhost\0lazyObjectState", preg_replace('/\\\\/', '', ClassicDummy::class)), (array) $ghost);
45+
$this->assertSame(123, $ghost->id);
4046
}
4147

48+
/**
49+
* @requires PHP < 8.4
50+
*/
4251
public function testCreateCacheFile()
4352
{
44-
(new LazyInstantiator($this->lazyGhostsDir))->instantiate(DummyWithNormalizerAttributes::class, []);
53+
(new LazyInstantiator($this->lazyGhostsDir))->instantiate(DummyWithNormalizerAttributes::class, function (ClassicDummy $object): void {});
4554

4655
$this->assertCount(1, glob($this->lazyGhostsDir.'/*'));
4756
}
57+
58+
/**
59+
* @requires PHP < 8.4
60+
*/
61+
public function testThrowIfLazyGhostDirNotDefined()
62+
{
63+
$this->expectException(InvalidArgumentException::class);
64+
new LazyInstantiator();
65+
}
66+
67+
/**
68+
* @requires PHP 8.4
69+
*/
70+
public function testCreateLazyGhostUsingPhp()
71+
{
72+
$ghost = (new LazyInstantiator())->instantiate(ClassicDummy::class, function (ClassicDummy $object): void {
73+
$object->id = 123;
74+
});
75+
76+
$this->assertSame(123, $ghost->id);
77+
}
4878
}

src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/nullable_object.stream.php

Lines changed: 9 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/nullable_object_dict.stream.php

Lines changed: 9 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/nullable_object_list.stream.php

Lines changed: 9 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)