Clean architecture in PHP has a reputation problem, and the architecture astronauts earned it. Codebases exist where creating a blog post touches nine files across four layers, every class hides behind an interface with one implementation, and the actual business logic is impossible to find under the ceremony. That is not clean architecture; that is cargo culting. The real idea is modest and valuable: keep your business rules independent of delivery and infrastructure details, so the code that makes your application worth running is easy to find, test and change. This post covers what that means in practice for PHP: SOLID as applied judgement rather than commandments, the handful of patterns that actually pay rent, and a project structure that grows without a rewrite.
The one rule that matters: dependencies point inward
Strip away the concentric-circle diagrams and one rule remains: domain logic must not depend on infrastructure. Your pricing rules should not know about Eloquent; your order state machine should not import an HTTP client. The mechanism is ordinary dependency inversion, the domain defines an interface, infrastructure implements it:

namespace App\Domain\Billing;
interface PaymentGateway
{
public function charge(Money $amount, PaymentMethod $method): PaymentResult;
}
namespace App\Infrastructure\Payment;
final class StripeGateway implements PaymentGateway
{
public function charge(Money $amount, PaymentMethod $method): PaymentResult
{
// Stripe SDK calls live here, and only here
}
} The payoff is concrete, not aesthetic: the billing logic is unit-testable with a fake gateway, Stripe can be swapped or supplemented without touching business rules, and a developer reading the domain sees policy, not plumbing. Notice what the rule does not say: it does not demand an interface for every class. Abstract the boundaries that are genuinely external or volatile, payments, mail, storage, clocks, and let stable internal collaborators be concrete classes. An interface with one implementation and no test double is indirection, not architecture.
SOLID, read as advice instead of law
The SOLID principles age well when treated as diagnostic questions rather than compliance checkboxes:
- Single responsibility is about reasons to change: a class that mixes tax rules with CSV formatting will change for two unrelated masters. Split along change boundaries, not into confetti.
- Open/closed earns its keep at plugin points: shipping calculators, notification channels, import formats. Adding a variant should mean adding a class, not editing a growing switch. Elsewhere, editing code is fine; that is what code is for.
- Liskov substitution is mostly a warning against inheritance. In modern PHP,
finalclasses, interfaces and composition sidestep the whole category of subtype surprises; deep class hierarchies are almost always a mistake. - Interface segregation means role-shaped contracts: a reader interface and a writer interface beat one fat repository contract every consumer half-implements.
- Dependency inversion is the inward-pointing rule above, and constructor injection is its whole implementation. If a class news up its own dependencies or reaches into globals, testing and reuse both suffer immediately.
Patterns that pay rent in PHP
Four patterns deliver outsized value in typical PHP applications, and all four got cheaper to write with modern language features. Value objects, readonly classes for Money, Email, DateRange, replace primitive obsession with types that validate once and cannot hold nonsense. Repositories put a domain-shaped query vocabulary (findOverdueFor($customer)) in one place, whether backed by Eloquent, Doctrine or raw PDO. Command-plus-handler (or action classes) gives every use case one obvious home:

final readonly class PlaceOrder
{
public function __construct(
private OrderRepository $orders,
private PaymentGateway $payments,
private EventDispatcher $events,
) {}
public function handle(PlaceOrderCommand $command): Order
{
$order = Order::place($command->customerId, $command->lines);
$this->payments->charge($order->total(), $command->method);
$this->orders->save($order);
$this->events->dispatch(new OrderPlaced($order->id));
return $order;
}
} The same handler serves the web controller, the API endpoint, the CLI command and the test suite, which is precisely the decoupling clean architecture promises. Finally, domain events keep side effects, emails, analytics, cache busting, out of core flows: the handler announces what happened, listeners decide what follows. Treat the rest of the pattern catalogue as a vocabulary to recognise, not a shopping list; a factory or strategy should appear because a problem demanded it.
A structure that scales: organise by feature, not by kind
The default framework skeleton groups code by kind: all controllers together, all models together. It works until roughly the fifty-model mark, when every feature change means a scavenger hunt across parallel trees. The structure that survives growth groups by business capability, with thin delivery and infrastructure rings around a domain core:
src/
Billing/
Domain/ Invoice.php, Money.php, PaymentGateway.php
Application/ PlaceOrder.php, RefundOrder.php
Infrastructure/StripeGateway.php, DoctrineOrderRepository.php
Catalog/
Domain/ ...
Shared/
Domain/ common value objects, event contracts Within a Laravel or Symfony app the same idea works with lighter ceremony: keep framework directories thin (controllers that validate, call a handler, shape a response) and push logic into feature-named namespaces. Enforce the boundaries mechanically rather than by folklore, Deptrac or Pest architecture tests can fail the build when Billing\Domain imports anything from Infrastructure or another module's internals. A boundary that is not enforced is a suggestion, and suggestions erode under deadline pressure.
Pragmatism: architecture should match stakes
The honest caveat is that not every application deserves the full treatment. A content site or a thin CRUD admin gains little from command buses and repository abstractions; framework idioms straight from the documentation are the right call, and dressing a weekend project in hexagonal layers is procrastination with extra folders. The judgement heuristic: invest architecture where the business logic is complicated or long-lived, and stay plain where the application is mostly moving data between forms and tables. Most real systems contain both kinds of code, and the mature move is a rich core for the complicated part sitting beside unapologetically boring CRUD for the rest. Start plainer than feels impressive, and extract layers when the pain is real: the third duplicated query, the untestable side effect, the class with five reasons to change.

The takeaway
Clean architecture in PHP reduces to a few durable habits: point dependencies inward, model the domain in value objects and use-case handlers, abstract only genuine boundaries, organise by feature, and enforce the rules with tools instead of memos. SOLID supplies the judgement calls, patterns supply the vocabulary, and modern PHP, readonly classes, enums, first-class interfaces, makes all of it cheaper than it has ever been. The goal was never diagram compliance. It is a codebase where the important logic is obvious, testable and safe to change, and everything else stays out of its way.
Related Service
๐ป Web Development
Custom websites and web applications built with PHP, Laravel, WordPress, and React by a Chandigarh & Mohali based web developer โ fast, secure, scalable, and trusted by clients across India and worldwide.
Explore Web Development →
Reviews & Comments
Reviews are moderated and appear after approval.
No reviews yet โ be the first to share your thoughts.
Leave a Review