Эта статья была рецензирована Юнесом Рафи . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!
Вероятно, вы использовали разные типы отношений между моделями или таблицами базы данных, как это обычно бывает в Laravel: один-к-одному, один-ко-многим, многие-ко-многим и имеет-много-сквозной. Но есть другой тип отношений, который не так распространен: полиморфный. Так что же такое полиморфные отношения?
Полиморфные отношения — это когда модель может принадлежать нескольким другим моделям в одной ассоциации.
Чтобы прояснить это, давайте создадим воображаемую ситуацию, в которой у нас есть модель « Topic
и « Post
. Пользователи могут оставлять комментарии как по темам, так и по сообщениям. Используя полиморфные отношения, мы можем использовать одну таблицу comments
для обоих этих сценариев. Удивительно, да? Это кажется немного непрактичным, так как в идеале нам нужно было бы создать таблицу topic_comments
таблицу topic_comments
для разграничения комментариев. С полиморфными отношениями нам не нужны две таблицы. Давайте рассмотрим полиморфные отношения на практическом примере.
Что мы будем строить
Мы будем создавать демонстрационное музыкальное приложение, в котором есть песни и альбомы. В этом приложении у нас будет возможность повысить голосование за песни и альбомы. Используя полиморфные отношения, мы будем использовать одну таблицу upvotes для обоих этих сценариев. Сначала давайте рассмотрим структуру таблицы, необходимую для построения этого отношения:
albums id - integer name - string songs id - integer title - string album_id - integer upvotes id - integer upvoteable_id - integer upvoteable_type - string
Давайте поговорим о upvoteable_id
и upvoteable_type
которые могут показаться немного upvoteable_type
тем, кто раньше не использовал полиморфные отношения. upvoteable_id
будет содержать значение идентификатора альбома или песни, а столбец upvoteable_type
будет содержать имя класса модели-владельца. upvoteable_type
— это то, как ORM определяет, какой «тип» модели-владельца будет возвращаться при доступе к отношению upvoteable
.
Генерация моделей наряду с миграциями
Я предполагаю, что у вас уже есть приложение Laravel, которое запущено и работает. Если нет, то этот премиум курс быстрого старта может помочь. Давайте начнем с создания трех моделей и миграций, а затем отредактируем миграции в соответствии с нашими потребностями.
php artisan make:model Album -m php artisan make:model Song -m php artisan make:model Upvote -m
Обратите внимание, что передача флага -m
при создании моделей приведет к миграции, связанной с этими моделями. Давайте настроим метод up
в этих миграциях, чтобы получить желаемую структуру таблицы:
{} some_timestamp _create_albums_table.php
public function up() { Schema::create('albums', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->timestamps(); }); }
{} some_timestamp _create_songs_table.php
public function up() { Schema::create('songs', function (Blueprint $table) { $table->increments('id'); $table->string('title'); $table->integer('album_id')->unsigned()->index(); $table->timestamps(); $table->foreign('album_id')->references('id')->on('album')->onDelete('cascade'); }); }
{} some_timestamp _create_upvotes_table.php
public function up() { Schema::create('upvotes', function (Blueprint $table) { $table->increments('id'); $table->morphs('upvoteable'); // Adds unsigned INTEGER upvoteable_id and STRING upvoteable_type $table->timestamps(); }); }
Теперь мы можем запустить команду artisan migrate
чтобы создать три таблицы:
php artisan migrate
Давайте теперь настроим наши модели, чтобы принять во внимание полиморфную связь между альбомами, песнями и голосами против:
Приложение / Upvote.php
[...] class Upvote extends Model { /** * Get all of the owning models. */ public function upvoteable() { return $this->morphTo(); } }
Приложение / Album.php
class Album extends Model { protected $fillable = ['name']; public function songs() { return $this->hasMany(Song::class); } public function upvotes() { return $this->morphMany(Upvote::class, 'upvoteable'); } }
Приложение / Song.php
class Song extends Model { protected $fillable = ['title', 'album_id']; public function album() { return $this->belongsTo(Album::class); } public function upvotes() { return $this->morphMany(Upvote::class, 'upvoteable'); } }
Метод upvotes
как в моделях Album
и в Song
определяет полиморфную связь «один ко многим» между этими моделями и моделью Upvote
и поможет нам получить все upvotes для экземпляра этой конкретной модели.
Определив отношения, мы можем теперь поиграть с приложением, чтобы лучше понять, как работают полиморфные отношения. Мы не будем создавать никаких представлений для этого приложения, мы просто поработаем с нашим приложением из консоли.
Если вы думаете о контроллерах и о том, где мы должны разместить метод upvote
, я предлагаю создать AlbumUpvoteController
и SongUpvoteController
. Этим мы как бы привязываем вещи к тому, с чем мы работаем при работе с полиморфными отношениями. В нашем случае мы можем проголосовать как за альбомы, так и за песни. Upvote не является частью альбома и не является частью песни. Кроме того, это не общая проблема upvote, в отличие от того, как у нас был бы UpvotesController
в большинстве отношений один-ко-многим. Надеюсь, это имеет смысл.
Давайте запустим консоль:
php artisan tinker >>> $album = App\Album::create(['name' => 'More Life']); >>> $song = App\Song::create(['title' => 'Free smoke', 'album_id' => 1]); >>> $upvote1 = new App\Upvote; >>> $upvote2 = new App\Upvote; >>> $upvote3 = new App\Upvote; >>> $album->upvotes()->save($upvote1) >>> $song->upvotes()->save($upvote2) >>> $album->upvotes()->save($upvote3)
Восстановление отношений
Теперь, когда у нас есть некоторые данные, мы можем получить доступ к нашим отношениям через наши модели. Ниже приведен скриншот данных в таблице upvotes :
Чтобы получить доступ ко всем upvotes для альбома, мы можем использовать динамическое свойство upvotes:
$album = App\Album::find(1); $upvotes = $album->upvotes; $upvotescount = $album->upvotes->count();
Также можно извлечь владельца полиморфного отношения из полиморфной модели, обратившись к имени метода, который выполняет вызов morphTo
. В нашем случае это метод upvoteable
на модели Upvote. Итак, мы будем обращаться к этому методу как к динамическому свойству:
$upvote = App\Upvote::find(1); $model = $upvote->upvoteable;
Отношение upvoteable
в модели Upvote вернет экземпляр Album
поскольку это upvote принадлежит экземпляру экземпляра Album
.
Так как можно получить количество голосов для песни или альбома, мы можем отсортировать песни или альбомы на основе голосов на представлении. Вот что происходит в музыкальных чартах.
В случае с песней мы бы получили такие отзывы:
$song = App\Song::find(1); $upvotes = $song->upvotes; $upvotescount = $song->upvotes->count();
Пользовательские Полиморфные Типы
По умолчанию Laravel будет использовать полное имя класса для хранения типа связанной модели. Например, учитывая приведенный выше пример, в котором Upvote
может принадлежать Album
или Song
, upvoteable_type
по умолчанию будет соответственно App\Album
или App\Song
.
Однако в этом есть один большой недостаток. Что если пространство имен модели Album
изменится? Нам придется выполнить какую-то миграцию, чтобы переименовать все вхождения в таблице upvotes
. И это немного хитроумно! Также, что происходит в случае длинных пространств имен (таких как App\Models\Data\Topics\Something\SomethingElse
)? Это означает, что мы должны установить максимальную длину столбца. И здесь нам на MorphMap
приходит метод MorphMap
.
Метод morphMap проинструктирует Eloquent использовать произвольное имя для каждой модели вместо имени класса:
use Illuminate\Database\Eloquent\Relations\Relation; Relation::morphMap([ 'album' => \App\Album::class, 'song' => \App\Song::class, ]);
Мы можем зарегистрировать morphMap
в функции загрузки нашего AppServiceProvider
или создать отдельного поставщика услуг. Чтобы новые изменения вступили в силу, мы должны выполнить команду composer dump-autoload
. Итак, теперь мы можем добавить эту новую запись upvote:
[ "id" => 4, "upvoteable_type" => "album", "upvoteable_id" => 1 ]
и он будет вести себя точно так же, как и в предыдущем примере.
Вывод
Даже если вы, вероятно, никогда не сталкивались с ситуацией, которая требовала от вас использования полиморфных отношений, этот день, вероятно, в конечном итоге наступит. Хорошая вещь при работе с Laravel — это то, что действительно легко справиться с этой ситуацией, не прибегая к каким-либо хитростям по связыванию моделей, чтобы все заработало. Laravel даже поддерживает полиморфные отношения «многие ко многим». Вы можете прочитать больше об этом здесь .
Я надеюсь, что вы теперь поняли полиморфные отношения и ситуации, которые могут потребовать такого рода отношений. Другой, немного более сложный пример полиморфных отношений доступен здесь . Если вы нашли это полезным, пожалуйста, поделитесь с друзьями и не забудьте нажать кнопку «Нравится». Не стесняйтесь оставлять свои мысли в разделе комментариев ниже!