directorytree/ldaprecord
LDAPRecord is an LDAP directory and Active Directory ORM for Laravel and PHP. It provides fluent models, query builder, authentication and user sync, event-driven operations, and easy integration with Laravel apps for managing and searching directory entries.
Installation:
composer require directorytree/ldaprecord
Publish the config:
php artisan vendor:publish --provider="DirectoryTree\LdapRecord\Laravel\LdapRecordServiceProvider" --tag="ldaprecord.config"
Configure LDAP Connection (config/ldap.php):
'connections' => [
'ad' => [
'host' => 'ldap.example.com',
'port' => 389,
'use_ssl' => true,
'base_dn' => 'dc=example,dc=com',
'username' => 'cn=admin,dc=example,dc=com',
'password' => 'password',
'timeout' => 5,
],
],
Define a Model (app/Models/Ldap/User.php):
use DirectoryTree\LdapRecord\Laravel\Model;
class User extends Model
{
protected $connection = 'ad';
protected $dn = 'cn=*,ou=users,dc=example,dc=com';
protected $attributes = [
'cn', 'sn', 'givenName', 'mail', 'userPrincipalName'
];
protected $casts = [
'mail' => 'string',
'userPrincipalName' => 'string',
];
}
First Query:
use App\Models\Ldap\User;
$users = User::where('mail', '*@example.com')->get();
use DirectoryTree\LdapRecord\Laravel\Auth\AuthenticatesUsers;
class LdapAuthController extends Controller
{
use AuthenticatesUsers;
public function login(Request $request)
{
$credentials = $request->only('username', 'password');
if ($this->attemptLdapLogin($credentials)) {
return redirect()->intended('/dashboard');
}
return back()->withErrors(['email' => 'Invalid credentials']);
}
}
Leverage familiar Laravel syntax for LDAP operations:
// Basic search
User::where('mail', 'john@example.com')->first();
// Nested conditions
User::where('department', 'IT')
->where(function ($query) {
$query->where('role', 'admin')
->orWhere('role', 'manager');
})
->get();
// Pagination
User::paginate(10);
// Chunking (for large datasets)
User::chunk(100, function ($users) {
foreach ($users as $user) {
// Process user
}
});
use DirectoryTree\LdapRecord\Laravel\Scope;
class TenantScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
$tenantId = auth()->id();
$builder->where('ou', "ou=tenant_{$tenantId},dc=example,dc=com");
}
}
class User extends Model
{
protected static function booted()
{
static::addGlobalScope(new TenantScope);
}
}
// Attempt login
if (Auth::attemptLdap(['username' => 'john', 'password' => 'secret'])) {
// Success
}
// Password updates
$user = User::find('john@example.com');
$user->password = 'newpassword';
$user->save();
// Bind directly
$ldap = app('ldap');
$bind = $ldap->bind('cn=admin,dc=example,dc=com', 'password');
class Group extends Model
{
public function members()
{
return $this->hasMany(User::class, 'memberOf', 'dn');
}
}
class User extends Model
{
public function groups()
{
return $this->belongsToMany(Group::class, 'memberOf', 'dn', 'member');
}
}
// Switch connections dynamically
User::onConnection('backup_ad')->get();
// Configure multiple connections
'connections' => [
'ad' => [...],
'backup_ad' => [
'host' => 'backup.ldap.example.com',
// ...
],
],
use DirectoryTree\LdapRecord\Testing\DirectoryFake;
beforeEach(function () {
DirectoryFake::new()
->withDirectory('dc=example,dc=com')
->withEntry('cn=john,ou=users,dc=example,dc=com', [
'cn' => 'John Doe',
'mail' => 'john@example.com',
]);
});
it('finds a user', function () {
$user = User::where('mail', 'john@example.com')->first();
expect($user->cn)->toBe('John Doe');
});
Global Scope Mutations:
where() and orWhere() in global scopes. Use withoutGlobalScopes() for debugging:
User::withoutGlobalScopes()->where('mail', '*')->get();
32-bit PHP Timestamp Issues:
protected $casts = [
'lastLoginTime' => 'datetime:Y-m-d H:i:s',
];
Empty whereIn Queries:
whereIn() with an empty array returns all results instead of none (fixed in v3.8.5).if (!empty($ids)) {
User::whereIn('id', $ids)->get();
}
TLS/SSL Errors:
ldap_start_tls() may fail with "Local error" (fixed in v3.6.0).use_ssl is set to false and start_tls is explicitly enabled:
'connections' => [
'ad' => [
'use_ssl' => false,
'start_tls' => true,
// ...
],
],
Boolean Casts:
protected $casts = [
'accountDisabled' => 'boolean',
];
Base DN Substitution:
{base} in whereMemberOf may not work (fixed in v3.8.2).Group::whereMemberOf('CN=IT,OU=Groups,{base}')->get();
Enable Debugging:
// config/ldap.php
'debug' => env('LDAP_DEBUG', false),
Use constants like Ldap::DEBUG_FILTER for granular logging.
Query Logging:
User::where('mail', '*')->toLdap()->get();
// Outputs the raw LDAP filter for debugging.
Connection Issues:
$ldap = app('ldap');
$ldap->connect();
Attribute Casting:
getAttribute() and setAttribute() for custom logic:
public function getFullNameAttribute()
{
return "{$this->givenName} {$this->sn}";
}
Performance:
select('*') for large datasets. Explicitly define attributes:
protected $attributes = ['cn', 'mail']; // Only fetch these
limit() and offset() for pagination:
User::limit(10)->offset(20)->get();
Custom Query Types: Extend the query builder for specialized operations:
use DirectoryTree\LdapRecord\Query\Builder;
class CustomBuilder extends Builder
{
public function listMembers()
{
return $this->queryType(self::QUERY_LIST)->get();
}
}
Attribute Casting: Override casting logic for non-standard attributes:
protected $casts = [
'binaryGuid' => 'binaryGuid',
'whenChanged' => '
How can I help you explore Laravel packages today?