Menü schliessen
Created: January 16th 2026
Last updated: January 13th 2026
Categories: IT Development,  Laravel,  Php
Author: Ian Walser

Dependency Injection in Laravel: A Complete Guide for Junior Developers (With Real Examples)

What is Dependency Injection and Why Should You Care?

If you're a junior developer working with Laravel, you've probably heard the term "dependency injection" thrown around in tutorials, documentation, and discussions. Maybe you've even used it without fully understanding what's happening behind the scenes. Don't worry – you're not alone, and by the end of this post, you'll have a solid grasp of what dependency injection is, how it works in Laravel, and why it's such a powerful tool in your development arsenal.

Dependency injection (DI) is a design pattern that allows you to write cleaner, more testable, and more maintainable code. In simple terms, it's a way of giving an object the things it needs to do its job, rather than having the object create those things itself. Think of it like this: instead of a chef going to the market to buy ingredients every time they need to cook, someone delivers the ingredients to the kitchen. The chef can focus on cooking, and the supply chain is handled separately.

Laravel makes dependency injection incredibly easy to use, thanks to its powerful Service Container. Let's dive in and explore how it all works.

The Problem: Code Without Dependency Injection

Before we talk about solutions, let's look at the problem. Here's an example of code that doesn't use dependency injection:

<?php

namespace App\Http\Controllers;

use App\Services\PaymentProcessor;
use Illuminate\Http\Request;

class OrderController extends Controller
{
    public function processOrder(Request $request)
    {
        // Creating the dependency directly inside the method
        $paymentProcessor = new PaymentProcessor();
        
        $result = $paymentProcessor->charge(
            $request->input('amount'),
            $request->input('card_token')
        );
        
        return response()->json($result);
    }
}

At first glance, this might seem fine. But there are several problems lurking here:

  • Tight coupling: The controller is tightly coupled to the PaymentProcessor class. If you want to use a different payment processor, you have to modify the controller code.
  • Hard to test: When you write tests for this controller, you'll actually charge real credit cards (or at least try to) because you can't easily swap out the PaymentProcessor with a fake version.
  • Hidden dependencies: By looking at the controller's constructor or method signature, you can't tell what dependencies this class needs.
  • Difficult to maintain: As your application grows, managing these manual instantiations becomes a nightmare.

The Solution: Using Dependency Injection

Now let's rewrite the same code using dependency injection:

<?php

namespace App\Http\Controllers;

use App\Services\PaymentProcessor;
use Illuminate\Http\Request;

class OrderController extends Controller
{
    protected $paymentProcessor;
    
    // Dependency is injected through the constructor
    public function __construct(PaymentProcessor $paymentProcessor)
    {
        $this->paymentProcessor = $paymentProcessor;
    }
    
    public function processOrder(Request $request)
    {
        $result = $this->paymentProcessor->charge(
            $request->input('amount'),
            $request->input('card_token')
        );
        
        return response()->json($result);
    }
}

Notice the difference? Instead of creating the PaymentProcessor inside our method, we're receiving it through the constructor. Laravel's Service Container automatically resolves this dependency and injects it for us. We didn't have to write any configuration or setup code – Laravel just handles it.

Understanding Laravel's Service Container

The magic that makes dependency injection work in Laravel is the Service Container (also called the IoC Container, where IoC stands for "Inversion of Control"). Think of the Service Container as Laravel's brain for managing class dependencies and performing dependency injection.

How the Service Container Works

When Laravel encounters a type-hinted parameter in a constructor or method, it follows these steps:

  1. Inspection: Laravel looks at the type hint (like PaymentProcessor $paymentProcessor) and determines what class needs to be created.
  2. Resolution: The Service Container checks if it knows how to create an instance of that class. For most simple classes, it can do this automatically using PHP's Reflection API.
  3. Injection: The Service Container creates the instance (including resolving any dependencies that class might have) and injects it into your class.
  4. Caching: For certain bindings (singletons), the container can cache the instance for reuse throughout the request lifecycle.

Automatic Resolution

Here's the really cool part: for many classes, you don't need to tell Laravel anything. The Service Container can automatically figure out how to instantiate classes. Let's look at an example:

<?php

