Dependency Inversion, Final Classes, and Pragmatic Testing in PHP 8.4

The evolution of PHP 8.4 brings powerful features that fundamentally change how we approach dependency inversion, class design, and testing strategies. While the SOLID principles remain timeless, the implementation details have evolved significantly with modern PHP capabilities, and the testing community has developed more nuanced approaches that go beyond the traditional "mock everything" mentality.

This comprehensive guide explores the intersection of three critical concepts: dependency inversion principle (DIP) with PHP 8.4's final classes, composition over inheritance patterns, and the pragmatic testing philosophy that combines the best of Detroit School (classical) and London School (mockist) approaches. We'll examine when to use real objects versus mocks, how to leverage union types for flexible testing, and why the "mockist vs classical TDD" debate has evolved into a more sophisticated understanding of testing strategies.

Understanding Dependency Inversion in Modern PHP

The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules—both should depend on abstractions. In PHP 8.4, this principle takes on new dimensions with enhanced language features like property hooks, asymmetric visibility, and improved type system capabilities.

The Problem: Violating Dependency Inversion

Let's first examine what not to do. The following pseudocode demonstrates a classic violation of DIP:

# Dependency Inversion Principle - Core Concepts

# 1. VIOLATION: High-level module depends on low-level module
CLASS OrderProcessor
    PROPERTY sqlDatabase AS MySqlDatabase  # Direct dependency!
    
    METHOD processOrder(order)
        sql = "INSERT INTO orders..."
        this.sqlDatabase.execute(sql)  # Tight coupling
    END METHOD
END CLASS

# 2. SOLUTION: Both depend on abstraction
INTERFACE IOrderStorage
    METHOD saveOrder(order) -> boolean
END INTERFACE

CLASS OrderProcessor  # High-level module
    PROPERTY storage AS IOrderStorage  # Depends on abstraction
    
    CONSTRUCTOR(storage AS IOrderStorage)
        this.storage = storage
    END CONSTRUCTOR
    
    METHOD processOrder(order)
        RETURN this.storage.saveOrder(order)  # Loose coupling
    END METHOD
END CLASS

CLASS MySqlOrderStorage IMPLEMENTS IOrderStorage  # Low-level module
    METHOD saveOrder(order) -> boolean
        # MySQL implementation details
        RETURN true
    END METHOD
END CLASS

# 3. FINAL CLASSES WITH COMPOSITION
FINAL CLASS PaymentProcessor
    PROPERTY paymentGateway AS IPaymentGateway
    PROPERTY logger AS ILogger
    
    CONSTRUCTOR(gateway AS IPaymentGateway, logger AS ILogger)
        this.paymentGateway = gateway
        this.logger = logger
    END CONSTRUCTOR
    
    METHOD processPayment(amount, method)
        this.logger.info("Processing payment: " + amount)
        result = this.paymentGateway.charge(amount, method)
        
        IF result.success THEN
            this.logger.info("Payment successful")
        ELSE
            this.logger.error("Payment failed: " + result.error)
        END IF
        
        RETURN result
    END METHOD
END CLASS

# 4. TESTING STRATEGY: Detroit vs London Schools
# Detroit School (Classical) - Use real objects when possible
TEST_CLASS OrderProcessorTest
    METHOD testProcessOrderWithInMemoryStorage()
        # Use real, lightweight implementation
        storage = NEW InMemoryOrderStorage()
        processor = NEW OrderProcessor(storage)
        order = NEW Order("123", 100.00)
        
        result = processor.processOrder(order)
        
        ASSERT result == true
        ASSERT storage.getOrderCount() == 1
    END METHOD
END TEST_CLASS

# London School (Mockist) - Mock dependencies for interaction testing
TEST_CLASS PaymentProcessorTest
    METHOD testProcessPaymentLogsCorrectly()
        # Mock when testing interactions are important
        mockGateway = MOCK IPaymentGateway
        mockLogger = MOCK ILogger
        processor = NEW PaymentProcessor(mockGateway, mockLogger)
        
        EXPECT mockGateway.charge(100, "credit") RETURNS successResult
        EXPECT mockLogger.info("Processing payment: 100")
        EXPECT mockLogger.info("Payment successful")
        
        processor.processPayment(100, "credit")
        
        VERIFY_ALL_EXPECTATIONS()
    END METHOD
END TEST_CLASS

# 5. PRAGMATIC APPROACH: When to Mock vs Use Real Objects
# Use REAL objects when:
# - Easy to instantiate and configure
# - No external dependencies (database, network)
# - Fast execution
# - Deterministic behavior

# Use MOCKS when:
# - External resources (database, HTTP APIs)
# - Slow operations
# - Non-deterministic behavior (random, time-based)
# - Testing error conditions
# - Interaction verification is important

# 6. UNION TYPES FOR FLEXIBLE TESTING (Modern approach)
INTERFACE ITestableService
    METHOD performOperation(data) -> Result
END INTERFACE

# Allow both real service AND mock in tests
TYPE TestService = RealService | MockService
WHERE MockService IMPLEMENTS ITestableService
  AND RealService IMPLEMENTS ITestableService

TEST_CLASS FlexibleServiceTest
    METHOD testWithRealService()
        service AS TestService = NEW RealService()
        result = service.performOperation(testData)
        ASSERT result.isValid()
    END METHOD
    
    METHOD testWithMockService()
        service AS TestService = MOCK MockService
        EXPECT service.performOperation(testData) RETURNS expectedResult
        # Test continues...
    END METHOD
END TEST_CLASS

This inheritance-heavy approach creates several problems:

  • Tight coupling: High-level OrderProcessor depends directly on low-level MySqlDatabase
  • Hard to test: Cannot isolate business logic from database concerns
  • Brittle inheritance: Changes to base class affect all subclasses
  • Limited extensibility: Adding new database types requires modifying existing code

Here's how this anti-pattern manifests in PHP code:

<?php

declare(strict_types=1);

/**
 * WRONG: Inheritance-heavy design with tight coupling
 * Problems:
 * - Violates Dependency Inversion Principle
 * - Hard to test in isolation
 * - Brittle inheritance hierarchy
 * - Cannot be final (extensible by design)
 */

// Base class that does too much
abstract class BaseOrderProcessor
{
    protected MySqlDatabase $database;
    protected EmailService $emailService;
    protected PaymentGateway $paymentGateway;
    
    public function __construct()
    {
        // Direct dependencies - violation of DIP!
        $this->database = new MySqlDatabase('localhost', 'orders_db');
        $this->emailService = new SmtpEmailService('smtp.example.com');
        $this->paymentGateway = new StripePaymentGateway('sk_test_123');
    }
    
    // Template method forcing inheritance
    abstract protected function validateOrder(array $orderData): bool;
    abstract protected function calculateTax(float $amount): float;
    
    public function processOrder(array $orderData): bool
    {
        // Rigid workflow - hard to change
        if (!$this->validateOrder($orderData)) {
            return false;
        }
        
        $amount = $orderData['amount'];
        $tax = $this->calculateTax($amount);
        $total = $amount + $tax;
        
        // Direct database access - tight coupling
        $orderId = $this->database->insertOrder([
            'customer_id' => $orderData['customer_id'],
            'amount' => $total,
            'status' => 'pending'
        ]);
        
        // Payment processing mixed with order logic
        $paymentResult = $this->paymentGateway->charge(
            $total,
            $orderData['payment_method']
        );
        
        if ($paymentResult->isSuccess()) {
            $this->database->updateOrderStatus($orderId, 'paid');
            $this->emailService->sendConfirmation(
                $orderData['customer_email'],
                $orderId
            );
            return true;
        }
        
        $this->database->updateOrderStatus($orderId, 'failed');
        return false;
    }
}

// Concrete implementations forced to extend
class StandardOrderProcessor extends BaseOrderProcessor
{
    protected function validateOrder(array $orderData): bool
    {
        return isset($orderData['customer_id'], $orderData['amount']);
    }
    
    protected function calculateTax(float $amount): float
    {
        return $amount * 0.08; // 8% tax
    }
}

class PremiumOrderProcessor extends BaseOrderProcessor
{
    protected function validateOrder(array $orderData): bool
    {
        return isset($orderData['customer_id'], $orderData['amount'])
            && $orderData['amount'] >= 100.00;
    }
    
    protected function calculateTax(float $amount): float
    {
        return $amount * 0.05; // 5% tax for premium customers
    }
}

/**
 * Supporting classes with their own problems
 */
class MySqlDatabase
{
    private string $host;
    private string $database;
    private PDO $connection;
    
    public function __construct(string $host, string $database)
    {
        $this->host = $host;
        $this->database = $database;
        // Direct connection in constructor - hard to test
        $this->connection = new PDO(
            "mysql:host={$host};dbname={$database}",
            'username',
            'password'
        );
    }
    
    public function insertOrder(array $data): int
    {
        // SQL directly embedded - not reusable
        $sql = "INSERT INTO orders (customer_id, amount, status, created_at) 
                VALUES (?, ?, ?, NOW())";
        $stmt = $this->connection->prepare($sql);
        $stmt->execute([
            $data['customer_id'],
            $data['amount'],
            $data['status']
        ]);
        return (int) $this->connection->lastInsertId();
    }
    
    public function updateOrderStatus(int $orderId, string $status): bool
    {
        $sql = "UPDATE orders SET status = ? WHERE id = ?";
        $stmt = $this->connection->prepare($sql);
        return $stmt->execute([$status, $orderId]);
    }
}

class SmtpEmailService
{
    private string $smtpHost;
    
    public function __construct(string $smtpHost)
    {
        $this->smtpHost = $smtpHost;
    }
    
    public function sendConfirmation(string $email, int $orderId): bool
    {
        // Direct SMTP call - external dependency
        return mail(
            $email,
            'Order Confirmation',
            "Your order #{$orderId} has been confirmed."
        );
    }
}

class StripePaymentGateway
{
    private string $apiKey;
    
    public function __construct(string $apiKey)
    {
        $this->apiKey = $apiKey;
    }
    
    public function charge(float $amount, string $paymentMethod): PaymentResult
    {
        // Simulated Stripe API call - external dependency
        // In reality, this would make HTTP requests
        $success = random_int(1, 10) > 2; // 80% success rate
        
        return new PaymentResult(
            $success,
            $success ? null : 'Payment declined'
        );
    }
}

class PaymentResult
{
    public function __construct(
        private bool $success,
        private ?string $error = null
    ) {}
    
    public function isSuccess(): bool
    {
        return $this->success;
    }
    
    public function getError(): ?string
    {
        return $this->error;
    }
}

/**
 * TESTING NIGHTMARE - Why this approach is problematic
 */

// Attempting to test this is painful:
// - Cannot mock dependencies (they're created in constructor)
// - Tests hit real database and email service
// - Inheritance makes it hard to test individual pieces
// - Random payment results make tests flaky

class StandardOrderProcessorTest extends PHPUnit\Framework\TestCase
{
    public function testProcessOrder(): void
    {
        // This test will:
        // 1. Try to connect to actual MySQL database
        // 2. Send real emails
        // 3. Make actual payment gateway calls
        // 4. Have random failures due to payment randomness
        
        $processor = new StandardOrderProcessor();
        $orderData = [
            'customer_id' => 123,
            'amount' => 100.00,
            'payment_method' => 'credit_card',
            'customer_email' => 'test@example.com'
        ];
        
        // This will likely fail due to external dependencies
        $result = $processor->processOrder($orderData);
        
        // What are we actually testing here?
        $this->assertTrue($result);
    }
}

