🎩 Книга «Денди-код» о том, как сделать код аккуратным и понятным

Мои первые шаги в Open Source: процессы, тесты и работа с сообществом

Я начал всерьёз погружаться в Open Source в начале 2025 года. Тогда это было скорее любопытство и желание попробовать что-то новое, чем чёткий план. Спустя время я заметил, что мои проекты начали находить отклик, появились первые пользователи, а затем и первые контрибьюторы.

За последний год я выпустил несколько релизов, постепенно выстроил базовые процессы тестирования и начал относиться к своим проектам как к продуктам, за которые я несу ответственность. Это был важный переход в мышлении — от «я что-то написал» к «я поддерживаю и развиваю проект».

Я не могу сказать, что у меня большой или глубокий опыт в Open Source. Скорее наоборот — я всё ещё в начале пути. Но все же я хочу рассказть о том на какие вещи, как мне кажется, стоит обратить внимание тем, кто только думает о создании своего первого открытого проекта.

Мой GitHub – https://github.com/prog-time

Основной фокус я хочу сделать не на технологиях, а на процессах: выборе идеи, масштабе проекта, внутренней мотивации и первых организационных решениях, которые сильно влияют на дальнейшую судьбу проекта.

С чего начинается идея

В основе почти всех Open Source-проектов лежит не прямая монетизация, а социальное признание: звёзды на GitHub, комментарии, репосты, подписчики. Эти метрики становятся своего рода обратной связью — сигналом, что твоя работа кому-то действительно нужна и интересна. Да, иногда появляются донаты или спонсоры, но рассчитывать на стабильный доход в начале пути — плохая идея.

Поэтому для себя я сделал простой вывод: начинать стоит с проектов, которые полезны лично тебе. Когда ты решаешь собственную проблему, мотивация поддерживать и развивать проект появляется естественно — даже если о нём пока никто не знает и звёзд почти нет.

Не менее важным оказался масштаб идеи. Сейчас я стараюсь выбирать такие проекты, где путь до первой MVP занимает не больше одной-двух недель. Если разработка затягивается и долго не приносит видимого результата, интерес начинает угасать, а риск выгорания резко возрастает.

Для вдохновения вы можете посмотреть проекты которые сейчас в тренде на GitHub или подписаться на Topic по интересующему вас направлению https://github.com/trending.

Если вы планируете разрабатывать библиотеку, фреймворк или инструмент для разработчиков, имеет смысл ориентироваться на технологии, которые востребованы на рынке. Использование популярных языков программирования и экосистем, как правило, позволяет охватить более широкую аудиторию.

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

Если проект ориентирован на пользователей, не связанных с разработкой (как в моём случае), приоритеты смещаются. Здесь особенно важно уделять внимание:

  • производительности и оптимизации;
  • качественной и понятной документации;
  • простоте установки и быстрому развёртыванию проекта.

Необходимо учитывать, что такая аудитория, как правило, не имеет технического бэкграунда. Поэтому ключевой фактор успеха — минимальный порог входа и понятный пользовательский опыт.

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

Со временем я пришёл к простому правилу — установка и первичная настройка не должны занимать больше двух-трёх шагов. Каждый лишний шаг резко снижает шанс того, что пользователь вообще дойдёт до первого запуска.

В моём случае проекты чаще представляли собой отдельные сервисы, поэтому довольно рано я сделал ставку на контейнеризацию. Docker оказался идеальным инструментом: он позволяет изолировать зависимости, стандартизировать окружение и избавить пользователя от большинства проблем, связанных с локальной настройкой. Это особенно важно в Open Source, где ты не можешь контролировать, в каком окружении проект будут запускать.

Хорошим примером для меня стал проект tg-support-bot. В нём я сразу заложил два сценария установки. Первый — полный, через docker-compose, где поднимаются все необходимые сервисы: PostgreSQL, PgAdmin, Loki, Grafana, Redis и другие компоненты. Второй — упрощённый, рассчитанный на обычный хостинг, с возможностью подключить внешнюю базу данных и отключить необязательные сервисы.

