Laravel AaaS — Actions as a Service

Источник: «Laravel AaaS - Actions as a Service»
Сейчас Action классы и Вызываемые Контроллеры горячая тема Laravel. В этой статье я объясню, почему считаю вызываемые контроллеры плохой идеей, и объясню архитектурный шаблон, который я создал и назвал AaaS.

Введение

Laravel — удивительный фреймворк. Мы можем создавать продукты очень быстро со всеми функциями и DX, которые он предоставляет. Обычно в Laravel есть много способов сделать что-то. Нет единственного правильного способа сделать это, и иногда это действительно зависит от личного уровня того, как мы хотим структурировать наше приложение.

Action классы и Вызываемые Контроллеры (Invokable Controller) — горячая тема в наши дни. Я вижу много людей, использующих и говорящих о них. Я также пробовал и экспериментировал с этими идеями, и в этой статье собираюсь объяснить, почему считаю Вызываемые Контроллеры плохой идеей, а также он Архитектурном Шаблоне, который я создал и использую. Который я назвал AaaS — Action как Услуга.

Как я уже говорил, в Laravel есть много способов сделать что-либо, и я собираюсь показать вам один из них. Мне это нравится, и для меня имеет смысл организовывать приложения таким образом, но если это не имеет смысла для вас, вы можете организовывать свои приложения по своему.

Action Классы

Action классы — это классы, предназначенные для выполнения одного действия. Обычно это классы с одним (публичным) методом. Действительно простым примером Action класса может быть создание нового пользователя.


namespace App\Actions\Users\CreateUser;

use App\Models\User;

class CreateUser
{
/**
* @param string $name
* @param string $email
* @return User
*/

function handle(string $name, string $email): User
{
User::query()->create([
'name' => $name,
'email' => $email,
]);
}
}

Как вы можете видеть в приведённой выше (упрощённой) реализации, класс CreateUser используется только для создания нового пользователя. В реальном коде у вас, вероятно, будет больше логики в этом классе, и он может иметь другие приватные методы для разделения логики более читаемым и удобным для сопровождения способом, но идея состоит в том, чтобы класс имел одну цель.

Вызываемые Контроллеры

Концепция Action классов обычно в основном используется для создания Вызываемых Контроллеров, которые является классами Controller с целью выполнения одного действия. Таким образом, вместо нескольких методов в Контроллере будет определён только метод __invoke(), который будет выполняться. Давайте посмотрим на приведённый выше пример Action, реализованный как Вызываемый Контроллер.


namespace App\Http\Controllers\Users;

use App\Http\Controllers\Controller;
use App\Http\Requests\Users\CreateUserRequest;
use App\Models\User;

class CreateUserController extends Controller
{
/**
* @param CreateUserRequest $request
* @return User
*/

function __invoke(CreateUserRequest $request): User
{
$data = $request->validated();
User::query()->create([
'name' => $data['name'],
'email' => $data['email'],
]);
}
}

Почему Вызываемые Контроллеры — это плохо

Сейчас Вызываемые Контроллеры являются горячей темой в Laravel, и эта концепция активно используется многими разработчиками. Я считаю, что это Плохо. Я не критикую людей, которые его используют, если это работает для вашего приложения, продолжайте делать это, но я объясню почему не использую его.

Насколько я видел, разработчики, использующие Вызываемые Контроллеры, используют их, чтобы избежать огромных Контроллеров. Я полностью понимаю это, но IMO контроллеры не должны быть огромными, даже если у них есть несколько методов. На самом деле, я ежедневно работаю с API в Laravel в течении последних 5 лет, и все мои методы контроллера имеют не более пяти строк кода, и это потому, что я использую контроллеры только как Коммуникационный Слой, как я говорил в этой статье. Поэтому для меня нет смысла создавать файл, в котором будет всего несколько строк кода. Контроллеры должны использоваться только для:

  1. Получения Request.
  2. Сопоставления входных свойств.
  3. Отправки сопоставленного ввода на Сервисный Слой.
  4. Получения результата от Сервисного Слой и отправки Response.

Шаблон AaaS

