From e5930b3a897e919fe8379a5323033e8981c3db9a Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Mon, 26 May 2025 08:30:12 +0200 Subject: [PATCH] [JsonStreamer] Remove "nikic/php-parser" dependency --- .../Component/JsonStreamer/CHANGELOG.md | 5 + .../DataModel/DataAccessorInterface.php | 29 - .../DataModel/FunctionDataAccessor.php | 57 -- .../DataModel/PhpExprDataAccessor.php | 34 - .../DataModel/PropertyDataAccessor.php | 46 -- .../DataModel/Read/ObjectNode.php | 5 +- .../DataModel/ScalarDataAccessor.php | 35 -- .../DataModel/VariableDataAccessor.php | 35 -- .../DataModel/Write/BackedEnumNode.php | 7 +- .../DataModel/Write/CollectionNode.php | 7 +- .../DataModel/Write/CompositeNode.php | 7 +- .../Write/DataModelNodeInterface.php | 5 +- .../DataModel/Write/ObjectNode.php | 19 +- .../DataModel/Write/ScalarNode.php | 7 +- .../JsonStreamer/Read/PhpAstBuilder.php | 590 ------------------ .../JsonStreamer/Read/PhpGenerator.php | 337 ++++++++++ .../Read/StreamReaderGenerator.php | 29 +- .../DataModel/Write/CompositeNodeTest.php | 31 +- .../Tests/DataModel/Write/ObjectNodeTest.php | 25 +- .../Write/MergingStringVisitor.php | 60 -- .../JsonStreamer/Write/PhpAstBuilder.php | 436 ------------- .../JsonStreamer/Write/PhpGenerator.php | 388 ++++++++++++ .../JsonStreamer/Write/PhpOptimizer.php | 43 -- .../Write/StreamWriterGenerator.php | 40 +- .../Component/JsonStreamer/composer.json | 1 - 25 files changed, 798 insertions(+), 1480 deletions(-) delete mode 100644 src/Symfony/Component/JsonStreamer/DataModel/DataAccessorInterface.php delete mode 100644 src/Symfony/Component/JsonStreamer/DataModel/FunctionDataAccessor.php delete mode 100644 src/Symfony/Component/JsonStreamer/DataModel/PhpExprDataAccessor.php delete mode 100644 src/Symfony/Component/JsonStreamer/DataModel/PropertyDataAccessor.php delete mode 100644 src/Symfony/Component/JsonStreamer/DataModel/ScalarDataAccessor.php delete mode 100644 src/Symfony/Component/JsonStreamer/DataModel/VariableDataAccessor.php delete mode 100644 src/Symfony/Component/JsonStreamer/Read/PhpAstBuilder.php create mode 100644 src/Symfony/Component/JsonStreamer/Read/PhpGenerator.php delete mode 100644 src/Symfony/Component/JsonStreamer/Write/MergingStringVisitor.php delete mode 100644 src/Symfony/Component/JsonStreamer/Write/PhpAstBuilder.php create mode 100644 src/Symfony/Component/JsonStreamer/Write/PhpGenerator.php delete mode 100644 src/Symfony/Component/JsonStreamer/Write/PhpOptimizer.php diff --git a/src/Symfony/Component/JsonStreamer/CHANGELOG.md b/src/Symfony/Component/JsonStreamer/CHANGELOG.md index 5294c5b5f3637..87f1e74c951da 100644 --- a/src/Symfony/Component/JsonStreamer/CHANGELOG.md +++ b/src/Symfony/Component/JsonStreamer/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.4 +--- + + * Remove `nikic/php-parser` dependency + 7.3 --- diff --git a/src/Symfony/Component/JsonStreamer/DataModel/DataAccessorInterface.php b/src/Symfony/Component/JsonStreamer/DataModel/DataAccessorInterface.php deleted file mode 100644 index 99f3dbfd0e9b8..0000000000000 --- a/src/Symfony/Component/JsonStreamer/DataModel/DataAccessorInterface.php +++ /dev/null @@ -1,29 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\JsonStreamer\DataModel; - -use PhpParser\Node\Expr; - -/** - * Represents a way to access data on PHP. - * - * @author Mathias Arlaud - * - * @internal - */ -interface DataAccessorInterface -{ - /** - * Converts to "nikic/php-parser" PHP expression. - */ - public function toPhpExpr(): Expr; -} diff --git a/src/Symfony/Component/JsonStreamer/DataModel/FunctionDataAccessor.php b/src/Symfony/Component/JsonStreamer/DataModel/FunctionDataAccessor.php deleted file mode 100644 index 8ad8960674d57..0000000000000 --- a/src/Symfony/Component/JsonStreamer/DataModel/FunctionDataAccessor.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\JsonStreamer\DataModel; - -use PhpParser\BuilderFactory; -use PhpParser\Node\Expr; - -/** - * Defines the way to access data using a function (or a method). - * - * @author Mathias Arlaud - * - * @internal - */ -final class FunctionDataAccessor implements DataAccessorInterface -{ - /** - * @param list $arguments - */ - public function __construct( - private string $functionName, - private array $arguments, - private ?DataAccessorInterface $objectAccessor = null, - ) { - } - - public function getObjectAccessor(): ?DataAccessorInterface - { - return $this->objectAccessor; - } - - public function withObjectAccessor(?DataAccessorInterface $accessor): self - { - return new self($this->functionName, $this->arguments, $accessor); - } - - public function toPhpExpr(): Expr - { - $builder = new BuilderFactory(); - $arguments = array_map(static fn (DataAccessorInterface $argument): Expr => $argument->toPhpExpr(), $this->arguments); - - if (null === $this->objectAccessor) { - return $builder->funcCall($this->functionName, $arguments); - } - - return $builder->methodCall($this->objectAccessor->toPhpExpr(), $this->functionName, $arguments); - } -} diff --git a/src/Symfony/Component/JsonStreamer/DataModel/PhpExprDataAccessor.php b/src/Symfony/Component/JsonStreamer/DataModel/PhpExprDataAccessor.php deleted file mode 100644 index 9806b94ed0a9f..0000000000000 --- a/src/Symfony/Component/JsonStreamer/DataModel/PhpExprDataAccessor.php +++ /dev/null @@ -1,34 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\JsonStreamer\DataModel; - -use PhpParser\Node\Expr; - -/** - * Defines the way to access data using PHP AST. - * - * @author Mathias Arlaud - * - * @internal - */ -final class PhpExprDataAccessor implements DataAccessorInterface -{ - public function __construct( - private Expr $php, - ) { - } - - public function toPhpExpr(): Expr - { - return $this->php; - } -} diff --git a/src/Symfony/Component/JsonStreamer/DataModel/PropertyDataAccessor.php b/src/Symfony/Component/JsonStreamer/DataModel/PropertyDataAccessor.php deleted file mode 100644 index f48c98064bb65..0000000000000 --- a/src/Symfony/Component/JsonStreamer/DataModel/PropertyDataAccessor.php +++ /dev/null @@ -1,46 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\JsonStreamer\DataModel; - -use PhpParser\BuilderFactory; -use PhpParser\Node\Expr; - -/** - * Defines the way to access data using an object property. - * - * @author Mathias Arlaud - * - * @internal - */ -final class PropertyDataAccessor implements DataAccessorInterface -{ - public function __construct( - private DataAccessorInterface $objectAccessor, - private string $propertyName, - ) { - } - - public function getObjectAccessor(): DataAccessorInterface - { - return $this->objectAccessor; - } - - public function withObjectAccessor(DataAccessorInterface $accessor): self - { - return new self($accessor, $this->propertyName); - } - - public function toPhpExpr(): Expr - { - return (new BuilderFactory())->propertyFetch($this->objectAccessor->toPhpExpr(), $this->propertyName); - } -} diff --git a/src/Symfony/Component/JsonStreamer/DataModel/Read/ObjectNode.php b/src/Symfony/Component/JsonStreamer/DataModel/Read/ObjectNode.php index 25d53c15fff60..e1a7e68927a6e 100644 --- a/src/Symfony/Component/JsonStreamer/DataModel/Read/ObjectNode.php +++ b/src/Symfony/Component/JsonStreamer/DataModel/Read/ObjectNode.php @@ -11,7 +11,6 @@ namespace Symfony\Component\JsonStreamer\DataModel\Read; -use Symfony\Component\JsonStreamer\DataModel\DataAccessorInterface; use Symfony\Component\TypeInfo\Type\ObjectType; use Symfony\Component\TypeInfo\Type\UnionType; @@ -25,7 +24,7 @@ final class ObjectNode implements DataModelNodeInterface { /** - * @param array $properties + * @param array $properties */ public function __construct( private ObjectType $type, @@ -50,7 +49,7 @@ public function getType(): ObjectType } /** - * @return array + * @return array */ public function getProperties(): array { diff --git a/src/Symfony/Component/JsonStreamer/DataModel/ScalarDataAccessor.php b/src/Symfony/Component/JsonStreamer/DataModel/ScalarDataAccessor.php deleted file mode 100644 index f60220dd82e7a..0000000000000 --- a/src/Symfony/Component/JsonStreamer/DataModel/ScalarDataAccessor.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\JsonStreamer\DataModel; - -use PhpParser\BuilderFactory; -use PhpParser\Node\Expr; - -/** - * Defines the way to access a scalar value. - * - * @author Mathias Arlaud - * - * @internal - */ -final class ScalarDataAccessor implements DataAccessorInterface -{ - public function __construct( - private mixed $value, - ) { - } - - public function toPhpExpr(): Expr - { - return (new BuilderFactory())->val($this->value); - } -} diff --git a/src/Symfony/Component/JsonStreamer/DataModel/VariableDataAccessor.php b/src/Symfony/Component/JsonStreamer/DataModel/VariableDataAccessor.php deleted file mode 100644 index 0046f55b4e7e0..0000000000000 --- a/src/Symfony/Component/JsonStreamer/DataModel/VariableDataAccessor.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\JsonStreamer\DataModel; - -use PhpParser\BuilderFactory; -use PhpParser\Node\Expr; - -/** - * Defines the way to access data using a variable. - * - * @author Mathias Arlaud - * - * @internal - */ -final class VariableDataAccessor implements DataAccessorInterface -{ - public function __construct( - private string $name, - ) { - } - - public function toPhpExpr(): Expr - { - return (new BuilderFactory())->var($this->name); - } -} diff --git a/src/Symfony/Component/JsonStreamer/DataModel/Write/BackedEnumNode.php b/src/Symfony/Component/JsonStreamer/DataModel/Write/BackedEnumNode.php index ba96b98319d1e..5a3b74861c3cd 100644 --- a/src/Symfony/Component/JsonStreamer/DataModel/Write/BackedEnumNode.php +++ b/src/Symfony/Component/JsonStreamer/DataModel/Write/BackedEnumNode.php @@ -11,7 +11,6 @@ namespace Symfony\Component\JsonStreamer\DataModel\Write; -use Symfony\Component\JsonStreamer\DataModel\DataAccessorInterface; use Symfony\Component\TypeInfo\Type\BackedEnumType; /** @@ -26,12 +25,12 @@ final class BackedEnumNode implements DataModelNodeInterface { public function __construct( - private DataAccessorInterface $accessor, + private string $accessor, private BackedEnumType $type, ) { } - public function withAccessor(DataAccessorInterface $accessor): self + public function withAccessor(string $accessor): self { return new self($accessor, $this->type); } @@ -41,7 +40,7 @@ public function getIdentifier(): string return (string) $this->getType(); } - public function getAccessor(): DataAccessorInterface + public function getAccessor(): string { return $this->accessor; } diff --git a/src/Symfony/Component/JsonStreamer/DataModel/Write/CollectionNode.php b/src/Symfony/Component/JsonStreamer/DataModel/Write/CollectionNode.php index 2f324fb404908..a334437c6891b 100644 --- a/src/Symfony/Component/JsonStreamer/DataModel/Write/CollectionNode.php +++ b/src/Symfony/Component/JsonStreamer/DataModel/Write/CollectionNode.php @@ -11,7 +11,6 @@ namespace Symfony\Component\JsonStreamer\DataModel\Write; -use Symfony\Component\JsonStreamer\DataModel\DataAccessorInterface; use Symfony\Component\TypeInfo\Type\CollectionType; /** @@ -24,13 +23,13 @@ final class CollectionNode implements DataModelNodeInterface { public function __construct( - private DataAccessorInterface $accessor, + private string $accessor, private CollectionType $type, private DataModelNodeInterface $item, ) { } - public function withAccessor(DataAccessorInterface $accessor): self + public function withAccessor(string $accessor): self { return new self($accessor, $this->type, $this->item); } @@ -40,7 +39,7 @@ public function getIdentifier(): string return (string) $this->getType(); } - public function getAccessor(): DataAccessorInterface + public function getAccessor(): string { return $this->accessor; } diff --git a/src/Symfony/Component/JsonStreamer/DataModel/Write/CompositeNode.php b/src/Symfony/Component/JsonStreamer/DataModel/Write/CompositeNode.php index 705d610fe7932..2469fbfb0e14c 100644 --- a/src/Symfony/Component/JsonStreamer/DataModel/Write/CompositeNode.php +++ b/src/Symfony/Component/JsonStreamer/DataModel/Write/CompositeNode.php @@ -11,7 +11,6 @@ namespace Symfony\Component\JsonStreamer\DataModel\Write; -use Symfony\Component\JsonStreamer\DataModel\DataAccessorInterface; use Symfony\Component\JsonStreamer\Exception\InvalidArgumentException; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\Type\UnionType; @@ -43,7 +42,7 @@ final class CompositeNode implements DataModelNodeInterface * @param list $nodes */ public function __construct( - private DataAccessorInterface $accessor, + private string $accessor, array $nodes, ) { if (\count($nodes) < 2) { @@ -60,7 +59,7 @@ public function __construct( $this->nodes = $nodes; } - public function withAccessor(DataAccessorInterface $accessor): self + public function withAccessor(string $accessor): self { return new self($accessor, array_map(static fn (DataModelNodeInterface $n): DataModelNodeInterface => $n->withAccessor($accessor), $this->nodes)); } @@ -70,7 +69,7 @@ public function getIdentifier(): string return (string) $this->getType(); } - public function getAccessor(): DataAccessorInterface + public function getAccessor(): string { return $this->accessor; } diff --git a/src/Symfony/Component/JsonStreamer/DataModel/Write/DataModelNodeInterface.php b/src/Symfony/Component/JsonStreamer/DataModel/Write/DataModelNodeInterface.php index fa94649cda40a..7768cd4179a85 100644 --- a/src/Symfony/Component/JsonStreamer/DataModel/Write/DataModelNodeInterface.php +++ b/src/Symfony/Component/JsonStreamer/DataModel/Write/DataModelNodeInterface.php @@ -11,7 +11,6 @@ namespace Symfony\Component\JsonStreamer\DataModel\Write; -use Symfony\Component\JsonStreamer\DataModel\DataAccessorInterface; use Symfony\Component\TypeInfo\Type; /** @@ -27,7 +26,7 @@ public function getIdentifier(): string; public function getType(): Type; - public function getAccessor(): DataAccessorInterface; + public function getAccessor(): string; - public function withAccessor(DataAccessorInterface $accessor): self; + public function withAccessor(string $accessor): self; } diff --git a/src/Symfony/Component/JsonStreamer/DataModel/Write/ObjectNode.php b/src/Symfony/Component/JsonStreamer/DataModel/Write/ObjectNode.php index 56dfcad38c0fe..1f8f79a171067 100644 --- a/src/Symfony/Component/JsonStreamer/DataModel/Write/ObjectNode.php +++ b/src/Symfony/Component/JsonStreamer/DataModel/Write/ObjectNode.php @@ -11,9 +11,6 @@ namespace Symfony\Component\JsonStreamer\DataModel\Write; -use Symfony\Component\JsonStreamer\DataModel\DataAccessorInterface; -use Symfony\Component\JsonStreamer\DataModel\FunctionDataAccessor; -use Symfony\Component\JsonStreamer\DataModel\PropertyDataAccessor; use Symfony\Component\TypeInfo\Type\ObjectType; /** @@ -29,29 +26,23 @@ final class ObjectNode implements DataModelNodeInterface * @param array $properties */ public function __construct( - private DataAccessorInterface $accessor, + private string $accessor, private ObjectType $type, private array $properties, private bool $mock = false, ) { } - public static function createMock(DataAccessorInterface $accessor, ObjectType $type): self + public static function createMock(string $accessor, ObjectType $type): self { return new self($accessor, $type, [], true); } - public function withAccessor(DataAccessorInterface $accessor): self + public function withAccessor(string $accessor): self { $properties = []; foreach ($this->properties as $key => $property) { - $propertyAccessor = $property->getAccessor(); - - if ($propertyAccessor instanceof PropertyDataAccessor || $propertyAccessor instanceof FunctionDataAccessor && $propertyAccessor->getObjectAccessor()) { - $propertyAccessor = $propertyAccessor->withObjectAccessor($accessor); - } - - $properties[$key] = $property->withAccessor($propertyAccessor); + $properties[$key] = $property->withAccessor(str_replace($this->accessor, $accessor, $property->getAccessor())); } return new self($accessor, $this->type, $properties, $this->mock); @@ -62,7 +53,7 @@ public function getIdentifier(): string return (string) $this->getType(); } - public function getAccessor(): DataAccessorInterface + public function getAccessor(): string { return $this->accessor; } diff --git a/src/Symfony/Component/JsonStreamer/DataModel/Write/ScalarNode.php b/src/Symfony/Component/JsonStreamer/DataModel/Write/ScalarNode.php index 53dc88b321d3f..d40319e0e5013 100644 --- a/src/Symfony/Component/JsonStreamer/DataModel/Write/ScalarNode.php +++ b/src/Symfony/Component/JsonStreamer/DataModel/Write/ScalarNode.php @@ -11,7 +11,6 @@ namespace Symfony\Component\JsonStreamer\DataModel\Write; -use Symfony\Component\JsonStreamer\DataModel\DataAccessorInterface; use Symfony\Component\TypeInfo\Type\BuiltinType; /** @@ -26,12 +25,12 @@ final class ScalarNode implements DataModelNodeInterface { public function __construct( - private DataAccessorInterface $accessor, + private string $accessor, private BuiltinType $type, ) { } - public function withAccessor(DataAccessorInterface $accessor): self + public function withAccessor(string $accessor): self { return new self($accessor, $this->type); } @@ -41,7 +40,7 @@ public function getIdentifier(): string return (string) $this->getType(); } - public function getAccessor(): DataAccessorInterface + public function getAccessor(): string { return $this->accessor; } diff --git a/src/Symfony/Component/JsonStreamer/Read/PhpAstBuilder.php b/src/Symfony/Component/JsonStreamer/Read/PhpAstBuilder.php deleted file mode 100644 index 7a6e23762beca..0000000000000 --- a/src/Symfony/Component/JsonStreamer/Read/PhpAstBuilder.php +++ /dev/null @@ -1,590 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\JsonStreamer\Read; - -use PhpParser\BuilderFactory; -use PhpParser\Node; -use PhpParser\Node\Expr; -use PhpParser\Node\Expr\Array_; -use PhpParser\Node\Expr\ArrayDimFetch; -use PhpParser\Node\Expr\ArrayItem; -use PhpParser\Node\Expr\Assign; -use PhpParser\Node\Expr\BinaryOp\BooleanAnd; -use PhpParser\Node\Expr\BinaryOp\Coalesce; -use PhpParser\Node\Expr\BinaryOp\Identical; -use PhpParser\Node\Expr\BinaryOp\NotIdentical; -use PhpParser\Node\Expr\Cast\Object_ as ObjectCast; -use PhpParser\Node\Expr\Cast\String_ as StringCast; -use PhpParser\Node\Expr\ClassConstFetch; -use PhpParser\Node\Expr\Closure; -use PhpParser\Node\Expr\ClosureUse; -use PhpParser\Node\Expr\Match_; -use PhpParser\Node\Expr\Ternary; -use PhpParser\Node\Expr\Throw_; -use PhpParser\Node\Expr\Yield_; -use PhpParser\Node\Identifier; -use PhpParser\Node\MatchArm; -use PhpParser\Node\Name\FullyQualified; -use PhpParser\Node\Param; -use PhpParser\Node\Stmt; -use PhpParser\Node\Stmt\Expression; -use PhpParser\Node\Stmt\Foreach_; -use PhpParser\Node\Stmt\If_; -use PhpParser\Node\Stmt\Return_; -use Psr\Container\ContainerInterface; -use Symfony\Component\JsonStreamer\DataModel\PhpExprDataAccessor; -use Symfony\Component\JsonStreamer\DataModel\Read\BackedEnumNode; -use Symfony\Component\JsonStreamer\DataModel\Read\CollectionNode; -use Symfony\Component\JsonStreamer\DataModel\Read\CompositeNode; -use Symfony\Component\JsonStreamer\DataModel\Read\DataModelNodeInterface; -use Symfony\Component\JsonStreamer\DataModel\Read\ObjectNode; -use Symfony\Component\JsonStreamer\DataModel\Read\ScalarNode; -use Symfony\Component\JsonStreamer\Exception\LogicException; -use Symfony\Component\JsonStreamer\Exception\UnexpectedValueException; -use Symfony\Component\TypeInfo\Type\BackedEnumType; -use Symfony\Component\TypeInfo\Type\BuiltinType; -use Symfony\Component\TypeInfo\Type\CollectionType; -use Symfony\Component\TypeInfo\Type\ObjectType; -use Symfony\Component\TypeInfo\Type\WrappingTypeInterface; -use Symfony\Component\TypeInfo\TypeIdentifier; - -/** - * Builds a PHP syntax tree that reads JSON stream. - * - * @author Mathias Arlaud - * - * @internal - */ -final class PhpAstBuilder -{ - private BuilderFactory $builder; - - public function __construct() - { - $this->builder = new BuilderFactory(); - } - - /** - * @param array $options - * @param array $context - * - * @return list - */ - public function build(DataModelNodeInterface $dataModel, bool $decodeFromStream, array $options = [], array $context = []): array - { - if ($decodeFromStream) { - return [new Return_(new Closure([ - 'static' => true, - 'params' => [ - new Param($this->builder->var('stream'), type: new Identifier('mixed')), - new Param($this->builder->var('valueTransformers'), type: new FullyQualified(ContainerInterface::class)), - new Param($this->builder->var('instantiator'), type: new FullyQualified(LazyInstantiator::class)), - new Param($this->builder->var('options'), type: new Identifier('array')), - ], - 'returnType' => new Identifier('mixed'), - 'stmts' => [ - ...$this->buildProvidersStatements($dataModel, $decodeFromStream, $context), - new Return_( - $this->nodeOnlyNeedsDecode($dataModel, $decodeFromStream) - ? $this->builder->staticCall(new FullyQualified(Decoder::class), 'decodeStream', [ - $this->builder->var('stream'), - $this->builder->val(0), - $this->builder->val(null), - ]) - : $this->builder->funcCall(new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($dataModel->getIdentifier())), [ - $this->builder->var('stream'), - $this->builder->val(0), - $this->builder->val(null), - ]), - ), - ], - ]))]; - } - - return [new Return_(new Closure([ - 'static' => true, - 'params' => [ - new Param($this->builder->var('string'), type: new Identifier('string|\\Stringable')), - new Param($this->builder->var('valueTransformers'), type: new FullyQualified(ContainerInterface::class)), - new Param($this->builder->var('instantiator'), type: new FullyQualified(Instantiator::class)), - new Param($this->builder->var('options'), type: new Identifier('array')), - ], - 'returnType' => new Identifier('mixed'), - 'stmts' => [ - ...$this->buildProvidersStatements($dataModel, $decodeFromStream, $context), - new Return_( - $this->nodeOnlyNeedsDecode($dataModel, $decodeFromStream) - ? $this->builder->staticCall(new FullyQualified(Decoder::class), 'decodeString', [new StringCast($this->builder->var('string'))]) - : $this->builder->funcCall(new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($dataModel->getIdentifier())), [ - $this->builder->staticCall(new FullyQualified(Decoder::class), 'decodeString', [new StringCast($this->builder->var('string'))]), - ]), - ), - ], - ]))]; - } - - /** - * @param array $context - * - * @return list - */ - private function buildProvidersStatements(DataModelNodeInterface $node, bool $decodeFromStream, array &$context): array - { - if ($context['providers'][$node->getIdentifier()] ?? false) { - return []; - } - - $context['providers'][$node->getIdentifier()] = true; - - if ($this->nodeOnlyNeedsDecode($node, $decodeFromStream)) { - return []; - } - - return match (true) { - $node instanceof ScalarNode || $node instanceof BackedEnumNode => $this->buildLeafProviderStatements($node, $decodeFromStream), - $node instanceof CompositeNode => $this->buildCompositeNodeStatements($node, $decodeFromStream, $context), - $node instanceof CollectionNode => $this->buildCollectionNodeStatements($node, $decodeFromStream, $context), - $node instanceof ObjectNode => $this->buildObjectNodeStatements($node, $decodeFromStream, $context), - default => throw new LogicException(\sprintf('Unexpected "%s" data model node.', $node::class)), - }; - } - - /** - * @return list - */ - private function buildLeafProviderStatements(ScalarNode|BackedEnumNode $node, bool $decodeFromStream): array - { - $accessor = $decodeFromStream - ? $this->builder->staticCall(new FullyQualified(Decoder::class), 'decodeStream', [ - $this->builder->var('stream'), - $this->builder->var('offset'), - $this->builder->var('length'), - ]) - : $this->builder->var('data'); - - $params = $decodeFromStream - ? [new Param($this->builder->var('stream')), new Param($this->builder->var('offset')), new Param($this->builder->var('length'))] - : [new Param($this->builder->var('data'))]; - - return [ - new Expression(new Assign( - new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($node->getIdentifier())), - new Closure([ - 'static' => true, - 'params' => $params, - 'stmts' => [new Return_($this->buildFormatValueStatement($node, $accessor))], - ]), - )), - ]; - } - - private function buildFormatValueStatement(DataModelNodeInterface $node, Expr $accessor): Node - { - if ($node instanceof BackedEnumNode) { - /** @var ObjectType $type */ - $type = $node->getType(); - - return $this->builder->staticCall(new FullyQualified($type->getClassName()), 'from', [$accessor]); - } - - if ($node instanceof ScalarNode) { - /** @var BuiltinType $type */ - $type = $node->getType(); - - return match (true) { - TypeIdentifier::NULL === $type->getTypeIdentifier() => $this->builder->val(null), - TypeIdentifier::OBJECT === $type->getTypeIdentifier() => new ObjectCast($accessor), - default => $accessor, - }; - } - - return $accessor; - } - - /** - * @param array $context - * - * @return list - */ - private function buildCompositeNodeStatements(CompositeNode $node, bool $decodeFromStream, array &$context): array - { - $prepareDataStmts = $decodeFromStream ? [ - new Expression(new Assign($this->builder->var('data'), $this->builder->staticCall(new FullyQualified(Decoder::class), 'decodeStream', [ - $this->builder->var('stream'), - $this->builder->var('offset'), - $this->builder->var('length'), - ]))), - ] : []; - - $providersStmts = []; - $nodesStmts = []; - - $nodeCondition = function (DataModelNodeInterface $node, Expr $accessor): Expr { - $type = $node->getType(); - - if ($type->isIdentifiedBy(TypeIdentifier::NULL)) { - return new Identical($this->builder->val(null), $this->builder->var('data')); - } - - if ($type->isIdentifiedBy(TypeIdentifier::TRUE)) { - return new Identical($this->builder->val(true), $this->builder->var('data')); - } - - if ($type->isIdentifiedBy(TypeIdentifier::FALSE)) { - return new Identical($this->builder->val(false), $this->builder->var('data')); - } - - if ($type->isIdentifiedBy(TypeIdentifier::MIXED)) { - return $this->builder->val(true); - } - - if ($type instanceof CollectionType) { - return $type->isList() - ? new BooleanAnd($this->builder->funcCall('\is_array', [$this->builder->var('data')]), $this->builder->funcCall('\array_is_list', [$this->builder->var('data')])) - : $this->builder->funcCall('\is_array', [$this->builder->var('data')]); - } - - while ($type instanceof WrappingTypeInterface) { - $type = $type->getWrappedType(); - } - - if ($type instanceof BackedEnumType) { - return $this->builder->funcCall('\is_'.$type->getBackingType()->getTypeIdentifier()->value, [$this->builder->var('data')]); - } - - if ($type instanceof ObjectType) { - return $this->builder->funcCall('\is_array', [$this->builder->var('data')]); - } - - if ($type instanceof BuiltinType) { - return $this->builder->funcCall('\is_'.$type->getTypeIdentifier()->value, [$this->builder->var('data')]); - } - - throw new LogicException(\sprintf('Unexpected "%s" type.', $type::class)); - }; - - foreach ($node->getNodes() as $n) { - if ($this->nodeOnlyNeedsDecode($n, $decodeFromStream)) { - $nodeValueStmt = $this->buildFormatValueStatement($n, $this->builder->var('data')); - } else { - $providersStmts = [...$providersStmts, ...$this->buildProvidersStatements($n, $decodeFromStream, $context)]; - $nodeValueStmt = $this->builder->funcCall( - new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($n->getIdentifier())), - [$this->builder->var('data')], - ); - } - - $nodesStmts[] = new If_($nodeCondition($n, $this->builder->var('data')), ['stmts' => [new Return_($nodeValueStmt)]]); - } - - $params = $decodeFromStream - ? [new Param($this->builder->var('stream')), new Param($this->builder->var('offset')), new Param($this->builder->var('length'))] - : [new Param($this->builder->var('data'))]; - - return [ - ...$providersStmts, - new Expression(new Assign( - new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($node->getIdentifier())), - new Closure([ - 'static' => true, - 'params' => $params, - 'uses' => [ - new ClosureUse($this->builder->var('options')), - new ClosureUse($this->builder->var('valueTransformers')), - new ClosureUse($this->builder->var('instantiator')), - new ClosureUse($this->builder->var('providers'), byRef: true), - ], - 'stmts' => [ - ...$prepareDataStmts, - ...$nodesStmts, - new Expression(new Throw_($this->builder->new(new FullyQualified(UnexpectedValueException::class), [$this->builder->funcCall('\sprintf', [ - $this->builder->val(\sprintf('Unexpected "%%s" value for "%s".', $node->getIdentifier())), - $this->builder->funcCall('\get_debug_type', [$this->builder->var('data')]), - ])]))), - ], - ]), - )), - ]; - } - - /** - * @param array $context - * - * @return list - */ - private function buildCollectionNodeStatements(CollectionNode $node, bool $decodeFromStream, array &$context): array - { - if ($decodeFromStream) { - $itemValueStmt = $this->nodeOnlyNeedsDecode($node->getItemNode(), $decodeFromStream) - ? $this->buildFormatValueStatement( - $node->getItemNode(), - $this->builder->staticCall(new FullyQualified(Decoder::class), 'decodeStream', [ - $this->builder->var('stream'), - new ArrayDimFetch($this->builder->var('v'), $this->builder->val(0)), - new ArrayDimFetch($this->builder->var('v'), $this->builder->val(1)), - ]), - ) - : $this->builder->funcCall( - new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($node->getItemNode()->getIdentifier())), [ - $this->builder->var('stream'), - new ArrayDimFetch($this->builder->var('v'), $this->builder->val(0)), - new ArrayDimFetch($this->builder->var('v'), $this->builder->val(1)), - ], - ); - } else { - $itemValueStmt = $this->nodeOnlyNeedsDecode($node->getItemNode(), $decodeFromStream) - ? $this->builder->var('v') - : $this->builder->funcCall( - new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($node->getItemNode()->getIdentifier())), - [$this->builder->var('v')], - ); - } - - $iterableClosureParams = $decodeFromStream - ? [new Param($this->builder->var('stream')), new Param($this->builder->var('data'))] - : [new Param($this->builder->var('data'))]; - - $iterableClosureStmts = [ - new Expression(new Assign( - $this->builder->var('iterable'), - new Closure([ - 'static' => true, - 'params' => $iterableClosureParams, - 'uses' => [ - new ClosureUse($this->builder->var('options')), - new ClosureUse($this->builder->var('valueTransformers')), - new ClosureUse($this->builder->var('instantiator')), - new ClosureUse($this->builder->var('providers'), byRef: true), - ], - 'stmts' => [ - new Foreach_($this->builder->var('data'), $this->builder->var('v'), [ - 'keyVar' => $this->builder->var('k'), - 'stmts' => [new Expression(new Yield_($itemValueStmt, $this->builder->var('k')))], - ]), - ], - ]), - )), - ]; - - $iterableValueStmt = $decodeFromStream - ? $this->builder->funcCall($this->builder->var('iterable'), [$this->builder->var('stream'), $this->builder->var('data')]) - : $this->builder->funcCall($this->builder->var('iterable'), [$this->builder->var('data')]); - - $prepareDataStmts = $decodeFromStream ? [ - new Expression(new Assign($this->builder->var('data'), $this->builder->staticCall( - new FullyQualified(Splitter::class), - $node->getType()->isList() ? 'splitList' : 'splitDict', - [$this->builder->var('stream'), $this->builder->var('offset'), $this->builder->var('length')], - ))), - ] : []; - - $params = $decodeFromStream - ? [new Param($this->builder->var('stream')), new Param($this->builder->var('offset')), new Param($this->builder->var('length'))] - : [new Param($this->builder->var('data'))]; - - return [ - new Expression(new Assign( - new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($node->getIdentifier())), - new Closure([ - 'static' => true, - 'params' => $params, - 'uses' => [ - new ClosureUse($this->builder->var('options')), - new ClosureUse($this->builder->var('valueTransformers')), - new ClosureUse($this->builder->var('instantiator')), - new ClosureUse($this->builder->var('providers'), byRef: true), - ], - 'stmts' => [ - ...$prepareDataStmts, - ...$iterableClosureStmts, - new Return_($node->getType()->isIdentifiedBy(TypeIdentifier::ARRAY) ? $this->builder->funcCall('\iterator_to_array', [$iterableValueStmt]) : $iterableValueStmt), - ], - ]), - )), - ...($this->nodeOnlyNeedsDecode($node->getItemNode(), $decodeFromStream) ? [] : $this->buildProvidersStatements($node->getItemNode(), $decodeFromStream, $context)), - ]; - } - - /** - * @param array $context - * - * @return list - */ - private function buildObjectNodeStatements(ObjectNode $node, bool $decodeFromStream, array &$context): array - { - if ($node->isMock()) { - return []; - } - - $propertyValueProvidersStmts = []; - $stringPropertiesValuesStmts = []; - $streamPropertiesValuesStmts = []; - - foreach ($node->getProperties() as $streamedName => $property) { - $propertyValueProvidersStmts = [ - ...$propertyValueProvidersStmts, - ...($this->nodeOnlyNeedsDecode($property['value'], $decodeFromStream) ? [] : $this->buildProvidersStatements($property['value'], $decodeFromStream, $context)), - ]; - - if ($decodeFromStream) { - $propertyValueStmt = $this->nodeOnlyNeedsDecode($property['value'], $decodeFromStream) - ? $this->buildFormatValueStatement( - $property['value'], - $this->builder->staticCall(new FullyQualified(Decoder::class), 'decodeStream', [ - $this->builder->var('stream'), - new ArrayDimFetch($this->builder->var('v'), $this->builder->val(0)), - new ArrayDimFetch($this->builder->var('v'), $this->builder->val(1)), - ]), - ) - : $this->builder->funcCall( - new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($property['value']->getIdentifier())), [ - $this->builder->var('stream'), - new ArrayDimFetch($this->builder->var('v'), $this->builder->val(0)), - new ArrayDimFetch($this->builder->var('v'), $this->builder->val(1)), - ], - ); - - $streamPropertiesValuesStmts[] = new MatchArm([$this->builder->val($streamedName)], new Assign( - $this->builder->propertyFetch($this->builder->var('object'), $property['name']), - $property['accessor'](new PhpExprDataAccessor($propertyValueStmt))->toPhpExpr(), - )); - } else { - $propertyValueStmt = $this->nodeOnlyNeedsDecode($property['value'], $decodeFromStream) - ? new Coalesce(new ArrayDimFetch($this->builder->var('data'), $this->builder->val($streamedName)), $this->builder->val('_symfony_missing_value')) - : new Ternary( - $this->builder->funcCall('\array_key_exists', [$this->builder->val($streamedName), $this->builder->var('data')]), - $this->builder->funcCall( - new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($property['value']->getIdentifier())), - [new ArrayDimFetch($this->builder->var('data'), $this->builder->val($streamedName))], - ), - $this->builder->val('_symfony_missing_value'), - ); - - $stringPropertiesValuesStmts[] = new ArrayItem( - $property['accessor'](new PhpExprDataAccessor($propertyValueStmt))->toPhpExpr(), - $this->builder->val($property['name']), - ); - } - } - - $params = $decodeFromStream - ? [new Param($this->builder->var('stream')), new Param($this->builder->var('offset')), new Param($this->builder->var('length'))] - : [new Param($this->builder->var('data'))]; - - $prepareDataStmts = $decodeFromStream ? [ - new Expression(new Assign($this->builder->var('data'), $this->builder->staticCall( - new FullyQualified(Splitter::class), - 'splitDict', - [$this->builder->var('stream'), $this->builder->var('offset'), $this->builder->var('length')], - ))), - ] : []; - - if ($decodeFromStream) { - $instantiateStmts = [ - new Return_($this->builder->methodCall($this->builder->var('instantiator'), 'instantiate', [ - new ClassConstFetch(new FullyQualified($node->getType()->getClassName()), 'class'), - new Closure([ - 'static' => true, - 'params' => [new Param($this->builder->var('object'))], - 'uses' => [ - new ClosureUse($this->builder->var('stream')), - new ClosureUse($this->builder->var('data')), - new ClosureUse($this->builder->var('options')), - new ClosureUse($this->builder->var('valueTransformers')), - new ClosureUse($this->builder->var('instantiator')), - new ClosureUse($this->builder->var('providers'), byRef: true), - ], - 'stmts' => [ - new Foreach_($this->builder->var('data'), $this->builder->var('v'), [ - 'keyVar' => $this->builder->var('k'), - 'stmts' => [new Expression(new Match_( - $this->builder->var('k'), - [...$streamPropertiesValuesStmts, new MatchArm(null, $this->builder->val(null))], - ))], - ]), - ], - ]), - ])), - ]; - } else { - $instantiateStmts = [ - new Return_($this->builder->methodCall($this->builder->var('instantiator'), 'instantiate', [ - new ClassConstFetch(new FullyQualified($node->getType()->getClassName()), 'class'), - $this->builder->funcCall('\array_filter', [ - new Array_($stringPropertiesValuesStmts, ['kind' => Array_::KIND_SHORT]), - new Closure([ - 'static' => true, - 'params' => [new Param($this->builder->var('v'))], - 'stmts' => [new Return_(new NotIdentical($this->builder->val('_symfony_missing_value'), $this->builder->var('v')))], - ]), - ]), - ])), - ]; - } - - return [ - new Expression(new Assign( - new ArrayDimFetch($this->builder->var('providers'), $this->builder->val($node->getIdentifier())), - new Closure([ - 'static' => true, - 'params' => $params, - 'uses' => [ - new ClosureUse($this->builder->var('options')), - new ClosureUse($this->builder->var('valueTransformers')), - new ClosureUse($this->builder->var('instantiator')), - new ClosureUse($this->builder->var('providers'), byRef: true), - ], - 'stmts' => [ - ...$prepareDataStmts, - ...$instantiateStmts, - ], - ]), - )), - ...$propertyValueProvidersStmts, - ]; - } - - private function nodeOnlyNeedsDecode(DataModelNodeInterface $node, bool $decodeFromStream): bool - { - if ($node instanceof CompositeNode) { - foreach ($node->getNodes() as $n) { - if (!$this->nodeOnlyNeedsDecode($n, $decodeFromStream)) { - return false; - } - } - - return true; - } - - if ($node instanceof CollectionNode) { - if ($decodeFromStream) { - return false; - } - - return $this->nodeOnlyNeedsDecode($node->getItemNode(), $decodeFromStream); - } - - if ($node instanceof ObjectNode) { - return false; - } - - if ($node instanceof BackedEnumNode) { - return false; - } - - if ($node instanceof ScalarNode) { - return !$node->getType()->isIdentifiedBy(TypeIdentifier::OBJECT); - } - - return true; - } -} diff --git a/src/Symfony/Component/JsonStreamer/Read/PhpGenerator.php b/src/Symfony/Component/JsonStreamer/Read/PhpGenerator.php new file mode 100644 index 0000000000000..28a9cc9200121 --- /dev/null +++ b/src/Symfony/Component/JsonStreamer/Read/PhpGenerator.php @@ -0,0 +1,337 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\JsonStreamer\Read; + +use Psr\Container\ContainerInterface; +use Symfony\Component\JsonStreamer\DataModel\Read\BackedEnumNode; +use Symfony\Component\JsonStreamer\DataModel\Read\CollectionNode; +use Symfony\Component\JsonStreamer\DataModel\Read\CompositeNode; +use Symfony\Component\JsonStreamer\DataModel\Read\DataModelNodeInterface; +use Symfony\Component\JsonStreamer\DataModel\Read\ObjectNode; +use Symfony\Component\JsonStreamer\DataModel\Read\ScalarNode; +use Symfony\Component\JsonStreamer\Exception\LogicException; +use Symfony\Component\JsonStreamer\Exception\UnexpectedValueException; +use Symfony\Component\TypeInfo\Type\BackedEnumType; +use Symfony\Component\TypeInfo\Type\BuiltinType; +use Symfony\Component\TypeInfo\Type\CollectionType; +use Symfony\Component\TypeInfo\Type\ObjectType; +use Symfony\Component\TypeInfo\Type\WrappingTypeInterface; +use Symfony\Component\TypeInfo\TypeIdentifier; + +/** + * Generates PHP code that reads JSON stream. + * + * @author Mathias Arlaud + * + * @internal + */ +final class PhpGenerator +{ + /** + * @param array $options + * @param array $context + */ + public function generate(DataModelNodeInterface $dataModel, bool $decodeFromStream, array $options = [], array $context = []): string + { + $context['indentation_level'] = 1; + + $providers = $this->generateProviders($dataModel, $decodeFromStream, $context); + + $context['indentation_level'] = 0; + + if ($decodeFromStream) { + return $this->line('line('', $context) + .$this->line('return static function (mixed $stream, \\'.ContainerInterface::class.' $valueTransformers, \\'.LazyInstantiator::class.' $instantiator, array $options): mixed {', $context) + .$providers + .($this->canBeDecodedWithJsonDecode($dataModel, $decodeFromStream) + ? $this->line(' return \\'.Decoder::class.'::decodeStream($stream, 0, null);', $context) + : $this->line(' return $providers[\''.$dataModel->getIdentifier().'\']($stream, 0, null);', $context)) + .$this->line('};', $context); + } + + return $this->line('line('', $context) + .$this->line('return static function (string|\\Stringable $string, \\'.ContainerInterface::class.' $valueTransformers, \\'.Instantiator::class.' $instantiator, array $options): mixed {', $context) + .$providers + .($this->canBeDecodedWithJsonDecode($dataModel, $decodeFromStream) + ? $this->line(' return \\'.Decoder::class.'::decodeString((string) $string);', $context) + : $this->line(' return $providers[\''.$dataModel->getIdentifier().'\'](\\'.Decoder::class.'::decodeString((string) $string));', $context)) + .$this->line('};', $context); + } + + /** + * @param array $context + */ + private function generateProviders(DataModelNodeInterface $node, bool $decodeFromStream, array $context): string + { + if ($context['providers'][$node->getIdentifier()] ?? false) { + return ''; + } + + $context['providers'][$node->getIdentifier()] = true; + + if ($this->canBeDecodedWithJsonDecode($node, $decodeFromStream)) { + return ''; + } + + if ($node instanceof ScalarNode || $node instanceof BackedEnumNode) { + $accessor = $decodeFromStream ? '\\'.Decoder::class.'::decodeStream($stream, $offset, $length)' : '$data'; + $arguments = $decodeFromStream ? '$stream, $offset, $length' : '$data'; + + return $this->line("\$providers['".$node->getIdentifier()."'] = static function ($arguments) {", $context) + .$this->line(' return '.$this->generateValueFormat($node, $accessor).';', $context) + .$this->line('};', $context); + } + + if ($node instanceof CompositeNode) { + $php = ''; + foreach ($node->getNodes() as $n) { + if (!$this->canBeDecodedWithJsonDecode($n, $decodeFromStream)) { + $php .= $this->generateProviders($n, $decodeFromStream, $context); + } + } + + $arguments = $decodeFromStream ? '$stream, $offset, $length' : '$data'; + + $php .= $this->line("\$providers['".$node->getIdentifier()."'] = static function ($arguments) use (\$options, \$valueTransformers, \$instantiator, &\$providers) {", $context); + + ++$context['indentation_level']; + + $php .= $decodeFromStream ? $this->line('$data = \\'.Decoder::class.'::decodeStream($stream, $offset, $length);', $context) : ''; + + foreach ($node->getNodes() as $n) { + $value = $this->canBeDecodedWithJsonDecode($n, $decodeFromStream) ? $this->generateValueFormat($n, '$data') : '$providers[\''.$n->getIdentifier().'\']($data)'; + $php .= $this->line('if ('.$this->generateCompositeNodeItemCondition($n, '$data').') {', $context) + .$this->line(" return $value;", $context) + .$this->line('}', $context); + } + + $php .= $this->line('throw new \\'.UnexpectedValueException::class.'(\\sprintf(\'Unexpected "%s" value for "'.$node->getIdentifier().'".\', \\get_debug_type($data)));', $context); + + --$context['indentation_level']; + + return $php.$this->line('};', $context); + } + + if ($node instanceof CollectionNode) { + $arguments = $decodeFromStream ? '$stream, $offset, $length' : '$data'; + + $php = $this->line("\$providers['".$node->getIdentifier()."'] = static function ($arguments) use (\$options, \$valueTransformers, \$instantiator, &\$providers) {", $context); + + ++$context['indentation_level']; + + $arguments = $decodeFromStream ? '$stream, $data' : '$data'; + $php .= ($decodeFromStream ? $this->line('$data = \\'.Splitter::class.'::'.($node->getType()->isList() ? 'splitList' : 'splitDict').'($stream, $offset, $length);', $context) : '') + .$this->line("\$iterable = static function ($arguments) use (\$options, \$valueTransformers, \$instantiator, &\$providers) {", $context) + .$this->line(' foreach ($data as $k => $v) {', $context); + + if ($decodeFromStream) { + $php .= $this->canBeDecodedWithJsonDecode($node->getItemNode(), $decodeFromStream) + ? $this->line(' yield $k => '.$this->generateValueFormat($node->getItemNode(), '\\'.Decoder::class.'::decodeStream($stream, $v[0], $v[1]);'), $context) + : $this->line(' yield $k => $providers[\''.$node->getItemNode()->getIdentifier().'\']($stream, $v[0], $v[1]);', $context); + } else { + $php .= $this->canBeDecodedWithJsonDecode($node->getItemNode(), $decodeFromStream) + ? $this->line(' yield $k => $v;', $context) + : $this->line(' yield $k => $providers[\''.$node->getItemNode()->getIdentifier().'\']($v);', $context); + } + + $php .= $this->line(' }', $context) + .$this->line('};', $context) + .$this->line('return '.($node->getType()->isIdentifiedBy(TypeIdentifier::ARRAY) ? "\\iterator_to_array(\$iterable($arguments))" : "\$iterable($arguments)").';', $context); + + --$context['indentation_level']; + + $php .= $this->line('};', $context); + + if (!$this->canBeDecodedWithJsonDecode($node->getItemNode(), $decodeFromStream)) { + $php .= $this->generateProviders($node->getItemNode(), $decodeFromStream, $context); + } + + return $php; + } + + if ($node instanceof ObjectNode) { + if ($node->isMock()) { + return ''; + } + + $arguments = $decodeFromStream ? '$stream, $offset, $length' : '$data'; + + $php = $this->line("\$providers['".$node->getIdentifier()."'] = static function ($arguments) use (\$options, \$valueTransformers, \$instantiator, &\$providers) {", $context); + + ++$context['indentation_level']; + + $php .= $decodeFromStream ? $this->line('$data = \\'.Splitter::class.'::splitDict($stream, $offset, $length);', $context) : ''; + + if ($decodeFromStream) { + $php .= $this->line('return $instantiator->instantiate(\\'.$node->getType()->getClassName().'::class, static function ($object) use ($stream, $data, $options, $valueTransformers, $instantiator, &$providers) {', $context) + .$this->line(' foreach ($data as $k => $v) {', $context) + .$this->line(' match ($k) {', $context); + + foreach ($node->getProperties() as $streamedName => $property) { + $propertyValuePhp = $this->canBeDecodedWithJsonDecode($property['value'], $decodeFromStream) + ? $this->generateValueFormat($property['value'], '\\'.Decoder::class.'::decodeStream($stream, $v[0], $v[1])') + : '$providers[\''.$property['value']->getIdentifier().'\']($stream, $v[0], $v[1])'; + + $php .= $this->line(" '$streamedName' => \$object->".$property['name'].' = '.$property['accessor']($propertyValuePhp).',', $context); + } + + $php .= $this->line(' default => null,', $context) + .$this->line(' };', $context) + .$this->line(' }', $context) + .$this->line('});', $context); + } else { + $propertiesValuePhp = '['; + $separator = ''; + foreach ($node->getProperties() as $streamedName => $property) { + $propertyValuePhp = $this->canBeDecodedWithJsonDecode($property['value'], $decodeFromStream) + ? "\$data['$streamedName'] ?? '_symfony_missing_value'" + : "\\array_key_exists('$streamedName', \$data) ? \$providers['".$property['value']->getIdentifier()."'](\$data['$streamedName']) : '_symfony_missing_value'"; + $propertiesValuePhp .= "$separator'".$property['name']."' => ".$property['accessor']($propertyValuePhp); + $separator = ', '; + } + $propertiesValuePhp .= ']'; + + $php .= $this->line('return $instantiator->instantiate(\\'.$node->getType()->getClassName()."::class, \\array_filter($propertiesValuePhp, static function (\$v) {", $context) + .$this->line(' return \'_symfony_missing_value\' !== $v;', $context) + .$this->line('}));', $context); + } + + --$context['indentation_level']; + + $php .= $this->line('};', $context); + + foreach ($node->getProperties() as $streamedName => $property) { + if (!$this->canBeDecodedWithJsonDecode($property['value'], $decodeFromStream)) { + $php .= $this->generateProviders($property['value'], $decodeFromStream, $context); + } + } + + return $php; + } + + throw new LogicException(\sprintf('Unexpected "%s" data model node.', $node::class)); + } + + private function generateValueFormat(DataModelNodeInterface $node, string $accessor): string + { + if ($node instanceof BackedEnumNode) { + /** @var ObjectType $type */ + $type = $node->getType(); + + return '\\'.$type->getClassName()."::from($accessor)"; + } + + if ($node instanceof ScalarNode) { + /** @var BuiltinType $type */ + $type = $node->getType(); + + return match (true) { + TypeIdentifier::NULL === $type->getTypeIdentifier() => 'null', + TypeIdentifier::OBJECT === $type->getTypeIdentifier() => "(object) $accessor", + default => $accessor, + }; + } + + return $accessor; + } + + private function generateCompositeNodeItemCondition(DataModelNodeInterface $node, string $accessor): string + { + $type = $node->getType(); + + if ($type->isIdentifiedBy(TypeIdentifier::NULL)) { + return "null === $accessor"; + } + + if ($type->isIdentifiedBy(TypeIdentifier::TRUE)) { + return "true === $accessor"; + } + + if ($type->isIdentifiedBy(TypeIdentifier::FALSE)) { + return "false === $accessor"; + } + + if ($type->isIdentifiedBy(TypeIdentifier::MIXED)) { + return 'true'; + } + + if ($type instanceof CollectionType) { + return $type->isList() ? "\\is_array($accessor) && \\array_is_list($accessor)" : "\\is_array($accessor)"; + } + + while ($type instanceof WrappingTypeInterface) { + $type = $type->getWrappedType(); + } + + if ($type instanceof BackedEnumType) { + return '\\is_'.$type->getBackingType()->getTypeIdentifier()->value."($accessor)"; + } + + if ($type instanceof ObjectType) { + return "\\is_array($accessor)"; + } + + if ($type instanceof BuiltinType) { + return '\\is_'.$type->getTypeIdentifier()->value."($accessor)"; + } + + throw new LogicException(\sprintf('Unexpected "%s" type.', $type::class)); + } + + /** + * @param array $context + */ + private function line(string $line, array $context): string + { + return str_repeat(' ', $context['indentation_level']).$line."\n"; + } + + /** + * Determines if the $node can be decoded using a simple "json_decode". + */ + private function canBeDecodedWithJsonDecode(DataModelNodeInterface $node, bool $decodeFromStream): bool + { + if ($node instanceof CompositeNode) { + foreach ($node->getNodes() as $n) { + if (!$this->canBeDecodedWithJsonDecode($n, $decodeFromStream)) { + return false; + } + } + + return true; + } + + if ($node instanceof CollectionNode) { + if ($decodeFromStream) { + return false; + } + + return $this->canBeDecodedWithJsonDecode($node->getItemNode(), $decodeFromStream); + } + + if ($node instanceof ObjectNode) { + return false; + } + + if ($node instanceof BackedEnumNode) { + return false; + } + + if ($node instanceof ScalarNode) { + return !$node->getType()->isIdentifiedBy(TypeIdentifier::OBJECT); + } + + return true; + } +} diff --git a/src/Symfony/Component/JsonStreamer/Read/StreamReaderGenerator.php b/src/Symfony/Component/JsonStreamer/Read/StreamReaderGenerator.php index 18720297b16c6..8f4dc27685351 100644 --- a/src/Symfony/Component/JsonStreamer/Read/StreamReaderGenerator.php +++ b/src/Symfony/Component/JsonStreamer/Read/StreamReaderGenerator.php @@ -11,21 +11,14 @@ namespace Symfony\Component\JsonStreamer\Read; -use PhpParser\PhpVersion; -use PhpParser\PrettyPrinter; -use PhpParser\PrettyPrinter\Standard; use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Filesystem; -use Symfony\Component\JsonStreamer\DataModel\DataAccessorInterface; -use Symfony\Component\JsonStreamer\DataModel\FunctionDataAccessor; use Symfony\Component\JsonStreamer\DataModel\Read\BackedEnumNode; use Symfony\Component\JsonStreamer\DataModel\Read\CollectionNode; use Symfony\Component\JsonStreamer\DataModel\Read\CompositeNode; use Symfony\Component\JsonStreamer\DataModel\Read\DataModelNodeInterface; use Symfony\Component\JsonStreamer\DataModel\Read\ObjectNode; use Symfony\Component\JsonStreamer\DataModel\Read\ScalarNode; -use Symfony\Component\JsonStreamer\DataModel\ScalarDataAccessor; -use Symfony\Component\JsonStreamer\DataModel\VariableDataAccessor; use Symfony\Component\JsonStreamer\Exception\RuntimeException; use Symfony\Component\JsonStreamer\Exception\UnsupportedException; use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoaderInterface; @@ -47,8 +40,7 @@ */ final class StreamReaderGenerator { - private ?PhpAstBuilder $phpAstBuilder = null; - private ?PrettyPrinter $phpPrinter = null; + private ?PhpGenerator $phpGenerator = null; private ?Filesystem $fs = null; public function __construct( @@ -69,13 +61,11 @@ public function generate(Type $type, bool $decodeFromStream, array $options = [] return $path; } - $this->phpAstBuilder ??= new PhpAstBuilder(); - $this->phpPrinter ??= new Standard(['phpVersion' => PhpVersion::fromComponents(8, 2)]); + $this->phpGenerator ??= new PhpGenerator(); $this->fs ??= new Filesystem(); $dataModel = $this->createDataModel($type, $options); - $nodes = $this->phpAstBuilder->build($dataModel, $decodeFromStream, $options); - $content = $this->phpPrinter->prettyPrintFile($nodes)."\n"; + $php = $this->phpGenerator->generate($dataModel, $decodeFromStream, $options); if (!$this->fs->exists($this->streamReadersDir)) { $this->fs->mkdir($this->streamReadersDir); @@ -84,7 +74,7 @@ public function generate(Type $type, bool $decodeFromStream, array $options = [] $tmpFile = $this->fs->tempnam(\dirname($path), basename($path)); try { - $this->fs->dumpFile($tmpFile, $content); + $this->fs->dumpFile($tmpFile, $php); $this->fs->rename($tmpFile, $path); $this->fs->chmod($path, 0666 & ~umask()); } catch (IOException $e) { @@ -103,7 +93,7 @@ private function getPath(Type $type, bool $decodeFromStream): string * @param array $options * @param array $context */ - public function createDataModel(Type $type, array $options = [], array $context = []): DataModelNodeInterface + private function createDataModel(Type $type, array $options = [], array $context = []): DataModelNodeInterface { $context['original_type'] ??= $type; @@ -140,11 +130,10 @@ public function createDataModel(Type $type, array $options = [], array $context $propertiesNodes[$streamedName] = [ 'name' => $propertyMetadata->getName(), 'value' => $this->createDataModel($propertyMetadata->getType(), $options, $context), - 'accessor' => function (DataAccessorInterface $accessor) use ($propertyMetadata): DataAccessorInterface { + 'accessor' => function (string $accessor) use ($propertyMetadata): string { foreach ($propertyMetadata->getStreamToNativeValueTransformers() as $valueTransformer) { if (\is_string($valueTransformer)) { - $valueTransformerServiceAccessor = new FunctionDataAccessor('get', [new ScalarDataAccessor($valueTransformer)], new VariableDataAccessor('valueTransformers')); - $accessor = new FunctionDataAccessor('transform', [$accessor, new VariableDataAccessor('options')], $valueTransformerServiceAccessor); + $accessor = "\$valueTransformers->get('$valueTransformer')->transform($accessor, \$options)"; continue; } @@ -158,9 +147,9 @@ public function createDataModel(Type $type, array $options = [], array $context $functionName = !$functionReflection->getClosureCalledClass() ? $functionReflection->getName() : \sprintf('%s::%s', $functionReflection->getClosureCalledClass()->getName(), $functionReflection->getName()); - $arguments = $functionReflection->isUserDefined() ? [$accessor, new VariableDataAccessor('options')] : [$accessor]; + $arguments = $functionReflection->isUserDefined() ? "$accessor, \$options" : $accessor; - $accessor = new FunctionDataAccessor($functionName, $arguments); + $accessor = "$functionName($arguments)"; } return $accessor; diff --git a/src/Symfony/Component/JsonStreamer/Tests/DataModel/Write/CompositeNodeTest.php b/src/Symfony/Component/JsonStreamer/Tests/DataModel/Write/CompositeNodeTest.php index a7ef7df343d6f..fb57df19ff044 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/DataModel/Write/CompositeNodeTest.php +++ b/src/Symfony/Component/JsonStreamer/Tests/DataModel/Write/CompositeNodeTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\JsonStreamer\Tests\DataModel\Write; use PHPUnit\Framework\TestCase; -use Symfony\Component\JsonStreamer\DataModel\VariableDataAccessor; use Symfony\Component\JsonStreamer\DataModel\Write\CollectionNode; use Symfony\Component\JsonStreamer\DataModel\Write\CompositeNode; use Symfony\Component\JsonStreamer\DataModel\Write\ObjectNode; @@ -27,7 +26,7 @@ public function testCannotCreateWithOnlyOneType() $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage(\sprintf('"%s" expects at least 2 nodes.', CompositeNode::class)); - new CompositeNode(new VariableDataAccessor('data'), [new ScalarNode(new VariableDataAccessor('data'), Type::int())]); + new CompositeNode('$data', [new ScalarNode('$data', Type::int())]); } public function testCannotCreateWithCompositeNodeParts() @@ -35,21 +34,21 @@ public function testCannotCreateWithCompositeNodeParts() $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage(\sprintf('Cannot set "%s" as a "%s" node.', CompositeNode::class, CompositeNode::class)); - new CompositeNode(new VariableDataAccessor('data'), [ - new CompositeNode(new VariableDataAccessor('data'), [ - new ScalarNode(new VariableDataAccessor('data'), Type::int()), - new ScalarNode(new VariableDataAccessor('data'), Type::int()), + new CompositeNode('$data', [ + new CompositeNode('$data', [ + new ScalarNode('$data', Type::int()), + new ScalarNode('$data', Type::int()), ]), - new ScalarNode(new VariableDataAccessor('data'), Type::int()), + new ScalarNode('$data', Type::int()), ]); } public function testSortNodesOnCreation() { - $composite = new CompositeNode(new VariableDataAccessor('data'), [ - $scalar = new ScalarNode(new VariableDataAccessor('data'), Type::int()), - $object = new ObjectNode(new VariableDataAccessor('data'), Type::object(self::class), []), - $collection = new CollectionNode(new VariableDataAccessor('data'), Type::list(), new ScalarNode(new VariableDataAccessor('data'), Type::int())), + $composite = new CompositeNode('$data', [ + $scalar = new ScalarNode('$data', Type::int()), + $object = new ObjectNode('$data', Type::object(self::class), []), + $collection = new CollectionNode('$data', Type::list(), new ScalarNode('$data', Type::int())), ]); $this->assertSame([$collection, $object, $scalar], $composite->getNodes()); @@ -57,14 +56,14 @@ public function testSortNodesOnCreation() public function testWithAccessor() { - $composite = new CompositeNode(new VariableDataAccessor('data'), [ - new ScalarNode(new VariableDataAccessor('foo'), Type::int()), - new ScalarNode(new VariableDataAccessor('bar'), Type::int()), + $composite = new CompositeNode('$data', [ + new ScalarNode('$foo', Type::int()), + new ScalarNode('$bar', Type::int()), ]); - $composite = $composite->withAccessor($newAccessor = new VariableDataAccessor('baz')); + $composite = $composite->withAccessor('$baz'); foreach ($composite->getNodes() as $node) { - $this->assertSame($newAccessor, $node->getAccessor()); + $this->assertSame('$baz', $node->getAccessor()); } } } diff --git a/src/Symfony/Component/JsonStreamer/Tests/DataModel/Write/ObjectNodeTest.php b/src/Symfony/Component/JsonStreamer/Tests/DataModel/Write/ObjectNodeTest.php index 0667f731e3d9f..cdc6bf71f4a15 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/DataModel/Write/ObjectNodeTest.php +++ b/src/Symfony/Component/JsonStreamer/Tests/DataModel/Write/ObjectNodeTest.php @@ -12,9 +12,6 @@ namespace Symfony\Component\JsonStreamer\Tests\DataModel\Write; use PHPUnit\Framework\TestCase; -use Symfony\Component\JsonStreamer\DataModel\FunctionDataAccessor; -use Symfony\Component\JsonStreamer\DataModel\PropertyDataAccessor; -use Symfony\Component\JsonStreamer\DataModel\VariableDataAccessor; use Symfony\Component\JsonStreamer\DataModel\Write\ObjectNode; use Symfony\Component\JsonStreamer\DataModel\Write\ScalarNode; use Symfony\Component\TypeInfo\Type; @@ -23,18 +20,18 @@ class ObjectNodeTest extends TestCase { public function testWithAccessor() { - $object = new ObjectNode(new VariableDataAccessor('foo'), Type::object(self::class), [ - new ScalarNode(new PropertyDataAccessor(new VariableDataAccessor('foo'), 'property'), Type::int()), - new ScalarNode(new FunctionDataAccessor('function', [], new VariableDataAccessor('foo')), Type::int()), - new ScalarNode(new FunctionDataAccessor('function', []), Type::int()), - new ScalarNode(new VariableDataAccessor('bar'), Type::int()), + $object = new ObjectNode('$foo', Type::object(self::class), [ + new ScalarNode('$foo->property', Type::int()), + new ScalarNode('$foo->method()', Type::int()), + new ScalarNode('function()', Type::int()), + new ScalarNode('$bar', Type::int()), ]); - $object = $object->withAccessor($newAccessor = new VariableDataAccessor('baz')); + $object = $object->withAccessor('$baz'); - $this->assertSame($newAccessor, $object->getAccessor()); - $this->assertSame($newAccessor, $object->getProperties()[0]->getAccessor()->getObjectAccessor()); - $this->assertSame($newAccessor, $object->getProperties()[1]->getAccessor()->getObjectAccessor()); - $this->assertNull($object->getProperties()[2]->getAccessor()->getObjectAccessor()); - $this->assertNotSame($newAccessor, $object->getProperties()[3]->getAccessor()); + $this->assertSame('$baz', $object->getAccessor()); + $this->assertSame('$baz->property', $object->getProperties()[0]->getAccessor()); + $this->assertSame('$baz->method()', $object->getProperties()[1]->getAccessor()); + $this->assertSame('function()', $object->getProperties()[2]->getAccessor()); + $this->assertSame('$bar', $object->getProperties()[3]->getAccessor()); } } diff --git a/src/Symfony/Component/JsonStreamer/Write/MergingStringVisitor.php b/src/Symfony/Component/JsonStreamer/Write/MergingStringVisitor.php deleted file mode 100644 index 289448ba465e8..0000000000000 --- a/src/Symfony/Component/JsonStreamer/Write/MergingStringVisitor.php +++ /dev/null @@ -1,60 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\JsonStreamer\Write; - -use PhpParser\Node; -use PhpParser\Node\Expr\Yield_; -use PhpParser\Node\Scalar\String_; -use PhpParser\Node\Stmt\Expression; -use PhpParser\NodeVisitor; -use PhpParser\NodeVisitorAbstract; - -/** - * Merges strings that are yielded consequently - * to reduce the call instructions amount. - * - * @author Mathias Arlaud - * - * @internal - */ -final class MergingStringVisitor extends NodeVisitorAbstract -{ - private string $buffer = ''; - - public function leaveNode(Node $node): int|Node|array|null - { - if (!$this->isMergeableNode($node)) { - return null; - } - - /** @var Node|null $next */ - $next = $node->getAttribute('next'); - - if ($next && $this->isMergeableNode($next)) { - $this->buffer .= $node->expr->value->value; - - return NodeVisitor::REMOVE_NODE; - } - - $string = $this->buffer.$node->expr->value->value; - $this->buffer = ''; - - return new Expression(new Yield_(new String_($string))); - } - - private function isMergeableNode(Node $node): bool - { - return $node instanceof Expression - && $node->expr instanceof Yield_ - && $node->expr->value instanceof String_; - } -} diff --git a/src/Symfony/Component/JsonStreamer/Write/PhpAstBuilder.php b/src/Symfony/Component/JsonStreamer/Write/PhpAstBuilder.php deleted file mode 100644 index f0b429b42c8f3..0000000000000 --- a/src/Symfony/Component/JsonStreamer/Write/PhpAstBuilder.php +++ /dev/null @@ -1,436 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\JsonStreamer\Write; - -use PhpParser\BuilderFactory; -use PhpParser\Node\ClosureUse; -use PhpParser\Node\Expr; -use PhpParser\Node\Expr\ArrayDimFetch; -use PhpParser\Node\Expr\Assign; -use PhpParser\Node\Expr\BinaryOp\GreaterOrEqual; -use PhpParser\Node\Expr\BinaryOp\Identical; -use PhpParser\Node\Expr\BinaryOp\Plus; -use PhpParser\Node\Expr\Closure; -use PhpParser\Node\Expr\Instanceof_; -use PhpParser\Node\Expr\PropertyFetch; -use PhpParser\Node\Expr\Ternary; -use PhpParser\Node\Expr\Throw_; -use PhpParser\Node\Expr\Yield_; -use PhpParser\Node\Expr\YieldFrom; -use PhpParser\Node\Identifier; -use PhpParser\Node\Name\FullyQualified; -use PhpParser\Node\Param; -use PhpParser\Node\Scalar\Encapsed; -use PhpParser\Node\Scalar\EncapsedStringPart; -use PhpParser\Node\Stmt; -use PhpParser\Node\Stmt\Catch_; -use PhpParser\Node\Stmt\Else_; -use PhpParser\Node\Stmt\ElseIf_; -use PhpParser\Node\Stmt\Expression; -use PhpParser\Node\Stmt\Foreach_; -use PhpParser\Node\Stmt\If_; -use PhpParser\Node\Stmt\Return_; -use PhpParser\Node\Stmt\TryCatch; -use Psr\Container\ContainerInterface; -use Symfony\Component\JsonStreamer\DataModel\VariableDataAccessor; -use Symfony\Component\JsonStreamer\DataModel\Write\BackedEnumNode; -use Symfony\Component\JsonStreamer\DataModel\Write\CollectionNode; -use Symfony\Component\JsonStreamer\DataModel\Write\CompositeNode; -use Symfony\Component\JsonStreamer\DataModel\Write\DataModelNodeInterface; -use Symfony\Component\JsonStreamer\DataModel\Write\ObjectNode; -use Symfony\Component\JsonStreamer\DataModel\Write\ScalarNode; -use Symfony\Component\JsonStreamer\Exception\LogicException; -use Symfony\Component\JsonStreamer\Exception\NotEncodableValueException; -use Symfony\Component\JsonStreamer\Exception\RuntimeException; -use Symfony\Component\JsonStreamer\Exception\UnexpectedValueException; -use Symfony\Component\TypeInfo\Type\BuiltinType; -use Symfony\Component\TypeInfo\Type\ObjectType; -use Symfony\Component\TypeInfo\Type\WrappingTypeInterface; -use Symfony\Component\TypeInfo\TypeIdentifier; - -/** - * Builds a PHP syntax tree that writes data to JSON stream. - * - * @author Mathias Arlaud - * - * @internal - */ -final class PhpAstBuilder -{ - private BuilderFactory $builder; - - public function __construct() - { - $this->builder = new BuilderFactory(); - } - - /** - * @param array $options - * @param array $context - * - * @return list - */ - public function build(DataModelNodeInterface $dataModel, array $options = [], array $context = []): array - { - $context['depth'] = 0; - - $generatorStmts = $this->buildGeneratorStatementsByIdentifiers($dataModel, $options, $context); - - // filter generators to mock only - $generatorStmts = array_merge(...array_values(array_intersect_key($generatorStmts, $context['mocks'] ?? []))); - $context['generators'] = array_intersect_key($context['generators'] ?? [], $context['mocks'] ?? []); - - return [new Return_(new Closure([ - 'static' => true, - 'params' => [ - new Param($this->builder->var('data'), type: new Identifier('mixed')), - new Param($this->builder->var('valueTransformers'), type: new FullyQualified(ContainerInterface::class)), - new Param($this->builder->var('options'), type: new Identifier('array')), - ], - 'returnType' => new FullyQualified(\Traversable::class), - 'stmts' => [ - ...$generatorStmts, - new TryCatch( - $this->buildYieldStatements($dataModel, $options, $context), - [new Catch_([new FullyQualified(\JsonException::class)], $this->builder->var('e'), [ - new Expression(new Throw_($this->builder->new(new FullyQualified(NotEncodableValueException::class), [ - $this->builder->methodCall($this->builder->var('e'), 'getMessage'), - $this->builder->val(0), - $this->builder->var('e'), - ]))), - ])] - ), - ], - ]))]; - } - - /** - * @param array $options - * @param array $context - * - * @return array> - */ - private function buildGeneratorStatementsByIdentifiers(DataModelNodeInterface $node, array $options, array &$context): array - { - if ($context['generators'][$node->getIdentifier()] ?? false) { - return []; - } - - if ($node instanceof CollectionNode) { - return $this->buildGeneratorStatementsByIdentifiers($node->getItemNode(), $options, $context); - } - - if ($node instanceof CompositeNode) { - $stmts = []; - - foreach ($node->getNodes() as $n) { - $stmts = [ - ...$stmts, - ...$this->buildGeneratorStatementsByIdentifiers($n, $options, $context), - ]; - } - - return $stmts; - } - - if (!$node instanceof ObjectNode) { - return []; - } - - if ($node->isMock()) { - $context['mocks'][$node->getIdentifier()] = true; - - return []; - } - - $context['building_generator'] = true; - - $stmts = [ - $node->getIdentifier() => [ - new Expression(new Assign( - new ArrayDimFetch($this->builder->var('generators'), $this->builder->val($node->getIdentifier())), - new Closure([ - 'static' => true, - 'params' => [ - new Param($this->builder->var('data')), - new Param($this->builder->var('depth')), - ], - 'uses' => [ - new ClosureUse($this->builder->var('valueTransformers')), - new ClosureUse($this->builder->var('options')), - new ClosureUse($this->builder->var('generators'), byRef: true), - ], - 'stmts' => [ - new If_(new GreaterOrEqual($this->builder->var('depth'), $this->builder->val(512)), [ - 'stmts' => [new Expression(new Throw_($this->builder->new(new FullyQualified(NotEncodableValueException::class), [$this->builder->val('Maximum stack depth exceeded')])))], - ]), - ...$this->buildYieldStatements($node->withAccessor(new VariableDataAccessor('data')), $options, $context), - ], - ]), - )), - ], - ]; - - foreach ($node->getProperties() as $n) { - $stmts = [ - ...$stmts, - ...$this->buildGeneratorStatementsByIdentifiers($n, $options, $context), - ]; - } - - unset($context['building_generator']); - $context['generators'][$node->getIdentifier()] = true; - - return $stmts; - } - - /** - * @param array $options - * @param array $context - * - * @return list - */ - private function buildYieldStatements(DataModelNodeInterface $dataModelNode, array $options, array $context): array - { - $accessor = $dataModelNode->getAccessor()->toPhpExpr(); - - if ($this->dataModelOnlyNeedsEncode($dataModelNode)) { - return [ - new Expression(new Yield_($this->encodeValue($accessor, $context))), - ]; - } - - if ($context['depth'] >= 512) { - return [ - new Expression(new Throw_($this->builder->new(new FullyQualified(NotEncodableValueException::class), [$this->builder->val('Maximum stack depth exceeded')]))), - ]; - } - - if ($dataModelNode instanceof ScalarNode) { - $scalarAccessor = match (true) { - TypeIdentifier::NULL === $dataModelNode->getType()->getTypeIdentifier() => $this->builder->val('null'), - TypeIdentifier::BOOL === $dataModelNode->getType()->getTypeIdentifier() => new Ternary($accessor, $this->builder->val('true'), $this->builder->val('false')), - default => $this->encodeValue($accessor, $context), - }; - - return [ - new Expression(new Yield_($scalarAccessor)), - ]; - } - - if ($dataModelNode instanceof BackedEnumNode) { - return [ - new Expression(new Yield_($this->encodeValue(new PropertyFetch($accessor, 'value'), $context))), - ]; - } - - if ($dataModelNode instanceof CompositeNode) { - $nodeCondition = function (DataModelNodeInterface $node): Expr { - $accessor = $node->getAccessor()->toPhpExpr(); - $type = $node->getType(); - - if ($type->isIdentifiedBy(TypeIdentifier::NULL, TypeIdentifier::NEVER, TypeIdentifier::VOID)) { - return new Identical($this->builder->val(null), $accessor); - } - - if ($type->isIdentifiedBy(TypeIdentifier::TRUE)) { - return new Identical($this->builder->val(true), $accessor); - } - - if ($type->isIdentifiedBy(TypeIdentifier::FALSE)) { - return new Identical($this->builder->val(false), $accessor); - } - - if ($type->isIdentifiedBy(TypeIdentifier::MIXED)) { - return $this->builder->val(true); - } - - while ($type instanceof WrappingTypeInterface) { - $type = $type->getWrappedType(); - } - - if ($type instanceof ObjectType) { - return new Instanceof_($accessor, new FullyQualified($type->getClassName())); - } - - if ($type instanceof BuiltinType) { - return $this->builder->funcCall('\is_'.$type->getTypeIdentifier()->value, [$accessor]); - } - - throw new LogicException(\sprintf('Unexpected "%s" type.', $type::class)); - }; - - $stmtsAndConditions = array_map(fn (DataModelNodeInterface $n): array => [ - 'condition' => $nodeCondition($n), - 'stmts' => $this->buildYieldStatements($n, $options, $context), - ], $dataModelNode->getNodes()); - - $if = $stmtsAndConditions[0]; - unset($stmtsAndConditions[0]); - - return [ - new If_($if['condition'], [ - 'stmts' => $if['stmts'], - 'elseifs' => array_map(fn (array $s): ElseIf_ => new ElseIf_($s['condition'], $s['stmts']), $stmtsAndConditions), - 'else' => new Else_([ - new Expression(new Throw_($this->builder->new(new FullyQualified(UnexpectedValueException::class), [$this->builder->funcCall('\sprintf', [ - $this->builder->val('Unexpected "%s" value.'), - $this->builder->funcCall('\get_debug_type', [$accessor]), - ])]))), - ]), - ]), - ]; - } - - if ($dataModelNode instanceof CollectionNode) { - ++$context['depth']; - - if ($dataModelNode->getType()->isList()) { - return [ - new Expression(new Yield_($this->builder->val('['))), - new Expression(new Assign($this->builder->var('prefix'), $this->builder->val(''))), - new Foreach_($accessor, $dataModelNode->getItemNode()->getAccessor()->toPhpExpr(), [ - 'stmts' => [ - new Expression(new Yield_($this->builder->var('prefix'))), - ...$this->buildYieldStatements($dataModelNode->getItemNode(), $options, $context), - new Expression(new Assign($this->builder->var('prefix'), $this->builder->val(','))), - ], - ]), - new Expression(new Yield_($this->builder->val(']'))), - ]; - } - - $escapedKey = $dataModelNode->getType()->getCollectionKeyType()->isIdentifiedBy(TypeIdentifier::INT) - ? new Ternary($this->builder->funcCall('is_int', [$this->builder->var('key')]), $this->builder->var('key'), $this->escapeString($this->builder->var('key'))) - : $this->escapeString($this->builder->var('key')); - - return [ - new Expression(new Yield_($this->builder->val('{'))), - new Expression(new Assign($this->builder->var('prefix'), $this->builder->val(''))), - new Foreach_($accessor, $dataModelNode->getItemNode()->getAccessor()->toPhpExpr(), [ - 'keyVar' => $this->builder->var('key'), - 'stmts' => [ - new Expression(new Assign($this->builder->var('key'), $escapedKey)), - new Expression(new Yield_(new Encapsed([ - $this->builder->var('prefix'), - new EncapsedStringPart('"'), - $this->builder->var('key'), - new EncapsedStringPart('":'), - ]))), - ...$this->buildYieldStatements($dataModelNode->getItemNode(), $options, $context), - new Expression(new Assign($this->builder->var('prefix'), $this->builder->val(','))), - ], - ]), - new Expression(new Yield_($this->builder->val('}'))), - ]; - } - - if ($dataModelNode instanceof ObjectNode) { - if (isset($context['generators'][$dataModelNode->getIdentifier()]) || $dataModelNode->isMock()) { - $depthArgument = ($context['building_generator'] ?? false) - ? new Plus($this->builder->var('depth'), $this->builder->val(1)) - : $this->builder->val($context['depth']); - - return [ - new Expression(new YieldFrom($this->builder->funcCall( - new ArrayDimFetch($this->builder->var('generators'), $this->builder->val($dataModelNode->getIdentifier())), - [$accessor, $depthArgument], - ))), - ]; - } - - $objectStmts = [new Expression(new Yield_($this->builder->val('{')))]; - $separator = ''; - - ++$context['depth']; - - foreach ($dataModelNode->getProperties() as $name => $propertyNode) { - $encodedName = json_encode($name); - if (false === $encodedName) { - throw new RuntimeException(\sprintf('Cannot encode "%s"', $name)); - } - - $encodedName = substr($encodedName, 1, -1); - - $objectStmts = [ - ...$objectStmts, - new Expression(new Yield_($this->builder->val($separator))), - new Expression(new Yield_($this->builder->val('"'))), - new Expression(new Yield_($this->builder->val($encodedName))), - new Expression(new Yield_($this->builder->val('":'))), - ...$this->buildYieldStatements($propertyNode, $options, $context), - ]; - - $separator = ','; - } - - $objectStmts[] = new Expression(new Yield_($this->builder->val('}'))); - - return $objectStmts; - } - - throw new LogicException(\sprintf('Unexpected "%s" node', $dataModelNode::class)); - } - - /** - * @param array $context - */ - private function encodeValue(Expr $value, array $context): Expr - { - return $this->builder->funcCall('\json_encode', [ - $value, - $this->builder->constFetch('\\JSON_THROW_ON_ERROR'), - $this->builder->val(512 - $context['depth']), - ]); - } - - private function escapeString(Expr $string): Expr - { - return $this->builder->funcCall('\substr', [ - $this->builder->funcCall('\json_encode', [$string]), - $this->builder->val(1), - $this->builder->val(-1), - ]); - } - - private function dataModelOnlyNeedsEncode(DataModelNodeInterface $dataModel, int $depth = 0): bool - { - if ($dataModel instanceof CompositeNode) { - foreach ($dataModel->getNodes() as $node) { - if (!$this->dataModelOnlyNeedsEncode($node, $depth)) { - return false; - } - } - - return true; - } - - if ($dataModel instanceof CollectionNode) { - return $this->dataModelOnlyNeedsEncode($dataModel->getItemNode(), $depth + 1); - } - - if (!$dataModel instanceof ScalarNode) { - return false; - } - - $type = $dataModel->getType(); - - // "null" will be written directly using the "null" string - // "bool" will be written directly using the "true" or "false" string - // but it must not prevent any json_encode if nested - if ($type->isIdentifiedBy(TypeIdentifier::NULL) || $type->isIdentifiedBy(TypeIdentifier::BOOL)) { - return $depth > 0; - } - - return true; - } -} diff --git a/src/Symfony/Component/JsonStreamer/Write/PhpGenerator.php b/src/Symfony/Component/JsonStreamer/Write/PhpGenerator.php new file mode 100644 index 0000000000000..0e79481007a65 --- /dev/null +++ b/src/Symfony/Component/JsonStreamer/Write/PhpGenerator.php @@ -0,0 +1,388 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\JsonStreamer\Write; + +use Psr\Container\ContainerInterface; +use Symfony\Component\JsonStreamer\DataModel\Write\BackedEnumNode; +use Symfony\Component\JsonStreamer\DataModel\Write\CollectionNode; +use Symfony\Component\JsonStreamer\DataModel\Write\CompositeNode; +use Symfony\Component\JsonStreamer\DataModel\Write\DataModelNodeInterface; +use Symfony\Component\JsonStreamer\DataModel\Write\ObjectNode; +use Symfony\Component\JsonStreamer\DataModel\Write\ScalarNode; +use Symfony\Component\JsonStreamer\Exception\LogicException; +use Symfony\Component\JsonStreamer\Exception\NotEncodableValueException; +use Symfony\Component\JsonStreamer\Exception\RuntimeException; +use Symfony\Component\JsonStreamer\Exception\UnexpectedValueException; +use Symfony\Component\TypeInfo\Type\BuiltinType; +use Symfony\Component\TypeInfo\Type\ObjectType; +use Symfony\Component\TypeInfo\Type\WrappingTypeInterface; +use Symfony\Component\TypeInfo\TypeIdentifier; + +/** + * Generates PHP code that writes data to JSON stream. + * + * @author Mathias Arlaud + * + * @internal + */ +final class PhpGenerator +{ + private string $yieldBuffer = ''; + + /** + * @param array $options + * @param array $context + */ + public function generate(DataModelNodeInterface $dataModel, array $options = [], array $context = []): string + { + $context['depth'] = 0; + $context['indentation_level'] = 1; + + $generators = $this->generateObjectGenerators($dataModel, $options, $context); + + // filter generators to mock only + $generators = array_intersect_key($generators, $context['mocks'] ?? []); + $context['generated_generators'] = array_intersect_key($context['generated_generators'] ?? [], $context['mocks'] ?? []); + + $context['indentation_level'] = 2; + $yields = $this->generateYields($dataModel, $options, $context) + .$this->flushYieldBuffer($context); + + $context['indentation_level'] = 0; + + return $this->line('line('', $context) + .$this->line('return static function (mixed $data, \\'.ContainerInterface::class.' $valueTransformers, array $options): \\Traversable {', $context) + .implode('', $generators) + .$this->line(' try {', $context) + .$yields + .$this->line(' } catch (\\JsonException $e) {', $context) + .$this->line(' throw new \\'.NotEncodableValueException::class.'($e->getMessage(), 0, $e);', $context) + .$this->line(' }', $context) + .$this->line('};', $context); + } + + /** + * @param array $options + * @param array $context + * + * @return array + */ + private function generateObjectGenerators(DataModelNodeInterface $node, array $options, array &$context): array + { + if ($context['generated_generators'][$node->getIdentifier()] ?? false) { + return []; + } + + if ($node instanceof CollectionNode) { + return $this->generateObjectGenerators($node->getItemNode(), $options, $context); + } + + if ($node instanceof CompositeNode) { + $generators = []; + foreach ($node->getNodes() as $n) { + $generators = [ + ...$generators, + ...$this->generateObjectGenerators($n, $options, $context), + ]; + } + + return $generators; + } + + if ($node instanceof ObjectNode) { + if ($node->isMock()) { + $context['mocks'][$node->getIdentifier()] = true; + + return []; + } + + $context['generating_generator'] = true; + + ++$context['indentation_level']; + $yields = $this->generateYields($node->withAccessor('$data'), $options, $context) + .$this->flushYieldBuffer($context); + --$context['indentation_level']; + + $generators = [ + $node->getIdentifier() => $this->line('$generators[\''.$node->getIdentifier().'\'] = static function ($data, $depth) use ($valueTransformers, $options, &$generators) {', $context) + .$this->line(' if ($depth >= 512) {', $context) + .$this->line(' throw new \\'.NotEncodableValueException::class.'(\'Maximum stack depth exceeded\');', $context) + .$this->line(' }', $context) + .$yields + .$this->line('};', $context), + ]; + + foreach ($node->getProperties() as $n) { + $generators = [ + ...$generators, + ...$this->generateObjectGenerators($n, $options, $context), + ]; + } + + unset($context['generating_generator']); + $context['generated_generators'][$node->getIdentifier()] = true; + + return $generators; + } + + return []; + } + + /** + * @param array $options + * @param array $context + */ + private function generateYields(DataModelNodeInterface $dataModelNode, array $options, array $context): string + { + $accessor = $dataModelNode->getAccessor(); + + if ($this->canBeEncodedWithJsonEncode($dataModelNode)) { + return $this->yield($this->encode($accessor, $context), $context); + } + + if ($context['depth'] >= 512) { + return $this->line('throw new '.NotEncodableValueException::class.'(\'Maximum stack depth exceeded\');', $context); + } + + if ($dataModelNode instanceof ScalarNode) { + return match (true) { + TypeIdentifier::NULL === $dataModelNode->getType()->getTypeIdentifier() => $this->yieldString('null', $context), + TypeIdentifier::BOOL === $dataModelNode->getType()->getTypeIdentifier() => $this->yield("$accessor ? 'true' : 'false'", $context), + default => $this->yield($this->encode($accessor, $context), $context), + }; + } + + if ($dataModelNode instanceof BackedEnumNode) { + return $this->yield($this->encode("{$accessor}->value", $context), $context); + } + + if ($dataModelNode instanceof CompositeNode) { + $php = $this->flushYieldBuffer($context); + foreach ($dataModelNode->getNodes() as $i => $node) { + $php .= $this->line((0 === $i ? 'if' : '} elseif').' ('.$this->generateCompositeNodeItemCondition($node).') {', $context); + + ++$context['indentation_level']; + $php .= $this->generateYields($node, $options, $context) + .$this->flushYieldBuffer($context); + --$context['indentation_level']; + } + + return $php + .$this->flushYieldBuffer($context) + .$this->line('} else {', $context) + .$this->line(' throw new \\'.UnexpectedValueException::class."(\\sprintf('Unexpected \"%s\" value.', \get_debug_type($accessor)));", $context) + .$this->line('}', $context); + } + + if ($dataModelNode instanceof CollectionNode) { + ++$context['depth']; + + if ($dataModelNode->getType()->isList()) { + $php = $this->yieldString('[', $context) + .$this->flushYieldBuffer($context) + .$this->line('$prefix = \'\';', $context) + .$this->line("foreach ($accessor as ".$dataModelNode->getItemNode()->getAccessor().') {', $context); + + ++$context['indentation_level']; + $php .= $this->yield('$prefix', $context) + .$this->generateYields($dataModelNode->getItemNode(), $options, $context) + .$this->flushYieldBuffer($context) + .$this->line('$prefix = \',\';', $context); + + --$context['indentation_level']; + + return $php + .$this->line('}', $context) + .$this->yieldString(']', $context); + } + + $escapedKey = $dataModelNode->getType()->getCollectionKeyType()->isIdentifiedBy(TypeIdentifier::INT) + ? '$key = is_int($key) ? $key : \substr(\json_encode($key), 1, -1);' + : '$key = \substr(\json_encode($key), 1, -1);'; + + $php = $this->yieldString('{', $context) + .$this->flushYieldBuffer($context) + .$this->line('$prefix = \'\';', $context) + .$this->line("foreach ($accessor as \$key => ".$dataModelNode->getItemNode()->getAccessor().') {', $context); + + ++$context['indentation_level']; + $php .= $this->line($escapedKey, $context) + .$this->yield('"{$prefix}\"{$key}\":"', $context) + .$this->generateYields($dataModelNode->getItemNode(), $options, $context) + .$this->flushYieldBuffer($context) + .$this->line('$prefix = \',\';', $context); + + --$context['indentation_level']; + + return $php + .$this->line('}', $context) + .$this->yieldString('}', $context); + } + + if ($dataModelNode instanceof ObjectNode) { + if (isset($context['generated_generators'][$dataModelNode->getIdentifier()]) || $dataModelNode->isMock()) { + $depthArgument = ($context['generating_generator'] ?? false) ? '$depth + 1' : (string) $context['depth']; + + return $this->line('yield from $generators[\''.$dataModelNode->getIdentifier().'\']('.$accessor.', '.$depthArgument.');', $context); + } + + $php = $this->yieldString('{', $context); + $separator = ''; + + ++$context['depth']; + + foreach ($dataModelNode->getProperties() as $name => $propertyNode) { + $encodedName = json_encode($name); + if (false === $encodedName) { + throw new RuntimeException(\sprintf('Cannot encode "%s"', $name)); + } + + $encodedName = substr($encodedName, 1, -1); + + $php .= $this->yieldString($separator, $context) + .$this->yieldString('"', $context) + .$this->yieldString($encodedName, $context) + .$this->yieldString('":', $context) + .$this->generateYields($propertyNode, $options, $context); + + $separator = ','; + } + + return $php + .$this->yieldString('}', $context); + } + + throw new LogicException(\sprintf('Unexpected "%s" node', $dataModelNode::class)); + } + + /** + * @param array $context + */ + private function encode(string $value, array $context): string + { + return "\json_encode($value, \\JSON_THROW_ON_ERROR, ". 512 - $context['depth'].')'; + } + + /** + * @param array $context + */ + private function yield(string $value, array $context): string + { + return $this->flushYieldBuffer($context) + .$this->line("yield $value;", $context); + } + + /** + * @param array $context + */ + private function yieldString(string $string, array $context): string + { + $this->yieldBuffer .= $string; + + return ''; + } + + /** + * @param array $context + */ + private function flushYieldBuffer(array $context): string + { + if ('' === $this->yieldBuffer) { + return ''; + } + + $yieldBuffer = $this->yieldBuffer; + $this->yieldBuffer = ''; + + return $this->yield("'$yieldBuffer'", $context); + } + + private function generateCompositeNodeItemCondition(DataModelNodeInterface $node): string + { + $accessor = $node->getAccessor(); + $type = $node->getType(); + + if ($type->isIdentifiedBy(TypeIdentifier::NULL, TypeIdentifier::NEVER, TypeIdentifier::VOID)) { + return "null === $accessor"; + } + + if ($type->isIdentifiedBy(TypeIdentifier::TRUE)) { + return "true === $accessor"; + } + + if ($type->isIdentifiedBy(TypeIdentifier::FALSE)) { + return "false === $accessor"; + } + + if ($type->isIdentifiedBy(TypeIdentifier::MIXED)) { + return 'true'; + } + + while ($type instanceof WrappingTypeInterface) { + $type = $type->getWrappedType(); + } + + if ($type instanceof ObjectType) { + return "$accessor instanceof \\".$type->getClassName(); + } + + if ($type instanceof BuiltinType) { + return '\\is_'.$type->getTypeIdentifier()->value."($accessor)"; + } + + throw new LogicException(\sprintf('Unexpected "%s" type.', $type::class)); + } + + /** + * @param array $context + */ + private function line(string $line, array $context): string + { + return str_repeat(' ', $context['indentation_level']).$line."\n"; + } + + /** + * Determines if the $node can be encoded using a simple "json_encode". + */ + private function canBeEncodedWithJsonEncode(DataModelNodeInterface $node, int $depth = 0): bool + { + if ($node instanceof CompositeNode) { + foreach ($node->getNodes() as $n) { + if (!$this->canBeEncodedWithJsonEncode($n, $depth)) { + return false; + } + } + + return true; + } + + if ($node instanceof CollectionNode) { + return $this->canBeEncodedWithJsonEncode($node->getItemNode(), $depth + 1); + } + + if (!$node instanceof ScalarNode) { + return false; + } + + $type = $node->getType(); + + // "null" will be written directly using the "null" string + // "bool" will be written directly using the "true" or "false" string + // but it must not prevent any json_encode if nested + if ($type->isIdentifiedBy(TypeIdentifier::NULL) || $type->isIdentifiedBy(TypeIdentifier::BOOL)) { + return $depth > 0; + } + + return true; + } +} diff --git a/src/Symfony/Component/JsonStreamer/Write/PhpOptimizer.php b/src/Symfony/Component/JsonStreamer/Write/PhpOptimizer.php deleted file mode 100644 index 4dddaf47aac70..0000000000000 --- a/src/Symfony/Component/JsonStreamer/Write/PhpOptimizer.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\JsonStreamer\Write; - -use PhpParser\Node; -use PhpParser\NodeTraverser; -use PhpParser\NodeVisitor\NodeConnectingVisitor; - -/** - * Optimizes a PHP syntax tree. - * - * @author Mathias Arlaud - * - * @internal - */ -final class PhpOptimizer -{ - /** - * @param list $nodes - * - * @return list - */ - public function optimize(array $nodes): array - { - $traverser = new NodeTraverser(); - $traverser->addVisitor(new NodeConnectingVisitor()); - $nodes = $traverser->traverse($nodes); - - $traverser = new NodeTraverser(); - $traverser->addVisitor(new MergingStringVisitor()); - - return $traverser->traverse($nodes); - } -} diff --git a/src/Symfony/Component/JsonStreamer/Write/StreamWriterGenerator.php b/src/Symfony/Component/JsonStreamer/Write/StreamWriterGenerator.php index c437ca0d179f5..4035d84770fbf 100644 --- a/src/Symfony/Component/JsonStreamer/Write/StreamWriterGenerator.php +++ b/src/Symfony/Component/JsonStreamer/Write/StreamWriterGenerator.php @@ -11,16 +11,8 @@ namespace Symfony\Component\JsonStreamer\Write; -use PhpParser\PhpVersion; -use PhpParser\PrettyPrinter; -use PhpParser\PrettyPrinter\Standard; use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Filesystem; -use Symfony\Component\JsonStreamer\DataModel\DataAccessorInterface; -use Symfony\Component\JsonStreamer\DataModel\FunctionDataAccessor; -use Symfony\Component\JsonStreamer\DataModel\PropertyDataAccessor; -use Symfony\Component\JsonStreamer\DataModel\ScalarDataAccessor; -use Symfony\Component\JsonStreamer\DataModel\VariableDataAccessor; use Symfony\Component\JsonStreamer\DataModel\Write\BackedEnumNode; use Symfony\Component\JsonStreamer\DataModel\Write\CollectionNode; use Symfony\Component\JsonStreamer\DataModel\Write\CompositeNode; @@ -48,9 +40,7 @@ */ final class StreamWriterGenerator { - private ?PhpAstBuilder $phpAstBuilder = null; - private ?PhpOptimizer $phpOptimizer = null; - private ?PrettyPrinter $phpPrinter = null; + private ?PhpGenerator $phpGenerator = null; private ?Filesystem $fs = null; public function __construct( @@ -71,17 +61,11 @@ public function generate(Type $type, array $options = []): string return $path; } - $this->phpAstBuilder ??= new PhpAstBuilder(); - $this->phpOptimizer ??= new PhpOptimizer(); - $this->phpPrinter ??= new Standard(['phpVersion' => PhpVersion::fromComponents(8, 2)]); + $this->phpGenerator ??= new PhpGenerator(); $this->fs ??= new Filesystem(); - $dataModel = $this->createDataModel($type, new VariableDataAccessor('data'), $options); - - $nodes = $this->phpAstBuilder->build($dataModel, $options); - $nodes = $this->phpOptimizer->optimize($nodes); - - $content = $this->phpPrinter->prettyPrintFile($nodes)."\n"; + $dataModel = $this->createDataModel($type, '$data', $options); + $php = $this->phpGenerator->generate($dataModel, $options); if (!$this->fs->exists($this->streamWritersDir)) { $this->fs->mkdir($this->streamWritersDir); @@ -90,7 +74,7 @@ public function generate(Type $type, array $options = []): string $tmpFile = $this->fs->tempnam(\dirname($path), basename($path)); try { - $this->fs->dumpFile($tmpFile, $content); + $this->fs->dumpFile($tmpFile, $php); $this->fs->rename($tmpFile, $path); $this->fs->chmod($path, 0666 & ~umask()); } catch (IOException $e) { @@ -109,7 +93,7 @@ private function getPath(Type $type): string * @param array $options * @param array $context */ - private function createDataModel(Type $type, DataAccessorInterface $accessor, array $options = [], array $context = []): DataModelNodeInterface + private function createDataModel(Type $type, string $accessor, array $options = [], array $context = []): DataModelNodeInterface { $context['original_type'] ??= $type; @@ -149,12 +133,12 @@ private function createDataModel(Type $type, DataAccessorInterface $accessor, ar $propertiesNodes = []; foreach ($propertiesMetadata as $streamedName => $propertyMetadata) { - $propertyAccessor = new PropertyDataAccessor($accessor, $propertyMetadata->getName()); + $propertyAccessor = $accessor.'->'.$propertyMetadata->getName(); foreach ($propertyMetadata->getNativeToStreamValueTransformer() as $valueTransformer) { if (\is_string($valueTransformer)) { - $valueTransformerServiceAccessor = new FunctionDataAccessor('get', [new ScalarDataAccessor($valueTransformer)], new VariableDataAccessor('valueTransformers')); - $propertyAccessor = new FunctionDataAccessor('transform', [$propertyAccessor, new VariableDataAccessor('options')], $valueTransformerServiceAccessor); + $valueTransformerServiceAccessor = "\$valueTransformers->get('$valueTransformer')"; + $propertyAccessor = "{$valueTransformerServiceAccessor}->transform($propertyAccessor, \$options)"; continue; } @@ -168,9 +152,9 @@ private function createDataModel(Type $type, DataAccessorInterface $accessor, ar $functionName = !$functionReflection->getClosureCalledClass() ? $functionReflection->getName() : \sprintf('%s::%s', $functionReflection->getClosureCalledClass()->getName(), $functionReflection->getName()); - $arguments = $functionReflection->isUserDefined() ? [$propertyAccessor, new VariableDataAccessor('options')] : [$propertyAccessor]; + $arguments = $functionReflection->isUserDefined() ? "$propertyAccessor, \$options" : $propertyAccessor; - $propertyAccessor = new FunctionDataAccessor($functionName, $arguments); + $propertyAccessor = "$functionName($arguments)"; } $propertiesNodes[$streamedName] = $this->createDataModel($propertyMetadata->getType(), $propertyAccessor, $options, $context); @@ -183,7 +167,7 @@ private function createDataModel(Type $type, DataAccessorInterface $accessor, ar return new CollectionNode( $accessor, $type, - $this->createDataModel($type->getCollectionValueType(), new VariableDataAccessor('value'), $options, $context), + $this->createDataModel($type->getCollectionValueType(), '$value', $options, $context), ); } diff --git a/src/Symfony/Component/JsonStreamer/composer.json b/src/Symfony/Component/JsonStreamer/composer.json index ba02d9fbc9172..a635710bbe5f1 100644 --- a/src/Symfony/Component/JsonStreamer/composer.json +++ b/src/Symfony/Component/JsonStreamer/composer.json @@ -16,7 +16,6 @@ } ], "require": { - "nikic/php-parser": "^5.3", "php": ">=8.2", "psr/container": "^1.1|^2.0", "psr/log": "^1|^2|^3",