/src/ORM/Filters/SearchFilter.php
PHP | 453 lines | 232 code | 46 blank | 175 comment | 17 complexity | 6e832514c019227acc997035fe1fcd4c MD5 | raw file
- <?php
- namespace SilverStripe\ORM\Filters;
- use SilverStripe\Core\ClassInfo;
- use SilverStripe\Core\Injector\Injectable;
- use SilverStripe\ORM\DataObject;
- use SilverStripe\ORM\DataQuery;
- use InvalidArgumentException;
- use SilverStripe\ORM\FieldType\DBField;
- /**
- * Base class for filtering implementations,
- * which work together with {@link SearchContext}
- * to create or amend a query for {@link DataObject} instances.
- * See {@link SearchContext} for more information.
- *
- * Each search filter must be registered in config as an "Injector" service with
- * the "DataListFilter." prefix. E.g.
- *
- * <code>
- * Injector:
- * DataListFilter.EndsWith:
- * class: EndsWithFilter
- * </code>
- */
- abstract class SearchFilter
- {
- use Injectable;
- /**
- * Classname of the inspected {@link DataObject}.
- * If pointing to a relation, this will be the classname of the leaf
- * class in the relation
- *
- * @var string
- */
- protected $model;
- /**
- * @var string
- */
- protected $name;
- /**
- * @var string
- */
- protected $fullName;
- /**
- * @var mixed
- */
- protected $value;
- /**
- * @var array
- */
- protected $modifiers;
- /**
- * @var array Parts of a has-one, has-many or many-many relation (not the classname).
- * Set in the constructor as part of the name in dot-notation, and used in
- * {@link applyRelation()}.
- *
- * Also used to build table prefix (see getRelationTablePrefix)
- */
- protected $relation = [];
- /**
- * An array of data about an aggregate column being used
- * ex:
- * [
- * 'function' => 'COUNT',
- * 'column' => 'ID'
- * ]
- * @var array
- */
- protected $aggregate;
- /**
- * @param string $fullName Determines the name of the field, as well as the searched database
- * column. Can contain a relation name in dot notation, which will automatically join
- * the necessary tables (e.g. "Comments.Name" to join the "Comments" has-many relationship and
- * search the "Name" column when applying this filter to a SiteTree class).
- * @param mixed $value
- * @param array $modifiers
- */
- public function __construct($fullName = null, $value = false, array $modifiers = array())
- {
- $this->fullName = $fullName;
- // sets $this->name and $this->relation
- $this->addRelation($fullName);
- $this->addAggregate($fullName);
- $this->value = $value;
- $this->setModifiers($modifiers);
- }
- /**
- * Called by constructor to convert a string pathname into
- * a well defined relationship sequence.
- *
- * @param string $name
- */
- protected function addRelation($name)
- {
- if (strstr($name, '.')) {
- $parts = explode('.', $name);
- $this->name = array_pop($parts);
- $this->relation = $parts;
- } else {
- $this->name = $name;
- }
- }
- /**
- * Parses the name for any aggregate functions and stores them in the $aggregate array
- *
- * @param string $name
- */
- protected function addAggregate($name)
- {
- if (!$this->relation) {
- return;
- }
- if (!preg_match('/([A-Za-z]+)\(\s*(?:([A-Za-z_*][A-Za-z0-9_]*))?\s*\)$/', $name, $matches)) {
- if (stristr($name, '(') !== false) {
- throw new InvalidArgumentException(sprintf(
- 'Malformed aggregate filter %s',
- $name
- ));
- }
- return;
- }
- $this->aggregate = [
- 'function' => strtoupper($matches[1]),
- 'column' => isset($matches[2]) ? $matches[2] : null
- ];
- }
- /**
- * Set the root model class to be selected by this
- * search query.
- *
- * @param string|DataObject $className
- */
- public function setModel($className)
- {
- $this->model = ClassInfo::class_name($className);
- }
- /**
- * Set the current value(s) to be filtered on.
- *
- * @param string|array $value
- */
- public function setValue($value)
- {
- $this->value = $value;
- }
- /**
- * Accessor for the current value to be filtered on.
- *
- * @return string|array
- */
- public function getValue()
- {
- return $this->value;
- }
- /**
- * Set the current modifiers to apply to the filter
- *
- * @param array $modifiers
- */
- public function setModifiers(array $modifiers)
- {
- $modifiers = array_map('strtolower', $modifiers);
- // Validate modifiers are supported
- $allowed = $this->getSupportedModifiers();
- $unsupported = array_diff($modifiers, $allowed);
- if ($unsupported) {
- throw new InvalidArgumentException(
- static::class . ' does not accept ' . implode(', ', $unsupported) . ' as modifiers'
- );
- }
- $this->modifiers = $modifiers;
- }
- /**
- * Gets supported modifiers for this filter
- *
- * @return array
- */
- public function getSupportedModifiers()
- {
- // By default support 'not' as a modifier for all filters
- return ['not'];
- }
- /**
- * Accessor for the current modifiers to apply to the filter.
- *
- * @return array
- */
- public function getModifiers()
- {
- return $this->modifiers;
- }
- /**
- * The original name of the field.
- *
- * @return string
- */
- public function getName()
- {
- return $this->name;
- }
- /**
- * @param String
- */
- public function setName($name)
- {
- $this->name = $name;
- }
- /**
- * The full name passed to the constructor,
- * including any (optional) relations in dot notation.
- *
- * @return string
- */
- public function getFullName()
- {
- return $this->fullName;
- }
- /**
- * @param String
- */
- public function setFullName($name)
- {
- $this->fullName = $name;
- }
- /**
- * Normalizes the field name to table mapping.
- *
- * @return string
- */
- public function getDbName()
- {
- // Special handler for "NULL" relations
- if ($this->name === "NULL") {
- return $this->name;
- }
- // Ensure that we're dealing with a DataObject.
- if (!is_subclass_of($this->model, DataObject::class)) {
- throw new InvalidArgumentException(
- "Model supplied to " . static::class . " should be an instance of DataObject."
- );
- }
- $tablePrefix = DataQuery::applyRelationPrefix($this->relation);
- $schema = DataObject::getSchema();
- if ($this->aggregate) {
- $column = $this->aggregate['column'];
- $function = $this->aggregate['function'];
- $table = $column ?
- $schema->tableForField($this->model, $column) :
- $schema->baseDataTable($this->model);
- if (!$table) {
- throw new InvalidArgumentException(sprintf(
- 'Invalid column %s for aggregate function %s on %s',
- $column,
- $function,
- $this->model
- ));
- }
- return sprintf(
- '%s("%s%s".%s)',
- $function,
- $tablePrefix,
- $table,
- $column ? "\"$column\"" : '"ID"'
- );
- }
- // Check if this column is a table on the current model
- $table = $schema->tableForField($this->model, $this->name);
- if ($table) {
- return $schema->sqlColumnForField($this->model, $this->name, $tablePrefix);
- }
- // fallback to the provided name in the event of a joined column
- // name (as the candidate class doesn't check joined records)
- $parts = explode('.', $this->fullName);
- return '"' . implode('"."', $parts) . '"';
- }
- /**
- * Return the value of the field as processed by the DBField class
- *
- * @return string
- */
- public function getDbFormattedValue()
- {
- // SRM: This code finds the table where the field named $this->name lives
- // Todo: move to somewhere more appropriate, such as DataMapper, the magical class-to-be?
- if ($this->aggregate) {
- return intval($this->value);
- }
- /** @var DBField $dbField */
- $dbField = singleton($this->model)->dbObject($this->name);
- $dbField->setValue($this->value);
- return $dbField->RAW();
- }
- /**
- * Given an escaped HAVING clause, add it along with the appropriate GROUP BY clause
- * @param DataQuery $query
- * @param string $having
- * @return DataQuery
- */
- public function applyAggregate(DataQuery $query, $having)
- {
- $schema = DataObject::getSchema();
- $baseTable = $schema->baseDataTable($query->dataClass());
- return $query
- ->having($having)
- ->groupby("\"{$baseTable}\".\"ID\"");
- }
- /**
- * Apply filter criteria to a SQL query.
- *
- * @param DataQuery $query
- * @return DataQuery
- */
- public function apply(DataQuery $query)
- {
- if (($key = array_search('not', $this->modifiers)) !== false) {
- unset($this->modifiers[$key]);
- return $this->exclude($query);
- }
- if (is_array($this->value)) {
- return $this->applyMany($query);
- } else {
- return $this->applyOne($query);
- }
- }
- /**
- * Apply filter criteria to a SQL query with a single value.
- *
- * @param DataQuery $query
- * @return DataQuery
- */
- abstract protected function applyOne(DataQuery $query);
- /**
- * Apply filter criteria to a SQL query with an array of values.
- *
- * @param DataQuery $query
- * @return DataQuery
- */
- protected function applyMany(DataQuery $query)
- {
- throw new InvalidArgumentException(static::class . " can't be used to filter by a list of items.");
- }
- /**
- * Exclude filter criteria from a SQL query.
- *
- * @param DataQuery $query
- * @return DataQuery
- */
- public function exclude(DataQuery $query)
- {
- if (($key = array_search('not', $this->modifiers)) !== false) {
- unset($this->modifiers[$key]);
- return $this->apply($query);
- }
- if (is_array($this->value)) {
- return $this->excludeMany($query);
- } else {
- return $this->excludeOne($query);
- }
- }
- /**
- * Exclude filter criteria from a SQL query with a single value.
- *
- * @param DataQuery $query
- * @return DataQuery
- */
- abstract protected function excludeOne(DataQuery $query);
- /**
- * Exclude filter criteria from a SQL query with an array of values.
- *
- * @param DataQuery $query
- * @return DataQuery
- */
- protected function excludeMany(DataQuery $query)
- {
- throw new InvalidArgumentException(static::class . " can't be used to filter by a list of items.");
- }
- /**
- * Determines if a field has a value,
- * and that the filter should be applied.
- * Relies on the field being populated with
- * {@link setValue()}
- *
- * @return boolean
- */
- public function isEmpty()
- {
- return false;
- }
- /**
- * Determines case sensitivity based on {@link getModifiers()}.
- *
- * @return Mixed TRUE or FALSE to enforce sensitivity, NULL to use field collation.
- */
- protected function getCaseSensitive()
- {
- $modifiers = $this->getModifiers();
- if (in_array('case', $modifiers)) {
- return true;
- } elseif (in_array('nocase', $modifiers)) {
- return false;
- } else {
- return null;
- }
- }
- }