Building LDAP Queries


The LdapQueryBuilder class provides an easy object oriented method of producing LDAP filters of any complexity. Those familiar with Doctrine's QueryBuilder will find this syntax easy to adapt to, as it is pretty much the same. This class takes care of escaping all values passed to it when generating the filter.

Generating LDAP Filters Without LdapManager


This class is most easily used in the context of the LdapManager, but it is also possible to use on its own if your only desire is to generate LDAP filters.

use LdapTools\Query\LdapQueryBuilder;

$lqb = new LdapQueryBuilder();

$filter = $lqb->select('givenName', 'sn', 'l')
    ->where(['objectClass' => 'user'])
    ->andWhere($lqb->filter()->like('sAMAccountName','*smith'))
    ->toLdapFilter();

echo "LDAP Filter: ".$filter.PHP_EOL;

Generating Queries When Using the LdapManager


When you call buildLdapQuery in the LdapManager you will get an instance of the LdapQueryBuilder class that knows all the information about the schema of your domain, and a LdapConnection capable of executing the query. With this information it can do a lot of the heavy lifting to allow it to easily generate any LDAP filter.

$lqb = $ldapManager->buildLdapQuery();

// When no attributes are specifically selected, it will pull a default set defined in the schema.
$users = $lqb->fromUsers()
    ->where(['state' => 'Wisconsin'])
    ->getLdapQuery()
    ->getResult();

foreach ($users as $user) {
    foreach ($user->toArray() as $attribute => $value) {
        echo $attribute.' => '.$value.PHP_EOL;
    }
}

LdapQueryBuilder Methods

This class provides many methods that simplify the process of creating complex LDAP filters. The following is a list of the methods and their general use.


select($attributes)

The select method allows you to choose specifically which attributes you would like to return from the query. Simply pass an array of attribute names to it that you would like, or a single attribute as a string. In the absence of anything passed to it, it will select the default set of attributes for the query as defined for the type in the schema.

To retrieve all attributes defined in the schema you can pass a single wildcard * as a selected attribute. In the absence of a schema doing that will also select all LDAP attributes. To select all LDAP attributes and all schema attributes for a LDAP object you can pass a double wildcard ** as a selected attribute.

Attribute names are looked for in the schema to see if they map to specific LDAP attributes.

$lqb->select(['firstName', 'city', 'state', 'sid']);

// Select only a single attribute
$lqb->select('guid');

// Attribute names will always be returned in the case you enter it in, irrespective of how LDAP returns the data.
$lqb->select(['FirstName', 'City', 'State', 'SID']);

// Select all attributes defined in the schema for a LDAP object
$lqb->select('*');

// Select all attributes both in the schema and from LDAP for an object
$lqb->select('**');

If you want the raw data to be returned from LDAP you can select LDAP attribute names explicitly. You can also include schema names at the same time. Attributes selected by their LDAP attribute name will NOT have attribute conversion done.

// Will return 'objectSid' AND 'sid'
$lqb->select(['givenName', 'l', 'objectSid', 'sid']);

from($ldapType, $alias = null)

The from method requires an argument for the LDAP type. This type must be defined in your LDAP schema. Common types that are in the schema by default include: user, group, contact, computer, ou. These types are defined as constants in the \LdapTools\Object\LdapObjectType class. Using this method makes the query aware of the attribute name mapping and converters defined for the type.

use LdapTools\Object\LdapObjectType;

// Search for users
$lqb->from(LdapObjectType::USER);

// Search for computers
$lqb->from(LdapObjectType::COMPUTER);

// Search for users and assign an alias to it
$lqb->from(LdapObjectType::USER, 'u');

You can also call the from() method dynamically for schema types you have defined:

// Selects from the 'ExchangeServer' schema type.
$lqb->fromExchangeServer();

// Selects from the 'container' schema type, and assigns it an alias of 'c'.
$lqb->fromContainer('c');

fromUsers($alias = null)

A convenience shortcut of the from method to select from LDAP user types. Optionally pass a string alias name.

// Search for users
$lqb->fromUsers();

fromGroups($alias = null)

A convenience shortcut of the from method to select from LDAP group types. Optionally pass a string alias name.

