Skip to content

Commit d8d2611

Browse files
committed
[FEATURE] Allow variables assigned with dotted path
Enables assignment of template variables with a dotted path as name: $view->assign(‘parent.property’, ‘newValue’); Each value in the path leading up to the final property name is created as an array if it does not exist, and is read by reference until a subject is identified. Then the value is set on that subject. If a path points to an object and the path to the property contains one or more scalar values, assignment is refused with an exception clearly stating why a subject could not be resolved. The feature is also enabled for the JsonVariableProvider by making it call the StandardVariableProvider’s method.
1 parent 60545b6 commit d8d2611

File tree

4 files changed

+140
-7
lines changed

4 files changed

+140
-7
lines changed

src/Core/Variables/JSONVariableProvider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ protected function load()
9696
} else {
9797
$source = $this->source;
9898
}
99-
$this->variables = json_decode($source, defined('JSON_OBJECT_AS_ARRAY') ? JSON_OBJECT_AS_ARRAY : 1);
99+
parent::setSource(json_decode($source, defined('JSON_OBJECT_AS_ARRAY') ? JSON_OBJECT_AS_ARRAY : 1));
100100
$this->lastLoaded = time();
101101
}
102102
}

src/Core/Variables/StandardVariableProvider.php

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class StandardVariableProvider implements VariableProviderInterface
3131
*/
3232
public function __construct(array $variables = [])
3333
{
34-
$this->variables = $variables;
34+
$this->setSource($variables);
3535
}
3636

3737
/**
@@ -57,7 +57,14 @@ public function getScopeCopy($variables)
5757
*/
5858
public function setSource($source)
5959
{
60-
$this->variables = $source;
60+
// Rather than assign $this->variables = $source we iterate in order to make sure that
61+
// the logic within add() which is capable of storing nested variables, is used. In other
62+
// words: $source can contain dotted-path keys which become a nested array structure or
63+
// become overrides for values on objects.
64+
$this->variables = [];
65+
foreach ($source as $key => $value) {
66+
$this->add($key, $value);
67+
}
6168
}
6269

6370
/**
@@ -90,7 +97,64 @@ public function getAll()
9097
*/
9198
public function add($identifier, $value)
9299
{
93-
$this->variables[$identifier] = $value;
100+
if (strpos($identifier, '.') === false) {
101+
$this->variables[$identifier] = $value;
102+
} else {
103+
$parts = explode('.', $identifier);
104+
$root = array_shift($parts);
105+
if (!isset($this->variables[$root])) {
106+
$this->variables[$root] = [];
107+
}
108+
$subject = &$this->variables[$root];
109+
$propertyName = array_pop($parts);
110+
$iterated = [$root];
111+
112+
$this->assertSubjectIsArrayOrObject($subject, $iterated, $identifier);
113+
114+
foreach ($parts as $part) {
115+
$iterated[] = $part;
116+
if (is_array($subject) || $subject instanceof \ArrayAccess || $subject instanceof \ArrayObject) {
117+
if (!isset($subject[$part])) {
118+
$subject[$part] = [];
119+
}
120+
$subject = &$subject[$part];
121+
} elseif (is_object($subject)) {
122+
$subject = $this->extractSingleValue($subject, $part);
123+
} else {
124+
$subject = null;
125+
}
126+
127+
$this->assertSubjectIsArrayOrObject($subject, $iterated, $identifier);
128+
}
129+
130+
// Assign the value on the $subject that is now a reference (either to somewhere in $this->variables
131+
// or itself an object that is by nature a reference).
132+
if (is_array($subject) || $subject instanceof \ArrayAccess || $subject instanceof \ArrayObject) {
133+
$subject[$propertyName] = $value;
134+
} elseif (is_object($subject)) {
135+
$setterMethodName = 'set' . ucfirst($propertyName);
136+
if (method_exists($subject, $setterMethodName)) {
137+
$subject->$setterMethodName($value);
138+
} else {
139+
$subject->$propertyName = $value;
140+
}
141+
}
142+
}
143+
}
144+
145+
protected function assertSubjectIsArrayOrObject($subject, array $segmentsUntilSubject, $originalPathToSet)
146+
{
147+
if (!(is_array($subject) || is_object($subject))) {
148+
throw new \UnexpectedValueException(
149+
sprintf(
150+
'Variable in path "%s" is scalar and is not the last segment in the full path "%s". ' .
151+
'Refusing to coerce value of parent segment - cannot assign variable.',
152+
implode('.', $segmentsUntilSubject),
153+
$originalPathToSet
154+
),
155+
1546878798
156+
);
157+
}
94158
}
95159

