diff --git a/composer.json b/composer.json index 1dececdeeaf37..c4a02d56cd14b 100644 --- a/composer.json +++ b/composer.json @@ -88,7 +88,8 @@ "egulias/email-validator": "~1.2,>=1.2.8|~2.0", "symfony/polyfill-apcu": "~1.1", "symfony/security-acl": "~2.8|~3.0", - "phpdocumentor/reflection-docblock": "^3.0" + "phpdocumentor/reflection-docblock": "^3.0", + "nikic/PHP-Parser": "~2.0" }, "conflict": { "phpdocumentor/reflection-docblock": "<3.0", diff --git a/src/Symfony/Component/AstGenerator/.gitignore b/src/Symfony/Component/AstGenerator/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/AstGenerator/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/AstGenerator/AstGeneratorChain.php b/src/Symfony/Component/AstGenerator/AstGeneratorChain.php new file mode 100644 index 0000000000000..7efb980f6f288 --- /dev/null +++ b/src/Symfony/Component/AstGenerator/AstGeneratorChain.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator; + +/** + * Generator delegating the generation to a chain of generators. + * + * @author Joel Wurtz + */ +class AstGeneratorChain implements AstGeneratorInterface +{ + /** @var AstGeneratorInterface[] A list of generators */ + protected $generators; + + /** @var bool Whether the generation must return as soon as possible or use all generators, default to false */ + protected $returnOnFirst; + + public function __construct(array $generators = array(), $returnOnFirst = false) + { + $this->generators = $generators; + $this->returnOnFirst = $returnOnFirst; + } + + /** + * {@inheritdoc} + */ + public function generate($object, array $context = array()) + { + $nodes = array(); + + foreach ($this->generators as $generator) { + if ($generator instanceof AstGeneratorInterface && $generator->supportsGeneration($object)) { + $nodes = array_merge($nodes, $generator->generate($object, $context)); + + if ($this->returnOnFirst) { + return $nodes; + } + } + } + + return $nodes; + } + + /** + * {@inheritdoc} + */ + public function supportsGeneration($object) + { + foreach ($this->generators as $generator) { + if ($generator instanceof AstGeneratorInterface && $generator->supportsGeneration($object)) { + return true; + } + } + + return false; + } +} diff --git a/src/Symfony/Component/AstGenerator/AstGeneratorInterface.php b/src/Symfony/Component/AstGenerator/AstGeneratorInterface.php new file mode 100644 index 0000000000000..30abb726aacec --- /dev/null +++ b/src/Symfony/Component/AstGenerator/AstGeneratorInterface.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator; + +/** + * An AstGeneratorInterface is a contract to transform an object into an AST. + * + * @author Joel Wurtz + */ +interface AstGeneratorInterface +{ + /** + * Generate an object into an AST given a specific context. + * + * @param mixed $object Object to generate AST from + * @param array $context Context for the generator + * + * @return \PhpParser\Node[] An array of statements (AST Node) + */ + public function generate($object, array $context = array()); + + /** + * Check whether the given object is supported for generation by this generator. + * + * @param mixed $object Object to generate AST from + * + * @return bool + */ + public function supportsGeneration($object); +} diff --git a/src/Symfony/Component/AstGenerator/Exception/MissingContextException.php b/src/Symfony/Component/AstGenerator/Exception/MissingContextException.php new file mode 100644 index 0000000000000..1edae8bb1e89c --- /dev/null +++ b/src/Symfony/Component/AstGenerator/Exception/MissingContextException.php @@ -0,0 +1,7 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator\Hydrate; + +use PhpParser\Node\Expr; +use PhpParser\Node\Scalar; + +/** + * Create AST Statement to normalize a Class into a stdClassObject. + * + * @author Joel Wurtz + */ +class ArrayHydrateGenerator extends HydrateFromObjectGenerator +{ + /** + * {@inheritdoc} + */ + protected function getAssignStatement($dataVariable) + { + return new Expr\Assign($dataVariable, new Expr\Array_()); + } + + /** + * {@inheritdoc} + */ + protected function getSubAssignVariableStatement($dataVariable, $property) + { + return new Expr\ArrayDimFetch($dataVariable, new Scalar\String_($property)); + } +} diff --git a/src/Symfony/Component/AstGenerator/Hydrate/HydrateFromObjectGenerator.php b/src/Symfony/Component/AstGenerator/Hydrate/HydrateFromObjectGenerator.php new file mode 100644 index 0000000000000..e2a6fdf0d3930 --- /dev/null +++ b/src/Symfony/Component/AstGenerator/Hydrate/HydrateFromObjectGenerator.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator\Hydrate; + +use PhpParser\Node\Name; +use PhpParser\Node\Expr; +use Symfony\Component\AstGenerator\AstGeneratorInterface; +use Symfony\Component\AstGenerator\Exception\MissingContextException; +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; + +/** + * Abstract class to generate hydration of data from object + * + * @author Joel Wurtz + */ +abstract class HydrateFromObjectGenerator implements AstGeneratorInterface +{ + /** @var PropertyInfoExtractorInterface Extract list of properties from a class */ + protected $propertyInfoExtractor; + + /** @var AstGeneratorInterface Generator for hydration of types */ + protected $typeHydrateAstGenerator; + + public function __construct(PropertyInfoExtractorInterface $propertyInfoExtractor, AstGeneratorInterface $typeHydrateAstGenerator) + { + $this->propertyInfoExtractor = $propertyInfoExtractor; + $this->typeHydrateAstGenerator = $typeHydrateAstGenerator; + } + + /** + * {@inheritdoc} + */ + public function generate($object, array $context = array()) + { + if (!isset($context['input']) || !($context['input'] instanceof Expr\Variable)) { + throw new MissingContextException('Input variable not defined or not a Expr\Variable in generation context'); + } + + if (!isset($context['output']) || !($context['output'] instanceof Expr\Variable)) { + throw new MissingContextException('Output variable not defined or not a Expr\Variable in generation context'); + } + + $statements = array($this->getAssignStatement($context['output'])); + + foreach ($this->propertyInfoExtractor->getProperties($object, $context) as $property) { + // Only normalize readable property + if (!$this->propertyInfoExtractor->isReadable($object, $property, $context)) { + continue; + } + + // @TODO Have property info extractor extract the way of reading a property (public or method with method name) + $input = new Expr\MethodCall($context['input'], 'get'.ucfirst($property)); + $output = $this->getSubAssignVariableStatement($context['output'], $property); + $types = $this->propertyInfoExtractor->getTypes($object, $property, $context); + + // If no type can be extracted, directly assign output to input + if (null === $types || count($types) == 0) { + $statements[] = new Expr\Assign($output, $input); + + continue; + } + + // If there is multiple types, we need to know which one we must normalize + $conditionNeeded = (boolean) (count($types) > 1); + $noAssignment = true; + + foreach ($types as $type) { + if (!$this->typeHydrateAstGenerator->supportsGeneration($type)) { + continue; + } + + $noAssignment = false; + $statements = array_merge($statements, $this->typeHydrateAstGenerator->generate($type, array_merge($context, [ + 'input' => $input, + 'output' => $output, + 'condition' => $conditionNeeded, + ]))); + } + + // If nothing has been assigned, we directly put input into output + if ($noAssignment) { + $statements[] = new Expr\Assign($output, $input); + } + } + + return $statements; + } + + /** + * {@inheritdoc} + */ + public function supportsGeneration($object) + { + return is_string($object) && class_exists($object); + } + + /** + * Create the assign statement. + * + * @param Expr\Variable $dataVariable Variable to use + * + * @return Expr\Assign An assignment for the variable + */ + abstract protected function getAssignStatement($dataVariable); + + /** + * Create the sub assign variable statement. + * + * @param Expr\Variable $dataVariable Variable to use + * @param string $property Property name for object or array dimension + * + * @return Expr\ArrayDimFetch|Expr\PropertyFetch + */ + abstract protected function getSubAssignVariableStatement($dataVariable, $property); +} diff --git a/src/Symfony/Component/AstGenerator/Hydrate/ObjectHydrateFromArrayGenerator.php b/src/Symfony/Component/AstGenerator/Hydrate/ObjectHydrateFromArrayGenerator.php new file mode 100644 index 0000000000000..3555b2ef7d0e4 --- /dev/null +++ b/src/Symfony/Component/AstGenerator/Hydrate/ObjectHydrateFromArrayGenerator.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator\Hydrate; + +use PhpParser\Node\Expr; +use PhpParser\Node\Scalar; + +class ObjectHydrateFromArrayGenerator extends ObjectHydrateGenerator +{ + /** + * {@inheritdoc} + */ + protected function createInputExpr(Expr\Variable $inputVariable, $property) + { + return new Expr\ArrayDimFetch($inputVariable, new Scalar\String_($property)); + } +} diff --git a/src/Symfony/Component/AstGenerator/Hydrate/ObjectHydrateFromStdClassGenerator.php b/src/Symfony/Component/AstGenerator/Hydrate/ObjectHydrateFromStdClassGenerator.php new file mode 100644 index 0000000000000..4498a0e24b6bb --- /dev/null +++ b/src/Symfony/Component/AstGenerator/Hydrate/ObjectHydrateFromStdClassGenerator.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator\Hydrate; + +use PhpParser\Node\Expr; + +class ObjectHydrateFromStdClassGenerator extends ObjectHydrateGenerator +{ + /** + * {@inheritdoc} + */ + protected function createInputExpr(Expr\Variable $inputVariable, $property) + { + return new Expr\PropertyFetch($inputVariable, sprintf("{'%s'}", $property)); + } +} diff --git a/src/Symfony/Component/AstGenerator/Hydrate/ObjectHydrateGenerator.php b/src/Symfony/Component/AstGenerator/Hydrate/ObjectHydrateGenerator.php new file mode 100644 index 0000000000000..dcd67e25ee6d1 --- /dev/null +++ b/src/Symfony/Component/AstGenerator/Hydrate/ObjectHydrateGenerator.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator\Hydrate; + +use PhpParser\Node\Arg; +use PhpParser\Node\Name; +use PhpParser\Node\Expr; +use Symfony\Component\AstGenerator\AstGeneratorInterface; +use Symfony\Component\AstGenerator\Exception\MissingContextException; +use Symfony\Component\AstGenerator\UniqueVariableScope; +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; + +/** + * Abstract class to generate hydration of object from data + * + * @author Joel Wurtz + */ +abstract class ObjectHydrateGenerator implements AstGeneratorInterface +{ + /** @var PropertyInfoExtractorInterface Extract list of properties from a class */ + protected $propertyInfoExtractor; + + /** @var AstGeneratorInterface Generator for hydration of types */ + protected $typeHydrateAstGenerator; + + public function __construct(PropertyInfoExtractorInterface $propertyInfoExtractor, AstGeneratorInterface $typeHydrateAstGenerator) + { + $this->propertyInfoExtractor = $propertyInfoExtractor; + $this->typeHydrateAstGenerator = $typeHydrateAstGenerator; + } + + /** + * {@inheritdoc} + */ + public function generate($object, array $context = array()) + { + if (!isset($context['input']) || !($context['input'] instanceof Expr\Variable)) { + throw new MissingContextException('Input variable not defined or not a Expr\Variable in generation context'); + } + + if (!isset($context['output']) || !($context['output'] instanceof Expr\Variable)) { + throw new MissingContextException('Output variable not defined or not a Expr\Variable in generation context'); + } + + $uniqueVariableScope = isset($context['unique_variable_scope']) ? $context['unique_variable_scope'] : new UniqueVariableScope(); + $statements = array( + new Expr\Assign($context['output'], new Expr\New_(new Name("\\".$object))), + ); + + foreach ($this->propertyInfoExtractor->getProperties($object, $context) as $property) { + // Only hydrate writable property + if (!$this->propertyInfoExtractor->isWritable($object, $property, $context)) { + continue; + } + + $output = new Expr\Variable($uniqueVariableScope->getUniqueName('output')); + $input = $this->createInputExpr($context['input'], $property); + $types = $this->propertyInfoExtractor->getTypes($object, $property, $context); + + // If no type can be extracted, directly assign output to input + if (null === $types || count($types) == 0) { + // @TODO Have property info extractor extract the way of writing a property (public or method with method name) + $statements[] = new Expr\MethodCall($context['output'], 'set'.ucfirst($property), [ + new Arg($input) + ]); + + continue; + } + + // If there is multiple types, we need to know which one we must normalize + $conditionNeeded = (boolean) (count($types) > 1); + $noAssignment = true; + + foreach ($types as $type) { + if (!$this->typeHydrateAstGenerator->supportsGeneration($type)) { + continue; + } + + $noAssignment = false; + $statements = array_merge($statements, $this->typeHydrateAstGenerator->generate($type, array_merge($context, [ + 'input' => $input, + 'output' => $output, + 'condition' => $conditionNeeded, + ]))); + } + + // If nothing has been assigned, we directly put input into output + if ($noAssignment) { + // @TODO Have property info extractor extract the way of writing a property (public or method with method name) + $statements[] = new Expr\MethodCall($context['output'], 'set'.ucfirst($property), [ + new Arg($input) + ]); + } else { + $statements[] = new Expr\MethodCall($context['output'], 'set'.ucfirst($property), [ + new Arg($output) + ]); + } + } + + return $statements; + } + + /** + * {@inheritdoc} + */ + public function supportsGeneration($object) + { + return is_string($object) && class_exists($object); + } + + /** + * Create the input expression for a specific property + * + * @param Expr\Variable $inputVariable Input variable of data + * @param string $property Property to fetch + * + * @return Expr + */ + abstract protected function createInputExpr(Expr\Variable $inputVariable, $property); +} diff --git a/src/Symfony/Component/AstGenerator/Hydrate/StdClassHydrateGenerator.php b/src/Symfony/Component/AstGenerator/Hydrate/StdClassHydrateGenerator.php new file mode 100644 index 0000000000000..ece42e738e105 --- /dev/null +++ b/src/Symfony/Component/AstGenerator/Hydrate/StdClassHydrateGenerator.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator\Hydrate; + +use PhpParser\Node\Name; +use PhpParser\Node\Expr; + +/** + * Create AST Statement to normalize a Class into a stdClassObject. + * + * @author Joel Wurtz + */ +class StdClassHydrateGenerator extends HydrateFromObjectGenerator +{ + /** + * {@inheritdoc} + */ + protected function getAssignStatement($dataVariable) + { + return new Expr\Assign($dataVariable, new Expr\New_(new Name('\\stdClass'))); + } + + /** + * {@inheritdoc} + */ + protected function getSubAssignVariableStatement($dataVariable, $property) + { + return new Expr\PropertyFetch($dataVariable, sprintf("{'%s'}", $property)); + } +} diff --git a/src/Symfony/Component/AstGenerator/Hydrate/Type/CollectionTypeGenerator.php b/src/Symfony/Component/AstGenerator/Hydrate/Type/CollectionTypeGenerator.php new file mode 100644 index 0000000000000..af878b67c0de1 --- /dev/null +++ b/src/Symfony/Component/AstGenerator/Hydrate/Type/CollectionTypeGenerator.php @@ -0,0 +1,179 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator\Hydrate\Type; + +use PhpParser\Node\Expr; +use PhpParser\Node\Name; +use PhpParser\Node\Stmt; +use Symfony\Component\AstGenerator\AstGeneratorInterface; +use Symfony\Component\AstGenerator\Exception\MissingContextException; +use Symfony\Component\AstGenerator\UniqueVariableScope; +use Symfony\Component\PropertyInfo\Type; + +/** + * Abstract class to generate collection hydration + * + * @author Joel Wurtz + */ +class CollectionTypeGenerator implements AstGeneratorInterface +{ + const COLLECTION_WITH_OBJECT = 0; + const COLLECTION_WITH_ARRAY = 1; + + const OBJECT_ASSIGNMENT_ARRAY = 0; + const OBJECT_ASSIGNMENT_PROPERTY = 1; + + /** @var AstGeneratorInterface Generator for the value of the collection */ + private $subValueTypeGenerator; + + /** @var int|null Output collection type to generate */ + private $outputCollectionType; + + /** @var string Class of object to use for the collection of the output */ + private $outputObjectClass; + + /** @var int Assignment type for the output */ + private $outputObjectAssignment; + + /** + * CollectionTypeGenerator constructor. + * + * @param AstGeneratorInterface $subValueTypeGenerator Generator for the value of the collection + * @param int|null $outputCollectionType Output collection type to generate, array or stdClass, use null + * to have a dynamic choice depending on the type of the collection key (where int will be array and string stdClass) + * @param string $outputObjectClass Class of object to use for the collection of the output + * @param int $outputObjectAssignment Assignment type for the output + */ + public function __construct( + AstGeneratorInterface $subValueTypeGenerator, + $outputCollectionType = null, + $outputObjectClass = '\\stdClass', + $outputObjectAssignment = self::OBJECT_ASSIGNMENT_PROPERTY + ) + { + $this->subValueTypeGenerator = $subValueTypeGenerator; + $this->outputCollectionType = $outputCollectionType; + $this->outputObjectClass = $outputObjectClass; + $this->outputObjectAssignment = $outputObjectAssignment; + } + + /** + * {@inheritdoc} + * + * @param Type $object A type extracted with PropertyInfo component + */ + public function generate($object, array $context = array()) + { + if (!isset($context['input']) || !($context['input'] instanceof Expr)) { + throw new MissingContextException('Input variable not defined or not an Expr in generation context'); + } + + if (!isset($context['output']) || !($context['output'] instanceof Expr)) { + throw new MissingContextException('Output variable not defined or not an Expr in generation context'); + } + + $uniqueVariableScope = isset($context['unique_variable_scope']) ? $context['unique_variable_scope'] : new UniqueVariableScope(); + $statements = array( + new Expr\Assign($context['output'], $this->createCollectionAssignStatement($object)), + ); + + // Create item input + $loopValueVar = new Expr\Variable($uniqueVariableScope->getUniqueName('value')); + + // Create item output + $loopKeyVar = new Expr\Variable($uniqueVariableScope->getUniqueName('key')); + $output = $this->createCollectionItemExpr($object, $loopKeyVar, $context['output']); + + // Loop statements + $loopStatements = array(new Expr\Assign($output, $loopValueVar)); + + if (null !== $object->getCollectionValueType() && $this->subValueTypeGenerator->supportsGeneration($object->getCollectionValueType())) { + $loopStatements = $this->subValueTypeGenerator->generate($object->getCollectionValueType(), array_merge($context, array( + 'input' => $loopValueVar, + 'output' => $output + ))); + } + + $statements[] = new Stmt\Foreach_($context['input'], $loopValueVar, array( + 'keyVar' => $loopKeyVar, + 'stmts' => $loopStatements + )); + + return $statements; + } + + /** + * {@inheritdoc} + */ + public function supportsGeneration($object) + { + return $object instanceof Type && $object->isCollection(); + } + + /** + * Create the collection assign statement + * + * @return Expr + */ + protected function createCollectionAssignStatement(Type $type) + { + $outputCollectionType = $this->getOutputCollectionType($type); + + if ($outputCollectionType === self::COLLECTION_WITH_ARRAY) { + return new Expr\Array_(); + } + + return new Expr\New_(new Name($this->outputObjectClass)); + } + + /** + * Create the expression for the output assignment of an item in the array + * + * @param Type $type Type of property + * @param Expr\Variable $loopKeyVar Variable for the key in the loop + * @param Expr $output Output to use for the collection + * + * @return Expr\ArrayDimFetch|Expr\PropertyFetch + */ + protected function createCollectionItemExpr(Type $type, Expr\Variable $loopKeyVar, Expr $output) + { + $outputCollectionType = $this->getOutputCollectionType($type); + + if ($outputCollectionType === self::COLLECTION_WITH_ARRAY || $this->outputObjectAssignment == self::OBJECT_ASSIGNMENT_ARRAY) { + return new Expr\ArrayDimFetch($output, $loopKeyVar); + } + + return new Expr\PropertyFetch($output, $loopKeyVar); + } + + /** + * Get output collection type, set in constructor or guessed from type of the collection key + * + * @param Type $type + * + * @return int|null + */ + private function getOutputCollectionType(Type $type) + { + $outputCollectionType = $this->outputCollectionType; + + if ($outputCollectionType === null) { + $outputCollectionType = self::COLLECTION_WITH_ARRAY; + + if ($type->getCollectionKeyType() !== null && $type->getCollectionKeyType()->getBuiltinType() !== Type::BUILTIN_TYPE_INT) { + $outputCollectionType = self::COLLECTION_WITH_OBJECT; + } + } + + return $outputCollectionType; + } +} diff --git a/src/Symfony/Component/AstGenerator/Hydrate/Type/DenormalizableObjectTypeGenerator.php b/src/Symfony/Component/AstGenerator/Hydrate/Type/DenormalizableObjectTypeGenerator.php new file mode 100644 index 0000000000000..abc7b442256cd --- /dev/null +++ b/src/Symfony/Component/AstGenerator/Hydrate/Type/DenormalizableObjectTypeGenerator.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator\Hydrate\Type; + +use PhpParser\Node\Arg; +use PhpParser\Node\Expr; +use PhpParser\Node\Stmt; +use PhpParser\Node\Name; +use PhpParser\Node\Scalar; +use Symfony\Component\AstGenerator\AstGeneratorInterface; +use Symfony\Component\AstGenerator\Exception\MissingContextException; +use Symfony\Component\PropertyInfo\Type; + +/** + * Generate hydration of normalizable object type. + * + * @author Guilhem N. + */ +class DenormalizableObjectTypeGenerator implements AstGeneratorInterface +{ + /** + * {@inheritdoc} + * + * @param Type $object A type extracted with PropertyInfo component + */ + public function generate($object, array $context = array()) + { + if (!isset($context['input']) || !($context['input'] instanceof Expr)) { + throw new MissingContextException('Input variable not defined or not an Expr in generation context'); + } + + if (!isset($context['output']) || !($context['output'] instanceof Expr)) { + throw new MissingContextException('Output variable not defined or not an Expr in generation context'); + } + + if (!isset($context['denormalizer']) || !($context['denormalizer'] instanceof Expr)) { + throw new MissingContextException('Denormalizer variable not defined or not an Expr in generation context'); + } + + $denormalizationArgs = array( + new Arg($context['input']), + new Arg(new Scalar\String_($object->getClassName())), + ); + if (isset($context['format'])) { + $denormalizationArgs[] = new Arg($context['format']); + } else { + $denormalizationArgs[] = new Arg(new Expr\ConstFetch(new Name('null'))); + } + if (isset($context['context'])) { + $denormalizationArgs[] = new Arg($context['context']); + } + + $assign = [ + new Expr\Assign($context['output'], new Expr\MethodCall( + $context['denormalizer'], + 'denormalize', + $denormalizationArgs + )) + ]; + + if (isset($context['condition']) && $context['condition']) { + return array(new Stmt\If_( + new Expr\BinaryOp\LogicalAnd( + new Expr\MethodCall( + $context['denormalizer'], + 'supportsDenormalization', + $normalizationArgs + ) + ), + array( + 'stmts' => $assign + ) + )); + } + + return $assign; + } + + /** + * {@inheritdoc} + */ + public function supportsGeneration($object) + { + return $object instanceof Type && Type::BUILTIN_TYPE_OBJECT === $object->getBuiltinType(); + } +} diff --git a/src/Symfony/Component/AstGenerator/Hydrate/Type/NormalizableObjectTypeGenerator.php b/src/Symfony/Component/AstGenerator/Hydrate/Type/NormalizableObjectTypeGenerator.php new file mode 100644 index 0000000000000..a4c82873b29e9 --- /dev/null +++ b/src/Symfony/Component/AstGenerator/Hydrate/Type/NormalizableObjectTypeGenerator.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator\Hydrate\Type; + +use PhpParser\Node\Arg; +use PhpParser\Node\Expr; +use PhpParser\Node\Stmt; +use PhpParser\Node\Name; +use Symfony\Component\AstGenerator\AstGeneratorInterface; +use Symfony\Component\AstGenerator\Exception\MissingContextException; +use Symfony\Component\PropertyInfo\Type; + +/** + * Generate hydration of normalizable object type. + * + * @author Guilhem N. + */ +class NormalizableObjectTypeGenerator implements AstGeneratorInterface +{ + /** + * {@inheritdoc} + * + * @param Type $object A type extracted with PropertyInfo component + */ + public function generate($object, array $context = array()) + { + if (!isset($context['input']) || !($context['input'] instanceof Expr)) { + throw new MissingContextException('Input variable not defined or not an Expr in generation context'); + } + + if (!isset($context['output']) || !($context['output'] instanceof Expr)) { + throw new MissingContextException('Output variable not defined or not an Expr in generation context'); + } + + if (!isset($context['normalizer']) || !($context['normalizer'] instanceof Expr)) { + throw new MissingContextException('Normalizer variable not defined or not an Expr in generation context'); + } + + $normalizationArgs = array(new Arg($context['input'])); + if (isset($context['format'])) { + $normalizationArgs[] = new Arg($context['format']); + } else { + $normalizationArgs[] = new Arg(new Expr\ConstFetch(new Name('null'))); + } + if (isset($context['context'])) { + $normalizationArgs[] = new Arg($context['context']); + } + + $assign = array( + new Expr\Assign($context['output'], new Expr\MethodCall( + $context['normalizer'], + 'normalize', + $normalizationArgs + )) + ); + + if (isset($context['condition']) && $context['condition']) { + return array(new Stmt\If_( + new Expr\BinaryOp\LogicalAnd( + new Expr\Instanceof_(new Expr\Variable('data'), new Name($object->getClassName())), + new Expr\MethodCall( + $context['normalizer'], + 'supportsNormalization', + $normalizationArgs + ) + ), + array( + 'stmts' => $assign + ) + )); + } + + return $assign; + } + + /** + * {@inheritdoc} + */ + public function supportsGeneration($object) + { + return $object instanceof Type && Type::BUILTIN_TYPE_OBJECT === $object->getBuiltinType(); + } +} diff --git a/src/Symfony/Component/AstGenerator/Hydrate/Type/TypeGenerator.php b/src/Symfony/Component/AstGenerator/Hydrate/Type/TypeGenerator.php new file mode 100644 index 0000000000000..7704648309b43 --- /dev/null +++ b/src/Symfony/Component/AstGenerator/Hydrate/Type/TypeGenerator.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator\Hydrate\Type; + +use PhpParser\Node\Arg; +use PhpParser\Node\Expr; +use PhpParser\Node\Stmt; +use PhpParser\Node\Name; +use Symfony\Component\AstGenerator\AstGeneratorInterface; +use Symfony\Component\AstGenerator\Exception\MissingContextException; +use Symfony\Component\PropertyInfo\Type; + +/** + * Generate hydration of simple type. + * + * @author Joel Wurtz + */ +class TypeGenerator implements AstGeneratorInterface +{ + protected $supportedTypes = array( + Type::BUILTIN_TYPE_BOOL, + Type::BUILTIN_TYPE_FLOAT, + Type::BUILTIN_TYPE_INT, + Type::BUILTIN_TYPE_NULL, + Type::BUILTIN_TYPE_STRING, + ); + + protected $conditionMapping = array( + Type::BUILTIN_TYPE_BOOL => 'is_bool', + Type::BUILTIN_TYPE_FLOAT => 'is_float', + Type::BUILTIN_TYPE_INT => 'is_int', + Type::BUILTIN_TYPE_NULL => 'is_null', + Type::BUILTIN_TYPE_STRING => 'is_string', + ); + + /** + * {@inheritdoc} + * + * @param Type $object A type extracted with PropertyInfo component + */ + public function generate($object, array $context = array()) + { + if (!isset($context['input']) || !($context['input'] instanceof Expr)) { + throw new MissingContextException('Input variable not defined or not an Expr in generation context'); + } + + if (!isset($context['output']) || !($context['output'] instanceof Expr)) { + throw new MissingContextException('Output variable not defined or not an Expr in generation context'); + } + + $assign = array( + new Expr\Assign($context['output'], $context['input']) + ); + + if (isset($context['condition']) && $context['condition']) { + return array(new Stmt\If_( + new Expr\FuncCall( + new Name($this->conditionMapping[$object->getBuiltinType()]), + array( + new Arg($context['input']) + ) + ), + array( + 'stmts' => $assign + ) + )); + } + + return $assign; + } + + /** + * {@inheritdoc} + */ + public function supportsGeneration($object) + { + return $object instanceof Type && in_array($object->getBuiltinType(), $this->supportedTypes); + } +} diff --git a/src/Symfony/Component/AstGenerator/LICENSE b/src/Symfony/Component/AstGenerator/LICENSE new file mode 100644 index 0000000000000..0564c5a9b7f1f --- /dev/null +++ b/src/Symfony/Component/AstGenerator/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2016 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/AstGenerator/Normalizer/NormalizerGenerator.php b/src/Symfony/Component/AstGenerator/Normalizer/NormalizerGenerator.php new file mode 100644 index 0000000000000..26219f77ddddd --- /dev/null +++ b/src/Symfony/Component/AstGenerator/Normalizer/NormalizerGenerator.php @@ -0,0 +1,229 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator\Normalizer; + +use PhpParser\Comment; +use PhpParser\Node\Name; +use PhpParser\Node\Param; +use PhpParser\Node\Stmt; +use PhpParser\Node\Expr; +use PhpParser\Node\Scalar; +use Symfony\Component\AstGenerator\AstGeneratorInterface; +use Symfony\Component\AstGenerator\UniqueVariableScope; + +/** + * Generate a Normalizer given a Class. + * + * @author Joel Wurtz + */ +class NormalizerGenerator implements AstGeneratorInterface +{ + /** @var AstGeneratorInterface Generator which generate the statements for normalization of a given class */ + protected $normalizeStatementsGenerator; + + /** @var AstGeneratorInterface Generator which generate the statements for denormalization of a given class */ + protected $denormalizeStatementsGenerator; + + /** + * NormalizerGenerator constructor. + * + * @param AstGeneratorInterface $normalizeStatementsGenerator Generator which generate the statements for normalization of a given class + * @param AstGeneratorInterface $denormalizeStatementsGenerator Generator which generate the statements for denormalization of a given class + */ + public function __construct(AstGeneratorInterface $normalizeStatementsGenerator, AstGeneratorInterface $denormalizeStatementsGenerator) + { + $this->normalizeStatementsGenerator = $normalizeStatementsGenerator; + $this->denormalizeStatementsGenerator = $denormalizeStatementsGenerator; + } + + /** + * {@inheritdoc} + */ + public function generate($object, array $context = array()) + { + if (!isset($context['name'])) { + $reflectionClass = new \ReflectionClass($object); + $context['name'] = $reflectionClass->getShortName().'Normalizer'; + } + + return array(new Stmt\Class_( + new Name($context['name']), + array( + 'stmts' => array( + new Stmt\Use_(array( + new Name('\Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait'), + new Name('\Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait'), + )), + $this->createSupportsNormalizationMethod($object), + $this->createSupportsDenormalizationMethod($object), + $this->createNormalizeMethod($object, array_merge($context, array( + 'unique_variable_scope' => new UniqueVariableScope(), + ))), + $this->createDenormalizeMethod($object, array_merge($context, array( + 'unique_variable_scope' => new UniqueVariableScope(), + ))), + ), + 'implements' => array( + new Name('\Symfony\Component\Serializer\Normalizer\DenormalizerInterface'), + new Name('\Symfony\Component\Serializer\Normalizer\NormalizerInterface'), + new Name('\Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface'), + new Name('\Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface'), + ), + ), + array( + 'comments' => array(new Comment("/**\n * This class is generated.\n * Please do not update it manually.\n */")), + ) + )); + } + + /** + * Create method to check if normalization is supported. + * + * @param string $class Fully Qualified name of the model class + * + * @return Stmt\ClassMethod + */ + protected function createSupportsNormalizationMethod($class) + { + if (strpos($class, '\\') !== 0) { + $class = '\\'.$class; + } + + return new Stmt\ClassMethod('supportsNormalization', array( + 'type' => Stmt\Class_::MODIFIER_PUBLIC, + 'params' => array( + new Param('data'), + new Param('format', new Expr\ConstFetch(new Name('null'))), + ), + 'stmts' => array( + new Stmt\If_( + new Expr\Instanceof_(new Expr\Variable('data'), new Name($class)), + array( + 'stmts' => array( + new Stmt\Return_(new Expr\ConstFetch(new Name('true'))), + ), + ) + ), + new Stmt\Return_(new Expr\ConstFetch(new Name('false'))), + ), + )); + } + + /** + * Create method to check if denormalization is supported. + * + * @param string $class Fully Qualified name of the model class + * + * @return Stmt\ClassMethod + */ + protected function createSupportsDenormalizationMethod($class) + { + return new Stmt\ClassMethod('supportsDenormalization', array( + 'type' => Stmt\Class_::MODIFIER_PUBLIC, + 'params' => array( + new Param('data'), + new Param('type'), + new Param('format', new Expr\ConstFetch(new Name('null'))), + ), + 'stmts' => array( + new Stmt\If_( + new Expr\BinaryOp\NotIdentical(new Expr\Variable('type'), new Scalar\String_($class)), + array( + 'stmts' => array( + new Stmt\Return_(new Expr\ConstFetch(new Name('false'))), + ), + ) + ), + new Stmt\Return_(new Expr\ConstFetch(new Name('true'))), + ), + )); + } + + /** + * Create the normalization method. + * + * @param string $class Class to create normalization from + * @param array $context Context of generation + * + * @return Stmt\ClassMethod + */ + protected function createNormalizeMethod($class, array $context = array()) + { + $input = new Expr\Variable('object'); + $output = new Expr\Variable('data'); + + return new Stmt\ClassMethod('normalize', array( + 'type' => Stmt\Class_::MODIFIER_PUBLIC, + 'params' => array( + new Param('object'), + new Param('format', new Expr\ConstFetch(new Name('null'))), + new Param('context', new Expr\Array_(), 'array'), + ), + 'stmts' => array_merge($this->normalizeStatementsGenerator->generate($class, array_merge($context, array( + 'input' => $input, + 'output' => $output, + 'normalizer' => new Expr\PropertyFetch( + new Expr\Variable('this'), + 'normalizer' + ), + 'format' => new Expr\Variable(new Name('format')), + 'context' => new Expr\Variable(new Name('context')), + ))), array( + new Stmt\Return_($output), + )) + )); + } + + /** + * Create the denormalization method. + * + * @param string $class Class to create denormalization from + * @param array $context Context of generation + * + * @return Stmt\ClassMethod + */ + protected function createDenormalizeMethod($class, array $context = array()) + { + $input = new Expr\Variable('data'); + $output = new Expr\Variable('object'); + + return new Stmt\ClassMethod('denormalize', array( + 'type' => Stmt\Class_::MODIFIER_PUBLIC, + 'params' => array( + new Param('data'), + new Param('class'), + new Param('format', new Expr\ConstFetch(new Name('null'))), + new Param('context', new Expr\Array_(), 'array'), + ), + 'stmts' => array_merge($this->denormalizeStatementsGenerator->generate($class, array_merge($context, array( + 'input' => $input, + 'output' => $output, + 'denormalizer' => new Expr\PropertyFetch( + new Expr\Variable('this'), + 'denormalizer' + ), + 'format' => new Expr\Variable(new Name('format')), + 'context' => new Expr\Variable(new Name('context')), + ))), array( + new Stmt\Return_($output), + )), + )); + } + + /** + * {@inheritdoc} + */ + public function supportsGeneration($object) + { + return is_string($object) && class_exists($object); + } +} diff --git a/src/Symfony/Component/AstGenerator/README.md b/src/Symfony/Component/AstGenerator/README.md new file mode 100644 index 0000000000000..d880147946395 --- /dev/null +++ b/src/Symfony/Component/AstGenerator/README.md @@ -0,0 +1,8 @@ +AstGenerator Component +====================== + +AstGenerator allows to generate PHP AST for several Component: + + * Transform class, properties and types extracted from the PropertyInfo Component into POPO objects ans Normalizers + compatible with Serializer Component + diff --git a/src/Symfony/Component/AstGenerator/Tests/AstGeneratorChainTest.php b/src/Symfony/Component/AstGenerator/Tests/AstGeneratorChainTest.php new file mode 100644 index 0000000000000..88ec9cf80c375 --- /dev/null +++ b/src/Symfony/Component/AstGenerator/Tests/AstGeneratorChainTest.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator\Tests; + +use Symfony\Component\AstGenerator\AstGeneratorChain; +use Symfony\Component\AstGenerator\AstGeneratorInterface; + +class AstGeneratorChainTest extends \PHPUnit_Framework_TestCase +{ + public function testEmpty() + { + $generator = new AstGeneratorChain(); + + $this->assertFalse($generator->supportsGeneration('dummy')); + $this->assertEmpty($generator->generate('dummy')); + } + + public function testSupports() + { + $generatorSub = $this->getGeneratorMock(true, array('ast')); + + $generator = new AstGeneratorChain(array($generatorSub)); + $this->assertTrue($generator->supportsGeneration('dummy')); + $this->assertEquals(array('ast'), $generator->generate('dummy')); + } + + public function testMultiSupports() + { + $generatorSub1 = $this->getGeneratorMock(true, array('ast1')); + $generatorSub2 = $this->getGeneratorMock(true, array('ast2')); + + $generator = new AstGeneratorChain(array($generatorSub1, $generatorSub2)); + $this->assertTrue($generator->supportsGeneration('dummy')); + $this->assertEquals(array('ast1', 'ast2'), $generator->generate('dummy')); + } + + public function testPartialSupports() + { + $generatorSub1 = $this->getGeneratorMock(true, array('ast1')); + $generatorSub2 = $this->getGeneratorMock(false); + + $generator = new AstGeneratorChain(array($generatorSub1, $generatorSub2)); + $this->assertTrue($generator->supportsGeneration('dummy')); + $this->assertEquals(array('ast1'), $generator->generate('dummy')); + } + + public function testMultiSupportsWithFirstReturn() + { + $generatorSub1 = $this->getGeneratorMock(true, array('ast1')); + $generatorSub2 = $this->getGeneratorMock(true, array('ast2')); + + $generator = new AstGeneratorChain(array($generatorSub1, $generatorSub2), true); + $this->assertTrue($generator->supportsGeneration('dummy')); + $this->assertEquals(array('ast1'), $generator->generate('dummy')); + } + + private function getGeneratorMock($support, $return = null) + { + $generatorSub = $this->getMockBuilder(AstGeneratorInterface::class)->getMock(); + $generatorSub + ->expects($this->any()) + ->method('supportsGeneration') + ->with('dummy') + ->willReturn($support); + if (null === $return) { + $generatorSub + ->expects($this->never()) + ->method('generate'); + } else { + $generatorSub + ->expects($this->any()) + ->method('generate') + ->with('dummy', array()) + ->willReturn($return); + } + + return $generatorSub; + } +} diff --git a/src/Symfony/Component/AstGenerator/Tests/Hydrate/AbstractHydratorTest.php b/src/Symfony/Component/AstGenerator/Tests/Hydrate/AbstractHydratorTest.php new file mode 100644 index 0000000000000..7d7d3460690fc --- /dev/null +++ b/src/Symfony/Component/AstGenerator/Tests/Hydrate/AbstractHydratorTest.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator\Tests\Hydrate; + +use PhpParser\Node\Expr; +use PhpParser\PrettyPrinter\Standard; +use Symfony\Component\AstGenerator\AstGeneratorInterface; +use Symfony\Component\AstGenerator\Hydrate\ArrayHydrateGenerator; +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; +use Symfony\Component\PropertyInfo\Type; + +abstract class AbstractHydratorTest extends \PHPUnit_Framework_TestCase +{ + /** @var Standard */ + protected $printer; + + protected function setUp() + { + $this->printer = new Standard(); + } + + protected function getPropertyInfoExtractor($inputClass) + { + $propertyInfoExtractor = $this->getMockBuilder(PropertyInfoExtractorInterface::class)->getMock(); + $propertyInfoExtractor + ->expects($this->any()) + ->method('getProperties') + ->with($inputClass, $this->isType('array')) + ->willReturn(['foo', 'bar']); + $propertyInfoExtractor + ->expects($this->any()) + ->method('isReadable') + ->with($inputClass, $this->logicalOr('foo', 'bar'), $this->isType('array')) + ->will($this->returnCallback(function ($class, $property) { + return 'foo' === $property; + })); + $propertyInfoExtractor + ->expects($this->any()) + ->method('isWritable') + ->with($inputClass, $this->logicalOr('foo', 'bar'), $this->isType('array')) + ->will($this->returnCallback(function ($class, $property) { + return 'bar' === $property; + })); + $propertyInfoExtractor + ->expects($this->any()) + ->method('getTypes') + ->with($inputClass, $this->logicalOr('foo', 'bar'), $this->isType('array')) + ->willReturn([new Type('string')]); + + return $propertyInfoExtractor; + } +} diff --git a/src/Symfony/Component/AstGenerator/Tests/Hydrate/ArrayHydrateGeneratorTest.php b/src/Symfony/Component/AstGenerator/Tests/Hydrate/ArrayHydrateGeneratorTest.php new file mode 100644 index 0000000000000..cac87917eec20 --- /dev/null +++ b/src/Symfony/Component/AstGenerator/Tests/Hydrate/ArrayHydrateGeneratorTest.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator\Tests\Hydrate; + +use PhpParser\Node\Expr; +use PhpParser\PrettyPrinter\Standard; +use Symfony\Component\AstGenerator\AstGeneratorInterface; +use Symfony\Component\AstGenerator\Hydrate\ArrayHydrateGenerator; +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; +use Symfony\Component\PropertyInfo\Type; + +class ArrayHydrateGeneratorTest extends AbstractHydratorTest +{ + public function testHydrateGenerator() + { + $propertyInfoExtractor = $this->getPropertyInfoExtractor(Dummy::class); + $hydrateGenerator = new ArrayHydrateGenerator($propertyInfoExtractor, new DummyTypeGenerator()); + + $this->assertTrue($hydrateGenerator->supportsGeneration(Dummy::class)); + + $dummyObject = new Dummy(); + $dummyObject->foo = 'test'; + + eval($this->printer->prettyPrint($hydrateGenerator->generate(Dummy::class, [ + 'input' => new Expr\Variable('dummyObject'), + 'output' => new Expr\Variable('dummyArray'), + ]))); + + $this->assertInternalType('array', $dummyArray); + $this->assertArrayHasKey('foo', $dummyArray); + $this->assertEquals('test', $dummyArray['foo']); + } + + /** + * @expectedException \Symfony\Component\AstGenerator\Exception\MissingContextException + */ + public function testNoInput() + { + $propertyInfoExtractor = $this->getMockBuilder(PropertyInfoExtractorInterface::class)->getMock(); + $hydrateGenerator = new ArrayHydrateGenerator($propertyInfoExtractor, new DummyTypeGenerator()); + $hydrateGenerator->generate(Dummy::class); + } + + /** + * @expectedException \Symfony\Component\AstGenerator\Exception\MissingContextException + */ + public function testNoOutput() + { + $propertyInfoExtractor = $this->getMockBuilder(PropertyInfoExtractorInterface::class)->getMock(); + $hydrateGenerator = new ArrayHydrateGenerator($propertyInfoExtractor, new DummyTypeGenerator()); + $hydrateGenerator->generate(Dummy::class, ['input' => new Expr\Variable('test')]); + } +} + +class Dummy +{ + public $foo; + + public $bar; + + /** + * @return mixed + */ + public function getFoo() + { + return $this->foo; + } + + /** + * @param mixed $bar + */ + public function setBar($bar) + { + $this->bar = $bar; + } +} + +class DummyTypeGenerator implements AstGeneratorInterface +{ + public function generate($object, array $context = []) + { + if (!isset($context['input'])) { + throw new \Exception('no input'); + } + + if (!isset($context['output'])) { + throw new \Exception('no output'); + } + + return [new Expr\Assign($context['output'], $context['input'])]; + } + + public function supportsGeneration($object) + { + return true; + } +} diff --git a/src/Symfony/Component/AstGenerator/Tests/Hydrate/ObjectHydrateFromArrayGeneratorTest.php b/src/Symfony/Component/AstGenerator/Tests/Hydrate/ObjectHydrateFromArrayGeneratorTest.php new file mode 100644 index 0000000000000..c87c64e8bafc2 --- /dev/null +++ b/src/Symfony/Component/AstGenerator/Tests/Hydrate/ObjectHydrateFromArrayGeneratorTest.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator\Tests\Hydrate; + +use PhpParser\Node\Expr; +use PhpParser\PrettyPrinter\Standard; +use Prophecy\Argument; +use Symfony\Component\AstGenerator\AstGeneratorInterface; +use Symfony\Component\AstGenerator\Hydrate\ObjectHydrateFromArrayGenerator; +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; +use Symfony\Component\PropertyInfo\Type; + +class ObjectHydrateFromArrayGeneratorTest extends AbstractHydratorTest +{ + public function testHydrateGenerator() + { + $propertyInfoExtractor = $this->getPropertyInfoExtractor(DummyObjectArray::class); + $hydrateGenerator = new ObjectHydrateFromArrayGenerator($propertyInfoExtractor, new DummyObjectArrayTypeGenerator()); + + $this->assertTrue($hydrateGenerator->supportsGeneration(DummyObjectArray::class)); + + $array = [ + 'bar' => 'test' + ]; + + eval($this->printer->prettyPrint($hydrateGenerator->generate(DummyObjectArray::class, [ + 'input' => new Expr\Variable('array'), + 'output' => new Expr\Variable('object'), + ]))); + + $this->assertInstanceOf(DummyObjectArray::class, $object); + $this->assertEquals('test', $object->bar); + } + + /** + * @expectedException \Symfony\Component\AstGenerator\Exception\MissingContextException + */ + public function testNoInput() + { + $propertyInfoExtractor = $this->getMockBuilder(PropertyInfoExtractorInterface::class)->getMock(); + $hydrateGenerator = new ObjectHydrateFromArrayGenerator($propertyInfoExtractor, new DummyObjectArrayTypeGenerator()); + $hydrateGenerator->generate(DummyObjectArray::class); + } + + /** + * @expectedException \Symfony\Component\AstGenerator\Exception\MissingContextException + */ + public function testNoOutput() + { + $propertyInfoExtractor = $this->getMockBuilder(PropertyInfoExtractorInterface::class)->getMock(); + $hydrateGenerator = new ObjectHydrateFromArrayGenerator($propertyInfoExtractor, new DummyObjectArrayTypeGenerator()); + $hydrateGenerator->generate(DummyObjectArray::class, ['input' => new Expr\Variable('test')]); + } +} + +class DummyObjectArray +{ + public $foo; + + public $bar; + + /** + * @return mixed + */ + public function getFoo() + { + return $this->foo; + } + + /** + * @param mixed $bar + */ + public function setBar($bar) + { + $this->bar = $bar; + } +} + +class DummyObjectArrayTypeGenerator implements AstGeneratorInterface +{ + public function generate($object, array $context = []) + { + if (!isset($context['input'])) { + throw new \Exception('no input'); + } + + if (!isset($context['output'])) { + throw new \Exception('no output'); + } + + return [new Expr\Assign($context['output'], $context['input'])]; + } + + public function supportsGeneration($object) + { + return true; + } +} diff --git a/src/Symfony/Component/AstGenerator/Tests/Hydrate/ObjectHydrateFromStdClassGeneratorTest.php b/src/Symfony/Component/AstGenerator/Tests/Hydrate/ObjectHydrateFromStdClassGeneratorTest.php new file mode 100644 index 0000000000000..a4c669d221f0b --- /dev/null +++ b/src/Symfony/Component/AstGenerator/Tests/Hydrate/ObjectHydrateFromStdClassGeneratorTest.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator\Tests\Hydrate; + +use PhpParser\Node\Expr; +use PhpParser\PrettyPrinter\Standard; +use Prophecy\Argument; +use Symfony\Component\AstGenerator\AstGeneratorInterface; +use Symfony\Component\AstGenerator\Hydrate\ObjectHydrateFromStdClassGenerator; +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; +use Symfony\Component\PropertyInfo\Type; + +class ObjectHydrateFromStdClassGeneratorTest extends AbstractHydratorTest +{ + public function testHydrateGenerator() + { + $propertyInfoExtractor = $this->getPropertyInfoExtractor(DummyObjectStdClass::class); + $hydrateGenerator = new ObjectHydrateFromStdClassGenerator($propertyInfoExtractor, new DummyObjectStdClassTypeGenerator()); + + $this->assertTrue($hydrateGenerator->supportsGeneration(DummyObjectStdClass::class)); + + $stdClass = new \stdClass(); + $stdClass->bar = "test"; + + eval($this->printer->prettyPrint($hydrateGenerator->generate(DummyObjectStdClass::class, [ + 'input' => new Expr\Variable('stdClass'), + 'output' => new Expr\Variable('object'), + ]))); + + $this->assertInstanceOf(DummyObjectStdClass::class, $object); + $this->assertEquals('test', $object->bar); + } + + /** + * @expectedException \Symfony\Component\AstGenerator\Exception\MissingContextException + */ + public function testNoInput() + { + $propertyInfoExtractor = $this->getMockBuilder(PropertyInfoExtractorInterface::class)->getMock(); + $hydrateGenerator = new ObjectHydrateFromStdClassGenerator($propertyInfoExtractor, new DummyObjectStdClassTypeGenerator()); + $hydrateGenerator->generate(DummyObjectStdClass::class); + } + + /** + * @expectedException \Symfony\Component\AstGenerator\Exception\MissingContextException + */ + public function testNoOutput() + { + $propertyInfoExtractor = $this->getMockBuilder(PropertyInfoExtractorInterface::class)->getMock(); + $hydrateGenerator = new ObjectHydrateFromStdClassGenerator($propertyInfoExtractor, new DummyObjectStdClassTypeGenerator()); + $hydrateGenerator->generate(DummyObjectStdClass::class, ['input' => new Expr\Variable('test')]); + } +} + +class DummyObjectStdClass +{ + public $foo; + + public $bar; + + /** + * @return mixed + */ + public function getFoo() + { + return $this->foo; + } + + /** + * @param mixed $bar + */ + public function setBar($bar) + { + $this->bar = $bar; + } +} + +class DummyObjectStdClassTypeGenerator implements AstGeneratorInterface +{ + public function generate($object, array $context = []) + { + if (!isset($context['input'])) { + throw new \Exception('no input'); + } + + if (!isset($context['output'])) { + throw new \Exception('no output'); + } + + return [new Expr\Assign($context['output'], $context['input'])]; + } + + public function supportsGeneration($object) + { + return true; + } +} diff --git a/src/Symfony/Component/AstGenerator/Tests/Hydrate/StdClassHydrateGeneratorTest.php b/src/Symfony/Component/AstGenerator/Tests/Hydrate/StdClassHydrateGeneratorTest.php new file mode 100644 index 0000000000000..62c82cc1a2d23 --- /dev/null +++ b/src/Symfony/Component/AstGenerator/Tests/Hydrate/StdClassHydrateGeneratorTest.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator\Tests\Hydrate; + +use PhpParser\Node\Expr; +use PhpParser\PrettyPrinter\Standard; +use Prophecy\Argument; +use Symfony\Component\AstGenerator\AstGeneratorInterface; +use Symfony\Component\AstGenerator\Hydrate\StdClassHydrateGenerator; +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; +use Symfony\Component\PropertyInfo\Type; + +class StdClassHydrateGeneratorTest extends AbstractHydratorTest +{ + public function testHydrateGenerator() + { + $propertyInfoExtractor = $this->getPropertyInfoExtractor(Foo::class); + $hydrateGenerator = new StdClassHydrateGenerator($propertyInfoExtractor, new FooTypeGenerator()); + + $this->assertTrue($hydrateGenerator->supportsGeneration(Foo::class)); + + $fooObject = new Foo(); + $fooObject->foo = 'test'; + + eval($this->printer->prettyPrint($hydrateGenerator->generate(Foo::class, [ + 'input' => new Expr\Variable('fooObject'), + 'output' => new Expr\Variable('dummyStdClass'), + ]))); + + $this->assertInstanceOf('stdClass', $dummyStdClass); + $this->assertObjectHasAttribute('foo', $dummyStdClass); + $this->assertEquals('test', $dummyStdClass->foo); + } + + /** + * @expectedException \Symfony\Component\AstGenerator\Exception\MissingContextException + */ + public function testNoInput() + { + $propertyInfoExtractor = $this->getMockBuilder(PropertyInfoExtractorInterface::class)->getMock(); + $hydrateGenerator = new StdClassHydrateGenerator($propertyInfoExtractor, new FooTypeGenerator()); + $hydrateGenerator->generate(Foo::class); + } + + /** + * @expectedException \Symfony\Component\AstGenerator\Exception\MissingContextException + */ + public function testNoOutput() + { + $propertyInfoExtractor = $this->getMockBuilder(PropertyInfoExtractorInterface::class)->getMock(); + $hydrateGenerator = new StdClassHydrateGenerator($propertyInfoExtractor, new FooTypeGenerator()); + $hydrateGenerator->generate(Foo::class, ['input' => new Expr\Variable('test')]); + } +} + +class Foo +{ + public $foo; + + public $bar; + + /** + * @return mixed + */ + public function getFoo() + { + return $this->foo; + } + + /** + * @param mixed $bar + */ + public function setBar($bar) + { + $this->bar = $bar; + } +} + +class FooTypeGenerator implements AstGeneratorInterface +{ + public function generate($object, array $context = []) + { + if (!isset($context['input'])) { + throw new \Exception('no input'); + } + + if (!isset($context['output'])) { + throw new \Exception('no output'); + } + + return [new Expr\Assign($context['output'], $context['input'])]; + } + + public function supportsGeneration($object) + { + return true; + } +} diff --git a/src/Symfony/Component/AstGenerator/Tests/Hydrate/Type/CollectionTypeGeneratorTest.php b/src/Symfony/Component/AstGenerator/Tests/Hydrate/Type/CollectionTypeGeneratorTest.php new file mode 100644 index 0000000000000..d261ebff3f65e --- /dev/null +++ b/src/Symfony/Component/AstGenerator/Tests/Hydrate/Type/CollectionTypeGeneratorTest.php @@ -0,0 +1,159 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator\Tests\Hydrate\Type; + +use PhpParser\Node\Expr; +use PhpParser\PrettyPrinter\Standard; +use Symfony\Component\AstGenerator\AstGeneratorInterface; +use Symfony\Component\AstGenerator\Hydrate\Type\CollectionTypeGenerator; +use Symfony\Component\PropertyInfo\Type; + +class CollectionTypeGeneratorTest extends \PHPUnit_Framework_TestCase +{ + /** @var Standard */ + protected $printer; + + public function setUp() + { + $this->printer = new Standard(); + } + + /** + * @expectedException \Symfony\Component\AstGenerator\Exception\MissingContextException + */ + public function testNoInput() + { + $itemGenerator = $this->getMockBuilder(AstGeneratorInterface::class)->getMock(); + $hydrateGenerator = new CollectionTypeGenerator($itemGenerator); + $hydrateGenerator->generate(new Type('array', false, null, true)); + } + + /** + * @expectedException \Symfony\Component\AstGenerator\Exception\MissingContextException + */ + public function testNoOutput() + { + $itemGenerator = $this->getMockBuilder(AstGeneratorInterface::class)->getMock(); + $hydrateGenerator = new CollectionTypeGenerator($itemGenerator); + $hydrateGenerator->generate(new Type('array', false, null, true), ['input' => new Expr\Variable('test')]); + } + + public function testDefaultWithNumericalArray() + { + $collectionKeyType = new Type('int'); + $collectionValueType = new Type('string'); + $type = new Type('array', false, null, true, $collectionKeyType, $collectionValueType); + + $itemGenerator = $this->getItemGeneratorMock($collectionValueType); + + $generator = new CollectionTypeGenerator($itemGenerator); + + $this->assertTrue($generator->supportsGeneration($type)); + + $input = [ + 'foo', + 'bar', + ]; + + eval($this->printer->prettyPrint($generator->generate($type, [ + 'input' => new Expr\Variable('input'), + 'output' => new Expr\Variable('output'), + ]))); + + $this->assertInternalType('array', $output); + $this->assertCount(2, $output); + $this->assertEquals('foo', $output[0]); + $this->assertEquals('bar', $output[1]); + } + + public function testDefaultWithMapArray() + { + $collectionKeyType = new Type('string'); + $collectionValueType = new Type('string'); + $type = new Type('array', false, null, true, $collectionKeyType, $collectionValueType); + + $itemGenerator = $this->getItemGeneratorMock($collectionValueType); + + $generator = new CollectionTypeGenerator($itemGenerator); + + $this->assertTrue($generator->supportsGeneration($type)); + + $input = [ + 'foo' => 'foo', + 'bar' => 'bar', + ]; + + eval($this->printer->prettyPrint($generator->generate($type, [ + 'input' => new Expr\Variable('input'), + 'output' => new Expr\Variable('output'), + ]))); + + $this->assertInstanceOf('\stdClass', $output); + $this->assertObjectHasAttribute('foo', $output); + $this->assertObjectHasAttribute('bar', $output); + $this->assertEquals('foo', $output->foo); + $this->assertEquals('bar', $output->bar); + } + + public function testCustomObject() + { + $collectionKeyType = new Type('string'); + $collectionValueType = new Type('string'); + $type = new Type('array', false, null, true, $collectionKeyType, $collectionValueType); + + $itemGenerator = $this->getItemGeneratorMock($collectionValueType); + + $generator = new CollectionTypeGenerator( + $itemGenerator, + CollectionTypeGenerator::COLLECTION_WITH_OBJECT, + '\ArrayObject', + CollectionTypeGenerator::OBJECT_ASSIGNMENT_ARRAY + ); + + $this->assertTrue($generator->supportsGeneration($type)); + + $input = [ + 'foo' => 'foo', + 'bar' => 'bar', + ]; + + eval($this->printer->prettyPrint($generator->generate($type, [ + 'input' => new Expr\Variable('input'), + 'output' => new Expr\Variable('output'), + ]))); + + $this->assertInstanceOf('\ArrayObject', $output); + $this->assertArrayHasKey('foo', $output); + $this->assertArrayHasKey('bar', $output); + $this->assertEquals('foo', $output['foo']); + $this->assertEquals('bar', $output['bar']); + } + + private function getItemGeneratorMock(Type $collectionValueType) + { + $itemGenerator = $this->getMockBuilder(AstGeneratorInterface::class)->getMock(); + $itemGenerator + ->expects($this->any()) + ->method('supportsGeneration') + ->with($collectionValueType) + ->willReturn(true); + $itemGenerator + ->expects($this->any()) + ->method('generate') + ->with($collectionValueType, $this->isType('array')) + ->will($this->returnCallback(function ($object, array $context) { + return [new Expr\Assign($context['output'], $context['input'])]; + })); + + return $itemGenerator; + } +} diff --git a/src/Symfony/Component/AstGenerator/Tests/Hydrate/Type/TypeGeneratorTest.php b/src/Symfony/Component/AstGenerator/Tests/Hydrate/Type/TypeGeneratorTest.php new file mode 100644 index 0000000000000..a1eff61df4c52 --- /dev/null +++ b/src/Symfony/Component/AstGenerator/Tests/Hydrate/Type/TypeGeneratorTest.php @@ -0,0 +1,268 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator\Tests\Hydrate\Type; + +use PhpParser\Node\Expr; +use PhpParser\PrettyPrinter\Standard; +use Symfony\Component\AstGenerator\Hydrate\Type\TypeGenerator; +use Symfony\Component\PropertyInfo\Type; + +class TypeGeneratorTest extends \PHPUnit_Framework_TestCase +{ + /** @var Standard */ + protected $printer; + + /** @var TypeGenerator */ + private $typeGenerator; + + public function setUp() + { + $this->typeGenerator = new TypeGenerator(); + $this->printer = new Standard(); + } + + /** + * @expectedException \Symfony\Component\AstGenerator\Exception\MissingContextException + */ + public function testNoInput() + { + $this->typeGenerator->generate(new Type('string')); + } + + /** + * @expectedException \Symfony\Component\AstGenerator\Exception\MissingContextException + */ + public function testNoOutput() + { + $this->typeGenerator->generate(new Type('string'), ['input' => new Expr\Variable('inputData')]); + } + + public function testString() + { + $type = new Type('string'); + $input = new Expr\Variable('inputData'); + $output = new Expr\Variable('outputData'); + + $inputData = "test"; + $outputData = null; + + $this->assertTrue($this->typeGenerator->supportsGeneration($type)); + + eval($this->printer->prettyPrint($this->typeGenerator->generate($type, ['input' => $input, 'output' => $output]))); + + $this->assertSame($inputData, $outputData); + } + + public function testInteger() + { + $type = new Type('int'); + $input = new Expr\Variable('inputData'); + $output = new Expr\Variable('outputData'); + + $inputData = 40; + $outputData = null; + + $this->assertTrue($this->typeGenerator->supportsGeneration($type)); + + eval($this->printer->prettyPrint($this->typeGenerator->generate($type, ['input' => $input, 'output' => $output]))); + + $this->assertSame($inputData, $outputData); + } + + public function testNull() + { + $type = new Type('null'); + $input = new Expr\Variable('inputData'); + $output = new Expr\Variable('outputData'); + + $inputData = null; + $outputData = "test"; + + $this->assertTrue($this->typeGenerator->supportsGeneration($type)); + + eval($this->printer->prettyPrint($this->typeGenerator->generate($type, ['input' => $input, 'output' => $output]))); + + $this->assertSame($inputData, $outputData); + } + + public function testFloat() + { + $type = new Type('float'); + $input = new Expr\Variable('inputData'); + $output = new Expr\Variable('outputData'); + + $inputData = 2.5; + $outputData = null; + + $this->assertTrue($this->typeGenerator->supportsGeneration($type)); + + eval($this->printer->prettyPrint($this->typeGenerator->generate($type, ['input' => $input, 'output' => $output]))); + + $this->assertSame($inputData, $outputData); + } + + public function testBool() + { + $type = new Type('bool'); + $input = new Expr\Variable('inputData'); + $output = new Expr\Variable('outputData'); + + $inputData = true; + $outputData = null; + + $this->assertTrue($this->typeGenerator->supportsGeneration($type)); + + eval($this->printer->prettyPrint($this->typeGenerator->generate($type, ['input' => $input, 'output' => $output]))); + + $this->assertSame($inputData, $outputData); + } + + public function testStringConditionOk() + { + $type = new Type('string'); + $input = new Expr\Variable('inputData'); + $output = new Expr\Variable('outputData'); + + $inputData = "test"; + $outputData = null; + + eval($this->printer->prettyPrint($this->typeGenerator->generate($type, ['condition' => true, 'input' => $input, 'output' => $output]))); + + $this->assertSame($inputData, $outputData); + } + + public function testIntegerConditionOk() + { + $type = new Type('int'); + $input = new Expr\Variable('inputData'); + $output = new Expr\Variable('outputData'); + + $inputData = 40; + $outputData = null; + + eval($this->printer->prettyPrint($this->typeGenerator->generate($type, ['condition' => true,'input' => $input, 'output' => $output]))); + + $this->assertSame($inputData, $outputData); + } + + public function testNullConditionOk() + { + $type = new Type('null'); + $input = new Expr\Variable('inputData'); + $output = new Expr\Variable('outputData'); + + $inputData = null; + $outputData = "test"; + + eval($this->printer->prettyPrint($this->typeGenerator->generate($type, ['condition' => true,'input' => $input, 'output' => $output]))); + + $this->assertSame($inputData, $outputData); + } + + public function testFloatConditionOk() + { + $type = new Type('float'); + $input = new Expr\Variable('inputData'); + $output = new Expr\Variable('outputData'); + + $inputData = 2.5; + $outputData = null; + + eval($this->printer->prettyPrint($this->typeGenerator->generate($type, ['condition' => true,'input' => $input, 'output' => $output]))); + + $this->assertSame($inputData, $outputData); + } + + public function testBoolConditionOk() + { + $type = new Type('bool'); + $input = new Expr\Variable('inputData'); + $output = new Expr\Variable('outputData'); + + $inputData = true; + $outputData = null; + + eval($this->printer->prettyPrint($this->typeGenerator->generate($type, ['condition' => true, 'input' => $input, 'output' => $output]))); + + $this->assertSame($inputData, $outputData); + } + + public function testStringConditionNOK() + { + $type = new Type('string'); + $input = new Expr\Variable('inputData'); + $output = new Expr\Variable('outputData'); + + $inputData = 1; + $outputData = null; + + eval($this->printer->prettyPrint($this->typeGenerator->generate($type, ['condition' => true, 'input' => $input, 'output' => $output]))); + + $this->assertNull($outputData); + } + + public function testIntegerConditionNOK() + { + $type = new Type('int'); + $input = new Expr\Variable('inputData'); + $output = new Expr\Variable('outputData'); + + $inputData = "test"; + $outputData = null; + + eval($this->printer->prettyPrint($this->typeGenerator->generate($type, ['condition' => true,'input' => $input, 'output' => $output]))); + + $this->assertNull($outputData); + } + + public function testNullConditionNOK() + { + $type = new Type('null'); + $input = new Expr\Variable('inputData'); + $output = new Expr\Variable('outputData'); + + $inputData = "test"; + $outputData = "test"; + + eval($this->printer->prettyPrint($this->typeGenerator->generate($type, ['condition' => true,'input' => $input, 'output' => $output]))); + + $this->assertNotNull($outputData); + } + + public function testFloatConditionNOK() + { + $type = new Type('float'); + $input = new Expr\Variable('inputData'); + $output = new Expr\Variable('outputData'); + + $inputData = "test"; + $outputData = null; + + eval($this->printer->prettyPrint($this->typeGenerator->generate($type, ['condition' => true,'input' => $input, 'output' => $output]))); + + $this->assertNull($outputData); + } + + public function testBoolConditionNOK() + { + $type = new Type('bool'); + $input = new Expr\Variable('inputData'); + $output = new Expr\Variable('outputData'); + + $inputData = "test"; + $outputData = null; + + eval($this->printer->prettyPrint($this->typeGenerator->generate($type, ['condition' => true, 'input' => $input, 'output' => $output]))); + + $this->assertNull($outputData); + } +} diff --git a/src/Symfony/Component/AstGenerator/Tests/Normalizer/NormalizerGeneratorTest.php b/src/Symfony/Component/AstGenerator/Tests/Normalizer/NormalizerGeneratorTest.php new file mode 100644 index 0000000000000..60c33e1434885 --- /dev/null +++ b/src/Symfony/Component/AstGenerator/Tests/Normalizer/NormalizerGeneratorTest.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator\Tests\Normalizer; + +use PhpParser\PrettyPrinter\Standard; +use PhpParser\Node\Name; +use PhpParser\Node\Stmt; +use PhpParser\Node\Expr; +use Prophecy\Argument; +use Symfony\Component\AstGenerator\AstGeneratorInterface; +use Symfony\Component\AstGenerator\Normalizer\NormalizerGenerator; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\SerializerAwareInterface; + +class NormalizerGeneratorTest extends \PHPUnit_Framework_TestCase +{ + /** @var Standard */ + protected $printer; + + public function setUp() + { + $this->printer = new Standard(); + } + + public function testGenerateDummyNormalizer() + { + $normalizerStatementsGenerator = $this->getMockBuilder(AstGeneratorInterface::class)->getMock(); + $normalizerStatementsGenerator + ->expects($this->any()) + ->method('supportsGeneration') + ->with(Dummy::class) + ->willReturn(true); + $normalizerStatementsGenerator + ->expects($this->any()) + ->method('generate') + ->with(Dummy::class, $this->isType('array')) + ->willReturn([ + new Stmt\Return_(new Expr\New_(new Name('\\stdClass'))), + ]); + + $denormalizerStatementsGenerator = $this->getMockBuilder(AstGeneratorInterface::class)->getMock(); + $denormalizerStatementsGenerator + ->expects($this->any()) + ->method('supportsGeneration') + ->with(Dummy::class) + ->willReturn(true); + $denormalizerStatementsGenerator + ->expects($this->any()) + ->method('generate') + ->with(Dummy::class, $this->isType('array')) + ->willReturn([ + new Stmt\Return_(new Expr\New_(new Name('\\'.Dummy::class))), + ]); + + $normalizerGenerator = new NormalizerGenerator($normalizerStatementsGenerator, $denormalizerStatementsGenerator); + + $this->assertTrue($normalizerGenerator->supportsGeneration(Dummy::class)); + + eval($this->printer->prettyPrint($normalizerGenerator->generate(Dummy::class))); + + $this->assertTrue(class_exists('DummyNormalizer')); + + $dummyNormalizer = new \DummyNormalizer(); + + $this->assertInstanceOf(NormalizerInterface::class, $dummyNormalizer); + $this->assertInstanceOf(DenormalizerInterface::class, $dummyNormalizer); + $this->assertInstanceOf(DenormalizerAwareInterface::class, $dummyNormalizer); + $this->assertInstanceOf(NormalizerAwareInterface::class, $dummyNormalizer); + + $this->assertTrue($dummyNormalizer->supportsNormalization(new Dummy())); + $this->assertTrue($dummyNormalizer->supportsDenormalization([], Dummy::class)); + + $normalized = $dummyNormalizer->normalize(new Dummy()); + $denormalized = $dummyNormalizer->denormalize(new \stdClass(), Dummy::class); + + $this->assertInstanceOf(\stdClass::class, $normalized); + $this->assertInstanceOf(Dummy::class, $denormalized); + } +} + +class Dummy +{ +} diff --git a/src/Symfony/Component/AstGenerator/Tests/UniqueVariableScopeTest.php b/src/Symfony/Component/AstGenerator/Tests/UniqueVariableScopeTest.php new file mode 100644 index 0000000000000..f46c5e4e5070d --- /dev/null +++ b/src/Symfony/Component/AstGenerator/Tests/UniqueVariableScopeTest.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator\Tests; + +use Symfony\Component\AstGenerator\UniqueVariableScope; + +class UniqueVariableScopeTest extends \PHPUnit_Framework_TestCase +{ + public function testUniqueVariable() + { + $uniqueVariableScope = new UniqueVariableScope(); + + $name = $uniqueVariableScope->getUniqueName('name'); + $this->assertEquals('name', $name); + + $name = $uniqueVariableScope->getUniqueName('name'); + $this->assertEquals('name_1', $name); + + $name = $uniqueVariableScope->getUniqueName('name'); + $this->assertEquals('name_2', $name); + } +} diff --git a/src/Symfony/Component/AstGenerator/UniqueVariableScope.php b/src/Symfony/Component/AstGenerator/UniqueVariableScope.php new file mode 100644 index 0000000000000..eed95c9a0c64a --- /dev/null +++ b/src/Symfony/Component/AstGenerator/UniqueVariableScope.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AstGenerator; + +/** + * Allow to get a unique variable name for a scope (like a method). + */ +class UniqueVariableScope +{ + private $registry = array(); + + /** + * Return an unique name for a variable. + * + * @param string $name Name of the variable + * + * @return string if not found return the $name given, if not return the name suffixed with a number + */ + public function getUniqueName($name) + { + if (!isset($this->registry[$name])) { + $this->registry[$name] = 0; + + return $name; + } + + ++$this->registry[$name]; + + return sprintf('%s_%s', $name, $this->registry[$name]); + } +} diff --git a/src/Symfony/Component/AstGenerator/composer.json b/src/Symfony/Component/AstGenerator/composer.json new file mode 100644 index 0000000000000..a0889cd91d11e --- /dev/null +++ b/src/Symfony/Component/AstGenerator/composer.json @@ -0,0 +1,38 @@ +{ + "name": "symfony/ast-generator", + "type": "library", + "description": "Symfony component to generate AST from and to various others components", + "keywords": ["symfony", "ast", "generator"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.5.9", + "nikic/php-parser": "~2.0", + "symfony/property-info": "~2.8|~3.0" + }, + "require-dev": { + "symfony/serializer": "~2.8|~3.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\AstGenerator\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + } +} diff --git a/src/Symfony/Component/AstGenerator/phpunit.xml.dist b/src/Symfony/Component/AstGenerator/phpunit.xml.dist new file mode 100644 index 0000000000000..5484c4319c9e4 --- /dev/null +++ b/src/Symfony/Component/AstGenerator/phpunit.xml.dist @@ -0,0 +1,28 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Tests + ./vendor + + + +