Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ldaprecord Laravel Package

directorytree/ldaprecord

LdapRecord is a full-featured LDAP ORM for PHP and Laravel. It offers ActiveRecord-style models, a fluent query builder, authentication, and multi-domain connection management, making OpenLDAP and Active Directory integration simple and expressive.

View on GitHub
Deep Wiki
Context7

Getting Started

LdapRecord is the directory layer; in Laravel you almost always want the companion integration, which pulls in the core ORM:

composer require directorytree/ldaprecord-laravel
php artisan vendor:publish --provider="LdapRecord\Laravel\LdapServiceProvider"

Configure one connection in config/ldap.php (or .env), point it at a test DC — not production — and confirm a bind before writing any model code:

'connections' => [
    'default' => [
        'hosts'    => [env('LDAP_HOST', 'ldap.acme.test')],
        'base_dn'  => env('LDAP_BASE_DN', 'dc=acme,dc=test'),
        'username' => env('LDAP_USERNAME'),
        'password' => env('LDAP_PASSWORD'),
        'port'     => 389,
        'tls'      => true,
        'timeout'  => 5,
    ],
],

Your first model. Keep it minimal — $objectClasses is what lets LdapRecord hydrate and search correctly:

namespace App\Ldap;

use LdapRecord\Models\Model;

class User extends Model
{
    public static array $objectClasses = [
        'top', 'person', 'organizationalPerson', 'user',
    ];
}

Then the first real use case — resolve a person by mail and read their DN:

$user = User::where('mail', '[email protected]')->first();

$user?->getDn();      // cn=Jane Doe,ou=Users,dc=acme,dc=test
$user?->getName();    // first RDN value: "Jane Doe"

Where to look first: ldaprecord.com/docs/core/v4 (the "Searching" and "Models" pages carry 80% of daily usage), then LdapRecord\Models\Model for the attribute/DN API and LdapRecord\Connection for config keys. Read the v4 upgrade guide once before you start — the v4 query builder filter semantics differ subtly from v3 (see v4.0.1/4.0.2 fixes), and it's cheaper to learn that now than mid-feature.

Implementation Patterns

Model per object class, not per table. Define App\Ldap\User, App\Ldap\Group, App\Ldap\OrganizationalUnit. Use $connection to route to non-default DCs, $casts to normalise AD weirdness, and morphInto()/morphIntoOrFail() when an entry's objectclass determines the subclass (v4.1.1 fixed successful subclass conversions in morphIntoOrFail).

class User extends Model
{
    public static array $objectClasses = ['top','person','organizationalPerson','user'];

    protected $casts = [
        'lastlogontimestamp' => 'datetime',
        'pwdlastset'         => 'datetime',
        'accountcontrol'     => 'integer',
    ];
}

Query builder as your authorization language. Prefer directory queries over a mirrored SQL org chart:

$isInGroup = User::find($dn)
    ->groups()
    ->where('cn', '=', 'VPN-Users')
    ->exists();

// Active Directory helper (note the {base} substitution fix in v3.8.2)
Group::whereMemberOf($user)->first();

For OU-scoped authorization use whereContains/whereStartsWith against the DN plus a global scope that pins the subtree — that keeps every query in the class from accidentally reaching the whole domain.

Relationships for membership. hasMany(Group::class, 'member') on the user side and the inverse on the group side. attach()/detach() writes LDAP directly. Treat detach with care — v4.1.1 fixed a bug where detaching relationships could delete populated groups; if you're on v3, don't automate detach.

Writes: create, rename, move. Set the DN (or use inside()) and save; {base} substitution now works during create and rename (v3.3.0), which makes provisioning code portable across environments:

$user = (new User)->inside('ou=Users,{base}');
$user->cn = 'Jane Doe';
$user->mail = '[email protected]';
$user->save();

$user->rename('cn=Jane Smith');   // or renameAndGetDn() when you need the new DN

Self-service passwords are a first-class path as of v4.1.0 — User::changePassword($old, $new) for OpenLDAP (including Argon2 hashes). The prerequisite is a TLS/SSL-bound connection; allow_insecure_password_changes exists to relax that, and it should be a documented, reviewed decision rather than an env default.

Multiple connections and failover. Give each domain/forest its own connection entry and resolve models with $model->on('corp')->... rather than swapping global state. Keep hosts as an array so failover works without a code change.

Testing without a DC. DirectoryFake::setup() in setUp(), DirectoryFake::tearDown() in tearDown() (the container flush landed in v3.6.4), then assert on the interface calls your code makes. This is the difference between "identity features ship continuously" and "identity features ship when someone gets VPN access to the lab."

Observability. Listen to model events (Retrieved, Saving, Created, Deleting…), the Laravel auth events (Authenticated, Failed — which carries a BindException since v3.6.1), and log query failures with DetailedError's non-nullable $errorCode/$errorMessage (v3.2.3).

Gotchas and Tips

  • LDAP attributes are always arrays. $user->mail returns ['[email protected]']. Use $user->getFirstAttribute('mail') or casts; writing a scalar string into a multi-valued attribute is a classic silent footgun.
  • No transactions. LDAP has no rollback. Any multi-step provisioning (create user → add to group → set password) needs compensating deletes and an idempotent retry strategy. Design for partial failure explicitly.
  • Base DN substitution is string-level. {base} resolves to the connection's base_dn at query time — great for env portability, dangerous if you cache a resolved DN string anywhere.
  • Query cache keys. Search bases and controls were only folded into cache keys in v4.1.1. If you cache directory results on an earlier version, you can serve the wrong group — for authorization that's a security bug, not a perf bug. Prefer no query cache until you're on ≥4.1.1, and never cache across connections without keying on the connection name.
  • Pagination and chunking leak controls if you bail early. paginate() and chunk() issue paged results; v4.1.1 fixed leftover paging controls when chunking stops early and reset stale pagination state. If you break out of a chunk() loop, verify the next query on the same connection isn't still paging.
  • ->whereIn() with an empty array returns zero results (fixed in v3.8.5) — it does not degrade to "no filter." Guard the empty case yourself; it's the most common source of "the list went blank" bugs.
  • refresh() returns false for missing entries instead of throwing (v4.1.1). Check the return value.
  • Server-side ordering is not universal. orderBy() uses an LDAP sort control; OpenLDAP generally doesn't support it. Sort in PHP for OpenLDAP, and remember chunking overrides the query limit to 0.
  • TLS state is per-connection and sticky. The secure flag is reset on close() (v3.8.3); if you're reusing connections in long-running workers, close and re-init after network errors rather than assuming the upgrade survived.
  • Debugging: Ldap::DEBUG_* constants (v3.7.2) map to LDAP_OPT_DEBUG_LEVEL bits — set them via the connection options in local only. For real diagnosis, read the DetailedError and the raw filter from the builder (->toRawFilter()) before guessing at schema.
  • Binary attributes. objectGUID and objectSid serialise/unserialise into binary; v3.5.1 fixed UTF-8 and ordering issues there. Don't put raw GUIDs in user-facing URLs without a stable string cast.
  • Upgrades are cheap if you sequence them. Laravel 11/12/13 support exists on both v3.8.6 and v4.0.3, so you can move framework versions and the LDAP major independently. Read the v4 release notes before touching orFilter/andFilter/orWhere call sites — those were the behavioural churn points in 4.0.x.
  • Extension points worth knowing: global scopes (fixed for query mutation in v4.0.6), custom Timestamp/cast classes, the protocol connection option (v3.7.0), and model morphInto for polymorphic object classes. Those four cover most "we need it to behave differently" requests without forking.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log