96160
/**
@@ -310,7 +374,7 @@ protected function canExtractWithAccessor($subject, $propertyName, $accessor)
310374
* @param string $accessor
311375
* @return mixed
312376
*/
313-
protected function extractWithAccessor($subject, $propertyName, $accessor)
377+
protected function extractWithAccessor(&$subject, $propertyName, $accessor)
314378
{
315379
if ($accessor === self::ACCESSOR_ARRAY && is_array($subject) && array_key_exists($propertyName, $subject)
316380
|| $subject instanceof \ArrayAccess && $subject->offsetExists($propertyName)

tests/Unit/Core/Variables/StandardVariableProviderTest.php

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ public function getAccessorsForPathTestValues()
254254
* @param string $accessor
255255
* @param mixed $expected
256256
* @test
257-
* @dataProvider getExtractRedectAccessorTestValues
257+
* @dataProvider getExtractRedetectsAccessorTestValues
258258
*/
259259
public function testExtractRedetectsAccessorIfUnusableAccessorPassed($subject, $path, $accessor, $expected)
260260
{
@@ -266,7 +266,7 @@ public function testExtractRedetectsAccessorIfUnusableAccessorPassed($subject, $
266266
/**
267267
* @return array
268268
*/
269-
public function getExtractRedectAccessorTestValues()
269+
public function getExtractRedetectsAccessorTestValues()
270270
{
271271
return [
272272
[['test' => 'test'], 'test', null, 'test'],
@@ -276,4 +276,65 @@ public function getExtractRedectAccessorTestValues()
276276
[['test' => 'test'], 'test', StandardVariableProvider::ACCESSOR_ASSERTER, 'test'],
277277
];
278278
}
279+
280+
/**
281+
* @param array $variables
282+
* @param string $path
283+
* @param mixed $value
284+
* @test
285+
* @dataProvider getAddWithDottedPathTestValues
286+
*/
287+
public function testAddWithDottedPath(array $variables, $path, $value)
288+
{
289+
$subject = new StandardVariableProvider($variables);
290+
if ($path !== null) {
291+
$subject->add($path, $value);
292+
$this->assertSame($value, $subject->getByPath($path));
293+
} else {
294+
$this->assertSame($value, $subject->getSource());
295+
}
296+
}
297+
298+
/**
299+
* @return array
300+
*/
301+
public function getAddWithDottedPathTestValues()
302+
{
303+
$user = new UserWithoutToString('testuser');
304+
return [
305+
'Plain string assigned into blank variables array' => [[], 'new.array', 'mystring'],
306+
'Array built from dotted paths in original array' => [['dotted.one' => 1, 'dotted.two' => 2], null, ['dotted' => ['one' => 1, 'two' => 2]]],
307+
'Plain string assigned into existing variable' => ['foo' => ['bar' => 'test'], 'foo.bar', 'new'],
308+
'Property value assigned on object via setter' => [['parent' => $user], 'parent.name', 'newname'],
309+
'Property value assigned on object via public property' => [['parent' => $user], 'parent.newProperty', 'newValue'],
310+
];
311+
}
312+
313+
/**
314+
* @param array $variables
315+
* @param string $path
316+
* @param mixed $value
317+
* @test
318+
* @dataProvider getAddWithDottedPathThrowsErrorIfSubjectIsScalarTestValues
319+
*/
320+
public function testAddWithDottedPathThrowsErrorIfSubjectIsScalar(array $variables, $path)
321+
{
322+
$this->setExpectedException(\UnexpectedValueException::class, null, 1546878798);
323+
$subject = new StandardVariableProvider($variables);
324+
if ($path !== null) {
325+
$subject->add($path, 'foo');
326+
}
327+
}
328+
329+
/**
330+
* @return array
331+
*/
332+
public function getAddWithDottedPathThrowsErrorIfSubjectIsScalarTestValues()
333+
{
334+
$user = new UserWithoutToString('testuser');
335+
return [
336+
'Invalid property on object added after source' => [['user' => $user], 'user.doesnotexist.sub', 'value'],
337+
'Invalid property on object added in source' => [['user' => $user, 'user.doesnotexist.sub' => 'value'], null, null],
338+
];
339+
}
279340
}

tests/Unit/ViewHelpers/Fixtures/UserWithoutToString.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ public function __construct($name)
2525
$this->name = $name;
2626
}
2727

28+
/**
29+
* @param string $name
30+
*/
31+
public function setName($name)
32+
{
33+
$this->name = $name;
34+
}
35+
2836
/**
2937
* @return string
3038
*/

0 commit comments

Comments
 (0)