mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Compare commits
2 Commits
ad34ec779d
...
0a971d5f87
Author | SHA1 | Date | |
---|---|---|---|
|
0a971d5f87 | ||
|
7d5e854ae8 |
@ -17,7 +17,8 @@ use SilverStripe\Model\ArrayData;
|
|||||||
use SilverStripe\View\SSViewer;
|
use SilverStripe\View\SSViewer;
|
||||||
use LogicException;
|
use LogicException;
|
||||||
use SilverStripe\Control\HTTPResponse_Exception;
|
use SilverStripe\Control\HTTPResponse_Exception;
|
||||||
use SilverStripe\View\SSViewer_FromString;
|
use SilverStripe\View\SSTemplateEngine;
|
||||||
|
use SilverStripe\View\ViewLayerData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is is responsible for adding objects to another object's has_many
|
* This class is is responsible for adding objects to another object's has_many
|
||||||
@ -284,12 +285,15 @@ class GridFieldAddExistingAutocompleter extends AbstractGridFieldComponent imple
|
|||||||
$json = [];
|
$json = [];
|
||||||
Config::nest();
|
Config::nest();
|
||||||
SSViewer::config()->set('source_file_comments', false);
|
SSViewer::config()->set('source_file_comments', false);
|
||||||
$viewer = SSViewer_FromString::create($this->resultsFormat);
|
|
||||||
|
$engine = new SSTemplateEngine();
|
||||||
foreach ($results as $result) {
|
foreach ($results as $result) {
|
||||||
if (!$result->canView()) {
|
if (!$result->canView()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$title = Convert::html2raw($viewer->process($result, cache: false));
|
$title = Convert::html2raw(
|
||||||
|
$engine->renderString($this->resultsFormat, ViewLayerData::create($result), cache: false)
|
||||||
|
);
|
||||||
$json[] = [
|
$json[] = [
|
||||||
'label' => $title,
|
'label' => $title,
|
||||||
'value' => $title,
|
'value' => $title,
|
||||||
|
@ -18,7 +18,6 @@ use stdClass;
|
|||||||
*/
|
*/
|
||||||
class ArrayData extends ModelData
|
class ArrayData extends ModelData
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var array
|
||||||
* @see ArrayData::_construct()
|
* @see ArrayData::_construct()
|
||||||
@ -87,6 +86,7 @@ class ArrayData extends ModelData
|
|||||||
*/
|
*/
|
||||||
public function setField(string $fieldName, mixed $value): static
|
public function setField(string $fieldName, mixed $value): static
|
||||||
{
|
{
|
||||||
|
$this->objCacheClear();
|
||||||
$this->array[$fieldName] = $value;
|
$this->array[$fieldName] = $value;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
@ -102,6 +102,11 @@ class ArrayData extends ModelData
|
|||||||
return isset($this->array[$fieldName]);
|
return isset($this->array[$fieldName]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function exists(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->array);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts an associative array to a simple object
|
* Converts an associative array to a simple object
|
||||||
*
|
*
|
||||||
|
@ -206,6 +206,7 @@ class ModelData implements Stringable
|
|||||||
|
|
||||||
public function setDynamicData(string $field, mixed $value): static
|
public function setDynamicData(string $field, mixed $value): static
|
||||||
{
|
{
|
||||||
|
$this->objCacheClear();
|
||||||
$this->dynamicData[$field] = $value;
|
$this->dynamicData[$field] = $value;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
@ -415,10 +416,8 @@ class ModelData implements Stringable
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear object cache
|
* Clear object cache
|
||||||
*
|
|
||||||
* @return $this
|
|
||||||
*/
|
*/
|
||||||
public function objCacheClear()
|
public function objCacheClear(): static
|
||||||
{
|
{
|
||||||
$this->objCache = [];
|
$this->objCache = [];
|
||||||
return $this;
|
return $this;
|
||||||
|
@ -1934,6 +1934,7 @@ class DataObject extends ModelData implements DataObjectInterface, i18nEntityPro
|
|||||||
string $eagerLoadRelation,
|
string $eagerLoadRelation,
|
||||||
EagerLoadedList|DataObject $eagerLoadedData
|
EagerLoadedList|DataObject $eagerLoadedData
|
||||||
): void {
|
): void {
|
||||||
|
$this->objCacheClear();
|
||||||
$this->eagerLoadedData[$eagerLoadRelation] = $eagerLoadedData;
|
$this->eagerLoadedData[$eagerLoadRelation] = $eagerLoadedData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,11 +21,11 @@ class CastingService
|
|||||||
/**
|
/**
|
||||||
* Cast a value to the relevant object (usually a DBField instance) for use in the view layer.
|
* Cast a value to the relevant object (usually a DBField instance) for use in the view layer.
|
||||||
*
|
*
|
||||||
* @param null|array|ModelData $source Where the data originates from. This is used both to check for casting helpers
|
* @param null|array|object $source Where the data originates from. This is used both to check for casting helpers
|
||||||
* and to help set the value in cast DBField instances.
|
* and to help set the value in cast DBField instances.
|
||||||
* @param bool $strict If true, an object will be returned even if $data is null.
|
* @param bool $strict If true, an object will be returned even if $data is null.
|
||||||
*/
|
*/
|
||||||
public function cast(mixed $data, null|array|ModelData $source = null, string $fieldName = '', bool $strict = false): ?object
|
public function cast(mixed $data, null|array|object $source = null, string $fieldName = '', bool $strict = false): ?object
|
||||||
{
|
{
|
||||||
// null is null - we shouldn't cast it to an object, because that makes it harder
|
// null is null - we shouldn't cast it to an object, because that makes it harder
|
||||||
// for downstream checks to know there's "no value".
|
// for downstream checks to know there's "no value".
|
||||||
@ -42,6 +42,10 @@ class CastingService
|
|||||||
$service = null;
|
$service = null;
|
||||||
if ($source instanceof ModelData) {
|
if ($source instanceof ModelData) {
|
||||||
$service = $source->castingHelper($fieldName);
|
$service = $source->castingHelper($fieldName);
|
||||||
|
} elseif (is_object($source)) {
|
||||||
|
// $source is passed into setValue for the DBField instances, but those don't accept
|
||||||
|
// objects that aren't ModelData
|
||||||
|
$source = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cast to object if there's an explicit casting for this field
|
// Cast to object if there's an explicit casting for this field
|
||||||
|
@ -5,6 +5,7 @@ namespace SilverStripe\View;
|
|||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Psr\SimpleCache\CacheInterface;
|
use Psr\SimpleCache\CacheInterface;
|
||||||
use SilverStripe\Control\Director;
|
use SilverStripe\Control\Director;
|
||||||
|
use SilverStripe\Core\Config\Configurable;
|
||||||
use SilverStripe\Core\Flushable;
|
use SilverStripe\Core\Flushable;
|
||||||
use SilverStripe\Core\Injector\Injectable;
|
use SilverStripe\Core\Injector\Injectable;
|
||||||
use SilverStripe\Core\Injector\Injector;
|
use SilverStripe\Core\Injector\Injector;
|
||||||
@ -32,6 +33,12 @@ use SilverStripe\View\Exception\MissingTemplateException;
|
|||||||
class SSTemplateEngine implements TemplateEngine, Flushable
|
class SSTemplateEngine implements TemplateEngine, Flushable
|
||||||
{
|
{
|
||||||
use Injectable;
|
use Injectable;
|
||||||
|
use Configurable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default prepended cache key for partial caching
|
||||||
|
*/
|
||||||
|
private static string $global_key = '$CurrentReadingMode, $CurrentUser.ID';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of models being processed
|
* List of models being processed
|
||||||
|
@ -781,7 +781,7 @@ class SSTemplateParser extends Parser implements TemplateParser
|
|||||||
// the passed cache key, the block index, and the sha hash of the template.
|
// the passed cache key, the block index, and the sha hash of the template.
|
||||||
$res['php'] .= '$keyExpression = function() use ($scope, $cache) {' . PHP_EOL;
|
$res['php'] .= '$keyExpression = function() use ($scope, $cache) {' . PHP_EOL;
|
||||||
$res['php'] .= '$val = \'\';' . PHP_EOL;
|
$res['php'] .= '$val = \'\';' . PHP_EOL;
|
||||||
if ($globalKey = SSViewer::config()->get('global_key')) {
|
if ($globalKey = SSTemplateEngine::config()->get('global_key')) {
|
||||||
// Embed the code necessary to evaluate the globalKey directly into the template,
|
// Embed the code necessary to evaluate the globalKey directly into the template,
|
||||||
// so that SSTemplateParser only needs to be called during template regeneration.
|
// so that SSTemplateParser only needs to be called during template regeneration.
|
||||||
// Warning: If the global key is changed, it's necessary to flush the template cache.
|
// Warning: If the global key is changed, it's necessary to flush the template cache.
|
||||||
|
@ -3430,7 +3430,7 @@ class SSTemplateParser extends Parser implements TemplateParser
|
|||||||
// the passed cache key, the block index, and the sha hash of the template.
|
// the passed cache key, the block index, and the sha hash of the template.
|
||||||
$res['php'] .= '$keyExpression = function() use ($scope, $cache) {' . PHP_EOL;
|
$res['php'] .= '$keyExpression = function() use ($scope, $cache) {' . PHP_EOL;
|
||||||
$res['php'] .= '$val = \'\';' . PHP_EOL;
|
$res['php'] .= '$val = \'\';' . PHP_EOL;
|
||||||
if ($globalKey = SSViewer::config()->get('global_key')) {
|
if ($globalKey = SSTemplateEngine::config()->get('global_key')) {
|
||||||
// Embed the code necessary to evaluate the globalKey directly into the template,
|
// Embed the code necessary to evaluate the globalKey directly into the template,
|
||||||
// so that SSTemplateParser only needs to be called during template regeneration.
|
// so that SSTemplateParser only needs to be called during template regeneration.
|
||||||
// Warning: If the global key is changed, it's necessary to flush the template cache.
|
// Warning: If the global key is changed, it's necessary to flush the template cache.
|
||||||
|
@ -48,11 +48,6 @@ class SSViewer
|
|||||||
*/
|
*/
|
||||||
private static bool $theme_enabled = true;
|
private static bool $theme_enabled = true;
|
||||||
|
|
||||||
/**
|
|
||||||
* Default prepended cache key for partial caching
|
|
||||||
*/
|
|
||||||
private static string $global_key = '$CurrentReadingMode, $CurrentUser.ID';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If true, rendered templates will include comments indicating which template file was used.
|
* If true, rendered templates will include comments indicating which template file was used.
|
||||||
* May not be supported for some rendering engines.
|
* May not be supported for some rendering engines.
|
||||||
@ -71,14 +66,18 @@ class SSViewer
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Overridden value of rewrite_hash_links config
|
* Overridden value of rewrite_hash_links config
|
||||||
|
*
|
||||||
|
* Can be set to "php" to rewrite hash links with PHP executable code.
|
||||||
*/
|
*/
|
||||||
protected static ?bool $current_rewrite_hash_links = null;
|
protected static null|bool|string $current_rewrite_hash_links = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instance variable to disable rewrite_hash_links (overrides global default)
|
* Instance variable to disable rewrite_hash_links (overrides global default)
|
||||||
* Leave null to use global state.
|
* Leave null to use global state.
|
||||||
|
*
|
||||||
|
* Can be set to "php" to rewrite hash links with PHP executable code.
|
||||||
*/
|
*/
|
||||||
protected ?bool $rewriteHashlinks = null;
|
protected null|bool|string $rewriteHashlinks = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether resources from the Requirements API are included in a processed result.
|
* Determines whether resources from the Requirements API are included in a processed result.
|
||||||
@ -250,10 +249,8 @@ class SSViewer
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if rewrite hash links are enabled on this instance
|
* Check if rewrite hash links are enabled on this instance
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
*/
|
||||||
public function getRewriteHashLinks()
|
public function getRewriteHashLinks(): null|bool|string
|
||||||
{
|
{
|
||||||
if ($this->rewriteHashlinks !== null) {
|
if ($this->rewriteHashlinks !== null) {
|
||||||
return $this->rewriteHashlinks;
|
return $this->rewriteHashlinks;
|
||||||
@ -263,11 +260,8 @@ class SSViewer
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set if hash links are rewritten for this instance
|
* Set if hash links are rewritten for this instance
|
||||||
*
|
|
||||||
* @param bool $rewrite
|
|
||||||
* @return $this
|
|
||||||
*/
|
*/
|
||||||
public function setRewriteHashLinks($rewrite)
|
public function setRewriteHashLinks(null|bool|string $rewrite): static
|
||||||
{
|
{
|
||||||
$this->rewriteHashlinks = $rewrite;
|
$this->rewriteHashlinks = $rewrite;
|
||||||
return $this;
|
return $this;
|
||||||
@ -275,10 +269,8 @@ class SSViewer
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get default value for rewrite hash links for all modules
|
* Get default value for rewrite hash links for all modules
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
*/
|
||||||
public static function getRewriteHashLinksDefault()
|
public static function getRewriteHashLinksDefault(): null|bool|string
|
||||||
{
|
{
|
||||||
// Check if config overridden
|
// Check if config overridden
|
||||||
if (static::$current_rewrite_hash_links !== null) {
|
if (static::$current_rewrite_hash_links !== null) {
|
||||||
@ -289,10 +281,8 @@ class SSViewer
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set default rewrite hash links
|
* Set default rewrite hash links
|
||||||
*
|
|
||||||
* @param bool $rewrite
|
|
||||||
*/
|
*/
|
||||||
public static function setRewriteHashLinksDefault($rewrite)
|
public static function setRewriteHashLinksDefault(null|bool|string $rewrite)
|
||||||
{
|
{
|
||||||
static::$current_rewrite_hash_links = $rewrite;
|
static::$current_rewrite_hash_links = $rewrite;
|
||||||
}
|
}
|
||||||
@ -369,39 +359,6 @@ PHP;
|
|||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the evaluated string, passing it the given data.
|
|
||||||
* Used by partial caching to evaluate custom cache keys expressed using
|
|
||||||
* template expressions
|
|
||||||
*
|
|
||||||
* @param string $content Input string
|
|
||||||
* @param mixed $data Data context
|
|
||||||
* @param array $arguments Additional arguments
|
|
||||||
* @param bool $globalRequirements
|
|
||||||
*
|
|
||||||
* @return string Evaluated result
|
|
||||||
*/
|
|
||||||
public static function execute_string($content, $data, $arguments = [], $globalRequirements = false)
|
|
||||||
{
|
|
||||||
// @TODO come back to this. Probably delete it but keeping for now due to $globalRequirements
|
|
||||||
$v = SSViewer_FromString::create($content);
|
|
||||||
|
|
||||||
if ($globalRequirements) {
|
|
||||||
$v->includeRequirements(false);
|
|
||||||
} else {
|
|
||||||
//nest a requirements backend for our template rendering
|
|
||||||
$origBackend = Requirements::backend();
|
|
||||||
Requirements::set_backend(Requirements_Backend::create());
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return $v->process($data, $arguments);
|
|
||||||
} finally {
|
|
||||||
if (!$globalRequirements) {
|
|
||||||
Requirements::set_backend($origBackend);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an appropriate base tag for the given template.
|
* Return an appropriate base tag for the given template.
|
||||||
* It will be closed on an XHTML document, and unclosed on an HTML document.
|
* It will be closed on an XHTML document, and unclosed on an HTML document.
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace SilverStripe\View;
|
|
||||||
|
|
||||||
use SilverStripe\Core\Config\Config;
|
|
||||||
use SilverStripe\Core\Injector\Injector;
|
|
||||||
use SilverStripe\ORM\FieldType\DBField;
|
|
||||||
use SilverStripe\ORM\FieldType\DBHTMLText;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Special SSViewer that will process a template passed as a string, rather than a filename.
|
|
||||||
*/
|
|
||||||
class SSViewer_FromString extends SSViewer
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The template to use
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $content;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $content
|
|
||||||
* @param TemplateParser $parser
|
|
||||||
*/
|
|
||||||
public function __construct(string $content, ?TemplateEngine $templateEngine = null)
|
|
||||||
{
|
|
||||||
$this->content = $content;
|
|
||||||
if (!$templateEngine) {
|
|
||||||
$templateEngine = Injector::inst()->create(TemplateEngine::class);
|
|
||||||
}
|
|
||||||
$this->setTemplateEngine($templateEngine);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function process(mixed $item, array $overlay = [], bool $cache = true): DBHTMLText
|
|
||||||
{
|
|
||||||
$item = ViewLayerData::create($item);
|
|
||||||
$output = $this->getTemplateEngine()->renderString($this->content, $item, $overlay, $cache);
|
|
||||||
$html = DBField::create_field('HTMLFragment', $output);
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,14 +2,11 @@
|
|||||||
|
|
||||||
namespace SilverStripe\View;
|
namespace SilverStripe\View;
|
||||||
|
|
||||||
use ArrayIterator;
|
|
||||||
use Countable;
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Iterator;
|
use Iterator;
|
||||||
use LogicException;
|
use LogicException;
|
||||||
use SilverStripe\Core\ClassInfo;
|
use SilverStripe\Core\ClassInfo;
|
||||||
use SilverStripe\Core\Injector\Injector;
|
use SilverStripe\Core\Injector\Injector;
|
||||||
use SilverStripe\ORM\FieldType\DBField;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This tracks the current scope for an SSViewer instance. It has three goals:
|
* This tracks the current scope for an SSViewer instance. It has three goals:
|
||||||
@ -47,68 +44,50 @@ class SSViewer_Scope
|
|||||||
/**
|
/**
|
||||||
* The stack of previous items ("scopes") - an indexed array of: item, item iterator, item iterator total,
|
* The stack of previous items ("scopes") - an indexed array of: item, item iterator, item iterator total,
|
||||||
* pop index, up index, current index & parent overlay
|
* pop index, up index, current index & parent overlay
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
*/
|
||||||
private $itemStack = [];
|
private array $itemStack = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current "global" item (the one any lookup starts from)
|
* The current "global" item (the one any lookup starts from)
|
||||||
*
|
|
||||||
* @var object
|
|
||||||
*/
|
*/
|
||||||
protected $item;
|
protected ?ViewLayerData $item;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If we're looping over the current "global" item, here's the iterator that tracks with item we're up to
|
* If we're looping over the current "global" item, here's the iterator that tracks with item we're up to
|
||||||
*
|
|
||||||
* @var Iterator
|
|
||||||
*/
|
*/
|
||||||
protected $itemIterator;
|
protected ?Iterator $itemIterator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Total number of items in the iterator
|
* Total number of items in the iterator
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
*/
|
||||||
protected $itemIteratorTotal;
|
protected int $itemIteratorTotal;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A pointer into the item stack for the item that will become the active scope on the next pop call
|
* A pointer into the item stack for the item that will become the active scope on the next pop call
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
*/
|
||||||
private $popIndex;
|
private ?int $popIndex;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A pointer into the item stack for which item is "up" from this one
|
* A pointer into the item stack for which item is "up" from this one
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
*/
|
||||||
private $upIndex;
|
private ?int $upIndex;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A pointer into the item stack for which the active item (or null if not in stack yet)
|
* A pointer into the item stack for which the active item (or null if not in stack yet)
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
*/
|
||||||
private $currentIndex;
|
private int $currentIndex;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store of copies of the main item stack, so it's preserved during a lookup from local scope
|
* A store of copies of the main item stack, so it's preserved during a lookup from local scope
|
||||||
* (which may push/pop items to/from the main item stack)
|
* (which may push/pop items to/from the main item stack)
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
*/
|
||||||
private $localStack = [];
|
private array $localStack = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The index of the current item in the main item stack, so we know where to restore the scope
|
* The index of the current item in the main item stack, so we know where to restore the scope
|
||||||
* stored in $localStack.
|
* stored in $localStack.
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
*/
|
||||||
private $localIndex = 0;
|
private int $localIndex = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of global property providers
|
* List of global property providers
|
||||||
@ -128,35 +107,24 @@ class SSViewer_Scope
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Overlay variables. Take precedence over anything from the current scope
|
* Overlay variables. Take precedence over anything from the current scope
|
||||||
*
|
|
||||||
* @var array|null
|
|
||||||
*/
|
*/
|
||||||
protected $overlay;
|
protected array $overlay;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flag for whether overlay should be preserved when pushing a new scope
|
* Flag for whether overlay should be preserved when pushing a new scope
|
||||||
*
|
|
||||||
* @see SSViewer_Scope::pushScope()
|
|
||||||
* @var bool
|
|
||||||
*/
|
*/
|
||||||
protected $preserveOverlay = false;
|
protected bool $preserveOverlay = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Underlay variables. Concede precedence to overlay variables or anything from the current scope
|
* Underlay variables. Concede precedence to overlay variables or anything from the current scope
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
*/
|
||||||
protected $underlay;
|
protected array $underlay;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var object $item
|
|
||||||
* @var SSViewer_Scope $inheritedScope
|
|
||||||
*/
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
$item,
|
?ViewLayerData $item,
|
||||||
array $overlay = null,
|
array $overlay = [],
|
||||||
array $underlay = null,
|
array $underlay = [],
|
||||||
SSViewer_Scope $inheritedScope = null
|
?SSViewer_Scope $inheritedScope = null
|
||||||
) {
|
) {
|
||||||
$this->item = $item;
|
$this->item = $item;
|
||||||
|
|
||||||
@ -164,8 +132,8 @@ class SSViewer_Scope
|
|||||||
$this->itemIteratorTotal = ($inheritedScope) ? $inheritedScope->itemIteratorTotal : 0;
|
$this->itemIteratorTotal = ($inheritedScope) ? $inheritedScope->itemIteratorTotal : 0;
|
||||||
$this->itemStack[] = [$this->item, $this->itemIterator, $this->itemIteratorTotal, null, null, 0];
|
$this->itemStack[] = [$this->item, $this->itemIterator, $this->itemIteratorTotal, null, null, 0];
|
||||||
|
|
||||||
$this->overlay = $overlay ?: [];
|
$this->overlay = $overlay;
|
||||||
$this->underlay = $underlay ?: [];
|
$this->underlay = $underlay;
|
||||||
|
|
||||||
$this->cacheGlobalProperties();
|
$this->cacheGlobalProperties();
|
||||||
$this->cacheIteratorProperties();
|
$this->cacheIteratorProperties();
|
||||||
@ -351,29 +319,18 @@ class SSViewer_Scope
|
|||||||
|
|
||||||
if (!$this->itemIterator) {
|
if (!$this->itemIterator) {
|
||||||
// Note: it is important that getIterator() is called before count() as implemenations may rely on
|
// Note: it is important that getIterator() is called before count() as implemenations may rely on
|
||||||
// this to efficiency get both the number of records and an iterator (e.g. DataList does this)
|
// this to efficiently get both the number of records and an iterator (e.g. DataList does this)
|
||||||
|
$this->itemIterator = $this->item->getIterator();
|
||||||
|
|
||||||
// Item may be an array or a regular IteratorAggregate
|
// This will execute code in a generator up to the first yield. For example, this ensures that
|
||||||
if (is_array($this->item)) {
|
// DataList::getIterator() is called before Datalist::count() which means we only run the query once
|
||||||
$this->itemIterator = new ArrayIterator($this->item);
|
// instead of running a separate explicit count() query
|
||||||
} elseif ($this->item instanceof Iterator) {
|
$this->itemIterator->rewind();
|
||||||
$this->itemIterator = $this->item;
|
|
||||||
} else {
|
|
||||||
$this->itemIterator = $this->item->getIterator();
|
|
||||||
|
|
||||||
// This will execute code in a generator up to the first yield. For example, this ensures that
|
// Get the number of items in the iterator.
|
||||||
// DataList::getIterator() is called before Datalist::count()
|
// Don't just use iterator_count because that results in running through the list
|
||||||
$this->itemIterator->rewind();
|
// which causes some iterators to no longer be iterable for some reason
|
||||||
}
|
$this->itemIteratorTotal = $this->item->getIteratorCount();
|
||||||
|
|
||||||
// If the item implements Countable, use that to fetch the count, otherwise we have to inspect the
|
|
||||||
// iterator and then rewind it.
|
|
||||||
if ($this->item instanceof Countable) {
|
|
||||||
$this->itemIteratorTotal = count($this->item);
|
|
||||||
} else {
|
|
||||||
$this->itemIteratorTotal = iterator_count($this->itemIterator);
|
|
||||||
$this->itemIterator->rewind();
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->itemStack[$this->localIndex][SSViewer_Scope::ITEM_ITERATOR] = $this->itemIterator;
|
$this->itemStack[$this->localIndex][SSViewer_Scope::ITEM_ITERATOR] = $this->itemIterator;
|
||||||
$this->itemStack[$this->localIndex][SSViewer_Scope::ITEM_ITERATOR_TOTAL] = $this->itemIteratorTotal;
|
$this->itemStack[$this->localIndex][SSViewer_Scope::ITEM_ITERATOR_TOTAL] = $this->itemIteratorTotal;
|
||||||
@ -412,7 +369,7 @@ class SSViewer_Scope
|
|||||||
} else {
|
} else {
|
||||||
$on = $this->getCurrentItem();
|
$on = $this->getCurrentItem();
|
||||||
if ($on && isset($on->$name)) {
|
if ($on && isset($on->$name)) {
|
||||||
$retval = $on->getRawDataValue($name, $type, $arguments);
|
$retval = $on->getRawDataValue($name, $arguments, $type);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($retval === null) {
|
if ($retval === null) {
|
||||||
@ -431,7 +388,7 @@ class SSViewer_Scope
|
|||||||
/**
|
/**
|
||||||
* Check if the current item in scope has a value for the named field.
|
* Check if the current item in scope has a value for the named field.
|
||||||
*/
|
*/
|
||||||
public function hasValue(string $name, array $arguments): bool
|
public function hasValue(string $name, array $arguments, string $type): bool
|
||||||
{
|
{
|
||||||
// @TODO: look for ways to remove the need to call hasValue (e.g. using isset($this->getCurrentItem()->$name) and an equivalent for over/underlays)
|
// @TODO: look for ways to remove the need to call hasValue (e.g. using isset($this->getCurrentItem()->$name) and an equivalent for over/underlays)
|
||||||
$retval = null;
|
$retval = null;
|
||||||
@ -443,7 +400,7 @@ class SSViewer_Scope
|
|||||||
if ($retval === null) {
|
if ($retval === null) {
|
||||||
$on = $this->getCurrentItem();
|
$on = $this->getCurrentItem();
|
||||||
if ($on) {
|
if ($on) {
|
||||||
$retval = $on->hasDataValue($name, $arguments);
|
$retval = $on->hasDataValue($name, $arguments, $type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
namespace SilverStripe\View;
|
namespace SilverStripe\View;
|
||||||
|
|
||||||
use BadMethodCallException;
|
use BadMethodCallException;
|
||||||
use Countable;
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use IteratorAggregate;
|
use IteratorAggregate;
|
||||||
use SilverStripe\Core\ClassInfo;
|
use SilverStripe\Core\ClassInfo;
|
||||||
@ -13,7 +12,7 @@ use SilverStripe\Model\ModelDataCustomised;
|
|||||||
use Stringable;
|
use Stringable;
|
||||||
use Traversable;
|
use Traversable;
|
||||||
|
|
||||||
class ViewLayerData implements IteratorAggregate, Stringable, Countable
|
class ViewLayerData implements IteratorAggregate, Stringable
|
||||||
{
|
{
|
||||||
use Injectable;
|
use Injectable;
|
||||||
|
|
||||||
@ -40,19 +39,18 @@ class ViewLayerData implements IteratorAggregate, Stringable, Countable
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Needed so we can rewind in SSViewer_Scope::next() after getting itemIteratorTotal without throwing an exception.
|
* Needed so we can rewind in SSViewer_Scope::next() after getting itemIteratorTotal without throwing an exception.
|
||||||
* @TODO see if we can remove the need for this
|
|
||||||
*/
|
*/
|
||||||
public function count(): int
|
public function getIteratorCount(): int
|
||||||
{
|
{
|
||||||
$count = $this->getRawDataValue('count');
|
$count = $this->getRawDataValue('count');
|
||||||
if ($count) {
|
if (is_numeric($count)) {
|
||||||
return $count;
|
return $count;
|
||||||
}
|
}
|
||||||
if (is_countable($this->data)) {
|
if (is_countable($this->data)) {
|
||||||
return count($this->data);
|
return count($this->data);
|
||||||
}
|
}
|
||||||
if (ClassInfo::hasMethod($this->data, 'getIterator')) {
|
if (ClassInfo::hasMethod($this->data, 'getIterator')) {
|
||||||
return count($this->data->getIterator());
|
return iterator_count($this->data->getIterator());
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -90,7 +88,7 @@ class ViewLayerData implements IteratorAggregate, Stringable, Countable
|
|||||||
|
|
||||||
public function __get(string $name): ?ViewLayerData
|
public function __get(string $name): ?ViewLayerData
|
||||||
{
|
{
|
||||||
$value = $this->getRawDataValue($name, ViewLayerData::TYPE_PROPERTY);
|
$value = $this->getRawDataValue($name, type: ViewLayerData::TYPE_PROPERTY);
|
||||||
if ($value === null) {
|
if ($value === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -100,7 +98,7 @@ class ViewLayerData implements IteratorAggregate, Stringable, Countable
|
|||||||
|
|
||||||
public function __call(string $name, array $arguments = []): ?ViewLayerData
|
public function __call(string $name, array $arguments = []): ?ViewLayerData
|
||||||
{
|
{
|
||||||
$value = $this->getRawDataValue($name, ViewLayerData::TYPE_METHOD, $arguments);
|
$value = $this->getRawDataValue($name, $arguments, ViewLayerData::TYPE_METHOD);
|
||||||
if ($value === null) {
|
if ($value === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -119,23 +117,38 @@ class ViewLayerData implements IteratorAggregate, Stringable, Countable
|
|||||||
/**
|
/**
|
||||||
* Check if there is a truthy value or (for ModelData) if the data exists().
|
* Check if there is a truthy value or (for ModelData) if the data exists().
|
||||||
*/
|
*/
|
||||||
public function hasDataValue(?string $name = null, array $arguments = []): bool
|
public function hasDataValue(?string $name = null, array $arguments = [], string $type = ViewLayerData::TYPE_ANY): bool
|
||||||
{
|
{
|
||||||
if ($name) {
|
if ($name) {
|
||||||
|
// Ask the model if it has a value for that field
|
||||||
if ($this->data instanceof ModelData) {
|
if ($this->data instanceof ModelData) {
|
||||||
return $this->data->hasValue($name, $arguments);
|
return $this->data->hasValue($name, $arguments);
|
||||||
}
|
}
|
||||||
return ViewLayerData::create($this->getRawDataValue($name, arguments: $arguments))->hasDataValue();
|
// Check for ourselves if there's a value for that field
|
||||||
|
// This mimics what ModelData does, which provides consistency
|
||||||
|
$value = $this->getRawDataValue($name, $arguments, $type);
|
||||||
|
if ($value === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return ViewLayerData::create($value, $this->data, $name)->hasDataValue();
|
||||||
}
|
}
|
||||||
|
// Ask the model if it "exists"
|
||||||
if ($this->data instanceof ModelData) {
|
if ($this->data instanceof ModelData) {
|
||||||
return $this->data->exists();
|
return $this->data->exists();
|
||||||
}
|
}
|
||||||
|
// Mimics ModelData checks on lists
|
||||||
|
if (is_countable($this->data)) {
|
||||||
|
return count($this->data) > 0;
|
||||||
|
}
|
||||||
|
// Check for truthiness (which is effectively `return true` since data is an object)
|
||||||
|
// We do this to mimic ModelData->hasValue() for consistency
|
||||||
return (bool) $this->data;
|
return (bool) $this->data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @TODO We need this publicly right now for the ss template engine method args, but need to check if
|
/**
|
||||||
// we can rely on it, since twig won't be calling this at all
|
* Get the raw value of some field/property/method on the data, without wrapping it in ViewLayerData.
|
||||||
public function getRawDataValue(string $name, string $type = ViewLayerData::TYPE_ANY, array $arguments = []): mixed
|
*/
|
||||||
|
public function getRawDataValue(string $name, array $arguments = [], string $type = ViewLayerData::TYPE_ANY): mixed
|
||||||
{
|
{
|
||||||
if ($type !== ViewLayerData::TYPE_ANY && $type !== ViewLayerData::TYPE_METHOD && $type !== ViewLayerData::TYPE_PROPERTY) {
|
if ($type !== ViewLayerData::TYPE_ANY && $type !== ViewLayerData::TYPE_METHOD && $type !== ViewLayerData::TYPE_PROPERTY) {
|
||||||
throw new InvalidArgumentException('$type must be one of the TYPE_* constant values');
|
throw new InvalidArgumentException('$type must be one of the TYPE_* constant values');
|
||||||
|
@ -19,6 +19,8 @@ use SilverStripe\Dev\FunctionalTest;
|
|||||||
use SilverStripe\Security\Member;
|
use SilverStripe\Security\Member;
|
||||||
use SilverStripe\View\SSViewer;
|
use SilverStripe\View\SSViewer;
|
||||||
use PHPUnit\Framework\Attributes\DataProvider;
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\Control\Tests\ControllerTest\ControllerWithDummyEngine;
|
||||||
|
use SilverStripe\Control\Tests\ControllerTest\DummyTemplateEngine;
|
||||||
|
|
||||||
class ControllerTest extends FunctionalTest
|
class ControllerTest extends FunctionalTest
|
||||||
{
|
{
|
||||||
@ -858,4 +860,12 @@ class ControllerTest extends FunctionalTest
|
|||||||
$response = $this->post('HTTPMethodTestController', ['dummy' => 'example']);
|
$response = $this->post('HTTPMethodTestController', ['dummy' => 'example']);
|
||||||
$this->assertEquals('Routed to postLegacyRoot', $response->getBody());
|
$this->assertEquals('Routed to postLegacyRoot', $response->getBody());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testTemplateEngineUsed()
|
||||||
|
{
|
||||||
|
$controller = new ControllerWithDummyEngine();
|
||||||
|
$this->assertSame('This is my controller', $controller->render()->getValue());
|
||||||
|
$this->assertSame('This is my controller', $controller->renderWith('literally-any-template')->getValue());
|
||||||
|
$this->assertInstanceOf(DummyTemplateEngine::class, $controller->getViewer('')->getTemplateEngine());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Control\Tests\ControllerTest;
|
||||||
|
|
||||||
|
use SilverStripe\Control\Controller;
|
||||||
|
use SilverStripe\Dev\TestOnly;
|
||||||
|
use SilverStripe\View\TemplateEngine;
|
||||||
|
|
||||||
|
class ControllerWithDummyEngine extends Controller implements TestOnly
|
||||||
|
{
|
||||||
|
protected function getTemplateEngine(): TemplateEngine
|
||||||
|
{
|
||||||
|
return new DummyTemplateEngine();
|
||||||
|
}
|
||||||
|
}
|
40
tests/php/Control/ControllerTest/DummyTemplateEngine.php
Normal file
40
tests/php/Control/ControllerTest/DummyTemplateEngine.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Control\Tests\ControllerTest;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\TestOnly;
|
||||||
|
use SilverStripe\View\TemplateEngine;
|
||||||
|
use SilverStripe\View\ViewLayerData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dummy template renderer that doesn't actually render any templates.
|
||||||
|
*/
|
||||||
|
class DummyTemplateEngine implements TemplateEngine, TestOnly
|
||||||
|
{
|
||||||
|
private string $output = 'This is my controller';
|
||||||
|
|
||||||
|
public function __construct(string|array $templateCandidates = [])
|
||||||
|
{
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTemplate(string|array $templateCandidates): static
|
||||||
|
{
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasTemplate(string|array $templateCandidates): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderString(string $template, ViewLayerData $model, array $overlay = [], bool $cache = true): string
|
||||||
|
{
|
||||||
|
return $this->output;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render(ViewLayerData $model, array $overlay = []): string
|
||||||
|
{
|
||||||
|
return $this->output;
|
||||||
|
}
|
||||||
|
}
|
206
tests/php/View/CastingServiceTest.php
Normal file
206
tests/php/View/CastingServiceTest.php
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\View\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use SilverStripe\Model\ArrayData;
|
||||||
|
use SilverStripe\Model\List\ArrayList;
|
||||||
|
use SilverStripe\ORM\FieldType\DBBoolean;
|
||||||
|
use SilverStripe\ORM\FieldType\DBCurrency;
|
||||||
|
use SilverStripe\ORM\FieldType\DBDate;
|
||||||
|
use SilverStripe\ORM\FieldType\DBField;
|
||||||
|
use SilverStripe\ORM\FieldType\DBFloat;
|
||||||
|
use SilverStripe\ORM\FieldType\DBHTMLText;
|
||||||
|
use SilverStripe\ORM\FieldType\DBInt;
|
||||||
|
use SilverStripe\ORM\FieldType\DBText;
|
||||||
|
use SilverStripe\ORM\FieldType\DBTime;
|
||||||
|
use SilverStripe\View\CastingService;
|
||||||
|
use SilverStripe\View\Tests\CastingServiceTest\TestDataObject;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
class CastingServiceTest extends SapphireTest
|
||||||
|
{
|
||||||
|
// protected static $extra_dataobjects = [
|
||||||
|
// TestDataObject::class,
|
||||||
|
// ];
|
||||||
|
|
||||||
|
protected $usesDatabase = false;
|
||||||
|
|
||||||
|
public static function provideCast(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'data' => null,
|
||||||
|
'source' => null,
|
||||||
|
'fieldName' => '',
|
||||||
|
'expected' => null,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => new stdClass(),
|
||||||
|
'source' => null,
|
||||||
|
'fieldName' => '',
|
||||||
|
'expected' => stdClass::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => new stdClass(),
|
||||||
|
'source' => TestDataObject::class,
|
||||||
|
'fieldName' => 'DateField',
|
||||||
|
'expected' => stdClass::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => new DBText(),
|
||||||
|
'source' => TestDataObject::class,
|
||||||
|
'fieldName' => 'DateField',
|
||||||
|
'expected' => stdClass::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => '2024-10-10',
|
||||||
|
'source' => TestDataObject::class,
|
||||||
|
'fieldName' => 'DateField',
|
||||||
|
'expected' => DBDate::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => 'some value',
|
||||||
|
'source' => TestDataObject::class,
|
||||||
|
'fieldName' => 'HtmlField',
|
||||||
|
'expected' => DBHTMLText::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => '12.35',
|
||||||
|
'source' => TestDataObject::class,
|
||||||
|
'fieldName' => 'OverrideCastingHelper',
|
||||||
|
'expected' => DBCurrency::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => '10:17:36',
|
||||||
|
'source' => TestDataObject::class,
|
||||||
|
'fieldName' => 'TimeField',
|
||||||
|
'expected' => DBTime::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => 123456,
|
||||||
|
'source' => TestDataObject::class,
|
||||||
|
'fieldName' => 'RandomField',
|
||||||
|
'expected' => DBInt::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => '<body>some text</body>',
|
||||||
|
'source' => TestDataObject::class,
|
||||||
|
'fieldName' => 'RandomField',
|
||||||
|
'expected' => DBText::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => '12.35',
|
||||||
|
'source' => null,
|
||||||
|
'fieldName' => 'OverrideCastingHelper',
|
||||||
|
'expected' => DBText::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => 123456,
|
||||||
|
'source' => null,
|
||||||
|
'fieldName' => 'RandomField',
|
||||||
|
'expected' => DBInt::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => '10:17:36',
|
||||||
|
'source' => null,
|
||||||
|
'fieldName' => 'TimeField',
|
||||||
|
'expected' => DBText::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => '<body>some text</body>',
|
||||||
|
'source' => null,
|
||||||
|
'fieldName' => '',
|
||||||
|
'expected' => DBText::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => true,
|
||||||
|
'source' => null,
|
||||||
|
'fieldName' => '',
|
||||||
|
'expected' => DBBoolean::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => false,
|
||||||
|
'source' => null,
|
||||||
|
'fieldName' => '',
|
||||||
|
'expected' => DBBoolean::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => 1.234,
|
||||||
|
'source' => null,
|
||||||
|
'fieldName' => '',
|
||||||
|
'expected' => DBFloat::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => [],
|
||||||
|
'source' => null,
|
||||||
|
'fieldName' => '',
|
||||||
|
'expected' => ArrayList::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => [1,2,3,4],
|
||||||
|
'source' => null,
|
||||||
|
'fieldName' => '',
|
||||||
|
'expected' => ArrayList::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => ['one' => 1, 'two' => 2],
|
||||||
|
'source' => null,
|
||||||
|
'fieldName' => '',
|
||||||
|
'expected' => ArrayData::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => ['one' => 1, 'two' => 2],
|
||||||
|
'source' => TestDataObject::class,
|
||||||
|
'fieldName' => 'AnyField',
|
||||||
|
'expected' => ArrayData::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => ['one' => 1, 'two' => 2],
|
||||||
|
'source' => TestDataObject::class,
|
||||||
|
'fieldName' => 'ArrayAsText',
|
||||||
|
'expected' => DBText::class,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideCast')]
|
||||||
|
public function testCast(mixed $data, ?string $source, string $fieldName, ?string $expected): void
|
||||||
|
{
|
||||||
|
// Can't instantiate DataObject in a data provider
|
||||||
|
if (is_string($source)) {
|
||||||
|
$source = new $source();
|
||||||
|
}
|
||||||
|
$service = new CastingService();
|
||||||
|
$value = $service->cast($data, $source, $fieldName);
|
||||||
|
|
||||||
|
// Check the cast object is the correct type
|
||||||
|
if ($expected === null) {
|
||||||
|
$this->assertNull($value);
|
||||||
|
} elseif (is_object($data)) {
|
||||||
|
$this->assertSame($data, $value);
|
||||||
|
} else {
|
||||||
|
$this->assertInstanceOf($expected, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the value is retained
|
||||||
|
if ($value instanceof DBField && !is_object($data)) {
|
||||||
|
$this->assertSame($data, $value->getValue());
|
||||||
|
}
|
||||||
|
if ($value instanceof ArrayData && !is_object($data)) {
|
||||||
|
$this->assertSame($data, $value->toMap());
|
||||||
|
}
|
||||||
|
if ($value instanceof ArrayList && !is_object($data)) {
|
||||||
|
$this->assertSame($data, $value->toArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCastStrict(): void
|
||||||
|
{
|
||||||
|
$service = new CastingService();
|
||||||
|
$value = $service->cast(null, strict: true);
|
||||||
|
$this->assertInstanceOf(DBText::class, $value);
|
||||||
|
$this->assertNull($value->getValue());
|
||||||
|
}
|
||||||
|
}
|
30
tests/php/View/CastingServiceTest/TestDataObject.php
Normal file
30
tests/php/View/CastingServiceTest/TestDataObject.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\View\Tests\CastingServiceTest;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\TestOnly;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
|
||||||
|
class TestDataObject extends DataObject implements TestOnly
|
||||||
|
{
|
||||||
|
private static string $table_name = 'CastingServiceTest_TestDataObject';
|
||||||
|
|
||||||
|
private static array $db = [
|
||||||
|
'HtmlField' => 'HTMLText',
|
||||||
|
'DateField' => 'Date',
|
||||||
|
];
|
||||||
|
|
||||||
|
private static array $casting = [
|
||||||
|
'DateField' => 'Text', // won't override
|
||||||
|
'TimeField' => 'Time',
|
||||||
|
'ArrayAsText' => 'Text',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function castingHelper(string $field): ?string
|
||||||
|
{
|
||||||
|
if ($field === 'OverrideCastingHelper') {
|
||||||
|
return 'Currency';
|
||||||
|
}
|
||||||
|
return parent::castingHelper($field);
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,8 @@ class SSTemplateEngineFindTemplateTest extends SapphireTest
|
|||||||
{
|
{
|
||||||
private string $base;
|
private string $base;
|
||||||
|
|
||||||
|
private ThemeResourceLoader $origLoader;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
@ -40,6 +42,7 @@ class SSTemplateEngineFindTemplateTest extends SapphireTest
|
|||||||
$themeManifest->setProject('myproject');
|
$themeManifest->setProject('myproject');
|
||||||
$themeManifest->init();
|
$themeManifest->init();
|
||||||
// New Loader for that root
|
// New Loader for that root
|
||||||
|
$this->origLoader = ThemeResourceLoader::inst();
|
||||||
$themeResourceLoader = new ThemeResourceLoader($this->base);
|
$themeResourceLoader = new ThemeResourceLoader($this->base);
|
||||||
$themeResourceLoader->addSet('$default', $themeManifest);
|
$themeResourceLoader->addSet('$default', $themeManifest);
|
||||||
ThemeResourceLoader::set_instance($themeResourceLoader);
|
ThemeResourceLoader::set_instance($themeResourceLoader);
|
||||||
@ -50,6 +53,7 @@ class SSTemplateEngineFindTemplateTest extends SapphireTest
|
|||||||
|
|
||||||
protected function tearDown(): void
|
protected function tearDown(): void
|
||||||
{
|
{
|
||||||
|
ThemeResourceLoader::set_instance($this->origLoader);
|
||||||
ModuleLoader::inst()->popManifest();
|
ModuleLoader::inst()->popManifest();
|
||||||
parent::tearDown();
|
parent::tearDown();
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,6 @@ use PHPUnit\Framework\Attributes\DataProvider;
|
|||||||
use PHPUnit\Framework\Attributes\DoesNotPerformAssertions;
|
use PHPUnit\Framework\Attributes\DoesNotPerformAssertions;
|
||||||
use SilverStripe\View\Exception\MissingTemplateException;
|
use SilverStripe\View\Exception\MissingTemplateException;
|
||||||
use SilverStripe\View\SSTemplateEngine;
|
use SilverStripe\View\SSTemplateEngine;
|
||||||
use SilverStripe\View\ThemeResourceLoader;
|
|
||||||
use SilverStripe\View\ViewLayerData;
|
use SilverStripe\View\ViewLayerData;
|
||||||
|
|
||||||
class SSTemplateEngineTest extends SapphireTest
|
class SSTemplateEngineTest extends SapphireTest
|
||||||
@ -2076,9 +2075,6 @@ after'
|
|||||||
$this->assertEquals(1, $count);
|
$this->assertEquals(1, $count);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests if caching for SSViewer_FromString is working
|
|
||||||
*/
|
|
||||||
public function testFromStringCaching()
|
public function testFromStringCaching()
|
||||||
{
|
{
|
||||||
$content = 'Test content';
|
$content = 'Test content';
|
||||||
@ -2177,7 +2173,7 @@ after'
|
|||||||
private function render(string $templateString, mixed $data = null, bool $cacheTemplate = false): string
|
private function render(string $templateString, mixed $data = null, bool $cacheTemplate = false): string
|
||||||
{
|
{
|
||||||
$engine = new SSTemplateEngine();
|
$engine = new SSTemplateEngine();
|
||||||
if (!$data) {
|
if ($data === null) {
|
||||||
$data = new SSTemplateEngineTest\TestFixture();
|
$data = new SSTemplateEngineTest\TestFixture();
|
||||||
}
|
}
|
||||||
$data = new ViewLayerData($data);
|
$data = new ViewLayerData($data);
|
||||||
|
@ -162,7 +162,6 @@ class SSViewerTest extends SapphireTest
|
|||||||
<body>
|
<body>
|
||||||
</html>'
|
</html>'
|
||||||
);
|
);
|
||||||
// Note: SSViewer_FromString doesn't rewrite hash links.
|
|
||||||
$tmpl = new SSViewer([], $engine);
|
$tmpl = new SSViewer([], $engine);
|
||||||
$result = $tmpl->process('pretend this is a model');
|
$result = $tmpl->process('pretend this is a model');
|
||||||
$this->assertStringContainsString(
|
$this->assertStringContainsString(
|
||||||
@ -206,7 +205,6 @@ class SSViewerTest extends SapphireTest
|
|||||||
<body>
|
<body>
|
||||||
</html>'
|
</html>'
|
||||||
);
|
);
|
||||||
// Note: SSViewer_FromString doesn't rewrite hash links.
|
|
||||||
$tmpl = new SSViewer([], $engine);
|
$tmpl = new SSViewer([], $engine);
|
||||||
$result = $tmpl->process('pretend this is a model');
|
$result = $tmpl->process('pretend this is a model');
|
||||||
|
|
||||||
|
@ -5,10 +5,12 @@ namespace SilverStripe\View\Tests;
|
|||||||
use ArrayIterator;
|
use ArrayIterator;
|
||||||
use BadMethodCallException;
|
use BadMethodCallException;
|
||||||
use Error;
|
use Error;
|
||||||
|
use InvalidArgumentException;
|
||||||
use PHPUnit\Framework\Attributes\DataProvider;
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
use SilverStripe\Dev\SapphireTest;
|
use SilverStripe\Dev\SapphireTest;
|
||||||
use SilverStripe\Model\ArrayData;
|
use SilverStripe\Model\ArrayData;
|
||||||
use SilverStripe\Model\List\ArrayList;
|
use SilverStripe\Model\List\ArrayList;
|
||||||
|
use SilverStripe\Model\ModelData;
|
||||||
use SilverStripe\ORM\FieldType\DBDate;
|
use SilverStripe\ORM\FieldType\DBDate;
|
||||||
use SilverStripe\ORM\FieldType\DBHTMLText;
|
use SilverStripe\ORM\FieldType\DBHTMLText;
|
||||||
use SilverStripe\View\Exception\MissingTemplateException;
|
use SilverStripe\View\Exception\MissingTemplateException;
|
||||||
@ -88,7 +90,7 @@ class ViewLayerDataTest extends SapphireTest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function provideCount(): array
|
public static function provideGetIteratorCount(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'uncountable object' => [
|
'uncountable object' => [
|
||||||
@ -100,8 +102,8 @@ class ViewLayerDataTest extends SapphireTest
|
|||||||
'expected' => 12,
|
'expected' => 12,
|
||||||
],
|
],
|
||||||
'uncountable object - has count field (non-int)' => [
|
'uncountable object - has count field (non-int)' => [
|
||||||
'data' => new ArrayData(['count' => 'aahhh', 'Field2' => 'value2']), // @TODO fix this
|
'data' => new ArrayData(['count' => 'aahhh', 'Field2' => 'value2']),
|
||||||
'expected' => 12,
|
'expected' => 0,
|
||||||
],
|
],
|
||||||
'empty array' => [
|
'empty array' => [
|
||||||
'data' => [],
|
'data' => [],
|
||||||
@ -130,11 +132,11 @@ class ViewLayerDataTest extends SapphireTest
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
#[DataProvider('provideCount')]
|
#[DataProvider('provideGetIteratorCount')]
|
||||||
public function testCount(mixed $data, int $expected): void
|
public function testGetIteratorCount(mixed $data, int $expected): void
|
||||||
{
|
{
|
||||||
$viewLayerData = new ViewLayerData($data);
|
$viewLayerData = new ViewLayerData($data);
|
||||||
$this->assertSame($expected, $viewLayerData->count());
|
$this->assertSame($expected, $viewLayerData->getIteratorCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function provideIsSet(): array
|
public static function provideIsSet(): array
|
||||||
@ -290,6 +292,8 @@ class ViewLayerDataTest extends SapphireTest
|
|||||||
|
|
||||||
public static function provideGetComplex(): array
|
public static function provideGetComplex(): array
|
||||||
{
|
{
|
||||||
|
// Note the actual value checks aren't very comprehensive here because that's done
|
||||||
|
// in more detail in testGetRawDataValue
|
||||||
return [
|
return [
|
||||||
'exception gets thrown if not __call() method' => [
|
'exception gets thrown if not __call() method' => [
|
||||||
'name' => 'badMethodCall',
|
'name' => 'badMethodCall',
|
||||||
@ -500,28 +504,225 @@ class ViewLayerDataTest extends SapphireTest
|
|||||||
$this->assertSame($expected, (string) $viewLayerData);
|
$this->assertSame($expected, (string) $viewLayerData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// public function provideHasDataValue(): array
|
public static function provideHasDataValue(): array
|
||||||
// {
|
{
|
||||||
// return [
|
return [
|
||||||
// [
|
'empty array' => [
|
||||||
|
'data' => [],
|
||||||
|
'name' => null,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'empty ArrayList' => [
|
||||||
|
'data' => new ArrayList(),
|
||||||
|
'name' => null,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'empty ArrayData' => [
|
||||||
|
'data' => new ArrayData(),
|
||||||
|
'name' => null,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'empty ArrayIterator' => [
|
||||||
|
'data' => new ArrayIterator(),
|
||||||
|
'name' => null,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'empty ModelData' => [
|
||||||
|
'data' => new ModelData(),
|
||||||
|
'name' => null,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'non-countable object' => [
|
||||||
|
'data' => new ExtensibleObject(),
|
||||||
|
'name' => null,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'array with data' => [
|
||||||
|
'data' => [1,2,3],
|
||||||
|
'name' => null,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'associative array' => [
|
||||||
|
'data' => ['one' => 1, 'two' => 2],
|
||||||
|
'name' => null,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'ArrayList with data' => [
|
||||||
|
'data' => new ArrayList([1,2,3]),
|
||||||
|
'name' => null,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'ArrayData with data' => [
|
||||||
|
'data' => new ArrayData(['one' => 1, 'two' => 2]),
|
||||||
|
'name' => null,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'ArrayIterator with data' => [
|
||||||
|
'data' => new ArrayIterator([1,2,3]),
|
||||||
|
'name' => null,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'ArrayData missing value' => [
|
||||||
|
'data' => new ArrayData(['one' => 1, 'two' => 2]),
|
||||||
|
'name' => 'three',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'ArrayData with truthy value' => [
|
||||||
|
'data' => new ArrayData(['one' => 1, 'two' => 2]),
|
||||||
|
'name' => 'one',
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'ArrayData with null value' => [
|
||||||
|
'data' => new ArrayData(['nullVal' => null, 'two' => 2]),
|
||||||
|
'name' => 'nullVal',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'ArrayData with falsy value' => [
|
||||||
|
'data' => new ArrayData(['zero' => 0, 'two' => 2]),
|
||||||
|
'name' => 'zero',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'Empty string' => [
|
||||||
|
'data' => '',
|
||||||
|
'name' => null,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'Truthy string' => [
|
||||||
|
'data' => 'has a value',
|
||||||
|
'name' => null,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'Field on a string' => [
|
||||||
|
'data' => 'has a value',
|
||||||
|
'name' => 'SomeField',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// ],
|
#[DataProvider('provideHasDataValue')]
|
||||||
// ];
|
public function testHasDataValue(mixed $data, ?string $name, bool $expected): void
|
||||||
// }
|
{
|
||||||
|
$viewLayerData = new ViewLayerData($data);
|
||||||
|
$this->assertSame($expected, $viewLayerData->hasDataValue($name)); // @TODO call methods with(out) args
|
||||||
|
}
|
||||||
|
|
||||||
// #[DataProvider('provideHasDataValue')]
|
public static function provideGetRawDataValue(): array
|
||||||
// public function testHasDataValue(): void
|
{
|
||||||
// {
|
$dbHtml = (new DBHTMLText())->setValue('Some html text');
|
||||||
|
// Note we're not checking the fetch order or passing args here - see testGet and testCall for that.
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'data' => ['MyField' => 'some value'],
|
||||||
|
'name' => 'MissingField',
|
||||||
|
'expected' => null,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => ['MyField' => null],
|
||||||
|
'name' => 'MyField',
|
||||||
|
'expected' => null,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => ['MyField' => 'some value'],
|
||||||
|
'name' => 'MyField',
|
||||||
|
'expected' => 'some value',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => ['MyField' => 123],
|
||||||
|
'name' => 'MyField',
|
||||||
|
'expected' => 123,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => ['MyField' => true],
|
||||||
|
'name' => 'MyField',
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => ['MyField' => false],
|
||||||
|
'name' => 'MyField',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => ['MyField' => $dbHtml],
|
||||||
|
'name' => 'MyField',
|
||||||
|
'expected' => $dbHtml,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => (new ArrayData(['MyField' => 1234]))->customise(new ArrayData(['MyField' => 'overridden value'])),
|
||||||
|
'name' => 'MyField',
|
||||||
|
'expected' => 'overridden value',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => (new ArrayData(['MyField' => 1234]))->customise(new ArrayData(['FieldTwo' => 'checks here'])),
|
||||||
|
'name' => 'FieldTwo',
|
||||||
|
'expected' => 'checks here',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => (new ArrayData(['MyField' => 1234]))->customise(new ArrayData(['FieldTwo' => 'not here'])),
|
||||||
|
'name' => 'MyField',
|
||||||
|
'expected' => 1234,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// }
|
#[DataProvider('provideGetRawDataValue')]
|
||||||
|
public function testGetRawDataValue(mixed $data, string $name, mixed $expected): void
|
||||||
|
{
|
||||||
|
$viewLayerData = new ViewLayerData($data);
|
||||||
|
$this->assertSame($expected, $viewLayerData->getRawDataValue($name));
|
||||||
|
}
|
||||||
|
|
||||||
// public function testGetRawDataValue(): void
|
public static function provideGetRawDataValueType(): array
|
||||||
// {
|
{
|
||||||
|
// The types aren't currently used, but are passed in so we can use them later
|
||||||
|
// if we find the distinction useful. We should test they do what we expect
|
||||||
|
// in the meantime.
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'type' => 'property',
|
||||||
|
'shouldThrow' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'method',
|
||||||
|
'shouldThrow' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'any',
|
||||||
|
'shouldThrow' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'constant',
|
||||||
|
'shouldThrow' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'randomtext',
|
||||||
|
'shouldThrow' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// }
|
#[DataProvider('provideGetRawDataValueType')]
|
||||||
|
public function testGetRawDataValueType(string $type, bool $shouldThrow): void
|
||||||
|
{
|
||||||
|
$viewLayerData = new ViewLayerData([]);
|
||||||
|
if ($shouldThrow) {
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
} else {
|
||||||
|
$this->expectNotToPerformAssertions();
|
||||||
|
}
|
||||||
|
$viewLayerData->getRawDataValue('something', type: $type);
|
||||||
|
}
|
||||||
|
|
||||||
// public function testCache(): void
|
public function testCache(): void
|
||||||
// {
|
{
|
||||||
|
$data = new ArrayData(['MyField' => 'some value']);
|
||||||
|
$viewLayerData = new ViewLayerData($data);
|
||||||
|
|
||||||
// }
|
// No cache because we haven't fetched anything
|
||||||
|
$this->assertNull($data->objCacheGet('MyField'));
|
||||||
|
|
||||||
|
// Fetching the value caches it
|
||||||
|
$viewLayerData->MyField;
|
||||||
|
$this->assertSame('some value', $data->objCacheGet('MyField'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ use SilverStripe\View\ThemeResourceLoader;
|
|||||||
use SilverStripe\View\ThemeManifest;
|
use SilverStripe\View\ThemeManifest;
|
||||||
use SilverStripe\Model\ModelData;
|
use SilverStripe\Model\ModelData;
|
||||||
use SilverStripe\View\SSViewer_Scope;
|
use SilverStripe\View\SSViewer_Scope;
|
||||||
|
use SilverStripe\View\ViewLayerData;
|
||||||
use Symfony\Component\Translation\Loader\ArrayLoader;
|
use Symfony\Component\Translation\Loader\ArrayLoader;
|
||||||
use Symfony\Component\Translation\Translator;
|
use Symfony\Component\Translation\Translator;
|
||||||
|
|
||||||
@ -73,7 +74,7 @@ trait i18nTestManifest
|
|||||||
{
|
{
|
||||||
// force SSViewer_Scope to cache global template vars before we switch to the
|
// force SSViewer_Scope to cache global template vars before we switch to the
|
||||||
// test-project class manifest (since it will lose visibility of core classes)
|
// test-project class manifest (since it will lose visibility of core classes)
|
||||||
$presenter = new SSViewer_Scope(new ModelData());
|
$presenter = new SSViewer_Scope(new ViewLayerData([]));
|
||||||
unset($presenter);
|
unset($presenter);
|
||||||
|
|
||||||
// Switch to test manifest
|
// Switch to test manifest
|
||||||
|
Loading…
Reference in New Issue
Block a user