The Solution: Final Classes with Dependency Inversion

The modern PHP 8.4 approach leverages final classes combined with dependency injection to achieve proper inversion of control. Final classes prevent inheritance, forcing developers to use composition—which aligns perfectly with dependency inversion principles.

<?php

declare(strict_types=1);

/**
 * RIGHT: Final classes with dependency inversion and composition
 * Benefits:
 * - Follows Dependency Inversion Principle
 * - Easy to test with real objects or mocks as needed
 * - Final classes prevent fragile inheritance
 * - Composition over inheritance
 * - PHP 8.4 features: Property hooks, lazy objects, asymmetric visibility
 */

/**
 * Interfaces define contracts (abstractions)
 */
interface OrderStorageInterface
{
    public function saveOrder(Order $order): OrderId;
    public function updateOrderStatus(OrderId $orderId, OrderStatus $status): bool;
    public function findOrder(OrderId $orderId): ?Order;
}

interface PaymentGatewayInterface
{
    public function processPayment(Payment $payment): PaymentResult;
}

interface NotificationServiceInterface
{
    public function sendOrderConfirmation(string $email, OrderId $orderId): bool;
}

interface TaxCalculatorInterface
{
    public function calculateTax(Money $amount, CustomerType $customerType): Money;
}

interface OrderValidatorInterface
{
    public function validate(OrderData $orderData): ValidationResult;
}

/**
 * Value Objects - PHP 8.4 with property hooks and asymmetric visibility
 */
readonly class OrderId
{
    public function __construct(private int $id) 
    {
        if ($id <= 0) {
            throw new InvalidArgumentException('Order ID must be positive');
        }
    }
    
    public function value(): int
    {
        return $this->id;
    }
    
    public function __toString(): string
    {
        return (string) $this->id;
    }
}

readonly class Money
{
    public function __construct(private float $amount)
    {
        if ($amount < 0) {
            throw new InvalidArgumentException('Amount cannot be negative');
        }
    }
    
    public function amount(): float
    {
        return $this->amount;
    }
    
    public function add(Money $other): Money
    {
        return new Money($this->amount + $other->amount);
    }
    
    public function multiply(float $factor): Money
    {
        return new Money($this->amount * $factor);
    }
}

enum OrderStatus: string
{
    case PENDING = 'pending';
    case PAID = 'paid';
    case FAILED = 'failed';
    case SHIPPED = 'shipped';
    case DELIVERED = 'delivered';
}

enum CustomerType: string
{
    case STANDARD = 'standard';
    case PREMIUM = 'premium';
    case VIP = 'vip';
}

/**
 * PHP 8.4 Property Hooks and Asymmetric Visibility
 */
class Order
{
    // Asymmetric visibility: public read, private write
    public private(set) OrderId $id {
        set {
            if ($this->id !== null) {
                throw new LogicException('Order ID cannot be changed once set');
            }
            $this->id = $value;
        }
    }
    
    public private(set) OrderStatus $status = OrderStatus::PENDING;
    
    public function __construct(
        private readonly int $customerId,
        private readonly Money $amount,
        private readonly string $paymentMethod,
        private readonly string $customerEmail,
        private readonly CustomerType $customerType = CustomerType::STANDARD
    ) {}
    
    public function getCustomerId(): int { return $this->customerId; }
    public function getAmount(): Money { return $this->amount; }
    public function getPaymentMethod(): string { return $this->paymentMethod; }
    public function getCustomerEmail(): string { return $this->customerEmail; }
    public function getCustomerType(): CustomerType { return $this->customerType; }
    
    public function setId(OrderId $id): void
    {
        $this->id = $id;
    }
    
    public function updateStatus(OrderStatus $status): void
    {
        $this->status = $status;
    }
}

/**
 * FINAL CLASS: Order Processor using composition
 * Cannot be extended - forces composition over inheritance
 */
final class OrderProcessor
{
    public function __construct(
        private readonly OrderValidatorInterface $validator,
        private readonly TaxCalculatorInterface $taxCalculator,
        private readonly OrderStorageInterface $storage,
        private readonly PaymentGatewayInterface $paymentGateway,
        private readonly NotificationServiceInterface $notificationService
    ) {}
    
    public function processOrder(OrderData $orderData): ProcessingResult
    {
        // Validation
        $validationResult = $this->validator->validate($orderData);
        if (!$validationResult->isValid()) {
            return ProcessingResult::failure($validationResult->getErrors());
        }
        
        // Create order
        $order = new Order(
            $orderData->customerId,
            $orderData->amount,
            $orderData->paymentMethod,
            $orderData->customerEmail,
            $orderData->customerType
        );
        
        // Calculate tax
        $tax = $this->taxCalculator->calculateTax(
            $order->getAmount(),
            $order->getCustomerType()
        );
        $totalAmount = $order->getAmount()->add($tax);
        
        // Save order
        $orderId = $this->storage->saveOrder($order);
        $order->setId($orderId);
        
        // Process payment
        $payment = new Payment($totalAmount, $order->getPaymentMethod());
        $paymentResult = $this->paymentGateway->processPayment($payment);
        
        if ($paymentResult->isSuccess()) {
            $order->updateStatus(OrderStatus::PAID);
            $this->storage->updateOrderStatus($orderId, OrderStatus::PAID);
            
            $this->notificationService->sendOrderConfirmation(
                $order->getCustomerEmail(),
                $orderId
            );
            
            return ProcessingResult::success($orderId);
        } else {
            $order->updateStatus(OrderStatus::FAILED);
            $this->storage->updateOrderStatus($orderId, OrderStatus::FAILED);
            
            return ProcessingResult::failure(['Payment failed: ' . $paymentResult->getError()]);
        }
    }
}

/**
 * Supporting classes and implementations
 */
readonly class OrderData
{
    public function __construct(
        public int $customerId,
        public Money $amount,
        public string $paymentMethod,
        public string $customerEmail,
        public CustomerType $customerType = CustomerType::STANDARD
    ) {}
}

readonly class Payment
{
    public function __construct(
        public Money $amount,
        public string $method
    ) {}
}

class PaymentResult
{
    public function __construct(
        private readonly bool $success,
        private readonly ?string $error = null
    ) {}
    
    public function isSuccess(): bool { return $this->success; }
    public function getError(): ?string { return $this->error; }
    
    public static function success(): self
    {
        return new self(true);
    }
    
    public static function failure(string $error): self
    {
        return new self(false, $error);
    }
}

class ValidationResult
{
    public function __construct(
        private readonly bool $valid,
        private readonly array $errors = []
    ) {}
    
    public function isValid(): bool { return $this->valid; }
    public function getErrors(): array { return $this->errors; }
    
    public static function valid(): self
    {
        return new self(true);
    }
    
    public static function invalid(array $errors): self
    {
        return new self(false, $errors);
    }
}

class ProcessingResult
{
    public function __construct(
        private readonly bool $success,
        private readonly ?OrderId $orderId = null,
        private readonly array $errors = []
    ) {}
    
    public function isSuccess(): bool { return $this->success; }
    public function getOrderId(): ?OrderId { return $this->orderId; }
    public function getErrors(): array { return $this->errors; }
    
    public static function success(OrderId $orderId): self
    {
        return new self(true, $orderId);
    }
    
    public static function failure(array $errors): self
    {
        return new self(false, null, $errors);
    }
}

/**
 * Concrete Implementations - Can be easily swapped
 */
final class StandardTaxCalculator implements TaxCalculatorInterface
{
    public function calculateTax(Money $amount, CustomerType $customerType): Money
    {
        $rate = match($customerType) {
            CustomerType::STANDARD => 0.08,
            CustomerType::PREMIUM => 0.05,
            CustomerType::VIP => 0.03
        };
        
        return $amount->multiply($rate);
    }
}

final class BasicOrderValidator implements OrderValidatorInterface
{
    public function validate(OrderData $orderData): ValidationResult
    {
        $errors = [];
        
        if ($orderData->customerId <= 0) {
            $errors[] = 'Invalid customer ID';
        }
        
        if ($orderData->amount->amount() <= 0) {
            $errors[] = 'Amount must be positive';
        }
        
        if (empty($orderData->paymentMethod)) {
            $errors[] = 'Payment method required';
        }
        
        if (!filter_var($orderData->customerEmail, FILTER_VALIDATE_EMAIL)) {
            $errors[] = 'Invalid email address';
        }
        
        return empty($errors) 
            ? ValidationResult::valid()
            : ValidationResult::invalid($errors);
    }
}

/**
 * In-memory implementation for testing
 */
final class InMemoryOrderStorage implements OrderStorageInterface
{
    private array $orders = [];
    private int $nextId = 1;
    
    public function saveOrder(Order $order): OrderId
    {
        $orderId = new OrderId($this->nextId++);
        $this->orders[$orderId->value()] = $order;
        return $orderId;
    }
    
    public function updateOrderStatus(OrderId $orderId, OrderStatus $status): bool
    {
        if (isset($this->orders[$orderId->value()])) {
            $this->orders[$orderId->value()]->updateStatus($status);
            return true;
        }
        return false;
    }
    
    public function findOrder(OrderId $orderId): ?Order
    {
        return $this->orders[$orderId->value()] ?? null;
    }
    
    public function getOrderCount(): int
    {
        return count($this->orders);
    }
}

/**
 * MySQL implementation for production
 */
final class MySqlOrderStorage implements OrderStorageInterface
{
    public function __construct(private readonly PDO $connection) {}
    
    public function saveOrder(Order $order): OrderId
    {
        $sql = "INSERT INTO orders (customer_id, amount, payment_method, email, customer_type, status, created_at) 
                VALUES (?, ?, ?, ?, ?, ?, NOW())";
        
        $stmt = $this->connection->prepare($sql);
        $stmt->execute([
            $order->getCustomerId(),
            $order->getAmount()->amount(),
            $order->getPaymentMethod(),
            $order->getCustomerEmail(),
            $order->getCustomerType()->value,
            $order->status->value
        ]);
        
        return new OrderId((int) $this->connection->lastInsertId());
    }
    
    public function updateOrderStatus(OrderId $orderId, OrderStatus $status): bool
    {
        $sql = "UPDATE orders SET status = ? WHERE id = ?";
        $stmt = $this->connection->prepare($sql);
        return $stmt->execute([$status->value, $orderId->value()]);
    }
    
    public function findOrder(OrderId $orderId): ?Order
    {
        $sql = "SELECT * FROM orders WHERE id = ?";
        $stmt = $this->connection->prepare($sql);
        $stmt->execute([$orderId->value()]);
        
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            return null;
        }
        
        $order = new Order(
            (int) $row['customer_id'],
            new Money((float) $row['amount']),
            $row['payment_method'],
            $row['email'],
            CustomerType::from($row['customer_type'])
        );
        
        $order->setId($orderId);
        $order->updateStatus(OrderStatus::from($row['status']));
        
        return $order;
    }
}