Мне очень понравилась идея Action классов, но, как я упоминал выше, для меня не имело смысла реализовывать их как Вызываемые Контроллеры. Поэтому я использовал Action классы по-другому. Я использовал их в качестве Сервисного Слоя, и именно так я назвал этот шаблон AaaS - Action как Сервис.

Это Архитектурный Шаблон, как MVC, и даже то, что я создал его из-за Laravel, не мешает применять его к другим фреймворкам и даже другим языкам программирования, если хотите. Этот шаблон/паттерн имеет четыре принципа, которые я объясню ниже.

Тонкий Коммуникационный Слой

Коммуникационный Слой — это слой наших приложений, которые получают пользовательский ввод. В Веб-приложении это наши Контроллеры, в CLI приложении — это Команды. Этот слой должен только:

Отдельная Валидация

Валидация данных не должна быть привязана к Коммуникационному Уровню. Это означает, что валидация данных должна быть связана с самими данными или с Action, для которого они используются. В Laravel приложении это означает, что валидация не должна выполняться с использованием Форм Запроса, а в DTO или самом Action.

Сопоставленный Ввод

Входные данные, необходимые для выполнения Action, должны быть сопоставлены с DTO когда ему требуется несколько входных свойств для улучшения качества кода и удобства обслуживания Action приложения.

Action с одной целью

Вся Бизнес-логика приложения должна находится в Action классах, и каждый Action класс должен отвечать за одно действие. При необходимости вы можете вызвать другой Action в Action, чтобы избежать дублирования кода.

Как реализовать AaaS шаблон/паттерн

Теперь, когда вы знаете, что такое AaaS Паттерн, давайте рассмотрим простой пример того, как его применить в Laravel приложении. Давайте представим простую реализацию API CRUD для пользователя нашего приложения.

Для начала посмотрим на класс UserController.


namespace App\Http\Controllers\Users;

use App\Actions\Users\DeleteUser;
use App\Actions\Users\FetchUsers;
use App\Actions\Users\SaveUser;
use App\Actions\Users\ShowUser;
use App\DTOs\Users\FetchUsersDTO;
use App\DTOs\Users\SaveUserDTO;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class UserController extends Controller
{
/**
* @param Request $request
* @param FetchUsers $action
* @return JsonResponse
*/

public function index(Request $request, FetchUsers $action): JsonResponse
{
return response()->json($action->handle(FetchUsersDTO::fromRequest($request)));
}

/**
* @param int $userId
* @param ShowUser $action
* @return JsonResponse
*/

public function show(int $userId, ShowUser $action): JsonResponse
{
return response()->json($action->handle($userId));
}

/**
* @param Request $request
* @param SaveUser $action
* @return JsonResponse
*/

public function store(Request $request, SaveUser $action): JsonResponse
{
return response()->json($action->handle(SaveUserDTO::fromRequest($request)), Response::HTTP_CREATED);
}

/**
* @param Request $request
* @param int $userId
* @param SaveUser $action
* @return JsonResponse
*/

public function update(Request $request, int $userId, SaveUser $action): JsonResponse
{
return response()->json($action->handle(SaveUserDTO::fromRequest($request), $userId), Response::HTTP_OK);
}

/**
* @param int $userId
* @param DeleteUser $action
* @return JsonResponse
*/

public function destroy(int $userId, DeleteUser $action): JsonResponse
{
$action->handle($userId);
return response()->noContent();
}
}

Как видите, все методы в Контроллере очень просты и следуют принципу Тонкого Коммуникационного Слоя.

Для принципов Отдельная Валидация и Сопоставленный Ввод я буду использовать созданный мной пакет Validated DTO для упрощения использования DTO, но вы можете использовать пакет по вашему выбору или даже не использовать пакет вообще.

Давайте взглянем на классы FetchUsersDTO и SaveUserDTO.


namespace App\DTOs\Users;

use WendellAdriel\ValidatedDTO\Casting\BooleanCast;
use WendellAdriel\ValidatedDTO\Casting\IntegerCast;
use WendellAdriel\ValidatedDTO\ValidatedDTO;

