Связи

Содержание:


Введение

Таблицы базы данных часто связаны одна с другой. Например, заметка в блоге может иметь большое количество комментариев или заказ может быть связан с пользователем, который разместил его. Eloquent делает управление и работу с этими отношеними простой. Поддерживаются следующие различные типы отношений:


Определение отношений

Отношения Eloquent определены как методы для ваших Eloquent модели классов. Поэтому. как и сами модели Eloquent, отношения также служат в качестве мощного конструктора запросов, определяя отношения как методы, что открывает большие возможности для составления запросов и использовании цепочки методов. Например, вы можете добавить дополнительные ограничения цепочкой для отношения posts:

$user->posts()->where('active', 1)->get();

Но перед тем как погрузиться глубже в использование отношений, давайте научимся определять каждый тип.

Один-к-одному

Отношение один-к-одному — это самое базовое отношение. Например, модель User (пользователь) может быть связана с одним Phone (телефоном). Чтобы определить эти отношения, мы разместим метод phone для модели User. Метод phone должен вызвать метод hasOne и вернуть его результаты:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Get the phone record associated with the user.
     */
    public function phone()
    {
        return $this->hasOne('App\Phone');
    }
}

Первый аргумент, передаваемый в метод hasOne, является именем связанной модели. После определения связи, мы можем получить связанную связь используя динимические свойства Eloquent. Динамически свойства позволяют вам получить доступ к методом отношения так, словно они были свойствами, определёнными для модели:

$phone = User::find(1)->phone;

Eloquent определяет внешний ключ отношения основанный на имени модели. В этом случае, предполагается , что модель Phone автоматически имеет внушний ключ user_id. Если вы хотите переопределить это соглашение, вы можете передать второй аргумент в метод hasOne:

return $this->hasOne('App\Phone', 'foreign_key');

Дополнительно, Eloquent предполагает, что внешние ключи должны иметь значение, которое соответвует id (или пользовательское $primaryKey) столбцу родителя. Другими словами, Eloquent будет искать значение столбца пользователя id в столбце user_id для записи Phone. Если вы хотите, чтобы отношение использовало значение отличное от id, вы можете передать третий аргумент в метод hasOne указывающий ваш пользовательский ключ:

return $this->hasOne('App\Phone', 'foreign_key', 'local_key');

Определения для второй модели.

Теперь, мы можем получить доступ к модели Phone из User. Теперь, давайте определим отношение для модели Phone, которое позволит нам получить доступ к пользователю User, который владеет телефоном. Мы можем определить обратное hasOne отношение используя метод belongsTo:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Phone extends Model
{
    /**
     * Get the user that owns the phone.
     */
    public function user()
    {
        return $this->belongsTo('App\User');
    }
}

В примере выше, Eloquent будет пытаться соединить user_id из модели Phone с id для модели User. Eloquent определяет имя внешнего ключа путём добавления _id к имени метода. Однако, если внешний ключ для модели Phone не user_id, вы можете передать собственное имя в качестве второго аргумента в метод belongsTo:

/**
 * Get the user that owns the phone.
 */
public function user()
{
    return $this->belongsTo('App\User', 'foreign_key');
}

Если ваша родительская модель не использует id в качестве первичного ключа, или вы хотели присоединить дочернюю модель к другому столбцу, вы можете передать третий аргумент в метод belongsTo, указывая пользовательский ключ вашей родительской таблицы:

/**
 * Get the user that owns the phone.
 */
public function user()
{
    return $this->belongsTo('App\User', 'foreign_key', 'other_key');
}

Один-ко-многим

Отношение один ко многим используется для определения отношений, когда одиночная модель владеет любым количеством других моделей. Например, заметка блога может иметь много комментариев. Как и все другие отношения Eloquent, отношения один ко многим определяется путём размещения функции для вашей модели Eloquent:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * Get the comments for the blog post.
     */
    public function comments()
    {
        return $this->hasMany('App\Comment');
    }
}

Запомните, Eloquent будет автоматически определять нужную колонку внешнего ключа для модели Comment. По соглашению, Eloquent возьмёт "змеиный_регистр" имени модели и добавит суффикс _id. Поэтому, для этого примера, Eloquent будет автоматически считать post_id внешним ключом для модели Comment.

После определения отношения, мы можем получить доступ к коллекции комментариев через свойство comments. В силу того, что Eloquent предоставляет динамические свойства, мы можем использовать методы отношений так, словно они были определены как свойства для модели:

$comments = App\Post::find(1)->comments;

foreach ($comments as $comment) {
    //
}

В силу того, что отношения выступают в качестве составной части конструктора запросов, вы можете добавить другие ограничения для получаемых комментариев путём вызова метода comments и продолжения цепочки условий для запроса:

$comment = App\Post::find(1)->comments()->where('title', 'foo')->first();

Как и для метода hasOne, вы можете также переопределить внешние и локальные ключи путём передачи дополнительных аргументов в метод hasMany:

return $this->hasMany('App\Comment', 'foreign_key');

return $this->hasMany('App\Comment', 'foreign_key', 'local_key');

Один-ко-многим (инверсия)

