FIX Respect explicit casting before casting arrays (#11271)

This commit is contained in:
Guy Sartorelli 2024-06-11 16:49:27 +12:00 committed by GitHub
parent e7d05aa524
commit b53cda8de0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 66 additions and 20 deletions

View File

@ -521,13 +521,13 @@ class Form extends ViewableData implements HasRequestHandler
return $this; return $this;
} }
public function castingHelper($field) public function castingHelper($field, bool $useFallback = true)
{ {
// Override casting for field message // Override casting for field message
if (strcasecmp($field ?? '', 'Message') === 0 && ($helper = $this->getMessageCastingHelper())) { if (strcasecmp($field ?? '', 'Message') === 0 && ($helper = $this->getMessageCastingHelper())) {
return $helper; return $helper;
} }
return parent::castingHelper($field); return parent::castingHelper($field, $useFallback);
} }
/** /**

View File

@ -790,13 +790,13 @@ class FormField extends RequestHandler
return $form->getSecurityToken()->isEnabled(); return $form->getSecurityToken()->isEnabled();
} }
public function castingHelper($field) public function castingHelper($field, bool $useFallback = true)
{ {
// Override casting for field message // Override casting for field message
if (strcasecmp($field ?? '', 'Message') === 0 && ($helper = $this->getMessageCastingHelper())) { if (strcasecmp($field ?? '', 'Message') === 0 && ($helper = $this->getMessageCastingHelper())) {
return $helper; return $helper;
} }
return parent::castingHelper($field); return parent::castingHelper($field, $useFallback);
} }
/** /**

View File

@ -56,7 +56,7 @@ class ReadonlyField extends FormField
return 'readonly'; return 'readonly';
} }
public function castingHelper($field) public function castingHelper($field, bool $useFallback = true)
{ {
// Get dynamic cast for 'Value' field // Get dynamic cast for 'Value' field
if (strcasecmp($field ?? '', 'Value') === 0) { if (strcasecmp($field ?? '', 'Value') === 0) {
@ -64,7 +64,7 @@ class ReadonlyField extends FormField
} }
// Fall back to default casting // Fall back to default casting
return parent::castingHelper($field); return parent::castingHelper($field, $useFallback);
} }
public function getSchemaStateDefaults() public function getSchemaStateDefaults()

View File

@ -3015,7 +3015,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function castingHelper($field) public function castingHelper($field, bool $useFallback = true)
{ {
$fieldSpec = static::getSchema()->fieldSpec(static::class, $field); $fieldSpec = static::getSchema()->fieldSpec(static::class, $field);
if ($fieldSpec) { if ($fieldSpec) {
@ -3033,7 +3033,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} }
} }
return parent::castingHelper($field); return parent::castingHelper($field, $useFallback);
} }
/** /**

View File

@ -319,14 +319,14 @@ abstract class DBComposite extends DBField
return $fieldObject; return $fieldObject;
} }
public function castingHelper($field) public function castingHelper($field, bool $useFallback = true)
{ {
$fields = $this->compositeDatabaseFields(); $fields = $this->compositeDatabaseFields();
if (isset($fields[$field])) { if (isset($fields[$field])) {
return $fields[$field]; return $fields[$field];
} }
return parent::castingHelper($field); return parent::castingHelper($field, $useFallback);
} }
public function getIndexSpecs() public function getIndexSpecs()

View File

@ -385,10 +385,11 @@ class ViewableData implements IteratorAggregate
* for a field on this object. This helper will be a subclass of DBField. * for a field on this object. This helper will be a subclass of DBField.
* *
* @param string $field * @param string $field
* @return string Casting helper As a constructor pattern, and may include arguments. * @param bool $useFallback If true, fall back on the default casting helper if there isn't an explicit one.
* @return string|null Casting helper As a constructor pattern, and may include arguments.
* @throws Exception * @throws Exception
*/ */
public function castingHelper($field) public function castingHelper($field, bool $useFallback = true)
{ {
// Get casting if it has been configured. // Get casting if it has been configured.
// DB fields and PHP methods are all case insensitive so we normalise casing before checking. // DB fields and PHP methods are all case insensitive so we normalise casing before checking.
@ -399,20 +400,41 @@ class ViewableData implements IteratorAggregate
} }
// If no specific cast is declared, fall back to failover. // If no specific cast is declared, fall back to failover.
// Note that if there is a failover, the default_cast will always
// be drawn from this object instead of the top level object.
$failover = $this->getFailover(); $failover = $this->getFailover();
if ($failover) { if ($failover) {
$cast = $failover->castingHelper($field); $cast = $failover->castingHelper($field, $useFallback);
if ($cast) { if ($cast) {
return $cast; return $cast;
} }
} }
// Fall back to default_cast if ($useFallback) {
return $this->defaultCastingHelper($field);
}
return null;
}
/**
* Return the default "casting helper" for use when no explicit casting helper is defined.
* This helper will be a subclass of DBField. See castingHelper()
*/
protected function defaultCastingHelper(string $field): string
{
// If there is a failover, the default_cast will always
// be drawn from this object instead of the top level object.
$failover = $this->getFailover();
if ($failover) {
$cast = $failover->defaultCastingHelper($field);
if ($cast) {
return $cast;
}
}
// Fall back to raw default_cast
$default = $this->config()->get('default_cast'); $default = $this->config()->get('default_cast');
if (empty($default)) { if (empty($default)) {
throw new Exception("No default_cast"); throw new Exception('No default_cast');
} }
return $default; return $default;
} }
@ -559,15 +581,25 @@ class ViewableData implements IteratorAggregate
$value = $this->$fieldName; $value = $this->$fieldName;
} }
// Try to cast object if we have an explicit cast set
if (!is_object($value)) {
$castingHelper = $this->castingHelper($fieldName, false);
if ($castingHelper !== null) {
$valueObject = Injector::inst()->create($castingHelper, $fieldName);
$valueObject->setValue($value, $this);
$value = $valueObject;
}
}
// Wrap list arrays in ViewableData so templates can handle them // Wrap list arrays in ViewableData so templates can handle them
if (is_array($value) && array_is_list($value)) { if (is_array($value) && array_is_list($value)) {
$value = ArrayList::create($value); $value = ArrayList::create($value);
} }
// Cast object // Fallback on default casting
if (!is_object($value)) { if (!is_object($value)) {
// Force cast // Force cast
$castingHelper = $this->castingHelper($fieldName); $castingHelper = $this->defaultCastingHelper($fieldName);
$valueObject = Injector::inst()->create($castingHelper, $fieldName); $valueObject = Injector::inst()->create($castingHelper, $fieldName);
$valueObject->setValue($value, $this); $valueObject->setValue($value, $this);
$value = $valueObject; $value = $valueObject;

View File

@ -6,6 +6,7 @@ use ReflectionMethod;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\FieldType\DBText;
use SilverStripe\View\ArrayData; use SilverStripe\View\ArrayData;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
use SilverStripe\View\Tests\ViewableDataTest\ViewableDataTestExtension; use SilverStripe\View\Tests\ViewableDataTest\ViewableDataTestExtension;
@ -59,6 +60,9 @@ class ViewableDataTest extends SapphireTest
$this->assertInstanceOf(ViewableDataTest\RequiresCasting::class, $caster->obj('alwaysCasted')); $this->assertInstanceOf(ViewableDataTest\RequiresCasting::class, $caster->obj('alwaysCasted'));
$this->assertInstanceOf(ViewableDataTest\Caster::class, $caster->obj('noCastingInformation')); $this->assertInstanceOf(ViewableDataTest\Caster::class, $caster->obj('noCastingInformation'));
$this->assertInstanceOf(DBText::class, $caster->obj('arrayOne'));
$this->assertInstanceOf(ArrayList::class, $caster->obj('arrayTwo'));
} }
public function testFailoverRequiresCasting() public function testFailoverRequiresCasting()

View File

@ -7,13 +7,13 @@ use SilverStripe\View\ViewableData;
class Castable extends ViewableData implements TestOnly class Castable extends ViewableData implements TestOnly
{ {
private static $default_cast = Caster::class; private static $default_cast = Caster::class;
private static $casting = [ private static $casting = [
'alwaysCasted' => RequiresCasting::class, 'alwaysCasted' => RequiresCasting::class,
'castedUnsafeXML' => UnescapedCaster::class, 'castedUnsafeXML' => UnescapedCaster::class,
'test' => 'Text', 'test' => 'Text',
'arrayOne' => 'Text',
]; ];
public $test = 'test'; public $test = 'test';
@ -25,6 +25,16 @@ class Castable extends ViewableData implements TestOnly
return 'alwaysCasted'; return 'alwaysCasted';
} }
public function arrayOne()
{
return ['value1', 'value2'];
}
public function arrayTwo()
{
return ['value1', 'value2'];
}
public function noCastingInformation() public function noCastingInformation()
{ {
return 'noCastingInformation'; return 'noCastingInformation';