Php Architecture

By the end of this lesson you'll be able to structure a real PHP application in clean layers — keeping your business rules independent of the framework — and apply SOLID, dependency injection, repositories, services, DTOs and the PSR standards that make code swappable and testable.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free Php course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ Layered (Clean) Architecture

Most messy PHP apps put everything in the controller: validation, business rules, SQL, email — all tangled together. Layered architecture (often called clean or hexagonal architecture) untangles this into rings that only point inwards. The golden rule is the dependency rule : inner layers must never know about outer ones.

Because dependencies only point inward, your domain and application code never import a single framework class — which is exactly what lets you unit-test it in isolation and survive a framework upgrade. The next sections build each piece.

2️⃣ Value Objects (the Domain Layer)

A value object wraps a primitive value so it can never be invalid . Instead of passing a raw string around and re-checking "is this really an email?" everywhere, you build an Email that validates itself once, in its constructor. If construction succeeds, every later line can trust it completely. It's defined by its value (two Emails with the same string are equal), it's immutable ( readonly ), and it carries behaviour that belongs to the concept.

Validation happens once, at the boundary, with a guard clause . After that, an Email is a promise: it cannot hold rubbish. This is the heart of a rich domain layer — push rules into small types instead of scattering if checks across the app.

3️⃣ The Repository Layer

A repository is an interface that hides where data lives. Your business logic asks "give me user 1" or "save this user" and never learns whether that's MySQL, Redis, a REST API or an in-memory array. Because the logic depends on the UserRepository interface , you can swap the storage at any time — and in tests you swap in a fast in-memory fake with zero changes to the code under test.

Notice the interface speaks the domain's language ( findById , save ) — never SQL. The concrete InMemoryUserRepository lives in the infrastructure layer; the interface lives next to your domain. That's the dependency rule in action.

4️⃣ Services & Dependency Injection

The service (or action ) layer holds your use cases — one method per thing a user can do. Crucially, a service never builds its own collaborators with new ; it is handed them through its constructor. That's dependency injection (DI): the class declares the interfaces it needs and lets someone else supply concrete implementations.

The single place that picks the real implementations and wires them together is the composition root . In a framework, a DI container (anything implementing PSR-11's ContainerInterface ) reads your config and does this assembly for you.

Because RegisterUser contains no new and no framework imports, you can test it by passing fake implementations — and the framework never leaks into your business logic.

5️⃣ SOLID Applied to PHP

SOLID is five principles that keep classes small and swappable. In PHP they look like this:

Here's the 'D' (which pulls the others along). PaymentService depends on a PaymentGateway interface, so adding PayPal means writing one class — the service itself never changes.

Now you try. A DTO (Data Transfer Object) is a small, immutable, typed bag of values you pass between layers instead of a loose array. Fill in the two missing types using the 👉 hints, then run it and check it against the Output panel.

One more. Make the Greeter depend on the interface so any greeting style can be injected — that's Dependency Inversion in one line.

6️⃣ PSR Standards That Hold It Together

PSRs are PHP Standards Recommendations from the PHP-FIG group — shared interfaces so libraries from different vendors fit together. Four matter most for architecture:

Coding against these interfaces — rather than a specific framework's classes — is what keeps your outer layer swappable. It's the same idea as your repositories and services, applied to the whole HTTP stack.

📋 Quick Reference — Architecture Building Blocks

No code is filled in this time — just a brief and an outline. Build the interface, the implementation, the injected service and the wiring yourself, run it on onecompiler.com/php or your own machine, then check your result against the expected output in the comments. This is exactly the layering you'll repeat for every real feature.

Practice quiz

In layered (clean) architecture, what is the dependency rule?

  • Outer layers must never depend on inner ones
  • Every layer depends on every other
  • Inner layers must never know about outer ones — dependencies only point inward
  • The database layer is the innermost

Answer: Inner layers must never know about outer ones — dependencies only point inward. Dependencies point inward only, so the domain and application layers never import framework classes and can be tested in isolation.

What is a value object such as the Email class in the lesson?

  • A small immutable type that validates itself so it can never hold an invalid value
  • A loose array of values
  • A database row
  • A controller method

Answer: A small immutable type that validates itself so it can never hold an invalid value. A value object wraps a primitive and validates in its constructor, so once built it's a promise the value can't be rubbish.

What is the main purpose of a repository?

  • To render HTML templates
  • To send emails
  • To compile PHP to bytecode
  • To hide WHERE data lives behind an interface so business logic doesn't know the storage

Answer: To hide WHERE data lives behind an interface so business logic doesn't know the storage. A repository exposes a domain-language interface (findById, save) so you can swap MySQL, an API, or an in-memory fake without changing the logic.

What does dependency injection mean for a service class?

  • It builds its own collaborators with new
  • It is handed its collaborators (usually via the constructor) instead of creating them
  • It injects SQL into queries
  • It loads classes via require

Answer: It is handed its collaborators (usually via the constructor) instead of creating them. With DI a class declares the interfaces it needs and someone else supplies the concrete implementations — no 'new' inside the service.

Where are the real implementations wired together?

  • In the composition root (in a framework, a PSR-11 DI container)
  • Inside each value object
  • In the database schema
  • In the repository interface

Answer: In the composition root (in a framework, a PSR-11 DI container). The composition root is the one place that picks concrete implementations and assembles them; a PSR-11 container does this from config.

What is the 'D' in SOLID, Dependency Inversion, in practice?

  • Depend on concrete classes for speed
  • Duplicate logic across classes
  • Depend on abstractions (interfaces) and inject them, not on concretions
  • Delete unused dependencies

Answer: Depend on abstractions (interfaces) and inject them, not on concretions. Type-hint and inject interfaces (like PaymentGateway) so adding a new implementation is one class and the consuming service never changes.

How does a DTO differ from a value object?

  • They are the same thing
  • A DTO carries typed data across a boundary; a value object encodes domain rules and validates itself
  • A DTO always has behaviour and a value object never does
  • A value object crosses layers; a DTO models a domain concept

Answer: A DTO carries typed data across a boundary; a value object encodes domain rules and validates itself. DTOs are typed data bags that move data between layers; value objects (Email, Money) model a concept, validate, and carry behaviour.

What does PSR-4 standardise?

  • HTTP message interfaces
  • The DI container API
  • Middleware handlers
  • Autoloading — mapping a namespace prefix to a directory so Composer loads classes on demand

Answer: Autoloading — mapping a namespace prefix to a directory so Composer loads classes on demand. PSR-4 maps a namespace prefix to a folder, so App\Domain\Email lives in src/Domain/Email.php with no require statements.

Which PSR defines the ContainerInterface with get() and has()?

  • PSR-4
  • PSR-11
  • PSR-7
  • PSR-15

Answer: PSR-11. PSR-11 is the ContainerInterface that every DI container implements; PSR-7 is HTTP messages and PSR-15 is HTTP handlers/middleware.

What error do you get from trying to instantiate an interface directly?

  • Class not found
  • Cannot modify readonly property
  • Cannot instantiate interface — instantiate a concrete class that implements it instead
  • Nothing, it works fine

Answer: Cannot instantiate interface — instantiate a concrete class that implements it instead. You cannot do new UserRepository() on an interface; create a concrete class like InMemoryUserRepository and inject that.