Key Benefits of Final Classes with DIP

  • Composition over inheritance: Final classes force you to inject dependencies rather than extending base classes
  • Clear contracts: Interfaces define explicit contracts between components
  • Easy testing: Dependencies can be easily swapped for testing
  • Better encapsulation: Final classes prevent unwanted extension and maintain integrity
  • PHP 8.4 optimizations: Final classes enable better opcache optimizations

The Testing Philosophy: Detroit vs London Schools

The testing community has long been divided between two approaches, historically known as the Detroit School (classical/state-based testing) and London School (mockist/interaction-based testing). However, modern practice has evolved toward a more pragmatic approach that combines both strategies.

Detroit School: Testing with Real Objects

The Detroit School, also known as the Classical approach, emphasizes:

  • State-based verification: Test what the system produces, not how it produces it
  • Real object usage: Use actual implementations when they're fast and deterministic
  • Inside-out development: Build from the domain core outward
  • Refactoring safety: Tests remain stable when implementation changes

London School: Testing with Mocks

The London School, or Mockist approach, focuses on:

  • Interaction-based verification: Test how objects collaborate
  • Heavy mocking: Mock all dependencies to isolate the system under test
  • Outside-in development: Start from user interface and work toward domain
  • Design feedback: Difficult mocking indicates poor design

The Pragmatic Approach: When to Use Each

Modern testing practice recognizes that both approaches have merit and should be used contextually:

<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;

/**
 * PRAGMATIC TESTING: When to use real objects vs mocks
 * 
 * Key Principles:
 * 1. Use real objects when they're fast, deterministic, and have no side effects
 * 2. Use mocks for external dependencies and when testing interactions
 * 3. Combine both approaches based on what you're testing
 * 4. Union types allow flexible testing strategies
 */

/**
 * DETROIT SCHOOL: Testing with real objects
 * Focus on state verification and end-to-end behavior
 */
class OrderProcessorDetroitTest extends TestCase
{
    private OrderProcessor $processor;
    private InMemoryOrderStorage $storage;
    
    protected function setUp(): void
    {
        // Use REAL implementations for fast, deterministic dependencies
        $this->storage = new InMemoryOrderStorage();
        $validator = new BasicOrderValidator();
        $taxCalculator = new StandardTaxCalculator();
        
        // Mock only the external, slow, or non-deterministic dependencies
        $paymentGateway = $this->createStub(PaymentGatewayInterface::class);
        $paymentGateway->method('processPayment')
                      ->willReturn(PaymentResult::success());
        
        $notificationService = $this->createStub(NotificationServiceInterface::class);
        $notificationService->method('sendOrderConfirmation')
                           ->willReturn(true);
        
        $this->processor = new OrderProcessor(
            $validator,        // REAL - fast and deterministic
            $taxCalculator,    // REAL - pure calculation
            $this->storage,    // REAL - in-memory, fast
            $paymentGateway,   // MOCK - external service
            $notificationService // MOCK - external service
        );
    }
    
    public function testProcessValidOrderSuccessfully(): void
    {
        $orderData = new OrderData(
            customerId: 123,
            amount: new Money(100.0),
            paymentMethod: 'credit_card',
            customerEmail: 'customer@example.com',
            customerType: CustomerType::STANDARD
        );
        
        $result = $this->processor->processOrder($orderData);
        
        // State-based verification using REAL storage
        $this->assertTrue($result->isSuccess());
        $this->assertEquals(1, $this->storage->getOrderCount());
        
        $savedOrder = $this->storage->findOrder($result->getOrderId());
        $this->assertEquals(OrderStatus::PAID, $savedOrder->status);
        $this->assertEquals(123, $savedOrder->getCustomerId());
    }
    
    public function testProcessOrderWithInvalidData(): void
    {
        $orderData = new OrderData(
            customerId: -1, // Invalid
            amount: new Money(100.0),
            paymentMethod: 'credit_card',
            customerEmail: 'invalid-email', // Invalid
            customerType: CustomerType::STANDARD
        );
        
        $result = $this->processor->processOrder($orderData);
        
        // Real validator gives us real validation errors
        $this->assertFalse($result->isSuccess());
        $this->assertContains('Invalid customer ID', $result->getErrors());
        $this->assertContains('Invalid email address', $result->getErrors());
        $this->assertEquals(0, $this->storage->getOrderCount());
    }
    
    public function testTaxCalculationForDifferentCustomerTypes(): void
    {
        $baseAmount = new Money(100.0);
        $standardOrderData = new OrderData(
            customerId: 123,
            amount: $baseAmount,
            paymentMethod: 'credit_card',
            customerEmail: 'standard@example.com',
            customerType: CustomerType::STANDARD
        );
        
        $premiumOrderData = new OrderData(
            customerId: 124,
            amount: $baseAmount,
            paymentMethod: 'credit_card',
            customerEmail: 'premium@example.com',
            customerType: CustomerType::PREMIUM
        );
        
        $this->processor->processOrder($standardOrderData);
        $this->processor->processOrder($premiumOrderData);
        
        // Using real tax calculator lets us verify actual calculations
        $this->assertEquals(2, $this->storage->getOrderCount());
        // Tax differences would be reflected in the stored orders
    }
}

/**
 * LONDON SCHOOL: Testing with mocks for interaction verification
 * Focus on behavior verification and message passing
 */
class OrderProcessorLondonTest extends TestCase
{
    private MockObject|OrderValidatorInterface $mockValidator;
    private MockObject|TaxCalculatorInterface $mockTaxCalculator;
    private MockObject|OrderStorageInterface $mockStorage;
    private MockObject|PaymentGatewayInterface $mockPaymentGateway;
    private MockObject|NotificationServiceInterface $mockNotificationService;
    private OrderProcessor $processor;
    
    protected function setUp(): void
    {
        // Mock ALL dependencies to focus on interactions
        $this->mockValidator = $this->createMock(OrderValidatorInterface::class);
        $this->mockTaxCalculator = $this->createMock(TaxCalculatorInterface::class);
        $this->mockStorage = $this->createMock(OrderStorageInterface::class);
        $this->mockPaymentGateway = $this->createMock(PaymentGatewayInterface::class);
        $this->mockNotificationService = $this->createMock(NotificationServiceInterface::class);
        
        $this->processor = new OrderProcessor(
            $this->mockValidator,
            $this->mockTaxCalculator,
            $this->mockStorage,
            $this->mockPaymentGateway,
            $this->mockNotificationService
        );
    }
    
    public function testProcessOrderFollowsCorrectSequence(): void
    {
        $orderData = new OrderData(
            customerId: 123,
            amount: new Money(100.0),
            paymentMethod: 'credit_card',
            customerEmail: 'test@example.com'
        );
        
        $orderId = new OrderId(1);
        $tax = new Money(8.0);
        $totalAmount = new Money(108.0);
        
        // Setup expectations in the correct order
        $this->mockValidator
             ->expects($this->once())
             ->method('validate')
             ->with($orderData)
             ->willReturn(ValidationResult::valid());
        
        $this->mockTaxCalculator
             ->expects($this->once())
             ->method('calculateTax')
             ->with(
                 $this->equalTo($orderData->amount),
                 $this->equalTo($orderData->customerType)
             )
             ->willReturn($tax);
        
        $this->mockStorage
             ->expects($this->once())
             ->method('saveOrder')
             ->willReturn($orderId);
        
        $this->mockPaymentGateway
             ->expects($this->once())
             ->method('processPayment')
             ->with($this->callback(function (Payment $payment) use ($totalAmount) {
                 return $payment->amount->amount() === $totalAmount->amount();
             }))
             ->willReturn(PaymentResult::success());
        
        $this->mockStorage
             ->expects($this->once())
             ->method('updateOrderStatus')
             ->with($orderId, OrderStatus::PAID)
             ->willReturn(true);
        
        $this->mockNotificationService
             ->expects($this->once())
             ->method('sendOrderConfirmation')
             ->with('test@example.com', $orderId)
             ->willReturn(true);
        
        $result = $this->processor->processOrder($orderData);
        
        $this->assertTrue($result->isSuccess());
        $this->assertEquals($orderId, $result->getOrderId());
    }
    
    public function testProcessOrderHandlesPaymentFailure(): void
    {
        $orderData = new OrderData(
            customerId: 123,
            amount: new Money(100.0),
            paymentMethod: 'credit_card',
            customerEmail: 'test@example.com'
        );
        
        $orderId = new OrderId(1);
        
        $this->mockValidator
             ->method('validate')
             ->willReturn(ValidationResult::valid());
        
        $this->mockTaxCalculator
             ->method('calculateTax')
             ->willReturn(new Money(8.0));
        
        $this->mockStorage
             ->method('saveOrder')
             ->willReturn($orderId);
        
        // Payment fails
        $this->mockPaymentGateway
             ->method('processPayment')
             ->willReturn(PaymentResult::failure('Card declined'));
        
        // Should update order status to failed
        $this->mockStorage
             ->expects($this->once())
             ->method('updateOrderStatus')
             ->with($orderId, OrderStatus::FAILED);
        
        // Should NOT send notification for failed payment
        $this->mockNotificationService
             ->expects($this->never())
             ->method('sendOrderConfirmation');
        
        $result = $this->processor->processOrder($orderData);
        
        $this->assertFalse($result->isSuccess());
        $this->assertContains('Payment failed: Card declined', $result->getErrors());
    }
}

/**
 * HYBRID APPROACH: Union types for flexible testing
 * PHP 8.4 allows more sophisticated type unions
 */

// Define union type for testing flexibility
type TestableOrderStorage = InMemoryOrderStorage|MockObject;
type TestablePaymentGateway = PaymentGatewayInterface|MockObject;

class OrderProcessorHybridTest extends TestCase
{
    /**
     * Test with combination of real and mock objects
     * Use real objects where they add value, mocks where necessary
     */
    public function testCompleteOrderFlowWithHybridApproach(): void
    {
        // REAL objects for deterministic, fast operations
        $validator = new BasicOrderValidator();
        $taxCalculator = new StandardTaxCalculator();
        $storage = new InMemoryOrderStorage();
        
        // MOCK for external payment service
        $paymentGateway = $this->createMock(PaymentGatewayInterface::class);
        $paymentGateway->method('processPayment')
                      ->willReturn(PaymentResult::success());
        
        // FAKE for notification (simple test implementation)
        $notificationService = new class implements NotificationServiceInterface {
            public array $sentNotifications = [];
            
            public function sendOrderConfirmation(string $email, OrderId $orderId): bool
            {
                $this->sentNotifications[] = ['email' => $email, 'orderId' => $orderId];
                return true;
            }
        };
        
        $processor = new OrderProcessor(
            $validator,
            $taxCalculator,
            $storage,
            $paymentGateway,
            $notificationService
        );
        
        $orderData = new OrderData(
            customerId: 123,
            amount: new Money(100.0),
            paymentMethod: 'credit_card',
            customerEmail: 'hybrid@example.com',
            customerType: CustomerType::PREMIUM
        );
        
        $result = $processor->processOrder($orderData);
        
        // Verify using real storage
        $this->assertTrue($result->isSuccess());
        $this->assertEquals(1, $storage->getOrderCount());
        
        // Verify using fake notification service
        $this->assertCount(1, $notificationService->sentNotifications);
        $this->assertEquals('hybrid@example.com', $notificationService->sentNotifications[0]['email']);
        
        // Verify real tax calculation (5% for premium)
        $savedOrder = $storage->findOrder($result->getOrderId());
        $this->assertEquals(OrderStatus::PAID, $savedOrder->status);
        
        // Could verify payment gateway interaction if needed
        // This gives us the best of both worlds: real behavior testing
        // with controlled external dependencies
    }
    
