Статьи

Повторное введение Eloquent в полиморфные соотношения

Эта статья была рецензирована Юнесом Рафи . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!


Laravel логотип

Вероятно, вы использовали разные типы отношений между моделями или таблицами базы данных, как это обычно бывает в 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 даже поддерживает полиморфные отношения «многие ко многим». Вы можете прочитать больше об этом здесь .

Я надеюсь, что вы теперь поняли полиморфные отношения и ситуации, которые могут потребовать такого рода отношений. Другой, немного более сложный пример полиморфных отношений доступен здесь . Если вы нашли это полезным, пожалуйста, поделитесь с друзьями и не забудьте нажать кнопку «Нравится». Не стесняйтесь оставлять свои мысли в разделе комментариев ниже!