Отдельно я постарался избежать жёстких связей между контейнерами. Для меня было важно, чтобы пользователь мог безболезненно отключать или заменять отдельные сервисы внутри docker-compose, не ломая при этом основное приложение. Такой подход даёт гибкость и делает проект более дружелюбным для разных сценариев использования.

Линтинг, CI и тесты

Перед тем как выкладывать проект в публичный доступ, я считаю обязательным настроить базовую инфраструктуру качества кода: линтинг, тесты и CI. Это не только снижает количество ошибок, но и сразу задаёт определённый уровень качества, который чувствуют все будущие контрибьюторы.

Первое, с чего я начинаю, — это правила code style. Чем более чётко и однозначно они описаны, тем проще поддерживать кодовую базу в читаемом и предсказуемом состоянии, особенно когда в проект приходят новые участники.

Дальше я настраиваю линтеры, которые автоматически проверяют код на несколько ключевых вещей: синтаксические ошибки и проблемы с типизацией, соответствие имен переменных и функций принятому стилю, а также типичные ошибки и потенциальные уязвимости. В результате большая часть проблем отлавливается ещё до ревью или запуска кода, а обсуждения в pull request’ах смещаются с формальных правок к действительно важным архитектурным решениям.

Параллельно этому я активно покрываю весь функционал Unit тестами. Чем больше функциональности покрыто тестами, тем спокойнее ты относишься к изменениям в коде. Это особенно чувствуется в Open Source, где в проект могут прийти сторонние контрибьюторы, и ты не всегда знаешь, какие именно изменения они предложат.

В проекте tg-support-bot я решил подойти к этому вопросу максимально прагматично. Вместо сложных и тяжёлых решений я реализовал набор shell-скриптов, которые тесно интегрированы с git-хуками и работают на нескольких уровнях.

На этапе коммита запускаются проверки только тех файлов, которые добавлены в индекс. Статический анализатор PHPStan проверяет код на ошибки и проблемы с типизацией, запускаются unit-тесты для изменённых классов. Если в коммите затронут Dockerfile, автоматически запускается Hadolint, а изменения в shell-скриптах проверяются с помощью ShellCheck, который хорошо выявляет типичные ошибки и потенциальные уязвимости.

Пример работы линтеров

При выполнении push PHPStan анализирует весь код и выполняются все unit-тесты. Этот этап часто помогает поймать проблемы, которые могли проскочить при локальной проверке, но влияют на проект в целом.

Финальным уровнем становится CI. Здесь проект проверяется ещё раз полностью, что особенно важно для Open Source. Внешние контрибьюторы могут прислать изменения, которые проходят локальные проверки, но ломают логику или инфраструктуру проекта. CI служит последней линией обороны и не позволяет таким изменениям попасть в основную ветку.

Пример работы CI

Если на любом из этапов обнаруживается ошибка, изменения просто не проходят дальше, пока проблема не будет исправлена. Со временем это формирует привычку писать более аккуратный и предсказуемый код.

Структура GitHub-репозитория

По мере роста проекта я довольно быстро столкнулся с тем, что хаотичная работа с ветками начинает мешать. Поэтому вопрос структуры GitHub-репозитория для меня стал очень важен.

Во всех своих проектах я стараюсь придерживаться одной и той же схемы работы с ветками. Основной веткой всегда является main, и прямые коммиты в неё запрещены. Любые изменения — даже самые мелкие — попадают туда только через дочерние ветки и pull request’ы.

Для каждой отдельной задачи из релиза создаётся своя ветка. В названии ветки обязательно указывается номер соответствующего Issue — это жёсткое правило, без исключений. Если ветка названа некорректно, она просто не пройдёт проверки CI, и изменения не смогут быть влиты в проект. На первый взгляд это может показаться излишней строгостью, но на практике такой подход сильно упрощает жизнь.