namespace App\Services;

class EmailService
{
    public function send($to, $subject, $body)
    {
        // Send email logic
    }
}

class UserService
{
    protected $emailService;
    
    // Laravel automatically resolves EmailService
    public function __construct(EmailService $emailService)
    {
        $this->emailService = $emailService;
    }
    
    public function createUser($data)
    {
        // Create user logic
        
        $this->emailService->send(
            $data['email'],
            'Welcome!',
            'Thanks for joining us.'
        );
    }
}

When you inject UserService somewhere, Laravel automatically creates an instance of EmailService and injects it into UserService. You don't have to configure anything – it just works!

Manual Binding in the Service Container

Sometimes you need more control over how classes are created. This is where manual binding comes in. You can register bindings in a service provider (typically AppServiceProvider):

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Services\PaymentProcessor;
use App\Services\StripePaymentProcessor;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        // Bind an interface to a concrete implementation
        $this->app->bind(PaymentProcessor::class, function ($app) {
            return new StripePaymentProcessor(
                config('services.stripe.key')
            );
        });
        
        // Or use a singleton (same instance throughout the request)
        $this->app->singleton(CacheManager::class, function ($app) {
            return new CacheManager(config('cache'));
        });
    }
}

This is especially useful when you're working with interfaces or when your class requires specific configuration during instantiation.

Constructor Injection: The Most Common Approach

Constructor injection is the most widely used form of dependency injection in Laravel. It's the pattern we've been using in our examples so far. Dependencies are passed to the class constructor, stored as properties, and then used throughout the class methods.

When to Use Constructor Injection

Use constructor injection when:

  • The dependency is required for the class to function properly
  • The dependency will be used across multiple methods in the class
  • You want to make dependencies explicit and obvious

Real-World Example: A Blog Post Controller

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use App\Services\SlugGenerator;
use App\Services\ImageOptimizer;
use Illuminate\Http\Request;

class PostController extends Controller
{
    protected $slugGenerator;
    protected $imageOptimizer;
    
    public function __construct(
        SlugGenerator $slugGenerator,
        ImageOptimizer $imageOptimizer
    ) {
        $this->slugGenerator = $slugGenerator;
        $this->imageOptimizer = $imageOptimizer;
    }
    
    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => 'required|max:255',
            'content' => 'required',
            'image' => 'required|image'
        ]);
        
        // Use injected dependencies
        $slug = $this->slugGenerator->generate($validated['title']);
        $optimizedImage = $this->imageOptimizer->optimize($request->file('image'));
        
        $post = Post::create([
            'title' => $validated['title'],
            'slug' => $slug,
            'content' => $validated['content'],
            'image' => $optimizedImage->path
        ]);
        
        return redirect()->route('posts.show', $post);
    }
    
    public function update(Request $request, Post $post)
    {
        // Dependencies available here too
        if ($request->has('title') && $request->title !== $post->title) {
            $post->slug = $this->slugGenerator->generate($request->title);
        }
        
        // Update logic...
        
        return redirect()->route('posts.show', $post);
    }
}

Notice how both store() and update() methods can use the injected dependencies. This is the power of constructor injection – the dependencies are available throughout the entire object's lifetime.

Method Injection: For Specific Use Cases

Method injection is when you type-hint dependencies directly in a method signature rather than in the constructor. Laravel's Service Container will automatically resolve these dependencies when the method is called.

When to Use Method Injection

Use method injection when:

  • The dependency is only needed for one specific method
  • You're working with route callbacks or controller methods
  • You want to keep your constructor clean and focused

Real-World Example: Method Injection in Controllers

<?php

namespace App\Http\Controllers;

use App\Models\Order;
use App\Services\InvoiceGenerator;
use Illuminate\Http\Request;

class OrderController extends Controller
{
    public function show(Order $order)
    {
        return view('orders.show', compact('order'));
    }
    
    // InvoiceGenerator is only needed here
    public function downloadInvoice(Order $order, InvoiceGenerator $generator)
    {
        $pdf = $generator->generate($order);
        
        return response()->download($pdf, "invoice-{$order->id}.pdf");
    }
    
