4.2 Service Container
Resumo
Service Container (container de serviços) — container IoC do Laravel para gerenciar dependências.
Registro:
bind()(instância nova),singleton()(uma por request),instance()(objeto que já existe).Importante: injeção automática pelo construtor. Interface liga na implementação.
Conteúdo
O que é
O que é: Service Container — o núcleo do Laravel para gerenciar dependências. Cria os objetos e injeta as dependências sozinho.
O essencial:
- Registro de serviços (
bind,singleton) - Resolução de dependências (injeção automática)
- Dependency Injection pelo construtor
Como funciona
Registro de serviços:
// app/Providers/AppServiceProvider.php
public function register(): void
{
// bind — instância nova toda vez
$this->app->bind(PaymentGateway::class, StripeGateway::class);
// singleton — uma instância por request
$this->app->singleton(CacheService::class, function ($app) {
return new CacheService(
$app->make('cache.store')
);
});
// instance — usa um objeto que já existe
$logger = new Logger('app');
$this->app->instance(Logger::class, $logger);
}Resolução de dependências:
// 1. Pelo construtor (automático)
class OrderController extends Controller
{
// O Laravel cria o OrderService sozinho
public function __construct(
private OrderService $orderService
) {}
}
// 2. Pelo helper app()
$service = app(OrderService::class);
// 3. Por resolve()
$service = resolve(OrderService::class);
// 4. Por make()
$service = app()->make(OrderService::class);
// 5. Pela facade
use Illuminate\Support\Facades\App;
$service = App::make(OrderService::class);Contextual Binding (binding contextual):
// Implementações diferentes para classes diferentes
public function register(): void
{
// OrderService recebe StripeGateway
$this->app->when(OrderService::class)
->needs(PaymentGateway::class)
->give(StripeGateway::class);
// RefundService recebe PayPalGateway
$this->app->when(RefundService::class)
->needs(PaymentGateway::class)
->give(PayPalGateway::class);
}Binding de interfaces:
// Interface
interface PaymentGateway
{
public function charge(int $amount): bool;
}
// Implementação
class StripeGateway implements PaymentGateway
{
public function charge(int $amount): bool
{
// Stripe API
}
}
// Registro
public function register(): void
{
$this->app->bind(
PaymentGateway::class,
StripeGateway::class
);
}
// Uso
class OrderService
{
// Recebe StripeGateway sozinho
public function __construct(
private PaymentGateway $gateway
) {}
}Quando usar
Use quando:
- Precisa trocar a implementação (interfaces)
- Teste (mock das dependências)
- Serviços singleton (logger, por exemplo)
- Inicialização pesada do objeto
Não use quando:
- Value object simples, sem dependência
- DTO (Data Transfer Objects)
- Models Eloquent (eles se criam sozinhos)
Exemplo prático
Exemplo típico com interfaces:
// 1. Interface (app/Contracts/NotificationService.php)
interface NotificationService
{
public function send(User $user, string $message): void;
}
// 2. Implementações
class EmailNotificationService implements NotificationService
{
public function __construct(
private Mailer $mailer
) {}
public function send(User $user, string $message): void
{
$this->mailer->to($user->email)->send(new Notification($message));
}
}
class SmsNotificationService implements NotificationService
{
public function __construct(
private SmsGateway $gateway
) {}
public function send(User $user, string $message): void
{
$this->gateway->send($user->phone, $message);
}
}
// 3. Registro (app/Providers/AppServiceProvider.php)
public function register(): void
{
// Escolhe a implementação pelo config
$this->app->bind(
NotificationService::class,
config('notifications.driver') === 'sms'
? SmsNotificationService::class
: EmailNotificationService::class
);
}
// 4. Uso
class OrderService
{
public function __construct(
private NotificationService $notificationService
) {}
public function create(User $user, array $data): Order
{
$order = Order::create($data);
// Envia por email ou SMS, conforme o config
$this->notificationService->send(
$user,
"Pedido #{$order->id} criado"
);
return $order;
}
}Singleton para operações caras:
// Service Provider
public function register(): void
{
// Uma instância por request
$this->app->singleton(ElasticsearchClient::class, function ($app) {
return new ElasticsearchClient(
config('services.elasticsearch.host')
);
});
}
// Uso em controllers diferentes
class ProductController
{
public function __construct(
private ElasticsearchClient $elasticsearch
) {}
public function search(Request $request)
{
// Mesma instância do UserController
return $this->elasticsearch->search($request->query('q'));
}
}Contextual Binding para implementações diferentes:
// Service Provider
public function register(): void
{
// ProductService usa ProductCache
$this->app->when(ProductService::class)
->needs(CacheRepository::class)
->give(function ($app) {
return new ProductCache(
$app->make('cache.store'),
ttl: 3600
);
});
// UserService usa UserCache
$this->app->when(UserService::class)
->needs(CacheRepository::class)
->give(function ($app) {
return new UserCache(
$app->make('cache.store'),
ttl: 600
);
});
}Tagged Services (agrupamento):
// Registro com tags
public function register(): void
{
$this->app->bind(StripePayment::class);
$this->app->bind(PayPalPayment::class);
$this->app->bind(YandexPayment::class);
$this->app->tag([
StripePayment::class,
PayPalPayment::class,
YandexPayment::class,
], 'payment.gateways');
}
// Uso de todos os serviços da tag
class PaymentRouter
{
private array $gateways;
public function __construct()
{
// Pega todos os serviços da tag
$this->gateways = app()->tagged('payment.gateways');
}
public function route(string $method): PaymentGateway
{
foreach ($this->gateways as $gateway) {
if ($gateway->supports($method)) {
return $gateway;
}
}
throw new UnsupportedPaymentException();
}
}Extending (estender serviços já registrados):
// Altera um registro que já existe
public function register(): void
{
$this->app->extend(PaymentService::class, function ($service, $app) {
// Adiciona log
return new PaymentServiceWithLogging(
$service,
$app->make(Logger::class)
);
});
}Teste com mock:
// tests/Feature/OrderTest.php
public function test_order_creation_sends_notification()
{
// Mock NotificationService
$notificationMock = Mockery::mock(NotificationService::class);
$notificationMock->shouldReceive('send')
->once()
->with(Mockery::type(User::class), Mockery::type('string'));
// Substitui no container
$this->app->instance(NotificationService::class, $notificationMock);
// Teste
$user = User::factory()->create();
$response = $this->actingAs($user)->postJson('/api/orders', [
'product_id' => 1,
'quantity' => 2,
]);
$response->assertStatus(201);
}Na entrevista
"Service Container é o container IoC do Laravel para DI. bind() cria instância nova toda vez, singleton() é uma por request. A injeção pelo construtor é automática. Eu registro no Service Provider. Interface liga na implementação com bind(Interface::class, Implementation::class). Contextual binding quando a implementação muda conforme a classe. No teste eu substituo com app()->instance()."
Exercícios práticos
Exercício 1: Configure o Contextual Binding
Enunciado: Você tem dois serviços: EmailService e SmsService. Os dois implementam NotificationInterface. OrderService usa email. UserService usa SMS. Configure o container.
Solução
// 1. Interface (app/Contracts/NotificationInterface.php)
interface NotificationInterface
{
public function send(string $message): void;
}
// 2. Implementações
class EmailService implements NotificationInterface
{
public function send(string $message): void
{
Mail::raw($message, function ($mail) {
$mail->to('joao@email.com');
});
}
}
class SmsService implements NotificationInterface
{
public function send(string $message): void
{
// SMS gateway API
}
}
// 3. Service Provider (app/Providers/AppServiceProvider.php)
public function register(): void
{
// OrderService recebe EmailService
$this->app->when(OrderService::class)
->needs(NotificationInterface::class)
->give(EmailService::class);
// UserService recebe SmsService
$this->app->when(UserService::class)
->needs(NotificationInterface::class)
->give(SmsService::class);
}
// 4. Uso
class OrderService
{
public function __construct(
private NotificationInterface $notification
) {}
public function create(array $data): Order
{
$order = Order::create($data);
// Envia por email
$this->notification->send("Pedido #{$order->id} criado");
return $order;
}
}
class UserService
{
public function __construct(
private NotificationInterface $notification
) {}
public function register(array $data): User
{
$user = User::create($data);
// Envia por SMS
$this->notification->send("Bem-vindo, {$user->name}!");
return $user;
}
}Exercício 2: Singleton vs Bind
Enunciado: Quando usar singleton() e quando usar bind()? Dê exemplos.
Solução
// ✅ SINGLETON — uma instância por request
// Use para:
// - Operação cara (conexão de DB, cliente HTTP)
// - Serviço stateful (cache, logger)
// - Serviço sem estado mutável compartilhado de propósito
public function register(): void
{
// 1. Database Connection
$this->app->singleton(DatabaseConnection::class, function ($app) {
return new DatabaseConnection(
config('database.host'),
config('database.port')
);
});
// 2. Logger (stateful)
$this->app->singleton(Logger::class, function ($app) {
return new Logger(storage_path('logs/app.log'));
});
// 3. HTTP Client
$this->app->singleton(HttpClient::class, function ($app) {
return new HttpClient([
'base_uri' => config('services.api.base_url'),
'timeout' => 30,
]);
});
// 4. Cache Service
$this->app->singleton(CacheService::class, function ($app) {
return new CacheService($app->make('cache.store'));
});
}
// ✅ BIND — instância nova toda vez
// Use para:
// - Serviço stateless
// - Value Objects
// - Serviço com estado mutável
public function register(): void
{
// 1. Order Calculator (cada cálculo — objeto novo)
$this->app->bind(OrderCalculator::class, function ($app) {
return new OrderCalculator(
taxRate: config('shop.tax_rate')
);
});
// 2. PDF Generator (arquivo novo toda vez)
$this->app->bind(PdfGenerator::class, function ($app) {
return new PdfGenerator();
});
// 3. Payment Processor (transação nova)
$this->app->bind(PaymentProcessor::class, function ($app) {
return new PaymentProcessor(
$app->make(PaymentGateway::class)
);
});
}
// Exemplo do problema: bind no lugar de singleton
class OrderService
{
public function __construct(
private DatabaseConnection $db // ❌ Conexão nova toda vez!
) {}
}
// Se registrou com bind():
$service1 = app(OrderService::class); // Cria DB connection #1
$service2 = app(OrderService::class); // Cria DB connection #2 (ruim!)
// Se registrou com singleton():
$service1 = app(OrderService::class); // Cria DB connection #1
$service2 = app(OrderService::class); // Reusa DB connection #1 (bom!)Regra:
- singleton() — se o serviço é caro ou stateful
- bind() — se o serviço é barato e stateless
Exercício 3: Mock do serviço no teste
Enunciado: Você precisa testar o OrderService sem chamar o PaymentGateway de verdade. Como substituir?
Solução
// Service (app/Services/OrderService.php)
class OrderService
{
public function __construct(
private PaymentGateway $paymentGateway
) {}
public function create(User $user, array $data): Order
{
$order = Order::create([
'user_id' => $user->id,
'total' => $data['total'],
]);
// Cobra pelo gateway
$this->paymentGateway->charge($order->total);
return $order;
}
}
// Test (tests/Feature/OrderServiceTest.php)
use Mockery;
use Tests\TestCase;
class OrderServiceTest extends TestCase
{
public function test_order_creation_charges_payment()
{
// 1. Cria o mock PaymentGateway
$paymentMock = Mockery::mock(PaymentGateway::class);
// 2. Configura as expectativas
$paymentMock->shouldReceive('charge')
->once()
->with(1000)
->andReturn(true);
// 3. Substitui no container
$this->app->instance(PaymentGateway::class, $paymentMock);
// 4. Teste
$user = User::factory()->create();
$service = app(OrderService::class); // Recebe o nosso mock
$order = $service->create($user, ['total' => 1000]);
// 5. Checagens
$this->assertDatabaseHas('orders', [
'id' => $order->id,
'user_id' => $user->id,
'total' => 1000,
]);
}
public function test_payment_failure_throws_exception()
{
// Mock que lança exceção
$paymentMock = Mockery::mock(PaymentGateway::class);
$paymentMock->shouldReceive('charge')
->once()
->andThrow(new PaymentException('Cartão recusado'));
$this->app->instance(PaymentGateway::class, $paymentMock);
// Esperamos a exceção
$this->expectException(PaymentException::class);
$this->expectExceptionMessage('Cartão recusado');
$user = User::factory()->create();
$service = app(OrderService::class);
$service->create($user, ['total' => 1000]);
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
}
// Alternativa: Laravel Mock (sem Mockery)
class OrderServiceTest extends TestCase
{
public function test_with_laravel_mock()
{
// Cria o mock
$paymentMock = $this->createMock(PaymentGateway::class);
// Configura as expectativas
$paymentMock->expects($this->once())
->method('charge')
->with(1000)
->willReturn(true);
// Substitui
$this->app->instance(PaymentGateway::class, $paymentMock);
// Teste
$user = User::factory()->create();
$service = app(OrderService::class);
$order = $service->create($user, ['total' => 1000]);
$this->assertEquals(1000, $order->total);
}
}Formas de substituir:
- app()->instance() — substitui qualquer classe
- Mockery::mock() — framework de mock mais poderoso
- createMock() — mock nativo do PHPUnit
- bind() no teste — registro temporário
// Forma 4: registro temporário
public function test_with_fake_implementation()
{
// Implementação fake
$this->app->bind(PaymentGateway::class, function () {
return new class implements PaymentGateway {
public function charge(int $amount): bool
{
return true; // Sempre dá certo
}
};
});
// Teste
$service = app(OrderService::class);
// ...
}Parte do PHP/Laravel Interview Handbook | Feito com ❤️ pela equipe CodeMate