FIX: Fix type preservation in <% include %> arguments

This commit is contained in:
Loz Calver 2018-10-05 12:06:58 +01:00 committed by Guy Sartorelli
parent 4339e4d02c
commit d6e8229352
No known key found for this signature in database
GPG Key ID: F313E3B9504D496A
3 changed files with 106 additions and 72 deletions

View File

@ -162,16 +162,17 @@ class SSViewer_DataPresenter extends SSViewer_Scope
public function getInjectedValue($property, array $params, $cast = true) public function getInjectedValue($property, array $params, $cast = true)
{ {
// Get source for this value // Get source for this value
$source = $this->getValueSource($property); $result = $this->getValueSource($property);
if (!$source) { if (!array_key_exists('source', $result)) {
return null; return null;
} }
// Look up the value - either from a callable, or from a directly provided value // Look up the value - either from a callable, or from a directly provided value
$source = $result['source'];
$res = []; $res = [];
if (isset($source['callable'])) { if (isset($source['callable'])) {
$res['value'] = $source['callable'](...$params); $res['value'] = $source['callable'](...$params);
} elseif (isset($source['value'])) { } elseif (array_key_exists('value', $source)) {
$res['value'] = $source['value']; $res['value'] = $source['value'];
} else { } else {
throw new InvalidArgumentException( throw new InvalidArgumentException(
@ -298,6 +299,8 @@ class SSViewer_DataPresenter extends SSViewer_Scope
$obj = $val['obj']; $obj = $val['obj'];
if ($name === 'hasValue') { if ($name === 'hasValue') {
$result = ($obj instanceof ViewableData) ? $obj->exists() : (bool)$obj; $result = ($obj instanceof ViewableData) ? $obj->exists() : (bool)$obj;
} elseif (is_null($obj) || (is_scalar($obj) && !is_string($obj))) {
$result = $obj; // Nulls and non-string scalars don't need casting
} else { } else {
$result = $obj->forTemplate(); // XML_val $result = $obj->forTemplate(); // XML_val
} }
@ -310,16 +313,18 @@ class SSViewer_DataPresenter extends SSViewer_Scope
} }
/** /**
* Evaluate a template override * Evaluate a template override. Returns an array where the presence of
* a 'value' key indiciates whether an override was successfully found,
* as null is a valid override value
* *
* @param string $property Name of override requested * @param string $property Name of override requested
* @param array $overrides List of overrides available * @param array $overrides List of overrides available
* @return null|array Null if not provided, or array with 'value' or 'callable' key * @return array An array with a 'value' key if a value has been found, or empty if not
*/ */
protected function processTemplateOverride($property, $overrides) protected function processTemplateOverride($property, $overrides)
{ {
if (!isset($overrides[$property])) { if (!array_key_exists($property, $overrides)) {
return null; return [];
} }
// Detect override type // Detect override type
@ -331,38 +336,40 @@ class SSViewer_DataPresenter extends SSViewer_Scope
// Late override may yet return null // Late override may yet return null
if (!isset($override)) { if (!isset($override)) {
return null; return [];
} }
} }
return [ 'value' => $override ]; return ['value' => $override];
} }
/** /**
* Determine source to use for getInjectedValue * Determine source to use for getInjectedValue. Returns an array where the presence of
* a 'source' key indiciates whether a value source was successfully found, as a source
* may be a null value returned from an override
* *
* @param string $property * @param string $property
* @return array|null * @return array An array with a 'source' key if a value source has been found, or empty if not
*/ */
protected function getValueSource($property) protected function getValueSource($property)
{ {
// Check for a presenter-specific override // Check for a presenter-specific override
$overlay = $this->processTemplateOverride($property, $this->overlay); $result = $this->processTemplateOverride($property, $this->overlay);
if (isset($overlay)) { if (array_key_exists('value', $result)) {
return $overlay; return ['source' => $result];
} }
// Check if the method to-be-called exists on the target object - if so, don't check any further // Check if the method to-be-called exists on the target object - if so, don't check any further
// injection locations // injection locations
$on = $this->itemIterator ? $this->itemIterator->current() : $this->item; $on = $this->itemIterator ? $this->itemIterator->current() : $this->item;
if (isset($on->$property) || method_exists($on, $property ?? '')) { if (isset($on->$property) || method_exists($on, $property ?? '')) {
return null; return [];
} }
// Check for a presenter-specific override // Check for a presenter-specific override
$underlay = $this->processTemplateOverride($property, $this->underlay); $result = $this->processTemplateOverride($property, $this->underlay);
if (isset($underlay)) { if (array_key_exists('value', $result)) {
return $underlay; return ['source' => $result];
} }
// Then for iterator-specific overrides // Then for iterator-specific overrides
@ -381,16 +388,19 @@ class SSViewer_DataPresenter extends SSViewer_Scope
// If we don't actually have an iterator at the moment, act like a list of length 1 // If we don't actually have an iterator at the moment, act like a list of length 1
$implementor->iteratorProperties(0, 1); $implementor->iteratorProperties(0, 1);
} }
return $source;
return ($source) ? ['source' => $source] : [];
} }
// And finally for global overrides // And finally for global overrides
if (array_key_exists($property, self::$globalProperties)) { if (array_key_exists($property, self::$globalProperties)) {
return self::$globalProperties[$property]; //get the method call return [
'source' => self::$globalProperties[$property] // get the method call
];
} }
// No value // No value
return null; return [];
} }
/** /**
@ -402,8 +412,8 @@ class SSViewer_DataPresenter extends SSViewer_Scope
*/ */
protected function castValue($value, $source) protected function castValue($value, $source)
{ {
// Already cast // If the value has already been cast, is null, or is a non-string scalar
if (is_object($value)) { if (is_object($value) || is_null($value) || (is_scalar($value) && !is_string($value))) {
return $value; return $value;
} }

View File

@ -713,63 +713,86 @@ after'
); );
} }
public function testTypesArePreserved() public function typePreservationDataProvider()
{
return [
// Null
['NULL:', 'null'],
['NULL:', 'NULL'],
// Booleans
['boolean:1', 'true'],
['boolean:1', 'TRUE'],
['boolean:', 'false'],
['boolean:', 'FALSE'],
// Strings which loosely look like booleans
['string:truthy', 'truthy'],
['string:falsy', 'falsy'],
// Integers
['integer:0', '0'],
['integer:1', '1'],
['integer:15', '15'],
['integer:-15', '-15'],
// Octal integers
['integer:83', '0123'],
['integer:-83', '-0123'],
// Hexadecimal integers
['integer:26', '0x1A'],
['integer:-26', '-0x1A'],
// Binary integers
['integer:255', '0b11111111'],
['integer:-255', '-0b11111111'],
// Floats (aka doubles)
['double:0', '0.0'],
['double:1', '1.0'],
['double:15.25', '15.25'],
['double:-15.25', '-15.25'],
['double:1200', '1.2e3'],
['double:-1200', '-1.2e3'],
['double:0.07', '7E-2'],
['double:-0.07', '-7E-2'],
// Explicitly quoted strings
['string:0', '"0"'],
['string:1', '\'1\''],
['string:foobar', '"foobar"'],
['string:foo bar baz', '"foo bar baz"'],
// Implicit strings
['string:foobar', 'foobar'],
['string:foo bar baz', 'foo bar baz']
];
}
/**
* @dataProvider typePreservationDataProvider
*/
public function testTypesArePreserved($expected, $templateArg)
{ {
$data = new ArrayData([ $data = new ArrayData([
'Test' => new TestViewableData() 'Test' => new TestViewableData()
]); ]);
// Null $this->assertEquals($expected, $this->render("\$Test.Type({$templateArg})", $data));
$this->assertEquals('NULL:', $this->render('$Test.Type(null)', $data)); }
$this->assertEquals('NULL:', $this->render('$Test.Type(NULL)', $data));
// Booleans /**
$this->assertEquals('boolean:1', $this->render('$Test.Type(TRUE)', $data)); * @dataProvider typePreservationDataProvider
$this->assertEquals('boolean:1', $this->render('$Test.Type(true)', $data)); */
$this->assertEquals('boolean:', $this->render('$Test.Type(FALSE)', $data)); public function testTypesArePreservedAsIncludeArguments($expected, $templateArg)
$this->assertEquals('boolean:', $this->render('$Test.Type(false)', $data)); {
$data = new ArrayData([
'Test' => new TestViewableData()
]);
// Strings which loosely look like booleans $this->assertEquals(
$this->assertEquals('string:truthy', $this->render('$Test.Type(truthy)', $data)); $expected,
$this->assertEquals('string:falsy', $this->render('$Test.Type(falsy)', $data)); $this->render("<% include SSViewerTestTypePreservation Argument={$templateArg} %>", $data)
);
}
// Integers public function testTypePreservationInConditionals()
$this->assertEquals('integer:0', $this->render('$Test.Type(0)', $data)); {
$this->assertEquals('integer:1', $this->render('$Test.Type(1)', $data)); $data = new ArrayData([
$this->assertEquals('integer:15', $this->render('$Test.Type(15)', $data)); 'Test' => new TestViewableData()
$this->assertEquals('integer:-15', $this->render('$Test.Type(-15)', $data)); ]);
# Octal integers
$this->assertEquals('integer:83', $this->render('$Test.Type(0123)', $data));
$this->assertEquals('integer:-83', $this->render('$Test.Type(-0123)', $data));
# Hexadecimal integers
$this->assertEquals('integer:26', $this->render('$Test.Type(0x1A)', $data));
$this->assertEquals('integer:-26', $this->render('$Test.Type(-0x1A)', $data));
# Binary integers
$this->assertEquals('integer:255', $this->render('$Test.Type(0b11111111)', $data));
$this->assertEquals('integer:-255', $this->render('$Test.Type(-0b11111111)', $data));
// Floats (aka doubles)
$this->assertEquals('double:0', $this->render('$Test.Type(0.0)', $data));
$this->assertEquals('double:1', $this->render('$Test.Type(1.0)', $data));
$this->assertEquals('double:15.25', $this->render('$Test.Type(15.25)', $data));
$this->assertEquals('double:-15.25', $this->render('$Test.Type(-15.25)', $data));
$this->assertEquals('double:1200', $this->render('$Test.Type(1.2e3)', $data));
$this->assertEquals('double:-1200', $this->render('$Test.Type(-1.2e3)', $data));
$this->assertEquals('double:0.07', $this->render('$Test.Type(7E-2)', $data));
$this->assertEquals('double:-0.07', $this->render('$Test.Type(-7E-2)', $data));
// Explicitly quoted strings
$this->assertEquals('string:0', $this->render('$Test.Type("0")', $data));
$this->assertEquals('string:1', $this->render('$Test.Type(\'1\')', $data));
$this->assertEquals('string:foobar', $this->render('$Test.Type("foobar")', $data));
$this->assertEquals('string:foo bar baz', $this->render('$Test.Type("foo bar baz")', $data));
// Implicit strings
$this->assertEquals('string:foobar', $this->render('$Test.Type(foobar)', $data));
$this->assertEquals('string:foo bar baz', $this->render('$Test.Type(foo bar baz)', $data));
// Types in conditionals // Types in conditionals
$this->assertEquals('pass', $this->render('<% if true %>pass<% else %>fail<% end_if %>', $data)); $this->assertEquals('pass', $this->render('<% if true %>pass<% else %>fail<% end_if %>', $data));

View File

@ -0,0 +1 @@
$Test.Type($Argument)