Если вы видите это, значит, я еще не придумал, что написать.
Сервисный слой часто разрастается до «толстых» классов, где сложно поддерживать код и переиспользовать отдельные части. В каком-то году по воле случая я смотрел ютубчик и наткнулся на подход Laravel Actions, основная суть заключается в создании простых классов, каждый из которых выполняет одну конкретную задачу (одно действие). Подход мне в целом понравился. Однако со временем, основной пакет стал обрастать фичами контекста, экшены как контроллер, как листенер, как консольная команда и т п, экшены стали размывать свою ответсвенность и хоть это и опционально, но я стал замечать, что во многих проектах это уже стало своего рода стандартом когда в один объект напихивают ответсвнности за все слои приложения.
Мне пришла идея создать пакет простых действий с решеним рутинных операций таких как “транзакции”, “кеширование”, “мемонизация”, “События”, “DIP”. А так же внедрить сценарный подход, когда есть объекты которые агрегируют простые и существующие действия в некий сценарий UseCase.
Так пришло начало Simple Actions – атомарные Actions и сценарные UseCases. Пакет: lemax10/simple-actions (GitHub).
use LeMaX10\SimpleActions\Action;
class CreateUserAction extends Action
{
protected function handle(string $name, string $email): \App\Models\User
{
return \App\Models\User::create(compact('name', 'email'));
}
}
// Запуск
$user = CreateUserAction::make()->run('John', 'john@example.com');
// или хелпером
$user = action(CreateUserAction::class, 'John', 'john@example.com');
Плюсы:
use LeMaX10\SimpleActions\UseCase;
class RegisterUserUseCase extends UseCase
{
protected function handle(array $data): \App\Models\User
{
$user = CreateUserAction::make()->run($data['name'], $data['email']);
SendWelcomeEmailAction::make()->run($user);
CreateUserProfileAction::make()->run($user, $data['profile']);
return $user;
}
}
// В контроллере
$user = RegisterUserUseCase::make()->run($request->validated());
// или
$user = usecase(RegisterUserUseCase::class, $request->validated());
Особенности:
CreateOrderAction::make()->withTransaction()->run($user, $items);
SomeReadOnlyUseCase::make()->withoutTransaction()->run($id);
Декларативно, без ручного Cache::remember(...) в каждом экшене.
$result = GetHeavyDataAction::make()
->remember('heavy:key', 60) // сек
->run($params);
$result = GetHeavyDataAction::make()
->rememberForever('heavy:key')
->run($params);
// Сгенерирует ключ автоматический по аргументам вызова с указанным префиксом
$result = GetHeavyDataAction::make()
->rememberAuto('heavyPrefix', 60)
->run($params);
Исключает дублирующиеся запросы в одном HTTP-запросе (например, при использовании одного экшена из разных слоёв). Я обычно замечал, что достаточно большая часть разработчиков при разработке проектов на разных слоях прибегает дублированию запросов, через повторые вызовы тех же методов сервисного слоя. В итоге у часто происходит проблема N+1.
// Первый вызов — выполнит handle() и запомнит результат
$user = GetUserAction::make()->memo()->run($userId);
// Повторный вызов с теми же аргументами — вернёт из памяти
$user = GetUserAction::make()->memo()->run($userId);
// Принудительно обновить мемоизированный результат
$user = GetUserAction::make()->memo(force: true)->run($userId);
// Запустить события даже при возврате из памяти
$user = GetUserAction::make()->memo(forceEvents: true)->run($userId);
По умолчанию для мемоизированных результатов события не запускаются При необходимости вы можете активировать аргумент forceEvents, чтобы события запустились.
В какой-то момент мне стало не хватать жизненого цикла экшенов и юзкейсов. Какое-то время я расставлял события в ручную, где-то прибегал к событиям моделей, после пришла мысль реализации Жизненного цикла экшенов и юзкейсов, за пример был взят подход из Eloquent ORM, в результате появились события: beforeRun, running, ran, failed, afterRun.
CreateUserAction::ran(function ($event) {
\Log::info('User created', ['id' => $event->result->id]);
});
// Мемоизация без повторных событий
CreateUserAction::make()->memo()->run($data);
// С принудительными событиями
CreateUserAction::make()->memo(forceEvents: true)->run($data);
// Обсерер
CreateUserAction::observe(UserNotification::class);
Возможности:
false).Подмена реализаций без изменения UseCase. Удобно в тестах и для разных окружений.
// Абстракция
abstract class NotificationAction extends Action {}
// Реализации
class SendEmailNotificationAction extends NotificationAction { /* ... */ }
class FakeNotificationAction extends NotificationAction { /* ... */ }
// В UseCase
app(NotificationAction::class)->run($user, 'Welcome!');
// В тестах
$this->app->bind(NotificationAction::class, FakeNotificationAction::class);
Структура:
app/
Actions/
User/
CreateUserAction.php
GetUserAction.php
UseCases/
User/
RegisterUserUseCase.php
Нейминг:
CreateUserAction, GetUserAction, SendInvoiceActionRegisterUserUseCase, CheckoutOrderUseCaseActions и UseCases дают:
Backend разработчик