Теперь мы можем получить доступ ко всем комментарием заметки. Давайте определим отношение, чтобы получать доступ через комментарий к родительской заметке. Для определения инверсии отношения hasMany, определим функцию отношение для дочерней модели, которая вызывает метод belongsTo:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * Get the post that owns the comment.
     */
    public function post()
    {
        return $this->belongsTo('App\Post');
    }
}

После определения отношения, мы можем получить модель Post для Comment путём доступа к динамическому свойству post:

$comment = App\Comment::find(1);

echo $comment->post->title;

В примере выше, Eloquent будет пытаться соединить post_id из модели Comment с id для модели Post. Eloquent определяет имя базового внешнего ключа путём проверки имени метода отношений и добовления суффикса с _ и именем колонки первичного ключа. Если внешний ключ для модели Comment не post_id, вы можете передать пользовательское имя ключа в качестве второго аргумента в метод belongsTo:

/**
 * Get the post that owns the comment.
 */
public function post()
{
    return $this->belongsTo('App\Post', 'foreign_key');
}

Если родительская модель не использует id в качестве первичного ключа, или вы хотите присоединить дочернюю модель к другому столбцу, вы можете передать третий аргумент в метод code>belongsTo, указывая ваш пользовательский ключ для таблицы родителя:

/**
 * Get the post that owns the comment.
 */
public function post()
{
    return $this->belongsTo('App\Post', 'foreign_key', 'other_key');
}

Многие-ко-многим

Связь "многие-ко-многим" чуть более сложная чем связь hasOne и hasMany. Примером такой связи может быть пользователь с большим числом ролей, где роли доступны для других пользователей. Например, много пользователей могут иметь роль "Admin". Для определения этих связей, необходимы три таблицы базы данных: users, roles, и role_user. Таблица role_user выводится из имён связанных моделей в алфавитном порядке и содержит столбцы user_id и role_id columns.

Связь многие-ко-многим определяется путём написания метода, который вернёт результаты метода belongsToMany. Например, давайте определим метод roles для нашей модели User:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * The roles that belong to the user.
     */
    public function roles()
    {
        return $this->belongsToMany('App\Role');
    }
}

После определения связи, вы можете получить роли пользователя используя динамическое свойство roles:

$user = App\User::find(1);

foreach ($user->roles as $role) {
    //
}

Как и для других типов связей, вы можете вызвать метод roles, чтобы продолжить цепочку ограничений запроса для отношения:

$roles = App\User::find(1)->roles()->orderBy('name')->get();

Как упоминалось ранее, для определения имени таблицы, которая выступает в качестве объединённой таблицы связей, Eloquent присоединит два связанных имени модели в алфавитном порядке. Но вы можете переопределить такое соглашения. Вы можете сделать так путём передачи второго аргумента в метод belongsToMany:

return $this->belongsToMany('App\Role', 'role_user');

Дополнительно к настройке имени объединённой таблицы, вы можете тажке настроить имена ключей столбцов для таблицы путём передачи дополнительных аргументов в метод belongsToMany. Третий аргумент является именем внешнего ключа модели для которой определено отношение, четвёртый аргумент является именем внешнего ключа модели, которую вы присоединяете:

return $this->belongsToMany('App\Role', 'role_user', 'user_id', 'role_id');

Определения обратной версии связи

Для определения обратного отношения для связи многие-ко-многим, вам необходимо разместить другой вызов belongsToMany для связанной модели. Продолжая пример с пользователями и ролями, давайте определим метод users для модели Role:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    /**
     * The users that belong to the role.
     */
    public function users()
    {
        return $this->belongsToMany('App\User');
    }
}

Как вы можете видеть, отношение определено таким же образом, как и для User, с различием в том, что ссылаемся на модель App\User. В силу того, что мы повторно используем метод belongsToMany, все обычные таблицы и ключевые опции кастомизации доступны при определении обратной версии связи многие-ко-многим.

Получение промежуточных столбцов таблицы

Как вы уже поняли, работа с отношениями многие-ко-многим требуют присутсвия дополнительной таблицы. Eloquent предоставляет несколько полезных способов взаимодействия с этой таблицей. Например, давайте предположим, что наш объект User располагает большим количествоам объектов Role, с которыми они и свзяны. После получения доступа к этому отношению, мы можем получить доступ к промежуточной таблице используя атрибут pivot для моделей:

$user = App\User::find(1);

foreach ($user->roles as $role) {
    echo $role->pivot->created_at;
}

Заметьте, что для каждой модели Role, которую мы получаем, автоматически приписывается атрибут pivot. Этот атрибут содержит модель, представляющую промежуточную таблицу, и можно использовать как любую другую модель Eloquent.

По умолчанию, только ключи модели будут присутсвовать в объекте pivot. Если сводная таблица содержит дополнительные атрибуты, вы должны указать их при использовании этой связи:

return $this->belongsToMany('App\Role')->withPivot('column1', 'column2');

Если вы хотите чтобы ваша сводная таблица поддерживала метки времени created_at и updated_at, используйте метод withTimestamps для определения связи:

return $this->belongsToMany('App\Role')->withTimestamps();

Настройка сводных атрибутов имени

