Compare commits

..

2 Commits

Author SHA1 Message Date
Guy Sartorelli
3ea7cf4383
Merge 0f386039df into 6bb9a0b33d 2024-10-14 05:30:35 +13:00
Guy Sartorelli
0f386039df
API Refactor template layer into its own module
Includes the following large-scale changes:
- Impoved barrier between model and view layers
- Improved casting of scalar to relevant DBField types
- Improved capabilities for rendering arbitrary data in templates
2024-10-11 11:31:00 +13:00
10 changed files with 35 additions and 137 deletions

View File

@ -4,7 +4,6 @@ namespace SilverStripe\Dev;
use Exception;
use InvalidArgumentException;
use LogicException;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Cookie_Backend;
use SilverStripe\Control\Director;
@ -215,7 +214,7 @@ class TestSession
$formCrawler = $page->filterXPath("//form[@id='$formID']");
$form = $formCrawler->form();
} catch (InvalidArgumentException $e) {
throw new LogicException("TestSession::submitForm failed to find the form '{$formID}'");
user_error("TestSession::submitForm failed to find the form {$formID}");
}
foreach ($data as $fieldName => $value) {
@ -236,7 +235,7 @@ class TestSession
if ($button) {
$btnXpath = "//button[@name='$button'] | //input[@name='$button'][@type='button' or @type='submit']";
if (!$formCrawler->children()->filterXPath($btnXpath)->count()) {
throw new LogicException("Can't find button '$button' to submit as part of test.");
throw new Exception("Can't find button '$button' to submit as part of test.");
}
$values[$button] = true;
}

View File

@ -4,7 +4,6 @@ namespace SilverStripe\Model;
use SilverStripe\Core\ArrayLib;
use InvalidArgumentException;
use JsonSerializable;
use stdClass;
/**
@ -17,9 +16,13 @@ use stdClass;
* ));
* </code>
*/
class ArrayData extends ModelData implements JsonSerializable
class ArrayData extends ModelData
{
protected array $array;
/**
* @var array
* @see ArrayData::_construct()
*/
protected $array;
/**
* @param object|array $value An associative array, or an object with simple properties.
@ -48,8 +51,10 @@ class ArrayData extends ModelData implements JsonSerializable
/**
* Get the source array
*
* @return array
*/
public function toMap(): array
public function toMap()
{
return $this->array;
}
@ -102,11 +107,6 @@ class ArrayData extends ModelData implements JsonSerializable
return !empty($this->array);
}
public function jsonSerialize(): array
{
return $this->array;
}
/**
* Converts an associative array to a simple object
*

View File

@ -1026,9 +1026,10 @@ class SSTemplateParser extends Parser implements TemplateParser
'arguments only.', $this);
}
// loop without arguments loops on the current scope
//loop without arguments loops on the current scope
if ($res['ArgumentCount'] == 0) {
$on = '$scope->locally()->self()';
$type = ViewLayerData::TYPE_METHOD;
$on = "\$scope->locally()->scopeToIntermediateValue('Me', [], '$type')";
} else { //loop in the normal way
$arg = $res['Arguments'][0];
if ($arg['ArgumentMode'] == 'string') {

View File

@ -4263,9 +4263,10 @@ class SSTemplateParser extends Parser implements TemplateParser
'arguments only.', $this);
}
// loop without arguments loops on the current scope
//loop without arguments loops on the current scope
if ($res['ArgumentCount'] == 0) {
$on = '$scope->locally()->self()';
$type = ViewLayerData::TYPE_METHOD;
$on = "\$scope->locally()->scopeToIntermediateValue('Me', [], '$type')";
} else { //loop in the normal way
$arg = $res['Arguments'][0];
if ($arg['ArgumentMode'] == 'string') {

View File

@ -376,6 +376,10 @@ class SSViewer_Scope
}
}
// if ($retval instanceof DBField) {
// $retval = $retval->getValue(); // Workaround because we're still calling obj in ViewLayerData
// }
$this->resetLocalScope();
return $retval;
}

View File

@ -9,7 +9,6 @@ use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Model\ModelData;
use SilverStripe\Model\ModelDataCustomised;
use SilverStripe\ORM\FieldType\DBClassName;
use Stringable;
use Traversable;
@ -23,16 +22,6 @@ class ViewLayerData implements IteratorAggregate, Stringable
public const TYPE_ANY = 'any';
/**
* Special variable names that can be used to get metadata about values
*/
public const META_DATA_NAMES = [
// Gets a DBClassName with the class name of $this->data
'ClassName',
// Returns $this->data
'Me',
];
private object $data;
public function __construct(mixed $data, mixed $source = null, string $name = '')
@ -93,8 +82,7 @@ class ViewLayerData implements IteratorAggregate, Stringable
{
// Note we explicitly DO NOT call count() or exists() on the data here because that would
// require fetching the data prematurely which could cause performance issues in extreme cases
return in_array($name, ViewLayerData::META_DATA_NAMES)
|| isset($this->data->$name)
return isset($this->data->$name)
|| ClassInfo::hasMethod($this->data, "get$name")
|| ClassInfo::hasMethod($this->data, $name);
}
@ -215,22 +203,9 @@ class ViewLayerData implements IteratorAggregate, Stringable
$data->objCacheSet($name, $arguments, $value);
}
if ($value === null && in_array($name, ViewLayerData::META_DATA_NAMES)) {
$value = $this->getMetaData($data, $name);
}
return $value;
}
private function getMetaData(object $data, string $name): mixed
{
return match ($name) {
'Me' => $data,
'ClassName' => DBClassName::create()->setValue(get_class($data)),
default => null
};
}
private function callDataMethod(object $data, string $name, array $arguments, bool &$fetchedValue = false): mixed
{
$hasDynamicMethods = method_exists($data, '__call');

View File

@ -28,7 +28,6 @@ use PHPUnit\Framework\Attributes\DoesNotPerformAssertions;
use SilverStripe\View\Exception\MissingTemplateException;
use SilverStripe\View\SSTemplateEngine;
use SilverStripe\View\ViewLayerData;
use stdClass;
class SSTemplateEngineTest extends SapphireTest
{
@ -499,23 +498,23 @@ class SSTemplateEngineTest extends SapphireTest
{
return [
[
'arg0:0,arg1:"string",arg2:true',
'arg1:0,arg2:"string",arg3:true',
'$methodWithTypedArguments(0, "string", true).RAW',
],
[
'arg0:false,arg1:"string",arg2:true',
'arg1:false,arg2:"string",arg3:true',
'$methodWithTypedArguments(false, "string", true).RAW',
],
[
'arg0:null,arg1:"string",arg2:true',
'arg1:null,arg2:"string",arg3:true',
'$methodWithTypedArguments(null, "string", true).RAW',
],
[
'arg0:"",arg1:"string",arg2:true',
'arg1:"",arg2:"string",arg3:true',
'$methodWithTypedArguments("", "string", true).RAW',
],
[
'arg0:0,arg1:1,arg2:2',
'arg1:0,arg2:1,arg3:2',
'$methodWithTypedArguments(0, 1, 2).RAW',
],
];
@ -527,62 +526,6 @@ class SSTemplateEngineTest extends SapphireTest
$this->assertEquals($expected, $this->render($template, new TestModelData()));
}
public static function provideEvaluatedArgumentTypes(): array
{
$stdobj = new stdClass();
$stdobj->key = 'value';
$scenarios = [
'null value' => [
'data' => ['Value' => null],
'useOverlay' => true,
'expected' => 'arg0:null',
],
'int value' => [
'data' => ['Value' => 1],
'useOverlay' => true,
'expected' => 'arg0:1',
],
'string value' => [
'data' => ['Value' => '1'],
'useOverlay' => true,
'expected' => 'arg0:"1"',
],
'boolean true' => [
'data' => ['Value' => true],
'useOverlay' => true,
'expected' => 'arg0:true',
],
'boolean false' => [
'data' => ['Value' => false],
'useOverlay' => true,
'expected' => 'arg0:false',
],
'object value' => [
'data' => ['Value' => $stdobj],
'useOverlay' => true,
'expected' => 'arg0:{"key":"value"}',
],
];
foreach ($scenarios as $key => $scenario) {
$scenario['useOverlay'] = false;
$scenarios[$key . ' no overlay'] = $scenario;
}
return $scenarios;
}
#[DataProvider('provideEvaluatedArgumentTypes')]
public function testEvaluatedArgumentTypes(array $data, bool $useOverlay, string $expected): void
{
$template = '$methodWithTypedArguments($Value).RAW';
$model = new TestModelData();
$overlay = $data;
if (!$useOverlay) {
$model = $model->customise($data);
$overlay = [];
}
$this->assertEquals($expected, $this->render($template, $model, $overlay));
}
public function testObjectDotArguments()
{
$this->assertEquals(
@ -2141,11 +2084,11 @@ after'
}
// Test instance behaviors
$this->render($content, cache: false);
$this->assertFileDoesNotExist($cacheFile, 'Cache file was created when caching was off');
$this->render($content, null, false);
$this->assertFalse(file_exists($cacheFile ?? ''), 'Cache file was created when caching was off');
$this->render($content, cache: true);
$this->assertFileExists($cacheFile, 'Cache file wasn\'t created when it was meant to');
$this->render($content, null, true);
$this->assertTrue(file_exists($cacheFile ?? ''), 'Cache file wasn\'t created when it was meant to');
unlink($cacheFile ?? '');
}
@ -2227,14 +2170,14 @@ after'
/**
* Small helper to render templates from strings
*/
private function render(string $templateString, mixed $data = null, array $overlay = [], bool $cache = false): string
private function render(string $templateString, mixed $data = null, bool $cacheTemplate = false): string
{
$engine = new SSTemplateEngine();
if ($data === null) {
$data = new SSTemplateEngineTest\TestFixture();
}
$data = new ViewLayerData($data);
return trim('' . $engine->renderString($templateString, $data, $overlay, $cache));
return trim('' . $engine->renderString($templateString, $data, cache: $cacheTemplate));
}
private function _renderWithSourceFileComments($name, $expected)

View File

@ -29,13 +29,9 @@ class TestModelData extends ModelData implements TestOnly
return "arg1:{$arg1},arg2:{$arg2}";
}
public function methodWithTypedArguments(...$args)
public function methodWithTypedArguments($arg1, $arg2, $arg3)
{
$ret = [];
foreach ($args as $i => $arg) {
$ret[] = "arg$i:" . json_encode($arg);
}
return implode(',', $ret);
return 'arg1:' . json_encode($arg1) . ',arg2:' . json_encode($arg2) . ',arg3:' . json_encode($arg3);
}
public function Type($arg)

View File

@ -23,7 +23,6 @@ use SilverStripe\View\Tests\ViewLayerDataTest\StringableObject;
use SilverStripe\View\Tests\ViewLayerDataTest\TestFixture;
use SilverStripe\View\Tests\ViewLayerDataTest\TestFixtureComplex;
use SilverStripe\View\ViewLayerData;
use stdClass;
use Throwable;
class ViewLayerDataTest extends SapphireTest
@ -728,24 +727,4 @@ class ViewLayerDataTest extends SapphireTest
$viewLayerData->MyField;
$this->assertSame('some value', $data->objCacheGet('MyField'));
}
public function testSpecialNames(): void
{
$data = new stdClass;
$viewLayerData = new ViewLayerData($data);
// Metadata values are available when there's nothing in the actual data
$this->assertTrue(isset($viewLayerData->ClassName));
$this->assertTrue(isset($viewLayerData->Me));
$this->assertSame(stdClass::class, $viewLayerData->getRawDataValue('ClassName')->getValue());
$this->assertSame($data, $viewLayerData->getRawDataValue('Me'));
// Metadata values are lower priority than real values in the actual data
$data->ClassName = 'some other class';
$data->Me = 'something else';
$this->assertTrue(isset($viewLayerData->ClassName));
$this->assertTrue(isset($viewLayerData->Me));
$this->assertSame('some other class', $viewLayerData->getRawDataValue('ClassName'));
$this->assertSame('something else', $viewLayerData->getRawDataValue('Me'));
}
}