Back to Articles
45 min read

Enterprise PHP Architecture: Composer, PSR Standards, and Design Patterns

Writing code is easy; engineering maintainable systems is hard. This comprehensive guide moves beyond syntax to explore the professional PHP ecosystem. We cover the lifecycle of dependency management via Composer, strict adherence to PSR interoperability standards, and the implementation of decoupled architecture using Dependency Injection containers and standard Design Patterns.

PHP Advanced Concepts Guide

Composer and Dependency Management

Installing Composer

Composer is PHP's de-facto dependency manager, installable globally via curl -sS https://getcomposer.org/installer | php on Unix or via the Windows installer. Once installed, you can run composer commands from any project directory to manage packages, autoloading, and scripts.

# Global installation on Linux/Mac curl -sS https://getcomposer.org/installer | php sudo mv composer.phar /usr/local/bin/composer # Verify installation composer --version

composer.json Structure

The composer.json file is the heart of any PHP project using Composer, defining metadata, dependencies, autoloading rules, and scripts in a JSON format that Composer reads to manage your project.

{ "name": "vendor/project-name", "description": "Project description", "type": "project", "require": { "php": "^8.1", "monolog/monolog": "^3.0" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "autoload": { "psr-4": { "App\\": "src/" } }, "scripts": { "test": "phpunit" } }

Installing Packages

Use composer require vendor/package to add production dependencies or composer require --dev vendor/package for development-only dependencies; Composer automatically updates composer.json and composer.lock, then downloads packages to the vendor/ directory.

# Production dependency composer require guzzlehttp/guzzle # Development dependency composer require --dev phpstan/phpstan # Install all dependencies from composer.lock composer install # Update dependencies to latest versions composer update

Autoloading (PSR-4, PSR-0, Classmap)

Composer generates an autoloader that eliminates manual require statements by mapping namespaces to directories (PSR-4/PSR-0) or scanning files for classes (classmap), with PSR-4 being the modern standard that maps namespace prefixes directly to base directories.

{ "autoload": { "psr-4": { "App\\": "src/", "App\\Tests\\": "tests/" }, "classmap": ["legacy/"], "files": ["src/helpers.php"] } }
PSR-4 Mapping:
┌─────────────────────────────┐
│ Namespace: App\Models\User  │
│         ↓                   │
│ File: src/Models/User.php   │
└─────────────────────────────┘

Semantic Versioning

Composer uses semantic versioning (SemVer) where versions follow MAJOR.MINOR.PATCH format, and you can specify constraints like ^2.0 (compatible with 2.x), ~2.0.3 (>=2.0.3 <2.1), or exact versions to control which package versions are acceptable.

Version: X.Y.Z
         │ │ │
         │ │ └── PATCH: Bug fixes (backward compatible)
         │ └──── MINOR: New features (backward compatible)
         └────── MAJOR: Breaking changes

Constraints:
┌────────────┬─────────────────────────────┐
│ ^1.2.3     │ >=1.2.3 <2.0.0              │
│ ~1.2.3     │ >=1.2.3 <1.3.0              │
│ 1.2.*      │ >=1.2.0 <1.3.0              │
│ >=1.0 <2.0 │ Explicit range              │
└────────────┴─────────────────────────────┘

Lock Files

The composer.lock file records the exact versions of all installed dependencies, ensuring every developer and deployment environment gets identical package versions; always commit this file to version control and use composer install (not update) in production.

Workflow:
┌──────────────────────────────────────────────────────────┐
│  composer.json  ──(composer update)──► composer.lock     │
│       │                                      │           │
│  "What I want"                    "What I got exactly"   │
│                                              │           │
│                    (composer install)────────┘           │
│                           │                              │
│                     vendor/ directory                    │
└──────────────────────────────────────────────────────────┘

Private Repositories

Composer can pull packages from private Git repositories, Satis, or Private Packagist by configuring the repositories key in composer.json and storing authentication credentials in auth.json or environment variables.

{ "repositories": [ { "type": "vcs", "url": "git@github.com:company/private-package.git" }, { "type": "composer", "url": "https://satis.company.com" } ], "require": { "company/private-package": "^1.0" } }
# Store credentials composer config --global --auth github-oauth.github.com <token>

Creating Packages

To create a reusable Composer package, structure your code with a composer.json defining the package name, autoloading, and dependencies, then organize source files following PSR-4 conventions in a src/ directory.

my-package/
├── composer.json
├── src/
│   └── MyClass.php
├── tests/
│   └── MyClassTest.php
├── README.md
└── LICENSE
{ "name": "yourvendor/my-package", "autoload": { "psr-4": { "YourVendor\\MyPackage\\": "src/" } } }

Publishing to Packagist

Packagist.org is the default public repository for Composer packages; to publish, create a GitHub repository, register on Packagist, submit your repository URL, and configure a webhook for automatic updates when you push new tags.

Publishing Flow:
┌────────────┐     ┌────────────┐     ┌─────────────┐
│   Local    │────►│   GitHub   │────►│  Packagist  │
│   Code     │push │   Repo     │hook │   Registry  │
└────────────┘     └────────────┘     └─────────────┘
                                            │
                                            ▼
                                    composer require
                                    yourvendor/package

Scripts and Hooks

Composer scripts let you run PHP callbacks or shell commands at various lifecycle events (pre/post install, update, autoload-dump) or as custom commands, enabling automation of tasks like cache clearing, asset compilation, or running tests.

{ "scripts": { "post-install-cmd": [ "@php artisan clear-compiled", "MyVendor\\MyClass::postInstall" ], "post-update-cmd": "@php artisan ide-helper:generate", "test": "phpunit", "cs-fix": "php-cs-fixer fix src/" } }
# Run custom script composer test composer cs-fix

PHP Standards Recommendations (PSR)

PSR-1: Basic Coding Standard

PSR-1 establishes fundamental coding elements: files must use <?php or <?= tags only, use UTF-8 encoding without BOM, classes must be in StudlyCaps, constants in UPPER_CASE, and methods in camelCase, ensuring baseline interoperability across PHP code.

<?php // PSR-1 Compliant namespace Vendor\Package; class ClassName { const VERSION = '1.0.0'; public function methodName() { // Method body } }

PSR-4: Autoloading Standard

PSR-4 defines how to autoload classes from file paths by mapping a namespace prefix to a base directory, where subsequent namespace portions map to subdirectories and the class name becomes the filename with .php extension.

┌─────────────────────────────────────────────────────────┐
│  Namespace Prefix  │  Base Directory  │  Example        │
├────────────────────┼──────────────────┼─────────────────┤
│  App\              │  src/            │                 │
│  App\Models\User   │       →          │ src/Models/User.php
└─────────────────────────────────────────────────────────┘
<?php // File: src/Models/User.php namespace App\Models; class User { }

PSR-7: HTTP Message Interface

PSR-7 defines immutable interfaces for HTTP messages (RequestInterface, ResponseInterface, StreamInterface, UriInterface), enabling framework-agnostic middleware and HTTP handlers that work across any compliant implementation like Guzzle, Laminas Diactoros, or Nyholm.

<?php use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; function handleRequest(ServerRequestInterface $request): ResponseInterface { $uri = $request->getUri(); $method = $request->getMethod(); $body = $request->getBody()->getContents(); // PSR-7 objects are immutable - returns new instance return $response ->withStatus(200) ->withHeader('Content-Type', 'application/json'); }

PSR-11: Container Interface

PSR-11 standardizes dependency injection container interoperability with two methods: get($id) to retrieve entries and has($id) to check existence, allowing libraries to accept any compliant container without coupling to specific implementations.

<?php use Psr\Container\ContainerInterface; class ServiceLocator { public function __construct(private ContainerInterface $container) {} public function getLogger() { if ($this->container->has('logger')) { return $this->container->get('logger'); } throw new ServiceNotFoundException('Logger not found'); } }

PSR-12: Extended Coding Style

PSR-12 extends PSR-1 and PSR-2 with comprehensive coding style rules: 4-space indentation, 120-character soft line limit, specific brace placement, blank line requirements, and detailed formatting for classes, methods, control structures, and operators.

<?php declare(strict_types=1); namespace Vendor\Package; use Vendor\Package\SomeClass; use Vendor\Package\AnotherClass as Another; class ClassName extends ParentClass implements InterfaceName { private string $property; public function methodName(int $arg1, ?string $arg2 = null): bool { if ($arg1 === 1) { return true; } return false; } }

PSR-15: HTTP Handlers

PSR-15 defines two interfaces for server-side request handling: RequestHandlerInterface with a handle() method for final handlers, and MiddlewareInterface with a process() method for middleware that can modify requests/responses or delegate to the next handler.

<?php use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Message\{ServerRequestInterface, ResponseInterface}; class AuthMiddleware implements MiddlewareInterface { public function process( ServerRequestInterface $request, RequestHandlerInterface $handler ): ResponseInterface { if (!$this->isAuthenticated($request)) { return new Response(401); } return $handler->handle($request); } }
Middleware Pipeline:
Request → [Auth] → [Logging] → [Handler] → Response
            ↓          ↓           ↓
         process()  process()   handle()

PSR-17: HTTP Factories

PSR-17 provides factory interfaces for creating PSR-7 HTTP objects (requests, responses, streams, URIs), enabling libraries to create these objects without depending on specific implementations, improving decoupling and testability.

<?php use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\StreamFactoryInterface; class ApiController { public function __construct( private ResponseFactoryInterface $responseFactory, private StreamFactoryInterface $streamFactory ) {} public function jsonResponse(array $data): ResponseInterface { $body = $this->streamFactory->createStream(json_encode($data)); return $this->responseFactory->createResponse(200) ->withHeader('Content-Type', 'application/json') ->withBody($body); } }

PSR-18: HTTP Client

PSR-18 defines a ClientInterface with a single sendRequest(RequestInterface): ResponseInterface method, standardizing HTTP client behavior and enabling libraries to make HTTP calls without coupling to Guzzle, cURL, or any specific implementation.

<?php use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; class GitHubApi { public function __construct( private ClientInterface $httpClient, private RequestFactoryInterface $requestFactory ) {} public function getUser(string $username): array { $request = $this->requestFactory ->createRequest('GET', "https://api.github.com/users/{$username}"); $response = $this->httpClient->sendRequest($request); return json_decode($response->getBody()->getContents(), true); } }

Design Patterns

Creational Patterns

Singleton

Singleton ensures a class has only one instance globally accessible, useful for database connections or configuration managers; implemented with a private constructor and static method, though often considered an anti-pattern due to global state and testing difficulties.

<?php class Database { private static ?Database $instance = null; private function __construct() {} // Prevent direct instantiation private function __clone() {} // Prevent cloning public static function getInstance(): Database { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } } // Usage $db = Database::getInstance();

Factory Method

Factory Method defines an interface for creating objects but lets subclasses decide which class to instantiate, promoting loose coupling by eliminating the need to bind application-specific classes into your code.

<?php abstract class NotificationFactory { abstract public function createNotification(): Notification; public function send(string $message): void { $notification = $this->createNotification(); $notification->deliver($message); } } class EmailNotificationFactory extends NotificationFactory { public function createNotification(): Notification { return new EmailNotification(); } } class SmsNotificationFactory extends NotificationFactory { public function createNotification(): Notification { return new SmsNotification(); } }

Abstract Factory

Abstract Factory provides an interface for creating families of related objects without specifying concrete classes, perfect for cross-platform UI toolkits or database abstraction where you need consistent object families that work together.

<?php interface UIFactory { public function createButton(): Button; public function createCheckbox(): Checkbox; } class MaterialUIFactory implements UIFactory { public function createButton(): Button { return new MaterialButton(); } public function createCheckbox(): Checkbox { return new MaterialCheckbox(); } } class BootstrapUIFactory implements UIFactory { public function createButton(): Button { return new BootstrapButton(); } public function createCheckbox(): Checkbox { return new BootstrapCheckbox(); } } // Client code works with any factory function renderForm(UIFactory $factory) { $button = $factory->createButton(); $checkbox = $factory->createCheckbox(); }

Builder

Builder separates complex object construction from its representation, allowing the same construction process to create different representations; particularly useful for objects requiring numerous optional parameters or step-by-step construction.

<?php class QueryBuilder { private array $select = ['*']; private string $from = ''; private array $where = []; public function select(array $columns): self { $this->select = $columns; return $this; } public function from(string $table): self { $this->from = $table; return $this; } public function where(string $column, mixed $value): self { $this->where[] = [$column, $value]; return $this; } public function build(): string { return "SELECT " . implode(', ', $this->select) . " FROM {$this->from}" . $this->buildWhere(); } } // Usage: Fluent interface $query = (new QueryBuilder()) ->select(['id', 'name']) ->from('users') ->where('active', 1) ->build();

Prototype

Prototype creates new objects by cloning existing instances rather than using constructors, useful when object creation is expensive or when you need copies of complex objects with slight modifications.

<?php class Document implements Cloneable { public function __construct( private string $title, private array $sections, private \DateTime $created ) {} public function __clone() { // Deep clone mutable objects $this->created = clone $this->created; $this->sections = array_map(fn($s) => clone $s, $this->sections); } } // Usage $template = new Document('Template', [...], new \DateTime()); $newDoc = clone $template; $newDoc->setTitle('New Document');

Structural Patterns

Adapter

Adapter converts an interface of one class to another interface clients expect, enabling incompatible classes to work together; commonly used for integrating third-party libraries or legacy code with new systems.

<?php // Third-party XML parser (incompatible interface) class XmlParser { public function parseXmlString(string $xml): array { /*...*/ } } // Our application expects this interface interface DataParserInterface { public function parse(string $data): array; } // Adapter makes XmlParser compatible class XmlParserAdapter implements DataParserInterface { public function __construct(private XmlParser $xmlParser) {} public function parse(string $data): array { return $this->xmlParser->parseXmlString($data); } }
┌────────────┐      ┌─────────────┐      ┌────────────┐
│   Client   │─────►│   Adapter   │─────►│  Adaptee   │
│            │      │ (Wrapper)   │      │ (XmlParser)│
└────────────┘      └─────────────┘      └────────────┘

Decorator

Decorator dynamically attaches additional responsibilities to objects without modifying their code, providing a flexible alternative to subclassing for extending functionality through composition and delegation.

<?php interface Coffee { public function cost(): float; public function description(): string; } class SimpleCoffee implements Coffee { public function cost(): float { return 2.00; } public function description(): string { return 'Coffee'; } } abstract class CoffeeDecorator implements Coffee { public function __construct(protected Coffee $coffee) {} } class MilkDecorator extends CoffeeDecorator { public function cost(): float { return $this->coffee->cost() + 0.50; } public function description(): string { return $this->coffee->description() . ', Milk'; } } // Usage: Stack decorators $coffee = new MilkDecorator(new MilkDecorator(new SimpleCoffee())); echo $coffee->description(); // "Coffee, Milk, Milk" echo $coffee->cost(); // 3.00

Facade

Facade provides a unified, simplified interface to a complex subsystem of classes, reducing coupling between clients and subsystem internals while making the subsystem easier to use without hiding its power for advanced users.

<?php class VideoConverter { public function convert(string $filename, string $format): File { $file = new VideoFile($filename); $codec = (new CodecFactory())->extract($file); $converter = ($format === 'mp4') ? new MPEG4Compressor() : new OggCompressor(); $audio = (new AudioMixer())->fix($codec); return $converter->convert($file, $audio); } } // Simple interface hides complex subsystem $converter = new VideoConverter(); $mp4 = $converter->convert('video.avi', 'mp4');
┌──────────────────────────────────────────┐
│                 FACADE                    │
│            VideoConverter                 │
└────────────────────┬─────────────────────┘
         ┌───────────┼───────────┐
         ▼           ▼           ▼
   ┌──────────┐ ┌──────────┐ ┌──────────┐
   │VideoFile │ │CodecFact │ │AudioMixer│
   └──────────┘ └──────────┘ └──────────┘

Proxy

Proxy provides a surrogate or placeholder for another object to control access, add lazy initialization, logging, access control, or caching without changing the original object's code.

<?php interface Image { public function display(): void; } class RealImage implements Image { public function __construct(private string $filename) { $this->loadFromDisk(); // Expensive operation } private function loadFromDisk(): void { /*...*/ } public function display(): void { echo "Displaying {$this->filename}"; } } class LazyImageProxy implements Image { private ?RealImage $realImage = null; public function __construct(private string $filename) {} public function display(): void { // Lazy loading - only load when actually needed $this->realImage ??= new RealImage($this->filename); $this->realImage->display(); } }

Composite

Composite composes objects into tree structures representing part-whole hierarchies, allowing clients to treat individual objects and compositions uniformly—ideal for representing hierarchies like file systems, menus, or organizational structures.

<?php interface FileSystemNode { public function getSize(): int; public function getName(): string; } class File implements FileSystemNode { public function __construct(private string $name, private int $size) {} public function getName(): string { return $this->name; } public function getSize(): int { return $this->size; } } class Directory implements FileSystemNode { private array $children = []; public function __construct(private string $name) {} public function add(FileSystemNode $node): void { $this->children[] = $node; } public function getName(): string { return $this->name; } public function getSize(): int { return array_sum(array_map(fn($c) => $c->getSize(), $this->children)); } }
         [Directory: root]
         /       |        \
   [Dir: src]  [File]  [Dir: tests]
      /    \              |
  [File]  [File]       [File]

Bridge

Bridge decouples an abstraction from its implementation so both can vary independently, useful when you have multiple orthogonal dimensions of variation like platform-specific rendering or different database drivers.

<?php // Implementation hierarchy interface Renderer { public function renderTitle(string $title): string; public function renderContent(string $content): string; } class HtmlRenderer implements Renderer { /*...*/ } class JsonRenderer implements Renderer { /*...*/ } // Abstraction hierarchy abstract class Page { public function __construct(protected Renderer $renderer) {} abstract public function render(): string; } class ArticlePage extends Page { public function render(): string { return $this->renderer->renderTitle($this->title) . $this->renderer->renderContent($this->content); } } // Combine independently $htmlArticle = new ArticlePage(new HtmlRenderer()); $jsonArticle = new ArticlePage(new JsonRenderer());

Flyweight

Flyweight minimizes memory usage by sharing common state (intrinsic) across many objects while keeping unique state (extrinsic) external, ideal for large numbers of similar objects like characters in a text editor or trees in a game forest.

<?php class CharacterStyle // Flyweight - shared intrinsic state { public function __construct( public readonly string $font, public readonly int $size, public readonly string $color ) {} } class CharacterStyleFactory { private array $styles = []; public function getStyle(string $font, int $size, string $color): CharacterStyle { $key = "{$font}_{$size}_{$color}"; return $this->styles[$key] ??= new CharacterStyle($font, $size, $color); } } class Character // Uses flyweight + extrinsic state (position) { public function __construct( public string $char, public CharacterStyle $style, // Shared public int $position // Unique ) {} }

Behavioral Patterns

Strategy

Strategy defines a family of interchangeable algorithms encapsulated in separate classes, letting the algorithm vary independently from clients that use it—perfect for payment processing, sorting, or validation with multiple approaches.

<?php interface PaymentStrategy { public function pay(float $amount): bool; } class CreditCardPayment implements PaymentStrategy { public function pay(float $amount): bool { /* Process CC */ return true; } } class PayPalPayment implements PaymentStrategy { public function pay(float $amount): bool { /* Process PayPal */ return true; } } class ShoppingCart { public function checkout(PaymentStrategy $paymentMethod): bool { $total = $this->calculateTotal(); return $paymentMethod->pay($total); } } // Usage - switch strategies at runtime $cart = new ShoppingCart(); $cart->checkout(new CreditCardPayment()); $cart->checkout(new PayPalPayment());

Observer

Observer defines a one-to-many dependency where when one object (subject) changes state, all dependents (observers) are notified automatically—ideal for event systems, UI updates, or any publish-subscribe mechanism.

<?php class EventDispatcher { private array $listeners = []; public function subscribe(string $event, callable $listener): void { $this->listeners[$event][] = $listener; } public function dispatch(string $event, mixed $data = null): void { foreach ($this->listeners[$event] ?? [] as $listener) { $listener($data); } } } // Usage $dispatcher = new EventDispatcher(); $dispatcher->subscribe('user.created', fn($user) => sendWelcomeEmail($user)); $dispatcher->subscribe('user.created', fn($user) => logUserCreation($user)); $dispatcher->dispatch('user.created', $newUser);

Command

Command encapsulates a request as an object, allowing you to parameterize clients with different requests, queue or log operations, and support undoable operations by storing the state needed to reverse effects.

<?php interface Command { public function execute(): void; public function undo(): void; } class AddTextCommand implements Command { private string $previousText; public function __construct(private Editor $editor, private string $text) {} public function execute(): void { $this->previousText = $this->editor->getText(); $this->editor->appendText($this->text); } public function undo(): void { $this->editor->setText($this->previousText); } } class CommandHistory { private array $history = []; public function push(Command $cmd): void { $this->history[] = $cmd; } public function pop(): ?Command { return array_pop($this->history); } }

Iterator

Iterator provides a way to access elements of a collection sequentially without exposing its underlying representation; PHP has built-in Iterator and IteratorAggregate interfaces that integrate with foreach loops.

<?php class BookCollection implements \IteratorAggregate { private array $books = []; public function addBook(Book $book): void { $this->books[] = $book; } public function getIterator(): \Traversable { return new \ArrayIterator($this->books); } } // Or implement Iterator for custom traversal class AlphabeticalBookIterator implements \Iterator { private int $position = 0; private array $sortedBooks; public function current(): Book { return $this->sortedBooks[$this->position]; } public function key(): int { return $this->position; } public function next(): void { $this->position++; } public function rewind(): void { $this->position = 0; } public function valid(): bool { return isset($this->sortedBooks[$this->position]); } }

State

State allows an object to alter its behavior when its internal state changes, appearing to change its class; it encapsulates state-specific behavior in separate classes and delegates to the current state object.

<?php interface OrderState { public function proceed(Order $order): void; public function cancel(Order $order): void; } class PendingState implements OrderState { public function proceed(Order $order): void { $order->setState(new PaidState()); } public function cancel(Order $order): void { $order->setState(new CancelledState()); } } class PaidState implements OrderState { public function proceed(Order $order): void { $order->setState(new ShippedState()); } public function cancel(Order $order): void { throw new \Exception("Cannot cancel paid order"); } } class Order { private OrderState $state; public function __construct() { $this->state = new PendingState(); } public function setState(OrderState $state): void { $this->state = $state; } public function proceed(): void { $this->state->proceed($this); } }
[Pending] ──proceed──► [Paid] ──proceed──► [Shipped] ──proceed──► [Delivered]
    │                    │
  cancel               cancel (throws)
    ▼
[Cancelled]

Template Method

Template Method defines the skeleton of an algorithm in a base class, letting subclasses override specific steps without changing the overall structure—perfect for frameworks where you want to control flow but allow customization of details.

<?php abstract class DataExporter { // Template method - defines the algorithm skeleton public final function export(array $data): string { $filtered = $this->filter($data); $formatted = $this->format($filtered); return $this->encode($formatted); } protected function filter(array $data): array { return $data; } // Hook abstract protected function format(array $data): array; // Required abstract protected function encode(array $data): string; // Required } class CsvExporter extends DataExporter { protected function format(array $data): array { /* Format for CSV */ } protected function encode(array $data): string { return implode(',', $data); } } class JsonExporter extends DataExporter { protected function format(array $data): array { return $data; } protected function encode(array $data): string { return json_encode($data); } }

Chain of Responsibility

Chain of Responsibility passes requests along a chain of handlers where each handler decides either to process the request or pass it to the next handler—commonly used for middleware pipelines, logging levels, or approval workflows.

<?php abstract class Handler { private ?Handler $next = null; public function setNext(Handler $handler): Handler { $this->next = $handler; return $handler; } public function handle(Request $request): ?Response { if ($this->next) { return $this->next->handle($request); } return null; } } class AuthHandler extends Handler { public function handle(Request $request): ?Response { if (!$request->hasValidToken()) { return new Response(401, 'Unauthorized'); } return parent::handle($request); } } class RateLimitHandler extends Handler { /*...*/ } class ValidationHandler extends Handler { /*...*/ } // Build chain $handler = new AuthHandler(); $handler->setNext(new RateLimitHandler())->setNext(new ValidationHandler()); $response = $handler->handle($request);

Mediator

Mediator defines an object that encapsulates how a set of objects interact, promoting loose coupling by preventing objects from referring to each other explicitly and centralizing complex communication logic.

<?php interface ChatMediator { public function sendMessage(string $message, User $sender): void; public function addUser(User $user): void; } class ChatRoom implements ChatMediator { private array $users = []; public function addUser(User $user): void { $this->users[] = $user; } public function sendMessage(string $message, User $sender): void { foreach ($this->users as $user) { if ($user !== $sender) { $user->receive($message, $sender->getName()); } } } } class User { public function __construct(private string $name, private ChatMediator $chat) {} public function send(string $message): void { $this->chat->sendMessage($message, $this); } public function receive(string $message, string $from): void { /* Display */ } }

Memento

Memento captures and externalizes an object's internal state so it can be restored later without violating encapsulation—essential for implementing undo mechanisms, checkpoints, or state snapshots.

<?php class EditorMemento { public function __construct(private readonly string $content) {} public function getContent(): string { return $this->content; } } class Editor { private string $content = ''; public function type(string $text): void { $this->content .= $text; } public function getContent(): string { return $this->content; } public function save(): EditorMemento { return new EditorMemento($this->content); } public function restore(EditorMemento $memento): void { $this->content = $memento->getContent(); } } class History { private array $mementos = []; public function push(EditorMemento $m): void { $this->mementos[] = $m; } public function pop(): ?EditorMemento { return array_pop($this->mementos); } }

Visitor

Visitor lets you add new operations to existing object structures without modifying them by separating algorithms from the objects they operate on—useful for operations across heterogeneous collections like AST traversal or report generation.

<?php interface ShapeVisitor { public function visitCircle(Circle $circle): void; public function visitRectangle(Rectangle $rect): void; } interface Shape { public function accept(ShapeVisitor $visitor): void; } class Circle implements Shape { public function __construct(public float $radius) {} public function accept(ShapeVisitor $visitor): void { $visitor->visitCircle($this); } } class AreaCalculator implements ShapeVisitor { public float $totalArea = 0; public function visitCircle(Circle $c): void { $this->totalArea += pi() * $c->radius ** 2; } public function visitRectangle(Rectangle $r): void { $this->totalArea += $r->width * $r->height; } } // Add new operations without changing Shape classes class JsonExportVisitor implements ShapeVisitor { /*...*/ }

Repository Pattern

Repository mediates between the domain and data mapping layers, acting like an in-memory collection of domain objects while encapsulating query logic and providing a clean separation between business logic and data access.

<?php interface UserRepositoryInterface { public function find(int $id): ?User; public function findByEmail(string $email): ?User; public function save(User $user): void; public function delete(User $user): void; public function all(): array; } class UserRepository implements UserRepositoryInterface { public function __construct(private PDO $pdo) {} public function find(int $id): ?User { $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = ?'); $stmt->execute([$id]); $row = $stmt->fetch(); return $row ? $this->hydrate($row) : null; } private function hydrate(array $row): User { return new User($row['id'], $row['name'], $row['email']); } }

Active Record Pattern

Active Record wraps a database row, encapsulating data access and adding domain logic directly to the object; the object knows how to persist itself, making it simple but tightly coupling business logic to persistence.

<?php class User { public int $id; public string $name; public string $email; public static function find(int $id): ?self { $row = Database::query('SELECT * FROM users WHERE id = ?', [$id]); return $row ? self::hydrate($row) : null; } public function save(): void { if (isset($this->id)) { Database::query('UPDATE users SET name=?, email=? WHERE id=?', [$this->name, $this->email, $this->id]); } else { Database::query('INSERT INTO users (name, email) VALUES (?, ?)', [$this->name, $this->email]); $this->id = Database::lastInsertId(); } } } // Usage - object knows how to persist itself $user = User::find(1); $user->email = 'new@email.com'; $user->save();

Data Mapper Pattern

Data Mapper transfers data between domain objects and database while keeping them independent of each other; domain objects have no knowledge of the database, and the mapper handles all persistence logic, providing better separation of concerns than Active Record.

<?php // Domain object - no persistence knowledge class User { public function __construct( public readonly ?int $id, public string $name, public string $email ) {} } // Mapper handles all persistence class UserMapper { public function __construct(private PDO $pdo) {} public function find(int $id): ?User { $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = ?'); $stmt->execute([$id]); $row = $stmt->fetch(); return $row ? new User($row['id'], $row['name'], $row['email']) : null; } public function insert(User $user): User { $stmt = $this->pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)'); $stmt->execute([$user->name, $user->email]); return new User((int)$this->pdo->lastInsertId(), $user->name, $user->email); } }

Service Layer Pattern

Service Layer defines an application's boundary with a layer of services that establishes operations available to clients and coordinates responses, orchestrating use cases while keeping domain logic in domain objects.

<?php class UserService { public function __construct( private UserRepository $users, private EmailService $mailer, private EventDispatcher $events ) {} public function registerUser(RegisterUserDTO $dto): User { // Orchestrate the use case $user = new User($dto->name, $dto->email); $user->setPassword(password_hash($dto->password, PASSWORD_DEFAULT)); $this->users->save($user); $this->mailer->sendWelcome($user); $this->events->dispatch('user.registered', $user); return $user; } public function changeEmail(int $userId, string $newEmail): void { $user = $this->users->find($userId) ?? throw new UserNotFoundException(); $user->changeEmail($newEmail); $this->users->save($user); } }

DTO (Data Transfer Object)

DTO is a simple object that carries data between processes or layers without any business logic, often used to aggregate data for API responses, form submissions, or transferring data between service boundaries.

<?php readonly class CreateUserDTO { public function __construct( public string $name, public string $email, public string $password, ) {} public static function fromRequest(Request $request): self { return new self( name: $request->get('name'), email: $request->get('email'), password: $request->get('password'), ); } } readonly class UserResponseDTO { public function __construct( public int $id, public string $name, public string $email, public string $createdAt, ) {} public static function fromUser(User $user): self { return new self($user->id, $user->name, $user->email, $user->createdAt->format('c')); } }

Value Object Pattern

Value Objects are immutable objects defined by their attributes rather than identity; two value objects are equal if their properties are equal, commonly used for Money, Email, DateRange, or Address where identity doesn't matter.

<?php readonly class Money { public function __construct( public int $amount, // Store as cents public string $currency ) { if ($amount < 0) throw new \InvalidArgumentException('Amount cannot be negative'); } public function add(Money $other): Money { $this->ensureSameCurrency($other); return new Money($this->amount + $other->amount, $this->currency); } public function equals(Money $other): bool { return $this->amount === $other->amount && $this->currency === $other->currency; } private function ensureSameCurrency(Money $other): void { if ($this->currency !== $other->currency) { throw new \InvalidArgumentException('Currency mismatch'); } } } // Usage - immutable, compared by value $price = new Money(1999, 'USD'); // $19.99 $tax = new Money(200, 'USD'); // $2.00 $total = $price->add($tax); // New object: $21.99

SOLID Principles

Single Responsibility Principle

A class should have only one reason to change, meaning it should have only one job or responsibility; this reduces coupling, makes code easier to understand, test, and maintain by ensuring each class focuses on a single concern.

<?php // ❌ BAD - Multiple responsibilities class User { public function save() { /* Database logic */ } public function sendEmail() { /* Email logic */ } public function generateReport() { /* Report logic */ } } // ✅ GOOD - Single responsibility each class User { public function __construct(public string $name, public string $email) {} } class UserRepository { public function save(User $user): void { /* Only database */ } } class UserMailer { public function sendWelcome(User $user): void { /* Only email */ } } class UserReportGenerator { public function generate(User $user): Report { /* Only reports */ } }

Open/Closed Principle

Software entities should be open for extension but closed for modification; you should be able to add new behavior without changing existing code, typically achieved through abstractions, interfaces, and polymorphism.

<?php // ❌ BAD - Must modify class to add new discount types class DiscountCalculator { public function calculate(string $type, float $amount): float { return match($type) { 'percentage' => $amount * 0.1, 'fixed' => $amount - 10, // Must add new cases here }; } } // ✅ GOOD - Extend by adding new classes interface Discount { public function apply(float $amount): float; } class PercentageDiscount implements Discount { public function __construct(private float $percent) {} public function apply(float $amount): float { return $amount * (1 - $this->percent); } } class FixedDiscount implements Discount { public function __construct(private float $value) {} public function apply(float $amount): float { return $amount - $this->value; } } // Add new discount types without modifying existing code class BuyOneGetOneFree implements Discount { /*...*/ }

Liskov Substitution Principle

Objects of a superclass should be replaceable with objects of its subclasses without breaking the application; subclasses must honor the contract of the parent, maintaining expected behavior, preconditions, postconditions, and invariants.

<?php // ❌ BAD - Square violates Rectangle's contract class Rectangle { public function setWidth(int $w): void { $this->width = $w; } public function setHeight(int $h): void { $this->height = $h; } } class Square extends Rectangle { public function setWidth(int $w): void { $this->width = $this->height = $w; } // Violates LSP! } // ✅ GOOD - Use composition or separate abstractions interface Shape { public function area(): float; } class Rectangle implements Shape { public function __construct(private float $width, private float $height) {} public function area(): float { return $this->width * $this->height; } } class Square implements Shape { public function __construct(private float $side) {} public function area(): float { return $this->side ** 2; } }

Interface Segregation Principle

Clients should not be forced to depend on interfaces they do not use; split large interfaces into smaller, focused ones so that implementing classes only need to provide the methods they actually need.

<?php // ❌ BAD - Fat interface forces unnecessary implementations interface Worker { public function work(): void; public function eat(): void; public function sleep(): void; } class Robot implements Worker { public function work(): void { /* OK */ } public function eat(): void { /* Robots don't eat! */ } public function sleep(): void { /* Robots don't sleep! */ } } // ✅ GOOD - Segregated interfaces interface Workable { public function work(): void; } interface Eatable { public function eat(): void; } interface Sleepable { public function sleep(): void; } class Human implements Workable, Eatable, Sleepable { /*...*/ } class Robot implements Workable { public function work(): void { /*...*/ } }

Dependency Inversion Principle

High-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions—this inverts the traditional dependency direction and enables flexible, testable code.

<?php // ❌ BAD - High-level depends on low-level class UserController { private MySQLDatabase $database; // Concrete dependency public function __construct() { $this->database = new MySQLDatabase(); // Tight coupling } } // ✅ GOOD - Both depend on abstraction interface DatabaseInterface { public function query(string $sql): array; } class MySQLDatabase implements DatabaseInterface { /*...*/ } class PostgresDatabase implements DatabaseInterface { /*...*/ } class UserController { public function __construct(private DatabaseInterface $database) {} // Works with ANY database implementation } // Dependency injection - invert the control $db = new MySQLDatabase(); $controller = new UserController($db);
Traditional:                    Inverted:
┌────────────────┐             ┌────────────────┐
│  UserController│             │  UserController│
└───────┬────────┘             └───────┬────────┘
        │                              │
        ▼                              ▼
┌────────────────┐             ┌────────────────┐
│  MySQLDatabase │             │DatabaseInterface│ ◄── Abstraction
└────────────────┘             └───────┬────────┘
                                       │
                               ┌───────┴───────┐
                               ▼               ▼
                         ┌──────────┐   ┌──────────┐
                         │  MySQL   │   │ Postgres │
                         └──────────┘   └──────────┘

PHP Advanced Topics Guide


Dependency Injection

Constructor Injection

The most common and recommended DI pattern where dependencies are passed through the constructor, making them required and immutable. This ensures the object is always in a valid state.

class UserService { public function __construct( private UserRepository $repo, private Logger $logger ) {} } $service = new UserService(new UserRepository(), new Logger());

Setter Injection

Dependencies are provided via setter methods after object instantiation, allowing optional dependencies and changing them at runtime, though it risks leaving objects in incomplete states.

class ReportGenerator { private ?Formatter $formatter = null; public function setFormatter(Formatter $formatter): void { $this->formatter = $formatter; } }

Interface Injection

The dependency declares an interface that clients must implement to receive the injection. Less common in PHP but enforces a contract for injection.

interface DatabaseAwareInterface { public function setDatabase(Database $db): void; } class UserRepo implements DatabaseAwareInterface { public function setDatabase(Database $db): void { $this->db = $db; } }

DI Containers

A container that manages object creation, automatically resolving and injecting dependencies based on configuration or type hints. Popular implementations include PHP-DI, Pimple, and Symfony DI.

┌─────────────────────────────────────┐
│ DI Container │
├─────────────────────────────────────┤
│ Request: UserService │
│ ├── Resolve: UserRepository │
│ │ └── Resolve: PDO │
│ └── Resolve: Logger │
│ Return: Fully configured object │
└─────────────────────────────────────┘

Service Containers

An extension of DI containers that manages services (singleton or factory patterns), handles lazy loading, and provides service tagging/aliasing. Laravel's Container and Symfony's ServiceContainer are prime examples.

$container->singleton(CacheInterface::class, RedisCache::class); $container->bind('mailer', fn($c) => new Mailer($c->get('config'))); $mailer = $container->get('mailer');

Auto-wiring

Automatic dependency resolution based on type declarations—the container inspects constructor parameters and resolves dependencies without explicit configuration.

// Container automatically resolves dependencies via reflection class OrderController { public function __construct( OrderService $orders, // Auto-resolved PaymentGateway $payment // Auto-resolved ) {} } // No manual wiring needed! $controller = $container->get(OrderController::class);