You're browsing documentation for an old version.

View Current

Models: Relationships

Introduction

LDAP records often contain attributes that reference other LDAP records in your directory. An example of this would be the member attribute on LDAP groups that contain a list of distinguished names whom are members of the group.

Using LdapRecord relationships, we can define what models contain references to other records and easily retrieve the referenced models to perform operations upon. There are several relationship types that LdapRecord supports:

Relationship Type
Has One Indicates a one-to-one relation, such as a user having one manager
Has Many Indicates a one-to-many relation, such as a user having many groups
Has Many (Inverse) Indicates an inverse one-to-many relation, such as a group having many members
Has Many In Indicates a one-to-many relation, but with virtual attributes that cannot be modified

Defining Relationships

Has One

A has one relationship is a basic relationship to work with. An example of a "has one" relationship would be a User having one manager. To define this relationship, we place a manager() method on our User model, and call the hasOne() method and return the result:

<?php

use LdapRecord\Models\Model;

class User extends Model
{
    /**
     * Retrieve the manager of the current user.
     */
    public function manager()
    {
        return $this->hasOne(User::class, 'manager');
    }
}

The first argument that is passed into the relation is the name of the related model. The second is the LDAP attribute on the current user that contains the relationships distinguished name.

If the relationships attribute you are defining does not contain a distinguished name, you can alter this and define a foreign key using the third parameter. For example, if our manager attribute actually contains a uid, we can change this so the related model is retrieved by a UID, instead of a distinguished name:

<?php

use LdapRecord\Models\Model;

class User extends Model
{
    /**
     * Retrieve the manager of the current user.
     */
    public function manager()
    {
        return $this->hasOne(User::class, 'manager', 'uid');
    }
}

Has Many

Defining a has many relationship indicates that the model can be apart of many of the given model.

For example, a User "has many" groups:

<?php

use LdapRecord\Models\Model;

class User extends Model
{
    /**
     * Retrieve the groups the user is apart of.
     */
    public function groups()
    {
        return $this->hasMany(Group::class, 'member');
    }
}

In the above example, LdapRecord will construct a query to locate all of the groups that the user is apart of using the users distinguished name. This users distinguished name will automatically be escaped to be able to properly locate all of the groups.

For example, this is the query filter that will be used in the search:

(member=cn\3dJohn Doe\2cdc\3dacme\2cdc\3dorg)

If you're using an alternate LDAP server or a different attribute to locate group membership, you may change the relation key. For example, you may want to use uniquemember for this relationship:

/**
 * Retrieve the groups the user is apart of.
 */
public function groups()
{
    return $this->hasMany(Group::class, 'uniquemember');
}

You may also define a foreign key in third parameter if the attribute you are using is not a distinguished name.

Has Many (Inverse)

Now that we have setup a User model that can access of their groups, lets define a Group model to be able to access its members.

Since an LDAP group can contain various types of objects (such as contacts, users, and other groups), we must pass in an array of models that are potential members of the group. This allows the relationship to properly create the models that are returned from the query results.

LdapRecord will return plain Entry models when it cannot locate the correct model in the given array.

<?php

use LdapRecord\Models\Model;

class Group extends Model
{
    /**
     * Retrieve the members of the group.
     */
    public function members()
    {
        return $this->hasMany([
            Group::class, User::class, Contact::class
        ], 'memberof')->using($this, 'member');
    }
}

For brevity, we have not shown the creation of the Contact model.

You can see from the above example, we have passed an array of models that are possible members of the group. The difference of this definition is the usage of the using() method.

Since LDAP does not offer bi-directional relationships, we must add the using() method. This method defines which model and attribute to use for attaching and detaching related models.

In this case, we pass in $this to indicate that the current model instance (the Group) contains the member attribute to add and remove models you pass into the attach() and detach() methods.

This method is paramount to be able to properly utilize this relationship.

When querying the above relationship, LdapRecord will construct the following filter:

(memberof=cn\3dAccounting\2cdc\3dacme\2cdc\3dorg)

Has Many In

The has many in relationship allows you to retrieve related models from the given parent models virtual attribute such as memberof.

Since this relationship uses virtual attributes, you cannot use attach() or detach() methods. This also means that for each entry that is contained in the virtual attribute, they will be queried for individually which can be very resource intensive depending on the group size.

Lets define a groups() relationship that utilizes the hasManyIn() method:

<?php

use LdapRecord\Models\Model;

class User extends Model
{
    public function groups()
    {
        return $this->hasManyIn(Group::class, 'memberof');
    }
}

Important Note for Querying