Пример структуры веток и коммитов

Документирование

Даже самая полезная библиотека или сервис теряют смысл, если пользователи и контрибьюторы не понимают, как её использовать.

Для меня документация делится на несколько ключевых элементов:

README.md

Это первая точка контакта с проектом для любого пользователя на GitHub. В нём я стараюсь сразу дать максимально понятную картину: краткое описание проекта, инструкции по установке и настройке, минимальный пример использования и ссылки на расширенные материалы, тесты и CI/CD процессы. README должен быть кратким, чтобы любой мог быстро установить и попробовать продукт, но достаточно информативным.

CONTRIBUTING.md

Он описывает, как внести изменения в проект, какие правила создания веток и коммитов я использую, как отправлять pull request и какие требования предъявляются к тестированию и линтингу. Без такого руководства сторонние контрибьюторы часто тратят лишнее время на догадки и пробные ошибки, что снижает их мотивацию.

GitHub Wiki

Для более детальной и структурированной документации я активно использую Wiki. Данный раздел позволяет создавать несколько страниц, объединённых логикой, и описывать архитектуру проекта, деплой, CI/CD, гайды по использованию библиотек и сервисов.

Практические правила, которыми я руководствуюсь:

  1. Документировать ключевое сразу, не откладывая на потом — иначе теряется контекст и появляются лишние вопросы.
  2. Использовать шаблоны и стандарты (Markdown, OpenAPI, DocBlock), чтобы документация была однородной.
  3. Поддерживать документацию в актуальном состоянии — каждое новое изменение должно сопровождаться обновлением описаний.

GitHub Wiki

Со временем я убедился: проект с хорошей документацией не только удобнее использовать, но и легче развивать. Новые участники быстрее входят в процесс, а ошибки на старте заметно снижаются.

Взаимодействие с аудиторией

Успешный Open Source-проект — это не только код, но и люди, которые его используют. Активное взаимодействие с аудиторией помогает не только продвигать проект, но и получать ценную обратную связь, которая часто оказывается важнее любой документации.

Первым шагом для меня стало создание удобного канала связи. В tg-support-bot я создал отдельную Telegram-группу, где публикую новости, отвечаю на часто задаваемые вопросы и просто общаюсь с пользователями.

Telegram tg-support-bot

Эта группа превратилась в настоящий центр активности: со временем пользователи стали помогать друг другу без моего участия, тестировать новые функции, выявлять ошибки и предлагать улучшения. Идеи для нового функционала часто приходят именно от участников сообщества, а голосования помогают учитывать мнение пользователей при планировании дальнейшей разработки.

Чтобы проект рос, важно привлекать пользователей из внешних источников. Для меня эффективным инструментом стал видеоконтент — Dev-блог, в котором я показываю процесс разработки и делюсь личным опытом. Видеоинструкции по установке и настройке проекта снижают порог входа и делают продукт более доступным для новичков.

Дополнительно я публикую статьи на популярных ресурсах, таких как Хабр или Пикабу, чтобы рассказывать о проекте более широкой аудитории. Если проект содержит интересные технические решения, он может привлекать внимание даже тех, кто не планирует использовать его напрямую, но хочет наблюдать за процессом разработки и изучать технические детали. Такой интерес создаёт дополнительную ценность и помогает проекту расти.

Заключение

Open Source — это не только код. Это процессы, дисциплина, сообщество и взаимодействие с пользователями. Продуманная структура репозитория, тесты, документация и активная коммуникация позволяют проекту развиваться, привлекать новых участников и становиться действительно полезным инструментом.

Для меня этот путь стал одновременно учебным и вдохновляющим: я не только совершенствую навыки разработки, но и вижу, как моя работа реально помогает людям. Если вам интересно следить за моими проектами или участвовать в их развитии, приглашаю на мой GitHub — там публикуются все обновления, и вы сможете присоединиться к работе над проектами.

Контекстное логирование с встроенными уведомлениями для Laravel

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

