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
Double:
class: SilverStripe\ORM\FieldType\DBDouble
Email:
class: SilverStripe\ORM\FieldType\DBEmail
Enum:
class: SilverStripe\ORM\FieldType\DBEnum
Float:

View File

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

View File

@ -16,7 +16,7 @@ class CliBypass implements Bypass
{
public function __construct()
{
Deprecation::withNoReplacement(function () {
Deprecation::withSuppressedNotice(function () {
Deprecation::notice(
'5.4.0',
'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\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Model\ModelData;
use BadMethodCallException;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,9 @@
namespace SilverStripe\Forms;
use SilverStripe\Core\Validation\ConstraintValidator;
use Symfony\Component\Validator\Constraints;
/**
* Text input field with validation for correct email format according to RFC 2822.
*/
@ -18,32 +21,27 @@ 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
*
* @return string
*/
public function validate($validator)
{
$result = true;
$this->value = trim($this->value ?? '');
$pattern = '^[a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$';
$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.
$safePattern = str_replace('/', '\\/', $pattern ?? '');
if ($this->value && !preg_match('/' . $safePattern . '/i', $this->value ?? '')) {
if (!$validationResult->isValid()) {
$validator->validationError(
$this->name,
_t('SilverStripe\\Forms\\EmailField.VALIDATION', 'Please enter an email address'),
'validation'
$validationResult->getMessages()[0]['message'],
);
$result = false;
}

View File

@ -10,11 +10,16 @@ use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Convert;
use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\View\AttributesHTML;
use SilverStripe\View\SSViewer;
use SilverStripe\Model\ModelData;
use SilverStripe\Model\ModelFields\ModelField;
use SilverStripe\Model\ModelFields\ModelFieldTrait;
use SilverStripe\Model\ModelFields\StringModelField;
use SilverStripe\Core\Injector\Injector;
/**
* Represents a field in a form.
@ -40,11 +45,28 @@ use SilverStripe\Model\ModelData;
* including both structure (name, id, attributes, etc.) and state (field value).
* Can be used by for JSON data which is consumed by a front-end application.
*/
class FormField extends RequestHandler
abstract class FormField extends RequestHandler
{
use AttributesHTML;
use FormMessage;
// todo: possibly should make this abstract
protected string $modelFieldClass = StringModelField::class;
private ModelField $modelField;
protected function getModelField(): ModelField
{
return $this->modelField;
}
private function initModelField(): void
{
$name = $this->getName();
$modelField = Injector::inst()->createWithArgs($this->modelFieldClass, [$name]);
$this->modelField = $modelField;
}
/** @see $schemaDataType */
const SCHEMA_DATA_TYPE_STRING = 'String';
@ -344,6 +366,7 @@ class FormField extends RequestHandler
parent::__construct();
$this->initModelField($name);
$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
if (is_string($result)) {
$result = trim($result ?? '');
} elseif ($result instanceof DBField) {
} elseif ($result instanceof ModelField) {
$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.
*
* Subclass methods should call $this->extendValidationResult(true, $validator)
* at the end of the method
*
* @param Validator $validator
* @return bool
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ namespace SilverStripe\Logging;
use Monolog\Formatter\FormatterInterface;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Level;
use Monolog\LogRecord;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
@ -34,10 +35,10 @@ class HTTPOutputHandler extends AbstractProcessingHandler
*/
private $cliFormatter = null;
public function __construct()
public function __construct(int|string|Level $level = Level::Debug, bool $bubble = true)
{
parent::__construct();
Deprecation::withNoReplacement(function () {
parent::__construct($level, $bubble);
Deprecation::withSuppressedNotice(function () {
Deprecation::notice(
'5.4.0',
'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
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())) {
$searchFilter->setModifiers($searchFilter->getModifiers() + ['case']);
} elseif (!$caseSensitive && in_array('nocase', $searchFilter->getSupportedModifiers())) {

View File

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

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\DB;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\ORM\FieldType\DBPrimaryKey;
/**

View File

@ -6,6 +6,7 @@ use mysqli;
use mysqli_sql_exception;
use mysqli_stmt;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Environment;
/**
* Connector for MySQL using the MySQLi method
@ -81,15 +82,21 @@ class MySQLiConnector extends DBConnector
// Connection charset and collation
$connCharset = Config::inst()->get(MySQLDatabase::class, 'connection_charset');
$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();
// Use native types (MysqlND only)
if (defined('MYSQLI_OPT_INT_AND_FLOAT_NATIVE')) {
$this->dbConn->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
// The alternative is not ideal, throw a notice-level error
} else {
// The alternative is not ideal, throw a notice-level error
user_error(
'mysqlnd PHP library is not available, numeric values will be fetched from the DB as strings',
E_USER_NOTICE
@ -117,7 +124,9 @@ class MySQLiConnector extends DBConnector
$parameters['username'],
$parameters['password'],
$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) {
@ -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
$charset = isset($parameters['charset'])
? $parameters['charset']
: $connCharset;
? $parameters['charset']
: $connCharset;
if (!empty($charset)) {
$this->dbConn->set_charset($charset);

View File

@ -20,6 +20,7 @@ use SilverStripe\Forms\FormField;
use SilverStripe\Forms\FormScaffolder;
use SilverStripe\Forms\CompositeValidator;
use SilverStripe\Forms\FieldsValidator;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
use SilverStripe\Forms\HiddenField;
@ -33,6 +34,7 @@ use SilverStripe\ORM\FieldType\DBComposite;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBEnum;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\ORM\FieldType\DBForeignKey;
use SilverStripe\ORM\Filters\PartialMatchFilter;
use SilverStripe\ORM\Filters\SearchFilter;
@ -45,7 +47,13 @@ use SilverStripe\Security\Permission;
use SilverStripe\Security\Security;
use SilverStripe\View\SSViewer;
use SilverStripe\Model\ModelData;
use SilverStripe\ORM\FieldType\DBText;
use SilverStripe\ORM\FieldType\DBVarchar;
use stdClass;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
use Symfony\Component\Validator\Constraints\Uuid;
/**
* A single database record & abstract class for the data-access-model.
@ -1230,10 +1238,44 @@ class DataObject extends ModelData implements DataObjectInterface, i18nEntityPro
public function validate()
{
$result = ValidationResult::create();
// Call validate() on every DBField
$specs = static::getSchema()->fieldSpecs(static::class);
foreach (array_keys($specs) as $fieldName) {
// Call DBField::validate() on every DBField
$dbField = $this->dbObject($fieldName);
$result->combineAnd($dbField->validate());
}
// If it passed DBField::validate(), then call FormField::validate()
// on every field that would be scaffolded
// This isn't ideal and can lead to double validation, however it's to preserve
// any legacy FormField::validate() logic on projects that has not been
// migrated to use DBField::validate()
// TODO: don't call this when in FormRequestHandler::httpSubmission()
// as it means that calling FormField::validate() is called twice
if ($result->isValid()) {
$form = $this->getFormForValidation();
$result->combineAnd($form->validationResult());
}
// Call extension hook and return
$this->extend('updateValidate', $result);
return $result;
}
private function getFormForValidation(): Form
{
$session = new Session([]);
$controller = new Controller();
$request = new HTTPRequest('GET', '/');
$request->setSession($session);
$controller->setRequest($request);
// Use getCMSFields rather than $dbField->scaffoldFormField()
// to ensure we get any CMS fields that were replaced by other fields
$fields = $this->getCMSFields();
$form = new Form($controller, sprintf('temp-%s', uniqid()), $fields, null, new FieldsValidator);
$form->loadDataFrom($this);
return $form;
}
/**
* Event handler called before writing to the database.
* You can overload this to clean up or otherwise process data before writing it to the
@ -1456,7 +1498,7 @@ class DataObject extends ModelData implements DataObjectInterface, i18nEntityPro
// if database column doesn't correlate to a DBField instance...
$fieldObj = $this->dbObject($fieldName);
if (!$fieldObj) {
$fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
$fieldObj = DBFieldHelper::create_field('Varchar', $fieldValue, $fieldName);
}
// Write to manipulation
@ -1982,7 +2024,7 @@ class DataObject extends ModelData implements DataObjectInterface, i18nEntityPro
if ($details['polymorphic']) {
$result = PolymorphicHasManyList::create($componentClass, $details['joinField'], static::class);
if ($details['needsRelation']) {
Deprecation::withNoReplacement(fn () => $result->setForeignRelation($componentName));
Deprecation::withSuppressedNotice(fn () => $result->setForeignRelation($componentName));
}
} else {
$result = HasManyList::create($componentClass, $details['joinField']);
@ -2422,16 +2464,15 @@ class DataObject extends ModelData implements DataObjectInterface, i18nEntityPro
}
// Otherwise, use the database field's scaffolder
} elseif ($object = $this->relObject($fieldName)) {
if (is_object($object) && $object->hasMethod('scaffoldSearchField')) {
$field = $object->scaffoldSearchField();
} else {
} elseif ($dbField = $this->relObject($fieldName)) {
if (!is_a($dbField, DBField::class)) {
throw new Exception(sprintf(
"SearchField '%s' on '%s' does not return a valid DBField instance.",
$fieldName,
get_class($this)
));
}
$field = $dbField->scaffoldSearchField();
}
// Allow fields to opt out of search

View File

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

View File

@ -67,7 +67,7 @@ class DatabaseAdmin extends Controller
public function __construct()
{
parent::__construct();
Deprecation::withNoReplacement(function () {
Deprecation::withSuppressedNotice(function () {
Deprecation::notice(
'5.4.0',
'Will be replaced with SilverStripe\Dev\Command\DbBuild',
@ -213,7 +213,7 @@ class DatabaseAdmin extends Controller
*/
public static function lastBuilt()
{
Deprecation::withNoReplacement(function () {
Deprecation::withSuppressedNotice(function () {
Deprecation::notice(
'5.4.0',
'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\ORM\Connect\DatabaseException;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use BadMethodCallException;
use InvalidArgumentException;
use LogicException;

View File

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

View File

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

View File

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

View File

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

View File

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

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;
use InvalidArgumentException;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Forms\FormField;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\Filters\SearchFilter;
use SilverStripe\ORM\Queries\SQLSelect;
use SilverStripe\Model\ModelData;
/**
* Single field in the database.
*
* Every field from the database is represented as a sub-class of DBField.
*
* <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>
* This interface is exists basically so that $object instanceof DBField checks work
*/
abstract class DBField extends ModelData implements DBIndexable
interface DBField extends DBIndexable
{
/**
* Raw value of this field
*/
protected mixed $value = null;
/**
* Table this field belongs to
*/
protected ?string $tableName = null;
/**
* Name of this field
*/
protected ?string $name = null;
/**
* Used for generating DB schema. {@see DBSchemaManager}
* Despite its name, this seems to be a string
*/
protected $arrayValue;
/**
* Optional parameters for this field
*/
protected array $options = [];
/**
* The escape type for this field when inserted into a template - either "xml" or "raw".
*/
private static string $escape_type = 'raw';
/**
* Subclass of {@link SearchFilter} for usage in {@link defaultSearchFilter()}.
*/
private static string $default_search_filter_class = 'PartialMatchFilter';
/**
* The type of index to use for this field. Can either be a string (one of the DBIndexable type options) or a
* boolean. When a boolean is given, false will not index the field, and true will use the default index type.
*/
private static string|bool $index = false;
private static array $casting = [
'ATT' => 'HTMLFragment',
'CDATA' => 'HTMLFragment',
'HTML' => 'HTMLFragment',
'HTMLATT' => 'HTMLFragment',
'JS' => 'HTMLFragment',
'RAW' => 'HTMLFragment',
'RAWURLATT' => 'HTMLFragment',
'URLATT' => 'HTMLFragment',
'XML' => 'HTMLFragment',
'ProcessedRAW' => 'HTMLFragment',
];
/**
* Default value in the database.
* Might be overridden on DataObject-level, but still useful for setting defaults on
* already existing records after a db-build.
*/
protected mixed $defaultVal = null;
/**
* Provide the DBField name and an array of options, e.g. ['index' => true], or ['nullifyEmpty' => false]
*
* @throws InvalidArgumentException If $options was passed by not an array
*/
public function __construct(?string $name = null, array $options = [])
{
$this->name = $name;
if ($options) {
if (!is_array($options)) {
throw new InvalidArgumentException("Invalid options $options");
}
$this->setOptions($options);
}
parent::__construct();
}
/**
* Create a DBField object that's not bound to any particular field.
*
* Useful for accessing the classes behaviour for other parts of your code.
*
* @param string $spec Class specification to construct. May include both service name and additional
* constructor arguments in the same format as DataObject.db config.
* @param mixed $value value of field
* @param null|string $name Name of field
* @param mixed $args Additional arguments to pass to constructor if not using args in service $spec
* Note: Will raise a warning if using both
*/
public static function create_field(string $spec, mixed $value, ?string $name = null, mixed ...$args): static
{
// Raise warning if inconsistent with DataObject::dbObject() behaviour
// This will cause spec args to be shifted down by the number of provided $args
if ($args && strpos($spec ?? '', '(') !== false) {
trigger_error('Additional args provided in both $spec and $args', E_USER_WARNING);
}
// Ensure name is always first argument
array_unshift($args, $name);
/** @var DBField $dbField */
$dbField = Injector::inst()->createWithArgs($spec, $args);
$dbField->setValue($value, null, false);
return $dbField;
}
/**
* Set the name of this field.
*
* The name should never be altered, but it if was never given a name in
* the first place you can set a name.
*
* If you try an alter the name a warning will be thrown.
*/
public function setName(?string $name): static
{
if ($this->name && $this->name !== $name) {
user_error("DBField::setName() shouldn't be called once a DBField already has a name."
. "It's partially immutable - it shouldn't be altered after it's given a value.", E_USER_WARNING);
}
$this->name = $name;
return $this;
}
/**
* Returns the name of this field.
*/
public function getName(): string
{
return $this->name ?? '';
}
/**
* Returns the value of this field.
*/
public function getValue(): mixed
{
return $this->value;
}
/**
* Set the value of this field in various formats.
* Used by {@link DataObject->getField()}, {@link DataObject->setCastedField()}
* {@link DataObject->dbObject()} and {@link DataObject->write()}.
*
* As this method is used both for initializing the field after construction,
* and actually changing its values, it needs a {@link $markChanged}
* parameter.
*
* @param null|ModelData|array $record An array or object that this field is part of
* @param bool $markChanged Indicate whether this field should be marked changed.
* Set to FALSE if you are initializing this field after construction, rather
* than setting a new value.
*/
public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static
{
$this->value = $value;
return $this;
}
/**
* Get default value assigned at the DB level
*/
public function getDefaultValue(): mixed
{
return $this->defaultVal;
}
/**
* Set default value to use at the DB level
*/
public function setDefaultValue(mixed $defaultValue): static
{
$this->defaultVal = $defaultValue;
return $this;
}
/**
* Update the optional parameters for this field
*/
public function setOptions(array $options = []): static
{
$this->options = $options;
return $this;
}
/**
* Get optional parameters for this field
*/
public function getOptions(): array
{
return $this->options;
}
public function setIndexType($type): string|bool
{
if (!is_bool($type)
&& !in_array($type, [DBIndexable::TYPE_INDEX, DBIndexable::TYPE_UNIQUE, DBIndexable::TYPE_FULLTEXT])
) {
throw new \InvalidArgumentException(
"{$type} is not a valid index type or boolean. Please see DBIndexable."
);
}
$this->options['index'] = $type;
return $this;
}
public function getIndexType()
{
if (array_key_exists('index', $this->options ?? [])) {
$type = $this->options['index'];
} else {
$type = static::config()->get('index');
}
if (is_bool($type)) {
if (!$type) {
return false;
}
$type = DBIndexable::TYPE_DEFAULT;
}
return $type;
}
/**
* Determines if the field has a value which is not considered to be 'null'
* in a database context.
*/
public function exists(): bool
{
return (bool)$this->value;
}
/**
* Return the transformed value ready to be sent to the database. This value
* will be escaped automatically by the prepared query processor, so it
* should not be escaped or quoted at all.
*
* @param mixed $value The value to check
* @return mixed The raw value, or escaped parameterised details
*/
public function prepValueForDB(mixed $value): mixed
{
if ($value === null ||
$value === "" ||
$value === false ||
($this->scalarValueOnly() && !is_scalar($value))
) {
return null;
} else {
return $value;
}
}
/**
* Prepare the current field for usage in a
* database-manipulation (works on a manipulation reference).
*
* Make value safe for insertion into
* a SQL SET statement by applying addslashes() -
* can also be used to apply special SQL-commands
* to the raw value (e.g. for GIS functionality).
* {@see prepValueForDB}
*/
public function writeToManipulation(array &$manipulation): void
{
$manipulation['fields'][$this->name] = $this->exists()
? $this->prepValueForDB($this->value) : $this->nullValue();
}
/**
* Add custom query parameters for this field,
* mostly SELECT statements for multi-value fields.
*
* By default, the ORM layer does a
* SELECT <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\NumericField;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBFieldTrait;
use SilverStripe\Model\ModelFields\ModelField;
use SilverStripe\ORM\FieldType\DBField;
/**
* Represents a floating point field.
*/
class DBFloat extends DBField
class DBFloat extends ModelField implements DBField
{
use DBFieldTrait;
public function __construct(?string $name = null, float|int $defaultVal = 0)
{
$this->defaultVal = is_float($defaultVal) ? $defaultVal : (float) 0;

View File

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

View File

@ -2,19 +2,15 @@
namespace SilverStripe\ORM\FieldType;
use SilverStripe\Model\ModelFields\StringModelField;
use SilverStripe\ORM\FieldType\DBFieldTrait;
/**
* An abstract base class for the string field types (i.e. Varchar and Text)
*/
abstract class DBString extends DBField
abstract class DBString extends StringModelField implements DBField
{
private static array $casting = [
'LimitCharacters' => 'Text',
'LimitCharactersToClosestWord' => 'Text',
'LimitWordCount' => 'Text',
'LowerCase' => 'Text',
'UpperCase' => 'Text',
'Plain' => 'Text',
];
use DBFieldTrait;
/**
* Set the default value for "nullify empty"
@ -95,127 +91,4 @@ abstract class DBString extends DBField
}
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\Security;
use SilverStripe\Model\ModelData;
use SilverStripe\ORM\FieldType\DBFieldTrait;
use SilverStripe\Model\ModelFields\ModelField;
use SilverStripe\ORM\FieldType\DBField;
/**
* Represents a column in the database with the type 'Time'.
@ -22,8 +25,10 @@ use SilverStripe\Model\ModelData;
* );
* </code>
*/
class DBTime extends DBField
class DBTime extends ModelField implements DBField
{
use DBFieldTrait;
/**
* 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\FormField;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBFieldTrait;
use SilverStripe\Model\ModelFields\ModelField;
use SilverStripe\ORM\FieldType\DBField;
/**
* Represents a single year field.
*/
class DBYear extends DBField
class DBYear extends ModelField implements DBField
{
use DBFieldTrait;
public function requireField(): void
{
$parts = ['datatype' => 'year', 'precision' => 4, 'arrayValue' => $this->arrayValue];

View File

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

View File

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

View File

@ -10,6 +10,7 @@ use SilverStripe\ORM\FieldType\DBComposite;
use InvalidArgumentException;
use Exception;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
/**
* 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\SS_List;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
/**
* Abstract representation of a DB relation field, either saved or in memory

View File

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

View File

@ -10,6 +10,7 @@ use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\View\Requirements;
use SilverStripe\View\SSViewer;
@ -197,7 +198,7 @@ PHP
// Show login
$controller = $controller->customise([
'Content' => DBField::create_field(DBHTMLText::class, _t(
'Content' => DBFieldHelper::create_field(DBHTMLText::class, _t(
__CLASS__ . '.SUCCESSCONTENT',
'<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',

View File

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

View File

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

View File

@ -11,6 +11,7 @@ use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Forms\Form;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
@ -92,7 +93,7 @@ class LostPasswordHandler extends RequestHandler
);
return [
'Content' => DBField::create_field('HTMLFragment', "<p>$message</p>"),
'Content' => DBFieldHelper::create_field('HTMLFragment', "<p>$message</p>"),
'Form' => $this->lostPasswordForm(),
];
}
@ -115,7 +116,7 @@ class LostPasswordHandler extends RequestHandler
'SilverStripe\\Security\\Security.PASSWORDRESETSENTHEADER',
"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\DB;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\Model\ArrayData;
@ -911,8 +912,8 @@ class Security extends Controller implements TemplateGlobalProvider
$fragments = array_merge(['Title' => $title], $fragments);
if ($message) {
$messageResult = [
'Content' => DBField::create_field('HTMLFragment', $message),
'Message' => DBField::create_field('HTMLFragment', $message),
'Content' => DBFieldHelper::create_field('HTMLFragment', $message),
'Message' => DBFieldHelper::create_field('HTMLFragment', $message),
'MessageType' => $messageType
];
$fragments = array_merge($fragments, $messageResult);

View File

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

View File

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

View File

@ -6,6 +6,7 @@ use InvalidArgumentException;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Model\ModelData;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
/**
* This extends SSViewer_Scope to mix in data on top of what the item provides. This can be "global"
@ -438,6 +439,6 @@ class SSViewer_DataPresenter extends SSViewer_Scope
? ModelData::config()->uninherited('default_cast')
: $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\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBFieldHelper;
/**
* Special SSViewer that will process a template passed as a string, rather than a filename.
@ -72,7 +73,7 @@ class SSViewer_FromString extends SSViewer
unlink($cacheFile ?? '');
}
$html = DBField::create_field('HTMLFragment', $val);
$html = DBFieldHelper::create_field('HTMLFragment', $val);
return $html;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -156,7 +156,7 @@ class PasswordEncryptorTest extends SapphireTest
'encryptors',
['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
$amdHash = 'h1fj0a6m4o6k0sosks88oo08ko4gc4s';
$intelHash = 'h1fj0a6m4o0g04ocg00o4kwoc4wowws';

View File

@ -111,7 +111,7 @@ class RememberLoginHashTest extends SapphireTest
$member = $this->objFromFixture(Member::class, 'main');
Deprecation::withNoReplacement(
Deprecation::withSuppressedNotice(
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
$storedHash = RememberLoginHash::get()->find('ID', $hash->ID);
Deprecation::withNoReplacement(fn() => $storedHash->renew());
Deprecation::withSuppressedNotice(fn() => $storedHash->renew());
if ($replaceToken) {
$this->assertNotEquals($oldToken, $storedHash->getToken());