    /**
     * Performance test using all real objects
     * When speed matters, avoid mocks
     */
    public function testHighThroughputProcessing(): void
    {
        $storage = new InMemoryOrderStorage();
        $processor = new OrderProcessor(
            new BasicOrderValidator(),
            new StandardTaxCalculator(),
            $storage,
            new class implements PaymentGatewayInterface {
                public function processPayment(Payment $payment): PaymentResult {
                    return PaymentResult::success(); // Always successful for speed
                }
            },
            new class implements NotificationServiceInterface {
                public function sendOrderConfirmation(string $email, OrderId $orderId): bool {
                    return true; // No-op for speed
                }
            }
        );
        
        $startTime = microtime(true);
        
        // Process many orders quickly with real objects
        for ($i = 0; $i < 1000; $i++) {
            $orderData = new OrderData(
                customerId: $i + 1,
                amount: new Money(100.0 + $i),
                paymentMethod: 'credit_card',
                customerEmail: "customer{$i}@example.com"
            );
            
            $result = $processor->processOrder($orderData);
            $this->assertTrue($result->isSuccess());
        }
        
        $endTime = microtime(true);
        $processingTime = $endTime - $startTime;
        
        $this->assertEquals(1000, $storage->getOrderCount());
        $this->assertLessThan(1.0, $processingTime, 'Should process 1000 orders in under 1 second');
    }
}

/**
 * INTEGRATION TEST: Testing with database using real implementations
 */
class OrderProcessorIntegrationTest extends TestCase
{
    private PDO $connection;
    private OrderProcessor $processor;
    
    protected function setUp(): void
    {
        // Use SQLite in-memory database for fast integration tests
        $this->connection = new PDO('sqlite::memory:');
        $this->connection->exec('
            CREATE TABLE orders (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                customer_id INTEGER NOT NULL,
                amount DECIMAL(10,2) NOT NULL,
                payment_method VARCHAR(50) NOT NULL,
                email VARCHAR(255) NOT NULL,
                customer_type VARCHAR(20) NOT NULL,
                status VARCHAR(20) NOT NULL,
                created_at DATETIME DEFAULT CURRENT_TIMESTAMP
            )
        ');
        
        // Use REAL database storage for integration test
        $storage = new MySqlOrderStorage($this->connection);
        
        // Mock only truly external services
        $paymentGateway = $this->createStub(PaymentGatewayInterface::class);
        $paymentGateway->method('processPayment')->willReturn(PaymentResult::success());
        
        $notificationService = $this->createStub(NotificationServiceInterface::class);
        $notificationService->method('sendOrderConfirmation')->willReturn(true);
        
        $this->processor = new OrderProcessor(
            new BasicOrderValidator(),
            new StandardTaxCalculator(),
            $storage, // REAL database implementation
            $paymentGateway,
            $notificationService
        );
    }
    
    public function testOrderPersistsToDatabase(): void
    {
        $orderData = new OrderData(
            customerId: 123,
            amount: new Money(100.0),
            paymentMethod: 'credit_card',
            customerEmail: 'integration@example.com',
            customerType: CustomerType::STANDARD
        );
        
        $result = $this->processor->processOrder($orderData);
        
        $this->assertTrue($result->isSuccess());
        
        // Verify data in actual database
        $stmt = $this->connection->query('SELECT * FROM orders');
        $orders = $stmt->fetchAll(PDO::FETCH_ASSOC);
        
        $this->assertCount(1, $orders);
        $this->assertEquals(123, $orders[0]['customer_id']);
        $this->assertEquals('paid', $orders[0]['status']);
        $this->assertEquals('standard', $orders[0]['customer_type']);
    }
}

Decision Matrix: Real Objects vs Mocks

Use Real Objects When Use Mocks When
Fast to instantiate and execute External dependencies (database, HTTP)
Deterministic behavior Non-deterministic behavior (random, time)
No side effects Testing error conditions
Pure functions or simple state Interaction verification is important
Value objects and entities Complex setup required

TypeScript Patterns: Learning from Structural Typing

TypeScript offers valuable insights for PHP developers working with dependency inversion. While PHP uses nominal typing (class-based), TypeScript's structural typing provides interesting patterns we can adapt:

/**
 * TypeScript Implementation: Dependency Inversion with Modern Patterns
 * 
 * Key TypeScript features used:
 * - Readonly classes (similar to PHP final classes)
 * - Union types for flexible testing
 * - Branded types for type safety
 * - Discriminated unions for result types
 * - Template literal types for strong typing
 */

// Branded types for stronger type safety
type OrderId = number & { readonly __brand: 'OrderId' };
type CustomerId = number & { readonly __brand: 'CustomerId' };
type Money = number & { readonly __brand: 'Money' };

// Helper functions for branded types
const createOrderId = (id: number): OrderId => {
  if (id <= 0) throw new Error('Order ID must be positive');
  return id as OrderId;
};

const createCustomerId = (id: number): CustomerId => {
  if (id <= 0) throw new Error('Customer ID must be positive');
  return id as CustomerId;
};

const createMoney = (amount: number): Money => {
  if (amount < 0) throw new Error('Amount cannot be negative');
  return amount as Money;
};

// Enums for type safety
enum OrderStatus {
  PENDING = 'pending',
  PAID = 'paid',
  FAILED = 'failed',
  SHIPPED = 'shipped',
  DELIVERED = 'delivered',
}

enum CustomerType {
  STANDARD = 'standard',
  PREMIUM = 'premium',
  VIP = 'vip',
}

// Result types using discriminated unions
type ValidationResult = 
  | { success: true; data: OrderData }
  | { success: false; errors: string[] };

type ProcessingResult = 
  | { success: true; orderId: OrderId }
  | { success: false; errors: string[] };

type PaymentResult = 
  | { success: true; transactionId: string }
  | { success: false; error: string };

// Data structures
interface OrderData {
  readonly customerId: CustomerId;
  readonly amount: Money;
  readonly paymentMethod: string;
  readonly customerEmail: string;
  readonly customerType: CustomerType;
}

interface Order extends OrderData {
  readonly id: OrderId;
  readonly status: OrderStatus;
  readonly createdAt: Date;
}

interface Payment {
  readonly amount: Money;
  readonly method: string;
}

// Abstract interfaces (contracts)
interface OrderValidator {
  validate(orderData: OrderData): ValidationResult;
}

interface TaxCalculator {
  calculateTax(amount: Money, customerType: CustomerType): Money;
}

interface OrderStorage {
  saveOrder(orderData: OrderData): Promise<OrderId>;
  updateOrderStatus(orderId: OrderId, status: OrderStatus): Promise<boolean>;
  findOrder(orderId: OrderId): Promise<Order | null>;
}

interface PaymentGateway {
  processPayment(payment: Payment): Promise<PaymentResult>;
}

interface NotificationService {
  sendOrderConfirmation(email: string, orderId: OrderId): Promise<boolean>;
}

/**
 * MAIN CLASS: Final-like class using readonly pattern
 * TypeScript doesn't have final classes, but readonly pattern achieves similar goals
 */
class OrderProcessor {
  constructor(
    private readonly validator: OrderValidator,
    private readonly taxCalculator: TaxCalculator,
    private readonly storage: OrderStorage,
    private readonly paymentGateway: PaymentGateway,
    private readonly notificationService: NotificationService
  ) {
    // Make the class immutable by freezing it
    Object.freeze(this);
  }

  async processOrder(orderData: OrderData): Promise<ProcessingResult> {
    // Validation
    const validationResult = this.validator.validate(orderData);
    if (!validationResult.success) {
      return { success: false, errors: validationResult.errors };
    }

    // Calculate tax
    const tax = this.taxCalculator.calculateTax(
      orderData.amount,
      orderData.customerType
    );
    const totalAmount = createMoney(orderData.amount + tax);

    try {
      // Save order
      const orderId = await this.storage.saveOrder(orderData);

      // Process payment
      const payment: Payment = {
        amount: totalAmount,
        method: orderData.paymentMethod,
      };

      const paymentResult = await this.paymentGateway.processPayment(payment);

      if (paymentResult.success) {
        await this.storage.updateOrderStatus(orderId, OrderStatus.PAID);
        await this.notificationService.sendOrderConfirmation(
          orderData.customerEmail,
          orderId
        );
        return { success: true, orderId };
      } else {
        await this.storage.updateOrderStatus(orderId, OrderStatus.FAILED);
        return { 
          success: false, 
          errors: [`Payment failed: ${paymentResult.error}`] 
        };
      }
    } catch (error) {
      return { 
        success: false, 
        errors: [`Processing error: ${error instanceof Error ? error.message : 'Unknown error'}`] 
      };
    }
  }
}

/**
 * Concrete Implementations
 */

// Readonly class for tax calculation
class StandardTaxCalculator implements TaxCalculator {
  private readonly taxRates = {
    [CustomerType.STANDARD]: 0.08,
    [CustomerType.PREMIUM]: 0.05,
    [CustomerType.VIP]: 0.03,
  } as const;

  constructor() {
    Object.freeze(this);
  }

  calculateTax(amount: Money, customerType: CustomerType): Money {
    const rate = this.taxRates[customerType];
    return createMoney(amount * rate);
  }
}

class BasicOrderValidator implements OrderValidator {
  constructor() {
    Object.freeze(this);
  }

  validate(orderData: OrderData): ValidationResult {
    const errors: string[] = [];

    if (orderData.customerId <= 0) {
      errors.push('Invalid customer ID');
    }

    if (orderData.amount <= 0) {
      errors.push('Amount must be positive');
    }

    if (!orderData.paymentMethod.trim()) {
      errors.push('Payment method required');
    }

    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(orderData.customerEmail)) {
      errors.push('Invalid email address');
    }

    return errors.length === 0
      ? { success: true, data: orderData }
      : { success: false, errors };
  }
}

/**
 * In-memory implementation for testing
 */
class InMemoryOrderStorage implements OrderStorage {
  private orders = new Map<OrderId, Order>();
  private nextId = 1;

  constructor() {
    Object.freeze(this);
  }

  async saveOrder(orderData: OrderData): Promise<OrderId> {
    const orderId = createOrderId(this.nextId++);
    const order: Order = {
      ...orderData,
      id: orderId,
      status: OrderStatus.PENDING,
      createdAt: new Date(),
    };

    this.orders.set(orderId, order);
    return orderId;
  }

  async updateOrderStatus(orderId: OrderId, status: OrderStatus): Promise<boolean> {
    const order = this.orders.get(orderId);
    if (!order) return false;

    this.orders.set(orderId, { ...order, status });
    return true;
  }

  async findOrder(orderId: OrderId): Promise<Order | null> {
    return this.orders.get(orderId) || null;
  }

  getOrderCount(): number {
    return this.orders.size;
  }

  getAllOrders(): Order[] {
    return Array.from(this.orders.values());
  }
}