При активном использовании логирования в проектах на Laravel я регулярно сталкивался со следующими проблемами:

  • Логирование события и отправка уведомления о нём требуют писать несколько строк кода: сформировать сообщение, записать его в лог, отправить уведомление. Это громоздко и снижает читаемость.
  • При необходимости отследить цепочку связанных событий (для конкретного пользователя, джобы, консольной команды или процесса) приходится заранее еще на этапе написания кода продумывать структуру сообщений и вручную передавать дополнительный контекст. В результате лог-сообщения становятся перегруженными.
  • При возникновении непредвиденных исключений требуется отправлять уведомления сразу в несколько каналов и каждый раз вручную передавать контекст: пользователя, процесс, сервер и другие детали. Код усложняется, а часть важного контекста легко упустить.

Чтобы решить эти (и не только) проблемы был разработан пакет — faustoff/laravel-contextify. Им я и хочу с вами поделиться.

Laravel Contextify

Контекстное логирование с встроенными уведомлениями для Laravel.

use Faustoff\Contextify\Facades\Contextify;

Contextify::notice('Updated', ['key' => 'value'])->notify(['mail']);
// [2025-01-01 12:00:00] local.NOTICE: Updated {"key":"value"} {"trace_id":"4f9c2a1b"}

Laravel Contextify расширяет возможности логирования Laravel двумя основными (но не единственными) функциями:

  1. Встроенные уведомленияотправка уведомлений вместе с логированием в одну строку без необходимости разделения кода на несколько строк для логирования и отправки уведомления.
  2. Автоматическое добавление контекста — логи и уведомления автоматически включают дополнительные контекстные данные из настроенных провайдеров контекста (встроенные: ID трассировки, ID процесса, Имя хоста, Файл и строка вызова и другие), помогая сохранять сообщения короткими и чистыми, перемещая дополнительный контекст из самого сообщения в отдельную область.

Предоставляет фасад Contextify, совместимый с фасадом Log от Laravel: те же методы (debug, info, notice, warning, error, critical, alert, emergency) с идентичными параметрами, плюс цепочный метод notify().

Происхождение названия: “Contextify” объединяет Context (контекст) и Notify (уведомлять), отражая двойное назначение — обогащать логи контекстными данными и отправлять уведомления о событиях логирования.

Возможности

  • 📧 Поддержка уведомлений: Отправка уведомлений через почту, telegram или любой канал уведомлений Laravel
  • 🔍 Автоматическое добавление контекста: Логи и уведомления автоматически включают дополнительные контекстные данные из настроенных провайдеров контекста
  • 🔌 Пользовательские провайдеры контекста: Расширяйте встроенные провайдеры своими собственными
  • 🔄 Статические и динамические провайдеры: Статические (кэшируемые) и динамические (обновляемые при каждом вызове) провайдеры контекста
  • 🎯 Контекст на основе групп: Отдельные провайдеры контекста для логов и уведомлений
  • 📊 Уровни логирования PSR-3: Все стандартные уровни логирования (debug, info, notice, warning, error, critical, alert, emergency)
  • 🎨 Пользовательские уведомления: Расширяйте классы уведомлений и добавляйте пользовательские каналы
  • 🔔 Фильтрация каналов уведомлений: Фильтрация каналов с параметрами only и except
  • 🔄 Удобный API: Цепочка методов для читаемого кода
  • Интеграция с Monolog: Интегрируется с логированием Laravel через процессоры Monolog:

Требования

  • PHP 8.0 или выше
  • Laravel 8.0 или выше
  • Monolog 2.0 или выше

Установка

Установите пакет через Composer:

composer require faustoff/laravel-contextify

Настройка

При необходимости опубликуйте файл конфигурации:

php artisan vendor:publish --tag=contextify-config

Это создаст config/contextify.php для настройки провайдеров контекста и уведомлений.

Переменные окружения

Добавьте в .env для настройки получателей уведомлений:

CONTEXTIFY_MAIL_ADDRESSES=admin@example.com,team@example.com
CONTEXTIFY_TELEGRAM_CHAT_ID=123456789

Примечание: Для уведомлений Telegram требуется установка пакета laravel-notification-channels/telegram вручную.

Использование

Запись логов

Используйте фасад Contextify так же, как фасад Log от Laravel. Логи автоматически включают дополнительный контекст из провайдеров контекста, настроенных для логирования:

use Faustoff\Contextify\Facades\Contextify;

Contextify::debug('Отладочное сообщение', ['key' => 'value']);
// [2025-01-01 12:00:00] local.DEBUG: Отладочное сообщение {"key":"value"} {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Services/ExampleService.php:42","class":"App\\Services\\ExampleService"}

Contextify::info('Пользователь вошёл в систему', ['user_id' => 123]);
// [2025-01-01 12:00:00] local.INFO: Пользователь вошёл в систему {"user_id":123} {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Http/Controllers/Auth/LoginController.php:55","class":"App\\Http\\Controllers\\Auth\\LoginController"}

Contextify::notice('Важное уведомление');
// [2025-01-01 12:00:00] local.NOTICE: Важное уведомление  {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"routes/web.php:10","class":null}

// ... и то же самое для warning, error, critical, alert и emergency

Отправка уведомлений

Цепочкой вызовите notify() после любого метода логирования для отправки уведомлений. Уведомления включают сообщение лога, контекст и дополнительный контекст из провайдеров контекста, настроенных для уведомлений.

Фильтруйте каналы с помощью параметров only и except:

use Faustoff\Contextify\Facades\Contextify;

Contextify::error('Ошибка обработки платежа', ['order_id' => 456])->notify();
// [2025-01-01 12:00:00] local.ERROR: Ошибка обработки платежа {"order_id":456} {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Http/Controllers/Api/OrderController.php:133","class":"App\\Http\\Controllers\\Api\\OrderController"}
// Уведомление с контекстом {"order_id":456} и дополнительным контекстом отправлено во все настроенные каналы уведомлений

Contextify::critical('Потеряно соединение с базой данных')->notify(only: ['mail']);
// [2025-01-01 12:00:00] local.CRITICAL: Потеряно соединение с базой данных  {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Console/Commands/MonitorCommand.php:71","class":"App\\Console\\Commands\\MonitorCommand"}
// Уведомление с дополнительным контекстом отправлено только в канал почты

Contextify::alert('Обнаружена попытка взлома')->notify(except: ['telegram']);
// [2025-01-01 12:00:00] local.ALERT: Обнаружена попытка взлома  {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Providers/AppServiceProvider.php:25","class":"App\\Providers\\AppServiceProvider"}
// Уведомление с дополнительным контекстом отправлено во все настроенные каналы уведомлений, кроме канала Telegram

По необходимости, вы можете переопределить стандартную реализацию уведомления LogNotification:

namespace App\Notifications;

use Faustoff\Contextify\Notifications\LogNotification;

class CustomLogNotification extends LogNotification
{
    // Переопределите методы или добавьте новые
}

Обновите конфигурацию:

'notifications' => [
    'class' => \App\Notifications\CustomLogNotification::class,
    // ... другие настройки уведомлений
],

Уведомления об исключениях

Уведомления об исключениях отправляются автоматически (включено по умолчанию). Уведомления включают детали исключения (сообщение и трассировку стека) и дополнительный контекст из провайдеров контекста, настроенных для уведомлений.

По необходимости, вы можете переопределить стандартную реализацию уведомления ExceptionNotification:

namespace App\Notifications;

use Faustoff\Contextify\Notifications\ExceptionNotification;

class CustomExceptionNotification extends ExceptionNotification
{
    // Переопределите методы или добавьте новые
}

Обновите конфигурацию:

'notifications' => [
    'exception_class' => \App\Notifications\CustomExceptionNotification::class,
],

Чтобы отключить автоматические уведомления об исключениях, установите reportable в null:

'notifications' => [
    'reportable' => null,
],

Примечание: ExceptionNotificationFailedException предотвращает бесконечные циклы при сбое уведомлений об исключениях.

Провайдеры контекста

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

Статические провайдеры контекста

Статические провайдеры возвращают данные, которые остаются постоянными на протяжении жизненного цикла запроса/процесса. Они реализуют StaticContextProviderInterface.

Встроенные:

  • ProcessIdContextProvider: Добавляет ID текущего процесса PHP (pid)
  • TraceIdContextProvider: Генерирует уникальный 16-символьный шестнадцатеричный ID трассировки (trace_id) для распределённой трассировки
  • HostnameContextProvider: Добавляет имя хоста сервера (hostname)
  • EnvironmentContextProvider: Добавляет окружение приложения (environment)

Обновление статического контекста

Статический контекст кэшируется при загрузке приложения. Используйте touch() для ручного обновления, что полезно при форке процесса (например, для воркеров очередей) для генерации нового ID трассировки:

use Faustoff\Contextify\Facades\Contextify;
use Faustoff\Contextify\Context\Providers\TraceIdContextProvider;

// Обновить конкретный провайдер (например, сгенерировать новый ID трассировки)
Contextify::touch(TraceIdContextProvider::class);

// Обновить все статические провайдеры
Contextify::touch();

Динамические провайдеры контекста

Динамические провайдеры обновляют данные при каждом вызове логирования. Они реализуют DynamicContextProviderInterface.

Встроенные:

  • CallContextProvider: Добавляет путь к файлу и номер строки (file) и имя класса (class) вызывающего кода
  • PeakMemoryUsageContextProvider: Добавляет пиковое использование памяти в байтах (peak_memory_usage)
  • DateTimeContextProvider: Добавляет текущую дату и время в формате логов Laravel (datetime)

Создание пользовательских провайдеров контекста

Реализуйте StaticContextProviderInterface или DynamicContextProviderInterface:

namespace App\Context\Providers;

use Faustoff\Contextify\Context\Contracts\StaticContextProviderInterface;

class CustomContextProvider implements StaticContextProviderInterface
{
    public function getContext(): array
    {
        return [
            // реализация ...
        ];
    }
}

Регистрация пользовательских провайдеров

Добавьте пользовательские провайдеры в config/contextify.php:

use App\Context\Providers\CustomContextProvider;

return [
    'logs' => [
        'providers' => [
            // Встроенные провайдеры...
            
            // Пользовательские провайдеры
            CustomContextProvider::class,
        ],
    ],

    'notifications' => [
        'providers' => [
            // Встроенные провайдеры...
            
            // Пользовательские провайдеры
            CustomContextProvider::class,
        ],
    ],
];

Контекст на основе групп

Определите отдельные провайдеры контекста для логов и уведомлений. Если провайдер присутствует в обоих наборах, те же контекстные данные используются для обоих случаев.

Настройте в config/contextify.php:

  • logs.providers — провайдеры для записей логов
  • notifications.providers — провайдеры для уведомлений

Пример:

use Faustoff\Contextify\Context\Providers\CallContextProvider;
use Faustoff\Contextify\Context\Providers\EnvironmentContextProvider;
use Faustoff\Contextify\Context\Providers\TraceIdContextProvider;

return [
    'logs' => [
        'providers' => [
            TraceIdContextProvider::class,     // Общий
            CallContextProvider::class,        // Только для логов
        ],
    ],

    'notifications' => [
        'providers' => [
            TraceIdContextProvider::class,     // Общий
            EnvironmentContextProvider::class, // Только для уведомлений
        ],
    ],
];

Уведомления

Поддерживаются каналы mail и telegram из коробки. Почта работает сразу; для Telegram требуется пакет laravel-notification-channels/telegram.

Настройка

Настройте каналы в config/contextify.php:

