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

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

Распространенным примером такого примера является комментарий. Например в блоге комментарии можно добавить конкретному посту или странице. Однако структура комментария остаётся одинаковой, независимо от того, пост это или страница.

В этой статье мы рассмотрим полиморфные связи в Laravel, как они работают и различные варианты их практического использования.

Что такое полиморфные отношения?

Рассматривая приведенный пример, мы имеем две сущности: Post и Page. Чтобы иметь возможность добавить комментарии к каждой из них, мы можем построить структуру базы данных таким образом:

posts:
  id
  title
  content
  
posts_comments:
  id
  post_id
  comment
  date
  
pages:
  id
  body
  
pages_comments:
  id
  page_id
  comment
  date

При таком подходе мы создаем несколько таблиц комментариев - posts_comments и pages_comments, которые делают одно и то же, за исключением того, что для каждой сущности комментариев создаётся отдельная таблица. Хотя, если посмотреть внимательно, то можно увидеть, что таблицы комментариев по своей структуре повторяют друг друга.

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

posts:
  id
  title
  content
  
pages:
  id
  body
  
comments:
  id
  commentable_id
  commentable_type
  date
  body

По определению, полиморфизм - это состояние, при котором одна функция, в даном случае, сущность, может обрабатывать данные разных типов. И это подход, которому мы пытаемся следовать выше. У нас есть две новые колонки: commentable_id и commentable_type.

В приведённом выше примере мы объединили таблицы page_comments и post_comments, заменив колонки post_id и page_id на универсальную колонку commentable_id и commentable_type для получения таблицы комментариев.

Колонка commentable_id будет содержать идентификатор сообщения или страницы. А тип commentable_type будет содержать имя класса модели, которой принадлежит запись.

Тип commentable_type будет хранить нечто наподобие App\Entity\Post, таким образом ORM будет определять, к какой модели принадлежит комментарий, и возвращать нужную сущность при обращении к ней.

Здесь мы имеем три сущности: PostPage и Comments.

Давайте создадим наши миграции:

Schema::create('posts', function (Blueprint $table) {
    $table->increments('id');
    $table->string('title');
    $table->text('content');
});

Schema::create('pages', function (Blueprint $table) {
    $table->increments('id');
    $table->text('body');
});

Schema::create('comments', function (Blueprint $table) {
    $table->increments('id');
    $table->morphs('comment');
    $table->text('body');
    $table->date('date');
});

Код $table->morphs('comment') автоматически создаст две колонки, используя переданный ему текст + "able". Таким образом, это приведет к типу commentable_id и commentable_type.

Далее мы создаём классы модели для наших сущностей:

namespace App\Entity;

use Illuminate\Database\Eloquent\Model;

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

use Illuminate\Database\Eloquent\Model;

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

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * Get all of the models that own comments.
     */
    public function commentable()
    {
        return $this->morphTo();
    }
}

В приведенном выше коде мы объявили наши модели, а также используем два метода, morphMany() и morphTo(), которые помогают нам создать полиморфное отношение между сущностями.

И модель Page, и модель Post имеют функцию comments(), которая возвращает morphMany() к модели Comment. Это указывает на то, что обе они, как ожидается, будут иметь связь один ко многим комментариям.

Модель Comment имеет функцию commentable(), которая возвращает функцию morphTo(), указывающую, что этот класс полиморфно связан с другими моделями.

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

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

// получаем все комментарии для страницы...
$page = Page::find(3);

foreach($page->comments as $comment){
    // тут работаем с комментариями
}

Для получения комментариев к сообщению:

// получаем все комментарии для поста...
$post = Post::find(1);

foreach($post->comments as $comment){
    // тут работаем с комментариями поста
}

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

$comment = Comment::find(23);

// получаем сущность...
var_dump($comment->commentable);

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

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

Сначала мы создадим миграцию для новой модели:

Schema::create('products', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
});

Затем мы создаем класс сущности:

namespace App\Entity;

use Illuminate\Database\Eloquent\Model;

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

И всё. Теперь для сущности Product доступны комментарии, работа с которыми аналогична тому, что и при работе с другими сущностями.

// получаем комментарии для конкретного продукта...
$product = Product::find(3);

foreach($product->comments as $comment){
    // тут мы работаем с комментариями...
}

Другие варианты полиморфных связей

Несколько типов пользователей

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

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

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

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

user:
   id
   name
   email
   avatar
   address
   phone
   experience
   userable_id
   userable_type
   
drivers:
  id
  region
  car_type //manual || automatic
  long_distance_drive
  
cleaners:
  id
  use_chemicals //использование химические вещества в очистке
  preferred_size //предпочтительный размер
  ...

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

Вложения и медиафайлы

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

messages:
  id
  user_id
  recipient_id
  content
  
attachment:
  id
  url
  attachable_id
  attachable_type

В приведенном выше примере тип attachable_type может быть моделью для сообщений, почты или страниц.

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

Резюме

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

Написать комментарий