class FetchUsersDTO extends ValidatedDTO
{
public int $page;
public int $per_page;
public bool $active_only;

/**
* @return array
*/

protected function rules(): array
{
return [
'page' => ['sometimes', 'integer'],
'per_page' => ['sometimes', 'integer'],
'active_only' => ['sometimes', 'boolean'],
];
}

/**
* @return array
*/

protected function defaults(): array
{
return [
'page' => 1,
'per_page' => 20,
'active_only' => true,
];
}

/**
* @return array
*/

protected function casts(): array
{
return [
'page' => new IntegerCast(),
'per_page' => new IntegerCast(),
'active_only' => new BooleanCast(),
];
}
}

namespace App\DTOs\Users;

use WendellAdriel\ValidatedDTO\Casting\StringCast;
use WendellAdriel\ValidatedDTO\ValidatedDTO;

class SaveUserDTO extends ValidatedDTO
{
public string $name;
public string $email;

/**
* @return array
*/

protected function rules(): array
{
return [
'name' => ['required', 'string'],
'email' => ['required', 'email'],
];
}

/**
* @return array
*/

protected function defaults(): array
{
return [];
}

/**
* @return array
*/

protected function casts(): array
{
return [
'name' => new StringCast(),
'email' => new StringCast(),
];
}
}

Как видите, валидация данных теперь выполняется в DTO, привязанном к самим данным, а не к Коммуникационному Слою. Если мне нужно вызвать Action с помощью DTO из команды CLI или другого Action, мне не нужно вручную проверять данные, как понадобилось бы, если валидация была бы привязана к Коммуникационному Слою, например, с использованием Запросов Формы.

Теперь, что касается последнего принципа — Action с одной целью — давайте посмотрим на наши Action классы FetchUsers, ShowUser, SaveUser и DeleteUser.


namespace App\Actions\Users;

use App\DTOs\Users\FetchUsersDTO;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;

class FetchUsers
{
/**
* @param FetchUsersDTO $dto
* @return Collection
*/

public function handle(FetchUsersDTO $dto): Collection
{
$query = User::query();

if ($dto->active_only) {
$query->where('is_active', true);
}

return $query->skip(($dto->page - 1) * $dto->per_page)
->take($dto->per_page)
->get();
}
}

namespace App\Actions\Users;

use App\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class ShowUser
{
/**
* @param int $userId
* @return User
*
* @throws ModelNotFoundException
*/

public function handle(int $userId): User
{
return User::query()->findOrFail($userId);
}
}

namespace App\Actions\Users;

use App\DTOs\Users\SaveUserDTO;
use App\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class SaveUser
{
public __construct(
private ShowUser $showAction
) {}

/**
* @param SaveUserDTO $dto
* @param int|null $userId
* @return User
*
* @throws ModelNotFoundException
*/

public function handle(SaveUserDTO $dto, ?int $userId = null): User
{
$user = is_null($userId)
? new User()
: $this->showAction->handle($userId);

$user->fill($dto->toArray());
$user->save();

return $user;
}
}

namespace App\Actions\Users;

use App\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class DeleteUser
{
public __construct(
private ShowUser $showAction
) {}

/**
* @param int $userId
* @return void
*
* @throws ModelNotFoundException
*/

public function handle(int $userId): void
{
$user = $this->showAction->handle($userId);
$user->delete();
}

Как видите, у каждого Action класса есть одно действие. В более сложных случаях вы даже можете разделить Action Сохранения и/или DTO на два разных: CreateUser и UpdateUser Action и CreateUserDTO и UpdateUserDTO. Для упрощения этого примера это было не нужно, поэтому оно было объединено в Action Сохранения.

Заключение

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

Я также представил созданный мной шаблон, AaaS Паттерн, который вы можете применить к любому проекту, над которым работаете, и это способ иметь кодовую базу, с которой легко работать, поддерживать и обновлять по мере необходимости.

Я надеюсь, что вам понравилась эта статья, и если да, то не забудьте поделиться ей с друзьями!!! До встречи!

Дополнительные материалы

Предыдущая Статья

Laravel 10: Чтение JSON файлов

Следующая Статья

Новое в Symfony 6.3 — Улучшения эмодзи