silverstripe-framework/src/Forms/MultiSelectField.php

287 lines
7.9 KiB
PHP

<?php
namespace SilverStripe\Forms;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\ORM\FieldType\DBMultiEnum;
use SilverStripe\ORM\Relation;
/**
* Represents a SelectField that may potentially have multiple selections, and may have
* a {@link ManyManyList} as a data source.
*/
abstract class MultiSelectField extends SelectField
{
/**
* List of items to mark as checked, and may not be unchecked
*
* @var array
*/
protected $defaultItems = [];
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_MULTISELECT;
/**
* Extracts the value of this field, normalised as an array.
* Scalar values will return a single length array, even if empty
*
* @return array List of values as an array
*/
public function getValueArray()
{
return $this->getListValues($this->Value());
}
/**
* Default selections, regardless of the {@link setValue()} settings.
* Note: Items marked as disabled through {@link setDisabledItems()} can still be
* selected by default through this method.
*
* @param array $items Collection of array keys, as defined in the $source array
* @return $this Self reference
*/
public function setDefaultItems($items)
{
$this->defaultItems = $this->getListValues($items);
return $this;
}
/**
* Default selections, regardless of the {@link setValue()} settings.
*
* @return array
*/
public function getDefaultItems()
{
return $this->defaultItems;
}
/**
* Load a value into this MultiSelectField
*
* @param mixed $value
* @param null|array|DataObject $obj {@see Form::loadDataFrom}
* @return $this
*/
public function setValue($value, $obj = null)
{
// If we're not passed a value directly,
// we can look for it in a relation method on the object passed as a second arg
if ($obj instanceof DataObject) {
$this->loadFrom($obj);
} else {
parent::setValue($value);
}
return $this;
}
/**
* Load the value from the dataobject into this field
*
* @param DataObject|DataObjectInterface $record
*/
public function loadFrom(DataObjectInterface $record)
{
$fieldName = $this->getName();
if (empty($fieldName) || empty($record)) {
return;
}
$relation = $record->hasMethod($fieldName)
? $record->$fieldName()
: null;
// Detect DB relation or field
if ($relation instanceof Relation) {
// Load ids from relation
$value = array_values($relation->getIDList() ?? []);
parent::setValue($value);
} elseif ($record->hasField($fieldName)) {
// Load dataValue from field... a CSV for DBMultiEnum
if ($record->obj($fieldName) instanceof DBMultiEnum) {
$value = $this->csvDecode($record->$fieldName);
// ... JSON-encoded string for other fields
} else {
$value = $this->stringDecode($record->$fieldName);
}
parent::setValue($value);
}
}
/**
* Save the current value of this MultiSelectField into a DataObject.
* If the field it is saving to is a has_many or many_many relationship,
* it is saved by setByIDList(), otherwise it creates a comma separated
* list for a standard DB text/varchar field.
*
* @param DataObject|DataObjectInterface $record The record to save into
*/
public function saveInto(DataObjectInterface $record)
{
$fieldName = $this->getName();
if (empty($fieldName) || empty($record)) {
return;
}
$relation = $record->hasMethod($fieldName)
? $record->$fieldName()
: null;
// Detect DB relation or field
$items = $this->getValueArray();
if ($relation instanceof Relation) {
// Save ids into relation
$relation->setByIDList($items);
} elseif ($record->hasField($fieldName)) {
// Save dataValue into field... a CSV for DBMultiEnum
if ($record->obj($fieldName) instanceof DBMultiEnum) {
$record->$fieldName = $this->csvEncode($items);
// ... JSON-encoded string for other fields
} else {
$record->$fieldName = $this->stringEncode($items);
}
}
}
/**
* Encode a list of values into a string, or null if empty (to simplify empty checks)
*
* @param array $value
* @return string|null
*/
public function stringEncode($value)
{
return $value
? json_encode(array_values($value))
: null;
}
/**
* Extract a string value into an array of values
*
* @param string $value
* @return array
*/
protected function stringDecode($value)
{
// Handle empty case
if (empty($value)) {
return [];
}
// If json deserialisation fails, then fallover to legacy format
$result = json_decode($value ?? '', true);
if ($result !== false) {
return $result;
}
throw new \InvalidArgumentException("Invalid string encoded value for multi select field");
}
/**
* Encode a list of values into a string as a comma separated list.
* Commas will be stripped from the items passed in
*
* @param array $value
* @return string|null
*/
protected function csvEncode($value)
{
if (!$value) {
return null;
}
return implode(
',',
array_map(
function ($x) {
return str_replace(',', '', $x ?? '');
},
array_values($value ?? [])
)
);
}
/**
* Decode a list of values from a comma separated string.
* Spaces are trimmed
*
* @param string $value
* @return array
*/
protected function csvDecode($value)
{
if (!$value) {
return [];
}
return preg_split('/\s*,\s*/', trim($value ?? ''));
}
/**
* Validate this field
*
* @param Validator $validator
* @return bool
*/
public function validate($validator)
{
$values = $this->getValueArray();
$validValues = $this->getValidValues();
// Filter out selected values not in the data source
$self = $this;
$invalidValues = array_filter(
$values ?? [],
function ($userValue) use ($self, $validValues) {
foreach ($validValues as $formValue) {
if ($self->isSelectedValue($formValue, $userValue)) {
return false;
}
}
return true;
}
);
if (empty($invalidValues)) {
return true;
}
// List invalid items
$validator->validationError(
$this->getName(),
_t(
'SilverStripe\\Forms\\MultiSelectField.SOURCE_VALIDATION',
"Please select values within the list provided. Invalid option(s) {value} given",
['value' => implode(',', $invalidValues)]
),
"validation"
);
return false;
}
/**
* Transforms the source data for this CheckboxSetField
* into a comma separated list of values.
*
* @return ReadonlyField
*/
public function performReadonlyTransformation()
{
$field = $this->castedCopy('SilverStripe\\Forms\\LookupField');
$field->setSource($this->getSource());
$field->setReadonly(true);
// Pass through default items
if (!$this->getValueArray() && $this->getDefaultItems()) {
$field->setValue($this->getDefaultItems());
}
return $field;
}
}