Skip to content

Commit de38c7b

Browse files
committed
Add collection type generator
1 parent 365bdb2 commit de38c7b

File tree

3 files changed

+269
-14
lines changed

3 files changed

+269
-14
lines changed

src/Symfony/Component/AstGenerator/Hydrate/Type/CollectionTypeGenerator.php

Lines changed: 106 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
1212
namespace Symfony\Component\AstGenerator\Hydrate\Type;
1313

1414
use PhpParser\Node\Expr;
15+
use PhpParser\Node\Name;
16+
use PhpParser\Node\Stmt;
1517
use Symfony\Component\AstGenerator\AstGeneratorInterface;
1618
use Symfony\Component\AstGenerator\Exception\MissingContextException;
19+
use Symfony\Component\AstGenerator\UniqueVariableScope;
1720
use Symfony\Component\PropertyInfo\Type;
1821

1922
/**
@@ -23,21 +26,44 @@
2326
*/
2427
class CollectionTypeGenerator implements AstGeneratorInterface
2528
{
26-
const COLLECTION_WITH_STDCLASS = 0;
29+
const COLLECTION_WITH_OBJECT = 0;
2730
const COLLECTION_WITH_ARRAY = 1;
2831

32+
const OBJECT_ASSIGNMENT_ARRAY = 0;
33+
const OBJECT_ASSIGNMENT_PROPERTY = 1;
34+
35+
/** @var AstGeneratorInterface Generator for the value of the collection */
36+
private $subValueTypeGenerator;
37+
38+
/** @var int|null Output collection type to generate */
39+
private $outputCollectionType;
40+
41+
/** @var string Class of object to use for the collection of the output */
42+
private $outputObjectClass;
43+
44+
/** @var int Assignment type for the output */
45+
private $outputObjectAssignment;
46+
2947
/**
3048
* CollectionTypeGenerator constructor.
3149
*
3250
* @param AstGeneratorInterface $subValueTypeGenerator Generator for the value of the collection
33-
* @param int|null $fromCollectionType From collection type to generate, array or stdClass, use null
34-
* to have a dynamic choice depending on the type of the collection key (where int will be array and string stdClass)
35-
* @param int|null $toCollectionType To collection type to generate, array or stdClass, use null
51+
* @param int|null $outputCollectionType Output collection type to generate, array or stdClass, use null
3652
* to have a dynamic choice depending on the type of the collection key (where int will be array and string stdClass)
53+
* @param string $outputObjectClass Class of object to use for the collection of the output
54+
* @param int $outputObjectAssignment Assignment type for the output
3755
*/
38-
public function __construct(AstGeneratorInterface $subValueTypeGenerator, $fromCollectionType = null, $toCollectionType = null)
56+
public function __construct(
57+
AstGeneratorInterface $subValueTypeGenerator,
58+
$outputCollectionType = null,
59+
$outputObjectClass = '\\stdClass',
60+
$outputObjectAssignment = self::OBJECT_ASSIGNMENT_PROPERTY
61+
)
3962
{
40-
63+
$this->subValueTypeGenerator = $subValueTypeGenerator;
64+
$this->outputCollectionType = $outputCollectionType;
65+
$this->outputObjectClass = $outputObjectClass;
66+
$this->outputObjectAssignment = $outputObjectAssignment;
4167
}
4268

4369
/**
@@ -55,12 +81,34 @@ public function generate($object, array $context = [])
5581
throw new MissingContextException('Output variable not defined or not an Expr in generation context');
5682
}
5783

84+
$uniqueVariableScope = isset($context['unique_variable_scope']) ? $context['unique_variable_scope'] : new UniqueVariableScope();
5885
$statements = [
59-
new Expr\Assign($context['output'], $this->createCollectionAssignStatement()),
86+
new Expr\Assign($context['output'], $this->createCollectionAssignStatement($object)),
6087
];
6188

62-
$loopValueVar = new Expr\Variable($context->getUniqueVariableName('value'));
63-
$loopKeyVar = $this->createLoopKeyStatement($context);
89+
// Create item input
90+
$loopValueVar = new Expr\Variable($uniqueVariableScope->getUniqueName('value'));
91+
92+
// Create item output
93+
$loopKeyVar = new Expr\Variable($uniqueVariableScope->getUniqueName('key'));
94+
$output = $this->createCollectionItemExpr($object, $loopKeyVar, $context['output']);
95+
96+
// Loop statements
97+
$loopStatements = [new Expr\Assign($output, $loopValueVar)];
98+
99+
if (null !== $object->getCollectionValueType() && $this->subValueTypeGenerator->supportsGeneration($object->getCollectionValueType())) {
100+
$loopStatements = $this->subValueTypeGenerator->generate($object->getCollectionValueType(), array_merge($context, [
101+
'input' => $loopValueVar,
102+
'output' => $output
103+
]));
104+
}
105+
106+
$statements[] = new Stmt\Foreach_($context['input'], $loopValueVar, [
107+
'keyVar' => $loopKeyVar,
108+
'stmts' => $loopStatements
109+
]);
110+
111+
return $statements;
64112
}
65113

66114
/**
@@ -76,8 +124,56 @@ public function supportsGeneration($object)
76124
*
77125
* @return Expr
78126
*/
79-
protected function createCollectionAssignStatement()
127+
protected function createCollectionAssignStatement(Type $type)
128+
{
129+
$outputCollectionType = $this->getOutputCollectionType($type);
130+
131+
if ($outputCollectionType === self::COLLECTION_WITH_ARRAY) {
132+
return new Expr\Array_();
133+
}
134+
135+
return new Expr\New_(new Name($this->outputObjectClass));
136+
}
137+
138+
/**
139+
* Create the expression for the output assignment of an item in the array
140+
*
141+
* @param Type $type Type of property
142+
* @param Expr\Variable $loopKeyVar Variable for the key in the loop
143+
* @param Expr $output Output to use for the collection
144+
*
145+
* @return Expr\ArrayDimFetch|Expr\PropertyFetch
146+
*/
147+
protected function createCollectionItemExpr(Type $type, Expr\Variable $loopKeyVar, Expr $output)
148+
{
149+
$outputCollectionType = $this->getOutputCollectionType($type);
150+
151+
if ($outputCollectionType === self::COLLECTION_WITH_ARRAY || $this->outputObjectAssignment == self::OBJECT_ASSIGNMENT_ARRAY) {
152+
return new Expr\ArrayDimFetch($output, $loopKeyVar);
153+
}
154+
155+
return new Expr\PropertyFetch($output, $loopKeyVar);
156+
}
157+
158+
/**
159+
* Get output collection type, set in constructor or guessed from type of the collection key
160+
*
161+
* @param Type $type
162+
*
163+
* @return int|null
164+
*/
165+
private function getOutputCollectionType(Type $type)
80166
{
167+
$outputCollectionType = $this->outputCollectionType;
168+
169+
if ($outputCollectionType === null) {
170+
$outputCollectionType = self::COLLECTION_WITH_ARRAY;
171+
172+
if ($type->getCollectionKeyType() !== null && $type->getCollectionKeyType()->getBuiltinType() !== Type::BUILTIN_TYPE_INT) {
173+
$outputCollectionType = self::COLLECTION_WITH_OBJECT;
174+
}
175+
}
81176

177+
return $outputCollectionType;
82178
}
83179
}