/**
 * External service implementations
 */
class StripePaymentGateway implements PaymentGateway {
  constructor(private readonly apiKey: string) {
    Object.freeze(this);
  }

  async processPayment(payment: Payment): Promise<PaymentResult> {
    // Simulate API call
    try {
      // In real implementation, this would call Stripe API
      const success = Math.random() > 0.1; // 90% success rate
      
      if (success) {
        return {
          success: true,
          transactionId: `txn_${Date.now()}`,
        };
      } else {
        return {
          success: false,
          error: 'Card declined',
        };
      }
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Payment processing error',
      };
    }
  }
}

class EmailNotificationService implements NotificationService {
  constructor(private readonly smtpConfig: { host: string; port: number }) {
    Object.freeze(this);
  }

  async sendOrderConfirmation(email: string, orderId: OrderId): Promise<boolean> {
    // Simulate email sending
    try {
      console.log(`Sending confirmation to ${email} for order ${orderId}`);
      return true;
    } catch (error) {
      console.error('Failed to send notification:', error);
      return false;
    }
  }
}

/**
 * TESTING PATTERNS: Union types for flexible testing
 */

// Union types allow mixing real and mock objects
type TestableOrderStorage = InMemoryOrderStorage | jest.Mocked<OrderStorage>;
type TestablePaymentGateway = StripePaymentGateway | jest.Mocked<PaymentGateway>;

/**
 * Test helper functions
 */
function createTestOrderData(): OrderData {
  return {
    customerId: createCustomerId(123),
    amount: createMoney(100.0),
    paymentMethod: 'credit_card',
    customerEmail: 'test@example.com',
    customerType: CustomerType.STANDARD,
  };
}

function createOrderProcessor(
  overrides: Partial<{
    validator: OrderValidator;
    taxCalculator: TaxCalculator;
    storage: OrderStorage;
    paymentGateway: PaymentGateway;
    notificationService: NotificationService;
  }> = {}
): OrderProcessor {
  const defaults = {
    validator: new BasicOrderValidator(),
    taxCalculator: new StandardTaxCalculator(),
    storage: new InMemoryOrderStorage(),
    paymentGateway: new StripePaymentGateway('test_key'),
    notificationService: new EmailNotificationService({ host: 'localhost', port: 587 }),
  };

  return new OrderProcessor(
    overrides.validator || defaults.validator,
    overrides.taxCalculator || defaults.taxCalculator,
    overrides.storage || defaults.storage,
    overrides.paymentGateway || defaults.paymentGateway,
    overrides.notificationService || defaults.notificationService
  );
}

/**
 * Example test cases showing pragmatic testing approach
 */

// Detroit School: Use real objects when possible
async function testWithRealObjects() {
  const storage = new InMemoryOrderStorage();
  const processor = createOrderProcessor({ storage });
  
  const orderData = createTestOrderData();
  const result = await processor.processOrder(orderData);

  if (result.success) {
    console.log('✓ Order processed successfully');
    console.log('✓ Order count:', storage.getOrderCount());
    
    const savedOrder = await storage.findOrder(result.orderId);
    console.log('✓ Order status:', savedOrder?.status);
  } else {
    console.log('✗ Order processing failed:', result.errors);
  }
}

// London School: Mock for interaction testing
async function testWithMocks() {
  const mockPaymentGateway: jest.Mocked<PaymentGateway> = {
    processPayment: jest.fn(),
  };

  const mockNotificationService: jest.Mocked<NotificationService> = {
    sendOrderConfirmation: jest.fn(),
  };

  mockPaymentGateway.processPayment.mockResolvedValue({
    success: true,
    transactionId: 'test_txn_123',
  });

  mockNotificationService.sendOrderConfirmation.mockResolvedValue(true);

  const processor = createOrderProcessor({
    paymentGateway: mockPaymentGateway,
    notificationService: mockNotificationService,
  });

  const orderData = createTestOrderData();
  const result = await processor.processOrder(orderData);

  if (result.success) {
    console.log('✓ Mocked order processed successfully');
    console.log('✓ Payment gateway called:', mockPaymentGateway.processPayment.mock.calls.length);
    console.log('✓ Notification sent:', mockNotificationService.sendOrderConfirmation.mock.calls.length);
  }
}

// Hybrid Approach: Mix real and mock objects
async function testHybridApproach() {
  const realStorage = new InMemoryOrderStorage();
  const mockPaymentGateway: jest.Mocked<PaymentGateway> = {
    processPayment: jest.fn().mockResolvedValue({
      success: true,
      transactionId: 'hybrid_txn_456',
    }),
  };

  const processor = createOrderProcessor({
    storage: realStorage,      // Real - fast and deterministic
    paymentGateway: mockPaymentGateway, // Mock - external service
  });

  const orderData = createTestOrderData();
  const result = await processor.processOrder(orderData);

  if (result.success) {
    console.log('✓ Hybrid test passed');
    console.log('✓ Real storage used, order count:', realStorage.getOrderCount());
    console.log('✓ Mock payment gateway called');
    
    const savedOrder = await realStorage.findOrder(result.orderId);
    console.log('✓ Real order status:', savedOrder?.status);
  }
}

/**
 * Advanced TypeScript patterns for dependency inversion
 */

// Generic factory pattern for creating processors with different configurations
type ProcessorConfig<T extends Record<string, unknown>> = {
  [K in keyof T]: T[K];
};

function createConfigurableProcessor<TConfig extends {
  validator?: OrderValidator;
  taxCalculator?: TaxCalculator;
  storage?: OrderStorage;
  paymentGateway?: PaymentGateway;
  notificationService?: NotificationService;
}>(config: TConfig): OrderProcessor {
  return new OrderProcessor(
    config.validator || new BasicOrderValidator(),
    config.taxCalculator || new StandardTaxCalculator(),
    config.storage || new InMemoryOrderStorage(),
    config.paymentGateway || new StripePaymentGateway('default_key'),
    config.notificationService || new EmailNotificationService({ host: 'localhost', port: 587 })
  );
}

// Template literal types for strongly typed configurations
type Environment = 'development' | 'testing' | 'production';
type ServiceConfig<T extends Environment> = T extends 'testing'
  ? { useInMemoryStorage: true; mockExternalServices: true }
  : T extends 'development'
  ? { useInMemoryStorage: false; mockExternalServices: false; debugMode: true }
  : { useInMemoryStorage: false; mockExternalServices: false; optimizeForProduction: true };

function createEnvironmentSpecificProcessor<T extends Environment>(
  env: T,
  config: ServiceConfig<T>
): OrderProcessor {
  // Type-safe configuration based on environment
  if (env === 'testing') {
    return createConfigurableProcessor({
      storage: new InMemoryOrderStorage(),
      // Mock other services in testing
    });
  }
  
  // Production or development configuration
  return createConfigurableProcessor({
    // Real implementations
  });
}

export {
  OrderProcessor,
  StandardTaxCalculator,
  BasicOrderValidator,
  InMemoryOrderStorage,
  StripePaymentGateway,
  EmailNotificationService,
  createTestOrderData,
  createOrderProcessor,
  createConfigurableProcessor,
  testWithRealObjects,
  testWithMocks,
  testHybridApproach,
};

export type {
  OrderId,
  CustomerId,
  Money,
  OrderData,
  Order,
  ValidationResult,
  ProcessingResult,
  PaymentResult,
  OrderValidator,
  TaxCalculator,
  OrderStorage,
  PaymentGateway,
  NotificationService,
  TestableOrderStorage,
  TestablePaymentGateway,
};

Union Types for Flexible Testing

The TypeScript code above demonstrates several advanced patterns that PHP developers can learn from, including branded types for stronger type safety, discriminated unions for result types, and template literal types for advanced configurations.

TypeScript's union types inspire a flexible testing approach where the same interface can accommodate both real implementations and mocks. While PHP doesn't have union types in the same way, we can achieve similar flexibility through careful interface design.

The key insight from TypeScript is that testing interfaces should be designed to accommodate both real and mock implementations naturally, rather than forcing a choice between approaches. The readonly pattern in TypeScript also provides inspiration for creating immutable dependencies in PHP.

Infrastructure as Code: Ansible and Dependency Inversion

Dependency inversion principles extend beyond application code to infrastructure automation. Ansible playbooks can demonstrate these concepts at the infrastructure level:

---
# Ansible Playbook: Dependency Inversion in Infrastructure Automation
# 
# This playbook demonstrates dependency inversion principles in infrastructure:
# - Abstract roles define interfaces (what should be done)
# - Concrete implementations handle specifics (how it's done)
# - Environment-specific variables control behavior
# - Composition over inheritance through role dependencies

# Main playbook that orchestrates the deployment
- name: Deploy order processing application with dependency inversion
  hosts: web_servers
  become: yes
  vars:
    # Abstract configuration - what we want, not how to achieve it
    app_name: order-processor
    app_version: "{{ lookup('env', 'APP_VERSION') | default('latest') }}"
    environment: "{{ lookup('env', 'ENVIRONMENT') | default('development') }}"
    
    # Dependency inversion: depend on abstractions, not concretions
    database_type: "{{ database_config.type }}"
    cache_type: "{{ cache_config.type }}"
    queue_type: "{{ queue_config.type }}"
    
    # Environment-specific configurations (injected dependencies)
    database_config:
      type: "{{ 'mysql' if environment == 'production' else 'sqlite' }}"
      host: "{{ 'db.prod.example.com' if environment == 'production' else 'localhost' }}"
      port: "{{ 3306 if environment == 'production' else 3306 }}"
      name: "{{ app_name }}_{{ environment }}"
    
    cache_config:
      type: "{{ 'redis' if environment in ['production', 'staging'] else 'memory' }}"
      host: "{{ 'cache.prod.example.com' if environment == 'production' else 'localhost' }}"
      port: 6379
    
    queue_config:
      type: "{{ 'rabbitmq' if environment == 'production' else 'database' }}"
      host: "{{ 'queue.prod.example.com' if environment == 'production' else 'localhost' }}"
      port: 5672

  # Role composition - dependency injection for infrastructure
  roles:
    # Core roles (always needed - stable dependencies)
    - role: system_preparation
      tags: [system, always]
    
    - role: php_runtime
      php_version: "8.4"
      tags: [php, runtime]
    
    # Conditional roles based on environment (injected dependencies)
    - role: database_service
      database_implementation: "{{ database_type }}"
      when: database_type is defined
      tags: [database]
    
    - role: cache_service
      cache_implementation: "{{ cache_type }}"
      when: cache_type != 'memory'
      tags: [cache]
    
    - role: queue_service
      queue_implementation: "{{ queue_type }}"
      when: queue_type != 'database'
      tags: [queue]
    
    # Application deployment (depends on abstractions above)
    - role: application_deployment
      tags: [app, deploy]
    
    # Monitoring and health checks (final layer)
    - role: monitoring
      when: environment in ['staging', 'production']
      tags: [monitoring]

  # Post-deployment tasks
  post_tasks:
    - name: Verify application health
      uri:
        url: "http://localhost:8080/health"
        method: GET
        status_code: 200
      register: health_check
      retries: 5
      delay: 10
      
    - name: Display deployment summary
      debug:
        msg: |
          Deployment completed successfully:
          - Application: {{ app_name }} v{{ app_version }}
          - Environment: {{ environment }}
          - Database: {{ database_type }} on {{ database_config.host }}
          - Cache: {{ cache_type }}
          - Queue: {{ queue_type }}
          - Health check: {{ 'PASSED' if health_check.status == 200 else 'FAILED' }}

---
# Database Service Role - Implements database abstraction
# roles/database_service/tasks/main.yml

- name: Include database-specific tasks
  include_tasks: "{{ database_implementation }}.yml"
  when: database_implementation is defined

---
# MySQL implementation - roles/database_service/tasks/mysql.yml
- name: Install MySQL server
  package:
    name: "{{ mysql_package_name }}"
    state: present
  vars:
    mysql_package_name: "{{ 'mysql-server' if ansible_os_family == 'Debian' else 'mysql-community-server' }}"

- name: Configure MySQL for high performance
  template:
    src: mysql.cnf.j2
    dest: /etc/mysql/mysql.conf.d/99-custom.cnf
    backup: yes
  notify: restart mysql
  vars:
    # PHP 8.4 optimized MySQL configuration
    mysql_config:
      innodb_buffer_pool_size: "{{ (ansible_memtotal_mb * 0.7) | int }}M"
      innodb_log_file_size: "256M"
      max_connections: "{{ 500 if environment == 'production' else 100 }}"
      query_cache_size: "{{ '256M' if environment == 'production' else '64M' }}"

- name: Create application database
  mysql_db:
    name: "{{ database_config.name }}"
    state: present
    encoding: utf8mb4
    collation: utf8mb4_unicode_ci
  notify: reload mysql privileges

- name: Create database user for application
  mysql_user:
    name: "{{ app_name }}"
    password: "{{ database_password | default(lookup('password', '/dev/null chars=ascii_letters,digits length=32')) }}"
    priv: "{{ database_config.name }}.*:ALL"
    host: "{{ 'localhost' if database_config.host == 'localhost' else '%' }}"
    state: present
  notify: reload mysql privileges

---
# SQLite implementation - roles/database_service/tasks/sqlite.yml  
- name: Install SQLite
  package:
    name: sqlite3
    state: present

- name: Create SQLite database directory
  file:
    path: /var/lib/{{ app_name }}/database
    state: directory
    owner: www-data
    group: www-data
    mode: '0750'

- name: Initialize SQLite database
  command: sqlite3 /var/lib/{{ app_name }}/database/{{ database_config.name }}.db "SELECT 1;"
  args:
    creates: /var/lib/{{ app_name }}/database/{{ database_config.name }}.db
  become_user: www-data

---
# Cache Service Role - Implements caching abstraction
# roles/cache_service/tasks/main.yml

- name: Include cache-specific tasks
  include_tasks: "{{ cache_implementation }}.yml"
  when: cache_implementation is defined and cache_implementation != 'memory'

---
# Redis implementation - roles/cache_service/tasks/redis.yml
- name: Install Redis server
  package:
    name: redis-server
    state: present

- name: Configure Redis for application caching
  template:
    src: redis.conf.j2
    dest: /etc/redis/redis.conf
    backup: yes
  notify: restart redis
  vars:
    redis_config:
      maxmemory: "{{ (ansible_memtotal_mb * 0.2) | int }}mb"
      maxmemory_policy: "allkeys-lru"
      save: "900 1 300 10 60 10000"  # Persistence configuration
      tcp_keepalive: 300
      timeout: 0

- name: Start and enable Redis service
  systemd:
    name: redis-server
    state: started
    enabled: yes

---
# Application Deployment Role - Uses injected dependencies
# roles/application_deployment/tasks/main.yml

- name: Create application directory structure
  file:
    path: "{{ item }}"
    state: directory
    owner: www-data
    group: www-data
    mode: '0755'
  loop:
    - /var/www/{{ app_name }}
    - /var/www/{{ app_name }}/storage
    - /var/www/{{ app_name }}/config
    - /var/log/{{ app_name }}

- name: Deploy application configuration
  template:
    src: app-config.php.j2
    dest: /var/www/{{ app_name }}/config/config.php
    owner: www-data
    group: www-data
    mode: '0640'
  vars:
    # Configuration uses abstract dependencies
    config:
      database:
        driver: "{{ database_type }}"
        host: "{{ database_config.host }}"
        port: "{{ database_config.port }}"
        name: "{{ database_config.name }}"
        username: "{{ app_name }}"
      cache:
        driver: "{{ cache_type }}"
        host: "{{ cache_config.host if cache_type != 'memory' else 'localhost' }}"
        port: "{{ cache_config.port if cache_type != 'memory' else null }}"
      queue:
        driver: "{{ queue_type }}"
        host: "{{ queue_config.host if queue_type != 'database' else null }}"
        port: "{{ queue_config.port if queue_type != 'database' else null }}"
      app:
        environment: "{{ environment }}"
        debug: "{{ environment in ['development', 'testing'] }}"
        log_level: "{{ 'debug' if environment == 'development' else 'warning' }}"

- name: Deploy PHP 8.4 application with dependency injection
  template:
    src: bootstrap.php.j2
    dest: /var/www/{{ app_name }}/bootstrap.php
    owner: www-data
    group: www-data
    mode: '0644'

- name: Install Composer dependencies
  composer:
    command: install
    working_dir: /var/www/{{ app_name }}
    no_dev: "{{ environment == 'production' }}"
  become_user: www-data

- name: Run database migrations
  shell: |
    cd /var/www/{{ app_name }}
    php artisan migrate --force
  become_user: www-data
  when: database_type is defined

---
# Testing Role - Demonstrates testing different configurations
# roles/application_testing/tasks/main.yml

- name: Run unit tests with real dependencies (Detroit school)
  shell: |
    cd /var/www/{{ app_name }}
    ./vendor/bin/phpunit --testsuite=unit --filter=DetroitTest
  become_user: www-data
  register: unit_test_results
  when: environment in ['development', 'testing']

- name: Run integration tests with mocked external services (London school)
  shell: |
    cd /var/www/{{ app_name }}
    ./vendor/bin/phpunit --testsuite=integration --filter=LondonTest
  become_user: www-data
  register: integration_test_results
  when: environment in ['development', 'testing']

- name: Run hybrid tests (pragmatic approach)
  shell: |
    cd /var/www/{{ app_name }}
    ./vendor/bin/phpunit --testsuite=hybrid
  become_user: www-data
  register: hybrid_test_results
  when: environment in ['development', 'testing']

- name: Display test results
  debug:
    msg: |
      Test Results Summary:
      - Unit Tests (Detroit): {{ 'PASSED' if unit_test_results.rc == 0 else 'FAILED' }}
      - Integration Tests (London): {{ 'PASSED' if integration_test_results.rc == 0 else 'FAILED' }}
      - Hybrid Tests: {{ 'PASSED' if hybrid_test_results.rc == 0 else 'FAILED' }}
  when: environment in ['development', 'testing']

---
# Error Handling and Rollback (Defensive Infrastructure)
# This demonstrates fail-fast principles in infrastructure

- name: Validate configuration before deployment
  block:
    - name: Check PHP 8.4 compatibility
      shell: php -v | grep "PHP 8.4"
      register: php_version_check
      failed_when: php_version_check.rc != 0

    - name: Validate database connectivity  
      shell: |
        php -r "
        try {
          {% if database_type == 'mysql' %}
          new PDO('mysql:host={{ database_config.host }};port={{ database_config.port }}', 'root', '');
          {% else %}
          new PDO('sqlite:/var/lib/{{ app_name }}/database/test.db');
          {% endif %}
          echo 'OK';
        } catch (Exception \$e) {
          echo 'FAILED: ' . \$e->getMessage();
          exit(1);
        }"
      register: db_connectivity_check

    - name: Validate cache connectivity
      shell: |
        {% if cache_type == 'redis' %}
        redis-cli -h {{ cache_config.host }} -p {{ cache_config.port }} ping
        {% else %}
        echo "PONG"  # Memory cache always available
        {% endif %}
      register: cache_connectivity_check
      when: cache_type != 'memory'

  rescue:
    - name: Rollback on validation failure
      debug:
        msg: |
          Validation failed - Rolling back deployment:
          - PHP Version: {{ php_version_check.stdout | default('FAILED') }}
          - Database: {{ db_connectivity_check.stdout | default('FAILED') }}
          - Cache: {{ cache_connectivity_check.stdout | default('SKIPPED') }}
      
    - name: Stop deployment
      fail:
        msg: "Pre-deployment validation failed. Deployment aborted."

---
# Handlers file - roles/database_service/handlers/main.yml
- name: restart mysql
  systemd:
    name: mysql
    state: restarted

- name: reload mysql privileges
  mysql_query:
    query: "FLUSH PRIVILEGES"

- name: restart redis
  systemd:
    name: redis-server
    state: restarted

Infrastructure Testing Strategies

Just as in application testing, infrastructure code benefits from pragmatic testing approaches:

  • Use real tools for deterministic operations (file creation, package installation)
  • Mock external services when testing deployment scripts
  • Integration tests with containers or VMs for complete workflows
  • Fail-fast validation to catch configuration errors early

Bash Scripting: Dependency Injection in Shell Scripts

Even shell scripts can benefit from dependency inversion principles. Here's how to apply these concepts in Bash:

#!/bin/bash

# Bash Script: Dependency Inversion and Testing Patterns in Shell Scripting
# 
# This script demonstrates:
# - Dependency injection through environment variables and configuration
# - Abstract interfaces through function contracts
# - Fail-fast error handling
# - Pragmatic testing approach: real tools vs mocked functions
# - Composition over inheritance through modular functions

set -euo pipefail  # Fail fast: exit on error, undefined vars, pipe failures
IFS=$'\n\t'       # Secure Internal Field Separator

# Global configuration - dependency injection through environment
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly PROJECT_ROOT="${PROJECT_ROOT:-$(dirname "$SCRIPT_DIR")}"
readonly ENVIRONMENT="${ENVIRONMENT:-development}"
readonly PHP_VERSION="${PHP_VERSION:-8.4}"
readonly LOG_LEVEL="${LOG_LEVEL:-info}"

# Color codes for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m' # No Color

# Abstract logging interface - dependency inversion principle
log() {
    local level="$1"
    shift
    local message="$*"
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    
    # Delegate to appropriate logging implementation
    case "${LOG_LEVEL}" in
        "debug")
            _log_debug "$timestamp" "$level" "$message"
            ;;
        "info")
            _log_info "$timestamp" "$level" "$message"
            ;;
        "warning")
            _log_warning "$timestamp" "$level" "$message"
            ;;
        *)
            _log_error "$timestamp" "$level" "$message"
            ;;
    esac
}

# Concrete logging implementations
_log_debug() {
    local timestamp="$1" level="$2" message="$3"
    echo -e "${BLUE}[$timestamp] [$level] $message${NC}" >&2
}

_log_info() {
    local timestamp="$1" level="$2" message="$3"
    case "$level" in
        "ERROR") echo -e "${RED}[$timestamp] [$level] $message${NC}" >&2 ;;
        "WARN")  echo -e "${YELLOW}[$timestamp] [$level] $message${NC}" >&2 ;;
        *)       echo -e "${GREEN}[$timestamp] [$level] $message${NC}" ;;
    esac
}

