Введение
Поговорим о возможном пременении трейтов вместе с полиморфными отношениями в Laravel.
Содержание статьи:
- Описание предметной области
- Создание приложения
- Возможные структуры БД
- Создание сущностей
- Использование трейта
- Написание тестов
Описание предметной области
Мы будем разрабатывать систему, в которой некие сотрудники и некие команды могут быть прикреплены к проекту. Сущностями предметной области будут сотрудники, команды и проекты: команда состоит из сотрудников, на проект могут быть прикреплены сотрудники и команды. Между командой и сотрудником отношение 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/
Добавить комментарий