Compare commits

...

8 Commits

Author SHA1 Message Date
Steve Boyd
da474707fb
Merge fe46daa04fa5dbd28a0de1641512313d57ac769e into c523022cb9d52cb8c2567127844f7e16be63cb77 2024-09-24 07:45:02 +00:00
Steve Boyd
fe46daa04f NEW DBField validation 2024-09-24 19:44:55 +12:00
Guy Sartorelli
c523022cb9
Merge branch '5' into 6 2024-09-24 14:07:57 +12:00
Guy Sartorelli
a81b7855de
Merge pull request #11393 from creative-commoners/pulls/5/fix-constructor
FIX Use correct contructor for HTTPOutputHandler
2024-09-24 09:35:35 +12:00
Steve Boyd
e93dafb2fe FIX Use correct contructor for HTTPOutputHandler 2024-09-19 17:21:12 +12:00
Guy Sartorelli
6287b6ebeb
API Rename Deprecation::withNoReplacement (#11390) 2024-09-19 11:27:08 +12:00
Guy Sartorelli
c7ba8d19c5
Merge pull request #11383 from wilr/features/9394-mysqli-flags
feat: support defining MySQLi flags
2024-09-19 09:38:13 +12:00
Will Rossiter
5fa88663b0
feat: support defining MySQLi flags 2024-09-19 07:24:19 +12:00
72 changed files with 1145 additions and 819 deletions

View File

@ -20,6 +20,8 @@ SilverStripe\Core\Injector\Injector:
class: SilverStripe\ORM\FieldType\DBDecimal class: SilverStripe\ORM\FieldType\DBDecimal
Double: Double:
class: SilverStripe\ORM\FieldType\DBDouble class: SilverStripe\ORM\FieldType\DBDouble
Email:
class: SilverStripe\ORM\FieldType\DBEmail
Enum: Enum:
class: SilverStripe\ORM\FieldType\DBEnum class: SilverStripe\ORM\FieldType\DBEnum
Float: Float:

View File

@ -13,6 +13,7 @@ use SilverStripe\Core\Extensible;
use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\Model\ArrayData; use SilverStripe\Model\ArrayData;
use SilverStripe\View\Requirements; use SilverStripe\View\Requirements;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
@ -509,7 +510,7 @@ class Email extends SymfonyEmail
// Plain render fallbacks to using the html render with html tags removed // Plain render fallbacks to using the html render with html tags removed
if (!$plainRender && $htmlRender) { if (!$plainRender && $htmlRender) {
// call html_entity_decode() to ensure any encoded HTML is also stripped inside ->Plain() // 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(); $plainRender = $dbField->Plain();
} }

View File

@ -16,7 +16,7 @@ class CliBypass implements Bypass
{ {
public function __construct() public function __construct()
{ {
Deprecation::withNoReplacement(function () { Deprecation::withSuppressedNotice(function () {
Deprecation::notice( Deprecation::notice(
'5.4.0', '5.4.0',
'Will be removed without equivalent functionality to replace it', 'Will be removed without equivalent functionality to replace it',

View File

@ -4,6 +4,7 @@ namespace SilverStripe\Control\RSS;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Model\ModelData; use SilverStripe\Model\ModelData;
use BadMethodCallException; use BadMethodCallException;

View File

@ -96,7 +96,7 @@ abstract class BuildTask
*/ */
public function getDescription() public function getDescription()
{ {
Deprecation::withNoReplacement( Deprecation::withSuppressedNotice(
fn() => Deprecation::notice('5.4.0', 'Will be replaced with a static method with the same name') fn() => Deprecation::notice('5.4.0', 'Will be replaced with a static method with the same name')
); );
return $this->description; return $this->description;

View File

@ -53,7 +53,7 @@ class Deprecation
/** /**
* @internal * @internal
*/ */
private static bool $insideWithNoReplacement = false; private static bool $insideNoticeSuppression = false;
/** /**
* @internal * @internal
@ -103,22 +103,32 @@ class Deprecation
} }
/** /**
* Used to wrap deprecated methods and deprecated config get()/set() that will be removed * Used to wrap deprecated methods and deprecated config get()/set() called from the vendor
* in the next major version with no replacement. This is done to surpress deprecation notices * dir that projects have no ability to change.
* by for calls from the vendor dir to deprecated code that projects have no ability to change
* *
* @return mixed * @return mixed
* @deprecated 5.4.0 Use withSuppressedNotice() instead
*/ */
public static function withNoReplacement(callable $func) public static function withNoReplacement(callable $func)
{ {
if (Deprecation::$insideWithNoReplacement) { Deprecation::notice('5.4.0', 'Use withSuppressedNotice() instead');
return Deprecation::withSuppressedNotice($func);
}
/**
* Used to wrap deprecated methods and deprecated config get()/set() called from the vendor
* dir that projects have no ability to change.
*/
public static function withSuppressedNotice(callable $func): mixed
{
if (Deprecation::$insideNoticeSuppression) {
return $func(); return $func();
} }
Deprecation::$insideWithNoReplacement = true; Deprecation::$insideNoticeSuppression = true;
try { try {
return $func(); return $func();
} finally { } finally {
Deprecation::$insideWithNoReplacement = false; Deprecation::$insideNoticeSuppression = false;
} }
} }
@ -137,8 +147,8 @@ class Deprecation
$level = 1; $level = 1;
} }
$newLevel = $level; $newLevel = $level;
// handle closures inside withNoReplacement() // handle closures inside withSuppressedNotice()
if (Deprecation::$insideWithNoReplacement if (Deprecation::$insideNoticeSuppression
&& substr($backtrace[$newLevel]['function'], -strlen('{closure}')) === '{closure}' && substr($backtrace[$newLevel]['function'], -strlen('{closure}')) === '{closure}'
) { ) {
$newLevel = $newLevel + 2; $newLevel = $newLevel + 2;
@ -247,8 +257,8 @@ class Deprecation
$count++; $count++;
$arr = array_shift(Deprecation::$userErrorMessageBuffer); $arr = array_shift(Deprecation::$userErrorMessageBuffer);
$message = $arr['message']; $message = $arr['message'];
$calledInsideWithNoReplacement = $arr['calledInsideWithNoReplacement']; $calledWithNoticeSuppression = $arr['calledWithNoticeSuppression'];
if ($calledInsideWithNoReplacement && !Deprecation::$showNoReplacementNotices) { if ($calledWithNoticeSuppression && !Deprecation::$showNoReplacementNotices) {
continue; continue;
} }
Deprecation::$isTriggeringError = true; Deprecation::$isTriggeringError = true;
@ -284,7 +294,7 @@ class Deprecation
$data = [ $data = [
'key' => sha1($string), 'key' => sha1($string),
'message' => $string, 'message' => $string,
'calledInsideWithNoReplacement' => Deprecation::$insideWithNoReplacement 'calledWithNoticeSuppression' => Deprecation::$insideNoticeSuppression
]; ];
} else { } else {
if (!Deprecation::isEnabled()) { if (!Deprecation::isEnabled()) {
@ -310,7 +320,7 @@ class Deprecation
$string .= "."; $string .= ".";
} }
$level = Deprecation::$insideWithNoReplacement ? 4 : 2; $level = Deprecation::$insideNoticeSuppression ? 4 : 2;
$string .= " Called from " . Deprecation::get_called_method_from_trace($backtrace, $level) . '.'; $string .= " Called from " . Deprecation::get_called_method_from_trace($backtrace, $level) . '.';
if ($caller) { if ($caller) {
@ -319,7 +329,7 @@ class Deprecation
$data = [ $data = [
'key' => sha1($string), 'key' => sha1($string),
'message' => $string, 'message' => $string,
'calledInsideWithNoReplacement' => Deprecation::$insideWithNoReplacement 'calledWithNoticeSuppression' => Deprecation::$insideNoticeSuppression
]; ];
} }
if ($data && !array_key_exists($data['key'], Deprecation::$userErrorMessageBuffer)) { if ($data && !array_key_exists($data['key'], Deprecation::$userErrorMessageBuffer)) {

View File

@ -34,7 +34,7 @@ class DevBuildController extends Controller implements PermissionProvider
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
Deprecation::withNoReplacement(function () { Deprecation::withSuppressedNotice(function () {
Deprecation::notice( Deprecation::notice(
'5.4.0', '5.4.0',
'Will be replaced with SilverStripe\Dev\Command\DbBuild', 'Will be replaced with SilverStripe\Dev\Command\DbBuild',

View File

@ -47,7 +47,7 @@ class DevConfigController extends Controller implements PermissionProvider
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
Deprecation::withNoReplacement(function () { Deprecation::withSuppressedNotice(function () {
Deprecation::notice( Deprecation::notice(
'5.4.0', '5.4.0',
'Will be replaced with SilverStripe\Dev\Command\ConfigDump', 'Will be replaced with SilverStripe\Dev\Command\ConfigDump',

View File

@ -233,7 +233,7 @@ class DevelopmentAdmin extends Controller implements PermissionProvider
*/ */
public function buildDefaults() public function buildDefaults()
{ {
Deprecation::withNoReplacement(function () { Deprecation::withSuppressedNotice(function () {
Deprecation::notice( Deprecation::notice(
'5.4.0', '5.4.0',
'Will be replaced with SilverStripe\Dev\Command\DbDefaults' 'Will be replaced with SilverStripe\Dev\Command\DbDefaults'
@ -266,7 +266,7 @@ class DevelopmentAdmin extends Controller implements PermissionProvider
*/ */
public function generatesecuretoken() public function generatesecuretoken()
{ {
Deprecation::withNoReplacement(function () { Deprecation::withSuppressedNotice(function () {
Deprecation::notice( Deprecation::notice(
'5.4.0', '5.4.0',
'Will be replaced with SilverStripe\Dev\Command\GenerateSecureToken' 'Will be replaced with SilverStripe\Dev\Command\GenerateSecureToken'

View File

@ -36,7 +36,7 @@ class CleanupTestDatabasesTask extends BuildTask
public function canView(): bool public function canView(): bool
{ {
Deprecation::withNoReplacement(function () { Deprecation::withSuppressedNotice(function () {
Deprecation::notice( Deprecation::notice(
'5.4.0', '5.4.0',
'Will be replaced with canRunInBrowser()' 'Will be replaced with canRunInBrowser()'

View File

@ -6,6 +6,7 @@ use LogicException;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\Security\Authenticator; use SilverStripe\Security\Authenticator;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\View\HTML; use SilverStripe\View\HTML;
@ -771,6 +772,6 @@ class ConfirmedPasswordField extends FormField
if (strpos($attributes, 'required="required"') === false && $this->Required()) { if (strpos($attributes, 'required="required"') === false && $this->Required()) {
$attributes .= ' required="required" aria-required="true"'; $attributes .= ' required="required" aria-required="true"';
} }
return DBField::create_field('HTMLFragment', $attributes); return DBFieldHelper::create_field('HTMLFragment', $attributes);
} }
} }

View File

@ -2,6 +2,9 @@
namespace SilverStripe\Forms; 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. * Text input field with validation for correct email format according to RFC 2822.
*/ */
@ -18,32 +21,27 @@ class EmailField extends TextField
} }
/** /**
* 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 * @param Validator $validator
* *
* @return string * @return string
*/ */
public function validate($validator) public function validate($validator)
{ {
$result = true;
$this->value = trim($this->value ?? ''); $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])?$'; $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();
// Escape delimiter characters. if (!$validationResult->isValid()) {
$safePattern = str_replace('/', '\\/', $pattern ?? '');
if ($this->value && !preg_match('/' . $safePattern . '/i', $this->value ?? '')) {
$validator->validationError( $validator->validationError(
$this->name, $this->name,
_t('SilverStripe\\Forms\\EmailField.VALIDATION', 'Please enter an email address'), $validationResult->getMessages()[0]['message'],
'validation'
); );
$result = false; $result = false;
} }

View File

@ -10,11 +10,16 @@ use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Core\Validation\ValidationResult; use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\View\AttributesHTML; use SilverStripe\View\AttributesHTML;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
use SilverStripe\Model\ModelData; 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. * Represents a field in a form.
@ -40,11 +45,28 @@ use SilverStripe\Model\ModelData;
* including both structure (name, id, attributes, etc.) and state (field value). * 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. * 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 AttributesHTML;
use FormMessage; 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 */ /** @see $schemaDataType */
const SCHEMA_DATA_TYPE_STRING = 'String'; const SCHEMA_DATA_TYPE_STRING = 'String';
@ -344,6 +366,7 @@ class FormField extends RequestHandler
parent::__construct(); parent::__construct();
$this->initModelField($name);
$this->setupDefaultClasses(); $this->setupDefaultClasses();
} }
@ -929,7 +952,7 @@ class FormField extends RequestHandler
// Trim whitespace from the result, so that trailing newlines are suppressed. Works for strings and HTMLText values // Trim whitespace from the result, so that trailing newlines are suppressed. Works for strings and HTMLText values
if (is_string($result)) { if (is_string($result)) {
$result = trim($result ?? ''); $result = trim($result ?? '');
} elseif ($result instanceof DBField) { } elseif ($result instanceof ModelField) {
$result->setValue(trim($result->getValue() ?? '')); $result->setValue(trim($result->getValue() ?? ''));
} }
@ -1231,9 +1254,12 @@ class FormField extends RequestHandler
} }
/** /**
* 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. * 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 * @param Validator $validator
* @return bool * @return bool
*/ */

View File

@ -216,10 +216,10 @@ class FormRequestHandler extends RequestHandler
// Action handlers may throw ValidationExceptions. // Action handlers may throw ValidationExceptions.
try { try {
// Or we can use the Validator attached to the form // Or we can use the Validator attached to the form
$result = $this->form->validationResult(); // $result = $this->form->validationResult();
if (!$result->isValid()) { // if (!$result->isValid()) {
return $this->getValidationErrorResponse($result); // return $this->getValidationErrorResponse($result);
} // }
// First, try a handler method on the controller (has been checked for allowed_actions above already) // First, try a handler method on the controller (has been checked for allowed_actions above already)
$controller = $this->form->getController(); $controller = $this->form->getController();

View File

@ -22,6 +22,7 @@ use SilverStripe\Model\List\ArrayList;
use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\Model\List\Filterable; use SilverStripe\Model\List\Filterable;
use SilverStripe\Model\List\Limitable; use SilverStripe\Model\List\Limitable;
use SilverStripe\Model\List\Sortable; use SilverStripe\Model\List\Sortable;
@ -382,14 +383,14 @@ class GridField extends FormField
if (strpos($castingDefinition ?? '', '->') === false) { if (strpos($castingDefinition ?? '', '->') === false) {
$castingFieldType = $castingDefinition; $castingFieldType = $castingDefinition;
$castingField = DBField::create_field($castingFieldType, $value); $castingField = DBFieldHelper::create_field($castingFieldType, $value);
return call_user_func_array([$castingField, 'XML'], $castingParams ?? []); return call_user_func_array([$castingField, 'XML'], $castingParams ?? []);
} }
list($castingFieldType, $castingMethod) = explode('->', $castingDefinition ?? ''); 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 ?? []); return call_user_func_array([$castingField, $castingMethod], $castingParams ?? []);
} }
@ -543,6 +544,8 @@ class GridField extends FormField
'footer' => '', 'footer' => '',
]; ];
$_c = $this->getComponents();
foreach ($this->getComponents() as $item) { foreach ($this->getComponents() as $item) {
if ($item instanceof GridField_HTMLProvider) { if ($item instanceof GridField_HTMLProvider) {
$fragments = $item->getHTMLFragments($this); $fragments = $item->getHTMLFragments($this);

View File

@ -25,7 +25,7 @@ class GridFieldConfig_Base extends GridFieldConfig
$this->addComponent(GridFieldPageCount::create('toolbar-header-right')); $this->addComponent(GridFieldPageCount::create('toolbar-header-right'));
$this->addComponent($pagination = GridFieldPaginator::create($itemsPerPage)); $this->addComponent($pagination = GridFieldPaginator::create($itemsPerPage));
Deprecation::withNoReplacement(function () use ($sort, $filter, $pagination) { Deprecation::withSuppressedNotice(function () use ($sort, $filter, $pagination) {
$sort->setThrowExceptionOnBadDataType(false); $sort->setThrowExceptionOnBadDataType(false);
$filter->setThrowExceptionOnBadDataType(false); $filter->setThrowExceptionOnBadDataType(false);
$pagination->setThrowExceptionOnBadDataType(false); $pagination->setThrowExceptionOnBadDataType(false);

View File

@ -32,7 +32,7 @@ class GridFieldConfig_RecordEditor extends GridFieldConfig
$this->addComponent($pagination = GridFieldPaginator::create($itemsPerPage)); $this->addComponent($pagination = GridFieldPaginator::create($itemsPerPage));
$this->addComponent(GridFieldDetailForm::create(null, $showPagination, $showAdd)); $this->addComponent(GridFieldDetailForm::create(null, $showPagination, $showAdd));
Deprecation::withNoReplacement(function () use ($sort, $filter, $pagination) { Deprecation::withSuppressedNotice(function () use ($sort, $filter, $pagination) {
$sort->setThrowExceptionOnBadDataType(false); $sort->setThrowExceptionOnBadDataType(false);
$filter->setThrowExceptionOnBadDataType(false); $filter->setThrowExceptionOnBadDataType(false);
$pagination->setThrowExceptionOnBadDataType(false); $pagination->setThrowExceptionOnBadDataType(false);

View File

@ -45,7 +45,7 @@ class GridFieldConfig_RelationEditor extends GridFieldConfig
$this->addComponent($pagination = GridFieldPaginator::create($itemsPerPage)); $this->addComponent($pagination = GridFieldPaginator::create($itemsPerPage));
$this->addComponent(GridFieldDetailForm::create()); $this->addComponent(GridFieldDetailForm::create());
Deprecation::withNoReplacement(function () use ($sort, $filter, $pagination) { Deprecation::withSuppressedNotice(function () use ($sort, $filter, $pagination) {
$sort->setThrowExceptionOnBadDataType(false); $sort->setThrowExceptionOnBadDataType(false);
$filter->setThrowExceptionOnBadDataType(false); $filter->setThrowExceptionOnBadDataType(false);
$pagination->setThrowExceptionOnBadDataType(false); $pagination->setThrowExceptionOnBadDataType(false);

View File

@ -5,6 +5,7 @@ namespace SilverStripe\Forms\GridField;
use LogicException; use LogicException;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\ORM\Hierarchy\Hierarchy; use SilverStripe\ORM\Hierarchy\Hierarchy;
use SilverStripe\Model\ArrayData; use SilverStripe\Model\ArrayData;
use SilverStripe\View\HTML; use SilverStripe\View\HTML;
@ -91,7 +92,7 @@ class GridFieldLevelup extends AbstractGridFieldComponent implements GridField_H
$linkTag = HTML::createTag('a', $attrs); $linkTag = HTML::createTag('a', $attrs);
$forTemplate = new ArrayData([ $forTemplate = new ArrayData([
'UpLink' => DBField::create_field('HTMLFragment', $linkTag) 'UpLink' => DBFieldHelper::create_field('HTMLFragment', $linkTag)
]); ]);
$template = SSViewer::get_templates_by_class($this, '', __CLASS__); $template = SSViewer::get_templates_by_class($this, '', __CLASS__);

View File

@ -6,6 +6,7 @@ use SilverStripe\Core\Convert;
use SilverStripe\Core\ArrayLib; use SilverStripe\Core\ArrayLib;
use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
/** /**
* Read-only complement of {@link MultiSelectField}. * Read-only complement of {@link MultiSelectField}.
@ -58,7 +59,7 @@ class LookupField extends MultiSelectField
} }
$properties = array_merge($properties, [ $properties = array_merge($properties, [
'AttrValue' => DBField::create_field('HTMLFragment', $attrValue), 'AttrValue' => DBFieldHelper::create_field('HTMLFragment', $attrValue),
'InputValue' => $inputValue 'InputValue' => $inputValue
]); ]);

View File

@ -6,6 +6,7 @@ use SilverStripe\Core\Convert;
use SilverStripe\Core\ArrayLib; use SilverStripe\Core\ArrayLib;
use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\Model\List\ArrayList; use SilverStripe\Model\List\ArrayList;
use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataList;

View File

@ -4,6 +4,7 @@ namespace SilverStripe\Forms;
use SilverStripe\Model\List\ArrayList; use SilverStripe\Model\List\ArrayList;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\View\HTML; use SilverStripe\View\HTML;
/** /**
@ -91,7 +92,7 @@ class SelectionGroup extends CompositeField
$itemID = $this->ID() . '_' . (++$count); $itemID = $this->ID() . '_' . (++$count);
$extra = [ $extra = [
"RadioButton" => DBField::create_field('HTMLFragment', HTML::createTag( "RadioButton" => DBFieldHelper::create_field('HTMLFragment', HTML::createTag(
'input', 'input',
[ [
'class' => 'selector', 'class' => 'selector',

View File

@ -6,6 +6,7 @@ use SilverStripe\Core\Convert;
use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\Model\List\Map; use SilverStripe\Model\List\Map;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
/** /**
* Read-only complement of {@link DropdownField}. * Read-only complement of {@link DropdownField}.
@ -125,7 +126,7 @@ class SingleLookupField extends SingleSelectField
} }
$properties = array_merge($properties, [ $properties = array_merge($properties, [
'AttrValue' => DBField::create_field('HTMLFragment', $attrValue), 'AttrValue' => DBFieldHelper::create_field('HTMLFragment', $attrValue),
'InputValue' => $inputValue 'InputValue' => $inputValue
]); ]);

View File

@ -2,11 +2,15 @@
namespace SilverStripe\Forms; namespace SilverStripe\Forms;
use SilverStripe\Model\ModelFields\StringModelField;
/** /**
* Text input field. * Text input field.
*/ */
class TextField extends FormField implements TippableFieldInterface class TextField extends FormField implements TippableFieldInterface
{ {
protected string $modelFieldClass = StringModelField::class;
/** /**
* @var int * @var int
*/ */

View File

@ -4,6 +4,7 @@ namespace SilverStripe\Logging;
use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\FormatterInterface;
use Monolog\Handler\AbstractProcessingHandler; use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Level;
use Monolog\LogRecord; use Monolog\LogRecord;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
@ -34,10 +35,10 @@ class HTTPOutputHandler extends AbstractProcessingHandler
*/ */
private $cliFormatter = null; private $cliFormatter = null;
public function __construct() public function __construct(int|string|Level $level = Level::Debug, bool $bubble = true)
{ {
parent::__construct(); parent::__construct($level, $bubble);
Deprecation::withNoReplacement(function () { Deprecation::withSuppressedNotice(function () {
Deprecation::notice( Deprecation::notice(
'5.4.0', '5.4.0',
'Will be renamed to ErrorOutputHandler', 'Will be renamed to ErrorOutputHandler',

View File

@ -720,7 +720,7 @@ class ArrayList extends ModelData implements SS_List, Filterable, Sortable, Limi
// Apply default case sensitivity for backwards compatability // Apply default case sensitivity for backwards compatability
if (!str_contains($filterKey, ':case') && !str_contains($filterKey, ':nocase')) { if (!str_contains($filterKey, ':case') && !str_contains($filterKey, ':nocase')) {
$caseSensitive = Deprecation::withNoReplacement(fn() => static::config()->get('default_case_sensitive')); $caseSensitive = Deprecation::withSuppressedNotice(fn() => static::config()->get('default_case_sensitive'));
if ($caseSensitive && in_array('case', $searchFilter->getSupportedModifiers())) { if ($caseSensitive && in_array('case', $searchFilter->getSupportedModifiers())) {
$searchFilter->setModifiers($searchFilter->getModifiers() + ['case']); $searchFilter->setModifiers($searchFilter->getModifiers() + ['case']);
} elseif (!$caseSensitive && in_array('nocase', $searchFilter->getSupportedModifiers())) { } elseif (!$caseSensitive && in_array('nocase', $searchFilter->getSupportedModifiers())) {

View File

@ -16,11 +16,12 @@ use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Debug; use SilverStripe\Dev\Debug;
use SilverStripe\Core\ArrayLib; use SilverStripe\Core\ArrayLib;
use SilverStripe\Model\List\ArrayList; use SilverStripe\Model\List\ArrayList;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Model\ArrayData; use SilverStripe\Model\ArrayData;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
use UnexpectedValueException; 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. * A ModelData object is any object that can be rendered into a template/view.
@ -327,7 +328,7 @@ class ModelData
/** /**
* Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value 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. * @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. * @return string|null Casting helper As a constructor pattern, and may include arguments.
@ -361,7 +362,7 @@ class ModelData
/** /**
* Return the default "casting helper" for use when no explicit casting helper is defined. * 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 protected function defaultCastingHelper(string $field): string
{ {
@ -402,7 +403,7 @@ class ModelData
{ {
$class = $this->castingClass($field) ?: $this->config()->get('default_cast'); $class = $this->castingClass($field) ?: $this->config()->get('default_cast');
/** @var DBField $type */ /** @var ModelField $type */
$type = Injector::inst()->get($class, true); $type = Injector::inst()->get($class, true);
return $type->config()->get('escape_type'); return $type->config()->get('escape_type');
} }
@ -495,9 +496,9 @@ class ModelData
* Get the value of a field on this object, automatically inserting the value into any available casting objects * Get the value of a field on this object, automatically inserting the value into any available casting objects
* that have been specified. * that have been specified.
* *
* @return object|DBField|null The specific object representing the field, or null if there is no * @return object|ModelField|null The specific object representing the field, or null if there is no
* property, method, or dynamic data available for that field. * 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. * be returned.
*/ */
public function obj( public function obj(
@ -520,7 +521,13 @@ class ModelData
// Load value from record // Load value from record
if ($this->hasMethod($fieldName)) { if ($this->hasMethod($fieldName)) {
$hasObj = true; $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 { } else {
$hasObj = $this->hasField($fieldName) || ($this->hasMethod("get{$fieldName}") && $this->isAccessibleMethod("get{$fieldName}")); $hasObj = $this->hasField($fieldName) || ($this->hasMethod("get{$fieldName}") && $this->isAccessibleMethod("get{$fieldName}"));
$value = $this->$fieldName; $value = $this->$fieldName;
@ -568,7 +575,7 @@ class ModelData
* A simple wrapper around {@link ModelData::obj()} that automatically caches the result so it can be used again * A simple wrapper around {@link ModelData::obj()} that automatically caches the result so it can be used again
* without re-running the method. * without re-running the method.
* *
* @return Object|DBField * @return Object|ModelField
*/ */
public function cachedCall(string $fieldName, array $arguments = [], ?string $cacheName = null): object public function cachedCall(string $fieldName, array $arguments = [], ?string $cacheName = null): object
{ {
@ -599,7 +606,13 @@ class ModelData
return ''; return '';
} }
// Might contain additional formatting over ->XML(). E.g. parse shortcodes, nl2br() // 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;
}
} }
/** /**

View File

@ -0,0 +1,25 @@
<?php
namespace SilverStripe\Model\ModelFields;
use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\Model\ModelFields\StringModelField;
use SilverStripe\Core\Validation\ConstraintValidator;
use Symfony\Component\Validator\Constraints;
class EmailModelField extends StringModelField
{
public function validate(): ValidationResult
{
$result = parent::validate();
$message = _t('SilverStripe\\Forms\\EmailField.VALIDATION', 'Please enter an email address');
$validationResult = ConstraintValidator::validate(
$this->getValue(),
new Constraints\Email(message: $message),
$this->getName()
);
return $result->combineAnd($validationResult);
}
}

View File

@ -0,0 +1,249 @@
<?php
namespace SilverStripe\Model\ModelFields;
use SilverStripe\Core\Convert;
use SilverStripe\Forms\FormField;
use SilverStripe\Forms\TextField;
use SilverStripe\Model\ModelData;
use SilverStripe\Core\Validation\ValidationResult;
abstract class ModelField extends ModelData
{
/**
* Raw value of this field
*/
protected mixed $value = null;
/**
* Name of this field
*/
protected ?string $name = null;
/**
* The escape type for this field when inserted into a template - either "xml" or "raw".
*/
private static string $escape_type = 'raw';
private static array $casting = [
'ATT' => '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 <<<DBG
<ul>
<li><b>Name:</b>{$this->name}</li>
<li><b>Table:</b>{$this->tableName}</li>
<li><b>Value:</b>{$this->value}</li>
</ul>
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;
}
}

View File

@ -0,0 +1,142 @@
<?php
namespace SilverStripe\Model\ModelFields;
use SilverStripe\Model\ModelFields\ModelField;
use SilverStripe\ORM\FieldType\DBString;
class StringModelField extends ModelField
{
private static array $casting = [
'LimitCharacters' => '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', '…');
}
}

View File

@ -10,6 +10,7 @@ use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\ORM\FieldType\DBPrimaryKey; use SilverStripe\ORM\FieldType\DBPrimaryKey;
/** /**

View File

@ -6,6 +6,7 @@ use mysqli;
use mysqli_sql_exception; use mysqli_sql_exception;
use mysqli_stmt; use mysqli_stmt;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Environment;
/** /**
* Connector for MySQL using the MySQLi method * Connector for MySQL using the MySQLi method
@ -81,15 +82,21 @@ class MySQLiConnector extends DBConnector
// Connection charset and collation // Connection charset and collation
$connCharset = Config::inst()->get(MySQLDatabase::class, 'connection_charset'); $connCharset = Config::inst()->get(MySQLDatabase::class, 'connection_charset');
$connCollation = Config::inst()->get(MySQLDatabase::class, 'connection_collation'); $connCollation = Config::inst()->get(MySQLDatabase::class, 'connection_collation');
$socket = Environment::getEnv('SS_DATABASE_SOCKET');
$flags = Environment::getEnv('SS_DATABASE_FLAGS');
$flags = $flags ? array_reduce(explode(',', $flags), function ($carry, $item) {
$item = trim($item);
return $carry | constant($item);
}, 0) : $flags;
$this->dbConn = mysqli_init(); $this->dbConn = mysqli_init();
// Use native types (MysqlND only) // Use native types (MysqlND only)
if (defined('MYSQLI_OPT_INT_AND_FLOAT_NATIVE')) { if (defined('MYSQLI_OPT_INT_AND_FLOAT_NATIVE')) {
$this->dbConn->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true); $this->dbConn->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
// The alternative is not ideal, throw a notice-level error
} else { } else {
// The alternative is not ideal, throw a notice-level error
user_error( user_error(
'mysqlnd PHP library is not available, numeric values will be fetched from the DB as strings', 'mysqlnd PHP library is not available, numeric values will be fetched from the DB as strings',
E_USER_NOTICE E_USER_NOTICE
@ -117,7 +124,9 @@ class MySQLiConnector extends DBConnector
$parameters['username'], $parameters['username'],
$parameters['password'], $parameters['password'],
$selectedDB, $selectedDB,
!empty($parameters['port']) ? $parameters['port'] : ini_get("mysqli.default_port") !empty($parameters['port']) ? $parameters['port'] : ini_get("mysqli.default_port"),
$socket ?? null,
$flags ?? 0
); );
if ($this->dbConn->connect_error) { if ($this->dbConn->connect_error) {
@ -126,8 +135,8 @@ class MySQLiConnector extends DBConnector
// Set charset and collation if given and not null. Can explicitly set to empty string to omit // Set charset and collation if given and not null. Can explicitly set to empty string to omit
$charset = isset($parameters['charset']) $charset = isset($parameters['charset'])
? $parameters['charset'] ? $parameters['charset']
: $connCharset; : $connCharset;
if (!empty($charset)) { if (!empty($charset)) {
$this->dbConn->set_charset($charset); $this->dbConn->set_charset($charset);

View File

@ -20,6 +20,7 @@ use SilverStripe\Forms\FormField;
use SilverStripe\Forms\FormScaffolder; use SilverStripe\Forms\FormScaffolder;
use SilverStripe\Forms\CompositeValidator; use SilverStripe\Forms\CompositeValidator;
use SilverStripe\Forms\FieldsValidator; use SilverStripe\Forms\FieldsValidator;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor; use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
use SilverStripe\Forms\HiddenField; use SilverStripe\Forms\HiddenField;
@ -33,6 +34,7 @@ use SilverStripe\ORM\FieldType\DBComposite;
use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBEnum; use SilverStripe\ORM\FieldType\DBEnum;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\ORM\FieldType\DBForeignKey; use SilverStripe\ORM\FieldType\DBForeignKey;
use SilverStripe\ORM\Filters\PartialMatchFilter; use SilverStripe\ORM\Filters\PartialMatchFilter;
use SilverStripe\ORM\Filters\SearchFilter; use SilverStripe\ORM\Filters\SearchFilter;
@ -45,7 +47,13 @@ use SilverStripe\Security\Permission;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
use SilverStripe\Model\ModelData; use SilverStripe\Model\ModelData;
use SilverStripe\ORM\FieldType\DBText;
use SilverStripe\ORM\FieldType\DBVarchar;
use stdClass; 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. * A single database record & abstract class for the data-access-model.
@ -1230,10 +1238,44 @@ class DataObject extends ModelData implements DataObjectInterface, i18nEntityPro
public function validate() public function validate()
{ {
$result = ValidationResult::create(); $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); $this->extend('updateValidate', $result);
return $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. * Event handler called before writing to the database.
* You can overload this to clean up or otherwise process data before writing it to the * You can overload this to clean up or otherwise process data before writing it to the
@ -1456,7 +1498,7 @@ class DataObject extends ModelData implements DataObjectInterface, i18nEntityPro
// if database column doesn't correlate to a DBField instance... // if database column doesn't correlate to a DBField instance...
$fieldObj = $this->dbObject($fieldName); $fieldObj = $this->dbObject($fieldName);
if (!$fieldObj) { if (!$fieldObj) {
$fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName); $fieldObj = DBFieldHelper::create_field('Varchar', $fieldValue, $fieldName);
} }
// Write to manipulation // Write to manipulation
@ -1982,7 +2024,7 @@ class DataObject extends ModelData implements DataObjectInterface, i18nEntityPro
if ($details['polymorphic']) { if ($details['polymorphic']) {
$result = PolymorphicHasManyList::create($componentClass, $details['joinField'], static::class); $result = PolymorphicHasManyList::create($componentClass, $details['joinField'], static::class);
if ($details['needsRelation']) { if ($details['needsRelation']) {
Deprecation::withNoReplacement(fn () => $result->setForeignRelation($componentName)); Deprecation::withSuppressedNotice(fn () => $result->setForeignRelation($componentName));
} }
} else { } else {
$result = HasManyList::create($componentClass, $details['joinField']); $result = HasManyList::create($componentClass, $details['joinField']);
@ -2422,16 +2464,15 @@ class DataObject extends ModelData implements DataObjectInterface, i18nEntityPro
} }
// Otherwise, use the database field's scaffolder // Otherwise, use the database field's scaffolder
} elseif ($object = $this->relObject($fieldName)) { } elseif ($dbField = $this->relObject($fieldName)) {
if (is_object($object) && $object->hasMethod('scaffoldSearchField')) { if (!is_a($dbField, DBField::class)) {
$field = $object->scaffoldSearchField();
} else {
throw new Exception(sprintf( throw new Exception(sprintf(
"SearchField '%s' on '%s' does not return a valid DBField instance.", "SearchField '%s' on '%s' does not return a valid DBField instance.",
$fieldName, $fieldName,
get_class($this) get_class($this)
)); ));
} }
$field = $dbField->scaffoldSearchField();
} }
// Allow fields to opt out of search // Allow fields to opt out of search

View File

@ -16,6 +16,7 @@ use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\Connect\DBSchemaManager; use SilverStripe\ORM\Connect\DBSchemaManager;
use SilverStripe\ORM\FieldType\DBComposite; use SilverStripe\ORM\FieldType\DBComposite;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
/** /**
* Provides dataobject and database schema mapping functionality * Provides dataobject and database schema mapping functionality

View File

@ -67,7 +67,7 @@ class DatabaseAdmin extends Controller
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
Deprecation::withNoReplacement(function () { Deprecation::withSuppressedNotice(function () {
Deprecation::notice( Deprecation::notice(
'5.4.0', '5.4.0',
'Will be replaced with SilverStripe\Dev\Command\DbBuild', 'Will be replaced with SilverStripe\Dev\Command\DbBuild',
@ -213,7 +213,7 @@ class DatabaseAdmin extends Controller
*/ */
public static function lastBuilt() public static function lastBuilt()
{ {
Deprecation::withNoReplacement(function () { Deprecation::withSuppressedNotice(function () {
Deprecation::notice( Deprecation::notice(
'5.4.0', '5.4.0',
'Will be replaced with SilverStripe\Dev\Command\DbBuild::lastBuilt()' 'Will be replaced with SilverStripe\Dev\Command\DbBuild::lastBuilt()'

View File

@ -6,6 +6,7 @@ use SilverStripe\Dev\Debug;
use SilverStripe\Model\ModelData; use SilverStripe\Model\ModelData;
use SilverStripe\ORM\Connect\DatabaseException; use SilverStripe\ORM\Connect\DatabaseException;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use BadMethodCallException; use BadMethodCallException;
use InvalidArgumentException; use InvalidArgumentException;
use LogicException; use LogicException;

View File

@ -7,12 +7,17 @@ use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\FormField; use SilverStripe\Forms\FormField;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\Model\ModelData; use SilverStripe\Model\ModelData;
use SilverStripe\Model\ModelFields\ModelField;
use SilverStripe\ORM\FieldType\DBFieldTrait;
use SilverStripe\ORM\FieldType\DBField;
/** /**
* Represents a boolean field. * 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) public function __construct(?string $name = null, bool|int $defaultVal = 0)
{ {
$this->defaultVal = ($defaultVal) ? 1 : 0; $this->defaultVal = ($defaultVal) ? 1 : 0;

View File

@ -8,6 +8,9 @@ use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\Queries\SQLSelect; use SilverStripe\ORM\Queries\SQLSelect;
use SilverStripe\Model\ModelData; 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. * Extend this class when designing a {@link DBField} that doesn't have a 1-1 mapping with a database field.
@ -23,8 +26,10 @@ use SilverStripe\Model\ModelData;
* } * }
* </code> * </code>
*/ */
abstract class DBComposite extends DBField abstract class DBComposite extends ModelField implements DBField
{ {
use DBFieldTrait;
/** /**
* Similar to {@link DataObject::$db}, * Similar to {@link DataObject::$db},
* holds an array of composite field names. * holds an array of composite field names.
@ -85,8 +90,6 @@ abstract class DBComposite extends DBField
*/ */
public function addToQuery(SQLSelect &$query): void public function addToQuery(SQLSelect &$query): void
{ {
parent::addToQuery($query);
foreach ($this->compositeDatabaseFields() as $field => $spec) { foreach ($this->compositeDatabaseFields() as $field => $spec) {
$table = $this->getTable(); $table = $this->getTable();
$key = $this->getName() . $field; $key = $this->getName() . $field;

View File

@ -12,6 +12,9 @@ use SilverStripe\ORM\DB;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\Model\ModelData; use SilverStripe\Model\ModelData;
use SilverStripe\ORM\FieldType\DBFieldTrait;
use SilverStripe\Model\ModelFields\ModelField;
use SilverStripe\ORM\FieldType\DBField;
/** /**
* Represents a date field. * Represents a date field.
@ -29,8 +32,10 @@ use SilverStripe\Model\ModelData;
* Date formats all follow CLDR standard format codes * Date formats all follow CLDR standard format codes
* @link https://unicode-org.github.io/icu/userguide/format_parse/datetime * @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 * Standard ISO format string for date in CLDR standard format
*/ */

View File

@ -260,7 +260,7 @@ class DBDatetime extends DBDate implements TemplateGlobalProvider
$time = DBDatetime::$mock_now ? DBDatetime::$mock_now->Value : time(); $time = DBDatetime::$mock_now ? DBDatetime::$mock_now->Value : time();
/** @var DBDatetime $now */ /** @var DBDatetime $now */
$now = DBField::create_field('Datetime', $time); $now = DBFieldHelper::create_field('Datetime', $time);
return $now; return $now;
} }
@ -277,7 +277,7 @@ class DBDatetime extends DBDate implements TemplateGlobalProvider
{ {
if (!$datetime instanceof DBDatetime) { if (!$datetime instanceof DBDatetime) {
$value = $datetime; $value = $datetime;
$datetime = DBField::create_field('Datetime', $datetime); $datetime = DBFieldHelper::create_field('Datetime', $datetime);
if ($datetime === false) { if ($datetime === false) {
throw new InvalidArgumentException('DBDatetime::set_mock_now(): Wrong format: ' . $value); throw new InvalidArgumentException('DBDatetime::set_mock_now(): Wrong format: ' . $value);
} }

View File

@ -6,12 +6,16 @@ use SilverStripe\Forms\FormField;
use SilverStripe\Forms\NumericField; use SilverStripe\Forms\NumericField;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\Model\ModelData; use SilverStripe\Model\ModelData;
use SilverStripe\ORM\FieldType\DBFieldTrait;
use SilverStripe\Model\ModelFields\ModelField;
use SilverStripe\ORM\FieldType\DBField;
/** /**
* Represents a Decimal field. * Represents a Decimal field.
*/ */
class DBDecimal extends DBField class DBDecimal extends ModelField implements DBField
{ {
use DBFieldTrait;
/** /**
* Whole number size * Whole number size
*/ */

View File

@ -0,0 +1,44 @@
<?php
namespace SilverStripe\ORM\FieldType;
use SilverStripe\ORM\FieldType\DBVarchar;
use Symfony\Component\Validator\Constraints;
use SilverStripe\Core\Validation\ConstraintValidator;
use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\Forms\EmailField;
use SilverStripe\Forms\NullableField;
use SilverStripe\Forms\FormField;
class DBEmail extends DBVarchar
{
// public function validate(): ValidationResult
// {
// // https://symfony.com/doc/current/reference/constraints/Email.html
// $result = parent::validate();
// // $message = _t('SilverStripe\\Forms\\EmailField.VALIDATION', 'Please enter an email address');
// // $result = $result->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;
}
}

View File

@ -2,567 +2,9 @@
namespace SilverStripe\ORM\FieldType; 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. * This interface is exists basically so that $object instanceof DBField checks work
*
* Every field from the database is represented as a sub-class of DBField.
*
* <b>Multi-value DBField objects</b>
*
* 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
*
* <b>Subclass Example</b>
*
* 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).
*
* <code>
* class Blob extends DBField {
* function requireField(): void {
* DB::require_field($this->tableName, $this->name, "blob");
* }
* }
* </code>
*/ */
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 <tablename>.* 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 <<<DBG
<ul>
<li><b>Name:</b>{$this->name}</li>
<li><b>Table:</b>{$this->tableName}</li>
<li><b>Value:</b>{$this->value}</li>
</ul>
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;
}
} }

View File

@ -0,0 +1,37 @@
<?php
namespace SilverStripe\ORM\FieldType;
use SilverStripe\Core\Injector\Injector;
class DBFieldHelper
{
/**
* 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): DBField
{
// 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);
$dbField = Injector::inst()->createWithArgs($spec, $args);
/** @var DBFieldTrait $dbField */
$dbField->setValue($value, null, false);
/** @var DBField $dbField */
return $dbField;
}
}

View File

@ -0,0 +1,345 @@
<?php
namespace SilverStripe\ORM\FieldType;
use InvalidArgumentException;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Model\ModelData;
use SilverStripe\ORM\Queries\SQLSelect;
use SilverStripe\ORM\Filters\SearchFilter;
use SilverStripe\Model\ModelFields\ModelField;
use SilverStripe\Model\ModelFields\ModelFieldTrait;
use SilverStripe\Model\ModelFields\StringModelField;
/**
* Single field in the database.
*
* Every field from the database is represented as a sub-class of DBField.
*
* <b>Multi-value DBField objects</b>
*
* 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
*
* <b>Subclass Example</b>
*
* 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).
*
* <code>
* class Blob extends DBField {
* function requireField(): void {
* DB::require_field($this->tableName, $this->name, "blob");
* }
* }
* </code>
*/
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 <tablename>.* 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;
}
}

View File

@ -5,12 +5,17 @@ namespace SilverStripe\ORM\FieldType;
use SilverStripe\Forms\FormField; use SilverStripe\Forms\FormField;
use SilverStripe\Forms\NumericField; use SilverStripe\Forms\NumericField;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBFieldTrait;
use SilverStripe\Model\ModelFields\ModelField;
use SilverStripe\ORM\FieldType\DBField;
/** /**
* Represents a floating point field. * 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) public function __construct(?string $name = null, float|int $defaultVal = 0)
{ {
$this->defaultVal = is_float($defaultVal) ? $defaultVal : (float) 0; $this->defaultVal = is_float($defaultVal) ? $defaultVal : (float) 0;

View File

@ -8,12 +8,16 @@ use SilverStripe\Model\List\ArrayList;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\Model\List\SS_List; use SilverStripe\Model\List\SS_List;
use SilverStripe\Model\ArrayData; use SilverStripe\Model\ArrayData;
use SilverStripe\Model\ModelFields\ModelField;
use SilverStripe\ORM\FieldType\DBField;
/** /**
* Represents a signed 32 bit integer field. * 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) public function __construct(?string $name = null, int $defaultVal = 0)
{ {
$this->defaultVal = is_int($defaultVal) ? $defaultVal : 0; $this->defaultVal = is_int($defaultVal) ? $defaultVal : 0;

View File

@ -2,19 +2,15 @@
namespace SilverStripe\ORM\FieldType; 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) * 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 = [ use DBFieldTrait;
'LimitCharacters' => 'Text',
'LimitCharactersToClosestWord' => 'Text',
'LimitWordCount' => 'Text',
'LowerCase' => 'Text',
'UpperCase' => 'Text',
'Plain' => 'Text',
];
/** /**
* Set the default value for "nullify empty" * Set the default value for "nullify empty"
@ -95,127 +91,4 @@ abstract class DBString extends DBField
} }
return ''; 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', '…');
}
} }

View File

@ -11,6 +11,9 @@ use SilverStripe\ORM\DB;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\Model\ModelData; 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'. * Represents a column in the database with the type 'Time'.
@ -22,8 +25,10 @@ use SilverStripe\Model\ModelData;
* ); * );
* </code> * </code>
*/ */
class DBTime extends DBField class DBTime extends ModelField implements DBField
{ {
use DBFieldTrait;
/** /**
* Standard ISO format string for time in CLDR standard format * Standard ISO format string for time in CLDR standard format
*/ */

View File

@ -5,12 +5,17 @@ namespace SilverStripe\ORM\FieldType;
use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\FormField; use SilverStripe\Forms\FormField;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBFieldTrait;
use SilverStripe\Model\ModelFields\ModelField;
use SilverStripe\ORM\FieldType\DBField;
/** /**
* Represents a single year field. * Represents a single year field.
*/ */
class DBYear extends DBField class DBYear extends ModelField implements DBField
{ {
use DBFieldTrait;
public function requireField(): void public function requireField(): void
{ {
$parts = ['datatype' => 'year', 'precision' => 4, 'arrayValue' => $this->arrayValue]; $parts = ['datatype' => 'year', 'precision' => 4, 'arrayValue' => $this->arrayValue];

View File

@ -11,6 +11,7 @@ use InvalidArgumentException;
use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Config\Configurable;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
/** /**
* Base class for filtering implementations, * Base class for filtering implementations,

View File

@ -9,6 +9,7 @@ use SilverStripe\Core\ArrayLib;
use SilverStripe\Model\List\ArrayList; use SilverStripe\Model\List\ArrayList;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\Model\List\SS_List; use SilverStripe\Model\List\SS_List;
use SilverStripe\Model\ArrayData; use SilverStripe\Model\ArrayData;

View File

@ -10,6 +10,7 @@ use SilverStripe\ORM\FieldType\DBComposite;
use InvalidArgumentException; use InvalidArgumentException;
use Exception; use Exception;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
/** /**
* Subclass of {@link DataList} representing a many_many relation. * Subclass of {@link DataList} representing a many_many relation.

View File

@ -7,6 +7,7 @@ use SilverStripe\Model\List\Limitable;
use SilverStripe\Model\List\Sortable; use SilverStripe\Model\List\Sortable;
use SilverStripe\Model\List\SS_List; use SilverStripe\Model\List\SS_List;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
/** /**
* Abstract representation of a DB relation field, either saved or in memory * Abstract representation of a DB relation field, either saved or in memory

View File

@ -6,6 +6,7 @@ use InvalidArgumentException;
use ArrayIterator; use ArrayIterator;
use SilverStripe\Model\List\ArrayList; use SilverStripe\Model\List\ArrayList;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use Traversable; use Traversable;
/** /**

View File

@ -10,6 +10,7 @@ use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Core\Manifest\ModuleLoader; use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\View\Requirements; use SilverStripe\View\Requirements;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
@ -197,7 +198,7 @@ PHP
// Show login // Show login
$controller = $controller->customise([ $controller = $controller->customise([
'Content' => DBField::create_field(DBHTMLText::class, _t( 'Content' => DBFieldHelper::create_field(DBHTMLText::class, _t(
__CLASS__ . '.SUCCESSCONTENT', __CLASS__ . '.SUCCESSCONTENT',
'<p>Login success. If you are not automatically redirected ' . '<a target="_top" href="{link}">click here</a></p>', '<p>Login success. If you are not automatically redirected ' . '<a target="_top" href="{link}">click here</a></p>',
'Login message displayed in the cms popup once a user has re-authenticated themselves', 'Login message displayed in the cms popup once a user has re-authenticated themselves',

View File

@ -10,6 +10,7 @@ use SilverStripe\Control\RequestHandler;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\Core\Validation\ValidationException; use SilverStripe\Core\Validation\ValidationException;
use SilverStripe\Security\Authenticator; use SilverStripe\Security\Authenticator;
use SilverStripe\Security\IdentityStore; use SilverStripe\Security\IdentityStore;
@ -83,7 +84,7 @@ class ChangePasswordHandler extends RequestHandler
$session = $this->getRequest()->getSession(); $session = $this->getRequest()->getSession();
if ($session->get('AutoLoginHash')) { if ($session->get('AutoLoginHash')) {
$message = DBField::create_field( $message = DBFieldHelper::create_field(
'HTMLFragment', 'HTMLFragment',
'<p>' . _t( '<p>' . _t(
'SilverStripe\\Security\\Security.ENTERNEWPASSWORD', 'SilverStripe\\Security\\Security.ENTERNEWPASSWORD',
@ -100,7 +101,7 @@ class ChangePasswordHandler extends RequestHandler
if (Security::getCurrentUser()) { if (Security::getCurrentUser()) {
// Logged in user requested a password change form. // Logged in user requested a password change form.
$message = DBField::create_field( $message = DBFieldHelper::create_field(
'HTMLFragment', 'HTMLFragment',
'<p>' . _t( '<p>' . _t(
'SilverStripe\\Security\\Security.CHANGEPASSWORDBELOW', 'SilverStripe\\Security\\Security.CHANGEPASSWORDBELOW',
@ -115,7 +116,7 @@ class ChangePasswordHandler extends RequestHandler
} }
// Show a friendly message saying the login token has expired // Show a friendly message saying the login token has expired
if ($token !== null && $member && !$member->validateAutoLoginToken($token)) { if ($token !== null && $member && !$member->validateAutoLoginToken($token)) {
$message = DBField::create_field( $message = DBFieldHelper::create_field(
'HTMLFragment', 'HTMLFragment',
_t( _t(
'SilverStripe\\Security\\Security.NOTERESETLINKINVALID', 'SilverStripe\\Security\\Security.NOTERESETLINKINVALID',

View File

@ -176,7 +176,7 @@ class CookieAuthenticationHandler implements AuthenticationHandler
} }
// Renew the token // Renew the token
Deprecation::withNoReplacement(fn() => $rememberLoginHash->renew()); Deprecation::withSuppressedNotice(fn() => $rememberLoginHash->renew());
// Send the new token to the client if it was changed // Send the new token to the client if it was changed
if ($rememberLoginHash->getToken()) { if ($rememberLoginHash->getToken()) {

View File

@ -11,6 +11,7 @@ use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
@ -92,7 +93,7 @@ class LostPasswordHandler extends RequestHandler
); );
return [ return [
'Content' => DBField::create_field('HTMLFragment', "<p>$message</p>"), 'Content' => DBFieldHelper::create_field('HTMLFragment', "<p>$message</p>"),
'Form' => $this->lostPasswordForm(), 'Form' => $this->lostPasswordForm(),
]; ];
} }
@ -115,7 +116,7 @@ class LostPasswordHandler extends RequestHandler
'SilverStripe\\Security\\Security.PASSWORDRESETSENTHEADER', 'SilverStripe\\Security\\Security.PASSWORDRESETSENTHEADER',
"Password reset link sent" "Password reset link sent"
), ),
'Content' => DBField::create_field('HTMLFragment', "<p>$message</p>"), 'Content' => DBFieldHelper::create_field('HTMLFragment', "<p>$message</p>"),
]; ];
} }

View File

@ -22,6 +22,7 @@ use SilverStripe\Model\List\ArrayList;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Core\Validation\ValidationResult; use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\Model\ArrayData; use SilverStripe\Model\ArrayData;
@ -911,8 +912,8 @@ class Security extends Controller implements TemplateGlobalProvider
$fragments = array_merge(['Title' => $title], $fragments); $fragments = array_merge(['Title' => $title], $fragments);
if ($message) { if ($message) {
$messageResult = [ $messageResult = [
'Content' => DBField::create_field('HTMLFragment', $message), 'Content' => DBFieldHelper::create_field('HTMLFragment', $message),
'Message' => DBField::create_field('HTMLFragment', $message), 'Message' => DBFieldHelper::create_field('HTMLFragment', $message),
'MessageType' => $messageType 'MessageType' => $messageType
]; ];
$fragments = array_merge($fragments, $messageResult); $fragments = array_merge($fragments, $messageResult);

View File

@ -17,7 +17,7 @@ use SilverStripe\Core\Manifest\ResourceURLGenerator;
use SilverStripe\Core\Path; use SilverStripe\Core\Path;
use SilverStripe\Dev\Debug; use SilverStripe\Dev\Debug;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBFieldHelper;
use Symfony\Component\Filesystem\Path as FilesystemPath; use Symfony\Component\Filesystem\Path as FilesystemPath;
class Requirements_Backend class Requirements_Backend
@ -1035,8 +1035,8 @@ class Requirements_Backend
i18n::config()->get('default_locale'), i18n::config()->get('default_locale'),
i18n::getData()->langFromLocale(i18n::get_locale()), i18n::getData()->langFromLocale(i18n::get_locale()),
i18n::get_locale(), i18n::get_locale(),
strtolower(DBField::create_field('Locale', i18n::get_locale())->RFC1766() ?? ''), strtolower(DBFieldHelper::create_field('Locale', i18n::get_locale())->RFC1766() ?? ''),
strtolower(DBField::create_field('Locale', i18n::config()->get('default_locale'))->RFC1766() ?? '') strtolower(DBFieldHelper::create_field('Locale', i18n::config()->get('default_locale'))->RFC1766() ?? '')
]; ];
$candidates = array_map( $candidates = array_map(

View File

@ -11,7 +11,7 @@ use SilverStripe\Core\Flushable;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use InvalidArgumentException; use InvalidArgumentException;
@ -670,7 +670,7 @@ PHP;
} }
/** @var DBHTMLText $html */ /** @var DBHTMLText $html */
$html = DBField::create_field('HTMLFragment', $output); $html = DBFieldHelper::create_field('HTMLFragment', $output);
// Reset global state // Reset global state
static::setRewriteHashLinksDefault($origRewriteDefault); static::setRewriteHashLinksDefault($origRewriteDefault);

View File

@ -6,6 +6,7 @@ use InvalidArgumentException;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Model\ModelData; use SilverStripe\Model\ModelData;
use SilverStripe\ORM\FieldType\DBField; 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" * This extends SSViewer_Scope to mix in data on top of what the item provides. This can be "global"
@ -438,6 +439,6 @@ class SSViewer_DataPresenter extends SSViewer_Scope
? ModelData::config()->uninherited('default_cast') ? ModelData::config()->uninherited('default_cast')
: $source['casting']; : $source['casting'];
return DBField::create_field($casting, $value); return DBFieldHelper::create_field($casting, $value);
} }
} }

View File

@ -4,6 +4,7 @@ namespace SilverStripe\View;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\ORM\FieldType\DBField; 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. * Special SSViewer that will process a template passed as a string, rather than a filename.
@ -72,7 +73,7 @@ class SSViewer_FromString extends SSViewer
unlink($cacheFile ?? ''); unlink($cacheFile ?? '');
} }
$html = DBField::create_field('HTMLFragment', $val); $html = DBFieldHelper::create_field('HTMLFragment', $val);
return $html; return $html;
} }

View File

@ -11,6 +11,7 @@ use SilverStripe\ORM\FieldType\DBText;
use SilverStripe\ORM\FieldType\DBFloat; use SilverStripe\ORM\FieldType\DBFloat;
use SilverStripe\ORM\FieldType\DBInt; use SilverStripe\ORM\FieldType\DBInt;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
/** /**
* This tracks the current scope for an SSViewer instance. It has three goals: * This tracks the current scope for an SSViewer instance. It has three goals:

View File

@ -10,6 +10,7 @@ use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Model\List\ArrayList; use SilverStripe\Model\List\ArrayList;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\Model\ArrayData; use SilverStripe\Model\ArrayData;
use SilverStripe\View\Embed\Embeddable; use SilverStripe\View\Embed\Embeddable;
use SilverStripe\View\HTML; use SilverStripe\View\HTML;
@ -197,7 +198,7 @@ class EmbedShortcodeProvider implements ShortcodeHandler
$data = [ $data = [
'Arguments' => $arguments, 'Arguments' => $arguments,
'Attributes' => $attributes, 'Attributes' => $attributes,
'Content' => DBField::create_field('HTMLFragment', $content) 'Content' => DBFieldHelper::create_field('HTMLFragment', $content)
]; ];
return ArrayData::create($data)->renderWith(EmbedShortcodeProvider::class . '_video')->forTemplate(); return ArrayData::create($data)->renderWith(EmbedShortcodeProvider::class . '_video')->forTemplate();

View File

@ -116,72 +116,72 @@ class DeprecationTest extends SapphireTest
Deprecation::outputNotices(); Deprecation::outputNotices();
} }
public function testWithNoReplacementDefault() public function testwithSuppressedNoticeDefault()
{ {
Deprecation::enable(); Deprecation::enable();
$ret = Deprecation::withNoReplacement(function () { $ret = Deprecation::withSuppressedNotice(function () {
return $this->myDeprecatedMethod(); return $this->myDeprecatedMethod();
}); });
$this->assertSame('abc', $ret); $this->assertSame('abc', $ret);
Deprecation::outputNotices(); Deprecation::outputNotices();
} }
public function testWithNoReplacementTrue() public function testwithSuppressedNoticeTrue()
{ {
$message = implode(' ', [ $message = implode(' ', [
'SilverStripe\Dev\Tests\DeprecationTest->myDeprecatedMethod is deprecated.', 'SilverStripe\Dev\Tests\DeprecationTest->myDeprecatedMethod is deprecated.',
'My message.', 'My message.',
'Called from SilverStripe\Dev\Tests\DeprecationTest->testWithNoReplacementTrue.' 'Called from SilverStripe\Dev\Tests\DeprecationTest->testwithSuppressedNoticeTrue.'
]); ]);
$this->expectException(DeprecationTestException::class); $this->expectException(DeprecationTestException::class);
$this->expectExceptionMessage($message); $this->expectExceptionMessage($message);
Deprecation::enable(true); Deprecation::enable(true);
$ret = Deprecation::withNoReplacement(function () { $ret = Deprecation::withSuppressedNotice(function () {
return $this->myDeprecatedMethod(); return $this->myDeprecatedMethod();
}); });
$this->assertSame('abc', $ret); $this->assertSame('abc', $ret);
Deprecation::outputNotices(); Deprecation::outputNotices();
} }
public function testWithNoReplacementTrueCallUserFunc() public function testwithSuppressedNoticeTrueCallUserFunc()
{ {
$message = implode(' ', [ $message = implode(' ', [
'SilverStripe\Dev\Tests\DeprecationTest->myDeprecatedMethod is deprecated.', 'SilverStripe\Dev\Tests\DeprecationTest->myDeprecatedMethod is deprecated.',
'My message.', 'My message.',
'Called from SilverStripe\Dev\Tests\DeprecationTest->testWithNoReplacementTrueCallUserFunc.' 'Called from SilverStripe\Dev\Tests\DeprecationTest->testwithSuppressedNoticeTrueCallUserFunc.'
]); ]);
$this->expectException(DeprecationTestException::class); $this->expectException(DeprecationTestException::class);
$this->expectExceptionMessage($message); $this->expectExceptionMessage($message);
Deprecation::enable(true); Deprecation::enable(true);
$ret = Deprecation::withNoReplacement(function () { $ret = Deprecation::withSuppressedNotice(function () {
return call_user_func([$this, 'myDeprecatedMethod']); return call_user_func([$this, 'myDeprecatedMethod']);
}); });
$this->assertSame('abc', $ret); $this->assertSame('abc', $ret);
Deprecation::outputNotices(); Deprecation::outputNotices();
} }
public function testNoticeWithNoReplacementTrue() public function testNoticewithSuppressedNoticeTrue()
{ {
$message = implode(' ', [ $message = implode(' ', [
'SilverStripe\Dev\Tests\DeprecationTest->testNoticeWithNoReplacementTrue is deprecated.', 'SilverStripe\Dev\Tests\DeprecationTest->testNoticewithSuppressedNoticeTrue is deprecated.',
'My message.', 'My message.',
'Called from PHPUnit\Framework\TestCase->runTest.' 'Called from PHPUnit\Framework\TestCase->runTest.'
]); ]);
$this->expectException(DeprecationTestException::class); $this->expectException(DeprecationTestException::class);
$this->expectExceptionMessage($message); $this->expectExceptionMessage($message);
Deprecation::enable(true); Deprecation::enable(true);
Deprecation::withNoReplacement(function () { Deprecation::withSuppressedNotice(function () {
Deprecation::notice('123', 'My message.'); Deprecation::notice('123', 'My message.');
}); });
Deprecation::outputNotices(); Deprecation::outputNotices();
} }
public function testClassWithNoReplacement() public function testClasswithSuppressedNotice()
{ {
$message = implode(' ', [ $message = implode(' ', [
'SilverStripe\Dev\Tests\DeprecationTest\DeprecationTestObject is deprecated.', 'SilverStripe\Dev\Tests\DeprecationTest\DeprecationTestObject is deprecated.',
'Some class message.', 'Some class message.',
'Called from SilverStripe\Dev\Tests\DeprecationTest->testClassWithNoReplacement.' 'Called from SilverStripe\Dev\Tests\DeprecationTest->testClasswithSuppressedNotice.'
]); ]);
$this->expectException(DeprecationTestException::class); $this->expectException(DeprecationTestException::class);
$this->expectExceptionMessage($message); $this->expectExceptionMessage($message);
@ -193,12 +193,12 @@ class DeprecationTest extends SapphireTest
Deprecation::outputNotices(); Deprecation::outputNotices();
} }
public function testClassWithInjectorWithNoReplacement() public function testClassWithInjectorwithSuppressedNotice()
{ {
$message = implode(' ', [ $message = implode(' ', [
'SilverStripe\Dev\Tests\DeprecationTest\DeprecationTestObject is deprecated.', 'SilverStripe\Dev\Tests\DeprecationTest\DeprecationTestObject is deprecated.',
'Some class message.', 'Some class message.',
'Called from SilverStripe\Dev\Tests\DeprecationTest->testClassWithInjectorWithNoReplacement.' 'Called from SilverStripe\Dev\Tests\DeprecationTest->testClassWithInjectorwithSuppressedNotice.'
]); ]);
$this->expectException(DeprecationTestException::class); $this->expectException(DeprecationTestException::class);
$this->expectExceptionMessage($message); $this->expectExceptionMessage($message);

View File

@ -11,7 +11,7 @@ class DeprecationTestObject extends DataObject implements TestOnly
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
Deprecation::withNoReplacement(function () { Deprecation::withSuppressedNotice(function () {
Deprecation::notice( Deprecation::notice(
'1.2.3', '1.2.3',
'Some class message', 'Some class message',

View File

@ -6,6 +6,7 @@ use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\TestOnly; use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\Model\ModelData; use SilverStripe\Model\ModelData;
class TestDbField extends DBField implements TestOnly class TestDbField extends DBField implements TestOnly

View File

@ -5,6 +5,7 @@ namespace SilverStripe\ORM\Tests\DataObjectTest;
use SilverStripe\Dev\TestOnly; use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\FieldType\DBBoolean; use SilverStripe\ORM\FieldType\DBBoolean;
use SilverStripe\ORM\FieldType\DBField; 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 * This is a fake DB field specifically design to test dynamic value assignment. You can set `scalarValueOnly` in

View File

@ -156,7 +156,7 @@ class PasswordEncryptorTest extends SapphireTest
'encryptors', 'encryptors',
['test_sha1legacy' => [PasswordEncryptor_LegacyPHPHash::class => 'sha1']] ['test_sha1legacy' => [PasswordEncryptor_LegacyPHPHash::class => 'sha1']]
); );
$e = Deprecation::withNoReplacement(fn() => PasswordEncryptor::create_for_algorithm('test_sha1legacy')); $e = Deprecation::withSuppressedNotice(fn() => PasswordEncryptor::create_for_algorithm('test_sha1legacy'));
// precomputed hashes for 'mypassword' from different architectures // precomputed hashes for 'mypassword' from different architectures
$amdHash = 'h1fj0a6m4o6k0sosks88oo08ko4gc4s'; $amdHash = 'h1fj0a6m4o6k0sosks88oo08ko4gc4s';
$intelHash = 'h1fj0a6m4o0g04ocg00o4kwoc4wowws'; $intelHash = 'h1fj0a6m4o0g04ocg00o4kwoc4wowws';

View File

@ -111,7 +111,7 @@ class RememberLoginHashTest extends SapphireTest
$member = $this->objFromFixture(Member::class, 'main'); $member = $this->objFromFixture(Member::class, 'main');
Deprecation::withNoReplacement( Deprecation::withSuppressedNotice(
fn() => RememberLoginHash::config()->set('replace_token_during_session_renewal', $replaceToken) fn() => RememberLoginHash::config()->set('replace_token_during_session_renewal', $replaceToken)
); );
@ -122,7 +122,7 @@ class RememberLoginHashTest extends SapphireTest
// Fetch the token from the DB - otherwise we still have the token from when this was originally created // Fetch the token from the DB - otherwise we still have the token from when this was originally created
$storedHash = RememberLoginHash::get()->find('ID', $hash->ID); $storedHash = RememberLoginHash::get()->find('ID', $hash->ID);
Deprecation::withNoReplacement(fn() => $storedHash->renew()); Deprecation::withSuppressedNotice(fn() => $storedHash->renew());
if ($replaceToken) { if ($replaceToken) {
$this->assertNotEquals($oldToken, $storedHash->getToken()); $this->assertNotEquals($oldToken, $storedHash->getToken());