Compare commits

..

7 Commits

Author SHA1 Message Date
Guy Sartorelli
75787d4819
Merge 84556e037ee490cd9515d43c92d51cc713bd3d8b into aa2b8c380e5e3720b92faaa849b33c6c786a80a0 2024-10-01 16:14:45 +13:00
Guy Sartorelli
84556e037e
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-01 16:14:36 +13:00
Guy Sartorelli
aa2b8c380e
FIX Fix NavigateCommandTest and don't try to write null (#11412) 2024-10-01 13:51:33 +13:00
github-actions
27708c7ebb Merge branch '5' into 6 2024-09-30 20:34:56 +00:00
github-actions
483e944601 Merge branch '5.3' into 5 2024-09-30 20:34:55 +00:00
github-actions
d7fa53139f Merge branch '5.2' into 5.3 2024-09-30 20:34:54 +00:00
Guy Sartorelli
862a65eacc
MNT Fix unit tests (#11409) 2024-09-30 18:32:17 +13:00
18 changed files with 399 additions and 279 deletions

View File

@ -27,7 +27,7 @@ class NavigateCommand extends Command
// Handle request and output resonse body // Handle request and output resonse body
$response = $app->handle($request); $response = $app->handle($request);
$output->writeln($response->getBody(), OutputInterface::OUTPUT_RAW); $output->writeln($response->getBody() ?? '', OutputInterface::OUTPUT_RAW);
// Transform HTTP status code into sensible exit code // Transform HTTP status code into sensible exit code
$responseCode = $response->getStatusCode(); $responseCode = $response->getStatusCode();

View File

@ -448,7 +448,7 @@ class ModelData implements Stringable
* that have been specified. * that have been specified.
* *
* @return object|DBField|null The specific object representing the field, or null if there is no * @return object|DBField|null The specific object representing the field, or null if there is no
* property, method, or dynamic data available for that field or if the value is explicitly null. * property, method, or dynamic data available for that field.
*/ */
public function obj( public function obj(
string $fieldName, string $fieldName,
@ -482,7 +482,7 @@ class ModelData implements Stringable
return null; return null;
} }
$value = CastingService::singleton()->cast($value, $this, $fieldName); $value = CastingService::singleton()->cast($value, $this, $fieldName, true);
// Record in cache // Record in cache
if ($cache) { if ($cache) {

View File

@ -2,6 +2,8 @@
namespace SilverStripe\View; namespace SilverStripe\View;
use LogicException;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Model\ArrayData; use SilverStripe\Model\ArrayData;
@ -16,11 +18,18 @@ class CastingService
{ {
use Injectable; use Injectable;
public function cast(mixed $data, null|array|ModelData $source = null, string $fieldName = ''): ?object /**
* 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
* 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.
*/
public function cast(mixed $data, null|array|ModelData $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".
if ($data === null) { if (!$strict && $data === null) {
return null; return null;
} }
@ -39,11 +48,14 @@ class CastingService
// Explicit casts take precedence over array casting // Explicit casts take precedence over array casting
if ($service) { if ($service) {
$castObject = Injector::inst()->create($service, $fieldName); $castObject = Injector::inst()->create($service, $fieldName);
if (!ClassInfo::hasMethod($castObject, 'setValue')) {
throw new LogicException('Explicit casting service must have a setValue method.');
}
$castObject->setValue($data, $source); $castObject->setValue($data, $source);
return $castObject; return $castObject;
} }
// Wrap list arrays in ModelData so templates can handle them // Wrap arrays in ModelData so templates can handle them
if (is_array($data)) { if (is_array($data)) {
return array_is_list($data) ? ArrayList::create($data) : ArrayData::create($data); return array_is_list($data) ? ArrayList::create($data) : ArrayData::create($data);
} }
@ -51,6 +63,9 @@ class CastingService
// Fall back to default casting // Fall back to default casting
$service = $this->defaultService($data, $source, $fieldName); $service = $this->defaultService($data, $source, $fieldName);
$castObject = Injector::inst()->create($service, $fieldName); $castObject = Injector::inst()->create($service, $fieldName);
if (!ClassInfo::hasMethod($castObject, 'setValue')) {
throw new LogicException('Default service must have a setValue method.');
}
$castObject->setValue($data, $source); $castObject->setValue($data, $source);
return $castObject; return $castObject;
} }

View File

@ -247,7 +247,7 @@ class SSTemplateParser extends Parser implements TemplateParser
} }
$res['php'] .= ($sub['ArgumentMode'] == 'default') ? $sub['string_php'] : $res['php'] .= ($sub['ArgumentMode'] == 'default') ? $sub['string_php'] :
str_replace('$$FINAL', 'getOutputValue', $sub['php'] ?? ''); str_replace('$$FINAL', 'getValueAsArgument', $sub['php'] ?? '');
} }
/*!* /*!*
@ -286,9 +286,11 @@ class SSTemplateParser extends Parser implements TemplateParser
if (isset($sub['Call']['CallArguments']) && isset($sub['Call']['CallArguments']['php'])) { if (isset($sub['Call']['CallArguments']) && isset($sub['Call']['CallArguments']['php'])) {
$arguments = $sub['Call']['CallArguments']['php']; $arguments = $sub['Call']['CallArguments']['php'];
$res['php'] .= "->$method('$property', [$arguments], 'method', true)"; $type = ViewLayerData::TYPE_METHOD;
$res['php'] .= "->$method('$property', [$arguments], '$type', true)";
} else { } else {
$res['php'] .= "->$method('$property', [], 'property', true)"; $type = ViewLayerData::TYPE_PROPERTY;
$res['php'] .= "->$method('$property', [], '$type', true)";
} }
} }

View File

@ -572,7 +572,7 @@ class SSTemplateParser extends Parser implements TemplateParser
} }
$res['php'] .= ($sub['ArgumentMode'] == 'default') ? $sub['string_php'] : $res['php'] .= ($sub['ArgumentMode'] == 'default') ? $sub['string_php'] :
str_replace('$$FINAL', 'getOutputValue', $sub['php'] ?? ''); str_replace('$$FINAL', 'getValueAsArgument', $sub['php'] ?? '');
} }
/* Call: Method:Word ( "(" < :CallArguments? > ")" )? */ /* Call: Method:Word ( "(" < :CallArguments? > ")" )? */
@ -777,9 +777,11 @@ class SSTemplateParser extends Parser implements TemplateParser
if (isset($sub['Call']['CallArguments']) && isset($sub['Call']['CallArguments']['php'])) { if (isset($sub['Call']['CallArguments']) && isset($sub['Call']['CallArguments']['php'])) {
$arguments = $sub['Call']['CallArguments']['php']; $arguments = $sub['Call']['CallArguments']['php'];
$res['php'] .= "->$method('$property', [$arguments], 'method', true)"; $type = ViewLayerData::TYPE_METHOD;
$res['php'] .= "->$method('$property', [$arguments], '$type', true)";
} else { } else {
$res['php'] .= "->$method('$property', [], 'property', true)"; $type = ViewLayerData::TYPE_PROPERTY;
$res['php'] .= "->$method('$property', [], '$type', true)";
} }
} }

