symfony/service-contracts
Symfony Service Contracts provides lightweight, battle-tested abstractions extracted from Symfony components. Use these shared interfaces to build interoperable libraries and apps with proven semantics and consistent behavior across the Symfony ecosystem.
## Getting Started
### Minimal Setup
1. **Install the package**:
```bash
composer require symfony/service-contracts
No configuration is needed—this package provides only interfaces, so it integrates seamlessly with Laravel’s native PSR-11 container (Illuminate\Container).
First use case: Lazy-loading a service in a Job
Replace constructor injection with ServiceSubscriberInterface for optional dependencies:
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Symfony\Contracts\Service\ServiceLocator;
class ProcessPayment implements ServiceSubscriberInterface
{
public static function getSubscribedServices(): array
{
return ['payment.gateway']; // Lazy-loaded via ServiceLocator
}
public function handle(ServiceLocator $locator)
{
$gateway = $locator->get('payment.gateway');
$gateway->charge(...);
}
}
Register the Job normally (e.g., via HandleJobsMiddleware or dispatch()).
Verify PSR-11 compliance:
Test service resolution in bootstrap/app.php:
$container = app();
$container->get('mailer'); // Should work without errors
Pattern: Use ServiceSubscriberInterface for services that are:
Example: A Command with conditional dependencies:
class ExportData implements ServiceSubscriberInterface
{
public static function getSubscribedServices(): array
{
return ['s3.client', 'mailer']; // Only 's3.client' is required
}
public function handle(ServiceLocator $locator)
{
$s3 = $locator->get('s3.client');
$mailer = $locator->has('mailer') ? $locator->get('mailer') : null;
// Use $mailer only if available
}
}
Pattern: Inject collections of services (e.g., event subscribers, middleware) via ServiceLocator.
Use case: Avoid circular dependencies when multiple services depend on each other.
Example: Registering event subscribers dynamically:
use Symfony\Contracts\EventDispatcher\EventSubscriberInterface;
class EventDispatcherService implements ServiceSubscriberInterface
{
public static function getSubscribedServices(): array
{
return ['event.subscribers']; // Array of subscribers
}
public function __invoke(ServiceLocator $locator)
{
$subscribers = $locator->get('event.subscribers');
foreach ($subscribers as $subscriber) {
if ($subscriber instanceof EventSubscriberInterface) {
$dispatcher->addSubscriber($subscriber);
}
}
}
}
Pattern: Type-hint for contracts in shared packages to avoid hard dependencies.
Use case: Build reusable libraries (e.g., laravel-notifications) that work with Symfony or Laravel.
Example: A notification service interface:
use Symfony\Contracts\Service\ServiceLocator;
interface NotificationService
{
public function send(ServiceLocator $locator, string $message);
}
Implementation in Laravel:
class LaravelNotificationService implements NotificationService
{
public function send(ServiceLocator $locator, string $message)
{
$mailer = $locator->get('mailer');
$mailer->send(new Mailable($message));
}
}
Pattern: Replace the container with a ServiceLocator in tests to isolate dependencies.
Use case: Unit testing Jobs, Commands, or domain services without bootstrapping the full app.
Example: Mocking a service in a test:
use Symfony\Contracts\Service\ServiceLocator;
public function testProcessPayment()
{
$mockGateway = $this->createMock(PaymentGateway::class);
$locator = new ServiceLocator([
'payment.gateway' => $mockGateway,
]);
$job = new ProcessPayment();
$job->handle($locator);
$mockGateway->expects($this->once())
->method('charge');
}
Pattern: Use contracts to bridge Laravel with Symfony’s ecosystem (e.g., Messenger, HTTP Client).
Use case: Adopt Symfony’s HttpClient or Messenger while keeping Laravel’s frontend.
Example: Using HttpClient with Laravel’s container:
use Symfony\Contracts\HttpClient\HttpClientInterface;
class ApiClient implements ServiceSubscriberInterface
{
public static function getSubscribedServices(): array
{
return ['http.client']; // Symfony HttpClient
}
public function fetch(ServiceLocator $locator, string $url)
{
$client = $locator->get('http.client');
return $client->request('GET', $url);
}
}
Register the client in Laravel:
$container->bind('http.client', function () {
return SymfonyHttpClient::create();
});
Missing Service Exceptions
ServiceLocator::get() throws ServiceNotFoundException if a service is unbound.has() to check availability or provide default services:
$locator->get('service', function () {
return new FallbackService();
});
Request Scoping Leaks
ServiceLocator instances across requests (e.g., in static properties) causes stale references.ServiceLocator instances. Use constructor injection for request-scoped services:
class MyService {
public function __construct(private ServiceLocator $locator) {}
}
Circular Dependencies
ServiceSubscriberInterface classes depending on each other.event.dispatcher) or lazy-load only one side.Laravel-Specific Quirks
ServiceSubscriberInterface like Symfony.spatie/laravel-service-container for auto-registration.Performance Overhead
bootstrap/app.php:
$container->get('db.connection'); // Force load early
$locator->get('service', function () {
logger()->debug('Creating fallback service');
return new FallbackService();
});
composer require --dev symfony/dependency-injection
php vendor/bin/dependency-injection-checker check
dd(app()->getBindings()); // Laravel 8+
// or
dd(app()->getServiceNames()); // Laravel 9+
Custom Service Locator
Extend ServiceLocator to add Laravel-specific features (e.g., request scoping):
class LaravelServiceLocator extends ServiceLocator
{
public function getRequestScoped(string $id)
{
return app('request')->offsetGet($id);
}
}
Auto-Registration
Build a Laravel service provider to auto-register ServiceSubscriberInterface classes:
use Symfony\Contracts\Service\ServiceSubscriberInterface;
class ServiceSubscriberProvider extends ServiceProvider
{
public function register()
{
foreach (glob(app_path('Services/*Subscriber.php')) as $file) {
$class = str_replace('.php', '', basename($file));
$this->app->bind($class, $file);
}
}
}
Fallback Services Implement a global fallback for missing services:
$locator = new ServiceLocator([
'defaultServices' => [
'logger' => fn() => new NullLogger(),
],
]);
ServiceSubscriberInterface. Manually bind subscribers:
$container->bind(ProcessPayment::class, function () {
return new ProcessPayment();
});
ServiceLocator for request-scoped services. Use Laravel’s native scoping:
$container->bindWhen(
RequestScopedService::class,
fn() => new RequestScopedService(),
fn($c) => $c->isRequestScoped()
);
MockApplicationHow can I help you explore Laravel packages today?