D — SOLID Design Principle

Dependency Inversion Principle

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Problem

OrderProcessingService's constructor directly instantiates MysqlProductRepository, MysqlStockRepository, and StripePaymentService. This high-level business logic is welded to low-level implementation details — switching database or payment provider means editing this class.

Solution

Introduce interfaces (ProductRepositoryInterface, StockRepositoryInterface, PayableInterface). OrderProcessingService depends only on these abstractions and receives concrete implementations via constructor injection — Laravel's service container decides which concrete class to hand over.

Analogy

A lamp plugs into a standard wall socket (the abstraction) — it doesn't care whether the electricity behind it comes from a coal plant, solar panels, or a generator. Swap the power source; the lamp never changes.

Participants (After)

Interface / Class Role
ProductRepositoryInterface / StockRepositoryInterface / PayableInterfaceThe abstractions both sides depend on
MysqlProductRepository / MysqlStockRepositoryLow-level implementations of the repository interfaces
StripePaymentService / PaypalPaymentServiceInterchangeable low-level payment implementations
OrderProcessingServiceHigh-level policy — depends only on interfaces, never changes

Run the Demo

Activity Log

Press a button above to see the pattern in action.

Source Code

Before: MysqlProductRepository.php
<?php

declare(strict_types=1);

namespace App\SolidPrinciples\Dip\Before;

/**
 * DIP VIOLATION SETUP — a concrete repository with no interface at all.
 * Nothing forces callers to depend on an abstraction.
 */
class MysqlProductRepository
{
    /** @var array<int, array{id:int,name:string,price:float}> */
    private array $products = [
        1 => ['id' => 1, 'name' => 'Widget', 'price' => 49.99],
    ];

    public function getById(int $productId): ?array
    {
        return $this->products[$productId] ?? null;
    }
}
Before: MysqlStockRepository.php
<?php

declare(strict_types=1);

namespace App\SolidPrinciples\Dip\Before;

class MysqlStockRepository
{
    private const MINIMUM_STOCK_LEVEL = 1;

    /** @var array<int, int> */
    private array $stock = [1 => 5];

    public function forProduct(int $productId): int
    {
        return $this->stock[$productId] ?? 0;
    }

    public function checkAvailability(int $productId): void
    {
        if ($this->forProduct($productId) < self::MINIMUM_STOCK_LEVEL) {
            throw new \RuntimeException('We are out of stock');
        }
    }

    public function record(int $productId): int
    {
        $this->stock[$productId]--;
        return $this->stock[$productId];
    }
}
Before: StripePaymentService.php
<?php

declare(strict_types=1);

namespace App\SolidPrinciples\Dip\Before;

class StripePaymentService
{
    public function process(string $total): string
    {
        return 'Processing payment of £' . $total . ' through Stripe';
    }
}
Before: OrderProcessingService.php
<?php

declare(strict_types=1);

namespace App\SolidPrinciples\Dip\Before;

/**
 * DIP VIOLATION — this HIGH-LEVEL service depends directly on THREE
 * LOW-LEVEL concrete classes (MysqlProductRepository, MysqlStockRepository,
 * StripePaymentService). Switching database, ORM, or payment provider
 * means editing this class's constructor and every call site that
 * builds one — the high-level policy is welded to low-level details.
 */
class OrderProcessingService
{
    private MysqlProductRepository $productRepository;
    private MysqlStockRepository $stockRepository;
    private StripePaymentService $paymentService;

    public function __construct()
    {
        // Hard-wired to concrete classes — cannot swap MySQL for
        // Postgres, or Stripe for PayPal, without editing this line.
        $this->productRepository = new MysqlProductRepository();
        $this->stockRepository = new MysqlStockRepository();
        $this->paymentService = new StripePaymentService();
    }

    public function execute(int $productId): array
    {
        $product = $this->productRepository->getById($productId);
        if (!$product) {
            throw new \RuntimeException('Product not found');
        }

        $this->stockRepository->checkAvailability($productId);

        $discount = 0.20 * $product['price'];
        $total = number_format($product['price'] - $discount, 2);

        $paymentMessage = $this->paymentService->process($total);

        $remainingStock = $this->stockRepository->record($productId);

        return [
            'payment_message'  => $paymentMessage,
            'discounted_price' => $total,
            'original_price'   => $product['price'],
            'remaining_stock'  => $remainingStock,
            'message'          => 'Thank you, your order is being processed',
        ];
    }
}
After: ProductRepositoryInterface.php
<?php

