Relationships

Relationship is the mechanism which associates tables by model and accesses values through properties.

Index

Basic Relationship

Let's learn how to define and use basic relations.

one-to-one

The table definitions used in this example are following:

-------------------------
customers table
 int    : id
 string : name
 int    : group_id
 string : phone

 primarykey : id
 index      : group
-------------------------
addresses table
 int    : customer_id
 string : zip
 string : state
 string : city
 string : street

 primarykey : customer_id
-------------------------
groups table
 int    : id
 string : name

 primarykey : id
-------------------------

Define Address association on Customer model:

clss Address extends Model
{
}

class Customer extends Model
{
    public function address()
    {
        return $this->hasOne('App\Address');
    }
}

Retrieving address.

$address = $customer->address;
if ($address !== null) {
    echo $address->state;
}

Note: $customer->address is a property ! When you access to address property, the address method will be called to get the object, and set it in the property. The code similar to following will be executed automatically:

function __get($name)
{
    if ($name === 'address') {
         $relation = $customer->address();
         $model = $relation->get($this);
         if ($model !== null) {
              $customer->address = $model;
              return $customer->address;
         }
         return null;
    }
}

If the object was not found, it returns null.

hasOne

The prototype of hasOne method is:

hasOne(
        string                  $className,
        integer|string|string[] $foreignKey = null,
        string[]                $keyValuePropertyNames = null,
        boolean                 $optimize = false
      )

$className

Specify associated model class name, with complete name including namespaces.

$foreignKey

The default value of $foreignKey is (model name in lowercase) + _id. In this case, it is customer_id. If the field name is not suitable, you can specify it directly:

public function address()
{
    return $this->hasOne('App\Address', 'id');
}

What is really needed for searching in addresses table is the index number. You can specify the number of the key.

public function address()
{
    return $this->hasOne('App\Address', 0);
}

$keyValuePropertyNames

The properties which have same names as the names of primary key fields is used for searching. If you want to use different properties, you can specify them directly:

public function address()
{
    return $this->hasOne('App\Address', 0, 'id');
}

one-to-many

The customer belongs to one of the groups. The model is following:

class Group extends Model
{
    public function customers()
    {
         return $this->hasMany('App\Customer');
    }
}

hasMany

Parameters of hasMany are same as hasOne.

Retrieving customers which belongs to the group ID 1.

$group = Group::find(1);
$customers = $group->customers;
echo count($customers);

If objects were not found, it returns an empty Collection object.

belongs-to

The opposite of hasOne and hasMany is belongsTo.

Note: belongsTo method name must be different from property names in the Model.

clss Address extends Model
{
    function customer()
    {
        return $this->belongsTo('App\Customer');
    }
}

class Customer extends Model
{
    // (omitted)

    public function group()
    {
        return $this->belongsTo('App\Group');
    }
}

belongsTo

The prototype of belongsTo is:

belongsTo(
          string                  $className,
          string[]                $keyValuePropertyNames = null,
          integer|string|string[] $otherKey = 'id',
          boolean                 $optimize = true
         )

$keyValuePropertyNames

The property (model name in lowercase) + _id is used for searching. If you want to use different properties, you can specify them directly:

public function group()
{
    return $this->belongsTo('App\Group', 'group_id');
}

$otherKey

The default value of $otherKey is id. If you want to use different property names, you can specify them directly.

public function group()
{
    return $this->belongsTo('App\Group', null, 'code');
}

Also, you can specify the number of the key.

public function group()
{
    return $this->belongsTo('App\Group', null, 0);
}

Show group name the customer belongs to

$customer = Customer::find(1);
echo $customer->group->name;

many-to-many

Let's make it possible that multiple tags can be attached to a customer. Prepare tags in the tags table.

The many-to-many relationship is defined with intermediate table. For this example, tag_customer is an intermediate table.

The table definitions:

-------------------------
tags table
 int    : id
 string : name

 primarykey : id
-------------------------
tag_customer table
 int    : tag_id
 int    : customer_id

 primarykey : tag_id, customer_id
 index : customer_id, tag_id
-------------------------

The models are following:

class Tag extends Model
{
}

// Must define an intermediate class
class TagCustomer extends Model
{
    // Converting table name to snake_case is not supported.
    protected static $table = 'tag_customer';
}

class Customer extends Model
{
    // (omitted)

    public function tags()
    {
        return $this->belongsToMany('App\Tag');
    }
}

belongsToMany

