Dependency Injection Mastery

Master constructor injection, method injection, and advanced DI patterns

Dependency Injection is the foundation of Laravel's architecture. Understanding it deeply will make you a better Laravel developer.

Types of Dependency Injection in Laravel:
1. Constructor Injection - Dependencies injected when object is created
2. Method Injection - Dependencies injected into specific methods
3. Property Injection - NOT supported in Laravel (use constructor instead)

Where Laravel Auto-Injects:
✅ Controller constructors and methods
✅ Job constructors and handle() methods
✅ Event listener handle() methods
✅ Middleware constructors and handle() methods
✅ Form Request classes
✅ Artisan command constructors and handle() methods

Benefits of Dependency Injection:
• Testability - Easy to mock dependencies
• Flexibility - Swap implementations easily
• Maintainability - Clear dependencies
• Single Responsibility - Each class does one thing

Real-World Pattern:
Most services use constructor injection for always-needed dependencies, and method injection for sometimes-needed dependencies.

Code Examples

Constructor Injection - Always-Needed Dependencies
✓ Best Practice php
<?php
// Use constructor injection for dependencies needed by MULTIPLE methods
class OrderService
{
    // These are injected automatically by Laravel
    public function __construct(
        protected PaymentGateway $payment,
        protected EmailService $email,
        protected InventoryManager $inventory,
        protected LoggerInterface $logger
    ) {}
    
    public function createOrder(array $data): Order
    {
        // All injected dependencies are available
        $this->logger->info('Creating order', $data);
        
        $order = Order::create($data);
        $this->inventory->reserve($order->items);
        
        return $order;
    }
    
    public function processPayment(Order $order): bool
    {
        // Same dependencies available here
        $this->logger->info("Processing payment for order #{$order->id}");
        
        $result = $this->payment->charge($order->total);
        
        if ($result->successful()) {
            $this->email->sendReceipt($order);
            $this->inventory->deduct($order->items);
        }
        
        return $result->successful();
    }
    
    public function refund(Order $order): void
    {
        // All dependencies available in every method
        $this->payment->refund($order->total);
        $this->inventory->restore($order->items);
        $this->logger->info("Refunded order #{$order->id}");
    }
}

// Benefits:
// ✅ Dependencies declared once, available everywhere
// ✅ Clear what this class needs to function
// ✅ Easy to test (inject mocks in constructor)

💡 Constructor injection is for dependencies used by multiple methods. Declare them once in the constructor and use them throughout the class.

Method Injection - Sometimes-Needed Dependencies
✓ Best Practice php
<?php
// Use method injection for dependencies needed by ONE specific method
class PostController extends Controller
{
    // Constructor injection for always-needed dependencies
    public function __construct(
        protected PostRepository $posts
    ) {}
    
    // Method injection for sometimes-needed dependencies
    public function index(Request $request, CacheManager $cache)
    {
        // $request and $cache are only needed in this method
        $cacheKey = 'posts.page.' . $request->get('page', 1);
        
        return $cache->remember($cacheKey, 3600, function () {
            return $this->posts->paginate(15);
        });
    }
    
    public function store(
        StorePostRequest $request,  // Custom form request
        ImageOptimizer $optimizer,   // Only needed when creating
        SlugGenerator $slugger       // Only needed when creating
    ) {
        // These dependencies are ONLY needed for store()
        $slug = $slugger->generate($request->title);
        
        $post = $this->posts->create([
            'title' => $request->title,
            'slug' => $slug,
            'content' => $request->content,
        ]);
        
        if ($request->hasFile('image')) {
            $optimized = $optimizer->optimize($request->file('image'));
            $post->attachImage($optimized);
        }
        
        return redirect()->route('posts.show', $post);
    }
    
    public function show(Post $post)
    {
        // Implicit route model binding
        // Laravel automatically injects the resolved Post model
        return view('posts.show', compact('post'));
    }
}

// Why method injection?
// ✅ ImageOptimizer only needed in store(), not in index() or show()
// ✅ Keeps constructor clean
// ✅ Clear which method needs which dependency

💡 Method injection is perfect for dependencies only needed in one specific method. It keeps your constructor clean and makes dependencies crystal clear.

Testing with Dependency Injection
💡 Solution php
<?php
namespace Tests\Unit;

use Tests\TestCase;
use App\Services\OrderService;
use App\Contracts\PaymentGateway;
use App\Contracts\EmailService;
use Mockery;

class OrderServiceTest extends TestCase
{
    public function test_order_processes_payment_and_sends_email()
    {
        // Create mocks for dependencies
        $paymentMock = Mockery::mock(PaymentGateway::class);
        $emailMock = Mockery::mock(EmailService::class);
        $inventoryMock = Mockery::mock(InventoryManager::class);
        $loggerMock = Mockery::mock(LoggerInterface::class);
        
        // Define expected behavior
        $paymentMock->shouldReceive('charge')
                   ->once()
                   ->with(100.00)
                   ->andReturn((object)['successful' => true]);
        
        $emailMock->shouldReceive('sendReceipt')
                 ->once();
        
        $inventoryMock->shouldReceive('reserve')->once();
        $inventoryMock->shouldReceive('deduct')->once();
        
        $loggerMock->shouldReceive('info')->times(2);
        
        // Inject mocks into service
        $orderService = new OrderService(
            $paymentMock,
            $emailMock,
            $inventoryMock,
            $loggerMock
        );
        
        // Create test order
        $order = Order::factory()->make(['total' => 100.00]);
        
        // Test the service
        $result = $orderService->processPayment($order);
        
        // Assert
        $this->assertTrue($result);
    }
}

// Without dependency injection, this test would be IMPOSSIBLE
// You'd be making real payments to Stripe!

// With DI:
// ✅ No real API calls
// ✅ Fast tests (milliseconds)
// ✅ Predictable outcomes
// ✅ Easy to test edge cases

💡 Dependency Injection makes testing trivial. You inject mock objects instead of real ones, so tests are fast, isolated, and don't hit external APIs.

Advanced: Contextual Binding
💡 Solution php
<?php
// Sometimes you want DIFFERENT implementations for different classes
// app/Providers/AppServiceProvider.php
public function register(): void
{
    // Default binding for PaymentGateway
    $this->app->bind(
        PaymentGatewayInterface::class,
        StripePaymentGateway::class
    );
    
    // But for RefundController, use a different implementation
    $this->app->when(RefundController::class)
              ->needs(PaymentGatewayInterface::class)
              ->give(RefundSpecialistGateway::class);
    
    // For subscription charges, use recurring payment gateway
    $this->app->when(SubscriptionChargeJob::class)
              ->needs(PaymentGatewayInterface::class)
              ->give(RecurringPaymentGateway::class);
}

// Now:
// OrderController gets StripePaymentGateway (default)
// RefundController gets RefundSpecialistGateway (specialized)
// SubscriptionChargeJob gets RecurringPaymentGateway (optimized for recurring)

// Real-world use case:
// You might use different email providers:
$this->app->when(MarketingEmailJob::class)
          ->needs(EmailServiceInterface::class)
          ->give(SendGridEmailService::class);  // Bulk email service

$this->app->when(TransactionalEmailJob::class)
          ->needs(EmailServiceInterface::class)
          ->give(PostmarkEmailService::class);  // High deliverability

// Each job gets the RIGHT tool for the job!

💡 Contextual binding lets you provide different implementations based on which class is requesting the dependency. Perfect for specialized use cases.

Advanced: Automatic Dependency Resolution
💡 Solution php
<?php
// Laravel can resolve dependencies recursively!
// You don't need to bind everything manually

// Service class with nested dependencies
class ReportGenerator
{
    public function __construct(
        protected DatabaseConnection $db,
        protected PdfRenderer $pdf,
        protected CacheManager $cache
    ) {}
}

class PdfRenderer
{
    public function __construct(
        protected FontManager $fonts,
        protected ImageProcessor $images
    ) {}
}

class FontManager
{
    public function __construct(
        protected FileSystem $files
    ) {}
}

// You inject ReportGenerator into a controller:
class DashboardController extends Controller
{
    public function __construct(
        protected ReportGenerator $generator
    ) {}
}

// Laravel automatically resolves:
// 1. DashboardController needs ReportGenerator
// 2. ReportGenerator needs DatabaseConnection, PdfRenderer, CacheManager
// 3. PdfRenderer needs FontManager, ImageProcessor
// 4. FontManager needs FileSystem
// 5. Laravel creates: FileSystem → FontManager → ImageProcessor → PdfRenderer → DatabaseConnection → CacheManager → ReportGenerator → DashboardController

// You don't configure ANY of this (unless using interfaces)!
// Laravel's reflection system figures it out automatically.

// When do you need to bind?
// ✅ When using interfaces (must tell Laravel which implementation)
// ✅ When needing singletons
// ✅ When needing contextual binding
// ✅ When requiring complex setup

// When does Laravel auto-resolve?
// ✅ Concrete classes with type-hinted constructor params
// ✅ Classes with no constructor
// ✅ Classes with only primitive params (if you provide defaults)

💡 Laravel can automatically resolve dependency chains. You only need to bind interfaces to implementations or when you need singletons/special configuration.