Back to Blogs
2026-07-06 Abdelrhman Yasser

Service Container & Dependency Injection in Laravel

Service Container & Dependency Injection: Laravel's Heart

Reading Time: 30 minutes

Introduction

The Laravel Service Container is the framework's central nervous system — it manages class dependencies, performs dependency injection, and controls object instantiation. Understanding the container is essential for writing testable, decoupled, and maintainable Laravel applications.

Many developers use the container daily without realizing it: route model binding, controller method injection, queued jobs, and event listeners are all resolved through the container.

What Is Dependency Injection?

Dependency Injection (DI) is a design pattern where a class receives its dependencies from the outside rather than creating them internally.

// ❌ Without DI — hardcoded dependency
class UserController
{
    private UserService $service;

    public function __construct()
    {
        $this->service = new UserService(new MySQLUserRepository());
    }
}

// ✅ With DI — dependencies injected
class UserController
{
    public function __construct(
        private UserService $service
    ) {}
}

Benefits of DI:

BenefitDescription
TestabilitySwap real implementations with mocks
DecouplingClasses depend on interfaces, not concretions
FlexibilityChange behavior without modifying consumers
ReusabilitySame class works in different contexts

The Container in Action

graph TD
    A[Your Code] --> B[resolve / app->make]
    A --> C[Automatic Injection]
    B --> D[Service Container]
    C --> D
    D --> E{Binding Exists?}
    E -->|Yes| F[Resolve from Binding]
    E -->|No| G[Reflection Resolve]
    G --> H[Build Dependencies]
    H --> I[Return Instance]
    F --> I
    I --> J[Cached if singleton]

Zero-Configuration Resolution

The container can resolve classes without any explicit binding — as long as the class has no unresolvable dependencies:

class SimpleService
{
    public function doSomething(): string
    {
        return 'Done!';
    }
}

// No binding needed
$service = app(SimpleService::class);

Automatic Injection in Controllers

class UserController extends Controller
{
    public function __construct(
        private UserRepository $repository
    ) {}

    public function show(string $id): JsonResponse
    {
        $user = $this->repository->find($id);
        return response()->json($user);
    }
}

Laravel automatically resolves UserRepository from the container when the controller is instantiated.

Binding Types

Simple Binding (bind)

Every time you resolve, you get a new instance:

$this->app->bind(HelpSpot\API::class, function ($app) {
    return new HelpSpot\API($app->config->get('helpspot.api_key'));
});

Singleton Binding (singleton)

The same instance is returned on subsequent resolutions:

$this->app->singleton(HelpSpot\API::class, function ($app) {
    return new HelpSpot\API($app->config->get('helpspot.api_key'));
});

When to use singleton:

  • Stateless services — HTTP clients, API wrappers, mailers
  • Configuration holders — settings, feature flags
  • Expensive to instantiate — database connections, SDK clients
  • Event dispatchers — should have one instance

When NOT to use singleton:

  • Stateful services — if the service holds per-request state, each request needs a fresh instance
  • Eloquent models — always new instances

Instance Binding (instance)

Bind an already-existing object:

$api = new HelpSpot\API('token');
$this->app->instance(HelpSpot\API::class, $api);

Scoped Binding (scoped)

Same instance within the same container scope (useful for Octane/long-running processes):

$this->app->scoped(HelpSpot\API::class, function ($app) {
    return new HelpSpot\API($app->config->get('helpspot.api_key'));
});

In Octane, scoped ensures each request gets its own instance, unlike singleton which persists across requests.

Binding Primitives

When a class needs a primitive value (string, int, array):

$this->app->when(EmailService::class)
    ->needs('$apiKey')
    ->give(config('services.mailgun.api_key'));

$this->app->when(ReportGenerator::class)
    ->needs('$columns')
    ->give(['name', 'email', 'created_at']);

Binding Interfaces to Implementations

This is the most powerful pattern for decoupling:

// Interface
interface PaymentGateway
{
    public function charge(int $amount, array $details): Transaction;
}

// Implementation
class StripeGateway implements PaymentGateway
{
    public function charge(int $amount, array $details): Transaction
    {
        // Stripe API call
    }
}

// ServiceProvider
$this->app->bind(PaymentGateway::class, StripeGateway::class);

// Controller — depends on interface, not concrete
class CheckoutController extends Controller
{
    public function __construct(
        private PaymentGateway $gateway
    ) {}

    public function store(Request $request): JsonResponse
    {
        $transaction = $this->gateway->charge($request->amount, $request->all());
        return response()->json($transaction);
    }
}

Switching providers becomes a one-line change:

// Switch to PayPal
$this->app->bind(PaymentGateway::class, PayPalGateway::class);

Contextual Binding

When different classes need different implementations of the same interface:

$this->app->when(PhotoController::class)
    ->needs(Filesystem::class)
    ->give(function () {
        return Storage::disk('local');
    });

