0

Dependency — one class needing another class to function

Beginner5 min read·eng-16-011

Concept

Dependency — when one class needs another class (or value) to function. Class A has a dependency on class B if A uses B to do its work.

Types of dependencies:

  • Hard dependency (bad): Class A creates its own instance of B inside itself. $this->logger = new FileLogger(). A is permanently coupled to FileLogger.
  • Soft dependency / Injected dependency (good): B is passed into A from outside. A depends on an interface, not a concrete class. Can be swapped.

Dependency Injection (DI): The practice of passing dependencies into a class rather than creating them inside. A class declares what it needs (via constructor parameters), and something else provides it.

Why injection is better:

  • Testable: Pass a mock/stub in tests. Without DI, you can't control what FileLogger does in tests.
  • Flexible: Swap FileLogger for NullLogger without changing A.
  • Explicit: Dependencies are visible from the constructor signature. With internal creation, it's hidden.

Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Both should depend on abstractions. OrderService shouldn't depend on MySQLOrderRepository — it should depend on OrderRepositoryInterface. The actual implementation is swapped in the service container.

Service Container: Laravel's container is a Dependency Injection Container (DIC). It knows how to construct objects and their dependencies. app(OrderService::class) — the container instantiates OrderService and injects its dependencies automatically.

Code Example

php
<?php
// ❌ HARD DEPENDENCY — tight coupling, untestable
class OrderService
{
    private FileLogger $logger;
    private MySQLOrderRepository $repo;

    public function __construct()
    {
        // Creates its own dependencies — can't swap, can't test in isolation
        $this->logger = new FileLogger('/var/log/orders.log');
        $this->repo   = new MySQLOrderRepository();
    }

    public function createOrder(array $data): Order
    {
        $order = $this->repo->save($data);
        $this->logger->log('Order created: ' . $order->id);
        return $order;
    }
}

// ✅ DEPENDENCY INJECTION — loose coupling, testable
interface LoggerInterface {
    public function log(string $msg): void;
}

interface OrderRepositoryInterface {
    public function save(array $data): Order;
}

class OrderService
{
    public function __construct(
        private readonly OrderRepositoryInterface $repo,   // depends on interface
        private readonly LoggerInterface          $logger, // depends on interface
    ) {}

    public function createOrder(array $data): Order
    {
        $order = $this->repo->save($data);
        $this->logger->log('Order created: ' . $order->id);
        return $order;
    }
}

// Concrete implementations (injected by the container)
class EloquentOrderRepository implements OrderRepositoryInterface
{
    public function save(array $data): Order { return Order::create($data); }
}

class FileLogger implements LoggerInterface
{
    public function log(string $msg): void { file_put_contents('/var/log/orders.log', $msg . "\n", FILE_APPEND); }
}

// Laravel service container — resolves dependencies automatically
// In AppServiceProvider:
$this->app->bind(OrderRepositoryInterface::class, EloquentOrderRepository::class);
$this->app->bind(LoggerInterface::class, FileLogger::class);

// Usage — container injects automatically
$service = app(OrderService::class);
// Container sees: OrderService needs OrderRepositoryInterface → creates EloquentOrderRepository
//                 OrderService needs LoggerInterface → creates FileLogger
// All wired up without manual instantiation

// Testing — inject test doubles
class OrderServiceTest extends \PHPUnit\Framework\TestCase
{
    public function test_creates_order(): void
    {
        $mockRepo   = $this->createMock(OrderRepositoryInterface::class);
        $nullLogger = new class implements LoggerInterface { public function log(string $msg): void {} };

        $mockRepo->expects($this->once())->method('save')->willReturn(new Order(['id' => 1]));

        $service = new OrderService($mockRepo, $nullLogger); // inject test doubles!
        $order   = $service->createOrder(['total' => 100]);

        $this->assertEquals(1, $order->id);
        // No real DB, no real file system — test is fast and isolated
    }
}