/ php

Полиморфные связи в Laravel и примеры их использования

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

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

В этой статье мы рассмотрим полиморфные связи в 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 будет определять, к какой модели принадлежит комментарий, и возвращать нужную сущность при обращении к ней.

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

Сущность Post может иметь Комментарии
Сущность Page может так же иметь Комментарии
Комментарии могут принадлежать как к сущности Post, так и Page.

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

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.

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

//file: app/Entity/Post.php
<?php

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');
    }
}


//file: app/Entity/Page.php
<?php

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');
    }
}


//file: app/Entity/Comment.php
<?php

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(13);

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');
});

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

//file: app/Product.php
<?php

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 может быть моделью для сообщений, почты или страниц.

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

Резюме

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