Как упоминалось ранее, атрибуты из дополнительной таблицы могут быть получены в модели используя атрибут pivot. Вы можете настроить имя этого атрибута, чтобы лучше показать его цели внутри вашего приложения.

Например, если ваше приложение содержит пользователей, которые могут подписать на подкасты, то вы скорее всего имеете связь многие-ко-многим между пользователями и подкастами. В этом случае, вы можете захотеть переименовать средство доступа для вашей дополнительной таблицы на subscription вместо pivot. Это можно сделать использую метод as при определении отношения:

return $this->belongsToMany('App\Podcast')
                ->as('subscription')
                ->withTimestamps();

После того как это сделано, вы можете получить доступ к данным дополнительной таблицы используя пользовательское имя:

$users = User::with('podcasts')->get();

foreach ($users->flatMap->podcasts as $podcast) {
    echo $podcast->subscription->created_at;
}

Фильтрация связей через столбцы промежуточной таблицы

Вы можете также отфильтровать результаты возвращаемые belongsToMany используя методы wherePivot и wherePivotIn при определении связи:

return $this->belongsToMany('App\Role')->wherePivot('approved', 1);

return $this->belongsToMany('App\Role')->wherePivotIn('priority', [1, 2]);

Определение пользовательских средних моделей таблиц

Если вы хотите определить пользовательскую модель для представления дополнительной таблицы ваших связей, вы можете вызвать метод using при определении связи. Пользовательские многие-ко-многим сводные модели должны расширять класс Illuminate\Database\Eloquent\Relations\Pivot, в то время как пользовательские полиморфные модели многие-ко-многим должны расширять класс Illuminate\Database\Eloquent\Relations\MorphPivot. Например, мы можем определить класс Role, который использует пользовательску сводную модель RoleUser:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    /**
     * The users that belong to the role.
     */
    public function users()
    {
        return $this->belongsToMany('App\User')->using('App\RoleUser');
    }
}

При определении модели RoleUser, мы будем расширять класс Pivot:

<?php

namespace App;

use Illuminate\Database\Eloquent\Relations\Pivot;

class RoleUser extends Pivot
{
    //
}

Вы можете скомбинировать using и withPivot, чтобы получить столбцы из дополнительной таблицы. Например, вы можете получить столбцы created_by и updated_by из сводной таблицы RoleUser путём передачи имен столбцов в метод withPivot:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    /**
     * The users that belong to the role.
     */
    public function users()
    {
        return $this->belongsToMany('App\User')
                        ->using('App\RoleUser')
                        ->withPivot([
                            'created_by',
                            'updated_by',
                        ]);
    }
}
Сводные модели могут не использовать трейт мягкого удаления SoftDeletes. Если вам необходимо мягко удалить записи сводной таблицы, вам необходимо преобразовать вашу модель в актуальную Eloquent модель.

Пользовательские сводные модели и инкрементные идентификаторы

Если вы определили связь много-ко-многим, которая использует пользовательскую сводну модель, и у этой сводной модели есть автоинкрементный первичный ключ, вы должны убедиться, что класс вашей сводной таблицы определил свойство incrementing, значение которого установлено как true.

/**
 * Indicates if the IDs are auto-incrementing.
 *
 * @var bool
 */
public $incrementing = true;

К-одному-через

Связь "к-одному-через" связывает модели через промежуточное отношение. Например, если у каждого поставщика есть один пользователь, и каждый пользователь ассоциирован с одной записью истории пользователя, тогда каждая модель поставщика может получить доступ к истории пользователя через самого пользователя. Давайте взглянем на таблицы базы данных, необходимых для определения этого отношения:

users
    id - integer
    supplier_id - integer

suppliers
    id - integer

history
    id - integer
    user_id - integer

Хотя таблица history не содержит столбца supplier_id, связь hasOneThrough может обеспечить доступ к истории пользоватяля через модель поставщика. Теперь, после того, как мы изучили структуру таблицы для отношения, давайте определим связь для модели Supplier:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Supplier extends Model
{
    /**
     * Get the user's history.
     */
    public function userHistory()
    {
        return $this->hasOneThrough('App\History', 'App\User');
    }
}

Первый аргумент, передаваемый в метод hasOneThrough является именем конечной модели, доступ к которой мы хотим получить. Второй аргумент является именем промежуточной модели.

Типовое соглашение Eloquent для внешних ключей будет использовано при реализации запросов связи. Если вы хотите настроить ваши ключи для связи, вы можете передавать их как третий и четвёртый аргументы в метод hasOneThrough. Третий аргумент является именем внешнего ключа для средней модели. Четвёртый аргумент является именем внешнего ключа для конечной модели. Кроме того, доступны пятый и шестой аргументы. Пятый аргумент является локальным ключом, а шестой будет локальным ключом для промежуточной модели:

class Supplier extends Model
{
    /**
     * Get the user's history.
     */
    public function userHistory()
    {
        return $this->hasOneThrough(
            'App\History',
            'App\User',
            'supplier_id', // Foreign key on users table...
            'user_id', // Foreign key on history table...
            'id', // Local key on suppliers table...
            'id' // Local key on users table...
        );
    }
}

Ко-многим-через

Отношение "Ко-многим-через" обечпечивает удобный короткий путь для доступа к удалённым связям через промежуточное отношение. Например, модель Country может иметь большое количество моделей Post через промежуточную модель User. В этом примере, вы можете легко собрать все заметки для данной страны. Давайте взглянем на таблицы, необходимые для определения этого отношения:

countries
    id - integer
    name - string

users
    id - integer
    country_id - integer
    name - string

posts
    id - integer
    user_id - integer
    title - string

Хотя posts не содержит столбец country_id, связь hasManyThrough обеспечивает доступ к записям страны через через $country->posts. Для реализации такого запроса, Eloquent проверяет country_id для промежуточной таблицы users. После нахождения соответсвующих ID пользователя, они используются для запроса к таблице posts.

Теперь, после изучения структуры таблицы для отношения, давайте определим связь для модели Country:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Country extends Model
{
    /**
     * Get all of the posts for the country.
     */
    public function posts()
    {
        return $this->hasManyThrough('App\Post', 'App\User');
    }
}

Первый аргумент, передаваемый в метод hasManyThrough является именем конечной модели, к которой мы хотели бы получить доступ. Второй аргумент является именем промежуточной модели.

Типовое соглашение Eloquent для внешних ключей будет использовано для реализации запросов связи. Чтобы настроить ваши пользовательские ключи, вы можете передать из в качестве третьего и четвёртого аргумента в метод hasManyThrough. Третий аргумент является именем для внешнего ключа для промежуточной модели. Четрвёрты аргумент является именем внешнего ключа для конечной модели. Пятый аргумент является локальным ключом, а шестой аргумент будет локальным ключом для промежуточной модели:

class Country extends Model
{
    public function posts()
    {
        return $this->hasManyThrough(
            'App\Post',
            'App\User',
            'country_id', // Foreign key on users table...
            'user_id', // Foreign key on posts table...
            'id', // Local key on countries table...
            'id' // Local key on users table...
        );
    }
}

Полиморфные отношения

Полиморфное отношение позволяют целевой модели принадлежать к нескольким типам связей используя одну ассоциацию.

Полиморфное отношение один-к-одному

Структура таблицы

Один-к-одному полиморфная связь похожа на обыкновенное отношение один-к-одному; однакое, целевая модель может принадлежать к более чем одному типу модели в одной ассоциации. Например, запись блога Post и пользователь User могут разделить полиморфное отношение с моделью Image. Используя один-к-одному полиморфную связь позволит вам получить список уникальных изображений, которые используются и для заметок блога, и для аккаунтов пользователей. Во-первых, давайте изучим структуру таблицы:

posts
    id - integer
    name - string

users
    id - integer
    name - string

images
    id - integer
    url - string
    imageable_id - integer
    imageable_type - string

Нужно обратить внимание на столбцы imageable_id и imageable_type для таблицы images. Столбец imageable_id будет содержать идентификатор заметки или пользователя, в то время как столбец imageable_type будет содержать имя класса родительской модели. Столбец imageable_type используется Eloquent для определения какой "тип" родительской модели необходимо возвращать при обращении к связи imageable.

Структура модели

После чего, проверим определение модели, необходимое для построения этой связи:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Image extends Model
{
    /**
     * Get the owning imageable model.
     */
    public function imageable()
    {
        return $this->morphTo();
    }
}

class Post extends Model
{
    /**
     * Get the post's image.
     */
    public function image()
    {
        return $this->morphOne('App\Image', 'imageable');
    }
}

class User extends Model
{
    /**
     * Get the user's image.
     */
    public function image()
    {
        return $this->morphOne('App\Image', 'imageable');
    }
}

Получение связи

После определения таблиц базы данных и моделей, вы можете получать доступ к связям через ваши модели. Например, чтобы получить изображение для записи, мы можем использовать динамическое свойство image:

$post = App\Post::find(1);

$image = $post->image;

Вы также можете получить родителя через полиморфную модель путём обращения к имени метода, выполняющего вызов MorphTo. В нашем случае, это метод imageable для модели Image. Итак, мы будем обращаться к этому методу как к динамическому свойству:

$image = App\Image::find(1);

$imageable = $image->imageable;

Связь imageable для модели Image будет возвращать экземпляр Post или User в зависимости от того, какой тип модели владеет изображением.

Полиморфное отношение один-ко-многим

Структура таблицы

Полиморфная связь один-ко-многим похожа на простуя свяь один-ко-многим. Однако, целевая модель может принадлежать к более чем одному типу модели в одной ассоциации. Например, представьте, что пользователи могут комментировать и заметки, и видео. Используя полиморфные связи, вы можете использовать одиночную таблицу comments для обоих этих сценариев. Во-первых, давайте определим структуру таблицы, необходимую для построения этого отношения:

posts
    id - integer
    title - string
    body - text

videos
    id - integer
    title - string
    url - string

comments
    id - integer
    body - text
    commentable_id - integer
    commentable_type - string

Структура модели

После чего, давайте обратимся к определению модели, необходимому для построения этого отношения:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * Get the owning commentable model.
     */
    public function commentable()
    {
        return $this->morphTo();
    }
}

class Post extends Model
{
    /**
     * Get all of the post's comments.
     */
    public function comments()
    {
        return $this->morphMany('App\Comment', 'commentable');
    }
}

class Video extends Model
{
    /**
     * Get all of the video's comments.
     */
    public function comments()
    {
        return $this->morphMany('App\Comment', 'commentable');
    }
}

Получение отношения

После определения таблиц базы данных и моделей, вы можете получать связи через модели. Например, чтобы получить все комментарии для заметки, мы можем использовать динамическое свойство comments:

$post = App\Post::find(1);

foreach ($post->comments as $comment) {
    //
}

Вы можете получить владельца полиморфного отношения из модели обратившись к имя метода, выполняющего вызов MorphTo. В нашем случае, это метод commentable для модели Comment. Поэтому, мы будем обращаться к этому методу как к динамическому свойству:

$comment = App\Comment::find(1);

$commentable = $comment->commentable;

Связь commentable для модели Comment вернёт или экземпляр Post или Video, в зависимости от типа модели, владеющего комментарием.

Полиморфное отношение много-ко-многим

Структура таблицы

Полиморфное отношение много-ко-многим чуть более сложное, чем отношения morphOne и morphMany. Например, модель заметки блога Post или code>Video могут разделить полиморфное отношение с моделью Tag. Полиморфное отношение много-ко-многим позволяет вам иметь единый список уникальных тэгов, которые используются и заметках блога, и для видео контента. Первое, давайте проверим структуру таблицы:

posts
    id - integer
    name - string

videos
    id - integer
    name - string

tags
    id - integer
    name - string

taggables
    tag_id - integer
    taggable_id - integer
    taggable_type - string

Структура модели

Следующим шагом мы готовы определить связь для модели. Обе модели Post и Video будут иметь метод tags, который вызывает метод morphToMany в базовом классе Eloquent:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * Get all of the tags for the post.
     */
    public function tags()
    {
        return $this->morphToMany('App\Tag', 'taggable');
    }
}

Определение второй части связи

После, для модели Tag вы должны определить метод для каждой из связанных моделей. Поэтому, для этого примера, мы определим метод posts и метод videos:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{
    /**
     * Get all of the posts that are assigned this tag.
     */
    public function posts()
    {
        return $this->morphedByMany('App\Post', 'taggable');
    }

    /**
     * Get all of the videos that are assigned this tag.
     */
    public function videos()
    {
        return $this->morphedByMany('App\Video', 'taggable');
    }
}

Получение связи

После определения ваших таблиц базы данных и моделей, вы можете воспользоваться связью через ваши модели. Например, чтобы получить все теги заметки, вы можете использовать динамическое свойство tags:

$post = App\Post::find(1);

foreach ($post->tags as $tag) {
    //
}

Вы можете также получить владельца полиморфной связи из полиморфной модели обратившись к имени метода, выполняющего вызов morphedByMany. В нашем случае, это методы posts и videos для модели Tag. Итак, вы сможете использовать эти методы как динамические свойства:

$tag = App\Tag::find(1);

foreach ($tag->videos as $video) {
    //
}

Пользовательские полиморфные типы

По умолчанию, Ларавел будет использовать полное имя класса для хранения типа связанной модели. Например, данная связь один-к-многим, указанная выше, где Comment может принадлежать к Post или Video, базовое commentable_type должно быть App\Post или App\Video, соответсвенно. Однако, вы можете захотеть отделить вашу базу данных от внутренней структуры приложения. В этом случае, вы можете определить карту "morph map", чтобы научить Eloquent использовать пользовательское имя для каждой модели вместо имени класса:

use Illuminate\Database\Eloquent\Relations\Relation;

Relation::morphMap([
    'posts' => 'App\Post',
    'videos' => 'App\Video',
]);

Вы можете регистрировать morphMap в функции boot вашего AppServiceProvider или создать отдельный сервис провайдер.

При добавлении "morph map" в ваше существующее приложение, вам также необходимо изменить значение столбца в базе данных, которое всё ещё содержит полное имя класса.

Запросы отношений

В силу того, что все типы отношений Eloquent определены через методы, вы можете вызвать эти методы, чтобы получить экземпляр связ без фактического выполнения запросов связи. Дополнительно, все типы отношений Eloquent обслуживаются как конструктор запросов, что позволяет задавать дополнитеьные условия цепочкой для связи до момента фактического выполнения SQL для ваше базы данных.

Например, представим систему блога, в которой модель пользователя User имеет много ассоциаций с моделями заметок Post:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Get all of the posts for the user.
     */
    public function posts()
    {
        return $this->hasMany('App\Post');
    }
}

Вы можете обратиться к связи заметки posts и добавить дополнитлеьные ограничения для связи следующим образом:

$user = App\User::find(1);

$user->posts()->where('active', 1)->get();

Вы можете использовать любые методы конструктора запросов для связи. Поэтому, рекомендуется обратиться к соответсвующему разделу документации.

Добавление условий orWhere после обращения к связи

Как показано в примере выше, вы можете легко добавлять дополнительные ограничения к связи, при выполнении запроса. Однако, использовать условия orWhere для связи нужно осторожно, так как orWhere условия будут логически сгруппированы на том же уровне, что и ограничения связи:

$user->posts()
        ->where('active', 1)
        ->orWhere('votes', '>=', 100)
        ->get();

// select * from posts
// where user_id = ? and active = 1 or votes >= 100

В большинстве случаев, лучше использовать группу ограничений, чтобы логически сгруппировать условия внитри скобок:

use Illuminate\Database\Eloquent\Builder;

$user->posts()
        ->where(function (Builder $query) {
            return $query->where('active', 1)
                         ->orWhere('votes', '>=', 100);
        })
        ->get();

// select * from posts
// where user_id = ? and (active = 1 or votes >= 100)

Методы отношений и динамические свойства

Если вам не нужно вводить дополнительных ограничений в запрос связи Eloquent, вы можете получить доступ к отнешению так, словно это свойство. Например, продолжая линию с примером моделей пользователя User и заметки Post, мы можем поулчить все заметки пользователя таким образом:

$user = App\User::find(1);

foreach ($user->posts as $post) {
    //
}

Для динамических свойств действует правило "ленивой загрузки", это означает, что данные связи будут подгружаться только при фактческом обращении к ним. Поэтому, разработчики часто используют "безотложную загрузку", чтобы предварительно загрузить отношение, с которым они будут иметь дело. "Безотложная загрузка" может значительно сократить количество SQL запросов, загружаемых для работы со связями моделей.

Запрос существования отношений

При запросе записи для модели, вы можете наложить ограничения на результаты основываясь на существовании связи. Например, давайте предствим, что вы хотите получить все записи блога, у которых есть как миниму один комментарий. Чтобы это сделать, вы можете передать имя связи в методы has или orHas:

// Retrieve all posts that have at least one comment...
$posts = App\Post::has('comments')->get();

Вы также можете указать оператор и число для дальнейшей настройки запроса:

// Retrieve all posts that have three or more comments...
$posts = App\Post::has('comments', '>=', 3)->get();

Вложенные операторы has могут быть также построены используя пунктуацию с точками. Например, вы можете получить все заметки, в которых есть один комментарий с голосами:

// Retrieve posts that have at least one comment with votes...
$posts = App\Post::has('comments.votes')->get();

Если вам нужны ещё более можные решения, вы можете использовать методы whereHas и orWhereHas, чтобы разместить условия "where" для ваших запросов has. Эти методы позволяют вам добавлять дополнительные пользовательские ограничение к ограничениям связи, таким как проверка контента комментария:

use Illuminate\Database\Eloquent\Builder;

// Retrieve posts with at least one comment containing words like foo%...
$posts = App\Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'foo%');
})->get();

// Retrieve posts with at least ten comments containing words like foo%...
$posts = App\Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'foo%');
}, '>=', 10)->get();

Запрос отсутствия отношений

При получении записей для модели, вы можете захотеть ограничить ваши результаты, основываясь на отсутсвии связи. Например, представим, что мы хотим получить все записи блога, у которых нет комментариев. Чтобы сделать так, вы можете передать имя связи в методы doesntHave и orDoesntHave:

$posts = App\Post::doesntHave('comments')->get();

Если вам нужно еще больше возможностей, вы можете использовать методы whereDoesntHave и orWhereDoesntHave, чтобы наложить условия для ваших запросов doesntHave. Эти методы позволят вам добавлять дополнительные ограничения для связи, такие как проврека контента комментария:

use Illuminate\Database\Eloquent\Builder;

$posts = App\Post::whereDoesntHave('comments', function (Builder $query) {
    $query->where('content', 'like', 'foo%');
})->get();

Вы можете использовать пунктуацию с точками для вложенных связей. Например, следующий запрос получит все заметки с комментариями от авторов, которые не в бане:

use Illuminate\Database\Eloquent\Builder;

$posts = App\Post::whereDoesntHave('comments.author', function (Builder $query) {
    $query->where('banned', 1);
})->get();

Запрос полиморфных отношений

Чтобы сделать запрос существования связи MorphTo, вы можете использовать метод whereHasMorph и соответсвующие ему методы:

use Illuminate\Database\Eloquent\Builder;

// Retrieve comments associated to posts or videos with a title like foo%...
$comments = App\Comment::whereHasMorph(
    'commentable',
    ['App\Post', 'App\Video'],
    function (Builder $query) {
        $query->where('title', 'like', 'foo%');
    }
)->get();

// Retrieve comments associated to posts with a title not like foo%...
$comments = App\Comment::whereDoesntHaveMorph(
    'commentable',
    'App\Post',
    function (Builder $query) {
        $query->where('title', 'like', 'foo%');
    }
)->get();

Вы можете использовать параметр $type, чтобы добавить различные ограничения в зависимости от связанной модели:

use Illuminate\Database\Eloquent\Builder;

$comments = App\Comment::whereHasMorph(
    'commentable',
    ['App\Post', 'App\Video'],
    function (Builder $query, $type) {
        $query->where('title', 'like', 'foo%');

        if ($type === 'App\Post') {
            $query->orWhere('content', 'like', 'foo%');
        }
    }
)->get();

Вместо передачи массива возможных полиморфных моделей, вы можете указать * в качестве маски и позволить Laravel получить все возможные полиморфные типы из базы данных. Ларавел выполнит дополнительный запрос для обеспечения этой операции:

use Illuminate\Database\Eloquent\Builder;

$comments = App\Comment::whereHasMorph('commentable', '*', function (Builder $query) {
    $query->where('title', 'like', 'foo%');
})->get();

Если вы хотите посчитать число результ из связи без реальной загрузки, вы можете использовать метод withCount. Этот метод добавит столбец {relation}_count в конечный результат выборки модели. Например:

$posts = App\Post::withCount('comments')->get();

foreach ($posts as $post) {
    echo $post->comments_count;
}

Вы можете добавить "counts" для нескольких связей, также как и добавить различные ограничения в запросы:

use Illuminate\Database\Eloquent\Builder;

$posts = App\Post::withCount(['votes', 'comments' => function (Builder $query) {
    $query->where('content', 'like', 'foo%');
}])->get();

echo $posts[0]->votes_count;
echo $posts[0]->comments_count;

Для результата подсчёта результатов вы можете придумать псевдоним, позволяя проводить несколько подсчётов для одной и той же связи:

use Illuminate\Database\Eloquent\Builder;

$posts = App\Post::withCount([
    'comments',
    'comments as pending_comments_count' => function (Builder $query) {
        $query->where('approved', false);
    },
])->get();

echo $posts[0]->comments_count;

echo $posts[0]->pending_comments_count;

Если вы используете комбинацию выражений withCount и select, убедитесь, что вызвали withCount после метода select:

$posts = App\Post::select(['title', 'body'])->withCount('comments')->get();

echo $posts[0]->title;
echo $posts[0]->body;
echo $posts[0]->comments_count;

Дополнительно, используя метод loadCount, вы можете загружать подсчёт связи после фактического получения родительской модели:

$book = App\Book::first();

$book->loadCount('genres');

Если вам необходимо установить дополнительные ограничения для отложенной загрузки, вы можете передать массив с ключами отношений, которые необходимо загружать. Значения массива должны быть экземплярами замыкания, которое получит экземплря конструктора запросов:

$book->loadCount(['reviews' => function ($query) {
    $query->where('rating', 5);
}])

Отложенная загрузка

При обращении к связям Eloquent как свойствам, для данных используется "ленивая загрузка". Это значит, что данные связи фактически не подгружаются до момента первого достпупа свойства. Однако, Eloquent может использовать "безотложную загрузку", в момент запроса родительской модели. "Безотложная загрузка" может помочь в решении N + 1 проблемы. Для иллюстрации такой проблемы, давайте предстваим, что модель книги Book связана с моделью автора Author:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    /**
     * Get the author that wrote the book.
     */
    public function author()
    {
        return $this->belongsTo('App\Author');
    }
}

Теперь, давайте получим все книги и их авторов:

$books = App\Book::all();

foreach ($books as $book) {
    echo $book->author->name;
}

Такой перебор будет выполнять 1 запрос для получения всех книг, и запрос для получение автора текущей книги. Поэтому, если у нас 25 книг, такой перебор сгенерирует 26 запросов: 1 для получения книг и еще 25 дополнительных для получения автора конкретной книги.

Однако, мы можем воспользоваться безотложной загрузкой и снизить количество операций до 2-ух. При составлении запроса, вы можете указать какие связи необходимо использовать при помощи метода with:

$books = App\Book::with('author')->get();

foreach ($books as $book) {
    echo $book->author->name;
}

Для этой операции будут выполнены только 2 запроса:

select * from books

select * from authors where id in (1, 2, 3, 4, 5, ...)

Безотложная загурзка нескольких связей

Иногда вы можете захотеть безотложно загрузить несколько различных связей одной операцией. Чтобы это сделать, вы можете передать дополнительные аргументы в метод with:

$books = App\Book::with(['author', 'publisher'])->get();

Вложенная безотложная загрузка

Чтобы безотложно загрузить вложенную связь, вы можете использовать синтаксис с точками. Например, давайте загрузим все книги автора и все персональные контакты автора в одном выражении Eloquent:

$books = App\Book::with('author.contacts')->get();

Вложенная безотложная загрузка связи morphTo

Если хотите воспользоваться безотложной загрузкой для связи morphTo, вы можете использовать метод morphWith. Вы можете комбинировать этот метод с with. Чтобы показать метод morphWith давайте обратимся к следующей модели:

<?php

use Illuminate\Database\Eloquent\Model;

class ActivityFeed extends Model
{
    /**
     * Get the parent of the activity feed record.
     */
    public function parentable()
    {
        return $this->morphTo();
    }
}

В этом примере давайте представим, что модели Event, Photo и Post могут создавать модели ActivityFeed. Давайте также представим, что модели событие Event принадлежат модели календарь Calendar, модели заметок Post принадлежат модели автора Author. Также модели фото Photo ассоциированы с моделями тэгов Tag.

Используя такое отношение моделей и связей, мы можем получить экземпляр модели ActivityFeed и безотложно загрузить все модели parentable и их вложенные связи:

use Illuminate\Database\Eloquent\Relations\MorphTo;

$activities = ActivityFeed::query()
    ->with(['parentable' => function (MorphTo $morphTo) {
        $morphTo->morphWith([
            Event::class => ['calendar'],
            Photo::class => ['tags'],
            Post::class => ['author'],
        ]);
    }])->get();

Безотложная загрузка определённых столбцов

Часто вам могут потребоваться не все столбцы из свзяи, которую вы получаете. По этой причине, Eloquent позволяет вам указывать какие столбцы из отношения вы хотите получить:

$books = App\Book::with('author:id,name')->get();
При использовании данной возможности, вы должны всегда включать столбец id и любые другие столбцы с внешним ключом в список желаемых для получения столбцов.

Безотложная загрузка по умолчанию

Иногда вы можете захотеть всегда загружать некоторые связи при получении модели. Чтобы справиться с такой задачей, вы можете определить свойство $with для модели:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    /**
     * The relationships that should always be loaded.
     *
     * @var array
     */
    protected $with = ['author'];

    /**
     * Get the author that wrote the book.
     */
    public function author()
    {
        return $this->belongsTo('App\Author');
    }
}

Если вы хотите убрать элемент из свойства $with для запроса, вы можете использовать метод without:

$books = App\Book::without('author')->get();

Ограничения отложенных загрузок

Иногда вы можете захотеть не только безотложно загрузить связь, но также указать дополнитлеьные условия для запроса. Приведём пример:

$users = App\User::with(['posts' => function ($query) {
    $query->where('title', 'like', '%first%');
}])->get();

В этом примере, Eloquent будет безотложно загружать заметки, где столбец заметки с заголовком title содержит слово first. Вы можете вызывать другие методы конструктора запросов для дальнейшей настройки:

$users = App\User::with(['posts' => function ($query) {
    $query->orderBy('created_at', 'desc');
}])->get();
Методы конструктора запросов limit и take не следует использовать при построении запросов с безотложной загрузкой.

Ленивая отложенная загрузка

Иногда может потребоваться безотложно загрузить связь после того, как родительская модель уже была получена. Например, это может быть полезным, для принятия динамических решений о том, когда загружать связанные модели:

$books = App\Book::all();

if ($someCondition) {
    $books->load('author', 'publisher');
}

Если вам необходимо установить дополнительные ограничения при построении запроса безотложной загрузки, вы можете передать массив с ключами связи, которую вы хотите загрузить. Значения массива должны быть экземплярами Замыкания, которое получает экзмепляр запроса:

$books->load(['author' => function ($query) {
    $query->orderBy('published_date', 'asc');
}]);

Для загрузки связи только в том случае, когда она не была уже загружена, используйте метод loadMissing:

public function format(Book $book)
{
    $book->loadMissing('author');

    return [
        'name' => $book->name,
        'author' => $book->author->name,
    ];
}

Вложенная ленивая безотложная загрузка & morphTo

Если вы хотите безотложно загружить связь morphTo, также как и вложенные связи в различных сущностях, которые могут быть возвращены этой связью, вы можете использовать метод loadMorph.

Этот метод принимает имя связи morphTo в качестве первого аргумента, и массив пар модель / связь в качестве второго. Чтобы помочь проиллюстрировать этот метод, давайте обративмя к следующей модели:

<?php

use Illuminate\Database\Eloquent\Model;

class ActivityFeed extends Model
{
    /**
     * Get the parent of the activity feed record.
     */
    public function parentable()
    {
        return $this->morphTo();
    }
}

В этом примере, давайте предположим, что модели code>Event, Photo, и Post могут создавать модели ActivityFeed. Дополнительно, давайте предположим, что модели Event принадлежат модели Calendar, а модели Photo ассоциированы с моделями Tag, и модели Post принадлежат модели Author.

Используя такие определения моделей и связей, мы можем получить экземпляр модели ActivityFeed и безотложно загрузить все модели parentable и их соответсвующие вложенные связи:

$activities = ActivityFeed::with('parentable')
    ->get()
    ->loadMorph('parentable', [
        Event::class => ['calendar'],
        Photo::class => ['tags'],
        Post::class => ['author'],
    ]);


Привязка родительских временных полей

Может быть полезно обновить родительские метки времени при обновлении дочерней модели. Такие возможности можно использовать когда модель принадлежит или принадлежит ко многим моделиям. Например, комментарии Comment принадлежат заметкам Post. При обновлении модели комментариев Comment, вы хотите автоматически обновить метки времени для соответсвующей заметки Post. Eloquent позволяет легко справиться с такой задачей. Просто добавьте свойство touches содержащее имена связей с дочерней моделью:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * All of the relationships to be touched.
     *
     * @var array
     */
    protected $touches = ['post'];

    /**
     * Get the post that the comment belongs to.
     */
    public function post()
    {
        return $this->belongsTo('App\Post');
    }
}

Теперь, при обновлении комментария Comment, старшая модель заметки Post также обновит столбец updated_at, что позволяет знать, когда обновлять кэш модели заметки Post:

$comment = App\Comment::find(1);

$comment->text = 'Edit to this comment!';

$comment->save();