Уже практически середина ноября, и мои парсеры давно собрали свежие данные. Сегодня я поделюсь анализом обновления рейтинга и новостями проекта.
В этом месяце мы добавили несколько технологий:
Fortran, ClickHouse, RabbitMQ, Kafka
Qt был перенесен из библиотеки в фреймворки
Удалён Ant Design из рейтинга
Главное улучшение этого месяца – полная переработка парсера. Мы значительно сократили процент нерелевантных вакансий, которые попадают в анализ. Алгоритм фильтрации стал умнее:
В ближайшем месяце ждёте ещё большие улучшения парсера.
В разделе “Часто задаваемые вопросы” мы подробно описали методологию расчёта рейтинга:
Go – поднялся на 3 пункта вверх, я услышал ваш фидбек начет go и после обновления парсера заменил поиск с golang на go.
JavaScript опередил C – JS занял 4-е место, сместив C на 5-е.
Топ-10 языков:
Jackson поднялась на 16 пунктов! caret обвалилась на 24 пункта!
Парсер стал точнее — результаты этого месяца более надёжны благодаря улучшениям
Спасибо, что следите за TrueIndex! Ваша обратная связь помогает нам становиться лучше.
Если у вас есть идеи по улучшению рейтинга или вы заметили неточности, пишите мне в Telegram
Если вы видите это, значит, я еще не придумал, что написать.
Вы когда-нибудь ловили TypeError, просто потому что .env вернул строку вместо числа? Или получали неожиданное поведение из-за опечатки в булевом флаге falose вместо false?
В файле .env можно задать некорректные значения, которые затем попадают в конфиг. В результате приложение может неожиданно падать с ошибкой 500 или вести себя непредсказуемо.
🔗 github.com/sushi-market/smart-cast
Этот пакет решает проблему: мы можем добавить уровень типобезопасной валидации и каста прямо в конфиг файлах. Если значение в .env невалидно — приложение сразу упадёт понятной ошибкой, а не в случайный момент времени.
Сейчас у нас есть два основных каста: stringToInt и stringToFloat
Передавая параметры в функцию, мы можем строго проверять значения в момент каста:
sign – если передано, то ограничивает значение только положительным или отрицательным. Полезно, например, для настройки TTL, который не может быть отрицательным.
strictType – по умолчанию true
При true каст "123.45" в int вызовет ошибку. Если указать false, значение будет приведено, но лучше оставлять строгий режим.
acceptsZero – по умолчанию true
Если установить false, ноль вызовет ошибку. Удобно для параметров вроде port.
acceptNull – по умолчанию false
Если true, то переданный null вернёт null вместо исключения – полезно для опциональных значений в конфиге.
'default_interval_length' => stringToInt(
value: env('INTERVALS_DEFAULT_LENGTH', 15),
sign: \DF\NumberSign::POSITIVE,
acceptsZero: false,
),
'debug' => stringToBoolean(
value: env('APP_DEBUG', false),
),
Пакет можно использовать не только в конфигурационных файлах, но и в любом месте кода, однако именно в конфиге его польза проявляется особенно ярко.
{message}