_log_warning() {
    local timestamp="$1" level="$2" message="$3"
    [[ "$level" =~ ^(ERROR|WARN)$ ]] && echo -e "${RED}[$timestamp] [$level] $message${NC}" >&2
}

_log_error() {
    local timestamp="$1" level="$2" message="$3"
    [[ "$level" == "ERROR" ]] && echo -e "${RED}[$timestamp] [$level] $message${NC}" >&2
}

# Error handling with dependency inversion
error_handler() {
    local exit_code=$?
    local line_number=$1
    log "ERROR" "Script failed at line $line_number with exit code $exit_code"
    
    # Delegate to appropriate error handler based on environment
    case "${ENVIRONMENT}" in
        "development")
            _handle_development_error "$exit_code" "$line_number"
            ;;
        "production")
            _handle_production_error "$exit_code" "$line_number"
            ;;
        *)
            _handle_generic_error "$exit_code" "$line_number"
            ;;
    esac
    
    exit "$exit_code"
}

trap 'error_handler $LINENO' ERR

# Abstract interface for database operations
database_execute() {
    local query="$1"
    local database_type="${DATABASE_TYPE:-mysql}"
    
    log "DEBUG" "Executing query with $database_type: $query"
    
    # Dependency inversion: delegate to appropriate implementation
    case "$database_type" in
        "mysql")
            _execute_mysql_query "$query"
            ;;
        "sqlite")
            _execute_sqlite_query "$query"
            ;;
        "test")
            _execute_test_query "$query"  # For testing
            ;;
        *)
            log "ERROR" "Unsupported database type: $database_type"
            return 1
            ;;
    esac
}

# Concrete database implementations
_execute_mysql_query() {
    local query="$1"
    local mysql_host="${MYSQL_HOST:-localhost}"
    local mysql_user="${MYSQL_USER:-root}"
    local mysql_db="${MYSQL_DB:-order_processor}"
    
    # Fail fast: validate connection before executing
    if ! mysql -h "$mysql_host" -u "$mysql_user" -e "SELECT 1" &>/dev/null; then
        log "ERROR" "Cannot connect to MySQL at $mysql_host"
        return 1
    fi
    
    mysql -h "$mysql_host" -u "$mysql_user" -D "$mysql_db" -e "$query"
}

_execute_sqlite_query() {
    local query="$1"
    local sqlite_db="${SQLITE_DB:-/tmp/order_processor.db}"
    
    # Fail fast: check if database file exists or can be created
    if [[ ! -f "$sqlite_db" ]] && ! touch "$sqlite_db" 2>/dev/null; then
        log "ERROR" "Cannot access SQLite database at $sqlite_db"
        return 1
    fi
    
    sqlite3 "$sqlite_db" "$query"
}

_execute_test_query() {
    local query="$1"
    log "DEBUG" "Test query executed: $query"
    echo "test_result"
}

# Abstract interface for payment processing
process_payment() {
    local amount="$1"
    local payment_method="$2"
    local payment_gateway="${PAYMENT_GATEWAY:-stripe}"
    
    log "INFO" "Processing payment of \$$amount via $payment_method using $payment_gateway"
    
    # Dependency inversion: delegate to appropriate gateway
    case "$payment_gateway" in
        "stripe")
            _process_stripe_payment "$amount" "$payment_method"
            ;;
        "paypal")
            _process_paypal_payment "$amount" "$payment_method"
            ;;
        "test")
            _process_test_payment "$amount" "$payment_method"
            ;;
        *)
            log "ERROR" "Unsupported payment gateway: $payment_gateway"
            return 1
            ;;
    esac
}

# Concrete payment implementations
_process_stripe_payment() {
    local amount="$1" payment_method="$2"
    local stripe_key="${STRIPE_SECRET_KEY:-}"
    
    if [[ -z "$stripe_key" ]]; then
        log "ERROR" "Stripe secret key not configured"
        return 1
    fi
    
    # Simulate API call
    if curl -s -X POST "https://api.stripe.com/v1/charges" \
        -H "Authorization: Bearer $stripe_key" \
        -d "amount=$((${amount%.*} * 100))" \
        -d "currency=usd" \
        -d "source=$payment_method" &>/dev/null; then
        log "INFO" "Stripe payment successful"
        return 0
    else
        log "ERROR" "Stripe payment failed"
        return 1
    fi
}

_process_paypal_payment() {
    local amount="$1" payment_method="$2"
    log "INFO" "PayPal payment processing: \$$amount"
    # PayPal implementation would go here
    return 0
}

_process_test_payment() {
    local amount="$1" payment_method="$2"
    log "DEBUG" "Test payment processed: \$$amount via $payment_method"
    
    # Simulate success/failure for testing
    local success_rate="${TEST_PAYMENT_SUCCESS_RATE:-90}"
    local random=$((RANDOM % 100))
    
    if (( random < success_rate )); then
        log "DEBUG" "Test payment successful (random: $random, threshold: $success_rate)"
        return 0
    else
        log "DEBUG" "Test payment failed (random: $random, threshold: $success_rate)"
        return 1
    fi
}

# Abstract notification interface
send_notification() {
    local recipient="$1"
    local subject="$2"
    local message="$3"
    local notification_service="${NOTIFICATION_SERVICE:-email}"
    
    log "INFO" "Sending notification to $recipient via $notification_service"
    
    case "$notification_service" in
        "email")
            _send_email_notification "$recipient" "$subject" "$message"
            ;;
        "slack")
            _send_slack_notification "$recipient" "$subject" "$message"
            ;;
        "test")
            _send_test_notification "$recipient" "$subject" "$message"
            ;;
        *)
            log "ERROR" "Unsupported notification service: $notification_service"
            return 1
            ;;
    esac
}

# Concrete notification implementations
_send_email_notification() {
    local recipient="$1" subject="$2" message="$3"
    
    if command -v mail >/dev/null 2>&1; then
        echo "$message" | mail -s "$subject" "$recipient"
        log "INFO" "Email sent successfully to $recipient"
    else
        log "WARN" "Mail command not available, notification not sent"
        return 1
    fi
}

_send_slack_notification() {
    local recipient="$1" subject="$2" message="$3"
    local slack_webhook="${SLACK_WEBHOOK:-}"
    
    if [[ -z "$slack_webhook" ]]; then
        log "ERROR" "Slack webhook not configured"
        return 1
    fi
    
    curl -X POST -H 'Content-type: application/json' \
        --data "{\"text\":\"$subject: $message\"}" \
        "$slack_webhook" &>/dev/null
}

_send_test_notification() {
    local recipient="$1" subject="$2" message="$3"
    log "DEBUG" "Test notification: To=$recipient, Subject=$subject, Message=$message"
}

# Main business logic using dependency injection
process_order() {
    local customer_id="$1"
    local amount="$2"
    local payment_method="$3"
    local email="$4"
    
    log "INFO" "Processing order: customer=$customer_id, amount=\$$amount"
    
    # Validate inputs (fail fast)
    if [[ ! "$customer_id" =~ ^[0-9]+$ ]] || (( customer_id <= 0 )); then
        log "ERROR" "Invalid customer ID: $customer_id"
        return 1
    fi
    
    if [[ ! "$amount" =~ ^[0-9]+(\.[0-9]{1,2})?$ ]] || (( $(echo "$amount <= 0" | bc -l) )); then
        log "ERROR" "Invalid amount: $amount"
        return 1
    fi
    
    if [[ ! "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
        log "ERROR" "Invalid email address: $email"
        return 1
    fi
    
    # Calculate tax using environment-specific rate
    local tax_rate
    case "${CUSTOMER_TYPE:-standard}" in
        "premium") tax_rate="0.05" ;;
        "vip") tax_rate="0.03" ;;
        *) tax_rate="0.08" ;;
    esac
    
    local tax
    tax=$(echo "$amount * $tax_rate" | bc -l)
    local total
    total=$(echo "$amount + $tax" | bc -l)
    
    log "INFO" "Order total: \$${amount} + \$${tax} tax = \$${total}"
    
    # Save order to database (using injected database dependency)
    local order_id
    if ! order_id=$(database_execute "INSERT INTO orders (customer_id, amount, status) VALUES ($customer_id, $total, 'pending'); SELECT last_insert_rowid();"); then
        log "ERROR" "Failed to save order to database"
        return 1
    fi
    
    log "INFO" "Order saved with ID: $order_id"
    
    # Process payment (using injected payment gateway)
    if process_payment "$total" "$payment_method"; then
        # Update order status
        if database_execute "UPDATE orders SET status = 'paid' WHERE id = $order_id"; then
            log "INFO" "Order $order_id marked as paid"
            
            # Send confirmation (using injected notification service)
            if send_notification "$email" "Order Confirmation" "Your order #$order_id has been processed successfully."; then
                log "INFO" "Order processing completed successfully"
                return 0
            else
                log "WARN" "Order processed but notification failed"
                return 0  # Don't fail the order for notification issues
            fi
        else
            log "ERROR" "Failed to update order status"
            return 1
        fi
    else
        # Payment failed - update order status
        database_execute "UPDATE orders SET status = 'failed' WHERE id = $order_id"
        log "ERROR" "Payment processing failed for order $order_id"
        return 1
    fi
}

# Testing functions - demonstrating different testing approaches

# Detroit School: Use real implementations where possible
test_with_real_implementations() {
    log "INFO" "Running Detroit School tests with real implementations"
    
    # Use SQLite (real but lightweight) instead of MySQL
    export DATABASE_TYPE="sqlite"
    export SQLITE_DB="/tmp/test_orders_real.db"
    
    # Use test payment gateway but real-ish logic
    export PAYMENT_GATEWAY="test"
    export TEST_PAYMENT_SUCCESS_RATE="100"
    
    # Use test notifications but with real message formatting
    export NOTIFICATION_SERVICE="test"
    
    # Initialize test database
    database_execute "CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY AUTOINCREMENT, customer_id INTEGER, amount REAL, status TEXT);"
    database_execute "DELETE FROM orders;"  # Clean state
    
    # Test valid order
    if process_order 123 "100.00" "credit_card" "test@example.com"; then
        log "INFO" "✓ Valid order test passed"
    else
        log "ERROR" "✗ Valid order test failed"
        return 1
    fi
    
    # Verify order was actually saved
    local order_count
    order_count=$(database_execute "SELECT COUNT(*) FROM orders WHERE status = 'paid';")
    if [[ "$order_count" == "1" ]]; then
        log "INFO" "✓ Database persistence test passed"
    else
        log "ERROR" "✗ Database persistence test failed (count: $order_count)"
        return 1
    fi
    
    # Test invalid inputs (should fail fast)
    if ! process_order -1 "100.00" "credit_card" "invalid-email"; then
        log "INFO" "✓ Input validation test passed"
    else
        log "ERROR" "✗ Input validation test failed"
        return 1
    fi
    
    log "INFO" "Detroit School tests completed successfully"
}

