3 namespace Adldap\Query
;
6 use Illuminate\Support\Arr
;
7 use Illuminate\Support\Str
;
8 use InvalidArgumentException
;
10 use Adldap\Models\Model
;
11 use Adldap\Schemas\SchemaInterface
;
12 use Adldap\Schemas\ActiveDirectory
;
13 use Adldap\Models\ModelNotFoundException
;
14 use Adldap\Connections\ConnectionInterface
;
19 * The selected columns to retrieve on the query.
23 public $columns = ['*'];
37 * The size limit of the query.
44 * Determines whether the current query is paginated.
48 public $paginated = false;
51 * The field to sort search results by.
55 protected $sortByField = '';
58 * The direction to sort the results by.
62 protected $sortByDirection = '';
65 * The sort flags for sorting query results.
69 protected $sortByFlags;
72 * The distinguished name to perform searches upon.
79 * The default query type.
83 protected $type = 'search';
86 * Determines whether or not to return LDAP results in their raw array format.
90 protected $raw = false;
93 * Determines whether the query is nested.
97 protected $nested = false;
100 * The current connection instance.
102 * @var ConnectionInterface
104 protected $connection;
107 * The current grammar instance.
114 * The current schema instance.
116 * @var SchemaInterface
123 * @param ConnectionInterface $connection
124 * @param Grammar|null $grammar
125 * @param SchemaInterface|null $schema
127 public function __construct(ConnectionInterface
$connection, Grammar
$grammar = null, SchemaInterface
$schema = null)
129 $this->setConnection($connection)
130 ->setGrammar($grammar)
131 ->setSchema($schema);
135 * Sets the current connection.
137 * @param ConnectionInterface $connection
141 public function setConnection(ConnectionInterface
$connection)
143 $this->connection
= $connection;
149 * Sets the current filter grammar.
151 * @param Grammar|null $grammar
155 public function setGrammar(Grammar
$grammar = null)
157 $this->grammar
= $grammar ?
: new Grammar();
163 * Sets the current schema.
165 * @param SchemaInterface|null $schema
169 public function setSchema(SchemaInterface
$schema = null)
171 $this->schema
= $schema ?
: new ActiveDirectory();
177 * Returns the current schema.
179 * @return SchemaInterface
181 public function getSchema()
183 return $this->schema
;
187 * Returns a new Query Builder instance.
189 * @param string $baseDn
193 public function newInstance($baseDn = null)
195 // We'll set the base DN of the new Builder so
196 // developers don't need to do this manually.
197 $dn = is_null($baseDn) ?
$this->getDn() : $baseDn;
199 return (new static($this->connection
, $this->grammar
, $this->schema
))
204 * Returns a new nested Query Builder instance.
206 * @param Closure|null $closure
210 public function newNestedInstance(Closure
$closure = null)
212 $query = $this->newInstance()->nested();
215 call_user_func($closure, $query);
222 * Returns the current query.
224 * @return \Illuminate\Support\Collection|array
226 public function get()
228 // We'll mute any warnings / errors here. We just need to
229 // know if any query results were returned.
230 return @$this->query($this->getQuery());
234 * Compiles and returns the current query string.
238 public function getQuery()
240 return $this->grammar
->compile($this);
244 * Returns the unescaped query.
248 public function getUnescapedQuery()
250 return Utilities
::unescape($this->getQuery());
254 * Returns the current Grammar instance.
258 public function getGrammar()
260 return $this->grammar
;
264 * Returns the current Connection instance.
266 * @return ConnectionInterface
268 public function getConnection()
270 return $this->connection
;
274 * Returns the builders DN to perform searches upon.
278 public function getDn()
284 * Sets the DN to perform searches upon.
286 * @param string|null $dn
290 public function setDn($dn = null)
292 $this->dn
= (string) $dn;
298 * Alias for setting the base DN of the query.
304 public function in($dn)
306 return $this->setDn($dn);
310 * Sets the size limit of the current query.
316 public function limit($limit = 0)
318 $this->limit
= $limit;
324 * Performs the specified query on the current LDAP connection.
326 * @param string $query
328 * @return \Illuminate\Support\Collection|array
330 public function query($query)
332 $results = $this->connection
->{$this->type
}(
336 $onlyAttributes = false,
340 return $this->newProcessor()->process($results);
344 * Paginates the current LDAP query.
346 * @param int $perPage
347 * @param int $currentPage
348 * @param bool $isCritical
352 public function paginate($perPage = 50, $currentPage = 0, $isCritical = true)
354 $this->paginated
= true;
361 $this->connection
->controlPagedResult($perPage, $isCritical, $cookie);
364 $resource = @$this->connection
->search($this->getDn(), $this->getQuery(), $this->getSelects());
367 $this->connection
->controlPagedResultResponse($resource, $cookie);
369 // We'll collect each resource result into the pages array.
370 $pages[] = $resource;
372 } while (!empty($cookie));
374 $paginator = $this->newProcessor()->processPaginated($pages, $perPage, $currentPage);
376 // Reset paged result on the current connection. We won't pass in the current $perPage
377 // parameter since we want to reset the page size to the default '1000'. Sending '0'
378 // eliminates any further opportunity for running queries in the same request,
379 // even though that is supposed to be the correct usage.
380 $this->connection
->controlPagedResult();
386 * Returns the first entry in a search result.
388 * @param array|string $columns
390 * @return Model|array|null
392 public function first($columns = [])
394 $results = $this->select($columns)->limit(1)->get();
396 return Arr
::get($results, 0);
400 * Returns the first entry in a search result.
402 * If no entry is found, an exception is thrown.
404 * @param array|string $columns
406 * @throws ModelNotFoundException
410 public function firstOrFail($columns = [])
412 $record = $this->first($columns);
415 throw (new ModelNotFoundException())
416 ->setQuery($this->getUnescapedQuery(), $this->getDn());
423 * Finds a record by the specified attribute and value.
425 * @param string $attribute
426 * @param string $value
427 * @param array|string $columns
431 public function findBy($attribute, $value, $columns = [])
433 return $this->whereEquals($attribute, $value)->first($columns);
437 * Finds a record by the specified attribute and value.
439 * If no record is found an exception is thrown.
441 * @param string $attribute
442 * @param string $value
443 * @param array|string $columns
445 * @throws ModelNotFoundException
449 public function findByOrFail($attribute, $value, $columns = [])
451 return $this->whereEquals($attribute, $value)->firstOrFail($columns);
455 * Finds a record using ambiguous name resolution.
457 * @param string|array $anr
458 * @param array|string $columns
462 public function find($anr, $columns = [])
464 if (is_array($anr)) {
465 return $this->findMany($anr, $columns);
468 return $this->findBy($this->schema
->anr(), $anr, $columns);
472 * Finds multiple records using ambiguous name resolution.
475 * @param array $columns
479 public function findMany(array $anrs = [], $columns = [])
481 return $this->findManyBy($this->schema
->anr(), $anrs, $columns);
485 * Finds many records by the specified attribute.
487 * @param string $attribute
488 * @param array $values
489 * @param array $columns
493 public function findManyBy($attribute, array $values = [], $columns = [])
495 $query = $this->select($columns);
497 foreach ($values as $value) {
498 $query->orWhere([$attribute => $value]);
501 return $query->get();
505 * Finds a record using ambiguous name resolution.
507 * If a record is not found, an exception is thrown.
510 * @param array|string $columns
512 * @throws ModelNotFoundException
516 public function findOrFail($anr, $columns = [])
518 $entry = $this->find($anr, $columns);
520 // Make sure we check if the result is an entry or an array before
521 // we throw an exception in case the user wants raw results.
522 if (!$entry instanceof Model
&& !is_array($entry)) {
523 throw (new ModelNotFoundException())
524 ->setQuery($this->getUnescapedQuery(), $this->getDn());
531 * Finds a record by its distinguished name.
533 * @param string|array $dn
534 * @param array|string $columns
538 public function findByDn($dn, $columns = [])
543 ->whereHas($this->schema
->objectClass())
548 * Finds a record by its distinguished name.
550 * Fails upon no records returned.
553 * @param array|string $columns
555 * @throws ModelNotFoundException
559 public function findByDnOrFail($dn, $columns = [])
564 ->whereHas($this->schema
->objectClass())
565 ->firstOrFail($columns);
569 * Finds a record by its string GUID.
571 * @param string $guid
572 * @param array|string $columns
576 public function findByGuid($guid, $columns = [])
578 if ($this->schema
->objectGuidRequiresConversion()) {
579 $guid = Utilities
::stringGuidToHex($guid);
582 return $this->select($columns)->whereRaw([
583 $this->schema
->objectGuid() => $guid
588 * Finds a record by its string GUID.
590 * Fails upon no records returned.
592 * @param string $guid
593 * @param array|string $columns
595 * @throws ModelNotFoundException
599 public function findByGuidOrFail($guid, $columns = [])
601 if ($this->schema
->objectGuidRequiresConversion()) {
602 $guid = Utilities
::stringGuidToHex($guid);
605 return $this->select($columns)->whereRaw([
606 $this->schema
->objectGuid() => $guid
611 * Finds a record by its Object SID.
614 * @param array|string $columns
618 public function findBySid($sid, $columns = [])
620 return $this->findBy($this->schema
->objectSid(), $sid, $columns);
624 * Finds a record by its Object SID.
626 * Fails upon no records returned.
629 * @param array|string $columns
631 * @throws ModelNotFoundException
635 public function findBySidOrFail($sid, $columns = [])
637 return $this->findByOrFail($this->schema
->objectSid(), $sid, $columns);
641 * Finds the Base DN of your domain controller.
643 * @return string|bool
645 public function findBaseDn()
647 $result = $this->setDn(null)
650 ->whereHas($this->schema
->objectClass())
653 $key = $this->schema
->defaultNamingContext();
655 if (is_array($result) && array_key_exists($key, $result)) {
656 if (array_key_exists(0, $result[$key])) {
657 return $result[$key][0];
665 * Adds the inserted fields to query on the current LDAP connection.
667 * @param array|string $columns
671 public function select($columns = [])
673 $columns = is_array($columns) ?
$columns : func_get_args();
675 if (!empty($columns)) {
676 $this->columns
= $columns;
683 * Adds a raw filter to the current query.
685 * @param array|string $filters
689 public function rawFilter($filters = [])
691 $filters = is_array($filters) ?
$filters : func_get_args();
693 foreach ($filters as $filter) {
694 $this->filters
['raw'][] = $filter;
701 * Adds a nested 'and' filter to the current query.
703 * @param Closure $closure
707 public function andFilter(Closure
$closure)
709 $query = $this->newNestedInstance($closure);
711 $filter = $this->grammar
->compileAnd($query->getQuery());
713 return $this->rawFilter($filter);
717 * Adds a nested 'or' filter to the current query.
719 * @param Closure $closure
723 public function orFilter(Closure
$closure)
725 $query = $this->newNestedInstance($closure);
727 $filter = $this->grammar
->compileOr($query->getQuery());
729 return $this->rawFilter($filter);
733 * Adds a nested 'not' filter to the current query.
735 * @param Closure $closure
739 public function notFilter(Closure
$closure)
741 $query = $this->newNestedInstance($closure);
743 $filter = $this->grammar
->compileNot($query->getQuery());
745 return $this->rawFilter($filter);
749 * Adds a where clause to the current query.
751 * @param string|array $field
752 * @param string $operator
753 * @param string $value
754 * @param string $boolean
757 * @throws InvalidArgumentException
761 public function where($field, $operator = null, $value = null, $boolean = 'and', $raw = false)
763 if (is_array($field)) {
764 // If the column is an array, we will assume it is an array of
765 // key-value pairs and can add them each as a where clause.
766 return $this->addArrayOfWheres($field, $boolean, $raw);
769 // We'll bypass the 'has' and 'notHas' operator since they
770 // only require two arguments inside the where method.
771 $bypass = [Operator
::$has, Operator
::$notHas];
773 // Here we will make some assumptions about the operator. If only
774 // 2 values are passed to the method, we will assume that
775 // the operator is 'equals' and keep going.
776 if (func_num_args() === 2 && in_array($operator, $bypass) === false) {
777 list($value, $operator) = [$operator, '='];
780 if (!in_array($operator, Operator
::all())) {
781 throw new InvalidArgumentException("Invalid where operator: {$operator}");
784 // We'll escape the value if raw isn't requested.
785 $value = $raw ?
$value : $this->escape($value);
787 $field = $this->escape($field, $ignore = null, 3);
789 $this->filters
[$boolean][] = compact('field', 'operator', 'value');
795 * Adds a raw where clause to the current query.
797 * Values given to this method are not escaped.
799 * @param string|array $field
800 * @param string $operator
801 * @param string $value
805 public function whereRaw($field, $operator = null, $value = null)
807 return $this->where($field, $operator, $value, 'and', true);
811 * Adds a 'where equals' clause to the current query.
813 * @param string $field
814 * @param string $value
818 public function whereEquals($field, $value)
820 return $this->where($field, Operator
::$equals, $value);
824 * Adds a 'where not equals' clause to the current query.
826 * @param string $field
827 * @param string $value
831 public function whereNotEquals($field, $value)
833 return $this->where($field, Operator
::$doesNotEqual, $value);
837 * Adds a 'where approximately equals' clause to the current query.
839 * @param string $field
840 * @param string $value
844 public function whereApproximatelyEquals($field, $value)
846 return $this->where($field, Operator
::$approximatelyEquals, $value);
850 * Adds a 'where has' clause to the current query.
852 * @param string $field
856 public function whereHas($field)
858 return $this->where($field, Operator
::$has);
862 * Adds a 'where not has' clause to the current query.
864 * @param string $field
868 public function whereNotHas($field)
870 return $this->where($field, Operator
::$notHas);
874 * Adds a 'where contains' clause to the current query.
876 * @param string $field
877 * @param string $value
881 public function whereContains($field, $value)
883 return $this->where($field, Operator
::$contains, $value);
887 * Adds a 'where contains' clause to the current query.
889 * @param string $field
890 * @param string $value
894 public function whereNotContains($field, $value)
896 return $this->where($field, Operator
::$notContains, $value);
900 * Adds a 'between' clause to the current query.
902 * @param string $field
903 * @param array $values
907 public function whereBetween($field, array $values)
909 return $this->where([
910 [$field, '>=', $values[0]],
911 [$field, '<=', $values[1]],
916 * Adds a 'where starts with' clause to the current query.
918 * @param string $field
919 * @param string $value
923 public function whereStartsWith($field, $value)
925 return $this->where($field, Operator
::$startsWith, $value);
929 * Adds a 'where *not* starts with' clause to the current query.
931 * @param string $field
932 * @param string $value
936 public function whereNotStartsWith($field, $value)
938 return $this->where($field, Operator
::$notStartsWith, $value);
942 * Adds a 'where ends with' clause to the current query.
944 * @param string $field
945 * @param string $value
949 public function whereEndsWith($field, $value)
951 return $this->where($field, Operator
::$endsWith, $value);
955 * Adds a 'where *not* ends with' clause to the current query.
957 * @param string $field
958 * @param string $value
962 public function whereNotEndsWith($field, $value)
964 return $this->where($field, Operator
::$notEndsWith, $value);
968 * Adds a enabled filter to the current query.
972 public function whereEnabled()
974 return $this->rawFilter($this->schema
->filterEnabled());
978 * Adds a disabled filter to the current query.
982 public function whereDisabled()
984 return $this->rawFilter($this->schema
->filterDisabled());
988 * Adds a 'member of' filter to the current query.
994 public function whereMemberOf($dn)
996 return $this->whereEquals($this->schema
->memberOfRecursive(), $dn);
1000 * Adds an 'or where' clause to the current query.
1002 * @param array|string $field
1003 * @param string|null $operator
1004 * @param string|null $value
1008 public function orWhere($field, $operator = null, $value = null)
1010 return $this->where($field, $operator, $value, 'or');
1014 * Adds a raw or where clause to the current query.
1016 * Values given to this method are not escaped.
1018 * @param string $field
1019 * @param string $operator
1020 * @param string $value
1024 public function orWhereRaw($field, $operator = null, $value = null)
1026 return $this->where($field, $operator, $value, 'or', true);
1030 * Adds an 'or where has' clause to the current query.
1032 * @param string $field
1036 public function orWhereHas($field)
1038 return $this->orWhere($field, Operator
::$has);
1042 * Adds a 'where not has' clause to the current query.
1044 * @param string $field
1048 public function orWhereNotHas($field)
1050 return $this->orWhere($field, Operator
::$notHas);
1054 * Adds an 'or where equals' clause to the current query.
1056 * @param string $field
1057 * @param string $value
1061 public function orWhereEquals($field, $value)
1063 return $this->orWhere($field, Operator
::$equals, $value);
1067 * Adds an 'or where not equals' clause to the current query.
1069 * @param string $field
1070 * @param string $value
1074 public function orWhereNotEquals($field, $value)
1076 return $this->orWhere($field, Operator
::$doesNotEqual, $value);
1080 * Adds a 'or where approximately equals' clause to the current query.
1082 * @param string $field
1083 * @param string $value
1087 public function orWhereApproximatelyEquals($field, $value)
1089 return $this->orWhere($field, Operator
::$approximatelyEquals, $value);
1093 * Adds an 'or where contains' clause to the current query.
1095 * @param string $field
1096 * @param string $value
1100 public function orWhereContains($field, $value)
1102 return $this->orWhere($field, Operator
::$contains, $value);
1106 * Adds an 'or where *not* contains' clause to the current query.
1108 * @param string $field
1109 * @param string $value
1113 public function orWhereNotContains($field, $value)
1115 return $this->orWhere($field, Operator
::$notContains, $value);
1119 * Adds an 'or where starts with' clause to the current query.
1121 * @param string $field
1122 * @param string $value
1126 public function orWhereStartsWith($field, $value)
1128 return $this->orWhere($field, Operator
::$startsWith, $value);
1132 * Adds an 'or where *not* starts with' clause to the current query.
1134 * @param string $field
1135 * @param string $value
1139 public function orWhereNotStartsWith($field, $value)
1141 return $this->orWhere($field, Operator
::$notStartsWith, $value);
1145 * Adds an 'or where ends with' clause to the current query.
1147 * @param string $field
1148 * @param string $value
1152 public function orWhereEndsWith($field, $value)
1154 return $this->orWhere($field, Operator
::$endsWith, $value);
1158 * Adds an 'or where *not* ends with' clause to the current query.
1160 * @param string $field
1161 * @param string $value
1165 public function orWhereNotEndsWith($field, $value)
1167 return $this->orWhere($field, Operator
::$notEndsWith, $value);
1171 * Adds an 'or where member of' filter to the current query.
1177 public function orWhereMemberOf($dn)
1179 return $this->orWhereEquals($this->schema
->memberOfRecursive(), $dn);
1183 * Returns true / false depending if the current object
1188 public function hasSelects()
1190 return count($this->getSelects()) > 0;
1194 * Returns the current selected fields to retrieve.
1198 public function getSelects()
1200 $selects = $this->columns
;
1202 // If the asterisk is not provided in the selected columns, we need to
1203 // ensure we always select the object class and category, as these
1204 // are used for constructing models. The asterisk indicates that
1205 // we want all attributes returned for LDAP records.
1206 if (!in_array('*', $selects)) {
1207 $selects[] = $this->schema
->objectCategory();
1208 $selects[] = $this->schema
->objectClass();
1215 * Sorts the LDAP search results by the specified field and direction.
1217 * @param string $field
1218 * @param string $direction
1219 * @param int|null $flags
1223 public function sortBy($field, $direction = 'asc', $flags = null)
1225 $this->sortByField
= $field;
1227 // Normalize direction.
1228 $direction = strtolower($direction);
1230 if ($direction === 'asc' ||
$direction === 'desc') {
1231 $this->sortByDirection
= $direction;
1234 if (is_null($flags)) {
1235 $this->sortByFlags
= SORT_NATURAL + SORT_FLAG_CASE
;
1242 * Set the query to search on the base distinguished name.
1244 * This will result in one record being returned.
1248 public function read()
1250 $this->type
= 'read';
1256 * Set the query to search one level on the base distinguished name.
1260 public function listing()
1262 $this->type
= 'listing';
1268 * Sets the query to search the entire directory on the base distinguished name.
1272 public function recursive()
1274 $this->type
= 'search';
1280 * Sets the recursive property to tell the search whether or
1281 * not to return the LDAP results in their raw format.
1287 public function raw($raw = true)
1289 $this->raw
= (bool) $raw;
1295 * Sets the nested property to tell the Grammar instance whether
1296 * or not the current query is already nested.
1298 * @param bool $nested
1302 public function nested($nested = true)
1304 $this->nested
= (bool) $nested;
1310 * Returns true / false if the current query is nested.
1314 public function isNested()
1316 return $this->nested
=== true;
1320 * Returns an escaped string for use in an LDAP filter.
1322 * @param string $value
1323 * @param string $ignore
1328 public function escape($value, $ignore = '', $flags = 0)
1330 return Utilities
::escape($value, $ignore, $flags);
1334 * Returns the query builders sort by field.
1338 public function getSortByField()
1340 return $this->sortByField
;
1344 * Returns the query builders sort by direction.
1348 public function getSortByDirection()
1350 return $this->sortByDirection
;
1354 * Returns the query builders sort by flags.
1358 public function getSortByFlags()
1360 return $this->sortByFlags
;
1364 * Returns bool that determines whether the current
1365 * query builder will return raw results.
1369 public function isRaw()
1375 * Returns bool that determines whether the current
1376 * query builder will return paginated results.
1380 public function isPaginated()
1382 return $this->paginated
;
1386 * Returns bool that determines whether the current
1387 * query builder will return sorted results.
1391 public function isSorted()
1393 return $this->sortByField ?
true : false;
1397 * Handle dynamic method calls on the query builder
1398 * object to be directed to the query processor.
1400 * @param string $method
1401 * @param array $parameters
1405 public function __call($method, $parameters)
1407 if (Str
::startsWith($method, 'where')) {
1408 return $this->dynamicWhere($method, $parameters);
1411 return call_user_func_array([$this->newProcessor(), $method], $parameters);
1415 * Handles dynamic "where" clauses to the query.
1417 * @param string $method
1418 * @param string $parameters
1422 public function dynamicWhere($method, $parameters)
1424 $finder = substr($method, 5);
1426 $segments = preg_split('/(And|Or)(?=[A-Z])/', $finder, -1, PREG_SPLIT_DELIM_CAPTURE
);
1428 // The connector variable will determine which connector will be used for the
1429 // query condition. We will change it as we come across new boolean values
1430 // in the dynamic method strings, which could contain a number of these.
1435 foreach ($segments as $segment) {
1436 // If the segment is not a boolean connector, we can assume it is a column's name
1437 // and we will add it to the query as a new constraint as a where clause, then
1438 // we can keep iterating through the dynamic method string's segments again.
1439 if ($segment != 'And' && $segment != 'Or') {
1440 $this->addDynamic($segment, $connector, $parameters, $index);
1445 // Otherwise, we will store the connector so we know how the next where clause we
1446 // find in the query should be connected to the previous ones, meaning we will
1447 // have the proper boolean connector to connect the next where clause found.
1449 $connector = $segment;
1457 * Adds an array of wheres to the current query.
1459 * @param array $wheres
1460 * @param string $boolean
1465 protected function addArrayOfWheres($wheres, $boolean, $raw)
1467 foreach ($wheres as $key => $value) {
1468 if (is_numeric($key) && is_array($value)) {
1469 // If the key is numeric and the value is an array, we'll
1470 // assume we've been given an array with conditionals.
1471 list($field, $condition) = $value;
1473 // Since a value is optional for some conditionals, we will
1474 // try and retrieve the third parameter from the array,
1475 // but is entirely optional.
1476 $value = Arr
::get($value, 2);
1478 $this->where($field, $condition, $value, $boolean);
1480 // If the value is not an array, we will assume an equals clause.
1481 $this->where($key, Operator
::$equals, $value, $boolean, $raw);
1489 * Add a single dynamic where clause statement to the query.
1491 * @param string $segment
1492 * @param string $connector
1493 * @param array $parameters
1498 protected function addDynamic($segment, $connector, $parameters, $index)
1500 // Once we have parsed out the columns and formatted the boolean operators we
1501 // are ready to add it to this query as a where clause just like any other
1502 // clause on the query. Then we'll increment the parameter index values.
1503 $bool = strtolower($connector);
1505 $this->where(Str
::snake($segment), '=', $parameters[$index], $bool);
1509 * Returns a new query Processor instance.
1513 protected function newProcessor()
1515 return new Processor($this);