'notifications' => [
    /*
     * Используйте формат ассоциативного массива ['channel' => 'queue'] для указания
     * очереди для каждого канала. При простом массиве ['channel'] будет использоваться очередь 'default'.
     */
    'channels' => [
        'mail' => 'mail-queue',
        'telegram' => 'telegram-queue',
    ],
    
    'mail_addresses' => explode(',', env('CONTEXTIFY_MAIL_ADDRESSES', '')),
    
    'telegram_chat_id' => env('CONTEXTIFY_TELEGRAM_CHAT_ID'),
],

Пользовательские каналы уведомлений

Например, чтобы добавить уведомления Slack, необходимо:

  1. Создать пользовательский класс уведомлений с реализованным методом toSlack() согласно документации:
namespace App\Notifications;

use Faustoff\Contextify\Notifications\LogNotification;
use Illuminate\Notifications\Messages\SlackMessage;

class CustomLogNotification extends LogNotification
{
    public function toSlack($notifiable): SlackMessage
    {
        // См. https://laravel.com/docs/12.x/notifications#formatting-slack-notifications
        
        return (new SlackMessage())
            ->content(ucfirst($this->level) . ': ' . $this->message);
    }
}
  1. Создать пользовательский класс notifiable с реализованным методом routeNotificationForSlack() согласно документации:
namespace App\Notifications;

use Faustoff\Contextify\Notifications\Notifiable;

class CustomNotifiable extends Notifiable
{
    public function routeNotificationForSlack($notification): string
    {
        // См. https://laravel.com/docs/12.x/notifications#routing-slack-notifications
    
        return config('services.slack.notifications.channel');
    }
}
  1. Настроить Slack в config/services.php.

  2. Обновить config/contextify.php:

'notifications' => [
    'class' => \App\Notifications\CustomLogNotification::class,

    'notifiable' => \App\Notifications\CustomNotifiable::class,

    'channels' => [
        'mail',
        'telegram',
        'slack'
    ],
],

Примечание: Для уведомлений об исключениях расширьте ExceptionNotification и добавьте метод toSlack() аналогичным образом.

Нужно больше каналов уведомлений? Добро пожаловать на Laravel Notifications Channels.

Консольные команды

Отслеживание

Используйте трейт Faustoff\Contextify\Console\Trackable для логирования начала, завершения и времени выполнения команды:

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Faustoff\Contextify\Console\Trackable;
use Faustoff\Contextify\Facades\Contextify;

class SyncData extends Command
{
    use Trackable;

    protected $signature = 'data:sync';

    public function handle(): int
    {
        // Ваша бизнес-логика ...
        
        Contextify::notice('Данные синхронизированы');

        return self::SUCCESS;
    }
}

Лог:

