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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy