Полиморфные связи в 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 может быть моделью для сообщений, почты или страниц.
Общая концепция использования полиморфных связей решает проблему определения сходства между тем, что может понадобиться двум или более моделям, и основывается на этом вместо того, чтобы дублировать и создавать многочисленные таблицы и код.
Резюме
Мы обсудили основное применение полиморфных связей и возможные варианты их использования. Следует также отметить, что полиморфные отношения не являются серебряной пулей для всего и должны использоваться только тогда, когда это удобно или кажется правильным.