PageRenderTime 217ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 1ms

/src/ORM/Filters/SearchFilter.php

https://gitlab.com/djpmedia/silverstripe-framework
PHP | 453 lines | 232 code | 46 blank | 175 comment | 17 complexity | 6e832514c019227acc997035fe1fcd4c MD5 | raw file
  1. <?php
  2. namespace SilverStripe\ORM\Filters;
  3. use SilverStripe\Core\ClassInfo;
  4. use SilverStripe\Core\Injector\Injectable;
  5. use SilverStripe\ORM\DataObject;
  6. use SilverStripe\ORM\DataQuery;
  7. use InvalidArgumentException;
  8. use SilverStripe\ORM\FieldType\DBField;
  9. /**
  10. * Base class for filtering implementations,
  11. * which work together with {@link SearchContext}
  12. * to create or amend a query for {@link DataObject} instances.
  13. * See {@link SearchContext} for more information.
  14. *
  15. * Each search filter must be registered in config as an "Injector" service with
  16. * the "DataListFilter." prefix. E.g.
  17. *
  18. * <code>
  19. * Injector:
  20. * DataListFilter.EndsWith:
  21. * class: EndsWithFilter
  22. * </code>
  23. */
  24. abstract class SearchFilter
  25. {
  26. use Injectable;
  27. /**
  28. * Classname of the inspected {@link DataObject}.
  29. * If pointing to a relation, this will be the classname of the leaf
  30. * class in the relation
  31. *
  32. * @var string
  33. */
  34. protected $model;
  35. /**
  36. * @var string
  37. */
  38. protected $name;
  39. /**
  40. * @var string
  41. */
  42. protected $fullName;
  43. /**
  44. * @var mixed
  45. */
  46. protected $value;
  47. /**
  48. * @var array
  49. */
  50. protected $modifiers;
  51. /**
  52. * @var array Parts of a has-one, has-many or many-many relation (not the classname).
  53. * Set in the constructor as part of the name in dot-notation, and used in
  54. * {@link applyRelation()}.
  55. *
  56. * Also used to build table prefix (see getRelationTablePrefix)
  57. */
  58. protected $relation = [];
  59. /**
  60. * An array of data about an aggregate column being used
  61. * ex:
  62. * [
  63. * 'function' => 'COUNT',
  64. * 'column' => 'ID'
  65. * ]
  66. * @var array
  67. */
  68. protected $aggregate;
  69. /**
  70. * @param string $fullName Determines the name of the field, as well as the searched database
  71. * column. Can contain a relation name in dot notation, which will automatically join
  72. * the necessary tables (e.g. "Comments.Name" to join the "Comments" has-many relationship and
  73. * search the "Name" column when applying this filter to a SiteTree class).
  74. * @param mixed $value
  75. * @param array $modifiers
  76. */
  77. public function __construct($fullName = null, $value = false, array $modifiers = array())
  78. {
  79. $this->fullName = $fullName;
  80. // sets $this->name and $this->relation
  81. $this->addRelation($fullName);
  82. $this->addAggregate($fullName);
  83. $this->value = $value;
  84. $this->setModifiers($modifiers);
  85. }
  86. /**
  87. * Called by constructor to convert a string pathname into
  88. * a well defined relationship sequence.
  89. *
  90. * @param string $name
  91. */
  92. protected function addRelation($name)
  93. {
  94. if (strstr($name, '.')) {
  95. $parts = explode('.', $name);
  96. $this->name = array_pop($parts);
  97. $this->relation = $parts;
  98. } else {
  99. $this->name = $name;
  100. }
  101. }
  102. /**
  103. * Parses the name for any aggregate functions and stores them in the $aggregate array
  104. *
  105. * @param string $name
  106. */
  107. protected function addAggregate($name)
  108. {
  109. if (!$this->relation) {
  110. return;
  111. }
  112. if (!preg_match('/([A-Za-z]+)\(\s*(?:([A-Za-z_*][A-Za-z0-9_]*))?\s*\)$/', $name, $matches)) {
  113. if (stristr($name, '(') !== false) {
  114. throw new InvalidArgumentException(sprintf(
  115. 'Malformed aggregate filter %s',
  116. $name
  117. ));
  118. }
  119. return;
  120. }
  121. $this->aggregate = [
  122. 'function' => strtoupper($matches[1]),
  123. 'column' => isset($matches[2]) ? $matches[2] : null
  124. ];
  125. }
  126. /**
  127. * Set the root model class to be selected by this
  128. * search query.
  129. *
  130. * @param string|DataObject $className
  131. */
  132. public function setModel($className)
  133. {
  134. $this->model = ClassInfo::class_name($className);
  135. }
  136. /**
  137. * Set the current value(s) to be filtered on.
  138. *
  139. * @param string|array $value
  140. */
  141. public function setValue($value)
  142. {
  143. $this->value = $value;
  144. }
  145. /**
  146. * Accessor for the current value to be filtered on.
  147. *
  148. * @return string|array
  149. */
  150. public function getValue()
  151. {
  152. return $this->value;
  153. }
  154. /**
  155. * Set the current modifiers to apply to the filter
  156. *
  157. * @param array $modifiers
  158. */
  159. public function setModifiers(array $modifiers)
  160. {
  161. $modifiers = array_map('strtolower', $modifiers);
  162. // Validate modifiers are supported
  163. $allowed = $this->getSupportedModifiers();
  164. $unsupported = array_diff($modifiers, $allowed);
  165. if ($unsupported) {
  166. throw new InvalidArgumentException(
  167. static::class . ' does not accept ' . implode(', ', $unsupported) . ' as modifiers'
  168. );
  169. }
  170. $this->modifiers = $modifiers;
  171. }
  172. /**
  173. * Gets supported modifiers for this filter
  174. *
  175. * @return array
  176. */
  177. public function getSupportedModifiers()
  178. {
  179. // By default support 'not' as a modifier for all filters
  180. return ['not'];
  181. }
  182. /**
  183. * Accessor for the current modifiers to apply to the filter.
  184. *
  185. * @return array
  186. */
  187. public function getModifiers()
  188. {
  189. return $this->modifiers;
  190. }
  191. /**
  192. * The original name of the field.
  193. *
  194. * @return string
  195. */
  196. public function getName()
  197. {
  198. return $this->name;
  199. }
  200. /**
  201. * @param String
  202. */
  203. public function setName($name)
  204. {
  205. $this->name = $name;
  206. }
  207. /**
  208. * The full name passed to the constructor,
  209. * including any (optional) relations in dot notation.
  210. *
  211. * @return string
  212. */
  213. public function getFullName()
  214. {
  215. return $this->fullName;
  216. }
  217. /**
  218. * @param String
  219. */
  220. public function setFullName($name)
  221. {
  222. $this->fullName = $name;
  223. }
  224. /**
  225. * Normalizes the field name to table mapping.
  226. *
  227. * @return string
  228. */
  229. public function getDbName()
  230. {
  231. // Special handler for "NULL" relations
  232. if ($this->name === "NULL") {
  233. return $this->name;
  234. }
  235. // Ensure that we're dealing with a DataObject.
  236. if (!is_subclass_of($this->model, DataObject::class)) {
  237. throw new InvalidArgumentException(
  238. "Model supplied to " . static::class . " should be an instance of DataObject."
  239. );
  240. }
  241. $tablePrefix = DataQuery::applyRelationPrefix($this->relation);
  242. $schema = DataObject::getSchema();
  243. if ($this->aggregate) {
  244. $column = $this->aggregate['column'];
  245. $function = $this->aggregate['function'];
  246. $table = $column ?
  247. $schema->tableForField($this->model, $column) :
  248. $schema->baseDataTable($this->model);
  249. if (!$table) {
  250. throw new InvalidArgumentException(sprintf(
  251. 'Invalid column %s for aggregate function %s on %s',
  252. $column,
  253. $function,
  254. $this->model
  255. ));
  256. }
  257. return sprintf(
  258. '%s("%s%s".%s)',
  259. $function,
  260. $tablePrefix,
  261. $table,
  262. $column ? "\"$column\"" : '"ID"'
  263. );
  264. }
  265. // Check if this column is a table on the current model
  266. $table = $schema->tableForField($this->model, $this->name);
  267. if ($table) {
  268. return $schema->sqlColumnForField($this->model, $this->name, $tablePrefix);
  269. }
  270. // fallback to the provided name in the event of a joined column
  271. // name (as the candidate class doesn't check joined records)
  272. $parts = explode('.', $this->fullName);
  273. return '"' . implode('"."', $parts) . '"';
  274. }
  275. /**
  276. * Return the value of the field as processed by the DBField class
  277. *
  278. * @return string
  279. */
  280. public function getDbFormattedValue()
  281. {
  282. // SRM: This code finds the table where the field named $this->name lives
  283. // Todo: move to somewhere more appropriate, such as DataMapper, the magical class-to-be?
  284. if ($this->aggregate) {
  285. return intval($this->value);
  286. }
  287. /** @var DBField $dbField */
  288. $dbField = singleton($this->model)->dbObject($this->name);
  289. $dbField->setValue($this->value);
  290. return $dbField->RAW();
  291. }
  292. /**
  293. * Given an escaped HAVING clause, add it along with the appropriate GROUP BY clause
  294. * @param DataQuery $query
  295. * @param string $having
  296. * @return DataQuery
  297. */
  298. public function applyAggregate(DataQuery $query, $having)
  299. {
  300. $schema = DataObject::getSchema();
  301. $baseTable = $schema->baseDataTable($query->dataClass());
  302. return $query
  303. ->having($having)
  304. ->groupby("\"{$baseTable}\".\"ID\"");
  305. }
  306. /**
  307. * Apply filter criteria to a SQL query.
  308. *
  309. * @param DataQuery $query
  310. * @return DataQuery
  311. */
  312. public function apply(DataQuery $query)
  313. {
  314. if (($key = array_search('not', $this->modifiers)) !== false) {
  315. unset($this->modifiers[$key]);
  316. return $this->exclude($query);
  317. }
  318. if (is_array($this->value)) {
  319. return $this->applyMany($query);
  320. } else {
  321. return $this->applyOne($query);
  322. }
  323. }
  324. /**
  325. * Apply filter criteria to a SQL query with a single value.
  326. *
  327. * @param DataQuery $query
  328. * @return DataQuery
  329. */
  330. abstract protected function applyOne(DataQuery $query);
  331. /**
  332. * Apply filter criteria to a SQL query with an array of values.
  333. *
  334. * @param DataQuery $query
  335. * @return DataQuery
  336. */
  337. protected function applyMany(DataQuery $query)
  338. {
  339. throw new InvalidArgumentException(static::class . " can't be used to filter by a list of items.");
  340. }
  341. /**
  342. * Exclude filter criteria from a SQL query.
  343. *
  344. * @param DataQuery $query
  345. * @return DataQuery
  346. */
  347. public function exclude(DataQuery $query)
  348. {
  349. if (($key = array_search('not', $this->modifiers)) !== false) {
  350. unset($this->modifiers[$key]);
  351. return $this->apply($query);
  352. }
  353. if (is_array($this->value)) {
  354. return $this->excludeMany($query);
  355. } else {
  356. return $this->excludeOne($query);
  357. }
  358. }
  359. /**
  360. * Exclude filter criteria from a SQL query with a single value.
  361. *
  362. * @param DataQuery $query
  363. * @return DataQuery
  364. */
  365. abstract protected function excludeOne(DataQuery $query);
  366. /**
  367. * Exclude filter criteria from a SQL query with an array of values.
  368. *
  369. * @param DataQuery $query
  370. * @return DataQuery
  371. */
  372. protected function excludeMany(DataQuery $query)
  373. {
  374. throw new InvalidArgumentException(static::class . " can't be used to filter by a list of items.");
  375. }
  376. /**
  377. * Determines if a field has a value,
  378. * and that the filter should be applied.
  379. * Relies on the field being populated with
  380. * {@link setValue()}
  381. *
  382. * @return boolean
  383. */
  384. public function isEmpty()
  385. {
  386. return false;
  387. }
  388. /**
  389. * Determines case sensitivity based on {@link getModifiers()}.
  390. *
  391. * @return Mixed TRUE or FALSE to enforce sensitivity, NULL to use field collation.
  392. */
  393. protected function getCaseSensitive()
  394. {
  395. $modifiers = $this->getModifiers();
  396. if (in_array('case', $modifiers)) {
  397. return true;
  398. } elseif (in_array('nocase', $modifiers)) {
  399. return false;
  400. } else {
  401. return null;
  402. }
  403. }
  404. }