View File

@ -6,13 +6,9 @@ use ArrayIterator;
use Countable; use Countable;
use InvalidArgumentException; use InvalidArgumentException;
use Iterator; use Iterator;
use LogicException;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Model\List\ArrayList; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Model\ModelData;
use SilverStripe\ORM\FieldType\DBBoolean;
use SilverStripe\ORM\FieldType\DBText;
use SilverStripe\ORM\FieldType\DBFloat;
use SilverStripe\ORM\FieldType\DBInt;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
/** /**
@ -226,26 +222,6 @@ class SSViewer_Scope
) = end($this->itemStack); ) = end($this->itemStack);
} }
public function getObj(string $name, array $arguments = [], string $type = '', bool $cache = false, ?string $cacheName = null): ?ViewLayerData
{
$overlay = $this->getOverlay($name, $arguments);
if ($overlay !== null) {
return $overlay;
}
// @TODO caching
$on = $this->getCurrentItem();
if ($on && isset($on->$name)) {
if ($type === 'method') {
return $on->$name(...$arguments);
}
// property
return $on->$name;
}
return $this->getUnderlay($name, $arguments);
}
/** /**
* Set scope to an intermediate value, which will be used for getting output later on. * Set scope to an intermediate value, which will be used for getting output later on.
*/ */
@ -444,20 +420,61 @@ class SSViewer_Scope
return $retval === null ? '' : $retval->__toString(); return $retval === null ? '' : $retval->__toString();
} }
/**
* Get the value to pass as an argument to a method.
*/
public function getValueAsArgument(string $name, array $arguments = [], string $type = '', bool $cache = false, ?string $cacheName = null): mixed
{
$retval = null;
if ($this->hasOverlay($name)) {
$retval = $this->getOverlay($name, $arguments, true);
} else {
// @TODO caching
$on = $this->getCurrentItem();
if ($on && isset($on->$name)) {
$retval = $on->getRawDataValue($name, $type, $arguments);
}
if ($retval === null) {
$retval = $this->getUnderlay($name, $arguments, true);
}
}
if ($retval instanceof DBField) {
$retval = $retval->getValue(); // Workaround because we're still calling obj in ViewLayerData
}
$this->resetLocalScope();
return $retval;
}
/** /**
* 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 = [], string $type = '', bool $cache = false, ?string $cacheName = null): bool public function hasValue(string $name, array $arguments = [], string $type = '', bool $cache = false, ?string $cacheName = null): bool
{ {
// return $this->getCurrentItem()->hasValue($name, $arguments); // eww // @TODO: look for ways to remove the need to call hasValue (e.g. using isset($this->getCurrentItem()->$name) and an equivalent for over/underlays)
// Kinda need a combination of the above, plus checking over-and-underlays. $retval = null;
$obj = $this->getObj($name, $arguments, $type, $cache, $cacheName); $overlay = $this->getOverlay($name, $arguments);
$this->resetLocalScope(); if ($overlay && $overlay->hasDataValue()) {
if (!$obj) { $retval = true;
return false;
} }
// @TODO: look for ways to remove the need to call this method (e.g. using isset($this->getCurrentItem()->$name) and an equivalent for over/underlays)
return $obj->hasValue(); if ($retval === null) {
$on = $this->getCurrentItem();
if ($on) {
$retval = $on->hasDataValue($name, $arguments);
}
}
if (!$retval) {
$underlay = $this->getUnderlay($name, $arguments);
$retval = $underlay && $underlay->hasDataValue();
}
$this->resetLocalScope();
return $retval;
} }
/** /**
@ -594,21 +611,46 @@ class SSViewer_Scope
); );
} }
private function getOverlay(string $property, array $args) private function getObj(string $name, array $arguments = [], string $type = '', bool $cache = false, ?string $cacheName = null): ?ViewLayerData
{
if ($this->hasOverlay($name)) {
return $this->getOverlay($name, $arguments);
}
// @TODO caching
$on = $this->getCurrentItem();
if ($on && isset($on->$name)) {
if ($type === ViewLayerData::TYPE_METHOD) {
return $on->$name(...$arguments);
}
// property
return $on->$name;
}
return $this->getUnderlay($name, $arguments);
}
private function hasOverlay(string $property): bool
{
$result = $this->processTemplateOverride($property, $this->overlay);
return array_key_exists('value', $result);
}
private function getOverlay(string $property, array $args, bool $getRaw = false): mixed
{ {
$result = $this->processTemplateOverride($property, $this->overlay); $result = $this->processTemplateOverride($property, $this->overlay);
if (array_key_exists('value', $result)) { if (array_key_exists('value', $result)) {
return $this->getInjectedValue($result, $property, $args); return $this->getInjectedValue($result, $property, $args, $getRaw);
} }
return null; return null;
} }
private function getUnderlay(string $property, array $args) private function getUnderlay(string $property, array $args, bool $getRaw = false): mixed
{ {
// Check for a presenter-specific override // Check for a presenter-specific override
$result = $this->processTemplateOverride($property, $this->underlay); $result = $this->processTemplateOverride($property, $this->underlay);
if (array_key_exists('value', $result)) { if (array_key_exists('value', $result)) {
return $this->getInjectedValue($result, $property, $args); return $this->getInjectedValue($result, $property, $args, $getRaw);
} }
// Then for iterator-specific overrides // Then for iterator-specific overrides
@ -628,7 +670,7 @@ class SSViewer_Scope
$implementor->iteratorProperties(0, 1); $implementor->iteratorProperties(0, 1);
} }
return $this->getInjectedValue($source, $property, $args); return $this->getInjectedValue($source, $property, $args, $getRaw);
} }
// And finally for global overrides // And finally for global overrides
@ -636,15 +678,20 @@ class SSViewer_Scope
return $this->getInjectedValue( return $this->getInjectedValue(
SSViewer_Scope::$globalProperties[$property], SSViewer_Scope::$globalProperties[$property],
$property, $property,
$args $args,
$getRaw
); );
} }
return null; return null;
} }
private function getInjectedValue(array|TemplateGlobalProvider|TemplateIteratorProvider $source, string $property, array $params) private function getInjectedValue(
{ array|TemplateGlobalProvider|TemplateIteratorProvider $source,
string $property,
array $params,
bool $getRaw = false
) {
// 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
$value = null; $value = null;
if (isset($source['callable'])) { if (isset($source['callable'])) {
@ -657,6 +704,20 @@ class SSViewer_Scope
); );
} }
return ViewLayerData::create($value); if ($value === null) {
return null;
}
// TemplateGlobalProviders can provide an explicit service to cast to which works outside of the regular cast flow
if (!$getRaw && isset($source['casting'])) {
$castObject = Injector::inst()->create($source['casting'], $property);
if (!ClassInfo::hasMethod($castObject, 'setValue')) {
throw new LogicException('Explicit cast from template global provider must have a setValue method.');
}
$castObject->setValue($value);
$value = $castObject;
}
return $getRaw ? $value : ViewLayerData::create($value);
} }
} }

View File

@ -4,6 +4,7 @@ namespace SilverStripe\View;
use BadMethodCallException; use BadMethodCallException;
use Countable; use Countable;
use InvalidArgumentException;
use IteratorAggregate; use IteratorAggregate;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Injector\Injectable;
@ -15,10 +16,16 @@ class ViewLayerData implements IteratorAggregate, Stringable, Countable
{ {
use Injectable; use Injectable;
private mixed $data; public const TYPE_PROPERTY = 'property';
public const TYPE_METHOD = 'method';
private object $data;
public function __construct(mixed $data, mixed $source = null, string $name = '') public function __construct(mixed $data, mixed $source = null, string $name = '')
{ {
if ($data === null) {
throw new InvalidArgumentException('$data must not be null');
}
if ($data instanceof ViewLayerData) { if ($data instanceof ViewLayerData) {
$data = $data->data; $data = $data->data;
} else { } else {
@ -27,17 +34,25 @@ class ViewLayerData implements IteratorAggregate, Stringable, Countable
$this->data = $data; $this->data = $data;
} }
/**
* 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 count(): int
{ {
// This will throw an exception if the data item isn't Countable, if (is_countable($this->data)) {
// but we have to have this so we can rewind in SSViewer_Scope::next() return count($this->data);
// after getting itemIteratorTotal without throwing an exception. }
// This could be avoided if we just return $this->data->getIterator() in if (ClassInfo::hasMethod($this->data, 'getIterator')) {
// the getIterator() method (or omit that method entirely and let it be return count($this->data->getIterator());
// handled with __call()) but then any time you loop you're using objects }
// that aren't ViewLayerData objects and therefore won't be cast or if (ClassInfo::hasMethod($this->data, 'count')) {
// escaped correctly by Twig. return $this->data->count();
return count($this->data); }
if (isset($this->data->count)) {
return $this->data->count;
}
return 0;
} }
public function getIterator(): Traversable public function getIterator(): Traversable
@ -51,26 +66,12 @@ class ViewLayerData implements IteratorAggregate, Stringable, Countable
if (!is_iterable($iterator)) { if (!is_iterable($iterator)) {
$iterator = $this->data->getIterator(); $iterator = $this->data->getIterator();
} }
$source = $this->data instanceof ModelData ? $this->data : null;
foreach ($iterator as $item) { foreach ($iterator as $item) {
yield ViewLayerData::create($item, $this->data); yield $item === null ? null : ViewLayerData::create($item, $source);
} }
} }
// temporary fix - need to remove later. Can't rely on this 'cause other engines won't be calling it
public function hasValue(?string $name = null, array $arguments = []): bool
{
if ($name) {
if ($this->data instanceof ModelData) {
return $this->data->hasValue($name, $arguments);
}
return isset($this->$name);
}
if ($this->data instanceof ModelData) {
return $this->data->exists();
}
return (bool) $this->data;
}
public function __isset(string $name): bool public function __isset(string $name): bool
{ {
// Might be worth reintroducing the way ss template engine checks if lists/countables "exist" here, // Might be worth reintroducing the way ss template engine checks if lists/countables "exist" here,
@ -86,32 +87,61 @@ class ViewLayerData implements IteratorAggregate, Stringable, Countable
public function __get(string $name): ?ViewLayerData public function __get(string $name): ?ViewLayerData
{ {
if ($this->data instanceof ModelData) { // temporary while I move things across. $value = $this->getRawDataValue($name, ViewLayerData::TYPE_PROPERTY);
$value = $this->data->obj($name); $source = $this->data instanceof ModelData ? $this->data : null;
} else { return ViewLayerData::create($value, $source, $name); // @TODO maybe not return this here, but wrap it again in the next layer? This may not play nicely with twig when passing values into args?
$value = isset($this->data->$name) ? $this->data->$name : null;
}
if ($value === null) {
return null;
}
return ViewLayerData::create($value, $this->data, $name);
} }
public function __call(string $name, array $arguments = []): ?ViewLayerData public function __call(string $name, array $arguments = []): ?ViewLayerData
{ {
if ($this->data instanceof ModelData) { // temporary while I move things across. $value = $this->getRawDataValue($name, ViewLayerData::TYPE_METHOD, $arguments);
$value = $this->data->obj($name, $arguments); $source = $this->data instanceof ModelData ? $this->data : null;
} else { return ViewLayerData::create($value, $source, $name); // @TODO maybe not return this here, but wrap it again in the next layer? This may not play nicely with twig when passing values into args?
$value = ClassInfo::hasMethod($this->data, $name) ? $this->data->$name(...$arguments) : null;
}
if ($value === null) {
return null;
}
return ViewLayerData::create($value, $this->data, $name);
} }
public function __toString(): string public function __toString(): string
{ {
if ($this->data instanceof ModelData) {
return $this->data->forTemplate();
}
return (string) $this->data; return (string) $this->data;
} }
// @TODO We need this right now for the ss template engine, but need to check if
// we can rely on it, since twig won't be calling this at all
public function hasDataValue(?string $name = null, array $arguments = []): bool
{
if ($name) {
if ($this->data instanceof ModelData) {
return $this->data->hasValue($name, $arguments);
}
return isset($this->$name);
}
if ($this->data instanceof ModelData) {
return $this->data->exists();
}
return (bool) $this->data;
}
// @TODO We need this right now for the ss template engine, but need to check if
// we can rely on it, since twig won't be calling this at all
public function getRawDataValue(string $name, string $type, array $arguments = []): mixed
{
if ($type === ViewLayerData::TYPE_PROPERTY) {
if ($this->data instanceof ModelData) { // temporary while I move things across.
return $this->data->obj($name);
}
return isset($this->data->$name) ? $this->data->$name : null;
}
if ($type === ViewLayerData::TYPE_METHOD) {
// If it's not a property it's a method
if ($this->data instanceof ModelData) { // temporary while I move things across.
return $this->data->obj($name, $arguments);
} elseif (ClassInfo::hasMethod($this->data, $name) || method_exists($this->data, '__call')) {
return $this->data->$name(...$arguments);
}
}
throw new InvalidArgumentException('$type must be one of the TYPE_* constant values');
}
} }

View File

@ -13,7 +13,7 @@ use Symfony\Component\Console\Output\BufferedOutput;
class NavigateCommandTest extends SapphireTest class NavigateCommandTest extends SapphireTest
{ {
protected $usesDatabase = false; protected $usesDatabase = true;
public static function provideExecute(): array public static function provideExecute(): array
{ {

View File

@ -12,7 +12,8 @@ class DateFieldDisabledTest extends SapphireTest
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
i18n::set_locale('en_NZ'); // Set to an explicit locale so project-level locale swapping doesn't affect tests
i18n::set_locale('en_US');
DBDatetime::set_mock_now('2011-02-01 8:34:00'); DBDatetime::set_mock_now('2011-02-01 8:34:00');
} }
@ -22,7 +23,7 @@ class DateFieldDisabledTest extends SapphireTest
$actual = DateField_Disabled::create('Test') $actual = DateField_Disabled::create('Test')
->setValue('2011-02-01') ->setValue('2011-02-01')
->Field(); ->Field();
$expected = '<span class="readonly" id="Test">1/02/2011 (today)</span>'; $expected = '<span class="readonly" id="Test">Feb 1, 2011 (today)</span>';
$this->assertEquals($expected, $actual); $this->assertEquals($expected, $actual);
// Test today's date with time // Test today's date with time
@ -38,14 +39,14 @@ class DateFieldDisabledTest extends SapphireTest
$actual = DateField_Disabled::create('Test') $actual = DateField_Disabled::create('Test')
->setValue('2011-01-27') ->setValue('2011-01-27')
->Field(); ->Field();
$expected = '<span class="readonly" id="Test">27/01/2011, 5 days ago</span>'; $expected = '<span class="readonly" id="Test">Jan 27, 2011, 5 days ago</span>';
$this->assertEquals($expected, $actual); $this->assertEquals($expected, $actual);
// Test future // Test future
$actual = DateField_Disabled::create('Test') $actual = DateField_Disabled::create('Test')
->setValue('2011-02-06') ->setValue('2011-02-06')
->Field(); ->Field();
$expected = '<span class="readonly" id="Test">6/02/2011, in 5 days</span>'; $expected = '<span class="readonly" id="Test">Feb 6, 2011, in 5 days</span>';
$this->assertEquals($expected, $actual); $this->assertEquals($expected, $actual);
} }

View File

@ -21,7 +21,8 @@ class DatetimeFieldTest extends SapphireTest
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
i18n::set_locale('en_NZ'); // Set to an explicit locale so project-level locale swapping doesn't affect tests
i18n::set_locale('en_US');
// Fix now to prevent race conditions // Fix now to prevent race conditions
DBDatetime::set_mock_now('2010-04-04'); DBDatetime::set_mock_now('2010-04-04');
$this->timezone = date_default_timezone_get(); $this->timezone = date_default_timezone_get();
@ -141,14 +142,14 @@ class DatetimeFieldTest extends SapphireTest
$datetimeField $datetimeField
->setHTML5(false) ->setHTML5(false)
->setLocale('en_NZ'); ->setLocale('de_DE');
$datetimeField->setSubmittedValue('29/03/2003 11:00:00 pm'); $datetimeField->setSubmittedValue('29/03/2003 23:00:00');
$this->assertEquals($datetimeField->dataValue(), '2003-03-29 23:00:00'); $this->assertEquals('2003-03-29 23:00:00', $datetimeField->dataValue());
// Some localisation packages exclude the ',' in default medium format // Some localisation packages exclude the ',' in default medium format
$this->assertMatchesRegularExpression( $this->assertMatchesRegularExpression(
'#29/03/2003(,)? 11:00:00 (PM|pm)#', '#29.03.2003(,)? 23:00:00#',
$datetimeField->Value(), $datetimeField->Value(),
'User value is formatted, and in user timezone' 'User value is formatted, and in user timezone'
); );

View File

@ -21,7 +21,8 @@ class DBDateTest extends SapphireTest
$this->oldError = error_reporting(); $this->oldError = error_reporting();
// Validate setup // Validate setup
assert(date_default_timezone_get() === 'UTC'); assert(date_default_timezone_get() === 'UTC');
i18n::set_locale('en_NZ'); // Set to an explicit locale so project-level locale swapping doesn't affect tests
i18n::set_locale('en_US');
} }
protected function tearDown(): void protected function tearDown(): void
@ -49,42 +50,42 @@ class DBDateTest extends SapphireTest
public function testNiceDate() public function testNiceDate()
{ {
$this->assertEquals( $this->assertEquals(
'31/03/2008', 'Mar 31, 2008',
DBField::create_field('Date', 1206968400)->Nice(), DBField::create_field('Date', 1206968400)->Nice(),
"Date->Nice() works with timestamp integers" "Date->Nice() works with timestamp integers"
); );
$this->assertEquals( $this->assertEquals(
'30/03/2008', 'Mar 30, 2008',
DBField::create_field('Date', 1206882000)->Nice(), DBField::create_field('Date', 1206882000)->Nice(),
"Date->Nice() works with timestamp integers" "Date->Nice() works with timestamp integers"
); );
$this->assertEquals( $this->assertEquals(
'31/03/2008', 'Mar 31, 2008',
DBField::create_field('Date', '1206968400')->Nice(), DBField::create_field('Date', '1206968400')->Nice(),
"Date->Nice() works with timestamp strings" "Date->Nice() works with timestamp strings"
); );
$this->assertEquals( $this->assertEquals(
'30/03/2008', 'Mar 30, 2008',
DBField::create_field('Date', '1206882000')->Nice(), DBField::create_field('Date', '1206882000')->Nice(),
"Date->Nice() works with timestamp strings" "Date->Nice() works with timestamp strings"
); );
$this->assertEquals( $this->assertEquals(
'4/03/2003', 'Mar 4, 2003',
DBField::create_field('Date', '4.3.2003')->Nice(), DBField::create_field('Date', '4.3.2003')->Nice(),
"Date->Nice() works with D.M.YYYY format" "Date->Nice() works with D.M.YYYY format"
); );
$this->assertEquals( $this->assertEquals(
'4/03/2003', 'Mar 4, 2003',
DBField::create_field('Date', '04.03.2003')->Nice(), DBField::create_field('Date', '04.03.2003')->Nice(),
"Date->Nice() works with DD.MM.YYYY format" "Date->Nice() works with DD.MM.YYYY format"
); );
$this->assertEquals( $this->assertEquals(
'4/03/2003', 'Mar 4, 2003',
DBField::create_field('Date', '2003-3-4')->Nice(), DBField::create_field('Date', '2003-3-4')->Nice(),
"Date->Nice() works with YYYY-M-D format" "Date->Nice() works with YYYY-M-D format"
); );
$this->assertEquals( $this->assertEquals(
'4/03/2003', 'Mar 4, 2003',
DBField::create_field('Date', '2003-03-04')->Nice(), DBField::create_field('Date', '2003-03-04')->Nice(),
"Date->Nice() works with YYYY-MM-DD format" "Date->Nice() works with YYYY-MM-DD format"
); );
@ -108,7 +109,7 @@ class DBDateTest extends SapphireTest
{ {
// iso8601 expects year first, but support year last // iso8601 expects year first, but support year last
$this->assertEquals( $this->assertEquals(
'4/03/2003', 'Mar 4, 2003',
DBField::create_field('Date', '04-03-2003')->Nice(), DBField::create_field('Date', '04-03-2003')->Nice(),
"Date->Nice() works with DD-MM-YYYY format" "Date->Nice() works with DD-MM-YYYY format"
); );
@ -153,32 +154,32 @@ class DBDateTest extends SapphireTest
public function testLongDate() public function testLongDate()
{ {
$this->assertEquals( $this->assertEquals(
'31 March 2008', 'March 31, 2008',
DBField::create_field('Date', 1206968400)->Long(), DBField::create_field('Date', 1206968400)->Long(),
"Date->Long() works with numeric timestamp" "Date->Long() works with numeric timestamp"
); );
$this->assertEquals( $this->assertEquals(
'31 March 2008', 'March 31, 2008',
DBField::create_field('Date', '1206968400')->Long(), DBField::create_field('Date', '1206968400')->Long(),
"Date->Long() works with string timestamp" "Date->Long() works with string timestamp"
); );
$this->assertEquals( $this->assertEquals(
'30 March 2008', 'March 30, 2008',
DBField::create_field('Date', 1206882000)->Long(), DBField::create_field('Date', 1206882000)->Long(),
"Date->Long() works with numeric timestamp" "Date->Long() works with numeric timestamp"
); );
$this->assertEquals( $this->assertEquals(
'30 March 2008', 'March 30, 2008',
DBField::create_field('Date', '1206882000')->Long(), DBField::create_field('Date', '1206882000')->Long(),
"Date->Long() works with numeric timestamp" "Date->Long() works with numeric timestamp"
); );
$this->assertEquals( $this->assertEquals(
'3 April 2003', 'April 3, 2003',
DBField::create_field('Date', '2003-4-3')->Long(), DBField::create_field('Date', '2003-4-3')->Long(),
"Date->Long() works with YYYY-M-D" "Date->Long() works with YYYY-M-D"
); );
$this->assertEquals( $this->assertEquals(
'3 April 2003', 'April 3, 2003',
DBField::create_field('Date', '3.4.2003')->Long(), DBField::create_field('Date', '3.4.2003')->Long(),
"Date->Long() works with D.M.YYYY" "Date->Long() works with D.M.YYYY"
); );
@ -187,7 +188,7 @@ class DBDateTest extends SapphireTest
public function testFull() public function testFull()
{ {
$this->assertEquals( $this->assertEquals(
'Monday, 31 March 2008', 'Monday, March 31, 2008',
DBField::create_field('Date', 1206968400)->Full(), DBField::create_field('Date', 1206968400)->Full(),
"Date->Full() works with timestamp integers" "Date->Full() works with timestamp integers"
); );

View File

@ -15,7 +15,8 @@ class DBDatetimeTest extends SapphireTest
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
i18n::set_locale('en_NZ'); // Set to an explicit locale so project-level locale swapping doesn't affect tests
i18n::set_locale('en_US');
} }
public function testNowWithSystemDate() public function testNowWithSystemDate()
@ -127,23 +128,23 @@ class DBDatetimeTest extends SapphireTest
$date = DBDatetime::create_field('Datetime', '2001-12-11 22:10:59'); $date = DBDatetime::create_field('Datetime', '2001-12-11 22:10:59');
// note: Some localisation packages exclude the ',' in default medium format // note: Some localisation packages exclude the ',' in default medium format
i18n::set_locale('en_NZ'); i18n::set_locale('de_DE');
$this->assertMatchesRegularExpression('#11/12/2001(,)? 10:10 PM#i', $date->Nice()); $this->assertMatchesRegularExpression('#11.12.2001(,)? 22:10#i', $date->Nice());
i18n::set_locale('en_US'); i18n::set_locale('en_US');
$this->assertMatchesRegularExpression('#Dec 11(,)? 2001(,)? 10:10 PM#i', $date->Nice()); $this->assertMatchesRegularExpression('#Dec 11(,)? 2001(,)? 10:10\hPM#iu', $date->Nice());
} }
public function testDate() public function testDate()
{ {
$date = DBDatetime::create_field('Datetime', '2001-12-31 22:10:59'); $date = DBDatetime::create_field('Datetime', '2001-12-31 22:10:59');
$this->assertEquals('31/12/2001', $date->Date()); $this->assertEquals('Dec 31, 2001', $date->Date());
} }
public function testTime() public function testTime()
{ {
$date = DBDatetime::create_field('Datetime', '2001-12-31 22:10:59'); $date = DBDatetime::create_field('Datetime', '2001-12-31 22:10:59');
$this->assertMatchesRegularExpression('#10:10:59 PM#i', $date->Time()); $this->assertMatchesRegularExpression('#10:10:59\hPM#iu', $date->Time());
} }
public function testTime24() public function testTime24()

View File

@ -49,12 +49,12 @@ class DBTimeTest extends SapphireTest
public function testNice() public function testNice()
{ {
$time = DBTime::create_field('Time', '17:15:55'); $time = DBTime::create_field('Time', '17:15:55');
$this->assertMatchesRegularExpression('#5:15:55 PM#i', $time->Nice()); $this->assertMatchesRegularExpression('#5:15:55\hPM#iu', $time->Nice());
} }
public function testShort() public function testShort()
{ {
$time = DBTime::create_field('Time', '17:15:55'); $time = DBTime::create_field('Time', '17:15:55');
$this->assertMatchesRegularExpression('#5:15 PM#i', $time->Short()); $this->assertMatchesRegularExpression('#5:15\hPM#iu', $time->Short());
} }
} }

View File

@ -197,20 +197,6 @@ class EndsWithFilterTest extends SapphireTest
'modifiers' => [], 'modifiers' => [],
'matches' => false, 'matches' => false,
], ],
// These will both evaluate to true because the __toString() method just returns the class name.
// We're testing this scenario because ArrayList might contain arbitrary values
[
'filterValue' => new ArrayData(['SomeField' => 'some value']),
'matchValue' => new ArrayData(['SomeField' => 'some value']),
'modifiers' => [],
'matches' => true,
],
[
'filterValue' => new ArrayData(['SomeField' => 'SoMe VaLuE']),
'matchValue' => new ArrayData(['SomeField' => 'some value']),
'modifiers' => [],
'matches' => true,
],
// case insensitive // case insensitive
[ [
'filterValue' => 'somevalue', 'filterValue' => 'somevalue',

View File

@ -197,20 +197,6 @@ class PartialMatchFilterTest extends SapphireTest
'modifiers' => [], 'modifiers' => [],
'matches' => false, 'matches' => false,
], ],
// These will both evaluate to true because the __toString() method just returns the class name.
// We're testing this scenario because ArrayList might contain arbitrary values
[
'filterValue' => new ArrayData(['SomeField' => 'some value']),
'matchValue' => new ArrayData(['SomeField' => 'some value']),
'modifiers' => [],
'matches' => true,
],
[
'filterValue' => new ArrayData(['SomeField' => 'SoMe VaLuE']),
'matchValue' => new ArrayData(['SomeField' => 'some value']),
'modifiers' => [],
'matches' => true,
],
// case insensitive // case insensitive
[ [
'filterValue' => 'somevalue', 'filterValue' => 'somevalue',

View File

@ -197,20 +197,6 @@ class StartsWithFilterTest extends SapphireTest
'modifiers' => [], 'modifiers' => [],
'matches' => false, 'matches' => false,
], ],
// These will both evaluate to true because the __toString() method just returns the class name.
// We're testing this scenario because ArrayList might contain arbitrary values
[
'filterValue' => new ArrayData(['SomeField' => 'some value']),
'matchValue' => new ArrayData(['SomeField' => 'some value']),
'modifiers' => [],
'matches' => true,
],
[
'filterValue' => new ArrayData(['SomeField' => 'SoMe VaLuE']),
'matchValue' => new ArrayData(['SomeField' => 'some value']),
'modifiers' => [],
'matches' => true,
],
// case insensitive // case insensitive
[ [
'filterValue' => 'somevalue', 'filterValue' => 'somevalue',

View File

@ -360,8 +360,9 @@ SS;
'z<div></div>z', 'z<div></div>z',
$this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLFragment)') $this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLFragment)')
); );
// Don't escape value when passing into a method call
$this->assertEquals( $this->assertEquals(
'z&lt;div&gt;&lt;/div&gt;z', 'z<div></div>z',
$this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLEscaped)') $this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLEscaped)')
); );
} }
@ -1118,57 +1119,59 @@ after'
public function testIncludeWithArguments() public function testIncludeWithArguments()
{ {
$this->assertEquals( $this->assertEquals(
$this->render('<% include SSViewerTestIncludeWithArguments %>'), '<p>[out:Arg1]</p><p>[out:Arg2]</p><p>[out:Arg2.Count]</p>',
'<p>[out:Arg1]</p><p>[out:Arg2]</p><p>[out:Arg2.Count]</p>' $this->render('<% include SSViewerTestIncludeWithArguments %>')
); );
$this->assertEquals( $this->assertEquals(
$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A %>'), '<p>A</p><p>[out:Arg2]</p><p>[out:Arg2.Count]</p>',
'<p>A</p><p>[out:Arg2]</p><p>[out:Arg2.Count]</p>' $this->render('<% include SSViewerTestIncludeWithArguments Arg1=A %>')
); );
$this->assertEquals( $this->assertEquals(
$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A, Arg2=B %>'), '<p>A</p><p>B</p><p></p>',
'<p>A</p><p>B</p><p></p>' $this->render('<% include SSViewerTestIncludeWithArguments Arg1=A, Arg2=B %>')
); );
$this->assertEquals( $this->assertEquals(
$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A Bare String, Arg2=B Bare String %>'), '<p>A Bare String</p><p>B Bare String</p><p></p>',
'<p>A Bare String</p><p>B Bare String</p><p></p>' $this->render('<% include SSViewerTestIncludeWithArguments Arg1=A Bare String, Arg2=B Bare String %>')
); );
$this->assertEquals( $this->assertEquals(
'<p>A</p><p>Bar</p><p></p>',
$this->render( $this->render(
'<% include SSViewerTestIncludeWithArguments Arg1="A", Arg2=$B %>', '<% include SSViewerTestIncludeWithArguments Arg1="A", Arg2=$B %>',
new ArrayData(['B' => 'Bar']) new ArrayData(['B' => 'Bar'])
), )
'<p>A</p><p>Bar</p><p></p>'
); );
$this->assertEquals( $this->assertEquals(
'<p>A</p><p>Bar</p><p></p>',
$this->render( $this->render(
'<% include SSViewerTestIncludeWithArguments Arg1="A" %>', '<% include SSViewerTestIncludeWithArguments Arg1="A" %>',
new ArrayData(['Arg1' => 'Foo', 'Arg2' => 'Bar']) new ArrayData(['Arg1' => 'Foo', 'Arg2' => 'Bar'])
), )
'<p>A</p><p>Bar</p><p></p>'
); );
$this->assertEquals( $this->assertEquals(
$this->render('<% include SSViewerTestIncludeWithArguments Arg1="A", Arg2=0 %>'), '<p>A</p><p>0</p><p></p>',
'<p>A</p><p>0</p><p></p>' $this->render('<% include SSViewerTestIncludeWithArguments Arg1="A", Arg2=0 %>')
); );
$this->assertEquals( $this->assertEquals(
$this->render('<% include SSViewerTestIncludeWithArguments Arg1="A", Arg2=false %>'), '<p>A</p><p></p><p></p>',
'<p>A</p><p></p><p></p>' $this->render('<% include SSViewerTestIncludeWithArguments Arg1="A", Arg2=false %>')
); );
$this->assertEquals( $this->assertEquals(
$this->render('<% include SSViewerTestIncludeWithArguments Arg1="A", Arg2=null %>'), '<p>A</p><p></p><p></p>',
'<p>A</p><p></p><p></p>' // Note Arg2 is explicitly overridden with null
$this->render('<% include SSViewerTestIncludeWithArguments Arg1="A", Arg2=null %>')
); );
$this->assertEquals( $this->assertEquals(
'SomeArg - Foo - Bar - SomeArg',
$this->render( $this->render(
'<% include SSViewerTestIncludeScopeInheritanceWithArgsInLoop Title="SomeArg" %>', '<% include SSViewerTestIncludeScopeInheritanceWithArgsInLoop Title="SomeArg" %>',
new ArrayData( new ArrayData(
@ -1179,19 +1182,19 @@ after'
] ]
)] )]
) )
), )
'SomeArg - Foo - Bar - SomeArg'
); );
$this->assertEquals( $this->assertEquals(
'A - B - A',
$this->render( $this->render(
'<% include SSViewerTestIncludeScopeInheritanceWithArgsInWith Title="A" %>', '<% include SSViewerTestIncludeScopeInheritanceWithArgsInWith Title="A" %>',
new ArrayData(['Item' => new ArrayData(['Title' =>'B'])]) new ArrayData(['Item' => new ArrayData(['Title' =>'B'])])
), )
'A - B - A'
); );
$this->assertEquals( $this->assertEquals(
'A - B - C - B - A',
$this->render( $this->render(
'<% include SSViewerTestIncludeScopeInheritanceWithArgsInNestedWith Title="A" %>', '<% include SSViewerTestIncludeScopeInheritanceWithArgsInNestedWith Title="A" %>',
new ArrayData( new ArrayData(
@ -1202,11 +1205,11 @@ after'
] ]
)] )]
) )
), )
'A - B - C - B - A'
); );
$this->assertEquals( $this->assertEquals(
'A - A - A',
$this->render( $this->render(
'<% include SSViewerTestIncludeScopeInheritanceWithUpAndTop Title="A" %>', '<% include SSViewerTestIncludeScopeInheritanceWithUpAndTop Title="A" %>',
new ArrayData( new ArrayData(
@ -1217,8 +1220,7 @@ after'
] ]
)] )]
) )
), )
'A - A - A'
); );
$data = new ArrayData( $data = new ArrayData(
@ -2202,7 +2204,66 @@ EOC;
} }
} }
public function testCallsWithArguments() public static function provideCallsWithArguments(): array
{
return [
[
'template' => '$Level.output(1)',
'expected' => '1-1',
],
[
'template' => '$Nest.Level.output($Set.First.Number)',
'expected' => '2-1',
],
[
'template' => '<% with $Set %>$Up.Level.output($First.Number)<% end_with %>',
'expected' => '1-1',
],
[
'template' => '<% with $Set %>$Top.Nest.Level.output($First.Number)<% end_with %>',
'expected' => '2-1',
],
[
'template' => '<% loop $Set %>$Up.Nest.Level.output($Number)<% end_loop %>',
'expected' => '2-12-22-32-42-5',
],
[
'template' => '<% loop $Set %>$Top.Level.output($Number)<% end_loop %>',
'expected' => '1-11-21-31-41-5',
],
[
'template' => '<% with $Nest %>$Level.output($Top.Set.First.Number)<% end_with %>',
'expected' => '2-1',
],
[
'template' => '<% with $Level %>$output($Up.Set.Last.Number)<% end_with %>',
'expected' => '1-5',
],
[
'template' => '<% with $Level.forWith($Set.Last.Number) %>$output("hi")<% end_with %>',
'expected' => '5-hi',
],
[
'template' => '<% loop $Level.forLoop($Set.First.Number) %>$Number<% end_loop %>',
'expected' => '!0',
],
[
'template' => '<% with $Nest %>
<% with $Level.forWith($Up.Set.First.Number) %>$output("hi")<% end_with %>
<% end_with %>',
'expected' => '1-hi',
],
[
'template' => '<% with $Nest %>
<% loop $Level.forLoop($Top.Set.Last.Number) %>$Number<% end_loop %>
<% end_with %>',
'expected' => '!0!1!2!3!4',
],
];
}
#[DataProvider('provideCallsWithArguments')]
public function testCallsWithArguments(string $template, string $expected): void
{ {
$data = new ArrayData( $data = new ArrayData(
[ [
@ -2222,28 +2283,7 @@ EOC;
] ]
); );
$tests = [ $this->assertEquals($expected, trim($this->render($template, $data) ?? ''));
'$Level.output(1)' => '1-1',
'$Nest.Level.output($Set.First.Number)' => '2-1',
'<% with $Set %>$Up.Level.output($First.Number)<% end_with %>' => '1-1',
'<% with $Set %>$Top.Nest.Level.output($First.Number)<% end_with %>' => '2-1',
'<% loop $Set %>$Up.Nest.Level.output($Number)<% end_loop %>' => '2-12-22-32-42-5',
'<% loop $Set %>$Top.Level.output($Number)<% end_loop %>' => '1-11-21-31-41-5',
'<% with $Nest %>$Level.output($Top.Set.First.Number)<% end_with %>' => '2-1',
'<% with $Level %>$output($Up.Set.Last.Number)<% end_with %>' => '1-5',
'<% with $Level.forWith($Set.Last.Number) %>$output("hi")<% end_with %>' => '5-hi',
'<% loop $Level.forLoop($Set.First.Number) %>$Number<% end_loop %>' => '!0',
'<% with $Nest %>
<% with $Level.forWith($Up.Set.First.Number) %>$output("hi")<% end_with %>
<% end_with %>' => '1-hi',
'<% with $Nest %>
<% loop $Level.forLoop($Top.Set.Last.Number) %>$Number<% end_loop %>
<% end_with %>' => '!0!1!2!3!4',
];
foreach ($tests as $template => $expected) {
$this->assertEquals($expected, trim($this->render($template, $data) ?? ''));
}
} }
public function testRepeatedCallsAreCached() public function testRepeatedCallsAreCached()

View File

@ -2,23 +2,78 @@
namespace SilverStripe\View\Tests\SSViewerTest; namespace SilverStripe\View\Tests\SSViewerTest;
use SilverStripe\Model\List\ArrayList; use ReflectionClass;
use SilverStripe\Model\ModelData; use SilverStripe\Dev\TestOnly;
use SilverStripe\View\SSViewer_Scope;
use Stringable;
/** /**
* A test fixture that will echo back the template item * A test fixture that will echo back the template item
*/ */
class TestFixture extends ModelData class TestFixture implements TestOnly, Stringable
{ {
protected $name; private ?string $name;
public function __construct($name = null) public function __construct($name = null)
{ {
$this->name = $name; $this->name = $name;
parent::__construct();
} }
private function argedName($fieldName, $arguments) public function __call(string $name, array $arguments = []): static|array|null
{
return $this->getValue($name, $arguments);
}
public function __get(string $name): static|array|null
{
return $this->getValue($name);
}
public function __isset(string $name): bool
{
if (preg_match('/NotSet/i', $name)) {
return false;
}
$reflectionScope = new ReflectionClass(SSViewer_Scope::class);
$globalProperties = $reflectionScope->getStaticPropertyValue('globalProperties');
if (array_key_exists($name, $globalProperties)) {
return false;
}
return true;
}
public function __toString(): string
{
if (preg_match('/NotSet/i', $this->name ?? '')) {
return '';
}
if (preg_match('/Raw/i', $this->name ?? '')) {
return $this->name ?? '';
}
return '[out:' . $this->name . ']';
}
private function getValue(string $name, array $arguments = []): static|array|null
{
$childName = $this->argedName($name, $arguments);
// Special field name Loop### to create a list
if (preg_match('/^Loop([0-9]+)$/', $name ?? '', $matches)) {
$output = [];
for ($i = 0; $i < $matches[1]; $i++) {
$output[] = new TestFixture($childName);
}
return $output;
}
if (preg_match('/NotSet/i', $name)) {
return null;
}
return new TestFixture($childName);
}
private function argedName(string $fieldName, array $arguments): string
{ {
$childName = $this->name ? "$this->name.$fieldName" : $fieldName; $childName = $this->name ? "$this->name.$fieldName" : $fieldName;
if ($arguments) { if ($arguments) {
@ -27,51 +82,4 @@ class TestFixture extends ModelData
return $childName; return $childName;
} }
} }
public function obj(
string $fieldName,
array $arguments = [],
bool $cache = false,
?string $cacheName = null
): ?object {
$childName = $this->argedName($fieldName, $arguments);
// Special field name Loop### to create a list
if (preg_match('/^Loop([0-9]+)$/', $fieldName ?? '', $matches)) {
$output = new ArrayList();
for ($i = 0; $i < $matches[1]; $i++) {
$output->push(new TestFixture($childName));
}
return $output;
} else {
if (preg_match('/NotSet/i', $fieldName ?? '')) {
return null;
} else {
return new TestFixture($childName);
}
}
}
public function XML_val(string $fieldName, array $arguments = [], bool $cache = false): string
{
if (preg_match('/NotSet/i', $fieldName ?? '')) {
return '';
} else {
if (preg_match('/Raw/i', $fieldName ?? '')) {
return $fieldName;
} else {
return '[out:' . $this->argedName($fieldName, $arguments) . ']';
}
}
}
public function hasValue(string $fieldName, array $arguments = [], bool $cache = true): bool
{
return (bool)$this->XML_val($fieldName, $arguments);
}
public function forTemplate(): string
{
return '[out:' . $this->name . ']';
}
} }