declare(strict_types=1);

namespace App\SolidPrinciples\Dip\After;

/**
 * DIP FIX — the abstraction. OrderProcessingService will depend on
 * THIS, never on a concrete MySQL/Postgres/whatever implementation.
 */
interface ProductRepositoryInterface
{
    public function getById(int $productId): ?array;
}
After: StockRepositoryInterface.php
<?php

declare(strict_types=1);

namespace App\SolidPrinciples\Dip\After;

interface StockRepositoryInterface
{
    public function forProduct(int $productId): int;
    public function checkAvailability(int $productId): void;
    public function record(int $productId): int;
}
After: PayableInterface.php
<?php

declare(strict_types=1);

namespace App\SolidPrinciples\Dip\After;

interface PayableInterface
{
    public function process(string $total): string;
}
After: MysqlProductRepository.php
<?php

declare(strict_types=1);

namespace App\SolidPrinciples\Dip\After;

/**
 * DIP FIX — a concrete LOW-LEVEL implementation of the abstraction.
 * OrderProcessingService never references this class name directly.
 */
class MysqlProductRepository implements ProductRepositoryInterface
{
    /** @var array<int, array{id:int,name:string,price:float}> */
    private array $products = [
        1 => ['id' => 1, 'name' => 'Widget', 'price' => 49.99],
    ];

    public function getById(int $productId): ?array
    {
        return $this->products[$productId] ?? null;
    }
}
After: MysqlStockRepository.php
<?php

declare(strict_types=1);

namespace App\SolidPrinciples\Dip\After;

class MysqlStockRepository implements StockRepositoryInterface
{
    private const MINIMUM_STOCK_LEVEL = 1;

    /** @var array<int, int> */
    private array $stock = [1 => 5];

    public function forProduct(int $productId): int
    {
        return $this->stock[$productId] ?? 0;
    }

    public function checkAvailability(int $productId): void
    {
        if ($this->forProduct($productId) < self::MINIMUM_STOCK_LEVEL) {
            throw new \RuntimeException('We are out of stock');
        }
    }

    public function record(int $productId): int
    {
        $this->stock[$productId]--;
        return $this->stock[$productId];
    }
}
After: StripePaymentService.php
<?php

declare(strict_types=1);

namespace App\SolidPrinciples\Dip\After;

class StripePaymentService implements PayableInterface
{
    public function process(string $total): string
    {
        return 'Processing payment of £' . $total . ' through Stripe';
    }
}
After: PaypalPaymentService.php
<?php

declare(strict_types=1);

namespace App\SolidPrinciples\Dip\After;

/**
 * DIP FIX PROOF — a completely different payment provider, added
 * WITHOUT touching OrderProcessingService at all. Because the service
 * depends on PayableInterface (not StripePaymentService), any class
 * implementing that interface can be swapped in.
 */
class PaypalPaymentService implements PayableInterface
{
    public function process(string $total): string
    {
        return 'Processing payment of £' . $total . ' through PayPal';
    }
}
After: OrderProcessingService.php
<?php

declare(strict_types=1);

namespace App\SolidPrinciples\Dip\After;

/**
 * DIP FIX — this HIGH-LEVEL service depends only on ABSTRACTIONS
 * (the three interfaces), never on concrete classes. Which concrete
 * implementation gets injected is decided elsewhere (e.g. Laravel's
 * service container in AppServiceProvider::register()) — this class
 * never changes no matter which database or payment provider is used.
 */
class OrderProcessingService
{
    public function __construct(
        private ProductRepositoryInterface $productRepository,
        private StockRepositoryInterface $stockRepository,
        private PayableInterface $paymentService,
    ) {}

    public function execute(int $productId): array
    {
        $product = $this->productRepository->getById($productId);
        if (!$product) {
            throw new \RuntimeException('Product not found');
        }

        $this->stockRepository->checkAvailability($productId);

        $discount = 0.20 * $product['price'];
        $total = number_format($product['price'] - $discount, 2);

        $paymentMessage = $this->paymentService->process($total);

        $remainingStock = $this->stockRepository->record($productId);

        return [
            'payment_message'  => $paymentMessage,
            'discounted_price' => $total,
            'original_price'   => $product['price'],
            'remaining_stock'  => $remainingStock,
            'message'          => 'Thank you, your order is being processed',
        ];
    }
}