Laravel AaaS — Actions as a Service
Введение
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 лет, и все мои методы контроллера имеют не более пяти строк кода, и это потому, что я использую контроллеры только как Коммуникационный Слой, как я говорил в этой статье. Поэтому для меня нет смысла создавать файл, в котором будет всего несколько строк кода. Контроллеры должны использоваться только для:
- Получения Request.
- Сопоставления входных свойств.
- Отправки сопоставленного ввода на Сервисный Слой.
- Получения результата от Сервисного Слой и отправки 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 Паттерн, который вы можете применить к любому проекту, над которым работаете, и это способ иметь кодовую базу, с которой легко работать, поддерживать и обновлять по мере необходимости.
Я надеюсь, что вам понравилась эта статья, и если да, то не забудьте поделиться ей с друзьями!!! До встречи!