blob: 3217173e46399a49686a02af7283e7d9e2fbed7c [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3namespace LdapRecord\Query;
4
5use UnexpectedValueException;
6
7class Grammar
8{
9 /**
10 * The query operators and their method names.
11 *
12 * @var array
13 */
14 public $operators = [
15 '*' => 'has',
16 '!*' => 'notHas',
17 '=' => 'equals',
18 '!' => 'doesNotEqual',
19 '!=' => 'doesNotEqual',
20 '>=' => 'greaterThanOrEquals',
21 '<=' => 'lessThanOrEquals',
22 '~=' => 'approximatelyEquals',
23 'starts_with' => 'startsWith',
24 'not_starts_with' => 'notStartsWith',
25 'ends_with' => 'endsWith',
26 'not_ends_with' => 'notEndsWith',
27 'contains' => 'contains',
28 'not_contains' => 'notContains',
29 ];
30
31 /**
32 * The query wrapper.
33 *
34 * @var string|null
35 */
36 protected $wrapper;
37
38 /**
39 * Get all the available operators.
40 *
41 * @return array
42 */
43 public function getOperators()
44 {
45 return array_keys($this->operators);
46 }
47
48 /**
49 * Wraps a query string in brackets.
50 *
51 * Produces: (query)
52 *
53 * @param string $query
54 * @param string $prefix
55 * @param string $suffix
56 *
57 * @return string
58 */
59 public function wrap($query, $prefix = '(', $suffix = ')')
60 {
61 return $prefix.$query.$suffix;
62 }
63
64 /**
65 * Compiles the Builder instance into an LDAP query string.
66 *
67 * @param Builder $query
68 *
69 * @return string
70 */
71 public function compile(Builder $query)
72 {
73 if ($this->queryMustBeWrapped($query)) {
74 $this->wrapper = 'and';
75 }
76
77 $filter = $this->compileRaws($query)
78 .$this->compileWheres($query)
79 .$this->compileOrWheres($query);
80
81 switch ($this->wrapper) {
82 case 'and':
83 return $this->compileAnd($filter);
84 case 'or':
85 return $this->compileOr($filter);
86 default:
87 return $filter;
88 }
89 }
90
91 /**
92 * Determine if the query must be wrapped in an encapsulating statement.
93 *
94 * @param Builder $query
95 *
96 * @return bool
97 */
98 protected function queryMustBeWrapped(Builder $query)
99 {
100 return ! $query->isNested() && $this->hasMultipleFilters($query);
101 }
102
103 /**
104 * Assembles all of the "raw" filters on the query.
105 *
106 * @param Builder $builder
107 *
108 * @return string
109 */
110 protected function compileRaws(Builder $builder)
111 {
112 return $this->concatenate($builder->filters['raw']);
113 }
114
115 /**
116 * Assembles all where clauses in the current wheres property.
117 *
118 * @param Builder $builder
119 * @param string $type
120 *
121 * @return string
122 */
123 protected function compileWheres(Builder $builder, $type = 'and')
124 {
125 $filter = '';
126
127 foreach ($builder->filters[$type] as $where) {
128 $filter .= $this->compileWhere($where);
129 }
130
131 return $filter;
132 }
133
134 /**
135 * Assembles all or where clauses in the current orWheres property.
136 *
137 * @param Builder $query
138 *
139 * @return string
140 */
141 protected function compileOrWheres(Builder $query)
142 {
143 $filter = $this->compileWheres($query, 'or');
144
145 if (! $this->hasMultipleFilters($query)) {
146 return $filter;
147 }
148
149 // Here we will detect whether the entire query can be
150 // wrapped inside of an "or" statement by checking
151 // how many filter statements exist for each type.
152 if ($this->queryCanBeWrappedInSingleOrStatement($query)) {
153 $this->wrapper = 'or';
154 } else {
155 $filter = $this->compileOr($filter);
156 }
157
158 return $filter;
159 }
160
161 /**
162 * Determine if the query can be wrapped in a single or statement.
163 *
164 * @param Builder $query
165 *
166 * @return bool
167 */
168 protected function queryCanBeWrappedInSingleOrStatement(Builder $query)
169 {
170 return $this->has($query, 'or', '>=', 1) &&
171 $this->has($query, 'and', '<=', 1) &&
172 $this->has($query, 'raw', '=', 0);
173 }
174
175 /**
176 * Concatenates filters into a single string.
177 *
178 * @param array $bindings
179 *
180 * @return string
181 */
182 public function concatenate(array $bindings = [])
183 {
184 // Filter out empty query segments.
185 return implode(
186 array_filter($bindings, [$this, 'bindingValueIsNotEmpty'])
187 );
188 }
189
190 /**
191 * Determine if the binding value is not empty.
192 *
193 * @param string $value
194 *
195 * @return bool
196 */
197 protected function bindingValueIsNotEmpty($value)
198 {
199 return ! empty($value);
200 }
201
202 /**
203 * Determine if the query is using multiple filters.
204 *
205 * @param Builder $query
206 *
207 * @return bool
208 */
209 protected function hasMultipleFilters(Builder $query)
210 {
211 return $this->has($query, ['and', 'or', 'raw'], '>', 1);
212 }
213
214 /**
215 * Determine if the query contains the given filter statement type.
216 *
217 * @param Builder $query
218 * @param string|array $type
219 * @param string $operator
220 * @param int $count
221 *
222 * @return bool
223 */
224 protected function has(Builder $query, $type, $operator = '>=', $count = 1)
225 {
226 $types = (array) $type;
227
228 $filters = 0;
229
230 foreach ($types as $type) {
231 $filters += count($query->filters[$type]);
232 }
233
234 switch ($operator) {
235 case '>':
236 return $filters > $count;
237 case '>=':
238 return $filters >= $count;
239 case '<':
240 return $filters < $count;
241 case '<=':
242 return $filters <= $count;
243 default:
244 return $filters == $count;
245 }
246 }
247
248 /**
249 * Returns a query string for equals.
250 *
251 * Produces: (field=value)
252 *
253 * @param string $field
254 * @param string $value
255 *
256 * @return string
257 */
258 public function compileEquals($field, $value)
259 {
260 return $this->wrap($field.'='.$value);
261 }
262
263 /**
264 * Returns a query string for does not equal.
265 *
266 * Produces: (!(field=value))
267 *
268 * @param string $field
269 * @param string $value
270 *
271 * @return string
272 */
273 public function compileDoesNotEqual($field, $value)
274 {
275 return $this->compileNot(
276 $this->compileEquals($field, $value)
277 );
278 }
279
280 /**
281 * Alias for does not equal operator (!=) operator.
282 *
283 * Produces: (!(field=value))
284 *
285 * @param string $field
286 * @param string $value
287 *
288 * @return string
289 */
290 public function compileDoesNotEqualAlias($field, $value)
291 {
292 return $this->compileDoesNotEqual($field, $value);
293 }
294
295 /**
296 * Returns a query string for greater than or equals.
297 *
298 * Produces: (field>=value)
299 *
300 * @param string $field
301 * @param string $value
302 *
303 * @return string
304 */
305 public function compileGreaterThanOrEquals($field, $value)
306 {
307 return $this->wrap("$field>=$value");
308 }
309
310 /**
311 * Returns a query string for less than or equals.
312 *
313 * Produces: (field<=value)
314 *
315 * @param string $field
316 * @param string $value
317 *
318 * @return string
319 */
320 public function compileLessThanOrEquals($field, $value)
321 {
322 return $this->wrap("$field<=$value");
323 }
324
325 /**
326 * Returns a query string for approximately equals.
327 *
328 * Produces: (field~=value)
329 *
330 * @param string $field
331 * @param string $value
332 *
333 * @return string
334 */
335 public function compileApproximatelyEquals($field, $value)
336 {
337 return $this->wrap("$field~=$value");
338 }
339
340 /**
341 * Returns a query string for starts with.
342 *
343 * Produces: (field=value*)
344 *
345 * @param string $field
346 * @param string $value
347 *
348 * @return string
349 */
350 public function compileStartsWith($field, $value)
351 {
352 return $this->wrap("$field=$value*");
353 }
354
355 /**
356 * Returns a query string for does not start with.
357 *
358 * Produces: (!(field=*value))
359 *
360 * @param string $field
361 * @param string $value
362 *
363 * @return string
364 */
365 public function compileNotStartsWith($field, $value)
366 {
367 return $this->compileNot(
368 $this->compileStartsWith($field, $value)
369 );
370 }
371
372 /**
373 * Returns a query string for ends with.
374 *
375 * Produces: (field=*value)
376 *
377 * @param string $field
378 * @param string $value
379 *
380 * @return string
381 */
382 public function compileEndsWith($field, $value)
383 {
384 return $this->wrap("$field=*$value");
385 }
386
387 /**
388 * Returns a query string for does not end with.
389 *
390 * Produces: (!(field=value*))
391 *
392 * @param string $field
393 * @param string $value
394 *
395 * @return string
396 */
397 public function compileNotEndsWith($field, $value)
398 {
399 return $this->compileNot($this->compileEndsWith($field, $value));
400 }
401
402 /**
403 * Returns a query string for contains.
404 *
405 * Produces: (field=*value*)
406 *
407 * @param string $field
408 * @param string $value
409 *
410 * @return string
411 */
412 public function compileContains($field, $value)
413 {
414 return $this->wrap("$field=*$value*");
415 }
416
417 /**
418 * Returns a query string for does not contain.
419 *
420 * Produces: (!(field=*value*))
421 *
422 * @param string $field
423 * @param string $value
424 *
425 * @return string
426 */
427 public function compileNotContains($field, $value)
428 {
429 return $this->compileNot(
430 $this->compileContains($field, $value)
431 );
432 }
433
434 /**
435 * Returns a query string for a where has.
436 *
437 * Produces: (field=*)
438 *
439 * @param string $field
440 *
441 * @return string
442 */
443 public function compileHas($field)
444 {
445 return $this->wrap("$field=*");
446 }
447
448 /**
449 * Returns a query string for a where does not have.
450 *
451 * Produces: (!(field=*))
452 *
453 * @param string $field
454 *
455 * @return string
456 */
457 public function compileNotHas($field)
458 {
459 return $this->compileNot(
460 $this->compileHas($field)
461 );
462 }
463
464 /**
465 * Wraps the inserted query inside an AND operator.
466 *
467 * Produces: (&query)
468 *
469 * @param string $query
470 *
471 * @return string
472 */
473 public function compileAnd($query)
474 {
475 return $query ? $this->wrap($query, '(&') : '';
476 }
477
478 /**
479 * Wraps the inserted query inside an OR operator.
480 *
481 * Produces: (|query)
482 *
483 * @param string $query
484 *
485 * @return string
486 */
487 public function compileOr($query)
488 {
489 return $query ? $this->wrap($query, '(|') : '';
490 }
491
492 /**
493 * Wraps the inserted query inside an NOT operator.
494 *
495 * @param string $query
496 *
497 * @return string
498 */
499 public function compileNot($query)
500 {
501 return $query ? $this->wrap($query, '(!') : '';
502 }
503
504 /**
505 * Assembles a single where query.
506 *
507 * @param array $where
508 *
509 * @throws UnexpectedValueException
510 *
511 * @return string
512 */
513 protected function compileWhere(array $where)
514 {
515 $method = $this->makeCompileMethod($where['operator']);
516
517 return $this->{$method}($where['field'], $where['value']);
518 }
519
520 /**
521 * Make the compile method name for the operator.
522 *
523 * @param string $operator
524 *
525 * @throws UnexpectedValueException
526 *
527 * @return string
528 */
529 protected function makeCompileMethod($operator)
530 {
531 if (! $this->operatorExists($operator)) {
532 throw new UnexpectedValueException("Invalid LDAP filter operator ['$operator']");
533 }
534
535 return 'compile'.ucfirst($this->operators[$operator]);
536 }
537
538 /**
539 * Determine if the operator exists.
540 *
541 * @param string $operator
542 *
543 * @return bool
544 */
545 protected function operatorExists($operator)
546 {
547 return array_key_exists($operator, $this->operators);
548 }
549}