From fe46daa04fa5dbd28a0de1641512313d57ac769e Mon Sep 17 00:00:00 2001 From: Steve Boyd Date: Tue, 24 Sep 2024 19:44:55 +1200 Subject: [PATCH] NEW DBField validation --- _config/model.yml | 2 + src/Control/Email/Email.php | 3 +- src/Control/RSS/RSSFeed_Entry.php | 1 + src/Forms/ConfirmedPasswordField.php | 3 +- src/Forms/EmailField.php | 28 +- src/Forms/FormField.php | 32 +- src/Forms/FormRequestHandler.php | 8 +- src/Forms/GridField/GridField.php | 7 +- src/Forms/GridField/GridFieldLevelup.php | 3 +- src/Forms/LookupField.php | 3 +- src/Forms/SearchableLookupField.php | 1 + src/Forms/SelectionGroup.php | 3 +- src/Forms/SingleLookupField.php | 3 +- src/Forms/TextField.php | 4 + src/Model/ModelData.php | 31 +- src/Model/ModelFields/EmailModelField.php | 25 + src/Model/ModelFields/ModelField.php | 249 ++++++++ src/Model/ModelFields/StringModelField.php | 142 +++++ src/ORM/Connect/DBSchemaManager.php | 1 + src/ORM/DataObject.php | 51 +- src/ORM/DataObjectSchema.php | 1 + src/ORM/EagerLoadedList.php | 1 + src/ORM/FieldType/DBBoolean.php | 7 +- src/ORM/FieldType/DBComposite.php | 9 +- src/ORM/FieldType/DBDate.php | 7 +- src/ORM/FieldType/DBDatetime.php | 4 +- src/ORM/FieldType/DBDecimal.php | 6 +- src/ORM/FieldType/DBEmail.php | 44 ++ src/ORM/FieldType/DBField.php | 562 +----------------- src/ORM/FieldType/DBFieldHelper.php | 37 ++ src/ORM/FieldType/DBFieldTrait.php | 345 +++++++++++ src/ORM/FieldType/DBFloat.php | 7 +- src/ORM/FieldType/DBInt.php | 6 +- src/ORM/FieldType/DBString.php | 137 +---- src/ORM/FieldType/DBTime.php | 7 +- src/ORM/FieldType/DBYear.php | 7 +- src/ORM/Filters/SearchFilter.php | 1 + src/ORM/Hierarchy/MarkedSet.php | 1 + src/ORM/ManyManyList.php | 1 + src/ORM/Relation.php | 1 + src/ORM/UnsavedRelationList.php | 1 + src/Security/CMSSecurity.php | 3 +- .../ChangePasswordHandler.php | 7 +- .../LostPasswordHandler.php | 5 +- src/Security/Security.php | 5 +- src/View/Requirements_Backend.php | 6 +- src/View/SSViewer.php | 4 +- src/View/SSViewer_DataPresenter.php | 3 +- src/View/SSViewer_FromString.php | 3 +- src/View/SSViewer_Scope.php | 1 + .../Shortcodes/EmbedShortcodeProvider.php | 3 +- tests/php/ORM/DBFieldTest/TestDbField.php | 1 + .../MockDynamicAssignmentDBField.php | 1 + 53 files changed, 1070 insertions(+), 764 deletions(-) create mode 100644 src/Model/ModelFields/EmailModelField.php create mode 100644 src/Model/ModelFields/ModelField.php create mode 100644 src/Model/ModelFields/StringModelField.php create mode 100644 src/ORM/FieldType/DBEmail.php create mode 100644 src/ORM/FieldType/DBFieldHelper.php create mode 100644 src/ORM/FieldType/DBFieldTrait.php diff --git a/_config/model.yml b/_config/model.yml index 4046feddb65..8e6840244c6 100644 --- a/_config/model.yml +++ b/_config/model.yml @@ -20,6 +20,8 @@ SilverStripe\Core\Injector\Injector: class: SilverStripe\ORM\FieldType\DBDecimal Double: class: SilverStripe\ORM\FieldType\DBDouble + Email: + class: SilverStripe\ORM\FieldType\DBEmail Enum: class: SilverStripe\ORM\FieldType\DBEnum Float: diff --git a/src/Control/Email/Email.php b/src/Control/Email/Email.php index c7c350906b3..e04847f2f0a 100644 --- a/src/Control/Email/Email.php +++ b/src/Control/Email/Email.php @@ -13,6 +13,7 @@ use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Injector\Injector; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; use SilverStripe\Model\ArrayData; use SilverStripe\View\Requirements; use SilverStripe\View\SSViewer; @@ -509,7 +510,7 @@ private function updateHtmlAndTextWithRenderedTemplates(bool $plainOnly = false) // Plain render fallbacks to using the html render with html tags removed if (!$plainRender && $htmlRender) { // call html_entity_decode() to ensure any encoded HTML is also stripped inside ->Plain() - $dbField = DBField::create_field('HTMLFragment', html_entity_decode($htmlRender)); + $dbField = DBFieldHelper::create_field('HTMLFragment', html_entity_decode($htmlRender)); $plainRender = $dbField->Plain(); } diff --git a/src/Control/RSS/RSSFeed_Entry.php b/src/Control/RSS/RSSFeed_Entry.php index 1ebaae7e7de..b5c756a0871 100644 --- a/src/Control/RSS/RSSFeed_Entry.php +++ b/src/Control/RSS/RSSFeed_Entry.php @@ -4,6 +4,7 @@ use SilverStripe\Control\Director; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\Model\ModelData; use BadMethodCallException; diff --git a/src/Forms/ConfirmedPasswordField.php b/src/Forms/ConfirmedPasswordField.php index fa61dc91a38..fd58fb64088 100644 --- a/src/Forms/ConfirmedPasswordField.php +++ b/src/Forms/ConfirmedPasswordField.php @@ -6,6 +6,7 @@ use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; use SilverStripe\Security\Authenticator; use SilverStripe\Security\Security; use SilverStripe\View\HTML; @@ -771,6 +772,6 @@ private function getAttributesHTMLForChild(FormField $child): DBField if (strpos($attributes, 'required="required"') === false && $this->Required()) { $attributes .= ' required="required" aria-required="true"'; } - return DBField::create_field('HTMLFragment', $attributes); + return DBFieldHelper::create_field('HTMLFragment', $attributes); } } diff --git a/src/Forms/EmailField.php b/src/Forms/EmailField.php index 68ea66d4199..0ec6d055f07 100644 --- a/src/Forms/EmailField.php +++ b/src/Forms/EmailField.php @@ -2,6 +2,9 @@ namespace SilverStripe\Forms; +use SilverStripe\Core\Validation\ConstraintValidator; +use Symfony\Component\Validator\Constraints; + /** * Text input field with validation for correct email format according to RFC 2822. */ @@ -18,32 +21,27 @@ public function Type() } /** - * Validates for RFC 2822 compliant email addresses. - * - * @see http://www.regular-expressions.info/email.html - * @see http://www.ietf.org/rfc/rfc2822.txt - * * @param Validator $validator * * @return string */ public function validate($validator) { - $result = true; $this->value = trim($this->value ?? ''); - $pattern = '^[a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$'; - - // Escape delimiter characters. - $safePattern = str_replace('/', '\\/', $pattern ?? ''); - - if ($this->value && !preg_match('/' . $safePattern . '/i', $this->value ?? '')) { + $result = true; + // $message = _t('SilverStripe\\Forms\\EmailField.VALIDATION', 'Please enter an email address'); + // $validationResult = ConstraintValidator::validate( + // $this->value, + // new Constraints\Email(message: $message) + // ); + $validationResult = $this->getModelField()->validate(); + + if (!$validationResult->isValid()) { $validator->validationError( $this->name, - _t('SilverStripe\\Forms\\EmailField.VALIDATION', 'Please enter an email address'), - 'validation' + $validationResult->getMessages()[0]['message'], ); - $result = false; } diff --git a/src/Forms/FormField.php b/src/Forms/FormField.php index 0d210436b0f..acf78790c70 100644 --- a/src/Forms/FormField.php +++ b/src/Forms/FormField.php @@ -10,11 +10,16 @@ use SilverStripe\Core\Convert; use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\Core\Validation\ValidationResult; use SilverStripe\View\AttributesHTML; use SilverStripe\View\SSViewer; use SilverStripe\Model\ModelData; +use SilverStripe\Model\ModelFields\ModelField; +use SilverStripe\Model\ModelFields\ModelFieldTrait; +use SilverStripe\Model\ModelFields\StringModelField; +use SilverStripe\Core\Injector\Injector; /** * Represents a field in a form. @@ -40,11 +45,28 @@ * including both structure (name, id, attributes, etc.) and state (field value). * Can be used by for JSON data which is consumed by a front-end application. */ -class FormField extends RequestHandler +abstract class FormField extends RequestHandler { use AttributesHTML; use FormMessage; + // todo: possibly should make this abstract + protected string $modelFieldClass = StringModelField::class; + + private ModelField $modelField; + + protected function getModelField(): ModelField + { + return $this->modelField; + } + + private function initModelField(): void + { + $name = $this->getName(); + $modelField = Injector::inst()->createWithArgs($this->modelFieldClass, [$name]); + $this->modelField = $modelField; + } + /** @see $schemaDataType */ const SCHEMA_DATA_TYPE_STRING = 'String'; @@ -344,6 +366,7 @@ public function __construct($name, $title = null, $value = null) parent::__construct(); + $this->initModelField($name); $this->setupDefaultClasses(); } @@ -929,7 +952,7 @@ public function Field($properties = []) // Trim whitespace from the result, so that trailing newlines are suppressed. Works for strings and HTMLText values if (is_string($result)) { $result = trim($result ?? ''); - } elseif ($result instanceof DBField) { + } elseif ($result instanceof ModelField) { $result->setValue(trim($result->getValue() ?? '')); } @@ -1231,9 +1254,12 @@ protected function extendValidationResult(bool $result, Validator $validator): b } /** - * Abstract method each {@link FormField} subclass must implement, determines whether the field + * Method each {@link FormField} subclass can implement, determines whether the field * is valid or not based on the value. * + * Subclass methods should call $this->extendValidationResult(true, $validator) + * at the end of the method + * * @param Validator $validator * @return bool */ diff --git a/src/Forms/FormRequestHandler.php b/src/Forms/FormRequestHandler.php index d2125881116..0ad7d60c24c 100644 --- a/src/Forms/FormRequestHandler.php +++ b/src/Forms/FormRequestHandler.php @@ -216,10 +216,10 @@ public function httpSubmission($request) // Action handlers may throw ValidationExceptions. try { // Or we can use the Validator attached to the form - $result = $this->form->validationResult(); - if (!$result->isValid()) { - return $this->getValidationErrorResponse($result); - } + // $result = $this->form->validationResult(); + // if (!$result->isValid()) { + // return $this->getValidationErrorResponse($result); + // } // First, try a handler method on the controller (has been checked for allowed_actions above already) $controller = $this->form->getController(); diff --git a/src/Forms/GridField/GridField.php b/src/Forms/GridField/GridField.php index 12b2522c087..7570d5a8d7e 100644 --- a/src/Forms/GridField/GridField.php +++ b/src/Forms/GridField/GridField.php @@ -22,6 +22,7 @@ use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; use SilverStripe\Model\List\Filterable; use SilverStripe\Model\List\Limitable; use SilverStripe\Model\List\Sortable; @@ -382,14 +383,14 @@ public function getCastedValue($value, $castingDefinition) if (strpos($castingDefinition ?? '', '->') === false) { $castingFieldType = $castingDefinition; - $castingField = DBField::create_field($castingFieldType, $value); + $castingField = DBFieldHelper::create_field($castingFieldType, $value); return call_user_func_array([$castingField, 'XML'], $castingParams ?? []); } list($castingFieldType, $castingMethod) = explode('->', $castingDefinition ?? ''); - $castingField = DBField::create_field($castingFieldType, $value); + $castingField = DBFieldHelper::create_field($castingFieldType, $value); return call_user_func_array([$castingField, $castingMethod], $castingParams ?? []); } @@ -543,6 +544,8 @@ public function FieldHolder($properties = []) 'footer' => '', ]; + $_c = $this->getComponents(); + foreach ($this->getComponents() as $item) { if ($item instanceof GridField_HTMLProvider) { $fragments = $item->getHTMLFragments($this); diff --git a/src/Forms/GridField/GridFieldLevelup.php b/src/Forms/GridField/GridFieldLevelup.php index dbadc7354a8..7fa5167a238 100644 --- a/src/Forms/GridField/GridFieldLevelup.php +++ b/src/Forms/GridField/GridFieldLevelup.php @@ -5,6 +5,7 @@ use LogicException; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; use SilverStripe\ORM\Hierarchy\Hierarchy; use SilverStripe\Model\ArrayData; use SilverStripe\View\HTML; @@ -91,7 +92,7 @@ public function getHTMLFragments($gridField) $linkTag = HTML::createTag('a', $attrs); $forTemplate = new ArrayData([ - 'UpLink' => DBField::create_field('HTMLFragment', $linkTag) + 'UpLink' => DBFieldHelper::create_field('HTMLFragment', $linkTag) ]); $template = SSViewer::get_templates_by_class($this, '', __CLASS__); diff --git a/src/Forms/LookupField.php b/src/Forms/LookupField.php index ac388fc394b..6609079fd13 100644 --- a/src/Forms/LookupField.php +++ b/src/Forms/LookupField.php @@ -6,6 +6,7 @@ use SilverStripe\Core\ArrayLib; use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; /** * Read-only complement of {@link MultiSelectField}. @@ -58,7 +59,7 @@ public function Field($properties = []) } $properties = array_merge($properties, [ - 'AttrValue' => DBField::create_field('HTMLFragment', $attrValue), + 'AttrValue' => DBFieldHelper::create_field('HTMLFragment', $attrValue), 'InputValue' => $inputValue ]); diff --git a/src/Forms/SearchableLookupField.php b/src/Forms/SearchableLookupField.php index 610d2385f30..1eaf84b841c 100644 --- a/src/Forms/SearchableLookupField.php +++ b/src/Forms/SearchableLookupField.php @@ -6,6 +6,7 @@ use SilverStripe\Core\ArrayLib; use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; use SilverStripe\Model\List\ArrayList; use SilverStripe\ORM\DataList; diff --git a/src/Forms/SelectionGroup.php b/src/Forms/SelectionGroup.php index d814960f6bb..db460c57ba9 100644 --- a/src/Forms/SelectionGroup.php +++ b/src/Forms/SelectionGroup.php @@ -4,6 +4,7 @@ use SilverStripe\Model\List\ArrayList; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; use SilverStripe\View\HTML; /** @@ -91,7 +92,7 @@ public function FieldList() $itemID = $this->ID() . '_' . (++$count); $extra = [ - "RadioButton" => DBField::create_field('HTMLFragment', HTML::createTag( + "RadioButton" => DBFieldHelper::create_field('HTMLFragment', HTML::createTag( 'input', [ 'class' => 'selector', diff --git a/src/Forms/SingleLookupField.php b/src/Forms/SingleLookupField.php index 5590574caa1..6c812199ccc 100644 --- a/src/Forms/SingleLookupField.php +++ b/src/Forms/SingleLookupField.php @@ -6,6 +6,7 @@ use SilverStripe\ORM\DataObjectInterface; use SilverStripe\Model\List\Map; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; /** * Read-only complement of {@link DropdownField}. @@ -125,7 +126,7 @@ public function Field($properties = []) } $properties = array_merge($properties, [ - 'AttrValue' => DBField::create_field('HTMLFragment', $attrValue), + 'AttrValue' => DBFieldHelper::create_field('HTMLFragment', $attrValue), 'InputValue' => $inputValue ]); diff --git a/src/Forms/TextField.php b/src/Forms/TextField.php index 49aa40b2c14..08ef5c26ef2 100644 --- a/src/Forms/TextField.php +++ b/src/Forms/TextField.php @@ -2,11 +2,15 @@ namespace SilverStripe\Forms; +use SilverStripe\Model\ModelFields\StringModelField; + /** * Text input field. */ class TextField extends FormField implements TippableFieldInterface { + protected string $modelFieldClass = StringModelField::class; + /** * @var int */ diff --git a/src/Model/ModelData.php b/src/Model/ModelData.php index 04d5a1fc027..e29776eb284 100644 --- a/src/Model/ModelData.php +++ b/src/Model/ModelData.php @@ -16,11 +16,12 @@ 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\SSViewer; use UnexpectedValueException; +use SilverStripe\Model\ModelField; +use SilverStripe\Core\Validation\ValidationResult; /** * A ModelData object is any object that can be rendered into a template/view. @@ -327,7 +328,7 @@ public function setCustomisedObj(ModelData $object) /** * Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value object) - * 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 ModelField. * * @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. @@ -361,7 +362,7 @@ public function castingHelper(string $field, bool $useFallback = true): ?string /** * Return the default "casting helper" for use when no explicit casting helper is defined. - * This helper will be a subclass of DBField. See castingHelper() + * This helper will be a subclass of ModelField. See castingHelper() */ protected function defaultCastingHelper(string $field): string { @@ -402,7 +403,7 @@ public function escapeTypeForField(string $field): string { $class = $this->castingClass($field) ?: $this->config()->get('default_cast'); - /** @var DBField $type */ + /** @var ModelField $type */ $type = Injector::inst()->get($class, true); return $type->config()->get('escape_type'); } @@ -495,9 +496,9 @@ protected function objCacheClear() * Get the value of a field on this object, automatically inserting the value into any available casting objects * that have been specified. * - * @return object|DBField|null The specific object representing the field, or null if there is no + * @return object|ModelField|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 + * Note that if there is a property or method that returns null, a relevant ModelField instance will * be returned. */ public function obj( @@ -520,7 +521,13 @@ public function obj( // Load value from record if ($this->hasMethod($fieldName)) { $hasObj = true; - $value = call_user_func_array([$this, $fieldName], $arguments ?: []); + // todo: remove try catch block + try { + $value = call_user_func_array([$this, $fieldName], $arguments ?: []); + } catch (Exception $e) { + $x=1; + throw $e; + } } else { $hasObj = $this->hasField($fieldName) || ($this->hasMethod("get{$fieldName}") && $this->isAccessibleMethod("get{$fieldName}")); $value = $this->$fieldName; @@ -568,7 +575,7 @@ public function obj( * A simple wrapper around {@link ModelData::obj()} that automatically caches the result so it can be used again * without re-running the method. * - * @return Object|DBField + * @return Object|ModelField */ public function cachedCall(string $fieldName, array $arguments = [], ?string $cacheName = null): object { @@ -599,7 +606,13 @@ public function XML_val(string $field, array $arguments = [], bool $cache = fals return ''; } // Might contain additional formatting over ->XML(). E.g. parse shortcodes, nl2br() - return $result->forTemplate(); + // todo: remove try catch block + try { + return $result->forTemplate(); + } catch (Exception $e) { + $x=1; + throw $e; + } } /** diff --git a/src/Model/ModelFields/EmailModelField.php b/src/Model/ModelFields/EmailModelField.php new file mode 100644 index 00000000000..e97569f73b1 --- /dev/null +++ b/src/Model/ModelFields/EmailModelField.php @@ -0,0 +1,25 @@ +getValue(), + new Constraints\Email(message: $message), + $this->getName() + ); + + return $result->combineAnd($validationResult); + } +} diff --git a/src/Model/ModelFields/ModelField.php b/src/Model/ModelFields/ModelField.php new file mode 100644 index 00000000000..9d440b670e0 --- /dev/null +++ b/src/Model/ModelFields/ModelField.php @@ -0,0 +1,249 @@ + 'HTMLFragment', + 'CDATA' => 'HTMLFragment', + 'HTML' => 'HTMLFragment', + 'HTMLATT' => 'HTMLFragment', + 'JS' => 'HTMLFragment', + 'RAW' => 'HTMLFragment', + 'RAWURLATT' => 'HTMLFragment', + 'URLATT' => 'HTMLFragment', + 'XML' => 'HTMLFragment', + 'ProcessedRAW' => 'HTMLFragment', + ]; + + public function __construct(?string $name = null) + { + $this->name = $name; + parent::__construct(); + } + + public function setName(?string $name): static + { + $this->name = $name; + return $this; + } + + /** + * Returns the name of this field. + */ + public function getName(): string + { + return $this->name ?? ''; + } + + /** + * Returns the value of this field. + */ + public function getValue(): mixed + { + return $this->value; + } + + public function setValue(mixed $value): static + { + $this->value = $value; + return $this; + } + + /** + * Determine 'default' casting for this field. + */ + public function forTemplate(): string + { + // Default to XML encoding + return $this->XML(); + } + + /** + * Gets the value appropriate for a HTML attribute string + */ + public function HTMLATT(): string + { + return Convert::raw2htmlatt($this->RAW()); + } + + /** + * urlencode this string + */ + public function URLATT(): string + { + return urlencode($this->RAW() ?? ''); + } + + /** + * rawurlencode this string + */ + public function RAWURLATT(): string + { + return rawurlencode($this->RAW() ?? ''); + } + + /** + * Gets the value appropriate for a HTML attribute string + */ + public function ATT(): string + { + return Convert::raw2att($this->RAW()); + } + + /** + * Gets the raw value for this field. + * Note: Skips processors implemented via forTemplate() + */ + public function RAW(): mixed + { + return $this->getValue(); + } + + /** + * Gets javascript string literal value + */ + public function JS(): string + { + return Convert::raw2js($this->RAW()); + } + + /** + * Return JSON encoded value + */ + public function JSON(): string + { + return json_encode($this->RAW()); + } + + /** + * Alias for {@see XML()} + */ + public function HTML(): string + { + return $this->XML(); + } + + /** + * XML encode this value + */ + public function XML(): string + { + return Convert::raw2xml($this->RAW()); + } + + /** + * Safely escape for XML string + */ + public function CDATA(): string + { + return $this->XML(); + } + + /** + * Saves this field to the given data object. + * + * TODO: probably rename, it's just setting a field on a model + */ + public function saveInto(ModelData $model): void + { + $fieldName = $this->name; + if (empty($fieldName)) { + throw new \BadMethodCallException( + "ModelField::saveInto() Called on a nameless '" . static::class . "' object" + ); + } + if ($this->value instanceof ModelField) { + $this->value->saveInto($model); + } else { + $model->__set($fieldName, $this->value); + } + } + + public function validate(): ValidationResult + { + return ValidationResult::create(); + } + + /** + * Returns a FormField instance used as a default + * for form scaffolding. + * + * Used by {@link SearchContext}, {@link ModelAdmin}, {@link DataObject::scaffoldFormFields()} + * + * @param string $title Optional. Localized title of the generated instance + */ + public function scaffoldFormField(?string $title = null, array $params = []): ?FormField + { + return TextField::create($this->name, $title); + } + + /** + * Returns a FormField instance used as a default + * for searchform scaffolding. + * + * Used by {@link SearchContext}, {@link ModelAdmin}, {@link DataObject::scaffoldFormFields()}. + * + * @param string $title Optional. Localized title of the generated instance + */ + public function scaffoldSearchField(?string $title = null): ?FormField + { + return $this->scaffoldFormField($title); + } + + /** + * Get formfield schema value for use in formschema response + */ + public function getSchemaValue(): mixed + { + return $this->RAW(); + } + + public function debug(): string + { + return << +
  • Name:{$this->name}
  • +
  • Table:{$this->tableName}
  • +
  • Value:{$this->value}
  • + +DBG; + } + + public function __toString(): string + { + return (string)$this->forTemplate(); + } + + /** + * Whether or not this ModelField only accepts scalar values. + * + * Composite ModelFields can override this method and return `false` so they can accept arrays of values. + */ + public function scalarValueOnly(): bool + { + return true; + } +} diff --git a/src/Model/ModelFields/StringModelField.php b/src/Model/ModelFields/StringModelField.php new file mode 100644 index 00000000000..34f0441a939 --- /dev/null +++ b/src/Model/ModelFields/StringModelField.php @@ -0,0 +1,142 @@ + 'Text', + 'LimitCharactersToClosestWord' => 'Text', + 'LimitWordCount' => 'Text', + 'LowerCase' => 'Text', + 'UpperCase' => 'Text', + 'Plain' => 'Text', + ]; + + public function forTemplate(): string + { + return nl2br(parent::forTemplate() ?? ''); + } + + /** + * Limit this field's content by a number of characters. + * This makes use of strip_tags() to avoid malforming the + * HTML tags in the string of text. + * + * @param int $limit Number of characters to limit by + * @param string|false $add Ellipsis to add to the end of truncated string + */ + public function LimitCharacters(int $limit = 20, string|false $add = false): string + { + $value = $this->Plain(); + if (mb_strlen($value ?? '') <= $limit) { + return $value; + } + return $this->addEllipsis(mb_substr($value ?? '', 0, $limit), $add); + } + + /** + * Limit this field's content by a number of characters and truncate + * the field to the closest complete word. All HTML tags are stripped + * from the field. + * + * @param int $limit Number of characters to limit by + * @param string|false $add Ellipsis to add to the end of truncated string + * @return string Plain text value with limited characters + */ + public function LimitCharactersToClosestWord(int $limit = 20, string|false $add = false): string + { + // Safely convert to plain text + $value = $this->Plain(); + + // Determine if value exceeds limit before limiting characters + if (mb_strlen($value ?? '') <= $limit) { + return $value; + } + + // Limit to character limit + $value = mb_substr($value ?? '', 0, $limit); + + // If value exceeds limit, strip punctuation off the end to the last space and apply ellipsis + $value = $this->addEllipsis( + preg_replace( + '/[^\w_]+$/', + '', + mb_substr($value ?? '', 0, mb_strrpos($value ?? '', " ")) + ), + $add + ); + return $value; + } + + /** + * Limit this field's content by a number of words. + * + * @param int $numWords Number of words to limit by. + * @param string|false $add Ellipsis to add to the end of truncated string. + */ + public function LimitWordCount(int $numWords = 26, string|false $add = false): string + { + $value = $this->Plain(); + $words = explode(' ', $value ?? ''); + if (count($words ?? []) <= $numWords) { + return $value; + } + + // Limit + $words = array_slice($words ?? [], 0, $numWords); + return $this->addEllipsis(implode(' ', $words), $add); + } + + /** + * Converts the current value for this StringField to lowercase. + * + * @return string Text with lowercase (HTML for some subclasses) + */ + public function LowerCase(): string + { + return mb_strtolower($this->RAW() ?? ''); + } + + /** + * Converts the current value for this StringField to uppercase. + * + * @return string Text with uppercase (HTML for some subclasses) + */ + public function UpperCase(): string + { + return mb_strtoupper($this->RAW() ?? ''); + } + + /** + * Plain text version of this string + */ + public function Plain(): string + { + return trim($this->RAW() ?? ''); + } + + /** + * Swap add for defaultEllipsis if need be + */ + private function addEllipsis(string $string, string|false $add): string + { + if ($add === false) { + $add = $this->defaultEllipsis(); + } + + return $string . $add; + } + + /** + * Get the default string to indicate that a string was cut off. + */ + public function defaultEllipsis(): string + { + // TODO: Change to translation key defined on StringModelField + return _t(DBString::class . '.ELLIPSIS', '…'); + } +} diff --git a/src/ORM/Connect/DBSchemaManager.php b/src/ORM/Connect/DBSchemaManager.php index c889bb66918..62319c85a1b 100644 --- a/src/ORM/Connect/DBSchemaManager.php +++ b/src/ORM/Connect/DBSchemaManager.php @@ -10,6 +10,7 @@ use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; use SilverStripe\ORM\FieldType\DBPrimaryKey; /** diff --git a/src/ORM/DataObject.php b/src/ORM/DataObject.php index 178db68e59d..b36dd378e1b 100644 --- a/src/ORM/DataObject.php +++ b/src/ORM/DataObject.php @@ -20,6 +20,7 @@ use SilverStripe\Forms\FormScaffolder; use SilverStripe\Forms\CompositeValidator; use SilverStripe\Forms\FieldsValidator; +use SilverStripe\Forms\Form; use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor; use SilverStripe\Forms\HiddenField; @@ -33,6 +34,7 @@ use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBEnum; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; use SilverStripe\ORM\FieldType\DBForeignKey; use SilverStripe\ORM\Filters\PartialMatchFilter; use SilverStripe\ORM\Filters\SearchFilter; @@ -45,7 +47,13 @@ use SilverStripe\Security\Security; use SilverStripe\View\SSViewer; use SilverStripe\Model\ModelData; +use SilverStripe\ORM\FieldType\DBText; +use SilverStripe\ORM\FieldType\DBVarchar; use stdClass; +use SilverStripe\Control\Controller; +use SilverStripe\Control\HTTPRequest; +use SilverStripe\Control\Session; +use Symfony\Component\Validator\Constraints\Uuid; /** * A single database record & abstract class for the data-access-model. @@ -1230,10 +1238,44 @@ public function forceChange() public function validate() { $result = ValidationResult::create(); + // Call validate() on every DBField + $specs = static::getSchema()->fieldSpecs(static::class); + foreach (array_keys($specs) as $fieldName) { + // Call DBField::validate() on every DBField + $dbField = $this->dbObject($fieldName); + $result->combineAnd($dbField->validate()); + } + // If it passed DBField::validate(), then call FormField::validate() + // on every field that would be scaffolded + // This isn't ideal and can lead to double validation, however it's to preserve + // any legacy FormField::validate() logic on projects that has not been + // migrated to use DBField::validate() + // TODO: don't call this when in FormRequestHandler::httpSubmission() + // as it means that calling FormField::validate() is called twice + if ($result->isValid()) { + $form = $this->getFormForValidation(); + $result->combineAnd($form->validationResult()); + } + // Call extension hook and return $this->extend('updateValidate', $result); return $result; } + private function getFormForValidation(): Form + { + $session = new Session([]); + $controller = new Controller(); + $request = new HTTPRequest('GET', '/'); + $request->setSession($session); + $controller->setRequest($request); + // Use getCMSFields rather than $dbField->scaffoldFormField() + // to ensure we get any CMS fields that were replaced by other fields + $fields = $this->getCMSFields(); + $form = new Form($controller, sprintf('temp-%s', uniqid()), $fields, null, new FieldsValidator); + $form->loadDataFrom($this); + return $form; + } + /** * Event handler called before writing to the database. * You can overload this to clean up or otherwise process data before writing it to the @@ -1456,7 +1498,7 @@ protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$ma // if database column doesn't correlate to a DBField instance... $fieldObj = $this->dbObject($fieldName); if (!$fieldObj) { - $fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName); + $fieldObj = DBFieldHelper::create_field('Varchar', $fieldValue, $fieldName); } // Write to manipulation @@ -2422,16 +2464,15 @@ public function scaffoldSearchFields($_params = null) } // Otherwise, use the database field's scaffolder - } elseif ($object = $this->relObject($fieldName)) { - if (is_object($object) && $object->hasMethod('scaffoldSearchField')) { - $field = $object->scaffoldSearchField(); - } else { + } elseif ($dbField = $this->relObject($fieldName)) { + if (!is_a($dbField, DBField::class)) { throw new Exception(sprintf( "SearchField '%s' on '%s' does not return a valid DBField instance.", $fieldName, get_class($this) )); } + $field = $dbField->scaffoldSearchField(); } // Allow fields to opt out of search diff --git a/src/ORM/DataObjectSchema.php b/src/ORM/DataObjectSchema.php index daeea0f6553..f13b3d6e281 100644 --- a/src/ORM/DataObjectSchema.php +++ b/src/ORM/DataObjectSchema.php @@ -16,6 +16,7 @@ use SilverStripe\ORM\Connect\DBSchemaManager; use SilverStripe\ORM\FieldType\DBComposite; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; /** * Provides dataobject and database schema mapping functionality diff --git a/src/ORM/EagerLoadedList.php b/src/ORM/EagerLoadedList.php index d65a49d3767..186f9a0d00d 100644 --- a/src/ORM/EagerLoadedList.php +++ b/src/ORM/EagerLoadedList.php @@ -6,6 +6,7 @@ use SilverStripe\Model\ModelData; use SilverStripe\ORM\Connect\DatabaseException; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; use BadMethodCallException; use InvalidArgumentException; use LogicException; diff --git a/src/ORM/FieldType/DBBoolean.php b/src/ORM/FieldType/DBBoolean.php index c4d4c1c4870..f51e37a6b3c 100644 --- a/src/ORM/FieldType/DBBoolean.php +++ b/src/ORM/FieldType/DBBoolean.php @@ -7,12 +7,17 @@ use SilverStripe\Forms\FormField; use SilverStripe\ORM\DB; use SilverStripe\Model\ModelData; +use SilverStripe\Model\ModelFields\ModelField; +use SilverStripe\ORM\FieldType\DBFieldTrait; +use SilverStripe\ORM\FieldType\DBField; /** * Represents a boolean field. */ -class DBBoolean extends DBField +class DBBoolean extends ModelField implements DBField { + use DBFieldTrait; + public function __construct(?string $name = null, bool|int $defaultVal = 0) { $this->defaultVal = ($defaultVal) ? 1 : 0; diff --git a/src/ORM/FieldType/DBComposite.php b/src/ORM/FieldType/DBComposite.php index 7060417eadc..05766b902e0 100644 --- a/src/ORM/FieldType/DBComposite.php +++ b/src/ORM/FieldType/DBComposite.php @@ -8,6 +8,9 @@ use SilverStripe\ORM\DB; use SilverStripe\ORM\Queries\SQLSelect; use SilverStripe\Model\ModelData; +use SilverStripe\Model\ModelFields\ModelField; +use SilverStripe\ORM\FieldType\DBFieldTrait; +use SilverStripe\ORM\FieldType\DBField; /** * Extend this class when designing a {@link DBField} that doesn't have a 1-1 mapping with a database field. @@ -23,8 +26,10 @@ * } * */ -abstract class DBComposite extends DBField +abstract class DBComposite extends ModelField implements DBField { + use DBFieldTrait; + /** * Similar to {@link DataObject::$db}, * holds an array of composite field names. @@ -85,8 +90,6 @@ public function writeToManipulation(array &$manipulation): void */ public function addToQuery(SQLSelect &$query): void { - parent::addToQuery($query); - foreach ($this->compositeDatabaseFields() as $field => $spec) { $table = $this->getTable(); $key = $this->getName() . $field; diff --git a/src/ORM/FieldType/DBDate.php b/src/ORM/FieldType/DBDate.php index d8a271d7878..64dd5c2f883 100644 --- a/src/ORM/FieldType/DBDate.php +++ b/src/ORM/FieldType/DBDate.php @@ -12,6 +12,9 @@ use SilverStripe\Security\Member; use SilverStripe\Security\Security; use SilverStripe\Model\ModelData; +use SilverStripe\ORM\FieldType\DBFieldTrait; +use SilverStripe\Model\ModelFields\ModelField; +use SilverStripe\ORM\FieldType\DBField; /** * Represents a date field. @@ -29,8 +32,10 @@ * Date formats all follow CLDR standard format codes * @link https://unicode-org.github.io/icu/userguide/format_parse/datetime */ -class DBDate extends DBField +class DBDate extends ModelField implements DBField { + use DBFieldTrait; + /** * Standard ISO format string for date in CLDR standard format */ diff --git a/src/ORM/FieldType/DBDatetime.php b/src/ORM/FieldType/DBDatetime.php index 287b64690d1..bcd8bd187d4 100644 --- a/src/ORM/FieldType/DBDatetime.php +++ b/src/ORM/FieldType/DBDatetime.php @@ -260,7 +260,7 @@ public static function now(): static $time = DBDatetime::$mock_now ? DBDatetime::$mock_now->Value : time(); /** @var DBDatetime $now */ - $now = DBField::create_field('Datetime', $time); + $now = DBFieldHelper::create_field('Datetime', $time); return $now; } @@ -277,7 +277,7 @@ public static function set_mock_now(DBDatetime|string $datetime): void { if (!$datetime instanceof DBDatetime) { $value = $datetime; - $datetime = DBField::create_field('Datetime', $datetime); + $datetime = DBFieldHelper::create_field('Datetime', $datetime); if ($datetime === false) { throw new InvalidArgumentException('DBDatetime::set_mock_now(): Wrong format: ' . $value); } diff --git a/src/ORM/FieldType/DBDecimal.php b/src/ORM/FieldType/DBDecimal.php index deab1bcd05c..36900fcb892 100644 --- a/src/ORM/FieldType/DBDecimal.php +++ b/src/ORM/FieldType/DBDecimal.php @@ -6,12 +6,16 @@ use SilverStripe\Forms\NumericField; use SilverStripe\ORM\DB; use SilverStripe\Model\ModelData; +use SilverStripe\ORM\FieldType\DBFieldTrait; +use SilverStripe\Model\ModelFields\ModelField; +use SilverStripe\ORM\FieldType\DBField; /** * Represents a Decimal field. */ -class DBDecimal extends DBField +class DBDecimal extends ModelField implements DBField { + use DBFieldTrait; /** * Whole number size */ diff --git a/src/ORM/FieldType/DBEmail.php b/src/ORM/FieldType/DBEmail.php new file mode 100644 index 00000000000..2586669fc6a --- /dev/null +++ b/src/ORM/FieldType/DBEmail.php @@ -0,0 +1,44 @@ +combineAnd( + // // ConstraintValidator::validate( + // // $this->getValue(), + // // new Constraints\Email(message: $message), + // // $this->getName() + // // ) + // // ); + // $result = $result->combineAnd($this->validateEmail()); + // $this->extend('updateValidate', $result); + // return $result; + // } + + public function scaffoldFormField(?string $title = null, array $params = []): ?FormField + { + // Set field with appropriate size + $field = EmailField::create($this->name, $title); + $field->setMaxLength($this->getSize()); + + // Allow the user to select if it's null instead of automatically assuming empty string is + if (!$this->getNullifyEmpty()) { + return NullableField::create($field); + } + return $field; + } +} diff --git a/src/ORM/FieldType/DBField.php b/src/ORM/FieldType/DBField.php index 38efb57583f..d1a02500028 100644 --- a/src/ORM/FieldType/DBField.php +++ b/src/ORM/FieldType/DBField.php @@ -2,567 +2,9 @@ namespace SilverStripe\ORM\FieldType; -use InvalidArgumentException; -use SilverStripe\Core\Convert; -use SilverStripe\Core\Injector\Injector; -use SilverStripe\Forms\FormField; -use SilverStripe\Forms\TextField; -use SilverStripe\ORM\Filters\SearchFilter; -use SilverStripe\ORM\Queries\SQLSelect; -use SilverStripe\Model\ModelData; - /** - * Single field in the database. - * - * Every field from the database is represented as a sub-class of DBField. - * - * Multi-value DBField objects - * - * Sometimes you will want to make DBField classes that don't have a 1-1 match - * to database fields. To do this, there are a number of fields for you to - * overload: - * - * - Overload {@link writeToManipulation} to add the appropriate references to - * the INSERT or UPDATE command - * - Overload {@link addToQuery} to add the appropriate items to a SELECT - * query's field list - * - Add appropriate accessor methods - * - * Subclass Example - * - * The class is easy to overload with custom types, e.g. the MySQL "BLOB" type - * (https://dev.mysql.com/doc/refman/8.4/en/blob.html). - * - * - * class Blob extends DBField { - * function requireField(): void { - * DB::require_field($this->tableName, $this->name, "blob"); - * } - * } - * + * This interface is exists basically so that $object instanceof DBField checks work */ -abstract class DBField extends ModelData implements DBIndexable +interface DBField extends DBIndexable { - - /** - * Raw value of this field - */ - protected mixed $value = null; - - /** - * Table this field belongs to - */ - protected ?string $tableName = null; - - /** - * Name of this field - */ - protected ?string $name = null; - - /** - * Used for generating DB schema. {@see DBSchemaManager} - * Despite its name, this seems to be a string - */ - protected $arrayValue; - - /** - * Optional parameters for this field - */ - protected array $options = []; - - /** - * The escape type for this field when inserted into a template - either "xml" or "raw". - */ - private static string $escape_type = 'raw'; - - /** - * Subclass of {@link SearchFilter} for usage in {@link defaultSearchFilter()}. - */ - private static string $default_search_filter_class = 'PartialMatchFilter'; - - /** - * The type of index to use for this field. Can either be a string (one of the DBIndexable type options) or a - * boolean. When a boolean is given, false will not index the field, and true will use the default index type. - */ - private static string|bool $index = false; - - private static array $casting = [ - 'ATT' => 'HTMLFragment', - 'CDATA' => 'HTMLFragment', - 'HTML' => 'HTMLFragment', - 'HTMLATT' => 'HTMLFragment', - 'JS' => 'HTMLFragment', - 'RAW' => 'HTMLFragment', - 'RAWURLATT' => 'HTMLFragment', - 'URLATT' => 'HTMLFragment', - 'XML' => 'HTMLFragment', - 'ProcessedRAW' => 'HTMLFragment', - ]; - - /** - * Default value in the database. - * Might be overridden on DataObject-level, but still useful for setting defaults on - * already existing records after a db-build. - */ - protected mixed $defaultVal = null; - - /** - * Provide the DBField name and an array of options, e.g. ['index' => true], or ['nullifyEmpty' => false] - * - * @throws InvalidArgumentException If $options was passed by not an array - */ - public function __construct(?string $name = null, array $options = []) - { - $this->name = $name; - - if ($options) { - if (!is_array($options)) { - throw new InvalidArgumentException("Invalid options $options"); - } - $this->setOptions($options); - } - - parent::__construct(); - } - - /** - * Create a DBField object that's not bound to any particular field. - * - * Useful for accessing the classes behaviour for other parts of your code. - * - * @param string $spec Class specification to construct. May include both service name and additional - * constructor arguments in the same format as DataObject.db config. - * @param mixed $value value of field - * @param null|string $name Name of field - * @param mixed $args Additional arguments to pass to constructor if not using args in service $spec - * Note: Will raise a warning if using both - */ - public static function create_field(string $spec, mixed $value, ?string $name = null, mixed ...$args): static - { - // Raise warning if inconsistent with DataObject::dbObject() behaviour - // This will cause spec args to be shifted down by the number of provided $args - if ($args && strpos($spec ?? '', '(') !== false) { - trigger_error('Additional args provided in both $spec and $args', E_USER_WARNING); - } - // Ensure name is always first argument - array_unshift($args, $name); - - /** @var DBField $dbField */ - $dbField = Injector::inst()->createWithArgs($spec, $args); - $dbField->setValue($value, null, false); - return $dbField; - } - - /** - * Set the name of this field. - * - * The name should never be altered, but it if was never given a name in - * the first place you can set a name. - * - * If you try an alter the name a warning will be thrown. - */ - public function setName(?string $name): static - { - if ($this->name && $this->name !== $name) { - user_error("DBField::setName() shouldn't be called once a DBField already has a name." - . "It's partially immutable - it shouldn't be altered after it's given a value.", E_USER_WARNING); - } - - $this->name = $name; - - return $this; - } - - /** - * Returns the name of this field. - */ - public function getName(): string - { - return $this->name ?? ''; - } - - /** - * Returns the value of this field. - */ - public function getValue(): mixed - { - return $this->value; - } - - /** - * Set the value of this field in various formats. - * Used by {@link DataObject->getField()}, {@link DataObject->setCastedField()} - * {@link DataObject->dbObject()} and {@link DataObject->write()}. - * - * As this method is used both for initializing the field after construction, - * and actually changing its values, it needs a {@link $markChanged} - * parameter. - * - * @param null|ModelData|array $record An array or object that this field is part of - * @param bool $markChanged Indicate whether this field should be marked changed. - * Set to FALSE if you are initializing this field after construction, rather - * than setting a new value. - */ - public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static - { - $this->value = $value; - return $this; - } - - /** - * Get default value assigned at the DB level - */ - public function getDefaultValue(): mixed - { - return $this->defaultVal; - } - - /** - * Set default value to use at the DB level - */ - public function setDefaultValue(mixed $defaultValue): static - { - $this->defaultVal = $defaultValue; - return $this; - } - - /** - * Update the optional parameters for this field - */ - public function setOptions(array $options = []): static - { - $this->options = $options; - return $this; - } - - /** - * Get optional parameters for this field - */ - public function getOptions(): array - { - return $this->options; - } - - public function setIndexType($type): string|bool - { - if (!is_bool($type) - && !in_array($type, [DBIndexable::TYPE_INDEX, DBIndexable::TYPE_UNIQUE, DBIndexable::TYPE_FULLTEXT]) - ) { - throw new \InvalidArgumentException( - "{$type} is not a valid index type or boolean. Please see DBIndexable." - ); - } - - $this->options['index'] = $type; - return $this; - } - - public function getIndexType() - { - if (array_key_exists('index', $this->options ?? [])) { - $type = $this->options['index']; - } else { - $type = static::config()->get('index'); - } - - if (is_bool($type)) { - if (!$type) { - return false; - } - $type = DBIndexable::TYPE_DEFAULT; - } - - return $type; - } - - /** - * Determines if the field has a value which is not considered to be 'null' - * in a database context. - */ - public function exists(): bool - { - return (bool)$this->value; - } - - /** - * Return the transformed value ready to be sent to the database. This value - * will be escaped automatically by the prepared query processor, so it - * should not be escaped or quoted at all. - * - * @param mixed $value The value to check - * @return mixed The raw value, or escaped parameterised details - */ - public function prepValueForDB(mixed $value): mixed - { - if ($value === null || - $value === "" || - $value === false || - ($this->scalarValueOnly() && !is_scalar($value)) - ) { - return null; - } else { - return $value; - } - } - - /** - * Prepare the current field for usage in a - * database-manipulation (works on a manipulation reference). - * - * Make value safe for insertion into - * a SQL SET statement by applying addslashes() - - * can also be used to apply special SQL-commands - * to the raw value (e.g. for GIS functionality). - * {@see prepValueForDB} - */ - public function writeToManipulation(array &$manipulation): void - { - $manipulation['fields'][$this->name] = $this->exists() - ? $this->prepValueForDB($this->value) : $this->nullValue(); - } - - /** - * Add custom query parameters for this field, - * mostly SELECT statements for multi-value fields. - * - * By default, the ORM layer does a - * SELECT .* which - * gets you the default representations - * of all columns. - */ - public function addToQuery(SQLSelect &$query) - { - } - - /** - * Assign this DBField to a table - */ - public function setTable(string $tableName): static - { - $this->tableName = $tableName; - return $this; - } - - /** - * Get the table this field belongs to, if assigned - */ - public function getTable(): ?string - { - return $this->tableName; - } - - /** - * Determine 'default' casting for this field. - */ - public function forTemplate(): string - { - // Default to XML encoding - return $this->XML(); - } - - /** - * Gets the value appropriate for a HTML attribute string - */ - public function HTMLATT(): string - { - return Convert::raw2htmlatt($this->RAW()); - } - - /** - * urlencode this string - */ - public function URLATT(): string - { - return urlencode($this->RAW() ?? ''); - } - - /** - * rawurlencode this string - */ - public function RAWURLATT(): string - { - return rawurlencode($this->RAW() ?? ''); - } - - /** - * Gets the value appropriate for a HTML attribute string - */ - public function ATT(): string - { - return Convert::raw2att($this->RAW()); - } - - /** - * Gets the raw value for this field. - * Note: Skips processors implemented via forTemplate() - */ - public function RAW(): mixed - { - return $this->getValue(); - } - - /** - * Gets javascript string literal value - */ - public function JS(): string - { - return Convert::raw2js($this->RAW()); - } - - /** - * Return JSON encoded value - */ - public function JSON(): string - { - return json_encode($this->RAW()); - } - - /** - * Alias for {@see XML()} - */ - public function HTML(): string - { - return $this->XML(); - } - - /** - * XML encode this value - */ - public function XML(): string - { - return Convert::raw2xml($this->RAW()); - } - - /** - * Safely escape for XML string - */ - public function CDATA(): string - { - return $this->XML(); - } - - /** - * Returns the value to be set in the database to blank this field. - * Usually it's a choice between null, 0, and '' - */ - public function nullValue(): mixed - { - return null; - } - - /** - * Saves this field to the given data object. - */ - public function saveInto(ModelData $model): void - { - $fieldName = $this->name; - if (empty($fieldName)) { - throw new \BadMethodCallException( - "DBField::saveInto() Called on a nameless '" . static::class . "' object" - ); - } - if ($this->value instanceof DBField) { - $this->value->saveInto($model); - } else { - $model->__set($fieldName, $this->value); - } - } - - /** - * Returns a FormField instance used as a default - * for form scaffolding. - * - * Used by {@link SearchContext}, {@link ModelAdmin}, {@link DataObject::scaffoldFormFields()} - * - * @param string $title Optional. Localized title of the generated instance - */ - public function scaffoldFormField(?string $title = null, array $params = []): ?FormField - { - return TextField::create($this->name, $title); - } - - /** - * Returns a FormField instance used as a default - * for searchform scaffolding. - * - * Used by {@link SearchContext}, {@link ModelAdmin}, {@link DataObject::scaffoldFormFields()}. - * - * @param string $title Optional. Localized title of the generated instance - */ - public function scaffoldSearchField(?string $title = null): ?FormField - { - return $this->scaffoldFormField($title); - } - - /** - * @param string $name Override name of this field - */ - public function defaultSearchFilter(?string $name = null): SearchFilter - { - $name = ($name) ? $name : $this->name; - $filterClass = static::config()->get('default_search_filter_class'); - return Injector::inst()->create($filterClass, $name); - } - - /** - * Add the field to the underlying database. - */ - abstract public function requireField(): void; - - public function debug(): string - { - return << -
  • Name:{$this->name}
  • -
  • Table:{$this->tableName}
  • -
  • Value:{$this->value}
  • - -DBG; - } - - public function __toString(): string - { - return (string)$this->forTemplate(); - } - - public function getArrayValue() - { - return $this->arrayValue; - } - - public function setArrayValue($value): static - { - $this->arrayValue = $value; - return $this; - } - - /** - * Get formfield schema value for use in formschema response - */ - public function getSchemaValue(): mixed - { - return $this->RAW(); - } - - public function getIndexSpecs(): ?array - { - $type = $this->getIndexType(); - if ($type) { - return [ - 'type' => $type, - 'columns' => [$this->getName()], - ]; - } - return null; - } - - /** - * Whether or not this DBField only accepts scalar values. - * - * Composite DBFields can override this method and return `false` so they can accept arrays of values. - */ - public function scalarValueOnly(): bool - { - return true; - } } diff --git a/src/ORM/FieldType/DBFieldHelper.php b/src/ORM/FieldType/DBFieldHelper.php new file mode 100644 index 00000000000..33bff09586e --- /dev/null +++ b/src/ORM/FieldType/DBFieldHelper.php @@ -0,0 +1,37 @@ +createWithArgs($spec, $args); + /** @var DBFieldTrait $dbField */ + $dbField->setValue($value, null, false); + /** @var DBField $dbField */ + return $dbField; + } +} diff --git a/src/ORM/FieldType/DBFieldTrait.php b/src/ORM/FieldType/DBFieldTrait.php new file mode 100644 index 00000000000..bf326f375d6 --- /dev/null +++ b/src/ORM/FieldType/DBFieldTrait.php @@ -0,0 +1,345 @@ +Multi-value DBField objects + * + * Sometimes you will want to make DBField classes that don't have a 1-1 match + * to database fields. To do this, there are a number of fields for you to + * overload: + * + * - Overload {@link writeToManipulation} to add the appropriate references to + * the INSERT or UPDATE command + * - Overload {@link addToQuery} to add the appropriate items to a SELECT + * query's field list + * - Add appropriate accessor methods + * + * Subclass Example + * + * The class is easy to overload with custom types, e.g. the MySQL "BLOB" type + * (https://dev.mysql.com/doc/refman/8.4/en/blob.html). + * + * + * class Blob extends DBField { + * function requireField(): void { + * DB::require_field($this->tableName, $this->name, "blob"); + * } + * } + * + */ +trait DBFieldTrait +{ + /** + * Raw value of this field + */ + protected mixed $value = null; + + /** + * Name of this field + */ + protected ?string $name = null; + + /** + * Table this field belongs to + */ + protected ?string $tableName = null; + + /** + * Used for generating DB schema. {@see DBSchemaManager} + * Despite its name, this seems to be a string + */ + protected $arrayValue; + + /** + * Optional parameters for this field + */ + protected array $options = []; + + /** + * The type of index to use for this field. Can either be a string (one of the DBIndexable type options) or a + * boolean. When a boolean is given, false will not index the field, and true will use the default index type. + */ + private static string|bool $index = false; + + /** + * Subclass of {@link SearchFilter} for usage in {@link defaultSearchFilter()}. + */ + private static string $default_search_filter_class = 'PartialMatchFilter'; + + /** + * Default value in the database. + * Might be overridden on DataObject-level, but still useful for setting defaults on + * already existing records after a db-build. + */ + protected mixed $defaultVal = null; + + /** + * Provide the DBField name and an array of options, e.g. ['index' => true], or ['nullifyEmpty' => false] + * + * @throws InvalidArgumentException If $options was passed by not an array + */ + public function __construct(?string $name = null, array $options = []) + { + if (!is_a(self::class, ModelField::class, true)) { + throw new InvalidArgumentException( + 'DBFieldTrait can only be used on classes that extend ' . ModelField::class + ); + } + if (!is_a(self::class, DBField::class, true)) { + throw new InvalidArgumentException( + 'DBFieldTrait can only be used on classes that implement ' . DBField::class + ); + } + if ($options) { + if (!is_array($options)) { + throw new InvalidArgumentException("Invalid options $options"); + } + $this->setOptions($options); + } + parent::__construct($name); + } + + /** + * Set the name of this field. + * + * The name should never be altered, but it if was never given a name in + * the first place you can set a name. + * + * If you try an alter the name a warning will be thrown. + */ + public function setName(?string $name): static + { + if ($this->name && $this->name !== $name) { + user_error("ModelField::setName() shouldn't be called once a ModelField already has a name." + . "It's partially immutable - it shouldn't be altered after it's given a value.", E_USER_WARNING); + } + + $this->name = $name; + + return $this; + } + + /** + * Set the value of this field in various formats. + * Used by {@link DataObject->getField()}, {@link DataObject->setCastedField()} + * {@link DataObject->dbObject()} and {@link DataObject->write()}. + * + * As this method is used both for initializing the field after construction, + * and actually changing its values, it needs a {@link $markChanged} + * parameter. + * + * @param null|ModelData|array $record An array or object that this field is part of + * @param bool $markChanged Indicate whether this field should be marked changed. + * Set to FALSE if you are initializing this field after construction, rather + * than setting a new value. + */ + public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static + { + $this->value = $value; + + return $this; + } + + /** + * Add the field to the underlying database. + */ + abstract public function requireField(): void; + + /** + * Get default value assigned at the DB level + */ + public function getDefaultValue(): mixed + { + return $this->defaultVal; + } + + /** + * Set default value to use at the DB level + */ + public function setDefaultValue(mixed $defaultValue): static + { + $this->defaultVal = $defaultValue; + return $this; + } + + /** + * Update the optional parameters for this field + */ + public function setOptions(array $options = []): static + { + $this->options = $options; + return $this; + } + + /** + * Get optional parameters for this field + */ + public function getOptions(): array + { + return $this->options; + } + + public function setIndexType($type): string|bool + { + if (!is_bool($type) + && !in_array($type, [DBIndexable::TYPE_INDEX, DBIndexable::TYPE_UNIQUE, DBIndexable::TYPE_FULLTEXT]) + ) { + throw new \InvalidArgumentException( + "{$type} is not a valid index type or boolean. Please see DBIndexable." + ); + } + + $this->options['index'] = $type; + return $this; + } + + public function getIndexType() + { + if (array_key_exists('index', $this->options ?? [])) { + $type = $this->options['index']; + } else { + $type = static::config()->get('index'); + } + + if (is_bool($type)) { + if (!$type) { + return false; + } + $type = DBIndexable::TYPE_DEFAULT; + } + + return $type; + } + + /** + * Determines if the field has a value which is not considered to be 'null' + * in a database context. + */ + public function exists(): bool + { + return (bool)$this->value; + } + + /** + * Return the transformed value ready to be sent to the database. This value + * will be escaped automatically by the prepared query processor, so it + * should not be escaped or quoted at all. + * + * @param mixed $value The value to check + * @return mixed The raw value, or escaped parameterised details + */ + public function prepValueForDB(mixed $value): mixed + { + if ($value === null || + $value === "" || + $value === false || + ($this->scalarValueOnly() && !is_scalar($value)) + ) { + return null; + } else { + return $value; + } + } + + /** + * Prepare the current field for usage in a + * database-manipulation (works on a manipulation reference). + * + * Make value safe for insertion into + * a SQL SET statement by applying addslashes() - + * can also be used to apply special SQL-commands + * to the raw value (e.g. for GIS functionality). + * {@see prepValueForDB} + */ + public function writeToManipulation(array &$manipulation): void + { + $manipulation['fields'][$this->name] = $this->exists() + ? $this->prepValueForDB($this->value) : $this->nullValue(); + } + + /** + * Add custom query parameters for this field, + * mostly SELECT statements for multi-value fields. + * + * By default, the ORM layer does a + * SELECT .* which + * gets you the default representations + * of all columns. + */ + public function addToQuery(SQLSelect &$query) + { + } + + /** + * Assign this DBField to a table + */ + public function setTable(string $tableName): static + { + $this->tableName = $tableName; + return $this; + } + + /** + * Get the table this field belongs to, if assigned + */ + public function getTable(): ?string + { + return $this->tableName; + } + + /** + * Returns the value to be set in the database to blank this field. + * Usually it's a choice between null, 0, and '' + */ + public function nullValue(): mixed + { + return null; + } + + /** + * @param string $name Override name of this field + */ + public function defaultSearchFilter(?string $name = null): SearchFilter + { + $name = ($name) ? $name : $this->name; + $filterClass = static::config()->get('default_search_filter_class'); + return Injector::inst()->create($filterClass, $name); + } + + public function getArrayValue() + { + return $this->arrayValue; + } + + public function setArrayValue($value): static + { + $this->arrayValue = $value; + return $this; + } + + public function getIndexSpecs(): ?array + { + $type = $this->getIndexType(); + if ($type) { + return [ + 'type' => $type, + 'columns' => [$this->getName()], + ]; + } + return null; + } +} diff --git a/src/ORM/FieldType/DBFloat.php b/src/ORM/FieldType/DBFloat.php index 824ff9f0b84..8a7b5d58cbf 100644 --- a/src/ORM/FieldType/DBFloat.php +++ b/src/ORM/FieldType/DBFloat.php @@ -5,12 +5,17 @@ use SilverStripe\Forms\FormField; use SilverStripe\Forms\NumericField; use SilverStripe\ORM\DB; +use SilverStripe\ORM\FieldType\DBFieldTrait; +use SilverStripe\Model\ModelFields\ModelField; +use SilverStripe\ORM\FieldType\DBField; /** * Represents a floating point field. */ -class DBFloat extends DBField +class DBFloat extends ModelField implements DBField { + use DBFieldTrait; + public function __construct(?string $name = null, float|int $defaultVal = 0) { $this->defaultVal = is_float($defaultVal) ? $defaultVal : (float) 0; diff --git a/src/ORM/FieldType/DBInt.php b/src/ORM/FieldType/DBInt.php index 0f46ddf54cc..9869fe1bf05 100644 --- a/src/ORM/FieldType/DBInt.php +++ b/src/ORM/FieldType/DBInt.php @@ -8,12 +8,16 @@ use SilverStripe\ORM\DB; use SilverStripe\Model\List\SS_List; use SilverStripe\Model\ArrayData; +use SilverStripe\Model\ModelFields\ModelField; +use SilverStripe\ORM\FieldType\DBField; /** * Represents a signed 32 bit integer field. */ -class DBInt extends DBField +class DBInt extends ModelField implements DBField { + use DBFieldTrait; + public function __construct(?string $name = null, int $defaultVal = 0) { $this->defaultVal = is_int($defaultVal) ? $defaultVal : 0; diff --git a/src/ORM/FieldType/DBString.php b/src/ORM/FieldType/DBString.php index 99d597aa9c5..eb0264401e4 100644 --- a/src/ORM/FieldType/DBString.php +++ b/src/ORM/FieldType/DBString.php @@ -2,19 +2,15 @@ namespace SilverStripe\ORM\FieldType; +use SilverStripe\Model\ModelFields\StringModelField; +use SilverStripe\ORM\FieldType\DBFieldTrait; + /** * An abstract base class for the string field types (i.e. Varchar and Text) */ -abstract class DBString extends DBField +abstract class DBString extends StringModelField implements DBField { - private static array $casting = [ - 'LimitCharacters' => 'Text', - 'LimitCharactersToClosestWord' => 'Text', - 'LimitWordCount' => 'Text', - 'LowerCase' => 'Text', - 'UpperCase' => 'Text', - 'Plain' => 'Text', - ]; + use DBFieldTrait; /** * Set the default value for "nullify empty" @@ -95,127 +91,4 @@ public function prepValueForDB(mixed $value): array|string|null } return ''; } - - public function forTemplate(): string - { - return nl2br(parent::forTemplate() ?? ''); - } - - /** - * Limit this field's content by a number of characters. - * This makes use of strip_tags() to avoid malforming the - * HTML tags in the string of text. - * - * @param int $limit Number of characters to limit by - * @param string|false $add Ellipsis to add to the end of truncated string - */ - public function LimitCharacters(int $limit = 20, string|false $add = false): string - { - $value = $this->Plain(); - if (mb_strlen($value ?? '') <= $limit) { - return $value; - } - return $this->addEllipsis(mb_substr($value ?? '', 0, $limit), $add); - } - - /** - * Limit this field's content by a number of characters and truncate - * the field to the closest complete word. All HTML tags are stripped - * from the field. - * - * @param int $limit Number of characters to limit by - * @param string|false $add Ellipsis to add to the end of truncated string - * @return string Plain text value with limited characters - */ - public function LimitCharactersToClosestWord(int $limit = 20, string|false $add = false): string - { - // Safely convert to plain text - $value = $this->Plain(); - - // Determine if value exceeds limit before limiting characters - if (mb_strlen($value ?? '') <= $limit) { - return $value; - } - - // Limit to character limit - $value = mb_substr($value ?? '', 0, $limit); - - // If value exceeds limit, strip punctuation off the end to the last space and apply ellipsis - $value = $this->addEllipsis( - preg_replace( - '/[^\w_]+$/', - '', - mb_substr($value ?? '', 0, mb_strrpos($value ?? '', " ")) - ), - $add - ); - return $value; - } - - /** - * Limit this field's content by a number of words. - * - * @param int $numWords Number of words to limit by. - * @param string|false $add Ellipsis to add to the end of truncated string. - */ - public function LimitWordCount(int $numWords = 26, string|false $add = false): string - { - $value = $this->Plain(); - $words = explode(' ', $value ?? ''); - if (count($words ?? []) <= $numWords) { - return $value; - } - - // Limit - $words = array_slice($words ?? [], 0, $numWords); - return $this->addEllipsis(implode(' ', $words), $add); - } - - /** - * Converts the current value for this StringField to lowercase. - * - * @return string Text with lowercase (HTML for some subclasses) - */ - public function LowerCase(): string - { - return mb_strtolower($this->RAW() ?? ''); - } - - /** - * Converts the current value for this StringField to uppercase. - * - * @return string Text with uppercase (HTML for some subclasses) - */ - public function UpperCase(): string - { - return mb_strtoupper($this->RAW() ?? ''); - } - - /** - * Plain text version of this string - */ - public function Plain(): string - { - return trim($this->RAW() ?? ''); - } - - /** - * Swap add for defaultEllipsis if need be - */ - private function addEllipsis(string $string, string|false $add): string - { - if ($add === false) { - $add = $this->defaultEllipsis(); - } - - return $string . $add; - } - - /** - * Get the default string to indicate that a string was cut off. - */ - public function defaultEllipsis(): string - { - return _t(DBString::class . '.ELLIPSIS', '…'); - } } diff --git a/src/ORM/FieldType/DBTime.php b/src/ORM/FieldType/DBTime.php index fac285be19c..d0e5a65f827 100644 --- a/src/ORM/FieldType/DBTime.php +++ b/src/ORM/FieldType/DBTime.php @@ -11,6 +11,9 @@ use SilverStripe\Security\Member; use SilverStripe\Security\Security; use SilverStripe\Model\ModelData; +use SilverStripe\ORM\FieldType\DBFieldTrait; +use SilverStripe\Model\ModelFields\ModelField; +use SilverStripe\ORM\FieldType\DBField; /** * Represents a column in the database with the type 'Time'. @@ -22,8 +25,10 @@ * ); * */ -class DBTime extends DBField +class DBTime extends ModelField implements DBField { + use DBFieldTrait; + /** * Standard ISO format string for time in CLDR standard format */ diff --git a/src/ORM/FieldType/DBYear.php b/src/ORM/FieldType/DBYear.php index 04618cae339..f26927acb7f 100644 --- a/src/ORM/FieldType/DBYear.php +++ b/src/ORM/FieldType/DBYear.php @@ -5,12 +5,17 @@ use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\FormField; use SilverStripe\ORM\DB; +use SilverStripe\ORM\FieldType\DBFieldTrait; +use SilverStripe\Model\ModelFields\ModelField; +use SilverStripe\ORM\FieldType\DBField; /** * Represents a single year field. */ -class DBYear extends DBField +class DBYear extends ModelField implements DBField { + use DBFieldTrait; + public function requireField(): void { $parts = ['datatype' => 'year', 'precision' => 4, 'arrayValue' => $this->arrayValue]; diff --git a/src/ORM/Filters/SearchFilter.php b/src/ORM/Filters/SearchFilter.php index f622252fbb0..81d964cd7d6 100644 --- a/src/ORM/Filters/SearchFilter.php +++ b/src/ORM/Filters/SearchFilter.php @@ -11,6 +11,7 @@ use SilverStripe\Core\Config\Configurable; use SilverStripe\ORM\DB; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; /** * Base class for filtering implementations, diff --git a/src/ORM/Hierarchy/MarkedSet.php b/src/ORM/Hierarchy/MarkedSet.php index e05b35b791c..b668e51fecb 100644 --- a/src/ORM/Hierarchy/MarkedSet.php +++ b/src/ORM/Hierarchy/MarkedSet.php @@ -9,6 +9,7 @@ use SilverStripe\Model\List\ArrayList; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; use SilverStripe\Model\List\SS_List; use SilverStripe\Model\ArrayData; diff --git a/src/ORM/ManyManyList.php b/src/ORM/ManyManyList.php index 52fdfa2c011..e493ad8035a 100644 --- a/src/ORM/ManyManyList.php +++ b/src/ORM/ManyManyList.php @@ -10,6 +10,7 @@ use InvalidArgumentException; use Exception; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; /** * Subclass of {@link DataList} representing a many_many relation. diff --git a/src/ORM/Relation.php b/src/ORM/Relation.php index 62b2b266cb2..93e12598d84 100644 --- a/src/ORM/Relation.php +++ b/src/ORM/Relation.php @@ -7,6 +7,7 @@ use SilverStripe\Model\List\Sortable; use SilverStripe\Model\List\SS_List; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; /** * Abstract representation of a DB relation field, either saved or in memory diff --git a/src/ORM/UnsavedRelationList.php b/src/ORM/UnsavedRelationList.php index e01ff241e17..1e899bc35f6 100644 --- a/src/ORM/UnsavedRelationList.php +++ b/src/ORM/UnsavedRelationList.php @@ -6,6 +6,7 @@ use ArrayIterator; use SilverStripe\Model\List\ArrayList; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; use Traversable; /** diff --git a/src/Security/CMSSecurity.php b/src/Security/CMSSecurity.php index 290134d4397..cf3df9eacc5 100644 --- a/src/Security/CMSSecurity.php +++ b/src/Security/CMSSecurity.php @@ -10,6 +10,7 @@ use SilverStripe\Core\Convert; use SilverStripe\Core\Manifest\ModuleLoader; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\View\Requirements; use SilverStripe\View\SSViewer; @@ -197,7 +198,7 @@ public function success() // Show login $controller = $controller->customise([ - 'Content' => DBField::create_field(DBHTMLText::class, _t( + 'Content' => DBFieldHelper::create_field(DBHTMLText::class, _t( __CLASS__ . '.SUCCESSCONTENT', '

    Login success. If you are not automatically redirected ' . 'click here

    ', 'Login message displayed in the cms popup once a user has re-authenticated themselves', diff --git a/src/Security/MemberAuthenticator/ChangePasswordHandler.php b/src/Security/MemberAuthenticator/ChangePasswordHandler.php index a2ec7b8c3ac..7df2e63eba5 100644 --- a/src/Security/MemberAuthenticator/ChangePasswordHandler.php +++ b/src/Security/MemberAuthenticator/ChangePasswordHandler.php @@ -10,6 +10,7 @@ use SilverStripe\Core\Injector\Injector; use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; use SilverStripe\Core\Validation\ValidationException; use SilverStripe\Security\Authenticator; use SilverStripe\Security\IdentityStore; @@ -83,7 +84,7 @@ public function changepassword() $session = $this->getRequest()->getSession(); if ($session->get('AutoLoginHash')) { - $message = DBField::create_field( + $message = DBFieldHelper::create_field( 'HTMLFragment', '

    ' . _t( 'SilverStripe\\Security\\Security.ENTERNEWPASSWORD', @@ -100,7 +101,7 @@ public function changepassword() if (Security::getCurrentUser()) { // Logged in user requested a password change form. - $message = DBField::create_field( + $message = DBFieldHelper::create_field( 'HTMLFragment', '

    ' . _t( 'SilverStripe\\Security\\Security.CHANGEPASSWORDBELOW', @@ -115,7 +116,7 @@ public function changepassword() } // Show a friendly message saying the login token has expired if ($token !== null && $member && !$member->validateAutoLoginToken($token)) { - $message = DBField::create_field( + $message = DBFieldHelper::create_field( 'HTMLFragment', _t( 'SilverStripe\\Security\\Security.NOTERESETLINKINVALID', diff --git a/src/Security/MemberAuthenticator/LostPasswordHandler.php b/src/Security/MemberAuthenticator/LostPasswordHandler.php index 0e019552e0d..0dde34bdff4 100644 --- a/src/Security/MemberAuthenticator/LostPasswordHandler.php +++ b/src/Security/MemberAuthenticator/LostPasswordHandler.php @@ -11,6 +11,7 @@ use SilverStripe\Core\Injector\Injector; use SilverStripe\Forms\Form; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; use SilverStripe\Security\Member; use SilverStripe\Security\Security; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; @@ -92,7 +93,7 @@ public function lostpassword() ); return [ - 'Content' => DBField::create_field('HTMLFragment', "

    $message

    "), + 'Content' => DBFieldHelper::create_field('HTMLFragment', "

    $message

    "), 'Form' => $this->lostPasswordForm(), ]; } @@ -115,7 +116,7 @@ public function passwordsent() 'SilverStripe\\Security\\Security.PASSWORDRESETSENTHEADER', "Password reset link sent" ), - 'Content' => DBField::create_field('HTMLFragment', "

    $message

    "), + 'Content' => DBFieldHelper::create_field('HTMLFragment', "

    $message

    "), ]; } diff --git a/src/Security/Security.php b/src/Security/Security.php index be3a14619e7..5a99e0a1489 100644 --- a/src/Security/Security.php +++ b/src/Security/Security.php @@ -22,6 +22,7 @@ use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\Core\Validation\ValidationResult; use SilverStripe\Model\ArrayData; @@ -911,8 +912,8 @@ protected function renderWrappedController($title, array $fragments, array $temp $fragments = array_merge(['Title' => $title], $fragments); if ($message) { $messageResult = [ - 'Content' => DBField::create_field('HTMLFragment', $message), - 'Message' => DBField::create_field('HTMLFragment', $message), + 'Content' => DBFieldHelper::create_field('HTMLFragment', $message), + 'Message' => DBFieldHelper::create_field('HTMLFragment', $message), 'MessageType' => $messageType ]; $fragments = array_merge($fragments, $messageResult); diff --git a/src/View/Requirements_Backend.php b/src/View/Requirements_Backend.php index 869f636a289..1ab20a80c20 100644 --- a/src/View/Requirements_Backend.php +++ b/src/View/Requirements_Backend.php @@ -17,7 +17,7 @@ use SilverStripe\Core\Path; use SilverStripe\Dev\Debug; use SilverStripe\i18n\i18n; -use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; use Symfony\Component\Filesystem\Path as FilesystemPath; class Requirements_Backend @@ -1035,8 +1035,8 @@ public function add_i18n_javascript($langDir, $return = false) i18n::config()->get('default_locale'), i18n::getData()->langFromLocale(i18n::get_locale()), i18n::get_locale(), - strtolower(DBField::create_field('Locale', i18n::get_locale())->RFC1766() ?? ''), - strtolower(DBField::create_field('Locale', i18n::config()->get('default_locale'))->RFC1766() ?? '') + strtolower(DBFieldHelper::create_field('Locale', i18n::get_locale())->RFC1766() ?? ''), + strtolower(DBFieldHelper::create_field('Locale', i18n::config()->get('default_locale'))->RFC1766() ?? '') ]; $candidates = array_map( diff --git a/src/View/SSViewer.php b/src/View/SSViewer.php index 63f0edc344c..159fd8ba488 100644 --- a/src/View/SSViewer.php +++ b/src/View/SSViewer.php @@ -11,7 +11,7 @@ use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injectable; use SilverStripe\Control\Director; -use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\Security\Permission; use InvalidArgumentException; @@ -670,7 +670,7 @@ public function process($item, $arguments = null, $inheritedScope = null) } /** @var DBHTMLText $html */ - $html = DBField::create_field('HTMLFragment', $output); + $html = DBFieldHelper::create_field('HTMLFragment', $output); // Reset global state static::setRewriteHashLinksDefault($origRewriteDefault); diff --git a/src/View/SSViewer_DataPresenter.php b/src/View/SSViewer_DataPresenter.php index dd8d491ef91..2e355a0822b 100644 --- a/src/View/SSViewer_DataPresenter.php +++ b/src/View/SSViewer_DataPresenter.php @@ -6,6 +6,7 @@ use SilverStripe\Core\ClassInfo; use SilverStripe\Model\ModelData; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; /** * This extends SSViewer_Scope to mix in data on top of what the item provides. This can be "global" @@ -438,6 +439,6 @@ protected function castValue($value, $source) ? ModelData::config()->uninherited('default_cast') : $source['casting']; - return DBField::create_field($casting, $value); + return DBFieldHelper::create_field($casting, $value); } } diff --git a/src/View/SSViewer_FromString.php b/src/View/SSViewer_FromString.php index c7712a9f2b3..6450b76ca07 100644 --- a/src/View/SSViewer_FromString.php +++ b/src/View/SSViewer_FromString.php @@ -4,6 +4,7 @@ use SilverStripe\Core\Config\Config; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; /** * Special SSViewer that will process a template passed as a string, rather than a filename. @@ -72,7 +73,7 @@ public function process($item, $arguments = null, $scope = null) unlink($cacheFile ?? ''); } - $html = DBField::create_field('HTMLFragment', $val); + $html = DBFieldHelper::create_field('HTMLFragment', $val); return $html; } diff --git a/src/View/SSViewer_Scope.php b/src/View/SSViewer_Scope.php index 928b7b4a338..085c9bea0fc 100644 --- a/src/View/SSViewer_Scope.php +++ b/src/View/SSViewer_Scope.php @@ -11,6 +11,7 @@ use SilverStripe\ORM\FieldType\DBFloat; use SilverStripe\ORM\FieldType\DBInt; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; /** * This tracks the current scope for an SSViewer instance. It has three goals: diff --git a/src/View/Shortcodes/EmbedShortcodeProvider.php b/src/View/Shortcodes/EmbedShortcodeProvider.php index ed23d778cae..cd8acbdb927 100644 --- a/src/View/Shortcodes/EmbedShortcodeProvider.php +++ b/src/View/Shortcodes/EmbedShortcodeProvider.php @@ -10,6 +10,7 @@ use SilverStripe\Core\Injector\Injector; use SilverStripe\Model\List\ArrayList; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; use SilverStripe\Model\ArrayData; use SilverStripe\View\Embed\Embeddable; use SilverStripe\View\HTML; @@ -197,7 +198,7 @@ protected static function videoEmbed($arguments, $content) $data = [ 'Arguments' => $arguments, 'Attributes' => $attributes, - 'Content' => DBField::create_field('HTMLFragment', $content) + 'Content' => DBFieldHelper::create_field('HTMLFragment', $content) ]; return ArrayData::create($data)->renderWith(EmbedShortcodeProvider::class . '_video')->forTemplate(); diff --git a/tests/php/ORM/DBFieldTest/TestDbField.php b/tests/php/ORM/DBFieldTest/TestDbField.php index ac26b778900..53917db5446 100644 --- a/tests/php/ORM/DBFieldTest/TestDbField.php +++ b/tests/php/ORM/DBFieldTest/TestDbField.php @@ -6,6 +6,7 @@ use SilverStripe\Dev\TestOnly; use SilverStripe\ORM\DB; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; use SilverStripe\Model\ModelData; class TestDbField extends DBField implements TestOnly diff --git a/tests/php/ORM/DataObjectTest/MockDynamicAssignmentDBField.php b/tests/php/ORM/DataObjectTest/MockDynamicAssignmentDBField.php index 6b9d04bfa3b..c680ae0b950 100644 --- a/tests/php/ORM/DataObjectTest/MockDynamicAssignmentDBField.php +++ b/tests/php/ORM/DataObjectTest/MockDynamicAssignmentDBField.php @@ -5,6 +5,7 @@ use SilverStripe\Dev\TestOnly; use SilverStripe\ORM\FieldType\DBBoolean; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBFieldHelper; /** * This is a fake DB field specifically design to test dynamic value assignment. You can set `scalarValueOnly` in