$this->app->when(VideoController::class)
    ->needs(Filesystem::class)
    ->give(function () {
        return Storage::disk('s3');
    });

Tagging

Group related bindings for batch resolution:

$this->app->tag([
    SpeedReport::class,
    MemoryReport::class,
    PerformanceReport::class,
], 'reports');

// Resolve all tagged bindings
$this->app->tagged('reports');
// Returns: [SpeedReport, MemoryReport, PerformanceReport]

Useful for report generators, event handlers, or any collection of related services.

Extending Resolved Instances

Modify or decorate a service after it has been resolved:

$this->app->extend(PaymentGateway::class, function ($gateway, $app) {
    return new LoggingPaymentGateway($gateway, $app->make(Logger::class));
});

The original service (StripeGateway) is passed in — you wrap it with a decorator.

Service Providers

Service providers are the central place to configure the container.

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Bind interfaces to implementations
        $this->app->bind(PaymentGateway::class, StripeGateway::class);

        // Register services
        $this->app->singleton(HelpSpot\API::class, function ($app) {
            return new HelpSpot\API($app->config->get('helpspot.api_key'));
        });
    }

    public function boot(): void
    {
        // After all providers are registered
        // Great place for event listeners, route macros, etc.
    }
}

Deferred Providers

Services that are only resolved when needed:

class StripeServiceProvider extends ServiceProvider
{
    // Only loaded when Stripe\Client or PaymentGateway is resolved
    public function provides(): array
    {
        return [PaymentGateway::class, Stripe\Client::class];
    }
}

Container Events

Resolving Callbacks

Fired every time a type is resolved:

$this->app->resolving(PaymentGateway::class, function ($gateway, $app) {
    // Configure the gateway after it's constructed
    $gateway->setLogger($app->make(Logger::class));
});

Rebinding Callbacks

Fired when a binding is overridden:

$this->app->rebinding(PaymentGateway::class, function ($app, $gateway) {
    // Notify other services that the gateway changed
});

Facades

Facades provide a static-like interface to services resolved from the container.

// Facade
Cache::get('key');

// Is equivalent to:
$app->make('cache')->get('key');

How Facades Work

class Cache extends Facade
{
    protected static function getFacadeAccessor(): string
    {
        return 'cache';
    }
}

Cache::get() is resolved at runtime — the facade gets the cache binding from the container and calls get() on it.

Real-Time Facades

Turn any class into a facade:

use Facades\App\Services\PaymentGateway;

PaymentGateway::charge(100, []);

Performance Impact

OperationCostFrequency
Reflection resolution (no binding)MediumFirst call only
Closure bindingLowRegistration
Singleton resolution (cached)NegligibleEvery call
Facade resolutionLowEvery call

In production, the route cache and config cache eliminate most container overhead.

Common Mistakes

Mistake 1: Constructor Overload

// ❌ Too many dependencies — violates Single Responsibility
class OrderController extends Controller
{
    public function __construct(
        private OrderService $orders,
        private PaymentGateway $gateway,
        private InventoryService $inventory,
        private NotificationService $notifications,
        private Logger $logger,
        private Mailer $mailer,
        private Cache $cache,
        private Analytics $analytics,
    ) {}
}

Fix: Split the controller or use action classes.

Mistake 2: Binding Too Many Things

// ❌ Binding everything "just in case"
$this->app->bind(Logger::class, Logger::class);
$this->app->bind(Cache::class, Cache::class);

Fix: Only bind interfaces, third-party SDKs, or classes that need configuration.

Mistake 3: Using app() in Views or Blade

// ❌ Service locator in template
{{ app(Logger::class)->info('view rendered') }}

Fix: Pass data from the controller via view()->with() or view composers.

Mistake 4: Forgetting to Use Interfaces

// ❌ Registered concrete class — can't easily swap
$this->app->bind(StripeGateway::class, StripeGateway::class);

// ✅ Register interface — swap by changing one line
$this->app->bind(PaymentGateway::class, StripeGateway::class);

Service Container vs Service Locator

AspectService ContainerService Locator
How it gets dependenciesInjected via constructorPulled from a registry
TestabilityExcellent (mock via container)Poor (hidden dependencies)
DiscoverabilityVisible in constructor signatureHidden inside methods
Framework supportBuilt into LaravelPattern used by app() helper

Recommendation: Prefer constructor injection over resolve() or app()->make().

Interview Questions

  1. What is the difference between bind and singleton?
  2. How does contextual binding work and when would you use it?
  3. Explain the register() vs boot() difference in service providers.
  4. What are facades and how do they work under the hood?
  5. When would you use tag and tagged?
  6. How does the container resolve classes without explicit binding?
  7. What is the problem with using app() inside views?

Related Articles

# Laravel# PHP# Dependency Injection# Architecture# IoC
WhatsApp
Chat on WhatsApp