[2025-01-01 12:00:00] local.DEBUG: Run with arguments {"command":"data:sync"} {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Console/Commands/SyncData.php:42","class":"App\\Console\\Commands\\SyncData"}
[2025-01-01 12:00:00] local.NOTICE: Данные синхронизированы {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Console/Commands/SyncData.php:42","class":"App\\Console\\Commands\\SyncData"}
[2025-01-01 12:00:00] local.DEBUG: Execution time: 1 second {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Console/Commands/SyncData.php:42","class":"App\\Console\\Commands\\SyncData"}

Перехват вывода

Используйте трейт Faustoff\Contextify\Console\Outputable для перехвата вывода консоли Laravel из методов типа info() и сохранения его в логах:

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Faustoff\Contextify\Console\Outputable;

class SyncData extends Command
{
    use Outputable;

    protected $signature = 'data:sync';

    public function handle(): int
    {
        // Ваша бизнес-логика ...
        
        $this->info('Данные синхронизированы');

        return self::SUCCESS;
    }
}

Лог:

[2025-01-01 12:00:00] local.NOTICE: Данные синхронизированы {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Console/Commands/SyncData.php:42","class":"App\\Console\\Commands\\SyncData"}

Обработка сигналов завершения

Обрабатывайте сигналы завершения (SIGQUIT, SIGINT, SIGTERM по умолчанию) для корректного завершения. Используйте соответствующий трейт с SignalableCommandInterface:

  • TerminatableV62 для symfony/console:<6.3 (Laravel 9, 10)
  • TerminatableV63 для symfony/console:^6.3 (Laravel 9, 10)
  • TerminatableV70 для symfony/console:^7.0 (Laravel 11+)
namespace App\Console\Commands;

use Faustoff\Contextify\Console\TerminatableV62;
use Illuminate\Console\Command;
use Symfony\Component\Console\Command\SignalableCommandInterface;

class ConsumeStats extends Command implements SignalableCommandInterface
{
    use TerminatableV62;

    protected $signature = 'stats:consume';

    public function handle(): void
    {
        while (true) {
            // ...

            if ($this->shouldTerminate) {
                // Выполнение прервано обработчиком сигнала завершения
                break;
            }
        }
    }
}

Лог:

[2025-01-01 12:00:00] local.WARNING: Received SIGTERM (15) shutdown signal {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Console/Commands/ConsumeStats.php:42","class":"App\\Console\\Commands\\ConsumeStats"}

GitHub

Actions и UseCases в Laravel: практичный подход к бизнес-логике?

Сервисный слой часто разрастается до «толстых» классов, где сложно поддерживать код и переиспользовать отдельные части. В каком-то году по воле случая я смотрел ютубчик и наткнулся на подход Laravel Actions, основная суть заключается в создании простых классов, каждый из которых выполняет одну конкретную задачу (одно действие). Подход мне в целом понравился. Однако со временем, основной пакет стал обрастать фичами контекста, экшены как контроллер, как листенер, как консольная команда и т п, экшены стали размывать свою ответсвенность и хоть это и опционально, но я стал замечать, что во многих проектах это уже стало своего рода стандартом когда в один объект напихивают ответсвнности за все слои приложения.

Мне пришла идея создать пакет простых действий с решеним рутинных операций таких как “транзакции”, “кеширование”, “мемонизация”, “События”, “DIP”. А так же внедрить сценарный подход, когда есть объекты которые агрегируют простые и существующие действия в некий сценарий UseCase. Так пришло начало Simple Actions – атомарные Actions и сценарные UseCases. Пакет: lemax10/simple-actions (GitHub).

Ключевая идея

  • Action: один класс = одно действие.
  • UseCase: агрегирует несколько Actions в единый сценарий.
  • Плюс: транзакции, кеширование, мемоизация, события, DIP через контейнер.

Action: один объект = одно действие

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');

Плюсы:

  • Прозрачная ответственность и предсказуемость.
  • Лёгкая подмена реализации.
  • Удобное тестирование.

UseCase: сценарий из нескольких Actions

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());

Особенности:

  • UseCase выполняется в транзакции (идеально для «создать -> уведомить -> дополнить»).
  • Явная оркестрация шагов, легко читать и расширять.

Транзакции без бойлерплейта

  • UseCase — в транзакции по умолчанию.
  • Можно управлять из кода:
CreateOrderAction::make()->withTransaction()->run($user, $items);
SomeReadOnlyUseCase::make()->withoutTransaction()->run($id);

Кеширование результата (используя Laravel CacheManager)

Декларативно, без ручного 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).
  • Observer-подход как в Eloquent для группировки логики в отдельных объектах.

DIP через контейнер

Подмена реализаций без изменения 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

Нейминг:

  • Actions: CreateUserAction, GetUserAction, SendInvoiceAction
  • UseCases: RegisterUserUseCase, CheckoutOrderUseCase

Вместо итога

Actions и UseCases дают:

  • чистую, предсказуемую архитектуру;
  • высокую тестируемость и подменяемость (DIP);
  • меньше дублирования и бойлерплейта;
  • простые транзакции, декларативное кеширование и мемоизацию;
  • контроль жизненного цикла через события.
5