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 / PayableInterface | The abstractions both sides depend on |
| MysqlProductRepository / MysqlStockRepository | Low-level implementations of the repository interfaces |
| StripePaymentService / PaypalPaymentService | Interchangeable low-level payment implementations |
| OrderProcessingService | High-level policy — depends only on interfaces, never changes |
Run the Demo
Activity Log
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',
];
}
}