blob: c75afa2e0d3bdc5b698031184e83da8c10adfcbb [file] [log] [blame]
<?php
namespace LdapRecord\Query;
use BadMethodCallException;
use Closure;
use DateTimeInterface;
use InvalidArgumentException;
use LdapRecord\Connection;
use LdapRecord\Container;
use LdapRecord\EscapesValues;
use LdapRecord\LdapInterface;
use LdapRecord\LdapRecordException;
use LdapRecord\Models\Model;
use LdapRecord\Query\Events\QueryExecuted;
use LdapRecord\Query\Model\Builder as ModelBuilder;
use LdapRecord\Query\Pagination\LazyPaginator;
use LdapRecord\Query\Pagination\Paginator;
use LdapRecord\Support\Arr;
use LdapRecord\Utilities;
class Builder
{
use EscapesValues;
/**
* The selected columns to retrieve on the query.
*
* @var array
*/
public $columns;
/**
* The query filters.
*
* @var array
*/
public $filters = [
'and' => [],
'or' => [],
'raw' => [],
];
/**
* The LDAP server controls to be sent.
*
* @var array
*/
public $controls = [];
/**
* The size limit of the query.
*
* @var int
*/
public $limit = 0;
/**
* Determines whether the current query is paginated.
*
* @var bool
*/
public $paginated = false;
/**
* The distinguished name to perform searches upon.
*
* @var string|null
*/
protected $dn;
/**
* The base distinguished name to perform searches inside.
*
* @var string|null
*/
protected $baseDn;
/**
* The default query type.
*
* @var string
*/
protected $type = 'search';
/**
* Determines whether the query is nested.
*
* @var bool
*/
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.
*
* @var Connection
*/
protected $connection;
/**
* The current grammar instance.
*
* @var Grammar
*/
protected $grammar;
/**
* The current cache instance.
*
* @var Cache|null
*/
protected $cache;
/**
* Constructor.
*
* @param Connection $connection
*/
public function __construct(Connection $connection)
{
$this->connection = $connection;
$this->grammar = new Grammar();
}
/**
* Set the current connection.
*
* @param Connection $connection
*
* @return $this
*/
public function setConnection(Connection $connection)
{
$this->connection = $connection;
return $this;
}
/**
* Set the current filter grammar.
*
* @param Grammar $grammar
*
* @return $this
*/
public function setGrammar(Grammar $grammar)
{
$this->grammar = $grammar;
return $this;
}
/**
* Set the cache to store query results.
*
* @param Cache|null $cache
*
* @return $this
*/
public function setCache(Cache $cache = null)
{
$this->cache = $cache;
return $this;
}
/**
* Returns a new Query Builder instance.
*
* @param string $baseDn
*
* @return $this
*/
public function newInstance($baseDn = null)
{
// We'll set the base DN of the new Builder so
// developers don't need to do this manually.
$dn = is_null($baseDn) ? $this->getDn() : $baseDn;
return (new static($this->connection))->setDn($dn);
}
/**
* Returns a new nested Query Builder instance.
*
* @param Closure|null $closure
*
* @return $this
*/
public function newNestedInstance(Closure $closure = null)
{
$query = $this->newInstance()->nested();
if ($closure) {
$closure($query);
}
return $query;
}
/**
* Executes the LDAP query.
*
* @param string|array $columns
*
* @return Collection|array
*/
public function get($columns = ['*'])
{
return $this->onceWithColumns(Arr::wrap($columns), function () {
return $this->query($this->getQuery());
});
}
/**
* Execute the given callback while selecting the given columns.
*
* After running the callback, the columns are reset to the original value.
*
* @param array $columns
* @param callable $callback
*
* @return mixed
*/
protected function onceWithColumns($columns, $callback)
{
$original = $this->columns;
if (is_null($original)) {
$this->columns = $columns;
}
$result = $callback();
$this->columns = $original;
return $result;
}
/**
* Compiles and returns the current query string.
*
* @return string
*/
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('objectclass');
}
return $this->grammar->compile($this);
}
/**
* Returns the unescaped query.
*
* @return string
*/
public function getUnescapedQuery()
{
return Utilities::unescape($this->getQuery());
}
/**
* Returns the current Grammar instance.
*
* @return Grammar
*/
public function getGrammar()
{
return $this->grammar;
}
/**
* Returns the current Cache instance.
*
* @return Cache|null
*/
public function getCache()
{
return $this->cache;
}
/**
* Returns the current Connection instance.
*
* @return Connection
*/
public function getConnection()
{
return $this->connection;
}
/**
* Returns the query type.
*
* @return string
*/
public function getType()
{
return $this->type;
}
/**
* Set the base distinguished name of the query.
*
* @param Model|string $dn
*
* @return $this
*/
public function setBaseDn($dn)
{
$this->baseDn = $this->substituteBaseInDn($dn);
return $this;
}
/**
* Get the base distinguished name of the query.
*
* @return string|null
*/
public function getBaseDn()
{
return $this->baseDn;
}
/**
* Get the distinguished name of the query.
*
* @return string
*/
public function getDn()
{
return $this->dn;
}
/**
* Set the distinguished name for the query.
*
* @param string|Model|null $dn
*
* @return $this
*/
public function setDn($dn = null)
{
$this->dn = $this->substituteBaseInDn($dn);
return $this;
}
/**
* Substitute the base DN string template for the current base.
*
* @param Model|string $dn
*
* @return string
*/
protected function substituteBaseInDn($dn)
{
return str_replace(
'{base}',
$this->baseDn,
$dn instanceof Model ? $dn->getDn() : $dn
);
}
/**
* Alias for setting the distinguished name for the query.
*
* @param string|Model|null $dn
*
* @return $this
*/
public function in($dn = null)
{
return $this->setDn($dn);
}
/**
* Set the size limit of the current query.
*
* @param int $limit
*
* @return $this
*/
public function limit($limit = 0)
{
$this->limit = $limit;
return $this;
}
/**
* Returns a new query for the given model.
*
* @param Model $model
*
* @return ModelBuilder
*/
public function model(Model $model)
{
return $model->newQueryBuilder($this->connection)
->setCache($this->connection->getCache())
->setBaseDn($this->baseDn)
->setModel($model);
}
/**
* Performs the specified query on the current LDAP connection.
*
* @param string $query
*
* @return Collection|array
*/
public function query($query)
{
$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));
};
$results = $this->getCachedResponse($query, $callback);
$this->logQuery($this, $this->type, $this->getElapsedTime($start));
return $this->process($results);
}
/**
* Paginates the current LDAP query.
*
* @param int $pageSize
* @param bool $isCritical
*
* @return Collection|array
*/
public function paginate($pageSize = 1000, $isCritical = false)
{
$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, $pageSize, $isCritical) {
return $this->runPaginate($query, $pageSize, $isCritical);
};
$pages = $this->getCachedResponse($query, $callback);
$this->logQuery($this, 'paginate', $this->getElapsedTime($start));
return $this->process($pages);
}
/**
* 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)
{
return $this->connection->run(function (LdapInterface $ldap) use ($filter, $perPage, $isCritical) {
return (new Paginator($this, $filter, $perPage, $isCritical))->execute($ldap);
});
}
/**
* Chunk the results of a paginated LDAP query.
*
* @param int $pageSize
* @param Closure $callback
* @param bool $isCritical
*
* @return void
*/
public function chunk($pageSize, Closure $callback, $isCritical = false)
{
$start = microtime(true);
$query = $this->getQuery();
foreach ($this->runChunk($query, $pageSize, $isCritical) as $chunk) {
$callback($this->process($chunk));
}
$this->logQuery($this, 'chunk', $this->getElapsedTime($start));
}
/**
* Runs the chunk operation with the given filter.
*
* @param string $filter
* @param int $perPage
* @param bool $isCritical
*
* @return array
*/
protected function runChunk($filter, $perPage, $isCritical)
{
return $this->connection->run(function (LdapInterface $ldap) use ($filter, $perPage, $isCritical) {
return (new LazyPaginator($this, $filter, $perPage, $isCritical))->execute($ldap);
});
}
/**
* Processes and converts the given LDAP results into models.
*
* @param array $results
*
* @return array
*/
protected function process(array $results)
{
unset($results['count']);
return $this->paginated ? $this->flattenPages($results) : $results;
}
/**
* Flattens LDAP paged results into a single array.
*
* @param array $pages
*
* @return array
*/
protected function flattenPages(array $pages)
{
$records = [];
foreach ($pages as $page) {
unset($page['count']);
$records = array_merge($records, $page);
}
return $records;
}
/**
* Get the cached response or execute and cache the callback value.
*
* @param string $query
* @param Closure $callback
*
* @return mixed
*/
protected function getCachedResponse($query, Closure $callback)
{
// 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) {
$key = $this->getCacheKey($query);
if ($this->flushCache) {
$this->cache->delete($key);
}
return $this->cache->remember($key, $this->cacheUntil, $callback);
}
// Otherwise, we will simply execute the callback.
return $callback();
}
/**
* Runs the query operation with the given filter.
*
* @param string $filter
*
* @return resource
*/
public function run($filter)
{
return $this->connection->run(function (LdapInterface $ldap) use ($filter) {
// We will avoid setting the controls during any pagination
// requests as it will clear the cookie we need to send
// to the server upon retrieving every page.
if (! $this->paginated) {
// Before running the query, we will set the LDAP server controls. This
// allows the controls to be automatically reset upon each new query
// that is conducted on the same connection during each request.
$ldap->setOption(LDAP_OPT_SERVER_CONTROLS, $this->controls);
}
return $ldap->{$this->type}(
$this->dn ?? $this->baseDn,
$filter,
$this->getSelects(),
$onlyAttributes = false,
$this->limit
);
});
}
/**
* Parses the given LDAP resource by retrieving its entries.
*
* @param resource $resource
*
* @return array
*/
public function parse($resource)
{
if (! $resource) {
return [];
}
return $this->connection->run(function (LdapInterface $ldap) use ($resource) {
$entries = $ldap->getEntries($resource);
// Free up memory.
if (is_resource($resource)) {
$ldap->freeResult($resource);
}
return $entries;
});
}
/**
* Returns the cache key.
*
* @param string $query
*
* @return string
*/
protected function getCacheKey($query)
{
$host = $this->connection->getLdapConnection()->getHost();
$key = $host
.$this->type
.$this->getDn()
.$query
.implode($this->getSelects())
.$this->limit
.$this->paginated;
return md5($key);
}
/**
* Returns the first entry in a search result.
*
* @param array|string $columns
*
* @return Model|null
*/
public function first($columns = ['*'])
{
return Arr::get($this->limit(1)->get($columns), 0);
}
/**
* Returns the first entry in a search result.
*
* If no entry is found, an exception is thrown.
*
* @param array|string $columns
*
* @throws ObjectNotFoundException
*
* @return Model|static
*/
public function firstOrFail($columns = ['*'])
{
if (! $record = $this->first($columns)) {
$this->throwNotFoundException($this->getUnescapedQuery(), $this->dn);
}
return $record;
}
/**
* Throws a not found exception.
*
* @param string $query
* @param string $dn
*
* @throws ObjectNotFoundException
*/
protected function throwNotFoundException($query, $dn)
{
throw ObjectNotFoundException::forQuery($query, $dn);
}
/**
* Finds a record by the specified attribute and value.
*
* @param string $attribute
* @param string $value
* @param array|string $columns
*
* @return Model|static|null
*/
public function findBy($attribute, $value, $columns = ['*'])
{
try {
return $this->findByOrFail($attribute, $value, $columns);
} catch (ObjectNotFoundException $e) {
return;
}
}
/**
* Finds a record by the specified attribute and value.
*
* If no record is found an exception is thrown.
*
* @param string $attribute
* @param string $value
* @param array|string $columns
*
* @throws ObjectNotFoundException
*
* @return Model
*/
public function findByOrFail($attribute, $value, $columns = ['*'])
{
return $this->whereEquals($attribute, $value)->firstOrFail($columns);
}
/**
* Find many records by distinguished name.
*
* @param array $dns
* @param array $columns
*
* @return array|Collection
*/
public function findMany($dns, $columns = ['*'])
{
if (empty($dns)) {
return $this->process([]);
}
$objects = [];
foreach ($dns as $dn) {
if (! is_null($object = $this->find($dn, $columns))) {
$objects[] = $object;
}
}
return $this->process($objects);
}
/**
* Finds many records by the specified attribute.
*
* @param string $attribute
* @param array $values
* @param array $columns
*
* @return Collection
*/
public function findManyBy($attribute, array $values = [], $columns = ['*'])
{
$query = $this->select($columns);
foreach ($values as $value) {
$query->orWhere([$attribute => $value]);
}
return $query->get();
}
/**
* Finds a record by its distinguished name.
*
* @param string|array $dn
* @param array|string $columns
*
* @return Model|static|array|Collection|null
*/
public function find($dn, $columns = ['*'])
{
if (is_array($dn)) {
return $this->findMany($dn, $columns);
}
try {
return $this->findOrFail($dn, $columns);
} catch (ObjectNotFoundException $e) {
return;
}
}
/**
* Finds a record by its distinguished name.
*
* Fails upon no records returned.
*
* @param string $dn
* @param array|string $columns
*
* @throws ObjectNotFoundException
*
* @return Model|static
*/
public function findOrFail($dn, $columns = ['*'])
{
return $this->setDn($dn)
->read()
->whereHas('objectclass')
->firstOrFail($columns);
}
/**
* Adds the inserted fields to query on the current LDAP connection.
*
* @param array|string $columns
*
* @return $this
*/
public function select($columns = ['*'])
{
$columns = is_array($columns) ? $columns : func_get_args();
if (! empty($columns)) {
$this->columns = $columns;
}
return $this;
}
/**
* Add a new select column to the query.
*
* @param array|mixed $column
*
* @return $this
*/
public function addSelect($column)
{
$column = is_array($column) ? $column : func_get_args();
$this->columns = array_merge((array) $this->columns, $column);
return $this;
}
/**
* Adds a raw filter to the current query.
*
* @param array|string $filters
*
* @return $this
*/
public function rawFilter($filters = [])
{
$filters = is_array($filters) ? $filters : func_get_args();
foreach ($filters as $filter) {
$this->filters['raw'][] = $filter;
}
return $this;
}
/**
* Adds a nested 'and' filter to the current query.
*
* @param Closure $closure
*
* @return $this
*/
public function andFilter(Closure $closure)
{
$query = $this->newNestedInstance($closure);
return $this->rawFilter(
$this->grammar->compileAnd($query->getQuery())
);
}
/**
* Adds a nested 'or' filter to the current query.
*
* @param Closure $closure
*
* @return $this
*/
public function orFilter(Closure $closure)
{
$query = $this->newNestedInstance($closure);
return $this->rawFilter(
$this->grammar->compileOr($query->getQuery())
);
}
/**
* Adds a nested 'not' filter to the current query.
*
* @param Closure $closure
*
* @return $this
*/
public function notFilter(Closure $closure)
{
$query = $this->newNestedInstance($closure);
return $this->rawFilter(
$this->grammar->compileNot($query->getQuery())
);
}
/**
* Adds a where clause to the current query.
*
* @param string|array $field
* @param string $operator
* @param string $value
* @param string $boolean
* @param bool $raw
*
* @throws InvalidArgumentException
*
* @return $this
*/
public function where($field, $operator = null, $value = null, $boolean = 'and', $raw = false)
{
if (is_array($field)) {
// If the field is an array, we will assume we have been
// provided with an array of key-value pairs and can
// add them each as their own seperate where clause.
return $this->addArrayOfWheres($field, $boolean, $raw);
}
// If we have been provided with two arguments not a "has" or
// "not has" operator, we'll assume the developer is creating
// an "equals" clause and set the proper operator in place.
if (func_num_args() === 2 && ! in_array($operator, ['*', '!*'])) {
[$value, $operator] = [$operator, '='];
}
if (! in_array($operator, $this->grammar->getOperators())) {
throw new InvalidArgumentException("Invalid LDAP filter operator [$operator]");
}
// We'll escape the value if raw isn't requested.
$value = $this->prepareWhereValue($field, $value, $raw);
$field = $this->escape($field)->both()->get();
$this->addFilter($boolean, compact('field', 'operator', 'value'));
return $this;
}
/**
* Prepare the value for being queried.
*
* @param string $field
* @param string $value
* @param bool $raw
*
* @return string
*/
protected function prepareWhereValue($field, $value, $raw = false)
{
return $raw ? $value : $this->escape($value);
}
/**
* Adds a raw where clause to the current query.
*
* Values given to this method are not escaped.
*
* @param string|array $field
* @param string $operator
* @param string $value
*
* @return $this
*/
public function whereRaw($field, $operator = null, $value = null)
{
return $this->where($field, $operator, $value, 'and', true);
}
/**
* Adds a 'where equals' clause to the current query.
*
* @param string $field
* @param string $value
*
* @return $this
*/
public function whereEquals($field, $value)
{
return $this->where($field, '=', $value);
}
/**
* Adds a 'where not equals' clause to the current query.
*
* @param string $field
* @param string $value
*
* @return $this
*/
public function whereNotEquals($field, $value)
{
return $this->where($field, '!', $value);
}
/**
* Adds a 'where approximately equals' clause to the current query.
*
* @param string $field
* @param string $value
*
* @return $this
*/
public function whereApproximatelyEquals($field, $value)
{
return $this->where($field, '~=', $value);
}
/**
* Adds a 'where has' clause to the current query.
*
* @param string $field
*
* @return $this
*/
public function whereHas($field)
{
return $this->where($field, '*');
}
/**
* Adds a 'where not has' clause to the current query.
*
* @param string $field
*
* @return $this
*/
public function whereNotHas($field)
{
return $this->where($field, '!*');
}
/**
* Adds a 'where contains' clause to the current query.
*
* @param string $field
* @param string $value
*
* @return $this
*/
public function whereContains($field, $value)
{
return $this->where($field, 'contains', $value);
}
/**
* Adds a 'where contains' clause to the current query.
*
* @param string $field
* @param string $value
*
* @return $this
*/
public function whereNotContains($field, $value)
{
return $this->where($field, 'not_contains', $value);
}
/**
* Query for entries that match any of the values provided for the given field.
*
* @param string $field
* @param array $values
*
* @return $this
*/
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.
*
* @param string $field
* @param array $values
*
* @return $this
*/
public function whereBetween($field, array $values)
{
return $this->where([
[$field, '>=', $values[0]],
[$field, '<=', $values[1]],
]);
}
/**
* Adds a 'where starts with' clause to the current query.
*
* @param string $field
* @param string $value
*
* @return $this
*/
public function whereStartsWith($field, $value)
{
return $this->where($field, 'starts_with', $value);
}
/**
* Adds a 'where *not* starts with' clause to the current query.
*
* @param string $field
* @param string $value
*
* @return $this
*/
public function whereNotStartsWith($field, $value)
{
return $this->where($field, 'not_starts_with', $value);
}
/**
* Adds a 'where ends with' clause to the current query.
*
* @param string $field
* @param string $value
*
* @return $this
*/
public function whereEndsWith($field, $value)
{
return $this->where($field, 'ends_with', $value);
}
/**
* Adds a 'where *not* ends with' clause to the current query.
*
* @param string $field
* @param string $value
*
* @return $this
*/
public function whereNotEndsWith($field, $value)
{
return $this->where($field, 'not_ends_with', $value);
}
/**
* Only include deleted models in the results.
*
* @return $this
*/
public function whereDeleted()
{
return $this->withDeleted()->whereEquals('isDeleted', 'TRUE');
}
/**
* Set the LDAP control option to include deleted LDAP models.
*
* @return $this
*/
public function withDeleted()
{
return $this->addControl(LdapInterface::OID_SERVER_SHOW_DELETED, $isCritical = true);
}
/**
* Add a server control to the query.
*
* @param string $oid
* @param bool $isCritical
* @param mixed $value
*
* @return $this
*/
public function addControl($oid, $isCritical = false, $value = null)
{
$this->controls[$oid] = compact('oid', 'isCritical', 'value');
return $this;
}
/**
* Determine if the server control exists on the query.
*
* @param string $oid
*
* @return bool
*/
public function hasControl($oid)
{
return array_key_exists($oid, $this->controls);
}
/**
* Adds an 'or where' clause to the current query.
*
* @param array|string $field
* @param string|null $operator
* @param string|null $value
*
* @return $this
*/
public function orWhere($field, $operator = null, $value = null)
{
return $this->where($field, $operator, $value, 'or');
}
/**
* Adds a raw or where clause to the current query.
*
* Values given to this method are not escaped.
*
* @param string $field
* @param string $operator
* @param string $value
*
* @return $this
*/
public function orWhereRaw($field, $operator = null, $value = null)
{
return $this->where($field, $operator, $value, 'or', true);
}
/**
* Adds an 'or where has' clause to the current query.
*
* @param string $field
*
* @return $this
*/
public function orWhereHas($field)
{
return $this->orWhere($field, '*');
}
/**
* Adds a 'where not has' clause to the current query.
*
* @param string $field
*
* @return $this
*/
public function orWhereNotHas($field)
{
return $this->orWhere($field, '!*');
}
/**
* Adds an 'or where equals' clause to the current query.
*
* @param string $field
* @param string $value
*
* @return $this
*/
public function orWhereEquals($field, $value)
{
return $this->orWhere($field, '=', $value);
}
/**
* Adds an 'or where not equals' clause to the current query.
*
* @param string $field
* @param string $value
*
* @return $this
*/
public function orWhereNotEquals($field, $value)
{
return $this->orWhere($field, '!', $value);
}
/**
* Adds a 'or where approximately equals' clause to the current query.
*
* @param string $field
* @param string $value
*
* @return $this
*/
public function orWhereApproximatelyEquals($field, $value)
{
return $this->orWhere($field, '~=', $value);
}
/**
* Adds an 'or where contains' clause to the current query.
*
* @param string $field
* @param string $value
*
* @return $this
*/
public function orWhereContains($field, $value)
{
return $this->orWhere($field, 'contains', $value);
}
/**
* Adds an 'or where *not* contains' clause to the current query.
*
* @param string $field
* @param string $value
*
* @return $this
*/
public function orWhereNotContains($field, $value)
{
return $this->orWhere($field, 'not_contains', $value);
}
/**
* Adds an 'or where starts with' clause to the current query.
*
* @param string $field
* @param string $value
*
* @return $this
*/
public function orWhereStartsWith($field, $value)
{
return $this->orWhere($field, 'starts_with', $value);
}
/**
* Adds an 'or where *not* starts with' clause to the current query.
*
* @param string $field
* @param string $value
*
* @return $this
*/
public function orWhereNotStartsWith($field, $value)
{
return $this->orWhere($field, 'not_starts_with', $value);
}
/**
* Adds an 'or where ends with' clause to the current query.
*
* @param string $field
* @param string $value
*
* @return $this
*/
public function orWhereEndsWith($field, $value)
{
return $this->orWhere($field, 'ends_with', $value);
}
/**
* Adds an 'or where *not* ends with' clause to the current query.
*
* @param string $field
* @param string $value
*
* @return $this
*/
public function orWhereNotEndsWith($field, $value)
{
return $this->orWhere($field, 'not_ends_with', $value);
}
/**
* Adds a filter binding 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)
{
if (! array_key_exists($type, $this->filters)) {
throw new InvalidArgumentException("Filter type: [$type] is invalid.");
}
// Each filter clause require key bindings to be set. We
// will validate this here to ensure all of them have
// been provided, or throw an exception otherwise.
if ($missing = $this->missingBindingKeys($bindings)) {
$keys = implode(', ', $missing);
throw new InvalidArgumentException("Invalid filter bindings. Missing: [$keys] keys.");
}
$this->filters[$type][] = $bindings;
return $this;
}
/**
* Extract any missing required binding keys.
*
* @param array $bindings
*
* @return array
*/
protected function missingBindingKeys($bindings)
{
$required = array_flip(['field', 'operator', 'value']);
$existing = array_intersect_key($required, $bindings);
return array_keys(array_diff_key($required, $existing));
}
/**
* Get all the filters on the query.
*
* @return array
*/
public function getFilters()
{
return $this->filters;
}
/**
* Clear the query filters.
*
* @return $this
*/
public function clearFilters()
{
foreach (array_keys($this->filters) as $type) {
$this->filters[$type] = [];
}
return $this;
}
/**
* Determine if the query has attributes selected.
*
* @return bool
*/
public function hasSelects()
{
return count($this->columns) > 0;
}
/**
* Get the attributes to select on the search.
*
* @return array
*/
public function getSelects()
{
$selects = $this->columns ?? ['*'];
if (in_array('*', $selects)) {
return $selects;
}
if (in_array('objectclass', $selects)) {
return $selects;
}
// If the * character is not provided in the selected columns,
// we need to ensure we always select the object class, as
// this is used for constructing models properly.
$selects[] = 'objectclass';
return $selects;
}
/**
* Set the query to search on the base distinguished name.
*
* This will result in one record being returned.
*
* @return $this
*/
public function read()
{
$this->type = 'read';
return $this;
}
/**
* Set the query to search one level on the base distinguished name.
*
* @return $this
*/
public function listing()
{
$this->type = 'listing';
return $this;
}
/**
* Set the query to search the entire directory on the base distinguished name.
*
* @return $this
*/
public function recursive()
{
$this->type = 'search';
return $this;
}
/**
* Whether to mark the current query as nested.
*
* @param bool $nested
*
* @return $this
*/
public function nested($nested = true)
{
$this->nested = (bool) $nested;
return $this;
}
/**
* Enables caching on the current query until the given date.
*
* 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 cache(DateTimeInterface $until = null, $flush = false)
{
$this->caching = true;
$this->cacheUntil = $until;
$this->flushCache = $flush;
return $this;
}
/**
* Determine if the query is nested.
*
* @return bool
*/
public function isNested()
{
return $this->nested === true;
}
/**
* Determine whether the query is paginated.
*
* @return bool
*/
public function isPaginated()
{
return $this->paginated;
}
/**
* Insert an entry into the directory.
*
* @param string $dn
* @param array $attributes
*
* @throws LdapRecordException
*
* @return bool
*/
public function insert($dn, array $attributes)
{
if (empty($dn)) {
throw new LdapRecordException('A new LDAP object must have a distinguished name (dn).');
}
if (! array_key_exists('objectclass', $attributes)) {
throw new LdapRecordException(
'A new LDAP object must contain at least one object class (objectclass) to be created.'
);
}
return $this->connection->run(function (LdapInterface $ldap) use ($dn, $attributes) {
return $ldap->add($dn, $attributes);
});
}
/**
* Create attributes on the entry in the directory.
*
* @param string $dn
* @param array $attributes
*
* @return bool
*/
public function insertAttributes($dn, array $attributes)
{
return $this->connection->run(function (LdapInterface $ldap) use ($dn, $attributes) {
return $ldap->modAdd($dn, $attributes);
});
}
/**
* Update the entry with the given modifications.
*
* @param string $dn
* @param array $modifications
*
* @return bool
*/
public function update($dn, array $modifications)
{
return $this->connection->run(function (LdapInterface $ldap) use ($dn, $modifications) {
return $ldap->modifyBatch($dn, $modifications);
});
}
/**
* Update an entries attribute in the directory.
*
* @param string $dn
* @param array $attributes
*
* @return bool
*/
public function updateAttributes($dn, array $attributes)
{
return $this->connection->run(function (LdapInterface $ldap) use ($dn, $attributes) {
return $ldap->modReplace($dn, $attributes);
});
}
/**
* Delete an entry from the directory.
*
* @param string $dn
*
* @return bool
*/
public function delete($dn)
{
return $this->connection->run(function (LdapInterface $ldap) use ($dn) {
return $ldap->delete($dn);
});
}
/**
* Delete attributes on the entry in the directory.
*
* @param string $dn
* @param array $attributes
*
* @return bool
*/
public function deleteAttributes($dn, array $attributes)
{
return $this->connection->run(function (LdapInterface $ldap) use ($dn, $attributes) {
return $ldap->modDelete($dn, $attributes);
});
}
/**
* Rename an entry in the directory.
*
* @param string $dn
* @param string $rdn
* @param string $newParentDn
* @param bool $deleteOldRdn
*
* @return bool
*/
public function rename($dn, $rdn, $newParentDn, $deleteOldRdn = true)
{
return $this->connection->run(function (LdapInterface $ldap) use ($dn, $rdn, $newParentDn, $deleteOldRdn) {
return $ldap->rename($dn, $rdn, $newParentDn, $deleteOldRdn);
});
}
/**
* Handle dynamic method calls on the query builder.
*
* @param string $method
* @param array $parameters
*
* @throws BadMethodCallException
*
* @return mixed
*/
public function __call($method, $parameters)
{
// If the beginning of the method being called contains
// 'where', we will assume a dynamic 'where' clause is
// being performed and pass the parameters to it.
if (substr($method, 0, 5) === 'where') {
return $this->dynamicWhere($method, $parameters);
}
throw new BadMethodCallException(sprintf(
'Call to undefined method %s::%s()',
static::class,
$method
));
}
/**
* Handles dynamic "where" clauses to the query.
*
* @param string $method
* @param array $parameters
*
* @return $this
*/
public function dynamicWhere($method, $parameters)
{
$finder = substr($method, 5);
$segments = preg_split('/(And|Or)(?=[A-Z])/', $finder, -1, PREG_SPLIT_DELIM_CAPTURE);
// The connector variable will determine which connector will be used for the
// query condition. We will change it as we come across new boolean values
// in the dynamic method strings, which could contain a number of these.
$connector = 'and';
$index = 0;
foreach ($segments as $segment) {
// If the segment is not a boolean connector, we can assume it is a column's name
// and we will add it to the query as a new constraint as a where clause, then
// we can keep iterating through the dynamic method string's segments again.
if ($segment != 'And' && $segment != 'Or') {
$this->addDynamic($segment, $connector, $parameters, $index);
$index++;
}
// Otherwise, we will store the connector so we know how the next where clause we
// find in the query should be connected to the previous ones, meaning we will
// have the proper boolean connector to connect the next where clause found.
else {
$connector = $segment;
}
}
return $this;
}
/**
* Adds an array of wheres to the current query.
*
* @param array $wheres
* @param string $boolean
* @param bool $raw
*
* @return $this
*/
protected function addArrayOfWheres($wheres, $boolean, $raw)
{
foreach ($wheres as $key => $value) {
if (is_numeric($key) && is_array($value)) {
// If the key is numeric and the value is an array, we'll
// assume we've been given an array with conditionals.
[$field, $condition] = $value;
// Since a value is optional for some conditionals, we will
// try and retrieve the third parameter from the array,
// but is entirely optional.
$value = Arr::get($value, 2);
$this->where($field, $condition, $value, $boolean);
} else {
// If the value is not an array, we will assume an equals clause.
$this->where($key, '=', $value, $boolean, $raw);
}
}
return $this;
}
/**
* Add a single dynamic where clause statement to the query.
*
* @param string $segment
* @param string $connector
* @param array $parameters
* @param int $index
*
* @return void
*/
protected function addDynamic($segment, $connector, $parameters, $index)
{
// If no parameters were given to the dynamic where clause,
// we can assume a "has" attribute filter is being added.
if (count($parameters) === 0) {
$this->where(strtolower($segment), '*', null, strtolower($connector));
} else {
$this->where(strtolower($segment), '=', $parameters[$index], strtolower($connector));
}
}
/**
* Logs the given executed query information by firing its query event.
*
* @param Builder $query
* @param string $type
* @param null|float $time
*
* @return void
*/
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 'chunk':
$event = new Events\Chunk(...$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
*
* @return void
*/
protected function fireQueryEvent(QueryExecuted $event)
{
Container::getInstance()->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);
}
}