    public function cancel(Order $order)
    {
        $order->update(['status' => 'cancelled']);
        
        return redirect()->route('orders.index');
    }
}

In this example, InvoiceGenerator is only used in the downloadInvoice() method, so there's no need to inject it through the constructor and make it available to all methods. Method injection keeps things clean and focused.

Method Injection in Route Closures

Method injection is particularly useful in route closures:

<?php

use App\Services\StatsCalculator;
use Illuminate\Support\Facades\Route;

Route::get('/dashboard', function (StatsCalculator $stats) {
    return view('dashboard', [
        'totalUsers' => $stats->getTotalUsers(),
        'activeUsers' => $stats->getActiveUsers(),
        'revenue' => $stats->getTotalRevenue()
    ]);
});

Laravel automatically resolves the StatsCalculator dependency when the route is accessed.

Combining Constructor and Method Injection

You can absolutely use both approaches in the same class:

<?php

namespace App\Http\Controllers;

use App\Models\User;
use App\Services\NotificationService;
use App\Services\ReportGenerator;
use Illuminate\Http\Request;

class UserController extends Controller
{
    protected $notifications;
    
    // Constructor injection for common dependencies
    public function __construct(NotificationService $notifications)
    {
        $this->notifications = $notifications;
    }
    
    public function destroy(User $user)
    {
        $user->delete();
        
        $this->notifications->send(
            $user,
            'Your account has been deleted'
        );
        
        return redirect()->route('users.index');
    }
    
    // Method injection for specific dependencies
    public function exportReport(ReportGenerator $generator)
    {
        $report = $generator->generateUserReport();
        
        return response()->download($report);
    }
}

Working with Interfaces and Dependency Injection

One of the most powerful aspects of dependency injection is the ability to program to interfaces rather than concrete implementations. This makes your code more flexible and easier to modify.

Creating an Interface

<?php

namespace App\Contracts;

interface PaymentGateway
{
    public function charge(float $amount, string $token): bool;
    public function refund(string $transactionId): bool;
}

Implementing the Interface

<?php

namespace App\Services;

use App\Contracts\PaymentGateway;

class StripeGateway implements PaymentGateway
{
    public function charge(float $amount, string $token): bool
    {
        // Stripe-specific implementation
        return true;
    }
    
    public function refund(string $transactionId): bool
    {
        // Stripe-specific implementation
        return true;
    }
}

class PayPalGateway implements PaymentGateway
{
    public function charge(float $amount, string $token): bool
    {
        // PayPal-specific implementation
        return true;
    }
    
    public function refund(string $transactionId): bool
    {
        // PayPal-specific implementation
        return true;
    }
}

Binding the Interface

In your AppServiceProvider, bind the interface to a concrete implementation:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Contracts\PaymentGateway;
use App\Services\StripeGateway;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind(PaymentGateway::class, StripeGateway::class);
        
        // Or with more complex logic
        $this->app->bind(PaymentGateway::class, function ($app) {
            if (config('payment.provider') === 'stripe') {
                return new StripeGateway();
            }
            return new PayPalGateway();
        });
    }
}

Using the Interface

<?php

namespace App\Http\Controllers;

use App\Contracts\PaymentGateway;
use Illuminate\Http\Request;

class CheckoutController extends Controller
{
    protected $gateway;
    
    // Type-hint the interface, not the concrete class
    public function __construct(PaymentGateway $gateway)
    {
        $this->gateway = $gateway;
    }
    
    public function process(Request $request)
    {
        $success = $this->gateway->charge(
            $request->input('amount'),
            $request->input('token')
        );
        
        if ($success) {
            return response()->json(['message' => 'Payment successful']);
        }
        
        return response()->json(['message' => 'Payment failed'], 400);
    }
}

Now your controller doesn't care whether you're using Stripe or PayPal – it just knows it has a PaymentGateway. You can switch between implementations by changing one line in your service provider. This is the essence of loose coupling!

Common Patterns and Best Practices

Repository Pattern with Dependency Injection

The repository pattern is commonly used with dependency injection to abstract database operations:

<?php

namespace App\Repositories;

use App\Models\User;

class UserRepository
{
    public function findById(int $id)
    {
        return User::findOrFail($id);
    }
    
