From 957e10758a4168c883b12edc2e9a6b537c32b5fe Mon Sep 17 00:00:00 2001 From: Guy Sartorelli Date: Thu, 26 Sep 2024 16:16:04 +1200 Subject: [PATCH] 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 --- src/Control/RSS/RSSFeed_Entry.php | 16 +- src/Core/CustomMethods.php | 45 +- src/Dev/Backtrace.php | 6 +- src/Forms/CheckboxSetField.php | 4 +- src/Forms/ConfirmedPasswordField.php | 4 +- src/Forms/CurrencyField_Disabled.php | 2 +- src/Forms/CurrencyField_Readonly.php | 2 +- src/Forms/DatalessField.php | 8 +- src/Forms/DateField_Disabled.php | 2 +- src/Forms/DropdownField.php | 6 +- src/Forms/FieldGroup.php | 6 +- src/Forms/FieldList.php | 4 +- src/Forms/FileField.php | 4 +- src/Forms/Form.php | 4 +- src/Forms/FormAction.php | 8 +- src/Forms/FormField.php | 31 +- src/Forms/FormScaffolder.php | 4 +- src/Forms/GridField/GridField.php | 8 +- src/Forms/GridField/GridFieldDeleteAction.php | 2 +- src/Forms/GridField/GridFieldExportButton.php | 2 +- src/Forms/GridField/GridFieldImportButton.php | 2 +- src/Forms/GridField/GridFieldPrintButton.php | 2 +- src/Forms/GridField/GridState_Component.php | 2 +- src/Forms/HTMLEditor/HTMLEditorField.php | 25 +- src/Forms/HTMLReadonlyField.php | 2 +- src/Forms/HiddenField.php | 8 +- src/Forms/ListboxField.php | 4 +- src/Forms/LiteralField.php | 8 +- src/Forms/LookupField.php | 4 +- src/Forms/NullableField.php | 6 +- src/Forms/OptionsetField.php | 4 +- src/Forms/PrintableTransformation_TabSet.php | 4 +- src/Forms/SearchableDropdownTrait.php | 2 +- src/Forms/SelectionGroup.php | 4 +- src/Forms/SingleLookupField.php | 4 +- src/Forms/TabSet.php | 2 +- src/Forms/ToggleCompositeField.php | 2 +- src/Forms/TreeDropdownField.php | 8 +- src/Forms/TreeDropdownField_Readonly.php | 4 +- src/Forms/TreeMultiselectField.php | 12 +- src/Forms/TreeMultiselectField_Readonly.php | 4 +- src/Model/List/ListDecorator.php | 4 +- src/Model/ModelData.php | 239 +++------ src/Model/ModelDataCustomised.php | 13 +- src/ORM/DataList.php | 3 +- src/ORM/DataObject.php | 17 +- src/ORM/EagerLoadedList.php | 2 +- src/ORM/FieldType/DBComposite.php | 4 +- src/ORM/FieldType/DBField.php | 5 - src/ORM/FieldType/DBVarchar.php | 2 +- src/ORM/Filters/SearchFilter.php | 2 +- src/ORM/Relation.php | 5 +- src/ORM/UnsavedRelationList.php | 5 +- src/PolyExecution/PolyOutput.php | 3 - src/Security/Member.php | 6 +- src/Security/PermissionCheckboxSetField.php | 12 +- src/View/CastingService.php | 100 ++++ src/View/SSTemplateParser.peg | 42 +- src/View/SSTemplateParser.php | 60 ++- src/View/SSViewer.php | 12 +- src/View/SSViewer_DataPresenter.php | 449 ---------------- src/View/SSViewer_FromString.php | 1 + src/View/SSViewer_Scope.php | 504 ++++++++++++++---- src/View/ViewLayerData.php | 195 +++++++ templates/RightLabelledFieldHolder.ss | 2 +- .../Forms/CheckboxField_holder.ss | 2 +- .../Forms/CheckboxField_holder_small.ss | 2 +- .../SilverStripe/Forms/CompositeField.ss | 4 +- .../Forms/CompositeField_holder.ss | 2 +- .../Forms/CompositeField_holder_small.ss | 4 +- templates/SilverStripe/Forms/FieldGroup.ss | 2 +- .../Forms/FieldGroup_DefaultFieldHolder.ss | 2 +- .../SilverStripe/Forms/FieldGroup_holder.ss | 2 +- .../SilverStripe/Forms/FormField_holder.ss | 2 +- .../Forms/FormField_holder_small.ss | 2 +- .../GridFieldAddExistingAutocompleter.ss | 2 +- .../GridField/GridFieldFilterHeader_Row.ss | 2 +- .../GridField/GridFieldSortableHeader_Row.ss | 2 +- .../HTMLEditorField_Toolbar_viewfile.ss | 2 +- templates/SilverStripe/Forms/Includes/Form.ss | 4 +- .../Forms/OptionsetField_holder.ss | 2 +- .../Includes/CMSMemberLoginForm.ss | 4 +- .../forms/HtmlEditorField_holder_small.ss | 2 +- tests/php/Forms/CheckboxFieldTest.php | 6 +- tests/php/Forms/CheckboxSetFieldTest.php | 4 +- tests/php/Forms/CompositeFieldTest.php | 10 +- .../php/Forms/ConfirmedPasswordFieldTest.php | 6 +- tests/php/Forms/CurrencyFieldDisabledTest.php | 4 +- tests/php/Forms/CurrencyFieldReadonlyTest.php | 6 +- tests/php/Forms/DatalessFieldTest.php | 8 +- tests/php/Forms/DateFieldDisabledTest.php | 14 +- tests/php/Forms/DropdownFieldTest.php | 44 +- tests/php/Forms/FieldListTest.php | 4 +- .../GridField/GridFieldActionMenuTest.php | 4 +- .../GridField/GridFieldDeleteActionTest.php | 4 +- .../GridField/GridFieldEditButtonTest.php | 4 +- .../GridField/GridFieldPaginatorTest.php | 4 +- tests/php/Forms/GridField/GridFieldTest.php | 12 +- tests/php/Forms/GroupedDropdownFieldTest.php | 10 +- .../Forms/HTMLEditor/HTMLEditorFieldTest.php | 8 +- tests/php/Forms/ListboxFieldTest.php | 12 +- tests/php/Forms/LookupFieldTest.php | 16 +- tests/php/Forms/NullableFieldTests.php | 6 +- tests/php/Forms/NumericFieldTest.php | 4 +- tests/php/Forms/OptionsetFieldTest.php | 6 +- .../PrintableTransformationTabSetTest.php | 4 +- .../php/Forms/SearchableDropdownTraitTest.php | 2 +- tests/php/Forms/SelectionGroupTest.php | 8 +- tests/php/Forms/SingleLookupFieldTest.php | 4 +- tests/php/Forms/TextareaFieldTest.php | 8 +- tests/php/Forms/TreeDropdownFieldTest.php | 4 +- tests/php/Forms/TreeMultiselectFieldTest.php | 8 +- tests/php/Model/ModelDataTest.php | 21 +- tests/php/Model/ModelDataTest/NotCached.php | 2 +- tests/php/ORM/Filters/EndsWithFilterTest.php | 14 - .../ORM/Filters/PartialMatchFilterTest.php | 14 - .../php/ORM/Filters/StartsWithFilterTest.php | 14 - tests/php/ORM/LabelFieldTest.php | 2 +- .../PermissionCheckboxSetFieldTest.php | 4 +- tests/php/Security/PermissionTest.php | 6 +- tests/php/View/SSViewerTest.php | 142 +++-- tests/php/View/SSViewerTest/TestFixture.php | 93 ++-- tests/php/i18n/i18nTestManifest.php | 6 +- 123 files changed, 1336 insertions(+), 1228 deletions(-) create mode 100644 src/View/CastingService.php delete mode 100644 src/View/SSViewer_DataPresenter.php create mode 100644 src/View/ViewLayerData.php diff --git a/src/Control/RSS/RSSFeed_Entry.php b/src/Control/RSS/RSSFeed_Entry.php index 1ebaae7e7de..810f2589649 100644 --- a/src/Control/RSS/RSSFeed_Entry.php +++ b/src/Control/RSS/RSSFeed_Entry.php @@ -47,7 +47,7 @@ class RSSFeed_Entry extends ModelData */ public function __construct($entry, $titleField, $descriptionField, $authorField) { - $this->failover = $entry; + $this->setFailover($entry); $this->titleField = $titleField; $this->descriptionField = $descriptionField; $this->authorField = $authorField; @@ -58,9 +58,9 @@ public function __construct($entry, $titleField, $descriptionField, $authorField /** * Get the description of this entry * - * @return DBField Returns the description of the entry. + * @return DBField|null Returns the description of the entry. */ - public function Title() + public function getTitle() { return $this->rssField($this->titleField); } @@ -68,9 +68,9 @@ public function Title() /** * Get the description of this entry * - * @return DBField Returns the description of the entry. + * @return DBField|null Returns the description of the entry. */ - public function Description() + public function getDescription() { $description = $this->rssField($this->descriptionField); @@ -85,9 +85,9 @@ public function Description() /** * Get the author of this entry * - * @return DBField Returns the author of the entry. + * @return DBField|null Returns the author of the entry. */ - public function Author() + public function getAuthor() { return $this->rssField($this->authorField); } @@ -96,7 +96,7 @@ public function Author() * Return the safely casted field * * @param string $fieldName Name of field - * @return DBField + * @return DBField|null */ public function rssField($fieldName) { diff --git a/src/Core/CustomMethods.php b/src/Core/CustomMethods.php index 97f84f07d64..e6edc573894 100644 --- a/src/Core/CustomMethods.php +++ b/src/Core/CustomMethods.php @@ -36,6 +36,8 @@ trait CustomMethods */ protected static $built_in_methods = []; + protected array $extraMethodsForInstance = []; + /** * Attempts to locate and call a method dynamically added to a class at runtime if a default cannot be located * @@ -175,7 +177,7 @@ protected function getExtraMethodConfig($method) $this->defineMethods(); } - return self::class::$extra_methods[$lowerClass][strtolower($method)] ?? null; + return $this->extraMethodsForInstance[strtolower($method)] ?? self::class::$extra_methods[$lowerClass][strtolower($method)] ?? null; } /** @@ -190,6 +192,9 @@ public function allMethodNames($custom = false) // Query extra methods $lowerClass = strtolower(static::class); + if ($custom && !empty($this->extraMethodsForInstance)) { + $methods = array_merge($this->extraMethodsForInstance, $methods); + } if ($custom && isset(self::class::$extra_methods[$lowerClass])) { $methods = array_merge(self::class::$extra_methods[$lowerClass], $methods); } @@ -256,7 +261,7 @@ protected function findMethodsFrom($object) * @param string|int $index an index to use if the property is an array * @throws InvalidArgumentException */ - protected function addMethodsFrom($property, $index = null) + protected function addMethodsFrom($property, $index = null, bool $static = true) { $class = static::class; $object = ($index !== null) ? $this->{$property}[$index] : $this->$property; @@ -280,10 +285,18 @@ protected function addMethodsFrom($property, $index = null) // Merge with extra_methods $lowerClass = strtolower($class); - if (isset(self::class::$extra_methods[$lowerClass])) { - self::class::$extra_methods[$lowerClass] = array_merge(self::class::$extra_methods[$lowerClass], $newMethods); + if ($static) { + if (isset(self::class::$extra_methods[$lowerClass])) { + self::class::$extra_methods[$lowerClass] = array_merge(self::class::$extra_methods[$lowerClass], $newMethods); + } else { + self::class::$extra_methods[$lowerClass] = $newMethods; + } } else { - self::class::$extra_methods[$lowerClass] = $newMethods; + if (!empty($this->extraMethodsForInstance)) { + $this->extraMethodsForInstance = array_merge($this->extraMethodsForInstance, $newMethods); + } else { + $this->extraMethodsForInstance = $newMethods; + } } } @@ -293,7 +306,7 @@ protected function addMethodsFrom($property, $index = null) * @param string $property the property name * @param string|int $index an index to use if the property is an array */ - protected function removeMethodsFrom($property, $index = null) + protected function removeMethodsFrom($property, $index = null, bool $static = true) { $extension = ($index !== null) ? $this->{$property}[$index] : $this->$property; $class = static::class; @@ -310,12 +323,22 @@ protected function removeMethodsFrom($property, $index = null) } $methods = $this->findMethodsFrom($extension); - // Unset by key - self::class::$extra_methods[$lowerClass] = array_diff_key(self::class::$extra_methods[$lowerClass], $methods); + if ($static) { + // Unset by key + self::class::$extra_methods[$lowerClass] = array_diff_key(self::class::$extra_methods[$lowerClass], $methods); - // Clear empty list - if (empty(self::class::$extra_methods[$lowerClass])) { - unset(self::class::$extra_methods[$lowerClass]); + // Clear empty list + if (empty(self::class::$extra_methods[$lowerClass])) { + unset(self::class::$extra_methods[$lowerClass]); + } + } else { + // Unset by key + $this->extraMethodsForInstance = array_diff_key($this->extraMethodsForInstance, $methods); + + // Clear empty list + if (empty($this->extraMethodsForInstance)) { + unset($this->extraMethodsForInstance); + } } } diff --git a/src/Dev/Backtrace.php b/src/Dev/Backtrace.php index 62d402efc51..9aa7b85ad81 100644 --- a/src/Dev/Backtrace.php +++ b/src/Dev/Backtrace.php @@ -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); } } diff --git a/src/Forms/CheckboxSetField.php b/src/Forms/CheckboxSetField.php index 6f11db35799..6386564f187 100644 --- a/src/Forms/CheckboxSetField.php +++ b/src/Forms/CheckboxSetField.php @@ -46,13 +46,13 @@ class CheckboxSetField extends MultiSelectField * @param array $properties * @return DBHTMLText */ - public function Field($properties = []) + public function renderField($properties = []) { $properties = array_merge($properties, [ 'Options' => $this->getOptions() ]); - return FormField::Field($properties); + return FormField::renderField($properties); } /** diff --git a/src/Forms/ConfirmedPasswordField.php b/src/Forms/ConfirmedPasswordField.php index fa61dc91a38..d990a28fa9b 100644 --- a/src/Forms/ConfirmedPasswordField.php +++ b/src/Forms/ConfirmedPasswordField.php @@ -195,7 +195,7 @@ public function setTitle($title) * * @return string */ - public function Field($properties = []) + public function renderField($properties = []) { // Build inner content $fieldContent = ''; @@ -209,7 +209,7 @@ public function Field($properties = []) } } - $fieldContent .= $field->FieldHolder(['AttributesHTML' => $this->getAttributesHTMLForChild($field)]); + $fieldContent .= $field->renderFieldHolder(['AttributesHTML' => $this->getAttributesHTMLForChild($field)]); } if (!$this->showOnClick) { diff --git a/src/Forms/CurrencyField_Disabled.php b/src/Forms/CurrencyField_Disabled.php index 4b26a7065d0..c5963b479a9 100644 --- a/src/Forms/CurrencyField_Disabled.php +++ b/src/Forms/CurrencyField_Disabled.php @@ -19,7 +19,7 @@ class CurrencyField_Disabled extends CurrencyField * @param array $properties * @return string */ - public function Field($properties = []) + public function renderField($properties = []) { if ($this->value) { $val = Convert::raw2xml($this->value); diff --git a/src/Forms/CurrencyField_Readonly.php b/src/Forms/CurrencyField_Readonly.php index d9bb4037fcf..28280f18ed7 100644 --- a/src/Forms/CurrencyField_Readonly.php +++ b/src/Forms/CurrencyField_Readonly.php @@ -17,7 +17,7 @@ class CurrencyField_Readonly extends ReadonlyField * @param array $properties * @return string */ - public function Field($properties = []) + public function renderField($properties = []) { $currencySymbol = DBCurrency::config()->get('currency_symbol'); if ($this->value) { diff --git a/src/Forms/DatalessField.php b/src/Forms/DatalessField.php index 0bddbc657a1..21d7a0fd1b4 100644 --- a/src/Forms/DatalessField.php +++ b/src/Forms/DatalessField.php @@ -42,9 +42,9 @@ public function getAttributes() * @param array $properties * @return DBHTMLText */ - public function FieldHolder($properties = []) + public function renderFieldHolder($properties = []) { - return $this->Field($properties); + return $this->renderField($properties); } /** @@ -54,9 +54,9 @@ public function FieldHolder($properties = []) * @param array $properties * @return DBHTMLText */ - public function SmallFieldHolder($properties = []) + public function renderSmallFieldHolder($properties = []) { - return $this->Field($properties); + return $this->renderField($properties); } /** diff --git a/src/Forms/DateField_Disabled.php b/src/Forms/DateField_Disabled.php index 6bdc64f882e..113ae0bb219 100644 --- a/src/Forms/DateField_Disabled.php +++ b/src/Forms/DateField_Disabled.php @@ -14,7 +14,7 @@ class DateField_Disabled extends DateField protected $disabled = true; - public function Field($properties = []) + public function renderField($properties = []) { // Default display value $displayValue = '(' . _t('SilverStripe\\Forms\\DateField.NOTSET', 'not set') . ')'; diff --git a/src/Forms/DropdownField.php b/src/Forms/DropdownField.php index ed5da300034..2dfba72b8c3 100644 --- a/src/Forms/DropdownField.php +++ b/src/Forms/DropdownField.php @@ -68,7 +68,7 @@ * DropdownField::create( * 'Country', * 'Country', - * singleton(MyObject::class)->dbObject('Country')->enumValues() + * singleton(MyObject::class)->dbObject('Country')?->enumValues() * ); * * @@ -128,7 +128,7 @@ public function getHasEmptyDefault() * @param array $properties * @return string */ - public function Field($properties = []) + public function renderField($properties = []) { $options = []; @@ -141,6 +141,6 @@ public function Field($properties = []) 'Options' => new ArrayList($options) ]); - return parent::Field($properties); + return parent::renderField($properties); } } diff --git a/src/Forms/FieldGroup.php b/src/Forms/FieldGroup.php index 9a0d6c67588..1a14021a78c 100644 --- a/src/Forms/FieldGroup.php +++ b/src/Forms/FieldGroup.php @@ -9,8 +9,8 @@ * Lets you include a nested group of fields inside a template. * This control gives you more flexibility over form layout. * - * Note: the child fields within a field group aren't rendered using FieldHolder(). Instead, - * SmallFieldHolder() is called, which just prefixes $Field with a