src/Symfony/Component/AstGenerator/Normalizer/NormalizerGenerator.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,16 +149,22 @@ protected function createSupportsDenormalizationMethod($class)
149149
*/
150150
protected function createNormalizeMethod($class, array $context = [])
151151
{
152+
$input = new Expr\Variable('object');
153+
$output = new Expr\Variable('data');
154+
152155
return new Stmt\ClassMethod('normalize', [
153156
'type' => Stmt\Class_::MODIFIER_PUBLIC,
154157
'params' => [
155158
new Param('object'),
156159
new Param('format', new Expr\ConstFetch(new Name('null'))),
157160
new Param('context', new Expr\Array_(), 'array'),
158161
],
159-
'stmts' => $this->normalizeStatementsGenerator->generate($class, array_merge($context, [
160-
'input' => new Expr\Variable('object'),
161-
])),
162+
'stmts' => array_merge($this->normalizeStatementsGenerator->generate($class, array_merge($context, [
163+
'input' => $input,
164+
'output' => $output
165+
])), [
166+
new Stmt\Return_($output)
167+
])
162168
]);
163169
}
164170

@@ -173,7 +179,7 @@ protected function createNormalizeMethod($class, array $context = [])
173179
protected function createDenormalizeMethod($class, array $context = [])
174180
{
175181
$input = new Expr\Variable('data');
176-
$output = new Expr\Variable('output');
182+
$output = new Expr\Variable('object');
177183

178184
return new Stmt\ClassMethod('denormalize', [
179185
'type' => Stmt\Class_::MODIFIER_PUBLIC,
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\AstGenerator\Tests\Hydrate\Type;
13+
14+
use PhpParser\Node\Expr;
15+
use PhpParser\PrettyPrinter\Standard;
16+
use Prophecy\Argument;
17+
use Symfony\Component\AstGenerator\AstGeneratorInterface;
18+
use Symfony\Component\AstGenerator\Hydrate\Type\CollectionTypeGenerator;
19+
use Symfony\Component\PropertyInfo\Type;
20+
21+
class CollectionTypeGeneratorTest extends \PHPUnit_Framework_TestCase
22+
{
23+
/** @var Standard */
24+
protected $printer;
25+
26+
public function setUp()
27+
{
28+
$this->printer = new Standard();
29+
}
30+
31+
/**
32+
* @expectedException \Symfony\Component\AstGenerator\Exception\MissingContextException
33+
*/
34+
public function testNoInput()
35+
{
36+
$itemGenerator = $this->prophesize(AstGeneratorInterface::class);
37+
$hydrateGenerator = new CollectionTypeGenerator($itemGenerator->reveal());
38+
$hydrateGenerator->generate(new Type('array', false, null, true));
39+
}
40+
41+
/**
42+
* @expectedException \Symfony\Component\AstGenerator\Exception\MissingContextException
43+
*/
44+
public function testNoOutput()
45+
{
46+
$itemGenerator = $this->prophesize(AstGeneratorInterface::class);
47+
$hydrateGenerator = new CollectionTypeGenerator($itemGenerator->reveal());
48+
$hydrateGenerator->generate(new Type('array', false, null, true), ['input' => new Expr\Variable('test')]);
49+
}
50+
51+
public function testDefaultWithNumericalArray()
52+
{
53+
$collectionKeyType = new Type('int');
54+
$collectionValueType = new Type('string');
55+
$type = new Type('array', false, null, true, $collectionKeyType, $collectionValueType);
56+
57+
$itemGenerator = $this->prophesize(AstGeneratorInterface::class);
58+
$itemGenerator->supportsGeneration($collectionValueType)->willReturn(true);
59+
$itemGenerator->generate($collectionValueType, Argument::type('array'))->will(function ($args) {
60+
return [new Expr\Assign($args[1]['output'], $args[1]['input'])];
61+
});
62+
63+
$generator = new CollectionTypeGenerator($itemGenerator->reveal());
64+
65+
$this->assertTrue($generator->supportsGeneration($type));
66+
67+
$input = [
68+
'foo',
69+
'bar',
70+
];
71+
72+
eval($this->printer->prettyPrint($generator->generate($type, [
73+
'input' => new Expr\Variable('input'),
74+
'output' => new Expr\Variable('output'),
75+
])));
76+
77+
$this->assertInternalType('array', $output);
78+
$this->assertCount(2, $output);
79+
$this->assertEquals('foo', $output[0]);
80+
$this->assertEquals('bar', $output[1]);
81+
}
82+
83+
public function testDefaultWithMapArray()
84+
{
85+
$collectionKeyType = new Type('string');
86+
$collectionValueType = new Type('string');
87+
$type = new Type('array', false, null, true, $collectionKeyType, $collectionValueType);
88+
89+
$itemGenerator = $this->prophesize(AstGeneratorInterface::class);
90+
$itemGenerator->supportsGeneration($collectionValueType)->willReturn(true);
91+
$itemGenerator->generate($collectionValueType, Argument::type('array'))->will(function ($args) {
92+
return [new Expr\Assign($args[1]['output'], $args[1]['input'])];
93+
});
94+
95+
$generator = new CollectionTypeGenerator($itemGenerator->reveal());
96+
97+
$this->assertTrue($generator->supportsGeneration($type));
98+
99+
$input = [
100+
'foo' => 'foo',
101+
'bar' => 'bar',
102+
];
103+
104+
eval($this->printer->prettyPrint($generator->generate($type, [
105+
'input' => new Expr\Variable('input'),
106+
'output' => new Expr\Variable('output'),
107+
])));
108+
109+
$this->assertInstanceOf('\stdClass', $output);
110+
$this->assertObjectHasAttribute('foo', $output);
111+
$this->assertObjectHasAttribute('bar', $output);
112+
$this->assertEquals('foo', $output->foo);
113+
$this->assertEquals('bar', $output->bar);
114+
}
115+
116+
public function testCustomObject()
117+
{
118+
$collectionKeyType = new Type('string');
119+
$collectionValueType = new Type('string');
120+
$type = new Type('array', false, null, true, $collectionKeyType, $collectionValueType);
121+
122+
$itemGenerator = $this->prophesize(AstGeneratorInterface::class);
123+
$itemGenerator->supportsGeneration($collectionValueType)->willReturn(true);
124+
$itemGenerator->generate($collectionValueType, Argument::type('array'))->will(function ($args) {
125+
return [new Expr\Assign($args[1]['output'], $args[1]['input'])];
126+
});
127+
128+
$generator = new CollectionTypeGenerator(
129+
$itemGenerator->reveal(),
130+
CollectionTypeGenerator::COLLECTION_WITH_OBJECT,
131+
'\ArrayObject',
132+
CollectionTypeGenerator::OBJECT_ASSIGNMENT_ARRAY
133+
);
134+
135+
$this->assertTrue($generator->supportsGeneration($type));
136+
137+
$input = [
138+
'foo' => 'foo',
139+
'bar' => 'bar',
140+
];
141+
142+
eval($this->printer->prettyPrint($generator->generate($type, [
143+
'input' => new Expr\Variable('input'),
144+
'output' => new Expr\Variable('output'),
145+
])));
146+
147+
$this->assertInstanceOf('\ArrayObject', $output);
148+
$this->assertArrayHasKey('foo', $output);
149+
$this->assertArrayHasKey('bar', $output);
150+
$this->assertEquals('foo', $output['foo']);
151+
$this->assertEquals('bar', $output['bar']);
152+
}
153+
}

0 commit comments

Comments
 (0)