Let's look at omitted parameters.

belongsToMany(
               string                  $className,
               string                  $IntermediateClass = null,
               integer|string|string[] $foreignKey = null,
               string[]                $intermediatePropertyNames = null
             )

If you specify them explicitly, it becomes:

class Customer extends Model
{
    // (omitted)

    public function tags()
    {
        return $this->belongsToMany('App\Tag', 'App\TagCustomer', 'customer_id', 'tag_id');
    }
}

Get the list of tags attached to a customer

$customer = Customer::find(1);
$tags = $customer->tags;
foreach($tags as $tag) {
    // (omitted)
}

How was it handled? See the code below:

// This is a pseudocode.
class Customer extends Model
{
    // (omitted)

    public function tags()
    {
        $relation = $this->hasMany('App\TagCustomer', 'customer_id', 'id');
        $tagCustomers = $relation->get($this);
        $results = array();
        $relation2 = TagCustomers::belongsTo('App\Tag', 'tag_id', 'id'); // static belongsTo actually does not exist.
        foreach($tagCustomers as $row) {
            $results[] = $relation2->get($row);
        }
        $this->tags = $results;
    }
}

Get the list of customers from the tag

class Tag extends Model
{
    public function customers()
    {
        return $this->belongsToMany('App\Customer');
    }
}
$tag = Tag::find(1);
$customers = $tag->customers;
foreach($customers as $customer) {
    // (omitted)
}

Performance

Lazy loading

Relationship methods access to the database for the first time when the property is read.

Eager loading

In general, there is no problem with lazy loading. However, if there are many objects, there is a problem of increasing the number of database accesses.

In this case, with method can resolve the relation of objects in collection with a single access. Specify the method name of the relationships for with.

$customers = Customer::with('group')->all();
echo $customer[0]->group->name; // No database access

with is similar to algorithm of JOIN. In the Transactd PHP ORM, you can use two algorithms nested-loop and sort-merge. These two are used properly as follows:

You can choose algorithms by $optimize parameter.

group_id is often the same for multiple customers, so use sort-merge.

public function group()
{
    return $this->belongsTo('App\Group', 'group_id', 0, true /* sort-merge */);
}

The address differs for each customer, so use nested-loop.

public function address()
{
    return $this->hasOne('App\Address', 0, 'id', false /* nested-loop */);
}

$optimize parameter only affect to with method.

Note: with method can not be used with findXX or firstXX methods. It is effective for retrieving models with get or all methods.

Whichever algorithm you select, the result is the same. Only performance is different.

Eager loading on Collection

The result Collection can use Eager loading too.

Use loadRelations to read objects associated with items in the collection at once.

$customers = Customer::all();
$customers->loadRelations(['address', 'group']); // load objects associated with address() and group()

Easy Relationship Definition

Above definitions are very similer to Laravel 5. This way is the best if there are tables that follows the rules of ActiveRecord.

However, relation method is more simple way, which can be used regardless of the table definition rules. All of one-to-one, one-to-many, many-to-many and belongs-to relationships are substitutable with it.

public function address()
{
    return $this->relation($className, $index, $keyValuePropertyNames, $optimize);
}

Any relationships can be defined with $className, $index and $keyValuePropertyNames.

For belongsToMany relationship, use addSubRelation method like bellow:

class Tag extends Model
{
    public function customers()
    {
        return $this->relation('App\TagCustomer', 0, 'id')
                ->addSubRelation('App\Customer', 0, 'customer_id');
    }
}

Polymorphic Relationship

Polymorphic Relationship is an association from one table to multiple tables.

Polymorphic one-to-one

address table stores not only address of customers, but also the address of vendors.

-------------------------
addresses table
 string : owner_type
 int    : owner_id
 string : zip
 string : state
 string : city
 string : street

 primarykey : owner_type, owner_id
-------------------------
vendors table
 int    : id
 string : name

 primarykey : id
-------------------------
class Vendor extends Model
{
    public function address()
    {
        return $this->morphOne('App\Address', 'owner');
    }
}

owner_type field is holds class name of owner_id with namespace.

You can use relation method too.

class Vendor extends Model
{
    public function address()
    {
        return $this->relation('App\Address', 0, ['[App\Vendor]', 'id']);
    }
}

In this case, always use the fixed value App\Vendor for owner_type field. To specify a fixed value instead of a property name, enclose it with [].

Definition of the opposite relationship

