Введение
Поговорим о возможном применении трейтов вместе с полиморфными отношениями в 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.
Fragster
Это про использование примесей для создания полиморфных связей между моделями, да?
svntmr Автор
Про использование трейтов для выноса общих методов полиморфных связей. Тут у модели команды и у модели человека будут одинаковые методы для связи к проекту, чтобы их не дублировать — вынес в трейт