Подвохи: Service Container и Dependency Injection
Service Container -- фундамент Laravel. На экзамене это одна из самых сложных тем с множеством ловушек.
Binding: bind vs singleton vs scoped
Разница между типами привязок
<?php
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// bind: NEW instance every time
$this->app->bind(PaymentGateway::class, StripeGateway::class);
// singleton: SAME instance for entire request lifecycle
$this->app->singleton(CartService::class, function ($app) {
return new CartService($app->make(PaymentGateway::class));
});
// scoped: SAME instance within scope, NEW between scopes
// (useful with Octane, queue workers)
$this->app->scoped(RequestContext::class, function () {
return new RequestContext();
});
// instance: bind existing object
$this->app->instance('config.custom', ['key' => 'value']);
}
}
ЛОВУШКА 1: Какой метод использовать?
<?php
// Question: How many instances of Logger are created?
$this->app->bind(Logger::class, FileLogger::class);
$a = app(Logger::class);
$b = app(Logger::class);
$c = app(Logger::class);
// Answer: 3 instances (bind creates new each time)
// Question: And with singleton?
$this->app->singleton(Logger::class, FileLogger::class);
$a = app(Logger::class);
$b = app(Logger::class);
$c = app(Logger::class);
// Answer: 1 instance (all three are the same object)
$a === $b; // true
$b === $c; // true
ЛОВУШКА 2: scoped vs singleton с Octane
<?php
// With Laravel Octane, singleton persists across requests!
// scoped resets between requests
// DANGEROUS with Octane:
$this->app->singleton(UserContext::class, function () {
return new UserContext(auth()->user()); // User leaks between requests!
});
// SAFE with Octane:
$this->app->scoped(UserContext::class, function () {
return new UserContext(auth()->user()); // Resets each request
});
Contextual Binding
<?php
use App\Services\Notifications\EmailNotifier;
use App\Services\Notifications\SmsNotifier;
use App\Services\Notifications\NotifierInterface;
use App\Http\Controllers\OrderController;
use App\Http\Controllers\AlertController;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// When OrderController needs NotifierInterface, give EmailNotifier
$this->app->when(OrderController::class)
->needs(NotifierInterface::class)
->give(EmailNotifier::class);
// When AlertController needs NotifierInterface, give SmsNotifier
$this->app->when(AlertController::class)
->needs(NotifierInterface::class)
->give(SmsNotifier::class);
}
}
ЛОВУШКА 3: Contextual binding с примитивами
<?php
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Binding primitive values (not interfaces!)
$this->app->when(ReportGenerator::class)
->needs('$format')
->give('pdf');
// Using config value
$this->app->when(MailService::class)
->needs('$maxRetries')
->giveConfig('mail.max_retries');
// Using tagged services
$this->app->when(NotificationManager::class)
->needs('$channels')
->giveTagged('notification.channels');
}
}
Экзаменационный вопрос: "Можно ли использовать contextual binding для примитивных типов (string, int)?"
Ответ: Да! Через $this->app->when()->needs('$paramName')->give('value'). Обратите внимание: имя параметра с $.
Tagged Services
<?php
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind('report.csv', CsvReportGenerator::class);
$this->app->bind('report.pdf', PdfReportGenerator::class);
$this->app->bind('report.excel', ExcelReportGenerator::class);
// Tag multiple bindings
$this->app->tag(
['report.csv', 'report.pdf', 'report.excel'],
'reports'
);
}
}
// Resolve all tagged services
$generators = app()->tagged('reports');
// Returns iterable of all three generators
foreach ($generators as $generator) {
$generator->generate($data);
}
ЛОВУШКА 4: tagged() возвращает iterable, а не массив или коллекцию. Для получения массива используйте iterator_to_array().
Facade Resolution
Как работают фасады (под капотом)
<?php
// When you call:
Cache::get('key');
// Laravel actually does:
// 1. Cache extends Facade
// 2. Facade::__callStatic('get', ['key']) is triggered
// 3. getFacadeAccessor() returns 'cache'
// 4. app('cache')->get('key') is called on the resolved instance
// The accessor:
class Cache extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'cache'; // This is the container binding key
}
}
ЛОВУШКА 5: Facade accessor может быть:
- Строка (binding key в контейнере):
'cache' - Класс (FQCN):
CacheManager::class
<?php
// String accessor -> resolves from container binding
protected static function getFacadeAccessor(): string
{
return 'cache'; // app('cache')
}
// Class accessor -> resolves class from container
protected static function getFacadeAccessor(): string
{
return \Illuminate\Cache\CacheManager::class; // app(CacheManager::class)
}
Real-time Facades
<?php
// Instead of importing the class and injecting it:
use App\Services\PaymentProcessor;
class OrderController
{
public function __construct(
private readonly PaymentProcessor $processor,
) {}
}
// You can use Real-time Facade:
use Facades\App\Services\PaymentProcessor;
class OrderController
{
public function store()
{
// Works like a facade!
PaymentProcessor::process($order);
// And can be mocked:
// PaymentProcessor::shouldReceive('process')->once();
}
}
ЛОВУШКА 6: Real-time Facades создаются добавлением Facades\ перед полным именем класса. Это работает для ЛЮБОГО класса, не только зарегистрированного в контейнере. Laravel автоматически генерирует facade-обёртку.
Экзаменационный вопрос: "Нужно ли регистрировать Real-time Facade в Service Provider?" Ответ: Нет! Laravel автоматически resolve-ит класс через контейнер. Регистрация не нужна.
Deferred Providers
<?php
declare(strict_types=1);
namespace App\Providers;
use App\Services\HeavyService;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;
class HeavyServiceProvider extends ServiceProvider implements DeferrableProvider
{
public function register(): void
{
$this->app->singleton(HeavyService::class, function () {
// This is only executed when HeavyService is actually resolved!
return new HeavyService(
config('services.heavy.api_key'),
);
});
}
// REQUIRED: Tell Laravel which bindings this provider provides
public function provides(): array
{
return [HeavyService::class];
}
}
ЛОВУШКА 7: Deferred Provider ОБЯЗАН:
- Реализовать интерфейс
DeferrableProvider - Определить метод
provides()со списком предоставляемых биндингов register()вызывается ТОЛЬКО когда один из binding запрашивается
<?php
// Without DeferrableProvider:
// register() is called on EVERY request (even if HeavyService is not needed)
// With DeferrableProvider:
// register() is called ONLY when HeavyService is resolved
// This improves performance when the service is rarely used
Экзаменационный вопрос: "Когда вызывается boot() у deferred provider?"
Ответ: boot() вызывается СРАЗУ ПОСЛЕ register(), когда провайдер наконец загружается (при первом resolve binding). НЕ при загрузке приложения.
Binding Interfaces to Implementations
<?php
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Simple binding
$this->app->bind(
\App\Contracts\PaymentGateway::class,
\App\Services\StripeGateway::class
);
// With closure (for complex construction)
$this->app->bind(
\App\Contracts\Logger::class,
function ($app) {
return new \App\Services\FileLogger(
storage_path('logs/app.log'),
$app->make('config')->get('app.log_level'),
);
}
);
}
}
ЛОВУШКА 8: Auto-resolution (автоматическое разрешение)
<?php
// Laravel CAN automatically resolve classes WITHOUT explicit binding
// IF the class has no interface dependency or all dependencies are resolvable
class UserService
{
public function __construct(
private readonly UserRepository $repo, // Auto-resolved if concrete class
) {}
}
// No binding needed! Laravel creates UserService and UserRepository automatically
$service = app(UserService::class); // Works!
// BUT if you depend on an INTERFACE:
class UserService
{
public function __construct(
private readonly UserRepositoryInterface $repo, // Cannot auto-resolve!
) {}
}
// This FAILS without explicit binding:
$service = app(UserService::class);
// BindingResolutionException: Target [UserRepositoryInterface] is not instantiable
Container Events
<?php
use Illuminate\Support\Facades\App;
// Called when ANY dependency is resolved
App::resolving(function (mixed $object, $app) {
// Runs for every resolution
});
// Called when specific class is resolved
App::resolving(UserService::class, function (UserService $service, $app) {
// Modify $service after creation
$service->setLogger($app->make(Logger::class));
});
// afterResolving: called after resolving callbacks
App::afterResolving(UserService::class, function (UserService $service) {
// Final modifications
});
Методы resolve
<?php
// All ways to resolve from container:
// 1. app() helper
$service = app(UserService::class);
$service = app('user.service');
// 2. make() method
$service = app()->make(UserService::class);
// 3. resolve() helper
$service = resolve(UserService::class);
// 4. Automatic injection (constructor/method)
public function __construct(private readonly UserService $service) {}
// 5. makeWith() - with parameters
$service = app()->makeWith(UserService::class, ['config' => $config]);
ЛОВУШКА 9: app() vs app()->make() -- функционально идентичны. Но makeWith() позволяет передать параметры конструктора на лету.
register() vs boot()
<?php
class AppServiceProvider extends ServiceProvider
{
// REGISTER: only bind things into container
// Called BEFORE all providers are registered
// Do NOT use other services here!
public function register(): void
{
$this->app->singleton(UserService::class, function ($app) {
return new UserService(
$app->make(UserRepository::class),
);
});
// WRONG in register():
// Route::get(...); // Routes not available yet!
// view()->share(...); // Views not registered yet!
}
// BOOT: use services, register routes, views, etc.
// Called AFTER all providers are registered
// All services are available here
public function boot(): void
{
// Correct place for:
view()->share('appName', config('app.name'));
\Illuminate\Database\Eloquent\Model::preventLazyLoading();
// Publishing config, views, migrations
$this->publishes([
__DIR__ . '/../config/custom.php' => config_path('custom.php'),
]);
}
}
ЛОВУШКА 10: В register() нельзя использовать другие сервисы -- они могут быть ещё не зарегистрированы. Используйте boot() для любой логики, зависящей от других сервисов.