Skip to content

Commit

Permalink
API Refactor template layer into its own module
Browse files Browse the repository at this point in the history
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
  • Loading branch information
GuySartorelli committed Sep 26, 2024
1 parent c523022 commit c675d46
Show file tree
Hide file tree
Showing 15 changed files with 658 additions and 669 deletions.
6 changes: 3 additions & 3 deletions src/Dev/Backtrace.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,11 @@ public static function full_func_name($item, $showArgs = false, $argCharLimit =
if ($showArgs && isset($item['args'])) {
$args = [];
foreach ($item['args'] as $arg) {
if (!is_object($arg) || method_exists($arg, '__toString')) {
if (is_object($arg)) {
$args[] = get_class($arg);
} else {
$sarg = is_array($arg) ? 'Array' : strval($arg);
$args[] = (strlen($sarg ?? '') > $argCharLimit) ? substr($sarg, 0, $argCharLimit) . '...' : $sarg;
} else {
$args[] = get_class($arg);
}
}

Expand Down
7 changes: 5 additions & 2 deletions src/Forms/FormField.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use SilverStripe\View\AttributesHTML;
use SilverStripe\View\SSViewer;
use SilverStripe\Model\ModelData;
use SilverStripe\ORM\DataObject;

/**
* Represents a field in a form.
Expand Down Expand Up @@ -458,7 +459,7 @@ public function Value()
*
* By default, makes use of $this->dataValue()
*
* @param ModelData|DataObjectInterface $record Record to save data into
* @param DataObjectInterface $record Record to save data into
*/
public function saveInto(DataObjectInterface $record)
{
Expand All @@ -469,7 +470,9 @@ public function saveInto(DataObjectInterface $record)
if (($pos = strrpos($this->name ?? '', '.')) !== false) {
$relation = substr($this->name ?? '', 0, $pos);
$fieldName = substr($this->name ?? '', $pos + 1);
$component = $record->relObject($relation);
if ($record instanceof DataObject) {
$component = $record->relObject($relation);
}
}

if ($fieldName && $component) {
Expand Down
21 changes: 15 additions & 6 deletions src/Forms/HTMLEditor/HTMLEditorField.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
use SilverStripe\Assets\Shortcodes\ImageShortcodeProvider;
use SilverStripe\Forms\FormField;
use SilverStripe\Forms\TextareaField;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataObjectInterface;
use Exception;
use SilverStripe\Model\ModelData;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\View\CastingService;
use SilverStripe\View\Parsers\HTMLValue;

/**
Expand Down Expand Up @@ -123,13 +125,9 @@ public function getAttributes()
);
}

/**
* @param DataObject|DataObjectInterface $record
* @throws Exception
*/
public function saveInto(DataObjectInterface $record)
{
if ($record->hasField($this->name) && $record->escapeTypeForField($this->name) != 'xml') {
if (!$this->usesXmlFriendlyField($record)) {
throw new Exception(
'HTMLEditorField->saveInto(): This field should save into a HTMLText or HTMLVarchar field.'
);
Expand Down Expand Up @@ -225,4 +223,15 @@ private function setEditorHeight(HTMLEditorConfig $config): HTMLEditorConfig

return $config;
}

private function usesXmlFriendlyField(DataObjectInterface $record): bool
{
if ($record instanceof ModelData && !$record->hasField($this->getName())) {
return true;
}

$castingService = CastingService::singleton();
$castValue = $castingService->cast($this->Value(), $record, $this->getName());
return $castValue instanceof DBField && $castValue::config()->get('escape_type') === 'xml';
}
}
122 changes: 25 additions & 97 deletions src/Model/ModelData.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
use SilverStripe\Core\Convert;
use SilverStripe\Core\Extensible;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Debug;
use SilverStripe\Core\ArrayLib;
use SilverStripe\Model\List\ArrayList;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Model\ArrayData;
use SilverStripe\View\CastingService;
use SilverStripe\View\SSViewer;
use Stringable;
use UnexpectedValueException;

/**
Expand All @@ -29,7 +29,7 @@
* is provided and automatically escaped by ModelData. Any class that needs to be available to a view (controllers,
* {@link DataObject}s, page controls) should inherit from this class.
*/
class ModelData
class ModelData implements Stringable
{
use Extensible {
defineMethods as extensibleDefineMethods;
Expand All @@ -38,7 +38,7 @@ class ModelData
use Configurable;

/**
* An array of objects to cast certain fields to. This is set up as an array in the format:
* An array of DBField classes to cast certain fields to. This is set up as an array in the format:
*
* <code>
* public static $casting = array (
Expand All @@ -47,16 +47,18 @@ class ModelData
* </code>
*/
private static array $casting = [
'CSSClasses' => 'Varchar'
'CSSClasses' => 'Varchar',
'forTemplate' => 'HTMLText',
];

/**
* The default object to cast scalar fields to if casting information is not specified, and casting to an object
* The default class to cast scalar fields to if casting information is not specified, and casting to an object
* is required.
* This can be any injectable service name but must resolve to a DBField subclass.
*
* If null, casting will be determined based on the type of value (e.g. integers will be cast to DBInt)
*/
private static string $default_cast = 'Text';

private static array $casting_cache = [];
private static ?string $default_cast = null;

/**
* Acts as a PHP 8.2+ compliant replacement for dynamic properties
Expand Down Expand Up @@ -305,12 +307,18 @@ public function exists(): bool
return true;
}

public function __toString(): string
{
return $this->forTemplate();
}

/**
* Return the class name (though subclasses may return something else)
* Return the HTML markup that represents this model when it is directly injected into a template (e.g. using $Me).
* By default this attempts to render the model using templates based on the class hierarchy.
*/
public function __toString(): string
public function forTemplate(): string
{
return static::class;
return $this->renderWith($this->getViewerTemplates());
}

public function getCustomisedObj(): ?ModelData
Expand All @@ -326,14 +334,10 @@ public function setCustomisedObj(ModelData $object)
// CASTING ---------------------------------------------------------------------------------------------------------

/**
* Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value object)
* Return the "casting helper" (an injectable service name)
* for a field on this object. This helper will be a subclass of DBField.
*
* @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
*/
public function castingHelper(string $field, bool $useFallback = true): ?string
public function castingHelper(string $field): ?string
{
// Get casting if it has been configured.
// DB fields and PHP methods are all case insensitive so we normalise casing before checking.
Expand All @@ -346,67 +350,15 @@ public function castingHelper(string $field, bool $useFallback = true): ?string
// If no specific cast is declared, fall back to failover.
$failover = $this->getFailover();
if ($failover) {
$cast = $failover->castingHelper($field, $useFallback);
$cast = $failover->castingHelper($field);
if ($cast) {
return $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');
if (empty($default)) {
throw new Exception('No default_cast');
}
return $default;
}

/**
* Get the class name a field on this object will be casted to.
*/
public function castingClass(string $field): string
{
// Strip arguments
$spec = $this->castingHelper($field);
return trim(strtok($spec ?? '', '(') ?? '');
}

/**
* Return the string-format type for the given field.
*
* @return string 'xml'|'raw'
*/
public function escapeTypeForField(string $field): string
{
$class = $this->castingClass($field) ?: $this->config()->get('default_cast');

/** @var DBField $type */
$type = Injector::inst()->get($class, true);
return $type->config()->get('escape_type');
}

// TEMPLATE ACCESS LAYER -------------------------------------------------------------------------------------------

/**
Expand Down Expand Up @@ -496,9 +448,7 @@ protected function objCacheClear()
* that have been specified.
*
* @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.
* Note that if there is a property or method that returns null, a relevant DBField instance will
* be returned.
* property, method, or dynamic data available for that field or if the value is explicitly null.
*/
public function obj(
string $fieldName,
Expand Down Expand Up @@ -532,29 +482,7 @@ public function obj(
return null;
}

// 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 ModelData so templates can handle them
if (is_array($value) && array_is_list($value)) {
$value = ArrayList::create($value);
}

// Fallback on default casting
if (!is_object($value)) {
// Force cast
$castingHelper = $this->defaultCastingHelper($fieldName);
$valueObject = Injector::inst()->create($castingHelper, $fieldName);
$valueObject->setValue($value, $this);
$value = $valueObject;
}
$value = CastingService::singleton()->cast($value, $this, $fieldName);

// Record in cache
if ($cache) {
Expand Down
10 changes: 5 additions & 5 deletions src/ORM/DataObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -3033,7 +3033,7 @@ public function setCastedField($fieldName, $value)
/**
* {@inheritdoc}
*/
public function castingHelper(string $field, bool $useFallback = true): ?string
public function castingHelper(string $field): ?string
{
$fieldSpec = static::getSchema()->fieldSpec(static::class, $field);
if ($fieldSpec) {
Expand All @@ -3051,7 +3051,7 @@ public function castingHelper(string $field, bool $useFallback = true): ?string
}
}

return parent::castingHelper($field, $useFallback);
return parent::castingHelper($field);
}

/**
Expand Down Expand Up @@ -3234,11 +3234,11 @@ public function debug(): string
* - it still returns an object even when the field has no value.
* - it only matches fields and not methods
* - it matches foreign keys generated by has_one relationships, eg, "ParentID"
* - if the field exists, the return value is ALWAYS a DBField instance
*
* @param string $fieldName Name of the field
* @return DBField The field as a DBField object
* Returns null if the field doesn't exist
*/
public function dbObject($fieldName)
public function dbObject(string $fieldName): ?DBField
{
// Check for field in DB
$schema = static::getSchema();
Expand Down
5 changes: 0 additions & 5 deletions src/ORM/FieldType/DBField.php
Original file line number Diff line number Diff line change
Expand Up @@ -520,11 +520,6 @@ public function debug(): string
DBG;
}

public function __toString(): string
{
return (string)$this->forTemplate();
}

public function getArrayValue()
{
return $this->arrayValue;
Expand Down
Loading

0 comments on commit c675d46

Please sign in to comment.