# London School: Mock external dependencies, focus on interactions
test_with_mocked_dependencies() {
    log "INFO" "Running London School tests with mocked dependencies"
    
    # Create mock functions that verify interactions
    declare -a mock_calls=()
    
    # Mock database operations
    _execute_test_query() {
        local query="$1"
        mock_calls+=("database: $query")
        
        if [[ "$query" =~ INSERT ]]; then
            echo "1"  # Return mock order ID
        elif [[ "$query" =~ SELECT.*COUNT ]]; then
            echo "1"  # Return mock count
        fi
    }
    
    # Mock payment processing
    _process_test_payment() {
        local amount="$1" payment_method="$2"
        mock_calls+=("payment: $amount via $payment_method")
        return 0  # Always succeed for interaction testing
    }
    
    # Mock notifications
    _send_test_notification() {
        local recipient="$1" subject="$2" message="$3"
        mock_calls+=("notification: $recipient - $subject")
        return 0
    }
    
    # Configure test environment
    export DATABASE_TYPE="test"
    export PAYMENT_GATEWAY="test"
    export NOTIFICATION_SERVICE="test"
    
    # Test order processing
    if process_order 123 "100.00" "credit_card" "test@example.com"; then
        log "INFO" "✓ Mocked order processing test passed"
    else
        log "ERROR" "✗ Mocked order processing test failed"
        return 1
    fi
    
    # Verify expected interactions occurred
    local expected_calls=4  # INSERT, UPDATE, payment, notification
    if (( ${#mock_calls[@]} == expected_calls )); then
        log "INFO" "✓ Interaction verification test passed (${#mock_calls[@]} calls)"
    else
        log "ERROR" "✗ Interaction verification test failed (expected $expected_calls, got ${#mock_calls[@]})"
        return 1
    fi
    
    # Display interaction log
    log "DEBUG" "Mock interactions:"
    for call in "${mock_calls[@]}"; do
        log "DEBUG" "  - $call"
    done
    
    log "INFO" "London School tests completed successfully"
}

# Hybrid Approach: Mix real and mock implementations
test_hybrid_approach() {
    log "INFO" "Running Hybrid tests with mixed implementations"
    
    # Use real database (fast SQLite)
    export DATABASE_TYPE="sqlite"
    export SQLITE_DB="/tmp/test_orders_hybrid.db"
    
    # Mock external payment service
    export PAYMENT_GATEWAY="test"
    export TEST_PAYMENT_SUCCESS_RATE="100"
    
    # Use real notification logic but mock delivery
    export NOTIFICATION_SERVICE="test"
    
    # Initialize test database
    database_execute "CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY AUTOINCREMENT, customer_id INTEGER, amount REAL, status TEXT);"
    database_execute "DELETE FROM orders;"
    
    # Test with real database persistence but mocked external services
    if process_order 456 "200.00" "paypal" "hybrid@example.com"; then
        log "INFO" "✓ Hybrid test passed"
    else
        log "ERROR" "✗ Hybrid test failed"
        return 1
    fi
    
    # Verify with real database query
    local saved_amount
    saved_amount=$(database_execute "SELECT amount FROM orders WHERE customer_id = 456;")
    expected_amount="216.00"  # 200 + 8% tax
    
    if [[ "$saved_amount" == "$expected_amount" ]]; then
        log "INFO" "✓ Real database verification passed"
    else
        log "ERROR" "✗ Real database verification failed (expected $expected_amount, got $saved_amount)"
        return 1
    fi
    
    log "INFO" "Hybrid tests completed successfully"
}

# Main execution function
main() {
    log "INFO" "Starting Order Processing System"
    log "INFO" "Environment: $ENVIRONMENT, PHP Version: $PHP_VERSION"
    log "INFO" "Script directory: $SCRIPT_DIR"
    
    # Parse command line arguments
    local action="${1:-help}"
    
    case "$action" in
        "process-order")
            shift
            if (( $# != 4 )); then
                log "ERROR" "Usage: $0 process-order <customer_id> <amount> <payment_method> <email>"
                exit 1
            fi
            process_order "$@"
            ;;
        
        "test-detroit")
            test_with_real_implementations
            ;;
        
        "test-london")
            test_with_mocked_dependencies
            ;;
        
        "test-hybrid")
            test_hybrid_approach
            ;;
        
        "test-all")
            test_with_real_implementations &&
            test_with_mocked_dependencies &&
            test_hybrid_approach &&
            log "INFO" "All tests passed successfully!"
            ;;
        
        "help"|*)
            cat <<EOF
Order Processing System - Dependency Inversion Demo

Usage: $0 <action> [arguments]

Actions:
  process-order <customer_id> <amount> <payment_method> <email>
    Process a single order with the given parameters
    
  test-detroit
    Run tests using real implementations (Detroit School)
    
  test-london  
    Run tests using mocked dependencies (London School)
    
  test-hybrid
    Run tests using mixed real/mock approach (Pragmatic)
    
  test-all
    Run all test suites
    
  help
    Show this help message

Environment Variables:
  ENVIRONMENT         - development|production (default: development)
  DATABASE_TYPE       - mysql|sqlite|test (default: mysql)
  PAYMENT_GATEWAY     - stripe|paypal|test (default: stripe)
  NOTIFICATION_SERVICE - email|slack|test (default: email)
  LOG_LEVEL          - debug|info|warning|error (default: info)
  CUSTOMER_TYPE      - standard|premium|vip (default: standard)

Examples:
  # Process an order in development
  ENVIRONMENT=development $0 process-order 123 100.00 credit_card user@example.com
  
  # Run tests with verbose output
  LOG_LEVEL=debug $0 test-all
  
  # Use test implementations
  DATABASE_TYPE=test PAYMENT_GATEWAY=test NOTIFICATION_SERVICE=test $0 process-order 456 50.00 paypal test@example.com
EOF
            ;;
    esac
}

# Execute main function with all arguments
main "$@"

Shell Script Testing Patterns

Testing shell scripts requires creativity, but the same principles apply:

  • Environment variable injection: Use environment variables as dependency injection mechanism
  • Function composition: Break scripts into testable functions
  • Mock external commands: Override external commands with functions for testing
  • Real file operations: Use temporary directories for actual file system tests

PHP 8.4 Specific Features for Dependency Inversion

PHP 8.4 introduces several features that enhance dependency inversion implementation:

Property Hooks for Lazy Initialization

class LazyOrderProcessor
{
    private ?OrderStorageInterface $storage = null;
    
    public OrderStorageInterface $storage {
        get {
            return $this->storage ??= $this->createStorage();
        }
        
        set {
            $this->storage = $value;
        }
    }
    
    private function createStorage(): OrderStorageInterface
    {
        return match($this->environment) {
            'testing' => new InMemoryOrderStorage(),
            'production' => new MySqlOrderStorage($this->connection),
            default => new SqliteOrderStorage()
        };
    }
}

Asymmetric Visibility for Immutable Dependencies

final class SecureOrderProcessor
{
    // Public read, private write - prevents external modification
    public private(set) PaymentGatewayInterface $paymentGateway;
    
    public function __construct(PaymentGatewayInterface $paymentGateway)
    {
        $this->paymentGateway = $paymentGateway;
    }
    
    // Gateway cannot be modified after construction
    // but can be read for testing and debugging
}

Lazy Objects for Performance

PHP 8.4's lazy objects feature allows for sophisticated dependency injection patterns:

$lazyDatabase = LazyObjectFactory::create(
    MySqlDatabase::class,
    function() {
        return new MySqlDatabase(
            $this->config['database']['host'],
            $this->config['database']['name']
        );
    }
);

// Database connection only created when first accessed
$processor = new OrderProcessor($validator, $taxCalculator, $lazyDatabase);

Performance Considerations

Dependency inversion and testing strategies have performance implications that should be considered:

Runtime Performance

  • Final classes: Enable better opcache optimizations in PHP 8.4
  • Interface calls: Minimal overhead in modern PHP versions
  • Lazy loading: Defer expensive object creation until needed
  • Container caching: Cache dependency injection container configuration

Testing Performance

  • Real objects: Often faster than mocks for simple operations
  • In-memory implementations: Provide realistic testing without I/O overhead
  • Mock setup overhead: Consider the cost of mock configuration
  • Parallel testing: Real objects enable better test parallelization

Common Pitfalls and Solutions

Over-Engineering with Interfaces

Problem: Creating interfaces for every class, even simple value objects.

Solution: Only create interfaces when you need polymorphism or dependency inversion. Simple data classes don't need interfaces.

Mock-Heavy Tests

Problem: Mocking everything leads to brittle tests that break on refactoring.

Solution: Use the pragmatic approach—mock external dependencies, use real objects for internal logic.

Inheritance Instead of Composition

Problem: Using abstract base classes instead of dependency injection.

Solution: Favor final classes with injected dependencies over inheritance hierarchies.

Configuration Explosion

Problem: Too many configuration options make the system hard to understand.

Solution: Provide sensible defaults and environment-based configurations.

Best Practices and Guidelines

Design Guidelines

  1. Prefer final classes: Use final classes with dependency injection over inheritance
  2. Design by contract: Create explicit interfaces for dependencies
  3. Single responsibility: Each class should have one reason to change
  4. Immutable dependencies: Don't allow dependencies to change after construction
  5. Environment-based configuration: Use environment variables for different implementations

Testing Guidelines

  1. Start with real objects: Use real implementations unless there's a compelling reason to mock
  2. Mock external boundaries: Always mock databases, HTTP services, file systems
  3. Test behavior, not implementation: Focus on what the code does, not how
  4. Use hybrid approaches: Combine real and mock objects in the same test
  5. Maintain test speed: Fast tests encourage frequent execution

PHP 8.4 Specific Guidelines

  1. Leverage property hooks: Use for lazy initialization and validation
  2. Use asymmetric visibility: Prevent unwanted modifications while maintaining transparency
  3. Adopt lazy objects: For expensive dependencies that may not be used
  4. Final by default: Make classes final unless extension is explicitly needed
  5. Type everything: Use PHP 8.4's enhanced type system for better static analysis

Tools and Resources

Essential PHP Tools

  • PHPUnit 10+: Modern testing framework with improved mocking capabilities
  • PHPStan: Static analysis for catching dependency injection issues
  • PHP-DI: Mature dependency injection container
  • Symfony DI: Powerful dependency injection component
  • Psalm: Advanced static analysis with template support

Testing Resources

PHP 8.4 Documentation

Conclusion

The combination of PHP 8.4's modern features, dependency inversion principles, and pragmatic testing approaches creates a powerful foundation for building maintainable, testable applications. The key insights from this exploration are:

  1. Final classes encourage composition: By preventing inheritance, final classes naturally lead to better dependency inversion patterns
  2. Testing is contextual: The Detroit vs London school debate misses the point—use the right approach for each situation
  3. Real objects are undervalued: Many dependencies can and should be tested with real implementations for better confidence
  4. Modern PHP enables elegant patterns: PHP 8.4's features make dependency inversion more natural and performant
  5. Pragmatism over purity: Combine approaches based on practical concerns rather than ideological adherence

As the PHP ecosystem continues to evolve, these patterns will become increasingly important for building scalable, maintainable applications. The investment in understanding and applying these concepts pays dividends in code quality, testing confidence, and long-term maintainability.

The future of PHP development lies not in choosing between different approaches, but in understanding when and how to apply each technique for maximum benefit. Whether you're building microservices, monoliths, or anything in between, these principles provide a solid foundation for clean, testable, and maintainable code.