Контейнер и привязки:
Контейнер умеет собрать любой класс, читая типы аргументов конструктора через рефлексию. Затык случается на интерфейсах: по интерфейсу не понять, какую реализацию создавать. Нужна привязка. Классический пример из практики: отправка 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'),
);
});
}
Провайдеры:
Привязкам нужно место регистрации, и это сервис-провайдеры. Пока проект маленький, всё живёт в AppServiceProvider, а когда он распухает, домен выносят в отдельный класс:
Код: Выделить всё
php artisan make:provider SmsServiceProvider
Свои 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;
}
}
Планировщик:
С 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'));
Код: Выделить всё
* * * * * cd /var/www/shop && php artisan schedule:run >> /dev/null 2>&1
Типичные грабли:
Резолв в 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).