Chris@18
|
1 <?php
|
Chris@18
|
2
|
Chris@18
|
3 namespace Drupal\jsonapi\Query;
|
Chris@18
|
4
|
Chris@18
|
5 use Drupal\Core\Cache\CacheableMetadata;
|
Chris@18
|
6 use Drupal\Core\Http\Exception\CacheableBadRequestHttpException;
|
Chris@18
|
7
|
Chris@18
|
8 /**
|
Chris@18
|
9 * A condition object for the EntityQuery.
|
Chris@18
|
10 *
|
Chris@18
|
11 * @internal JSON:API maintains no PHP API since its API is the HTTP API. This
|
Chris@18
|
12 * class may change at any time and this will break any dependencies on it.
|
Chris@18
|
13 *
|
Chris@18
|
14 * @see https://www.drupal.org/project/jsonapi/issues/3032787
|
Chris@18
|
15 * @see jsonapi.api.php
|
Chris@18
|
16 */
|
Chris@18
|
17 class EntityCondition {
|
Chris@18
|
18
|
Chris@18
|
19 /**
|
Chris@18
|
20 * The field key in the filter condition: filter[lorem][condition][<field>].
|
Chris@18
|
21 *
|
Chris@18
|
22 * @var string
|
Chris@18
|
23 */
|
Chris@18
|
24 const PATH_KEY = 'path';
|
Chris@18
|
25
|
Chris@18
|
26 /**
|
Chris@18
|
27 * The value key in the filter condition: filter[lorem][condition][<value>].
|
Chris@18
|
28 *
|
Chris@18
|
29 * @var string
|
Chris@18
|
30 */
|
Chris@18
|
31 const VALUE_KEY = 'value';
|
Chris@18
|
32
|
Chris@18
|
33 /**
|
Chris@18
|
34 * The operator key in the condition: filter[lorem][condition][<operator>].
|
Chris@18
|
35 *
|
Chris@18
|
36 * @var string
|
Chris@18
|
37 */
|
Chris@18
|
38 const OPERATOR_KEY = 'operator';
|
Chris@18
|
39
|
Chris@18
|
40 /**
|
Chris@18
|
41 * The allowed condition operators.
|
Chris@18
|
42 *
|
Chris@18
|
43 * @var string[]
|
Chris@18
|
44 */
|
Chris@18
|
45 public static $allowedOperators = [
|
Chris@18
|
46 '=', '<>',
|
Chris@18
|
47 '>', '>=', '<', '<=',
|
Chris@18
|
48 'STARTS_WITH', 'CONTAINS', 'ENDS_WITH',
|
Chris@18
|
49 'IN', 'NOT IN',
|
Chris@18
|
50 'BETWEEN', 'NOT BETWEEN',
|
Chris@18
|
51 'IS NULL', 'IS NOT NULL',
|
Chris@18
|
52 ];
|
Chris@18
|
53
|
Chris@18
|
54 /**
|
Chris@18
|
55 * The field to be evaluated.
|
Chris@18
|
56 *
|
Chris@18
|
57 * @var string
|
Chris@18
|
58 */
|
Chris@18
|
59 protected $field;
|
Chris@18
|
60
|
Chris@18
|
61 /**
|
Chris@18
|
62 * The condition operator.
|
Chris@18
|
63 *
|
Chris@18
|
64 * @var string
|
Chris@18
|
65 */
|
Chris@18
|
66 protected $operator;
|
Chris@18
|
67
|
Chris@18
|
68 /**
|
Chris@18
|
69 * The value against which the field should be evaluated.
|
Chris@18
|
70 *
|
Chris@18
|
71 * @var mixed
|
Chris@18
|
72 */
|
Chris@18
|
73 protected $value;
|
Chris@18
|
74
|
Chris@18
|
75 /**
|
Chris@18
|
76 * Constructs a new EntityCondition object.
|
Chris@18
|
77 */
|
Chris@18
|
78 public function __construct($field, $value, $operator = NULL) {
|
Chris@18
|
79 $this->field = $field;
|
Chris@18
|
80 $this->value = $value;
|
Chris@18
|
81 $this->operator = ($operator) ? $operator : '=';
|
Chris@18
|
82 }
|
Chris@18
|
83
|
Chris@18
|
84 /**
|
Chris@18
|
85 * The field to be evaluated.
|
Chris@18
|
86 *
|
Chris@18
|
87 * @return string
|
Chris@18
|
88 * The field upon which to evaluate the condition.
|
Chris@18
|
89 */
|
Chris@18
|
90 public function field() {
|
Chris@18
|
91 return $this->field;
|
Chris@18
|
92 }
|
Chris@18
|
93
|
Chris@18
|
94 /**
|
Chris@18
|
95 * The comparison operator to use for the evaluation.
|
Chris@18
|
96 *
|
Chris@18
|
97 * For a list of allowed operators:
|
Chris@18
|
98 *
|
Chris@18
|
99 * @see \Drupal\jsonapi\Query\EntityCondition::allowedOperators
|
Chris@18
|
100 *
|
Chris@18
|
101 * @return string
|
Chris@18
|
102 * The condition operator.
|
Chris@18
|
103 */
|
Chris@18
|
104 public function operator() {
|
Chris@18
|
105 return $this->operator;
|
Chris@18
|
106 }
|
Chris@18
|
107
|
Chris@18
|
108 /**
|
Chris@18
|
109 * The value against which the condition should be evaluated.
|
Chris@18
|
110 *
|
Chris@18
|
111 * @return mixed
|
Chris@18
|
112 * The condition comparison value.
|
Chris@18
|
113 */
|
Chris@18
|
114 public function value() {
|
Chris@18
|
115 return $this->value;
|
Chris@18
|
116 }
|
Chris@18
|
117
|
Chris@18
|
118 /**
|
Chris@18
|
119 * Creates an EntityCondition object from a query parameter.
|
Chris@18
|
120 *
|
Chris@18
|
121 * @param mixed $parameter
|
Chris@18
|
122 * The `filter[condition]` query parameter from the request.
|
Chris@18
|
123 *
|
Chris@18
|
124 * @return self
|
Chris@18
|
125 * An EntityCondition object with defaults.
|
Chris@18
|
126 */
|
Chris@18
|
127 public static function createFromQueryParameter($parameter) {
|
Chris@18
|
128 static::validate($parameter);
|
Chris@18
|
129 $field = $parameter[static::PATH_KEY];
|
Chris@18
|
130 $value = (isset($parameter[static::VALUE_KEY])) ? $parameter[static::VALUE_KEY] : NULL;
|
Chris@18
|
131 $operator = (isset($parameter[static::OPERATOR_KEY])) ? $parameter[static::OPERATOR_KEY] : NULL;
|
Chris@18
|
132 return new static($field, $value, $operator);
|
Chris@18
|
133 }
|
Chris@18
|
134
|
Chris@18
|
135 /**
|
Chris@18
|
136 * Validates the filter has the required fields.
|
Chris@18
|
137 */
|
Chris@18
|
138 protected static function validate($parameter) {
|
Chris@18
|
139 $valid_key_combinations = [
|
Chris@18
|
140 [static::PATH_KEY, static::VALUE_KEY],
|
Chris@18
|
141 [static::PATH_KEY, static::OPERATOR_KEY],
|
Chris@18
|
142 [static::PATH_KEY, static::VALUE_KEY, static::OPERATOR_KEY],
|
Chris@18
|
143 ];
|
Chris@18
|
144
|
Chris@18
|
145 $given_keys = array_keys($parameter);
|
Chris@18
|
146 $valid_key_set = array_reduce($valid_key_combinations, function ($valid, $set) use ($given_keys) {
|
Chris@18
|
147 return ($valid) ? $valid : count(array_diff($set, $given_keys)) === 0;
|
Chris@18
|
148 }, FALSE);
|
Chris@18
|
149
|
Chris@18
|
150 $has_operator_key = isset($parameter[static::OPERATOR_KEY]);
|
Chris@18
|
151 $has_path_key = isset($parameter[static::PATH_KEY]);
|
Chris@18
|
152 $has_value_key = isset($parameter[static::VALUE_KEY]);
|
Chris@18
|
153
|
Chris@18
|
154 $cacheability = (new CacheableMetadata())->addCacheContexts(['url.query_args:filter']);
|
Chris@18
|
155 if (!$valid_key_set) {
|
Chris@18
|
156 // Try to provide a more specific exception is a key is missing.
|
Chris@18
|
157 if (!$has_operator_key) {
|
Chris@18
|
158 if (!$has_path_key) {
|
Chris@18
|
159 throw new CacheableBadRequestHttpException($cacheability, "Filter parameter is missing a '" . static::PATH_KEY . "' key.");
|
Chris@18
|
160 }
|
Chris@18
|
161 if (!$has_value_key) {
|
Chris@18
|
162 throw new CacheableBadRequestHttpException($cacheability, "Filter parameter is missing a '" . static::VALUE_KEY . "' key.");
|
Chris@18
|
163 }
|
Chris@18
|
164 }
|
Chris@18
|
165
|
Chris@18
|
166 // Catchall exception.
|
Chris@18
|
167 $reason = "You must provide a valid filter condition. Check that you have set the required keys for your filter.";
|
Chris@18
|
168 throw new CacheableBadRequestHttpException($cacheability, $reason);
|
Chris@18
|
169 }
|
Chris@18
|
170
|
Chris@18
|
171 if ($has_operator_key) {
|
Chris@18
|
172 $operator = $parameter[static::OPERATOR_KEY];
|
Chris@18
|
173 if (!in_array($operator, static::$allowedOperators)) {
|
Chris@18
|
174 $reason = "The '" . $operator . "' operator is not allowed in a filter parameter.";
|
Chris@18
|
175 throw new CacheableBadRequestHttpException($cacheability, $reason);
|
Chris@18
|
176 }
|
Chris@18
|
177
|
Chris@18
|
178 if (in_array($operator, ['IS NULL', 'IS NOT NULL']) && $has_value_key) {
|
Chris@18
|
179 $reason = "Filters using the '" . $operator . "' operator should not provide a value.";
|
Chris@18
|
180 throw new CacheableBadRequestHttpException($cacheability, $reason);
|
Chris@18
|
181 }
|
Chris@18
|
182 }
|
Chris@18
|
183 }
|
Chris@18
|
184
|
Chris@18
|
185 }
|