    public function findByEmail(string $email)
    {
        return User::where('email', $email)->first();
    }
    
    public function create(array $data)
    {
        return User::create($data);
    }
}

// Usage in a controller
namespace App\Http\Controllers;

use App\Repositories\UserRepository;
use Illuminate\Http\Request;

class UserController extends Controller
{
    protected $users;
    
    public function __construct(UserRepository $users)
    {
        $this->users = $users;
    }
    
    public function show(int $id)
    {
        $user = $this->users->findById($id);
        return view('users.show', compact('user'));
    }
}

Service Layer Pattern

Organize business logic in service classes and inject them where needed:

<?php

namespace App\Services;

use App\Models\User;
use App\Repositories\UserRepository;
use Illuminate\Support\Facades\Hash;

class UserRegistrationService
{
    protected $users;
    protected $emailService;
    
    public function __construct(
        UserRepository $users,
        EmailService $emailService
    ) {
        $this->users = $users;
        $this->emailService = $emailService;
    }
    
    public function register(array $data)
    {
        $user = $this->users->create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => Hash::make($data['password'])
        ]);
        
        $this->emailService->sendWelcomeEmail($user);
        
        return $user;
    }
}

Avoiding Common Pitfalls

Here are some things to watch out for:

  • Don't over-inject: If you find yourself injecting 7-8 dependencies into a single class, it's probably doing too much. Consider breaking it into smaller, focused classes.
  • Be consistent: Stick to constructor injection for persistent dependencies and method injection for one-off needs. Don't mix patterns unnecessarily.
  • Type-hint properly: Always use type hints. Laravel's container relies on them for automatic resolution.
  • Understand the difference between bind and singleton: Use bind() when you need a fresh instance each time, and singleton() when you want to reuse the same instance.

Practical Benefits You'll Experience

Now that you understand how dependency injection works in Laravel, let's recap the concrete benefits you'll see in your daily development:

Cleaner, More Readable Code

Your class dependencies are clearly visible in the constructor or method signature. Anyone reading your code can instantly see what dependencies a class needs.

Easier Code Maintenance

When you need to change how a dependency works, you only need to modify the service provider binding or the dependency class itself. Your controllers and other classes that use the dependency don't need to change at all.

Better Code Organization

Dependency injection encourages you to break your code into focused, single-responsibility classes. This naturally leads to better architecture.

Flexibility

Swapping implementations becomes trivial. Need to switch from Stripe to PayPal? Change one line in your service provider. Need to use a different caching strategy? Same thing.

Future-Proof Your Code

As your application grows, well-structured dependency injection makes it easier to add new features without breaking existing functionality.

Next Steps: Putting It Into Practice

Understanding dependency injection is one thing; using it effectively is another. Here's how to start applying what you've learned:

  1. Start small: Look at one of your existing controllers. Can you extract some logic into a service class and inject it?
  2. Identify tight coupling: Find places in your code where you're using new ClassName(). Can these be replaced with dependency injection?
  3. Experiment with interfaces: Create an interface for a service you're using and implement it. Bind it in your service provider and see how it works.
  4. Read Laravel's documentation: The official Laravel docs have excellent information about the Service Container and dependency injection.
  5. Practice regularly: Like any programming concept, dependency injection becomes second nature with practice.

Conclusion

Dependency injection is one of the fundamental concepts that separates beginner Laravel developers from intermediate ones. It's not just about following best practices – it's about writing code that's genuinely easier to work with, easier to test, and easier to maintain as your application grows.

Laravel's Service Container makes dependency injection almost effortless. Once you understand the basic principles – inject dependencies rather than creating them, use constructor injection for persistent dependencies and method injection for specific ones, and leverage interfaces for flexibility – you'll find yourself writing better code naturally.

Don't feel like you need to refactor your entire application to use dependency injection everywhere. Start applying these concepts to new code you write, and gradually refactor old code as you work with it. Over time, you'll develop an intuition for when and how to use dependency injection effectively.

The most important thing is to keep practicing. Build projects, experiment with different approaches, and don't be afraid to make mistakes. Every Laravel developer has written tightly coupled code at some point – what matters is that you're learning and improving.