When using the above relationship from query results, you must ensure you select the LDAP property you have defined as the foreign key in the relationship. This attribute contains the values needed to locate the related models.

For example, the following relationship query below will return no results because we have explicitly requested attributes excluding memberof:

// Selecting only the 'cn', and 'sn' attributes:
$user = User::select(['cn', 'sn'])->find('cn=John Doe,dc=acme,dc=org');

// Returns an empty collection.
$groups = $user->groups()->get();

Querying Relationships

LdapRecord relationships also serve as query builders. This means you can chain query builder methods onto relationship methods to add constraints to the relationship query prior to retrieving the results from your directory.

For example, lets define a User model that can be a member of many groups:

<?php

use App\Group;
use LdapRecord\Models\Model;

class User extends Model
{
    /**
     * Retrieve groups that the current user is apart of.
     */
    public function groups()
    {
        return $this->hasMany(Group::class, 'member');
    }
}

Now, lets retrieve a user's groups, but only return those groups that have a common name starting with 'Admin':

$user = User::find('cn=John Doe,dc=acme,dc=org');

$adminGroups = $user->groups()->whereStartsWith('cn', 'Admin')->get();

By default, querying relations will not include recursive results. More on this below.

Recursive Queries

To request all of the relationships results, such as nested groups in groups, call the recursive() method, prior to retrieving results via get():

$user = User::find('cn=John Doe,dc=acme,dc=org');

$allGroups = $user->groups()->recursive()->get();

Be careful when calling recursive on large sets of group memberships. If you are not careful, you could run out of memory due to thousands of models being returned.

The recursive method sets a flag on the LdapRecord relationship indicating you would like recursive results included (groups of groups).

Recursive results are gathered by first retrieving the groups that the user is a member of, then retrieving the groups that are members of each resulting parent group. This means an LDAP search query is executed for each group that your user is apart of.

Circular group dependencies are rejected automatically to prevent infinite looping.

Attaching & Detatching Relationships

Using relationships you define, you can easily attach and detach related models from each other. For example, you may want to attach a Group to a User, or vice-versa.

Attaching

Using the above relationship examples, lets walk through attaching a user to a group:

$user = User::find('cn=John Doe,dc=acme,dc=org');
$group = Group::find('cn=Accounting,dc=acme,dc=org');

// Attaching a group to a user:
$user->groups()->attach($user);

// Attaching a user to a group:
$group->members()->attach($user);

You may also use the attachMany() method to attach many models at once.

For this example, let's say we have an organizational unit that contains groups all new users must be apart of:

$ou = OrganizationalUnit::find('ou=Groups,dc=acme,dc=org');

$groups = Group::in($ou)->get();

$user = User::find('cn=John Doe,ou=Users,dc=acme,dc=org');

$user->groups()->attachMany($groups);

As you can see above, we took a complex LDAP operation and completed it in just 4 lines of code.

Detach

Using the above relationship examples, lets walk through detaching a user from a group:

$user = User::find('cn=John Doe,dc=acme,dc=org');

// Retrieve the first group that the user is apart of:
$group = $user->groups()->get()->first();

$user->groups()->detach($group);

You may also want to detach a user from all groups, if for example they are leaving the company and you it is apart of your off-boarding process.

You may accomplish this task by using the detachAll() method:

$user = User::find('cn=John Doe,ou=Users,dc=acme,dc=org');

$user->groups()->detachAll();

Checking Relationship Existence

To check if a model exists inside of a relationship, use the exists() relationship method.

If you're using Active Directory and are simply looking to check if a user is inside of a particular group, utilize the Model::whereMemberOf method that is available on all Active Directory models to locate users whom are members of that group.

For example, lets determine if a User is a member of a Group:

$user = User::find('cn=John Doe,dc=acme,dc=org');
$group = Group::find('cn=Accounting,dc=acme,dc=org');

if ($user->groups()->exists($group)) {
    // This user is a member of the 'Accounting' group.
}

This method can be used on all relationship types.

For another example, lets determine if a User is a manager of another:

$user = User::find('cn=John Doe,dc=acme,dc=org');
$manager = User::find('cn=Jane Doe,dc=acme,dc=org');

if ($user->manager()->exists($manager)) {
    // Jane Doe is John Doe's manager.
}

You can also determine if the model has any groups or members by simply calling exists():

$user = User::find('cn=John Doe,dc=acme,dc=org');

if ($user->manager()->exists()) {
    // This user has a manager.
}

if ($user->groups()->exists()) {
    // This user is a member of at least one group.
}
Generated on March 17, 2024
Edit on GitHub