Skip to content

SOLID Principles

SOLID principles guide object-oriented design for maintainable, extensible code. Phexium's architecture enforces these principles through its layered structure and plugin system.

Single Responsibility Principle (SRP)

A class should have only one reason to change. Each component in the architecture has a single, well-defined responsibility.

Phexium application:

Component Responsibility Does NOT do
Controller Parse HTTP request, dispatch to bus Business logic, data formatting
Handler Orchestrate domain operations HTTP concerns, presentation
Presenter Transform response to view model Fetch data, modify state
Repository Persist and retrieve entities Validation, business rules
Entity Enforce business invariants Persistence, HTTP

Violations to avoid:

  • Controller containing business logic (should delegate to handler)
  • Handler formatting output for display (should return domain data)
  • Entity persisting itself (should use repository)
  • Repository validating business rules (should only persist)

Open/Closed Principle (OCP)

Software entities should be open for extension but closed for modification. Adding new features should not require changing existing code.

Phexium application:

Adding a new feature follows a predictable pattern:

New feature = New Command + New Handler

Existing handlers remain unchanged. The bus discovers and routes to the new handler automatically.

Extension patterns:

  • New repository implementation → Create new Adapter implementing existing Port
  • New event reaction → Add new listener, no handler changes needed
  • New query variation → Create new Query + Handler pair
  • New validation rule → Extend Value Object, don't modify existing ones

Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base types without altering program correctness. All implementations of an interface must behave consistently.

Phexium application:

All repository implementations (InMemory, Sqlite, Mysql, Postgresql) are interchangeable. Code using BookRepositoryInterface works identically regardless of which adapter is injected.

Requirements for adapters:

  • Honor the full interface contract
  • Throw only expected exception types
  • Maintain consistent behavior across implementations
  • Pass the same test suite regardless of implementation

Violations to avoid:

  • Adapter throwing unexpected exceptions
  • Implementation ignoring part of the interface contract
  • Adapter with different side effects than others

Interface Segregation Principle (ISP)

Clients should not depend on methods they don't use. Prefer small, focused interfaces over large, general-purpose ones.

Phexium application:

The framework splits concerns into separate interfaces:

Instead of Phexium uses
Generic BusInterface CommandBusInterface + QueryBusInterface
Monolithic SessionInterface SessionInterface + FlashInterface
Large RepositoryInterface Per-aggregate interfaces (BookRepositoryInterface, UserRepositoryInterface)

Benefits:

  • Classes depend only on methods they actually use
  • Easier to create focused test doubles
  • Clear contracts for each concern

Dependency Inversion Principle (DIP)

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

Phexium application:

The application layer never imports concrete infrastructure:

// Application layer depends on abstraction (Port)
public function __construct(
    private readonly BookRepositoryInterface $repository,
)

// NOT on concrete implementation (Adapter)
// private readonly SqliteBookRepository $repository  ← WRONG

Dependency flow:

Controller → CommandBusInterface → Handler → BookRepositoryInterface
                                          (injected by DI container)
                                          SqliteBookRepository (or any adapter)

The DI container in config/{app}/container.php wires concrete implementations to interfaces. Application code remains unaware of which adapter is used.

Validation Checklist

When reviewing code, verify:

  • Each class has a single, clear responsibility
  • New features add new classes rather than modifying existing handlers
  • All adapters fully implement their port contracts
  • Interfaces are minimal and focused on one concern
  • No direct imports of concrete infrastructure in application layer

See Also