Используем трейты для полиморфных релейшенов в Laravel

от автора

Введение

Поговорим о возможном пременении трейтов вместе с полиморфными отношениями в Laravel.

Содержание статьи:

  1. Описание предметной области
  2. Создание приложения
  3. Возможные структуры БД
  4. Создание сущностей
  5. Использование трейта
  6. Написание тестов

Описание предметной области

Мы будем разрабатывать систему, в которой некие сотрудники и некие команды могут быть прикреплены к проекту. Сущностями предметной области будут сотрудники, команды и проекты: команда состоит из сотрудников, на проект могут быть прикреплены сотрудники и команды. Между командой и сотрудником отношение many-to-many (допустим, что сотрудник может участвовать в разных командах), many-to-many между проектами и сотрудниками, many-to-many между командами и проектами. Для дальнейшего рассмотрения опустим реализацию связи между командой и сотрудниками, сосредоточимся на отношении команд и сотрудников к проекту.

Создание приложения

Приложения на Laravel очень просто создавать, используя пакет-создатель приложений. После его установки создание нового приложения умещается в одну команду:

laravel new system

Возможные структуры БД

Если идти нормализованным путем, то нам понадобится три таблицы для сущностей и ещё три таблицы для связей: сотрудники-команды, сотрудники-проекты, команды-проекты.
Если снизить уровень нормализации, то можно объединить таблицы для связей сотрудник-проект и команда-проект в одну, разделяя тип связи по дополнительному полю с типом (допустим, 1 — сотрудник, 2 — команда).
Идея морф-связей похожа на менее нормализованный вариант, только вместо дополнительного поля с типом используется два — один для имени класса модели, второй для её идентификатора.

Создание сущностей

Нам понадобятся модели, миграции и фабрики для сотрудников, команд, проектов и прикреплений. Команды для создания всего этого:

php artisan make:model Employee -f // модель и фабрика сотрудника php artisan make:model Team -f // модель и фабрика команды php artisan make:model Project -f // модель и фабрика проекта php artisan make:migration CreateEntitiesTables // общая миграция для всех сущностей php artisan make:model Attach -m // модель и миграция прикрепления

После выполнения команды мы получим файлы моделей в App/, файлы миграций в папке database/migrations/ и фабрики в database/factories/.

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

Миграция для сущностей

<?php  use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema;  class CreateEntitesTables extends Migration {     /**      * Run the migrations.      *      * @return void      */     public function up()     {         Schema::create('employees', function (Blueprint $table) {             $table->id();             $table->string('name');             $table->timestamps();         });          Schema::create('teams', function (Blueprint $table) {             $table->id();             $table->string('name');             $table->timestamps();         });          Schema::create('projects', function (Blueprint $table) {             $table->id();             $table->string('name');             $table->timestamps();         });     }      /**      * Reverse the migrations.      *      * @return void      */     public function down()     {         Schema::dropIfExists('employees');         Schema::dropIfExists('teams');         Schema::dropIfExists('projects');     } }

Для полиморфного отношения

<?php  use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema;  class CreateAttachesTable extends Migration {     /**      * Run the migrations.      *      * @return void      */     public function up()     {         Schema::create('attachments', function (Blueprint $table) {             $table->id();             $table->morphs('attachable');             $table->unsignedInteger('project_id');             $table->timestamps();              $table->foreign('project_id')->references('id')->on('projects')                 ->onDelete('cascade');         });     }      /**      * Reverse the migrations.      *      * @return void      */     public function down()     {         Schema::dropIfExists('attachments');     } }

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

Теперь к моделям.
Модель команды идентична модели сотрудника:

<?php  namespace App;  use Illuminate\Database\Eloquent\Model;  class Employee extends Model {     protected $fillable = ['name']; }

<?php  namespace App;  use Illuminate\Database\Eloquent\Model;  class Team extends Model {     protected $fillable = ['name']; }

Модель проекта

<?php  namespace App;  use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphToMany;  class Project extends Model {     protected $fillable = ['name'];      /**      * Relation for project attachments      * @return HasMany      */     public function attachments()     {         return $this->hasMany(Attach::class);     }      /**      * Relation for project employees      * @return MorphToMany      */     public function employees()     {         return $this->morphedByMany(Employee::class, 'attachable', 'attachments');     }      /**      * Relation for project teams      * @return MorphToMany      */     public function teams()     {         return $this->morphedByMany(Team::class, 'attachable', 'attachments');     }     }

Прикрепление

<?php  namespace App;  use Illuminate\Database\Eloquent\Model;  class Attach extends Model {     protected $table = 'attachments';     protected $fillable = ['attachable_id', 'attachable_type', 'project_id']; }

Фабрики идентичны для всех сущностей

<?php  /** @var \Illuminate\Database\Eloquent\Factory $factory */  use Faker\Generator as Faker;  $factory->define(/* (сотрудник/команда/проект) */, function (Faker $faker) {     return [         'name' => $faker->colorName     ]; });

Сущности готовы, переходим к трейту.

Использование трейта

Полиморфные отношения в Laravel подразумевают разные типы отношений для главных и прикрепляемых моделей — в проекте указывается тип связи morphedByMany(), а в сущностях — morphToMany(). Для всех прикрепляемых моделей метод для описания релейшена будет одинаков, поэтому логично вынести этот метод в трейт и использовать его в модели сотрудника и команды.
Создадим новую директорию app/Traits и трейт с названием релейшена: Attachable.php

<?php  namespace App\Traits;  use App\Project; use Illuminate\Database\Eloquent\Relations\MorphToMany;  trait Attachable {     /**      * Relation for entity attachments      * @return MorphToMany      */     public function attachments()     {         return $this->morphToMany(Project::class, 'attachable', 'attachments');     } }

Осталось добавить этот трейт в модели сотрудника и команды через use.

...     use Attachable; ...

Переходим к проверке работоспособности с помощью тестов.

Написание тестов

По стандарту, Laravel использует PHPUnit для тестирования. Создать тесты для релейшена:

php artisan make:test AttachableTest

Файл теста можно найти в tests/Feature/. Для обновления состояния БД перед запуском тестов будем использовать трейт RefreshDatabase.

Проверим работу морфа со стороны проекта и трейта со стороны команды и сотрудников

<?php  namespace Tests\Feature;  use App\Team; use App\Employee; use App\Project; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase;  class OrderTest extends TestCase {     use RefreshDatabase;      /** @test */     public function polymorphic_relations_scheme(): void     {         // Given project         $project = factory(Project::class)->create();         // Given team         $team = factory(Team::class)->create();         // Given employee         $employee = factory(Employee::class)->create();          // When we add team and employee to project         $project->teams()->save($team);         $project->employees()->save($employee);          // Then project should have two attachments         $this->assertCount(2, $project->attachments);         $this->assertCount(1, $project->teams);         $this->assertCount(1, $project->employees);         $this->assertEquals($team->id, $project->teams->first()->id);         $this->assertEquals($employee->id, $project->employees->first()->id);         // Team and employee should have attachment to project         $this->assertCount(1, $team->attachments);         $this->assertCount(1, $employee->attachments);         $this->assertEquals($project->id, $team->attachments->first()->id);         $this->assertEquals($project->id, $employee->attachments->first()->id);     } }

Тест прошел!

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

Буду рад слышать ваши кейсы применения трейтов в Laravel и PHP.

ссылка на оригинал статьи https://habr.com/ru/post/494658/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *