blob: b9e31960e771f9f452199851b6f4a7d58cb8791f [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3namespace LdapRecord\Query;
4
5use BadMethodCallException;
6use Closure;
7use DateTimeInterface;
8use InvalidArgumentException;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01009use LDAP\Result;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020010use LdapRecord\Connection;
11use LdapRecord\Container;
12use LdapRecord\EscapesValues;
13use LdapRecord\LdapInterface;
14use LdapRecord\LdapRecordException;
15use LdapRecord\Models\Model;
16use LdapRecord\Query\Events\QueryExecuted;
17use LdapRecord\Query\Model\Builder as ModelBuilder;
18use LdapRecord\Query\Pagination\LazyPaginator;
19use LdapRecord\Query\Pagination\Paginator;
20use LdapRecord\Support\Arr;
21use LdapRecord\Utilities;
22
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010023/** @psalm-suppress UndefinedClass */
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020024class Builder
25{
26 use EscapesValues;
27
28 /**
29 * The selected columns to retrieve on the query.
30 *
31 * @var array
32 */
33 public $columns;
34
35 /**
36 * The query filters.
37 *
38 * @var array
39 */
40 public $filters = [
41 'and' => [],
42 'or' => [],
43 'raw' => [],
44 ];
45
46 /**
47 * The LDAP server controls to be sent.
48 *
49 * @var array
50 */
51 public $controls = [];
52
53 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010054 * The LDAP server controls that were processed.
55 *
56 * @var array
57 */
58 public $controlsResponse = [];
59
60 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020061 * The size limit of the query.
62 *
63 * @var int
64 */
65 public $limit = 0;
66
67 /**
68 * Determines whether the current query is paginated.
69 *
70 * @var bool
71 */
72 public $paginated = false;
73
74 /**
75 * The distinguished name to perform searches upon.
76 *
77 * @var string|null
78 */
79 protected $dn;
80
81 /**
82 * The base distinguished name to perform searches inside.
83 *
84 * @var string|null
85 */
86 protected $baseDn;
87
88 /**
89 * The default query type.
90 *
91 * @var string
92 */
93 protected $type = 'search';
94
95 /**
96 * Determines whether the query is nested.
97 *
98 * @var bool
99 */
100 protected $nested = false;
101
102 /**
103 * Determines whether the query should be cached.
104 *
105 * @var bool
106 */
107 protected $caching = false;
108
109 /**
110 * How long the query should be cached until.
111 *
112 * @var DateTimeInterface|null
113 */
114 protected $cacheUntil = null;
115
116 /**
117 * Determines whether the query cache must be flushed.
118 *
119 * @var bool
120 */
121 protected $flushCache = false;
122
123 /**
124 * The current connection instance.
125 *
126 * @var Connection
127 */
128 protected $connection;
129
130 /**
131 * The current grammar instance.
132 *
133 * @var Grammar
134 */
135 protected $grammar;
136
137 /**
138 * The current cache instance.
139 *
140 * @var Cache|null
141 */
142 protected $cache;
143
144 /**
145 * Constructor.
146 *
147 * @param Connection $connection
148 */
149 public function __construct(Connection $connection)
150 {
151 $this->connection = $connection;
152 $this->grammar = new Grammar();
153 }
154
155 /**
156 * Set the current connection.
157 *
158 * @param Connection $connection
159 *
160 * @return $this
161 */
162 public function setConnection(Connection $connection)
163 {
164 $this->connection = $connection;
165
166 return $this;
167 }
168
169 /**
170 * Set the current filter grammar.
171 *
172 * @param Grammar $grammar
173 *
174 * @return $this
175 */
176 public function setGrammar(Grammar $grammar)
177 {
178 $this->grammar = $grammar;
179
180 return $this;
181 }
182
183 /**
184 * Set the cache to store query results.
185 *
186 * @param Cache|null $cache
187 *
188 * @return $this
189 */
190 public function setCache(Cache $cache = null)
191 {
192 $this->cache = $cache;
193
194 return $this;
195 }
196
197 /**
198 * Returns a new Query Builder instance.
199 *
200 * @param string $baseDn
201 *
202 * @return $this
203 */
204 public function newInstance($baseDn = null)
205 {
206 // We'll set the base DN of the new Builder so
207 // developers don't need to do this manually.
208 $dn = is_null($baseDn) ? $this->getDn() : $baseDn;
209
210 return (new static($this->connection))->setDn($dn);
211 }
212
213 /**
214 * Returns a new nested Query Builder instance.
215 *
216 * @param Closure|null $closure
217 *
218 * @return $this
219 */
220 public function newNestedInstance(Closure $closure = null)
221 {
222 $query = $this->newInstance()->nested();
223
224 if ($closure) {
225 $closure($query);
226 }
227
228 return $query;
229 }
230
231 /**
232 * Executes the LDAP query.
233 *
234 * @param string|array $columns
235 *
236 * @return Collection|array
237 */
238 public function get($columns = ['*'])
239 {
240 return $this->onceWithColumns(Arr::wrap($columns), function () {
241 return $this->query($this->getQuery());
242 });
243 }
244
245 /**
246 * Execute the given callback while selecting the given columns.
247 *
248 * After running the callback, the columns are reset to the original value.
249 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100250 * @param array $columns
251 * @param Closure $callback
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200252 *
253 * @return mixed
254 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100255 protected function onceWithColumns($columns, Closure $callback)
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200256 {
257 $original = $this->columns;
258
259 if (is_null($original)) {
260 $this->columns = $columns;
261 }
262
263 $result = $callback();
264
265 $this->columns = $original;
266
267 return $result;
268 }
269
270 /**
271 * Compiles and returns the current query string.
272 *
273 * @return string
274 */
275 public function getQuery()
276 {
277 // We need to ensure we have at least one filter, as
278 // no query results will be returned otherwise.
279 if (count(array_filter($this->filters)) === 0) {
280 $this->whereHas('objectclass');
281 }
282
283 return $this->grammar->compile($this);
284 }
285
286 /**
287 * Returns the unescaped query.
288 *
289 * @return string
290 */
291 public function getUnescapedQuery()
292 {
293 return Utilities::unescape($this->getQuery());
294 }
295
296 /**
297 * Returns the current Grammar instance.
298 *
299 * @return Grammar
300 */
301 public function getGrammar()
302 {
303 return $this->grammar;
304 }
305
306 /**
307 * Returns the current Cache instance.
308 *
309 * @return Cache|null
310 */
311 public function getCache()
312 {
313 return $this->cache;
314 }
315
316 /**
317 * Returns the current Connection instance.
318 *
319 * @return Connection
320 */
321 public function getConnection()
322 {
323 return $this->connection;
324 }
325
326 /**
327 * Returns the query type.
328 *
329 * @return string
330 */
331 public function getType()
332 {
333 return $this->type;
334 }
335
336 /**
337 * Set the base distinguished name of the query.
338 *
339 * @param Model|string $dn
340 *
341 * @return $this
342 */
343 public function setBaseDn($dn)
344 {
345 $this->baseDn = $this->substituteBaseInDn($dn);
346
347 return $this;
348 }
349
350 /**
351 * Get the base distinguished name of the query.
352 *
353 * @return string|null
354 */
355 public function getBaseDn()
356 {
357 return $this->baseDn;
358 }
359
360 /**
361 * Get the distinguished name of the query.
362 *
363 * @return string
364 */
365 public function getDn()
366 {
367 return $this->dn;
368 }
369
370 /**
371 * Set the distinguished name for the query.
372 *
373 * @param string|Model|null $dn
374 *
375 * @return $this
376 */
377 public function setDn($dn = null)
378 {
379 $this->dn = $this->substituteBaseInDn($dn);
380
381 return $this;
382 }
383
384 /**
385 * Substitute the base DN string template for the current base.
386 *
387 * @param Model|string $dn
388 *
389 * @return string
390 */
391 protected function substituteBaseInDn($dn)
392 {
393 return str_replace(
394 '{base}',
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100395 $this->baseDn ?: '',
396 (string) ($dn instanceof Model ? $dn->getDn() : $dn)
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200397 );
398 }
399
400 /**
401 * Alias for setting the distinguished name for the query.
402 *
403 * @param string|Model|null $dn
404 *
405 * @return $this
406 */
407 public function in($dn = null)
408 {
409 return $this->setDn($dn);
410 }
411
412 /**
413 * Set the size limit of the current query.
414 *
415 * @param int $limit
416 *
417 * @return $this
418 */
419 public function limit($limit = 0)
420 {
421 $this->limit = $limit;
422
423 return $this;
424 }
425
426 /**
427 * Returns a new query for the given model.
428 *
429 * @param Model $model
430 *
431 * @return ModelBuilder
432 */
433 public function model(Model $model)
434 {
435 return $model->newQueryBuilder($this->connection)
436 ->setCache($this->connection->getCache())
437 ->setBaseDn($this->baseDn)
438 ->setModel($model);
439 }
440
441 /**
442 * Performs the specified query on the current LDAP connection.
443 *
444 * @param string $query
445 *
446 * @return Collection|array
447 */
448 public function query($query)
449 {
450 $start = microtime(true);
451
452 // Here we will create the execution callback. This allows us
453 // to only execute an LDAP request if caching is disabled
454 // or if no cache of the given query exists yet.
455 $callback = function () use ($query) {
456 return $this->parse($this->run($query));
457 };
458
459 $results = $this->getCachedResponse($query, $callback);
460
461 $this->logQuery($this, $this->type, $this->getElapsedTime($start));
462
463 return $this->process($results);
464 }
465
466 /**
467 * Paginates the current LDAP query.
468 *
469 * @param int $pageSize
470 * @param bool $isCritical
471 *
472 * @return Collection|array
473 */
474 public function paginate($pageSize = 1000, $isCritical = false)
475 {
476 $this->paginated = true;
477
478 $start = microtime(true);
479
480 $query = $this->getQuery();
481
482 // Here we will create the pagination callback. This allows us
483 // to only execute an LDAP request if caching is disabled
484 // or if no cache of the given query exists yet.
485 $callback = function () use ($query, $pageSize, $isCritical) {
486 return $this->runPaginate($query, $pageSize, $isCritical);
487 };
488
489 $pages = $this->getCachedResponse($query, $callback);
490
491 $this->logQuery($this, 'paginate', $this->getElapsedTime($start));
492
493 return $this->process($pages);
494 }
495
496 /**
497 * Runs the paginate operation with the given filter.
498 *
499 * @param string $filter
500 * @param int $perPage
501 * @param bool $isCritical
502 *
503 * @return array
504 */
505 protected function runPaginate($filter, $perPage, $isCritical)
506 {
507 return $this->connection->run(function (LdapInterface $ldap) use ($filter, $perPage, $isCritical) {
508 return (new Paginator($this, $filter, $perPage, $isCritical))->execute($ldap);
509 });
510 }
511
512 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100513 * Execute a callback over each item while chunking.
514 *
515 * @param Closure $callback
516 * @param int $count
517 *
518 * @return bool
519 */
520 public function each(Closure $callback, $count = 1000)
521 {
522 return $this->chunk($count, function ($results) use ($callback) {
523 foreach ($results as $key => $value) {
524 if ($callback($value, $key) === false) {
525 return false;
526 }
527 }
528 });
529 }
530
531 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200532 * Chunk the results of a paginated LDAP query.
533 *
534 * @param int $pageSize
535 * @param Closure $callback
536 * @param bool $isCritical
537 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100538 * @return bool
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200539 */
540 public function chunk($pageSize, Closure $callback, $isCritical = false)
541 {
542 $start = microtime(true);
543
544 $query = $this->getQuery();
545
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100546 $page = 1;
547
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200548 foreach ($this->runChunk($query, $pageSize, $isCritical) as $chunk) {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100549 if ($callback($this->process($chunk), $page) === false) {
550 return false;
551 }
552
553 $page++;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200554 }
555
556 $this->logQuery($this, 'chunk', $this->getElapsedTime($start));
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100557
558 return true;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200559 }
560
561 /**
562 * Runs the chunk operation with the given filter.
563 *
564 * @param string $filter
565 * @param int $perPage
566 * @param bool $isCritical
567 *
568 * @return array
569 */
570 protected function runChunk($filter, $perPage, $isCritical)
571 {
572 return $this->connection->run(function (LdapInterface $ldap) use ($filter, $perPage, $isCritical) {
573 return (new LazyPaginator($this, $filter, $perPage, $isCritical))->execute($ldap);
574 });
575 }
576
577 /**
578 * Processes and converts the given LDAP results into models.
579 *
580 * @param array $results
581 *
582 * @return array
583 */
584 protected function process(array $results)
585 {
586 unset($results['count']);
587
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100588 if ($this->paginated) {
589 return $this->flattenPages($results);
590 }
591
592 return $results;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200593 }
594
595 /**
596 * Flattens LDAP paged results into a single array.
597 *
598 * @param array $pages
599 *
600 * @return array
601 */
602 protected function flattenPages(array $pages)
603 {
604 $records = [];
605
606 foreach ($pages as $page) {
607 unset($page['count']);
608
609 $records = array_merge($records, $page);
610 }
611
612 return $records;
613 }
614
615 /**
616 * Get the cached response or execute and cache the callback value.
617 *
618 * @param string $query
619 * @param Closure $callback
620 *
621 * @return mixed
622 */
623 protected function getCachedResponse($query, Closure $callback)
624 {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100625 if ($this->cache && $this->caching) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200626 $key = $this->getCacheKey($query);
627
628 if ($this->flushCache) {
629 $this->cache->delete($key);
630 }
631
632 return $this->cache->remember($key, $this->cacheUntil, $callback);
633 }
634
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200635 return $callback();
636 }
637
638 /**
639 * Runs the query operation with the given filter.
640 *
641 * @param string $filter
642 *
643 * @return resource
644 */
645 public function run($filter)
646 {
647 return $this->connection->run(function (LdapInterface $ldap) use ($filter) {
648 // We will avoid setting the controls during any pagination
649 // requests as it will clear the cookie we need to send
650 // to the server upon retrieving every page.
651 if (! $this->paginated) {
652 // Before running the query, we will set the LDAP server controls. This
653 // allows the controls to be automatically reset upon each new query
654 // that is conducted on the same connection during each request.
655 $ldap->setOption(LDAP_OPT_SERVER_CONTROLS, $this->controls);
656 }
657
658 return $ldap->{$this->type}(
659 $this->dn ?? $this->baseDn,
660 $filter,
661 $this->getSelects(),
662 $onlyAttributes = false,
663 $this->limit
664 );
665 });
666 }
667
668 /**
669 * Parses the given LDAP resource by retrieving its entries.
670 *
671 * @param resource $resource
672 *
673 * @return array
674 */
675 public function parse($resource)
676 {
677 if (! $resource) {
678 return [];
679 }
680
681 return $this->connection->run(function (LdapInterface $ldap) use ($resource) {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100682 $this->controlsResponse = $this->controls;
683
684 $errorCode = 0;
685 $dn = $errorMessage = $refs = null;
686
687 // Process the server controls response.
688 $ldap->parseResult(
689 $resource,
690 $errorCode,
691 $dn,
692 $errorMessage,
693 $refs,
694 $this->controlsResponse
695 );
696
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200697 $entries = $ldap->getEntries($resource);
698
699 // Free up memory.
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100700 if (is_resource($resource) || $resource instanceof Result) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200701 $ldap->freeResult($resource);
702 }
703
704 return $entries;
705 });
706 }
707
708 /**
709 * Returns the cache key.
710 *
711 * @param string $query
712 *
713 * @return string
714 */
715 protected function getCacheKey($query)
716 {
717 $host = $this->connection->getLdapConnection()->getHost();
718
719 $key = $host
720 .$this->type
721 .$this->getDn()
722 .$query
723 .implode($this->getSelects())
724 .$this->limit
725 .$this->paginated;
726
727 return md5($key);
728 }
729
730 /**
731 * Returns the first entry in a search result.
732 *
733 * @param array|string $columns
734 *
735 * @return Model|null
736 */
737 public function first($columns = ['*'])
738 {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100739 return Arr::first(
740 $this->limit(1)->get($columns)
741 );
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200742 }
743
744 /**
745 * Returns the first entry in a search result.
746 *
747 * If no entry is found, an exception is thrown.
748 *
749 * @param array|string $columns
750 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100751 * @return Model|array
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200752 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100753 * @throws ObjectNotFoundException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200754 */
755 public function firstOrFail($columns = ['*'])
756 {
757 if (! $record = $this->first($columns)) {
758 $this->throwNotFoundException($this->getUnescapedQuery(), $this->dn);
759 }
760
761 return $record;
762 }
763
764 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100765 * Return the first entry in a result, or execute the callback.
766 *
767 * @param Closure $callback
768 *
769 * @return Model|mixed
770 */
771 public function firstOr(Closure $callback)
772 {
773 return $this->first() ?: $callback();
774 }
775
776 /**
777 * Execute the query and get the first result if it's the sole matching record.
778 *
779 * @param array|string $columns
780 *
781 * @return Model|array
782 *
783 * @throws ObjectsNotFoundException
784 * @throws MultipleObjectsFoundException
785 */
786 public function sole($columns = ['*'])
787 {
788 $result = $this->limit(2)->get($columns);
789
790 if (empty($result)) {
791 throw new ObjectsNotFoundException;
792 }
793
794 if (count($result) > 1) {
795 throw new MultipleObjectsFoundException;
796 }
797
798 return reset($result);
799 }
800
801 /**
802 * Determine if any results exist for the current query.
803 *
804 * @return bool
805 */
806 public function exists()
807 {
808 return ! is_null($this->first());
809 }
810
811 /**
812 * Determine if no results exist for the current query.
813 *
814 * @return bool
815 */
816 public function doesntExist()
817 {
818 return ! $this->exists();
819 }
820
821 /**
822 * Execute the given callback if no rows exist for the current query.
823 *
824 * @param Closure $callback
825 *
826 * @return bool|mixed
827 */
828 public function existsOr(Closure $callback)
829 {
830 return $this->exists() ? true : $callback();
831 }
832
833 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200834 * Throws a not found exception.
835 *
836 * @param string $query
837 * @param string $dn
838 *
839 * @throws ObjectNotFoundException
840 */
841 protected function throwNotFoundException($query, $dn)
842 {
843 throw ObjectNotFoundException::forQuery($query, $dn);
844 }
845
846 /**
847 * Finds a record by the specified attribute and value.
848 *
849 * @param string $attribute
850 * @param string $value
851 * @param array|string $columns
852 *
853 * @return Model|static|null
854 */
855 public function findBy($attribute, $value, $columns = ['*'])
856 {
857 try {
858 return $this->findByOrFail($attribute, $value, $columns);
859 } catch (ObjectNotFoundException $e) {
860 return;
861 }
862 }
863
864 /**
865 * Finds a record by the specified attribute and value.
866 *
867 * If no record is found an exception is thrown.
868 *
869 * @param string $attribute
870 * @param string $value
871 * @param array|string $columns
872 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200873 * @return Model
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100874 *
875 * @throws ObjectNotFoundException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200876 */
877 public function findByOrFail($attribute, $value, $columns = ['*'])
878 {
879 return $this->whereEquals($attribute, $value)->firstOrFail($columns);
880 }
881
882 /**
883 * Find many records by distinguished name.
884 *
885 * @param array $dns
886 * @param array $columns
887 *
888 * @return array|Collection
889 */
890 public function findMany($dns, $columns = ['*'])
891 {
892 if (empty($dns)) {
893 return $this->process([]);
894 }
895
896 $objects = [];
897
898 foreach ($dns as $dn) {
899 if (! is_null($object = $this->find($dn, $columns))) {
900 $objects[] = $object;
901 }
902 }
903
904 return $this->process($objects);
905 }
906
907 /**
908 * Finds many records by the specified attribute.
909 *
910 * @param string $attribute
911 * @param array $values
912 * @param array $columns
913 *
914 * @return Collection
915 */
916 public function findManyBy($attribute, array $values = [], $columns = ['*'])
917 {
918 $query = $this->select($columns);
919
920 foreach ($values as $value) {
921 $query->orWhere([$attribute => $value]);
922 }
923
924 return $query->get();
925 }
926
927 /**
928 * Finds a record by its distinguished name.
929 *
930 * @param string|array $dn
931 * @param array|string $columns
932 *
933 * @return Model|static|array|Collection|null
934 */
935 public function find($dn, $columns = ['*'])
936 {
937 if (is_array($dn)) {
938 return $this->findMany($dn, $columns);
939 }
940
941 try {
942 return $this->findOrFail($dn, $columns);
943 } catch (ObjectNotFoundException $e) {
944 return;
945 }
946 }
947
948 /**
949 * Finds a record by its distinguished name.
950 *
951 * Fails upon no records returned.
952 *
953 * @param string $dn
954 * @param array|string $columns
955 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200956 * @return Model|static
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100957 *
958 * @throws ObjectNotFoundException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200959 */
960 public function findOrFail($dn, $columns = ['*'])
961 {
962 return $this->setDn($dn)
963 ->read()
964 ->whereHas('objectclass')
965 ->firstOrFail($columns);
966 }
967
968 /**
969 * Adds the inserted fields to query on the current LDAP connection.
970 *
971 * @param array|string $columns
972 *
973 * @return $this
974 */
975 public function select($columns = ['*'])
976 {
977 $columns = is_array($columns) ? $columns : func_get_args();
978
979 if (! empty($columns)) {
980 $this->columns = $columns;
981 }
982
983 return $this;
984 }
985
986 /**
987 * Add a new select column to the query.
988 *
989 * @param array|mixed $column
990 *
991 * @return $this
992 */
993 public function addSelect($column)
994 {
995 $column = is_array($column) ? $column : func_get_args();
996
997 $this->columns = array_merge((array) $this->columns, $column);
998
999 return $this;
1000 }
1001
1002 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001003 * Add an order by control to the query.
1004 *
1005 * @param string $attribute
1006 * @param string $direction
1007 *
1008 * @return $this
1009 */
1010 public function orderBy($attribute, $direction = 'asc')
1011 {
1012 return $this->addControl(LDAP_CONTROL_SORTREQUEST, true, [
1013 ['attr' => $attribute, 'reverse' => $direction === 'desc'],
1014 ]);
1015 }
1016
1017 /**
1018 * Add an order by descending control to the query.
1019 *
1020 * @param string $attribute
1021 *
1022 * @return $this
1023 */
1024 public function orderByDesc($attribute)
1025 {
1026 return $this->orderBy($attribute, 'desc');
1027 }
1028
1029 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001030 * Adds a raw filter to the current query.
1031 *
1032 * @param array|string $filters
1033 *
1034 * @return $this
1035 */
1036 public function rawFilter($filters = [])
1037 {
1038 $filters = is_array($filters) ? $filters : func_get_args();
1039
1040 foreach ($filters as $filter) {
1041 $this->filters['raw'][] = $filter;
1042 }
1043
1044 return $this;
1045 }
1046
1047 /**
1048 * Adds a nested 'and' filter to the current query.
1049 *
1050 * @param Closure $closure
1051 *
1052 * @return $this
1053 */
1054 public function andFilter(Closure $closure)
1055 {
1056 $query = $this->newNestedInstance($closure);
1057
1058 return $this->rawFilter(
1059 $this->grammar->compileAnd($query->getQuery())
1060 );
1061 }
1062
1063 /**
1064 * Adds a nested 'or' filter to the current query.
1065 *
1066 * @param Closure $closure
1067 *
1068 * @return $this
1069 */
1070 public function orFilter(Closure $closure)
1071 {
1072 $query = $this->newNestedInstance($closure);
1073
1074 return $this->rawFilter(
1075 $this->grammar->compileOr($query->getQuery())
1076 );
1077 }
1078
1079 /**
1080 * Adds a nested 'not' filter to the current query.
1081 *
1082 * @param Closure $closure
1083 *
1084 * @return $this
1085 */
1086 public function notFilter(Closure $closure)
1087 {
1088 $query = $this->newNestedInstance($closure);
1089
1090 return $this->rawFilter(
1091 $this->grammar->compileNot($query->getQuery())
1092 );
1093 }
1094
1095 /**
1096 * Adds a where clause to the current query.
1097 *
1098 * @param string|array $field
1099 * @param string $operator
1100 * @param string $value
1101 * @param string $boolean
1102 * @param bool $raw
1103 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001104 * @return $this
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001105 *
1106 * @throws InvalidArgumentException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001107 */
1108 public function where($field, $operator = null, $value = null, $boolean = 'and', $raw = false)
1109 {
1110 if (is_array($field)) {
1111 // If the field is an array, we will assume we have been
1112 // provided with an array of key-value pairs and can
1113 // add them each as their own seperate where clause.
1114 return $this->addArrayOfWheres($field, $boolean, $raw);
1115 }
1116
1117 // If we have been provided with two arguments not a "has" or
1118 // "not has" operator, we'll assume the developer is creating
1119 // an "equals" clause and set the proper operator in place.
1120 if (func_num_args() === 2 && ! in_array($operator, ['*', '!*'])) {
1121 [$value, $operator] = [$operator, '='];
1122 }
1123
1124 if (! in_array($operator, $this->grammar->getOperators())) {
1125 throw new InvalidArgumentException("Invalid LDAP filter operator [$operator]");
1126 }
1127
1128 // We'll escape the value if raw isn't requested.
1129 $value = $this->prepareWhereValue($field, $value, $raw);
1130
1131 $field = $this->escape($field)->both()->get();
1132
1133 $this->addFilter($boolean, compact('field', 'operator', 'value'));
1134
1135 return $this;
1136 }
1137
1138 /**
1139 * Prepare the value for being queried.
1140 *
1141 * @param string $field
1142 * @param string $value
1143 * @param bool $raw
1144 *
1145 * @return string
1146 */
1147 protected function prepareWhereValue($field, $value, $raw = false)
1148 {
1149 return $raw ? $value : $this->escape($value);
1150 }
1151
1152 /**
1153 * Adds a raw where clause to the current query.
1154 *
1155 * Values given to this method are not escaped.
1156 *
1157 * @param string|array $field
1158 * @param string $operator
1159 * @param string $value
1160 *
1161 * @return $this
1162 */
1163 public function whereRaw($field, $operator = null, $value = null)
1164 {
1165 return $this->where($field, $operator, $value, 'and', true);
1166 }
1167
1168 /**
1169 * Adds a 'where equals' clause to the current query.
1170 *
1171 * @param string $field
1172 * @param string $value
1173 *
1174 * @return $this
1175 */
1176 public function whereEquals($field, $value)
1177 {
1178 return $this->where($field, '=', $value);
1179 }
1180
1181 /**
1182 * Adds a 'where not equals' clause to the current query.
1183 *
1184 * @param string $field
1185 * @param string $value
1186 *
1187 * @return $this
1188 */
1189 public function whereNotEquals($field, $value)
1190 {
1191 return $this->where($field, '!', $value);
1192 }
1193
1194 /**
1195 * Adds a 'where approximately equals' clause to the current query.
1196 *
1197 * @param string $field
1198 * @param string $value
1199 *
1200 * @return $this
1201 */
1202 public function whereApproximatelyEquals($field, $value)
1203 {
1204 return $this->where($field, '~=', $value);
1205 }
1206
1207 /**
1208 * Adds a 'where has' clause to the current query.
1209 *
1210 * @param string $field
1211 *
1212 * @return $this
1213 */
1214 public function whereHas($field)
1215 {
1216 return $this->where($field, '*');
1217 }
1218
1219 /**
1220 * Adds a 'where not has' clause to the current query.
1221 *
1222 * @param string $field
1223 *
1224 * @return $this
1225 */
1226 public function whereNotHas($field)
1227 {
1228 return $this->where($field, '!*');
1229 }
1230
1231 /**
1232 * Adds a 'where contains' clause to the current query.
1233 *
1234 * @param string $field
1235 * @param string $value
1236 *
1237 * @return $this
1238 */
1239 public function whereContains($field, $value)
1240 {
1241 return $this->where($field, 'contains', $value);
1242 }
1243
1244 /**
1245 * Adds a 'where contains' clause to the current query.
1246 *
1247 * @param string $field
1248 * @param string $value
1249 *
1250 * @return $this
1251 */
1252 public function whereNotContains($field, $value)
1253 {
1254 return $this->where($field, 'not_contains', $value);
1255 }
1256
1257 /**
1258 * Query for entries that match any of the values provided for the given field.
1259 *
1260 * @param string $field
1261 * @param array $values
1262 *
1263 * @return $this
1264 */
1265 public function whereIn($field, array $values)
1266 {
1267 return $this->orFilter(function (self $query) use ($field, $values) {
1268 foreach ($values as $value) {
1269 $query->whereEquals($field, $value);
1270 }
1271 });
1272 }
1273
1274 /**
1275 * Adds a 'between' clause to the current query.
1276 *
1277 * @param string $field
1278 * @param array $values
1279 *
1280 * @return $this
1281 */
1282 public function whereBetween($field, array $values)
1283 {
1284 return $this->where([
1285 [$field, '>=', $values[0]],
1286 [$field, '<=', $values[1]],
1287 ]);
1288 }
1289
1290 /**
1291 * Adds a 'where starts with' clause to the current query.
1292 *
1293 * @param string $field
1294 * @param string $value
1295 *
1296 * @return $this
1297 */
1298 public function whereStartsWith($field, $value)
1299 {
1300 return $this->where($field, 'starts_with', $value);
1301 }
1302
1303 /**
1304 * Adds a 'where *not* starts with' clause to the current query.
1305 *
1306 * @param string $field
1307 * @param string $value
1308 *
1309 * @return $this
1310 */
1311 public function whereNotStartsWith($field, $value)
1312 {
1313 return $this->where($field, 'not_starts_with', $value);
1314 }
1315
1316 /**
1317 * Adds a 'where ends with' clause to the current query.
1318 *
1319 * @param string $field
1320 * @param string $value
1321 *
1322 * @return $this
1323 */
1324 public function whereEndsWith($field, $value)
1325 {
1326 return $this->where($field, 'ends_with', $value);
1327 }
1328
1329 /**
1330 * Adds a 'where *not* ends with' clause to the current query.
1331 *
1332 * @param string $field
1333 * @param string $value
1334 *
1335 * @return $this
1336 */
1337 public function whereNotEndsWith($field, $value)
1338 {
1339 return $this->where($field, 'not_ends_with', $value);
1340 }
1341
1342 /**
1343 * Only include deleted models in the results.
1344 *
1345 * @return $this
1346 */
1347 public function whereDeleted()
1348 {
1349 return $this->withDeleted()->whereEquals('isDeleted', 'TRUE');
1350 }
1351
1352 /**
1353 * Set the LDAP control option to include deleted LDAP models.
1354 *
1355 * @return $this
1356 */
1357 public function withDeleted()
1358 {
1359 return $this->addControl(LdapInterface::OID_SERVER_SHOW_DELETED, $isCritical = true);
1360 }
1361
1362 /**
1363 * Add a server control to the query.
1364 *
1365 * @param string $oid
1366 * @param bool $isCritical
1367 * @param mixed $value
1368 *
1369 * @return $this
1370 */
1371 public function addControl($oid, $isCritical = false, $value = null)
1372 {
1373 $this->controls[$oid] = compact('oid', 'isCritical', 'value');
1374
1375 return $this;
1376 }
1377
1378 /**
1379 * Determine if the server control exists on the query.
1380 *
1381 * @param string $oid
1382 *
1383 * @return bool
1384 */
1385 public function hasControl($oid)
1386 {
1387 return array_key_exists($oid, $this->controls);
1388 }
1389
1390 /**
1391 * Adds an 'or where' clause to the current query.
1392 *
1393 * @param array|string $field
1394 * @param string|null $operator
1395 * @param string|null $value
1396 *
1397 * @return $this
1398 */
1399 public function orWhere($field, $operator = null, $value = null)
1400 {
1401 return $this->where($field, $operator, $value, 'or');
1402 }
1403
1404 /**
1405 * Adds a raw or where clause to the current query.
1406 *
1407 * Values given to this method are not escaped.
1408 *
1409 * @param string $field
1410 * @param string $operator
1411 * @param string $value
1412 *
1413 * @return $this
1414 */
1415 public function orWhereRaw($field, $operator = null, $value = null)
1416 {
1417 return $this->where($field, $operator, $value, 'or', true);
1418 }
1419
1420 /**
1421 * Adds an 'or where has' clause to the current query.
1422 *
1423 * @param string $field
1424 *
1425 * @return $this
1426 */
1427 public function orWhereHas($field)
1428 {
1429 return $this->orWhere($field, '*');
1430 }
1431
1432 /**
1433 * Adds a 'where not has' clause to the current query.
1434 *
1435 * @param string $field
1436 *
1437 * @return $this
1438 */
1439 public function orWhereNotHas($field)
1440 {
1441 return $this->orWhere($field, '!*');
1442 }
1443
1444 /**
1445 * Adds an 'or where equals' clause to the current query.
1446 *
1447 * @param string $field
1448 * @param string $value
1449 *
1450 * @return $this
1451 */
1452 public function orWhereEquals($field, $value)
1453 {
1454 return $this->orWhere($field, '=', $value);
1455 }
1456
1457 /**
1458 * Adds an 'or where not equals' clause to the current query.
1459 *
1460 * @param string $field
1461 * @param string $value
1462 *
1463 * @return $this
1464 */
1465 public function orWhereNotEquals($field, $value)
1466 {
1467 return $this->orWhere($field, '!', $value);
1468 }
1469
1470 /**
1471 * Adds a 'or where approximately equals' clause to the current query.
1472 *
1473 * @param string $field
1474 * @param string $value
1475 *
1476 * @return $this
1477 */
1478 public function orWhereApproximatelyEquals($field, $value)
1479 {
1480 return $this->orWhere($field, '~=', $value);
1481 }
1482
1483 /**
1484 * Adds an 'or where contains' clause to the current query.
1485 *
1486 * @param string $field
1487 * @param string $value
1488 *
1489 * @return $this
1490 */
1491 public function orWhereContains($field, $value)
1492 {
1493 return $this->orWhere($field, 'contains', $value);
1494 }
1495
1496 /**
1497 * Adds an 'or where *not* contains' clause to the current query.
1498 *
1499 * @param string $field
1500 * @param string $value
1501 *
1502 * @return $this
1503 */
1504 public function orWhereNotContains($field, $value)
1505 {
1506 return $this->orWhere($field, 'not_contains', $value);
1507 }
1508
1509 /**
1510 * Adds an 'or where starts with' clause to the current query.
1511 *
1512 * @param string $field
1513 * @param string $value
1514 *
1515 * @return $this
1516 */
1517 public function orWhereStartsWith($field, $value)
1518 {
1519 return $this->orWhere($field, 'starts_with', $value);
1520 }
1521
1522 /**
1523 * Adds an 'or where *not* starts with' clause to the current query.
1524 *
1525 * @param string $field
1526 * @param string $value
1527 *
1528 * @return $this
1529 */
1530 public function orWhereNotStartsWith($field, $value)
1531 {
1532 return $this->orWhere($field, 'not_starts_with', $value);
1533 }
1534
1535 /**
1536 * Adds an 'or where ends with' clause to the current query.
1537 *
1538 * @param string $field
1539 * @param string $value
1540 *
1541 * @return $this
1542 */
1543 public function orWhereEndsWith($field, $value)
1544 {
1545 return $this->orWhere($field, 'ends_with', $value);
1546 }
1547
1548 /**
1549 * Adds an 'or where *not* ends with' clause to the current query.
1550 *
1551 * @param string $field
1552 * @param string $value
1553 *
1554 * @return $this
1555 */
1556 public function orWhereNotEndsWith($field, $value)
1557 {
1558 return $this->orWhere($field, 'not_ends_with', $value);
1559 }
1560
1561 /**
1562 * Adds a filter binding onto the current query.
1563 *
1564 * @param string $type The type of filter to add.
1565 * @param array $bindings The bindings of the filter.
1566 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001567 * @return $this
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001568 *
1569 * @throws InvalidArgumentException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001570 */
1571 public function addFilter($type, array $bindings)
1572 {
1573 if (! array_key_exists($type, $this->filters)) {
1574 throw new InvalidArgumentException("Filter type: [$type] is invalid.");
1575 }
1576
1577 // Each filter clause require key bindings to be set. We
1578 // will validate this here to ensure all of them have
1579 // been provided, or throw an exception otherwise.
1580 if ($missing = $this->missingBindingKeys($bindings)) {
1581 $keys = implode(', ', $missing);
1582
1583 throw new InvalidArgumentException("Invalid filter bindings. Missing: [$keys] keys.");
1584 }
1585
1586 $this->filters[$type][] = $bindings;
1587
1588 return $this;
1589 }
1590
1591 /**
1592 * Extract any missing required binding keys.
1593 *
1594 * @param array $bindings
1595 *
1596 * @return array
1597 */
1598 protected function missingBindingKeys($bindings)
1599 {
1600 $required = array_flip(['field', 'operator', 'value']);
1601
1602 $existing = array_intersect_key($required, $bindings);
1603
1604 return array_keys(array_diff_key($required, $existing));
1605 }
1606
1607 /**
1608 * Get all the filters on the query.
1609 *
1610 * @return array
1611 */
1612 public function getFilters()
1613 {
1614 return $this->filters;
1615 }
1616
1617 /**
1618 * Clear the query filters.
1619 *
1620 * @return $this
1621 */
1622 public function clearFilters()
1623 {
1624 foreach (array_keys($this->filters) as $type) {
1625 $this->filters[$type] = [];
1626 }
1627
1628 return $this;
1629 }
1630
1631 /**
1632 * Determine if the query has attributes selected.
1633 *
1634 * @return bool
1635 */
1636 public function hasSelects()
1637 {
1638 return count($this->columns) > 0;
1639 }
1640
1641 /**
1642 * Get the attributes to select on the search.
1643 *
1644 * @return array
1645 */
1646 public function getSelects()
1647 {
1648 $selects = $this->columns ?? ['*'];
1649
1650 if (in_array('*', $selects)) {
1651 return $selects;
1652 }
1653
1654 if (in_array('objectclass', $selects)) {
1655 return $selects;
1656 }
1657
1658 // If the * character is not provided in the selected columns,
1659 // we need to ensure we always select the object class, as
1660 // this is used for constructing models properly.
1661 $selects[] = 'objectclass';
1662
1663 return $selects;
1664 }
1665
1666 /**
1667 * Set the query to search on the base distinguished name.
1668 *
1669 * This will result in one record being returned.
1670 *
1671 * @return $this
1672 */
1673 public function read()
1674 {
1675 $this->type = 'read';
1676
1677 return $this;
1678 }
1679
1680 /**
1681 * Set the query to search one level on the base distinguished name.
1682 *
1683 * @return $this
1684 */
1685 public function listing()
1686 {
1687 $this->type = 'listing';
1688
1689 return $this;
1690 }
1691
1692 /**
1693 * Set the query to search the entire directory on the base distinguished name.
1694 *
1695 * @return $this
1696 */
1697 public function recursive()
1698 {
1699 $this->type = 'search';
1700
1701 return $this;
1702 }
1703
1704 /**
1705 * Whether to mark the current query as nested.
1706 *
1707 * @param bool $nested
1708 *
1709 * @return $this
1710 */
1711 public function nested($nested = true)
1712 {
1713 $this->nested = (bool) $nested;
1714
1715 return $this;
1716 }
1717
1718 /**
1719 * Enables caching on the current query until the given date.
1720 *
1721 * If flushing is enabled, the query cache will be flushed and then re-cached.
1722 *
1723 * @param DateTimeInterface $until When to expire the query cache.
1724 * @param bool $flush Whether to force-flush the query cache.
1725 *
1726 * @return $this
1727 */
1728 public function cache(DateTimeInterface $until = null, $flush = false)
1729 {
1730 $this->caching = true;
1731 $this->cacheUntil = $until;
1732 $this->flushCache = $flush;
1733
1734 return $this;
1735 }
1736
1737 /**
1738 * Determine if the query is nested.
1739 *
1740 * @return bool
1741 */
1742 public function isNested()
1743 {
1744 return $this->nested === true;
1745 }
1746
1747 /**
1748 * Determine whether the query is paginated.
1749 *
1750 * @return bool
1751 */
1752 public function isPaginated()
1753 {
1754 return $this->paginated;
1755 }
1756
1757 /**
1758 * Insert an entry into the directory.
1759 *
1760 * @param string $dn
1761 * @param array $attributes
1762 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001763 * @return bool
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001764 *
1765 * @throws LdapRecordException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001766 */
1767 public function insert($dn, array $attributes)
1768 {
1769 if (empty($dn)) {
1770 throw new LdapRecordException('A new LDAP object must have a distinguished name (dn).');
1771 }
1772
1773 if (! array_key_exists('objectclass', $attributes)) {
1774 throw new LdapRecordException(
1775 'A new LDAP object must contain at least one object class (objectclass) to be created.'
1776 );
1777 }
1778
1779 return $this->connection->run(function (LdapInterface $ldap) use ($dn, $attributes) {
1780 return $ldap->add($dn, $attributes);
1781 });
1782 }
1783
1784 /**
1785 * Create attributes on the entry in the directory.
1786 *
1787 * @param string $dn
1788 * @param array $attributes
1789 *
1790 * @return bool
1791 */
1792 public function insertAttributes($dn, array $attributes)
1793 {
1794 return $this->connection->run(function (LdapInterface $ldap) use ($dn, $attributes) {
1795 return $ldap->modAdd($dn, $attributes);
1796 });
1797 }
1798
1799 /**
1800 * Update the entry with the given modifications.
1801 *
1802 * @param string $dn
1803 * @param array $modifications
1804 *
1805 * @return bool
1806 */
1807 public function update($dn, array $modifications)
1808 {
1809 return $this->connection->run(function (LdapInterface $ldap) use ($dn, $modifications) {
1810 return $ldap->modifyBatch($dn, $modifications);
1811 });
1812 }
1813
1814 /**
1815 * Update an entries attribute in the directory.
1816 *
1817 * @param string $dn
1818 * @param array $attributes
1819 *
1820 * @return bool
1821 */
1822 public function updateAttributes($dn, array $attributes)
1823 {
1824 return $this->connection->run(function (LdapInterface $ldap) use ($dn, $attributes) {
1825 return $ldap->modReplace($dn, $attributes);
1826 });
1827 }
1828
1829 /**
1830 * Delete an entry from the directory.
1831 *
1832 * @param string $dn
1833 *
1834 * @return bool
1835 */
1836 public function delete($dn)
1837 {
1838 return $this->connection->run(function (LdapInterface $ldap) use ($dn) {
1839 return $ldap->delete($dn);
1840 });
1841 }
1842
1843 /**
1844 * Delete attributes on the entry in the directory.
1845 *
1846 * @param string $dn
1847 * @param array $attributes
1848 *
1849 * @return bool
1850 */
1851 public function deleteAttributes($dn, array $attributes)
1852 {
1853 return $this->connection->run(function (LdapInterface $ldap) use ($dn, $attributes) {
1854 return $ldap->modDelete($dn, $attributes);
1855 });
1856 }
1857
1858 /**
1859 * Rename an entry in the directory.
1860 *
1861 * @param string $dn
1862 * @param string $rdn
1863 * @param string $newParentDn
1864 * @param bool $deleteOldRdn
1865 *
1866 * @return bool
1867 */
1868 public function rename($dn, $rdn, $newParentDn, $deleteOldRdn = true)
1869 {
1870 return $this->connection->run(function (LdapInterface $ldap) use ($dn, $rdn, $newParentDn, $deleteOldRdn) {
1871 return $ldap->rename($dn, $rdn, $newParentDn, $deleteOldRdn);
1872 });
1873 }
1874
1875 /**
1876 * Handle dynamic method calls on the query builder.
1877 *
1878 * @param string $method
1879 * @param array $parameters
1880 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001881 * @return mixed
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001882 *
1883 * @throws BadMethodCallException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001884 */
1885 public function __call($method, $parameters)
1886 {
1887 // If the beginning of the method being called contains
1888 // 'where', we will assume a dynamic 'where' clause is
1889 // being performed and pass the parameters to it.
1890 if (substr($method, 0, 5) === 'where') {
1891 return $this->dynamicWhere($method, $parameters);
1892 }
1893
1894 throw new BadMethodCallException(sprintf(
1895 'Call to undefined method %s::%s()',
1896 static::class,
1897 $method
1898 ));
1899 }
1900
1901 /**
1902 * Handles dynamic "where" clauses to the query.
1903 *
1904 * @param string $method
1905 * @param array $parameters
1906 *
1907 * @return $this
1908 */
1909 public function dynamicWhere($method, $parameters)
1910 {
1911 $finder = substr($method, 5);
1912
1913 $segments = preg_split('/(And|Or)(?=[A-Z])/', $finder, -1, PREG_SPLIT_DELIM_CAPTURE);
1914
1915 // The connector variable will determine which connector will be used for the
1916 // query condition. We will change it as we come across new boolean values
1917 // in the dynamic method strings, which could contain a number of these.
1918 $connector = 'and';
1919
1920 $index = 0;
1921
1922 foreach ($segments as $segment) {
1923 // If the segment is not a boolean connector, we can assume it is a column's name
1924 // and we will add it to the query as a new constraint as a where clause, then
1925 // we can keep iterating through the dynamic method string's segments again.
1926 if ($segment != 'And' && $segment != 'Or') {
1927 $this->addDynamic($segment, $connector, $parameters, $index);
1928
1929 $index++;
1930 }
1931
1932 // Otherwise, we will store the connector so we know how the next where clause we
1933 // find in the query should be connected to the previous ones, meaning we will
1934 // have the proper boolean connector to connect the next where clause found.
1935 else {
1936 $connector = $segment;
1937 }
1938 }
1939
1940 return $this;
1941 }
1942
1943 /**
1944 * Adds an array of wheres to the current query.
1945 *
1946 * @param array $wheres
1947 * @param string $boolean
1948 * @param bool $raw
1949 *
1950 * @return $this
1951 */
1952 protected function addArrayOfWheres($wheres, $boolean, $raw)
1953 {
1954 foreach ($wheres as $key => $value) {
1955 if (is_numeric($key) && is_array($value)) {
1956 // If the key is numeric and the value is an array, we'll
1957 // assume we've been given an array with conditionals.
1958 [$field, $condition] = $value;
1959
1960 // Since a value is optional for some conditionals, we will
1961 // try and retrieve the third parameter from the array,
1962 // but is entirely optional.
1963 $value = Arr::get($value, 2);
1964
1965 $this->where($field, $condition, $value, $boolean);
1966 } else {
1967 // If the value is not an array, we will assume an equals clause.
1968 $this->where($key, '=', $value, $boolean, $raw);
1969 }
1970 }
1971
1972 return $this;
1973 }
1974
1975 /**
1976 * Add a single dynamic where clause statement to the query.
1977 *
1978 * @param string $segment
1979 * @param string $connector
1980 * @param array $parameters
1981 * @param int $index
1982 *
1983 * @return void
1984 */
1985 protected function addDynamic($segment, $connector, $parameters, $index)
1986 {
1987 // If no parameters were given to the dynamic where clause,
1988 // we can assume a "has" attribute filter is being added.
1989 if (count($parameters) === 0) {
1990 $this->where(strtolower($segment), '*', null, strtolower($connector));
1991 } else {
1992 $this->where(strtolower($segment), '=', $parameters[$index], strtolower($connector));
1993 }
1994 }
1995
1996 /**
1997 * Logs the given executed query information by firing its query event.
1998 *
1999 * @param Builder $query
2000 * @param string $type
2001 * @param null|float $time
2002 *
2003 * @return void
2004 */
2005 protected function logQuery($query, $type, $time = null)
2006 {
2007 $args = [$query, $time];
2008
2009 switch ($type) {
2010 case 'listing':
2011 $event = new Events\Listing(...$args);
2012 break;
2013 case 'read':
2014 $event = new Events\Read(...$args);
2015 break;
2016 case 'chunk':
2017 $event = new Events\Chunk(...$args);
2018 break;
2019 case 'paginate':
2020 $event = new Events\Paginate(...$args);
2021 break;
2022 default:
2023 $event = new Events\Search(...$args);
2024 break;
2025 }
2026
2027 $this->fireQueryEvent($event);
2028 }
2029
2030 /**
2031 * Fires the given query event.
2032 *
2033 * @param QueryExecuted $event
2034 *
2035 * @return void
2036 */
2037 protected function fireQueryEvent(QueryExecuted $event)
2038 {
2039 Container::getInstance()->getEventDispatcher()->fire($event);
2040 }
2041
2042 /**
2043 * Get the elapsed time since a given starting point.
2044 *
2045 * @param int $start
2046 *
2047 * @return float
2048 */
2049 protected function getElapsedTime($start)
2050 {
2051 return round((microtime(true) - $start) * 1000, 2);
2052 }
2053}