Сервис-контейнер, провайдеры, свои artisan-команды и планировщик

Рейтинг: 59.6% · 10 голосов
Курс по Laravel: маршруты, Eloquent, Blade, миграции, очереди и API. Уроки по главам с обсуждением.
Ответить
Аватара пользователя
oleg_php
Сообщения: 25
Зарегистрирован: 14 май 2026, 08:06

Сервис-контейнер, провайдеры, свои artisan-команды и планировщик

Сообщение oleg_php »

АкадемияLaravel с нуляГлава 16 из 18
Оглавление курса (18)
  1. Знакомство с Laravel и установка окружения
  2. Маршруты и контроллеры
  3. Blade: шаблоны и вёрстка страниц
  4. Миграции и структура базы данных
  5. Eloquent ORM: модели и CRUD
  6. Связи в Eloquent: hasMany, belongsTo и другие
  7. Формы и валидация данных
  8. Аутентификация пользователей
  9. Middleware и защита маршрутов
  10. Очереди и фоновые задачи
  11. Отправка почты и уведомления
  12. Строим REST API на Laravel
  13. Авторизация: Gates и Policies
  14. Работа с файлами: загрузка, Storage, диски local и S3
  15. Тестирование: Pest и PHPUnit, фабрики, сидеры, RefreshDatabase
  16. Сервис-контейнер, провайдеры, свои artisan-команды и планировщик (вы здесь)
  17. Деплой в продакшен и обзор современного фронтенда (Vite, Livewire, Inertia)
  18. События и слушатели, кеширование, логирование
К шестнадцатой главе вы уже не раз пользовались сервис-контейнером, просто не называли его по имени. Каждый раз, когда Laravel сам подставлял Request в метод контроллера или CartService в конструктор, работал именно он. Пора разобрать механизм явно, а заодно научиться писать свои artisan-команды и вешать их на расписание. Примеры проверены на Laravel 12 и PHP 8.3.

Контейнер и привязки:

Контейнер умеет собрать любой класс, читая типы аргументов конструктора через рефлексию. Затык случается на интерфейсах: по интерфейсу не понять, какую реализацию создавать. Нужна привязка. Классический пример из практики: отправка SMS. Код зависит от интерфейса, а конкретный провайдер (smsc.ru, скажем) подключается в одном месте.

Код: Выделить всё

// app/Providers/AppServiceProvider.php
use App\Services\Sms\SmsSender;
use App\Services\Sms\SmscClient;

public function register(): void
{
    $this->app->singleton(SmsSender::class, function ($app) {
        return new SmscClient(
            login: config('services.smsc.login'),
            password: config('services.smsc.password'),
        );
    });
}
Способов привязки три. bind() создаёт новый объект при каждом resolve. singleton() держит один объект на весь процесс. scoped() даёт один объект на время одного HTTP-запроса или job'а, потом контейнер его выбрасывает. Под классическим PHP-FPM singleton и scoped ведут себя одинаково, процесс и так живёт ровно один запрос. Под Octane или в воркере очереди разница принципиальная: singleton с состоянием запроса (текущий пользователь, корзина) протечёт в соседний запрос. Правило: всё, что хранит состояние запроса, объявляйте через scoped, чистые stateless-сервисы через singleton. Есть ещё контекстные привязки (when, needs, give) на случай, когда одному конкретному классу нужна другая реализация интерфейса, ищите в документации contextual binding.

Провайдеры:

Привязкам нужно место регистрации, и это сервис-провайдеры. Пока проект маленький, всё живёт в AppServiceProvider, а когда он распухает, домен выносят в отдельный класс:

Код: Выделить всё

php artisan make:provider SmsServiceProvider
На Laravel 11 и новее команда сама допишет класс в bootstrap/providers.php, руками ничего регистрировать не нужно. У провайдера два метода с жёстким разделением ролей. В register() только привязки. В boot() контейнер уже собран целиком, здесь регистрируют view-композеры, обсерверы моделей, макросы.

Свои artisan-команды:

Код: Выделить всё

// php artisan make:command PruneStaleCarts
class PruneStaleCarts extends Command
{
    protected $signature = 'carts:prune {--days=14 : Удалять корзины старше N дней}';

    protected $description = 'Чистит брошенные корзины';

    public function handle(CartService $carts): int
    {
        $deleted = $carts->pruneOlderThan((int) $this->option('days'));

        $this->info("Удалено корзин: {$deleted}");

        return self::SUCCESS;
    }
}
Файл лежит в app/Console/Commands и подхватывается автоматически, никаких списков заполнять не надо. handle() резолвится через контейнер, поэтому зависимости объявляем прямо в сигнатуре метода. Возвращайте self::SUCCESS или self::FAILURE, по коду выхода мониторинг поймёт, что задача упала. Если команду нельзя запускать в два экземпляра параллельно, добавьте классу интерфейс Illuminate\Contracts\Console\Isolatable, и у команды появится флаг --isolated.

Планировщик:

С Laravel 11 консольного Kernel больше нет, расписание объявляется прямо в routes/console.php:

Код: Выделить всё

use Illuminate\Support\Facades\Schedule;