clss Address extends Model
{
    function owner()
    {
        return $this->morphTo('owner');
    }
    // or
    function owner()
    {
        // $className is unknown, so set to null.
        return $this->relation(null, 0, ['owner_type', 'owner_id']);
    }
}

Show owner names of addresses

$adresses = Address::all();
foreach($adresses as $address) {
   echo $address->owner->name;
}

However, holding class names as string type is not a usual way. Usually, classes are saved as integer code. In this case, pass an array of class names to convert class codes to names.

// When type of 'owner_type' field in 'addresses' table is integer.
clss Address extends Model
{
    protected static $classTypeMap = [1 => 'App\Vendor', 2 => 'App\Customer'];
    function owner()
    {
        return $this->morphTo('owner', null, null, self::$classTypeMap);
    }
    // or
    function owner()
    {
        // $className is unknown, so set to null.
        return $this->relation(null, 0, ['owner_type', 'owner_id'])
                   ->setMorphClassMap(self::$classTypeMap);
    }
}

Polymorphic one-to-many

Let's make it possible that multiple addresses can be attached to customers or vendors.

-------------------------
addresses table
 int    : id
 string : owner_type
 int    : owner_id
 string : zip
 string : state
 string : city
 string : street

 primarykey : id
 index : owner_type, owner_id (duplicatable)
-------------------------
class Customer extends Model
{
    // (omitted)

    public function address()
    {
         return $this->morphMany('App\Address', 'owner');
    }
    // or
    public function address()
    {
        // The index numbers are different.
        return $this->relation('App\Address', 1, ['[App\Customer]', 'id']);
    }
}

The opposite relationship is the same as morphOne.

Polymorphic many-to-many

Let's make it possible also that multiple tags can be attached to a vendor.

-------------------------
taggables table
 string : taggable_type
 int    : taggable_id
 int    : tag_id

 primarykey : taggable_type, taggable_id, tag_id
 index      : tag_id, taggable_type, taggable_id
-------------------------
class Taggable
{
}

class Tag extends Model
{
    // Get all customers which have this tag.
    public function customers()
    {
        return $this->morphedByMany('App\Customer', 'taggable', 'App\Taggable');
    }

    // Get all vendors which have this tag.
    public function vendors()
    {
        return $this->morphedByMany('App\Vendor', 'taggable', 'App\Taggable');
    }

    // or

    // Get all customers which have this tag.
    public function customers()
    {
        return $this->relation('App\Taggable', 1, 'id')
               ->addSubRelation('App\Customer', 0, 'taggable_id');
    }

    // Get all vendors which have this tag.
    public function vendors()
    {
        return $this->relation('App\Taggable', 1, 'id')
               ->addSubRelation('App\Vendor', 0, 'taggable_id');
    }
}

The opposite relationship is:

class Customer extends Model
{
    // (omitted)

    function tags()
    {
        return $this->morphToMany('App\Tag', 'taggable', 'App\Taggable');
    }

    // or

    function tags()
    {
        return $this->relation('App\Taggable', 0, ['[App\Customer]', 'id'])
               ->addSubRelation('App\Tag', 0, 'tag_id');
    }
}

class Vendor extends Model
{
    // (omitted)

    function tags()
    {
        return $this->morphToMany('App\Tag', 'taggable', 'App\Taggable');
    }

    // or

    function tags()
    {
        return $this->relation('App\Taggable', 0, ['[App\Customer]', 'id'])
               ->addSubRelation('App\Tag', 0, 'tag_id');
    }
}

The access modifiers of relationship properties

The properties of the model used in the relation must be public accessible. These propaties will be read and wrote from Relation object. It can be used through getter or setter.

class Tag extends Model
{
    protected $id; // It is OK, because getter/setter of it is public.
    public function customers()
    {
        return $this->relation('App\TagCustomer', 0, 'id')
                ->addSubRelation('App\Customer', 0, 'customer_id');
    }
    public __get($name)
    {
         // The getter of 'id' must exist for relationship, because $this->id is protected.
         if ($name === 'id') {
              return $this->id;
         }
         return parent::__get($name); // Important for relationships.
    }
    public __set($name, $value)
    {
         // The setter of 'id' must exist for relationship, because $this->id is protected.
         if ($name === 'id') {
              $this->id = $value;
         }
    }
}

/* Followings must be 'public' for relationship:
   * Tag::id
   * TagCustomer::customer_id, tag_id (Fields of index number 1)
   * Customer::id
*/