|
|
@@ -3,12 +3,14 @@
|
|
|
namespace Adldap\Query;
|
|
|
|
|
|
use Closure;
|
|
|
-use InvalidArgumentException;
|
|
|
-use Illuminate\Support\Arr;
|
|
|
+use Adldap\Adldap;
|
|
|
use Adldap\Utilities;
|
|
|
use Adldap\Models\Model;
|
|
|
-use Adldap\Schemas\SchemaInterface;
|
|
|
+use Illuminate\Support\Arr;
|
|
|
+use InvalidArgumentException;
|
|
|
use Adldap\Schemas\ActiveDirectory;
|
|
|
+use Adldap\Schemas\SchemaInterface;
|
|
|
+use Adldap\Query\Events\QueryExecuted;
|
|
|
use Adldap\Models\ModelNotFoundException;
|
|
|
use Adldap\Connections\ConnectionInterface;
|
|
|
|
|
|
@@ -28,7 +30,7 @@ class Builder
|
|
|
*/
|
|
|
public $filters = [
|
|
|
'and' => [],
|
|
|
- 'or' => [],
|
|
|
+ 'or' => [],
|
|
|
'raw' => [],
|
|
|
];
|
|
|
|
|
|
@@ -95,6 +97,27 @@ class Builder
|
|
|
*/
|
|
|
protected $nested = false;
|
|
|
|
|
|
+ /**
|
|
|
+ * Determines whether the query should be cached.
|
|
|
+ *
|
|
|
+ * @var bool
|
|
|
+ */
|
|
|
+ protected $caching = false;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * How long the query should be cached until.
|
|
|
+ *
|
|
|
+ * @var \DateTimeInterface|null
|
|
|
+ */
|
|
|
+ protected $cacheUntil = null;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Determines whether the query cache must be flushed.
|
|
|
+ *
|
|
|
+ * @var bool
|
|
|
+ */
|
|
|
+ protected $flushCache = false;
|
|
|
+
|
|
|
/**
|
|
|
* The current connection instance.
|
|
|
*
|
|
|
@@ -116,6 +139,13 @@ class Builder
|
|
|
*/
|
|
|
protected $schema;
|
|
|
|
|
|
+ /**
|
|
|
+ * The current cache instance.
|
|
|
+ *
|
|
|
+ * @var Cache|null
|
|
|
+ */
|
|
|
+ protected $cache;
|
|
|
+
|
|
|
/**
|
|
|
* Constructor.
|
|
|
*
|
|
|
@@ -182,6 +212,18 @@ class Builder
|
|
|
return $this->schema;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Sets the cache to store query results.
|
|
|
+ *
|
|
|
+ * @param Cache|null $cache
|
|
|
+ */
|
|
|
+ public function setCache(Cache $cache = null)
|
|
|
+ {
|
|
|
+ $this->cache = $cache;
|
|
|
+
|
|
|
+ return $this;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Returns a new Query Builder instance.
|
|
|
*
|
|
|
@@ -220,7 +262,7 @@ class Builder
|
|
|
/**
|
|
|
* Returns the current query.
|
|
|
*
|
|
|
- * @return \Illuminate\Support\Collection|array
|
|
|
+ * @return Collection|array
|
|
|
*/
|
|
|
public function get()
|
|
|
{
|
|
|
@@ -236,6 +278,12 @@ class Builder
|
|
|
*/
|
|
|
public function getQuery()
|
|
|
{
|
|
|
+ // We need to ensure we have at least one filter, as
|
|
|
+ // no query results will be returned otherwise.
|
|
|
+ if (count(array_filter($this->filters)) === 0) {
|
|
|
+ $this->whereHas($this->schema->objectClass());
|
|
|
+ }
|
|
|
+
|
|
|
return $this->grammar->compile($this);
|
|
|
}
|
|
|
|
|
|
@@ -324,18 +372,32 @@ class Builder
|
|
|
*
|
|
|
* @param string $query
|
|
|
*
|
|
|
- * @return \Illuminate\Support\Collection|array
|
|
|
+ * @return \Adldap\Query\Collection|array
|
|
|
*/
|
|
|
public function query($query)
|
|
|
{
|
|
|
- $results = $this->connection->{$this->type}(
|
|
|
- $this->getDn(),
|
|
|
- $query,
|
|
|
- $this->getSelects(),
|
|
|
- $onlyAttributes = false,
|
|
|
- $this->limit
|
|
|
- );
|
|
|
+ $start = microtime(true);
|
|
|
+
|
|
|
+ // Here we will create the execution callback. This allows us
|
|
|
+ // to only execute an LDAP request if caching is disabled
|
|
|
+ // or if no cache of the given query exists yet.
|
|
|
+ $callback = function () use ($query) {
|
|
|
+ return $this->parse($this->run($query));
|
|
|
+ };
|
|
|
+
|
|
|
+ // If caching is enabled and we have a cache instance available,
|
|
|
+ // we will try to retrieve the cached results instead.
|
|
|
+ // Otherwise, we will simply execute the callback.
|
|
|
+ if ($this->caching && $this->cache) {
|
|
|
+ $results = $this->getCachedResponse($this->getCacheKey($query), $callback);
|
|
|
+ } else {
|
|
|
+ $results = $callback();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Log the query.
|
|
|
+ $this->logQuery($this, $this->type, $this->getElapsedTime($start));
|
|
|
|
|
|
+ // Process & return the results.
|
|
|
return $this->newProcessor()->process($results);
|
|
|
}
|
|
|
|
|
|
@@ -348,10 +410,82 @@ class Builder
|
|
|
*
|
|
|
* @return Paginator
|
|
|
*/
|
|
|
- public function paginate($perPage = 50, $currentPage = 0, $isCritical = true)
|
|
|
+ public function paginate($perPage = 1000, $currentPage = 0, $isCritical = true)
|
|
|
{
|
|
|
$this->paginated = true;
|
|
|
|
|
|
+ $start = microtime(true);
|
|
|
+
|
|
|
+ $query = $this->getQuery();
|
|
|
+
|
|
|
+ // Here we will create the pagination callback. This allows us
|
|
|
+ // to only execute an LDAP request if caching is disabled
|
|
|
+ // or if no cache of the given query exists yet.
|
|
|
+ $callback = function () use ($query, $perPage, $isCritical) {
|
|
|
+ return $this->runPaginate($query, $perPage, $isCritical);
|
|
|
+ };
|
|
|
+
|
|
|
+ // If caching is enabled and we have a cache instance available,
|
|
|
+ // we will try to retrieve the cached results instead.
|
|
|
+ if ($this->caching && $this->cache) {
|
|
|
+ $pages = $this->getCachedResponse($this->getCacheKey($query), $callback);
|
|
|
+ } else {
|
|
|
+ $pages = $callback();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Log the query.
|
|
|
+ $this->logQuery($this, 'paginate', $this->getElapsedTime($start));
|
|
|
+
|
|
|
+ // Process & return the results.
|
|
|
+ return $this->newProcessor()->processPaginated($pages, $perPage, $currentPage);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get the cached response or execute and cache the callback value.
|
|
|
+ *
|
|
|
+ * @param string $key
|
|
|
+ * @param Closure $callback
|
|
|
+ *
|
|
|
+ * @return mixed
|
|
|
+ */
|
|
|
+ protected function getCachedResponse($key, Closure $callback)
|
|
|
+ {
|
|
|
+ if ($this->flushCache) {
|
|
|
+ $this->cache->delete($key);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $this->cache->remember($key, $this->cacheUntil, $callback);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Runs the query operation with the given filter.
|
|
|
+ *
|
|
|
+ * @param string $filter
|
|
|
+ *
|
|
|
+ * @return resource
|
|
|
+ */
|
|
|
+ protected function run($filter)
|
|
|
+ {
|
|
|
+ return $this->connection->{$this->type}(
|
|
|
+ $this->getDn(),
|
|
|
+ $filter,
|
|
|
+ $this->getSelects(),
|
|
|
+ $onlyAttributes = false,
|
|
|
+ $this->limit
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Runs the paginate operation with the given filter.
|
|
|
+ *
|
|
|
+ * @param string $filter
|
|
|
+ * @param int $perPage
|
|
|
+ * @param bool $isCritical
|
|
|
+ *
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ protected function runPaginate($filter, $perPage, $isCritical)
|
|
|
+ {
|
|
|
$pages = [];
|
|
|
|
|
|
$cookie = '';
|
|
|
@@ -360,25 +494,65 @@ class Builder
|
|
|
$this->connection->controlPagedResult($perPage, $isCritical, $cookie);
|
|
|
|
|
|
// Run the search.
|
|
|
- $resource = @$this->connection->search($this->getDn(), $this->getQuery(), $this->getSelects());
|
|
|
+ $resource = $this->run($filter);
|
|
|
|
|
|
if ($resource) {
|
|
|
+ // If we have been given a valid resource, we will retrieve the next
|
|
|
+ // pagination cookie to send for our next pagination request.
|
|
|
$this->connection->controlPagedResultResponse($resource, $cookie);
|
|
|
|
|
|
- // We'll collect each resource result into the pages array.
|
|
|
- $pages[] = $resource;
|
|
|
+ $pages[] = $this->parse($resource);
|
|
|
}
|
|
|
} while (!empty($cookie));
|
|
|
|
|
|
- $paginator = $this->newProcessor()->processPaginated($pages, $perPage, $currentPage);
|
|
|
-
|
|
|
// Reset paged result on the current connection. We won't pass in the current $perPage
|
|
|
// parameter since we want to reset the page size to the default '1000'. Sending '0'
|
|
|
// eliminates any further opportunity for running queries in the same request,
|
|
|
// even though that is supposed to be the correct usage.
|
|
|
$this->connection->controlPagedResult();
|
|
|
|
|
|
- return $paginator;
|
|
|
+ return $pages;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Parses the given LDAP resource by retrieving its entries.
|
|
|
+ *
|
|
|
+ * @param resource $resource
|
|
|
+ *
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ protected function parse($resource)
|
|
|
+ {
|
|
|
+ // Normalize entries. Get entries returns false on failure.
|
|
|
+ // We'll always want an array in this situation.
|
|
|
+ $entries = $this->connection->getEntries($resource) ?: [];
|
|
|
+
|
|
|
+ // Free up memory.
|
|
|
+ if (is_resource($resource)) {
|
|
|
+ $this->connection->freeResult($resource);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $entries;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns the cache key.
|
|
|
+ *
|
|
|
+ * @param string $query
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ protected function getCacheKey($query)
|
|
|
+ {
|
|
|
+ $key = $this->connection->getHost()
|
|
|
+ .$this->type
|
|
|
+ .$this->getDn()
|
|
|
+ .$query
|
|
|
+ .implode('', $this->getSelects())
|
|
|
+ .$this->limit
|
|
|
+ .$this->paginated;
|
|
|
+
|
|
|
+ return md5($key);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -435,7 +609,7 @@ class Builder
|
|
|
try {
|
|
|
return $this->findByOrFail($attribute, $value, $columns);
|
|
|
} catch (ModelNotFoundException $e) {
|
|
|
- return false;
|
|
|
+ return;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -472,7 +646,7 @@ class Builder
|
|
|
}
|
|
|
|
|
|
// If we're not using ActiveDirectory, we can't use ANR. We'll make our own query.
|
|
|
- if (! is_a($this->schema, ActiveDirectory::class)) {
|
|
|
+ if (!is_a($this->schema, ActiveDirectory::class)) {
|
|
|
return $this->prepareAnrEquivalentQuery($value)->first($columns);
|
|
|
}
|
|
|
|
|
|
@@ -485,13 +659,13 @@ class Builder
|
|
|
* @param array $values
|
|
|
* @param array $columns
|
|
|
*
|
|
|
- * @return \Illuminate\Support\Collection|array
|
|
|
+ * @return \Adldap\Query\Collection|array
|
|
|
*/
|
|
|
public function findMany(array $values = [], $columns = [])
|
|
|
{
|
|
|
$this->select($columns);
|
|
|
|
|
|
- if (! is_a($this->schema, ActiveDirectory::class)) {
|
|
|
+ if (!is_a($this->schema, ActiveDirectory::class)) {
|
|
|
$query = $this;
|
|
|
|
|
|
foreach ($values as $value) {
|
|
|
@@ -513,7 +687,7 @@ class Builder
|
|
|
*/
|
|
|
protected function prepareAnrEquivalentQuery($value)
|
|
|
{
|
|
|
- return $this->orFilter(function (Builder $query) use ($value) {
|
|
|
+ return $this->orFilter(function (self $query) use ($value) {
|
|
|
$locateBy = [
|
|
|
$this->schema->name(),
|
|
|
$this->schema->email(),
|
|
|
@@ -537,7 +711,7 @@ class Builder
|
|
|
* @param array $values
|
|
|
* @param array $columns
|
|
|
*
|
|
|
- * @return \Illuminate\Support\Collection|array
|
|
|
+ * @return \Adldap\Query\Collection|array
|
|
|
*/
|
|
|
public function findManyBy($attribute, array $values = [], $columns = [])
|
|
|
{
|
|
|
@@ -589,7 +763,7 @@ class Builder
|
|
|
try {
|
|
|
return $this->findByDnOrFail($dn, $columns);
|
|
|
} catch (ModelNotFoundException $e) {
|
|
|
- return false;
|
|
|
+ return;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -639,7 +813,7 @@ class Builder
|
|
|
try {
|
|
|
return $this->findByGuidOrFail($guid, $columns);
|
|
|
} catch (ModelNotFoundException $e) {
|
|
|
- return false;
|
|
|
+ return;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -662,7 +836,7 @@ class Builder
|
|
|
}
|
|
|
|
|
|
return $this->select($columns)->whereRaw([
|
|
|
- $this->schema->objectGuid() => $guid
|
|
|
+ $this->schema->objectGuid() => $guid,
|
|
|
])->firstOrFail();
|
|
|
}
|
|
|
|
|
|
@@ -679,7 +853,7 @@ class Builder
|
|
|
try {
|
|
|
return $this->findBySidOrFail($sid, $columns);
|
|
|
} catch (ModelNotFoundException $e) {
|
|
|
- return false;
|
|
|
+ return;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -849,7 +1023,7 @@ class Builder
|
|
|
|
|
|
$field = $this->escape($field, $ignore = null, 3);
|
|
|
|
|
|
- $this->filters[$boolean][] = compact('field', 'operator', 'value');
|
|
|
+ $this->addFilter($boolean, compact('field', 'operator', 'value'));
|
|
|
|
|
|
return $this;
|
|
|
}
|
|
|
@@ -959,6 +1133,23 @@ class Builder
|
|
|
return $this->where($field, Operator::$notContains, $value);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Query for entries that match any of the values provided for the given field.
|
|
|
+ *
|
|
|
+ * @param string $field
|
|
|
+ * @param array $values
|
|
|
+ *
|
|
|
+ * @return Builder
|
|
|
+ */
|
|
|
+ public function whereIn($field, array $values)
|
|
|
+ {
|
|
|
+ return $this->orFilter(function (self $query) use ($field, $values) {
|
|
|
+ foreach ($values as $value) {
|
|
|
+ $query->whereEquals($field, $value);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Adds a 'between' clause to the current query.
|
|
|
*
|
|
|
@@ -1242,6 +1433,53 @@ class Builder
|
|
|
return $this->orWhereEquals($this->schema->memberOfRecursive(), $dn);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Adds a filter onto the current query.
|
|
|
+ *
|
|
|
+ * @param string $type The type of filter to add.
|
|
|
+ * @param array $bindings The bindings of the filter.
|
|
|
+ *
|
|
|
+ * @throws InvalidArgumentException
|
|
|
+ *
|
|
|
+ * @return $this
|
|
|
+ */
|
|
|
+ public function addFilter($type, array $bindings)
|
|
|
+ {
|
|
|
+ // Here we will ensure we have been given a proper filter type.
|
|
|
+ if (!array_key_exists($type, $this->filters)) {
|
|
|
+ throw new InvalidArgumentException("Invalid filter type: {$type}.");
|
|
|
+ }
|
|
|
+
|
|
|
+ // The required filter key bindings.
|
|
|
+ $required = ['field', 'operator', 'value'];
|
|
|
+
|
|
|
+ // Here we will ensure the proper key bindings are given.
|
|
|
+ if (count(array_intersect_key(array_flip($required), $bindings)) !== count($required)) {
|
|
|
+ // Retrieve the keys that are missing in the bindings array.
|
|
|
+ $missing = implode(', ', array_diff($required, array_flip($bindings)));
|
|
|
+
|
|
|
+ throw new InvalidArgumentException("Invalid filter bindings. Missing: {$missing} keys.");
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->filters[$type][] = $bindings;
|
|
|
+
|
|
|
+ return $this;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Clear the query builders filters.
|
|
|
+ *
|
|
|
+ * @return $this
|
|
|
+ */
|
|
|
+ public function clearFilters()
|
|
|
+ {
|
|
|
+ foreach ($this->filters as $type => $filters) {
|
|
|
+ $this->filters[$type] = [];
|
|
|
+ }
|
|
|
+
|
|
|
+ return $this;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Returns true / false depending if the current object
|
|
|
* contains selects.
|
|
|
@@ -1340,8 +1578,7 @@ class Builder
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Sets the recursive property to tell the search whether or
|
|
|
- * not to return the LDAP results in their raw format.
|
|
|
+ * Whether to return the LDAP results in their raw format.
|
|
|
*
|
|
|
* @param bool $raw
|
|
|
*
|
|
|
@@ -1355,8 +1592,7 @@ class Builder
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Sets the nested property to tell the Grammar instance whether
|
|
|
- * or not the current query is already nested.
|
|
|
+ * Whether the current query is nested.
|
|
|
*
|
|
|
* @param bool $nested
|
|
|
*
|
|
|
@@ -1370,13 +1606,22 @@ class Builder
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Returns true / false if the current query is nested.
|
|
|
+ * Enables caching on the current query until the given date.
|
|
|
*
|
|
|
- * @return bool
|
|
|
+ * If flushing is enabled, the query cache will be flushed and then re-cached.
|
|
|
+ *
|
|
|
+ * @param \DateTimeInterface $until When to expire the query cache.
|
|
|
+ * @param bool $flush Whether to force-flush the query cache.
|
|
|
+ *
|
|
|
+ * @return $this
|
|
|
*/
|
|
|
- public function isNested()
|
|
|
+ public function cache(\DateTimeInterface $until = null, $flush = false)
|
|
|
{
|
|
|
- return $this->nested === true;
|
|
|
+ $this->caching = true;
|
|
|
+ $this->cacheUntil = $until;
|
|
|
+ $this->flushCache = $flush;
|
|
|
+
|
|
|
+ return $this;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -1423,6 +1668,16 @@ class Builder
|
|
|
return $this->sortByFlags;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Returns true / false if the current query is nested.
|
|
|
+ *
|
|
|
+ * @return bool
|
|
|
+ */
|
|
|
+ public function isNested()
|
|
|
+ {
|
|
|
+ return $this->nested === true;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Returns bool that determines whether the current
|
|
|
* query builder will return raw results.
|
|
|
@@ -1568,6 +1823,57 @@ class Builder
|
|
|
$this->where($field, '=', $parameters[$index], $bool);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Logs the given executed query information by firing its query event.
|
|
|
+ *
|
|
|
+ * @param Builder $query
|
|
|
+ * @param string $type
|
|
|
+ * @param null|float $time
|
|
|
+ */
|
|
|
+ protected function logQuery($query, $type, $time = null)
|
|
|
+ {
|
|
|
+ $args = [$query, $time];
|
|
|
+
|
|
|
+ switch ($type) {
|
|
|
+ case 'listing':
|
|
|
+ $event = new Events\Listing(...$args);
|
|
|
+ break;
|
|
|
+ case 'read':
|
|
|
+ $event = new Events\Read(...$args);
|
|
|
+ break;
|
|
|
+ case 'paginate':
|
|
|
+ $event = new Events\Paginate(...$args);
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ $event = new Events\Search(...$args);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->fireQueryEvent($event);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Fires the given query event.
|
|
|
+ *
|
|
|
+ * @param QueryExecuted $event
|
|
|
+ */
|
|
|
+ protected function fireQueryEvent(QueryExecuted $event)
|
|
|
+ {
|
|
|
+ Adldap::getEventDispatcher()->fire($event);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get the elapsed time since a given starting point.
|
|
|
+ *
|
|
|
+ * @param int $start
|
|
|
+ *
|
|
|
+ * @return float
|
|
|
+ */
|
|
|
+ protected function getElapsedTime($start)
|
|
|
+ {
|
|
|
+ return round((microtime(true) - $start) * 1000, 2);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Returns a new query Processor instance.
|
|
|
*
|