Schedule::command('carts:prune --days=30')->dailyAt('03:10');

Schedule::command('reports:daily')
    ->dailyAt('07:00')
    ->timezone('Europe/Moscow')
    ->withoutOverlapping(30)
    ->onOneServer()
    ->appendOutputTo(storage_path('logs/reports.log'));
На сервере нужна ровно одна строка в cron. Она дёргает планировщик каждую минуту, а тот сам решает, чьё время пришло:

Код: Выделить всё

* * * * * cd /var/www/shop && php artisan schedule:run >> /dev/null 2>&1
Локально cron не нужен, php artisan schedule:work крутит тот же цикл прямо в терминале. Что и когда запустится, показывает php artisan schedule:list.

Типичные грабли:

Резолв в register(). Прочитать config() можно, а вот доставать из контейнера чужой сервис в register() нельзя: порядок регистрации провайдеров не гарантирован, и однажды вы получите недособранную зависимость. Всё, что использует другие сервисы, живёт в boot().

Забытая строка в cron. Планировщик сам по себе не работает. Симптом классический: локально через schedule:work всё летает, на проде тишина.

withoutOverlapping() и onOneServer() держат блокировки в кэше, нужен драйвер с поддержкой локов (redis, database, memcached). И аккуратнее с php artisan cache:clear на проде: он снесёт и активные локи тоже.

Время. Cron живёт в таймзоне сервера, планировщик по умолчанию считает в app.timezone, а это чаще всего UTC. Не угадывайте, задавайте timezone() явно у задач, привязанных к человеческому времени.

Долгие задачи. По умолчанию задачи одного запуска идут последовательно, и отчёт на двадцать минут задержит всё остальное. Вешайте runInBackground(), а ещё лучше пусть команда просто кидает job в очередь из главы 10.

Итог: контейнер развязывает код от конкретных реализаций, провайдеры дают привязкам дом, команды и планировщик закрывают фоновую рутину без сторонних демонов. В следующей, последней главе доведём проект до продакшена: деплой, тот самый cron, supervisor для воркеров и обзор фронтенд-стека (Vite, Livewire, Inertia).
👍3 ❤️4 🔥5 😄 🤔
✔ Лучший ответ сформирован автоматически — ansibleandy
oleg_php писал(а):аккуратнее с php artisan cache:clear на проде: он снесёт и активные локи тоже плюсую, у нас так дважды стартанул импорт прайсов на 40к позиций. админ дернул cache:clear посреди деплоя, лок от withoutOverlapping испарился, второй инстанс радостно поехал. вынесли локи в отдельный redis-стор и спим спокойно
Перейти к ответу →
Аватара пользователя
pythonninja
Сообщения: 1
Зарегистрирован: 27 май 2026, 08:04

Re: Сервис-контейнер, провайдеры, свои artisan-команды и планировщик

Сообщение pythonninja »

Прошлой осенью переезжали на Octane и поймали ровно описанное: корзина одного юзера уехала другому, потому что сервис был singleton и держал в себе user_id. Дебажили два дня, в логах ноль криминала. С тех пор на ревью правило: видишь singleton со свойством, которое меняется в рантайме, заворачивай PR без разговоров.
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
ansibleandy
Сообщения: 1
Зарегистрирован: 11 май 2026, 19:28

Re: Сервис-контейнер, провайдеры, свои artisan-команды и планировщик

Сообщение ansibleandy »

✔ Лучший ответ — сформирован автоматически
oleg_php писал(а):аккуратнее с php artisan cache:clear на проде: он снесёт и активные локи тоже
плюсую, у нас так дважды стартанул импорт прайсов на 40к позиций. админ дернул cache:clear посреди деплоя, лок от withoutOverlapping испарился, второй инстанс радостно поехал. вынесли локи в отдельный redis-стор и спим спокойно
👍2 ❤️ 🔥1 😄 🤔
Аватара пользователя
lonelyburnout
Сообщения: 1
Зарегистрирован: 25 май 2026, 11:18

Re: Сервис-контейнер, провайдеры, свои artisan-команды и планировщик

Сообщение lonelyburnout »

schedule:work под supervisor жить может, но его придется рестартить при каждом деплое, как воркеры очередей, иначе будет крутиться старый код. крон тупее и потому надежнее: каждый запуск читает свежий код с диска. я за крон
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
ansible_andy
Сообщения: 1
Зарегистрирован: 01 июн 2026, 07:36

Re: Сервис-контейнер, провайдеры, свои artisan-команды и планировщик

Сообщение ansible_andy »

А чем плох schedule:work на проде, если завернуть его в supervisor? По сути тот же цикл, зато с crontab возиться не надо.
👍1 ❤️1 🔥 😄 🤔1
Ответить
← Предыдущая глава
Тестирование: Pest и PHPUnit, фабрики, сидеры, RefreshDatabase
Следующая глава →
Деплой в продакшен и обзор современного фронтенда (Vite, Livewire, Inertia)

Все главы курса «Laravel с нуля»

Поделиться темой: ✈ Telegram VK

Вернуться в «Laravel с нуля»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость