Dependency Injection Mastery
Master constructor injection, method injection, and advanced DI patterns
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
<?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.
<?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.
<?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.
<?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.
<?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.