// Search for groups
$lqb->fromGroups();

fromOUs($alias = null)

A convenience shortcut of the from method to select from LDAP ou types. Optionally pass a string alias name.

// Search for OUs
$lqb->fromOUs();

where(...$statements)

This method encapsulates its arguments into a logical 'AND' statement. You can either pass a simple array of attributes and values that must be met, or any number of filter operator statements.

// Pass a simple array of attributes => values. These attributes must be equal to these values.
$lqb->where(['firstName' => 'Timmy', 'state' => 'Wyoming']);

// Pass filter operator statements instead. It will take care of attribute value conversions as well.
$lqb->where($lqb->filter()->gte('created', new \DateTime('5-4-2000'));

// A more complex series of statements
$lqb->where(
    $lqb->filter()->neq('firstName', 'Jimbo'),
    $lqb->filter()->bOr(
        $lqb->filter()->eq('lastName', 'Dodgson'), 
        $lqb->filter()->eq('lastName', 'Venkman')
    )
);

andWhere(...$statements)

This method is the same as the where method. It encapsulates its arguments into a logical 'AND' statement. Statements passed will be added to the same logical 'AND' statement that the where method created.

// This adds to the existing and statement...
$lqb->andWhere(['lastName' => 'Smith']);

orWhere(...$statements)

This method is the same as the where method, but it will instead encapsulate any passed arguments into a logical 'OR' statement.

// This creates a separate OR statement...
$lqb->orWhere(['department' => 'IT', 'department' => 'Marketing']);

add(...$statements)

This is a low-level method that will take any object that is an instance of a BaseOperator and add it to the query. The shorthand $lqb->filter() methods is what does the heavy-lifting for creating the various operator objects. You should not need to call this method explicitly, but it is included if you need it.

// Add some operators directly to the query
$lqb->add($lqb->filter()->startsWith('name', 'srv'), $lqb->filter()->notPresent('description'));

setServer($server)

This lets you set the LDAP server that the query will run against when executed. After the query finishes executing the connection switches back to the LDAP server it was originally connected to.

// Query a specific LDAP server
$lqb->setServer('dc3.example.local');

setBaseDn($baseDn)

This method sets the base DN (distinguished name) for the query. That means that any LDAP object at or below this point in the directory will be queried for. This will default first to a base_dn set in the schema for the object type you are searching for and if that is not set it will default to whatever you set in the domain configuration for the base_dn value.

$lqb->setBaseDn('OU=Employees,OU=Users,DC=example,DC=com')

setScopeSubTree()

'subtree' is the default search scope for the query and should not need to be called explicitly. This method sets the LDAP search scope recursively from the point of the base DN onwards.


setScopeOneLevel()

This method sets the LDAP search scope to one level at the point of the base DN. This is equivalent to a non-recursive listing of the contents of a folder directory. The search will not recurse any further than the level of the base DN.


setScopeBase()

This method sets the LDAP search scope to the base level. This is used to retrieve the contents of a single entry, and is most commonly used to retrieve the RootDSE of a domain.

Example usage to return the RootDSE for a domain:

// NOTE: You can also call the getRootDse() method on the connection object to get the RootDSE...
$rootDse = $lqb->where($lqb->filter()->present('objectClass'))
                ->setBaseDn('')
                ->setScopeBase()
                ->getLdapQuery()
                ->getSingleResult();

setScope($scope)

Explicitly set the scope for the query using the QueryOperation::SCOPE constant. The available options are: QueryOperation::SCOPE['SUBTREE'], QueryOperation::SCOPE['ONELEVEL'], QueryOperation::SCOPE['BASE']

use LdapTools\Operation\QueryOperation;

$lqb->setScope(QueryOperation::SCOPE['ONELEVEL']);

orderBy($attribute, $direction = 'ASC')

This method sets the attribute to order the results by in either ascending (default) or descending order. Calling this overwrites any already set orderBy statements. To stack multiple order statements call addOrderBy($attribute).

// Order results by last name (ascending).
$users = $lqb->fromUsers()
    ->where(['firstName' => 'John'])
    ->orderBy('lastName')
    ->getLdapQuery()
    ->getResult();

By default the results are ordered in a case-insensitive manner. The order results using a case-sensitive manner use the setIsCaseSensitiveSort() method of the LdapQuery class:

// Order results in a case-sensitive manner.
$users = $lqb->fromUsers()
    ->where(['firstName' => 'John'])
    ->orderBy('lastName')
    ->getLdapQuery()
    ->setIsCaseSensitiveSort(true)
    ->getResult();

addOrderBy($attribute, $direction = 'ASC')

This method works the same as orderBy($attribute), only calling this one will not overwrite already declared order-by statements. Call this when you want to order by multiple attributes.

// Order results by last name (descending) and first name (ascending).
$users = $lqb->fromUsers()
    ->where(['state' => 'Wisconsin'])
    ->orderBy('lastName', 'DESC')
    ->addOrderBy('firstName', 'ASC')
    ->getResult();

setSizeLimit($size)

This methods sets the size limit for the amount of results returned from LDAP for the query.

$lqb->setSizeLimit(10);

setPageSize($size)

This methods sets the paging size for the query. It will default to whatever value you set in your configuration. The default when no value is explicitly set is 1000.

$lqb->setPageSize(500);

setUsePaging($usePaging)

This methods lets you set whether or not paging should be used for the query. This overrides whatever is set in the domain configuration. If this is not set, then whatever is set in the domain configuration is used.

$lqb->setUsePaging(false);

toLdapFilter()

Gets the LDAP filter, as a string, that the query would produce.

$filter = $lqb->toLdapFilter();

Using Aliases

When you want to search for multiple object types you can assign them specific aliases to refer to them in your filter. This makes it easy to get all the results you need with a single query:

use LdapTools\Object\LdapObjectType;

$query = $ldap->buildLdapQuery();

// Get all users with a department that starts with IT and groups that contain 'admin' in their description.
// The resulting objects are ordered by their name (for both users and groups).
$results = $query
    ->fromUsers('u')
    ->fromGroups('g')
    ->where($query->filter()->startsWith('u.department', 'IT'))
    ->andWhere($query->filter()->contains('g.description', 'admin'))
    ->sortBy('name')
    ->getLdapQuery()
    ->getResult();

foreach ($results as $result) {
    if ($result->isType(LdapObjectType::USER)) {
        echo "User: ".$result->getName();
    } else {
        echo "Group: ".$result->getName();
    }
}

// Select all OUs and Containers at the root of the domain. Order them by name with OUs first, then containers.
$results = $ldap->buildLdapQuery()
    ->from(LdapObjectType::OU, 'u')
    ->from(LdapObjectType::CONTAINER, 'c')
    ->addOrderBy('u.name', 'ASC')
    ->addOrderBy('c.name', 'ASC')
    ->setScopeOneLevel()
    ->getLdapQuery()
    ->getResult();

You can reference an alias in the query builder anywhere that you would reference a specific attribute (select, orderBy, where/andWhere/or/orWhere statements, etc). The only rule that applies for alias names is that they can only be alphanumeric (but can also contain underscores).

LdapQuery Methods to Retrieve LDAP Results


There are a few ways to retrieve LDAP results after you have a query built. How you retrieve the results depends upon what type of data you're looking for. To start to retrieve results you need to first get a LdapQuery instance by using the getLdapQuery() method.

The getLdapQuery() method retrieves an instance of the LdapQuery object that you can then call methods on to get your results. The LdapQuery object has the filter, page size, base DN, scope, etc that you set in the builder and takes care of converting the LDAP results array using a hydration process. It returns an easier to use set of objects. Or you can have it return a simple set of arrays with the attributes and values.


// By default the results will be a collection of LdapUser objects you can iterate over...
$results = $lqb->getLdapQuery()->getResult();

foreach ($results as $result) {
    echo $result->getEmailAddress();
}

// If you just want simple arrays returned you can specify that
$results = $lqb->getLdapQuery()->getArrayResult();

foreach ($results as $result) {
    foreach ($result as $attribute => $value) {
        echo "$attribute => $value";
    }
}

execute($hydrationType = HydratorFactory::TO_OBJECT)


This LdapQuery method executes the LDAP filter with the options you have set and returns the results as either a set objects (this is the default) or as an array (use the hydration type HydratorFactory::TO_ARRAY). See previous example for full usage.

getResult($hydrationType = HydratorFactory::TO_OBJECT)


This is an alias for the execute() method. It will return a LdapObjectCollection by default, or an array of LDAP entries if specified as getResult(HydratorFactory::TO_ARRAY).

getArrayResult()


This functions the same as the getResult() method, but it will always return the LDAP entries as an array instead of a collection of objects. This is identical to calling getResult(HydratorFactory::TO_ARRAY).

getSingleResult($hydrationType = HydratorFactory::TO_OBJECT)


This LdapQuery method will retrieve a single result from LDAP. So instead of a collection of objects or arrays you will be given a single result you can immediately begin to work with.

$lqb = $ldap->buildLdapQuery();

// Retrieve a single LdapObject from a query...
$user = $lqb->fromUsers()
    ->Where(['username' => 'chad'])
    ->getLdapQuery()
    ->getSingleResult();

echo "DN : ".$user->getDn();

If an empty result set is returned from LDAP then it will throw a \LdapTools\Exception\EmptyResultException. If more than one result is returned from LDAP then it will throw a \LdapTools\Exception\MultiResultException. Additionally, you may pass an explicit hydration type to this method if you wish to get the result as a single array of attributes and values.

getOneOrNullResult($hydrationType = HydratorFactory::TO_OBJECT)


The behavior of this method is very similar to getSingleResult(), but if no results are found for the query it will return null instead of throwing an exception. However, it will still throw an exception in the case that more than one result is returned from LDAP.

$lqb = $ldap->buildLdapQuery();

// Retrieve a single LdapObject from a query, or a null result if it doesn't exist...
$user = $lqb->fromUsers()
    ->Where(['username' => 'john'])
    ->getLdapQuery()
    ->getOneOrNullResult();

// Could be null, so check first...
if ($user) {
    echo "DN : ".$user->getDn();
}

getSingleScalarResult()


Using this method you can get the value of a single attribute from the query. If the LDAP object or attribute does not exist then it will throw an exception.

$lqb = $ldap->buildLdapQuery();

// Retrieve the GUID string of a specific AD user...
$guid = $lqb->select('guid')
    ->fromUsers()
    ->Where(['username' => 'chad'])
    ->getLdapQuery()
    ->getSingleScalarResult();

echo "GUID : ".$guid;

getSingleScalarOrNullResult()


The behavior of this method is very similar to getSingleScalarResult(), but if the attribute is not found/set for the LDAP object it will return null instead of throwing an exception. However, it will still throw an exception in the case that more than one result is returned from LDAP or if the LDAP object does not exist.

$lqb = $ldap->buildLdapQuery();

// Retrieve the title of a specific AD user...
$title = $lqb->select('title')
    ->fromUsers()
    ->Where(['username' => 'chad'])
    ->getLdapQuery()
    ->getSingleScalarOrNullResult();

// Check if the attribute actually had a value first
$title = $title ?: 'Unknown';

echo $title;

Caching Queries


If you have enabled/set a caching a method in your configuration, you can use several different options to cache queries going to LDAP. This could save considerable time, as the raw LDAP results will be fetched from the cache for the query operation.

// Retrieve all of the users...
$allUsers = $ldap
    ->fromUsers()
    ->getLdapQuery()
    // Grab the results from the cache, or cache the result if it does not exist.
    ->useCache(true)
    // Expire the cached result in one day (accepts any \DateTimeInterface object...
    ->expireCacheAt((new \DateTime())->modify('+1 day'))
    ->getResult();

On the first run of the above query it will grab the results from LDAP, then store it in the cache with an expiration 1 day from now. To retrieve the results from the cache you need to run the same query with useCache(true).


useCache($useCache = false)

Set whether or not the cache should be used for the query. This controls both retrieval and storage of the result in the cache. You must set this if you want to retrieve an already cached result from the cache.


expireCacheAt(\DateTimeInterface $time = null)

Set this to force the cache to expire at a specific time. This can be any \DateTimeInterface object. To never expire the cache item set it to null.


executeOnCacheMiss($executeOnCacheMiss = true)

Set whether or not the query should execute if useCache() was set to true and the result was not already in the cache. If this is set to false and the result is not in the cache then a CacheMissException will be thrown. By default this is set to true, so the item is not in the cache the operation will re-run and re-cache the result.


invalidateCache($invalidateCache = false)

Set whether or not to delete a cached result for the query (if it exists in the cache). You do not have to set useCache() for this to be triggered. However, you can use it in conjunction with useCache(true) to force a refresh of an already cached item.

// Retrieve all of the users...
$allUsers = $ldap
    ->fromUsers()
    ->getLdapQuery()
    // Force any existing cache item to be removed first...
    ->invalidateCache(true)
    ->useCache(true)
    ->expireCacheAt((new \DateTime())->modify('+1 day'))
    ->getResult();

Filter Method Shortcuts


The filter() method of the LdapQueryBuilder returns a helper class that provides many shortcut methods for creating the LDAP operator classes within the \LdapTools\Query\Operator namespace. This way you do not have to manually construct the operators by doing:

use \LdapTools\Query\Operator\bOr;
use \LdapTools\Query\Operator\Comparison;

// ...
$lqb->where(new bOr(
    new Comparison('firstName', Comparison::EQ, 'Bill'),
    new Comparison('firstName', Comparison::EQ, 'Egon')
));

Instead you can write:

$lqb->where($lqb->filter()->or(
    $lqb->filter()->eq('firstName', 'Bill'),
    $lqb->filter()->eq('firstName', 'Egon')
));

When you call filter() you are just calling a method on the \LdapTools\Query\Builder\FilterBuilder class. The full list and description of available methods is below.


aeq($attribute, $value)

Creates an "approximately-equal-to" comparison between the attribute and the value. The results are dependent on the LDAP specific implementation of this operator. But it will typically function as a "sounds like" comparison: (attribute~=value)

$lqb->filter()->aeq('firstName', 'Sue');

eq($attribute, $value)

Creates an "equal-to" comparison between the attribute and the value: (attribute=value)

$lqb->filter()->eq('lastName', 'Sikorra');

neq($attribute, $value)

Creates a "not-equal-to" comparison between the attribute and the value. This is equivalent to wrapping a eq($attribute, $value) within a 'NOT' statement: (!(attribute=value))

$lqb->filter()->neq('department', 'Purchasing');

lt($attribute, $value)

Creates a "less-than" comparison between the attribute and the value. Since an actual '<' operator does not exist in LDAP, this is a combination of a greater-than-or-equal-to operator along with a check if the attribute is set/present. This is encapsulated within a logical 'AND' operator: (&(!(attribute>=value))(attribute=*))

$lqb->filter()->lt('badPasswordCount', 2);

leq($attribute, $value)

Creates a "less-than-or-equal-to" comparison between the attribute and the value: (attribute<=value)

$lqb->filter()->leq('badPasswordCount', 1);

gt($attribute, $value)

Creates a "greater-than" comparison between the attribute and the value. Since an actual '>' operator does not exist in LDAP, this is a combination of a less-than-or-equal-to operator along with a check if the attribute is set/present. This is encapsulated within a logical 'AND' operator: (&(!(attribute<=value))(attribute=*))

$lqb->filter()->gt('created', new \DateTime('01-20-2013'));

geq($attribute, $value)

Creates a "greater-than-or-equal-to" comparison between the attribute and the value: (attribute>=value)

$lqb->filter()->geq('badPasswordCount', 3);

match($attribute, $rule, $value, $dnFlag = false)

Creates an extensible match against an attribute or dn: (attribute:caseExactMatch:=value)

$lqb->filter()->match('name', 'caseExactMatch', 'Chad');

matchDn($attribute, $value)

Creates an extensible match with the DN flag. This can help in searching multiple OUs: (ou:dn:=Sales)

Note: AD Does not support this aspect of the extensible match.

$lqb->filter()->matchDn('ou', 'Sales');

in($attribute, array $values)

Check if an attribute value matches any of the values in the list of values provided. This is a shortcut for a multiple OR condition: (|(id=1)(id=2)(id=3)(id=4)(id=5))

$lqb->filter()->in('id', [1, 2, 3, 4, 5]);

bitwiseAnd($attribute, $value)

Creates a bitwise 'AND' comparison between the attribute and the value: (attribute:1.2.840.113556.1.4.803:=value)

use LdapTools\Enums\AD\UserAccountControl;

$lqb->filter()->bitwiseAnd('userAccountControl', UserAccountControl::Disabled);

bitwiseOr($attribute, $value)

Creates a bitwise 'OR' comparison between the attribute and the value: (attribute:1.2.840.113556.1.4.804:=value)

use LdapTools\Enums\AD\GroupType;

$lqb->filter()->bitwiseOr('groupType', GroupType::UniversalGroup);

startsWith($attribute, $value)

Creates a "equal-to" comparison with a wildcard after the value: (attribute=value*)

$lqb->filter()->startsWith('department', 'IT');

endsWith($attribute, $value)

Creates a "equal-to" comparison with a wildcard before the value: (attribute=*value)

$lqb->filter()->endsWith('description', 'service');

contains($attribute, $value)

Creates a "equal-to" comparison with a wildcards at each end of the value: (attribute=*value*)

$lqb->filter()->contains('name', 'admin');

like($attribute, $value)

Creates a "equal-to" comparison that will not escape any wildcards you use in the value: (attribute=v*a*l*u*e).

$lqb->filter()->like('description', '*Some*thing*');

present($attribute)

Creates a "equal-to" comparison with a single wildcard as the value. Returns any entry with this attribute populated: (attribute=*)

$lqb->filter()->present('mail');

notPresent($attribute)

Creates a negated form of the present($attribute) method. Returns any entry that does not contain the attribute: (!(attribute=*))

$lqb->filter()->notPresent('department');

bAnd(...$statements)

Creates a logical 'AND' statement against all other operators passed to it: (&((attribute=value)(attribute=value)))

$lqb->filter()->bAnd(
    $lqb->filter()->eq('department', 'IT'), 
    $lqb->filter()->startsWith('firstName', 'Tim')
);

bOr(...$statements)

Creates a logical 'OR' statement against all other operators passed to it: (|((attribute=value)(attribute=value)))

$lqb->filter()->bOr(
    $lqb->filter()->eq('department', 'IT'), 
    $lqb->filter()->eq('department', 'Purchasing')
);

bNot($statements)

Creates a logical 'NOT' statement against whatever other statement you pass it it: (!(attribute=value))

$lqb->filter()->bNot($lqb->filter()->eq('department', 'IT'));

Active Directory Filter Method Shortcuts

If you're using a LdapConnection that has a LDAP type set as ad, then when you call filter() you will also have additional filter method shortcuts that are specific to Active Directory:


hasMemberRecursively($member, $attribute = 'members')


Recursively checks groups for a specific member. The $member parameter can be any of the following:

This creates a matching rule comparison using the OID IN_CHAIN against the groups members attribute by default. If you have a custom attribute, or some other attribute you would like to run it against, you must pass it as the second argument.

$username = 'chad';

// Query by a username to get all of their groups recursively...
$groups = $ldap->buildLdapQuery()
    ->fromGroups()
    ->where($query->filter()->hasMemberRecursively($username))
    ->getLdapQuery()
    ->getResult();

isRecursivelyMemberOf($group)

Recursively checks an object's group membership for a group. The $group parameter can be any of the following:

This creates a matching rule comparison using the OID IN_CHAIN against the users groups attribute.

// Query by a group name...
$query = $ldap->buildLdapQuery();
$users = $query->select()
    ->fromUsers()
    ->where($query->filter()->isRecursivelyMemberOf('Employees'))
    ->getLdapQuery()
    ->getResult();

// If you are not targeting a specific set of objects from the schema, then you must
// pass 'false' as the second argument and specify a full DN. Otherwise this method
// will attempt to use the 'groups' attribute from the schema by default.
$ldapObjects = $ldap->buildLdapQuery()
    ->select('description')
    ->where(['cn' => 'foo'])
    ->andWhere($query->filter()->isRecursivelyMemberOf('CN=Foo,DC=foo,DC=bar', false))
    ->getLdapQuery()
    ->getResult();

mailEnabled()


Performs a simple check to determine whether an LDAP object is mail-enabled (ie. can receive email from Exchange).