Behavioral Design Patterns
Understand the eight most practically useful behavioral patterns -- Strategy, Observer, Command, State, Template Method, Chain of Responsibility, Iterator, and Specification -- well enough to recognize the runtime-behavior problem each one solves in real PHP web applications, implement each with working PHP 8 code, and judge when the pattern is worth its indirection versus when a plain conditional or a native language feature already does the job.
Where structural patterns are about how objects are wired together, behavioral patterns are about how objects communicate and share responsibility for a piece of logic at runtime. They answer questions like: how do I swap an algorithm without an if/else ladder? How do parts of the system react to something happening elsewhere without being tightly coupled to it? How do I treat 'do this later' as a first-class value I can queue, log, or retry? How does an object's behavior change as it moves through a lifecycle? How do I share a common algorithm's skeleton while letting subclasses fill in specific steps? How do I run a request through a pipeline of independent handlers? How do I walk a collection without exposing its internals? And how do I express 'does this thing match these criteria' as a reusable, combinable object instead of scattered conditionals? PHP has native language support for two of these (Iterator, and Generators as a lighter alternative) and idiomatic library support for several others, which is exactly why it's worth knowing when the classic OOP form is still the right tool and when PHP already gives you something simpler.
Strategy -- swapping an algorithm without branching
Strategy extracts an algorithm (or a business rule) into its own interface with one or more implementations, and has the context object hold a reference to whichever strategy applies, calling it polymorphically instead of branching on a type or flag internally. The classic PHP web-app case is pricing: a Cart shouldn't contain if ($customer->isPremium()) { ... } elseif ($isBulkOrder) { ... } -- that logic belongs in swappable PricingStrategy implementations the Cart simply delegates to.
It earns its place when the set of algorithms is genuinely open-ended (new pricing rules get added regularly) or when the same context needs to run under different rules in different situations (A/B tests, per-tenant behavior). It becomes overengineering when there are only ever two fixed cases that never change -- a single if/else inline is more direct than a two-class strategy hierarchy for something that will never grow a third case.
Observer -- reacting to events without tight coupling
Observer lets a subject notify a list of interested observers when something happens, without the subject needing to know what any observer actually does in response. In modern PHP this is almost always implemented as a lightweight event system (dispatch an event object to a list of registered listeners) rather than PHP's built-in SplSubject/SplObserver interfaces, which most teams find more ceremony than they're worth.
It earns its place when one action genuinely has multiple, independent side effects that shouldn't know about each other -- registering a user might need to send a welcome email, start a trial subscription, and log an analytics event, and none of those three should be hardcoded inside the registration method itself. It becomes overengineering when there's exactly one reaction to exactly one action -- at that point a direct method call is more traceable than an indirect event dispatch.
Command -- turning an action into an object
Command wraps a request or action as an object with a single method (commonly execute() or handle()), so the action can be passed around, queued, logged, retried, or undone like any other value, instead of being an immediate function call that's gone the moment it runs. This is the foundation of every background job queue: a SendInvoiceEmailCommand is serialized, pushed onto a queue, and executed later by a worker process, completely decoupled from the HTTP request that created it.
It earns its place whenever an action needs to outlive the current call stack -- queued, scheduled, retried on failure, or logged for audit/undo purposes. It becomes overengineering for something that only ever needs to run synchronously, once, right now -- wrapping every method call in a Command object 'for consistency' adds ceremony with no queuing, retry, or logging benefit actually being used.
State -- letting behavior change with lifecycle
State gives each stage of an object's lifecycle its own class implementing a shared interface, and has the object delegate behavior to whichever state it currently holds, so that 'what happens when I call ship()' is naturally different depending on whether the order is Paid or Cancelled -- without a wall of if ($this->status === 'paid') checks scattered across every method.
It earns its place when an object has several states with genuinely different, non-trivial allowed behavior and transitions (an order that can be paid, shipped, or cancelled, each permitting different next actions), especially when invalid transitions need to be actively rejected. It becomes overengineering for a simple boolean toggle (active/inactive) where an if-statement is completely sufficient and a full State class hierarchy would be pure ceremony.
Template Method -- one skeleton, many customized steps
Template Method defines the overall skeleton of an algorithm in a base class method, with certain steps deferred to abstract methods that subclasses must implement, and possibly other steps left as overridable 'hooks' with a sensible default. The base class controls the sequence; subclasses control the specifics of individual steps -- e.g. an abstract ReportGenerator whose generate() always fetches data, then formats it, then wraps it in a header/footer, but where fetchData() and format() differ completely between a CsvReportGenerator and a PdfReportGenerator.
It earns its place when several variants share a genuinely fixed sequence of steps and only the individual step implementations differ. It becomes overengineering when the 'shared skeleton' is actually just one or two lines -- inheritance for that little shared logic is heavier than just duplicating those two lines or extracting a shared helper function.
Chain of Responsibility -- a pipeline of independent handlers
Chain of Responsibility passes a request along a chain of handler objects, where each handler decides whether to act on the request, pass it to the next handler, or both, without the caller needing to know how many handlers exist or in what order. This is exactly how HTTP middleware pipelines work in every modern PHP framework: an AuthMiddleware, then a RateLimitMiddleware, then a LoggingMiddleware, each wrapping the next and the final route handler, each able to short-circuit the chain (e.g. return a 401 without ever reaching the route).
It earns its place when a request genuinely needs to pass through an ordered, composable set of independent concerns, especially when any one of them might need to stop the chain early. It becomes overengineering for a single fixed check that will never grow a second step -- wrapping one if in a handler chain of one adds a layer with nothing to chain.
Iterator -- walking a collection without exposing its internals
Iterator provides a standard way to step through a collection's elements one at a time without exposing whether that collection is a plain array, a linked structure, or something computed lazily on demand. PHP has first-class, built-in support for this: implementing the Iterator interface (current(), key(), next(), valid(), rewind()) makes an object work directly with foreach, and PHP's Generator functions (using yield) give you the same lazy, one-at-a-time iteration with dramatically less boilerplate for most cases.
It earns its place when a collection's underlying storage genuinely needs to stay hidden (it might fetch pages of results from a database or API lazily as you iterate, rather than loading everything into memory up front) or when you want foreach to work naturally over a custom object. It becomes overengineering for a collection that's just a plain in-memory PHP array -- arrays are already iterable with foreach for free, so wrapping one in a full Iterator implementation just to loop over it adds nothing.
Specification -- expressing 'does this match?' as a reusable object
Specification encapsulates a business rule or filter condition -- 'is this user active?', 'is this order eligible for free shipping?' -- as an object with an isSatisfiedBy(mixed $candidate): bool method, so the rule can be named, tested in isolation, reused across the codebase, and combined with other specifications using and/or/not composition instead of being duplicated as inline conditionals or scattered WHERE clauses.
It earns its place when the same business rule is checked in multiple places (filtering a list in memory, and separately validating a single object, and separately building a query) and when rules genuinely need to compose ("active AND (premium OR trial) AND NOT suspended"). It becomes overengineering for a one-off filter used exactly once -- array_filter($users, fn ($u) => $u->isActive()) is clearer than a whole ActiveUserSpecification class for a condition that appears nowhere else.
Choosing a behavioral pattern -- and knowing when a conditional is fine
Every pattern in this lesson exists to replace some form of scattered, duplicated, or tangled conditional logic with a named, reusable object -- a swappable algorithm, a decoupled reaction, a queueable action, a lifecycle-aware behavior, a shared skeleton, a composable pipeline stage, a hidden-internals walk, or a composable rule. That's real value when the logic is genuinely reused, genuinely varies, or genuinely needs to be treated as data (queued, composed, tested alone).
The overengineering trap is the same across all eight: introducing the pattern's ceremony (an interface, one or more classes, wiring) for something that is used exactly once, never varies, and was already perfectly readable as a plain if, a match, or a five-line function. PHP's own language features -- match expressions, first-class callables, arrow functions, and Generators -- now cover a lot of ground that used to require a full pattern implementation; reach for the classic OOP pattern when those lighter tools genuinely run out of room, not by default.
• Strategy: use for genuinely swappable, growing algorithms; skip for two cases that will never become three. • Observer: use when one action has several independent reactions; skip for a single 1-to-1 reaction (just call the method). • Command: use when an action must be queued, retried, logged, or run later; skip for something that only ever runs synchronously once. • State: use for a lifecycle with several non-trivial states and transition rules; skip for a plain boolean toggle. • Template Method: use when several variants share a real, fixed multi-step skeleton; skip when the shared part is one or two lines. • Chain of Responsibility: use for an ordered, composable pipeline of independent checks/handlers; skip for a single check. • Iterator: PHP arrays are already iterable for free; implement Iterator (or use a Generator) only when internals must stay hidden or iteration is lazy. • Specification: use when a rule is reused and composed across the codebase; skip for a one-off filter used exactly once.
Behavioral pattern skeletons
PHP's Generator functions (yield) satisfy the Traversable/Iterator contract implicitly and need none of the five explicit Iterator methods -- prefer them over a hand-written Iterator class whenever you don't need to rewind or expose extra collection methods. match expressions and first-class callable syntax (strategy(...)) often replace what used to require a full Strategy or Command class for very small, non-reused cases.
// Strategy
interface PricingStrategy { public function priceFor(Cart $cart): int; }
final class Cart {
public function __construct(private PricingStrategy $pricing) {}
public function total(): int { return $this->pricing->priceFor($this); }
}
// Observer (lightweight event dispatcher form)
final class EventDispatcher {
private array $listeners = [];
public function listen(string $event, callable $listener): void { $this->listeners[$event][] = $listener; }
public function dispatch(string $event, mixed $payload): void {
foreach ($this->listeners[$event] ?? [] as $listener) { $listener($payload); }
}
}
// Command
interface Command { public function execute(): void; }
final class SendInvoiceEmailCommand implements Command {
public function __construct(private string $invoiceId) {}
public function execute(): void { /* send the email */ }
}
// State
interface OrderState { public function ship(Order $order): void; }
final class PaidState implements OrderState {
public function ship(Order $order): void { $order->setState(new ShippedState()); }
}
// Template Method
abstract class ReportGenerator {
final public function generate(): string
…Code Examples
Cart never contains pricing logic itself -- it just calls priceFor() on whatever strategy it was given. Adding a new pricing rule (a holiday sale, a coupon code) means writing one new PricingStrategy class; Cart never changes. Expected result: 10000 8500 9500 public function total(): int { return $this->pricing->priceFor($this->subtotalCents); }
The registration code that calls dispatch() has no idea there are three listeners, or what any of them do -- it just announces 'a user registered'. Each listener (email, trial, analytics) can be added, removed, or fail independently without touching the registration logic or each other. Expected result: Sending welcome email to [email protected] Starting 14-day trial for u_42 Logging signup analytics event for u_42 foreach ($this->listeners[get_class($event)] ?? [] as $listener) { $listener($event); }
SendInvoiceEmailCommand and RecalculateReportCommand are both just data plus one execute() method -- they can be pushed onto a queue, serialized to storage, retried on failure, or logged, all without the code that created them (and needs to move on immediately) ever calling them directly. A real queue (Redis, database-backed) works the same way, just durable across process restarts. Expected result: Sending invoice INV-1001 to [email protected] Recalculating report #7 public function push(Command $command): void { $this->jobs[] = $command; }
Order itself contains no status checks at all -- every method just delegates to $this->state, and each state class decides for itself which transitions are legal, throwing for anything invalid. Adding a new state (e.g. Refunded) means writing one new class implementing OrderState, not touching every existing state's logic. Expected result: pending paid shipped public function ship(): void { $this->state->ship($this); } generate() is marked final -- the sequence (fetch, then format, wrapped in header/footer) is fixed and cannot be reordered by subclasses. Each subclass only supplies its own fetchData()/format(), and JsonReportGenerator additionally overrides the header()/footer() hooks to opt out of the default text wrapper, which is exactly the kind of customization Template Method's optional hooks are for. Expected result: === Report === 1,42 2,17 === End === [{"id":1,"total":42},{"id":2,"total":17}] final public function generate(): string { return $this->header(). $this->format($this->fetchData()). $this->footer(); }
Each middleware only knows about the next callable in the chain, not the whole pipeline -- AuthMiddleware can short-circuit with a 401 without ever calling $next, while LoggingMiddleware wraps around everything after it. Reordering or adding a middleware only means editing the array passed to buildChain(). Expected result: -> /orders Checking rate limit for /orders <- 200
$next = fn (Request $req) => $middleware->handle($req, $next);
Both versions let plain foreach walk a collection that's actually being fetched one API page at a time, never loading the whole result set into memory -- but the Iterator interface needs five explicit methods tracking position by hand, while the Generator version (yield from inside a normal function) gets the same lazy behavior almost for free. Reach for a hand-written Iterator class mainly when you need extra methods (count(), rewind-and-reuse) beyond plain one-pass iteration. PHP arrays already implement Traversable implicitly for foreach -- never wrap a plain in-memory array in a custom Iterator class just to loop over it; Iterator/Generator earn their keep specifically when the underlying data isn't already a simple array (lazy fetches, infinite sequences, hidden internal structure).
function paginatedUsers(ApiClient $client): Generator { while (...) { yield from $rows; } }
ActiveUserSpecification and PremiumUserSpecification are each testable and reusable on their own, and AndSpecification/NotSpecification let you build new combined rules ('active AND premium', 'active AND NOT premium') without writing a new conditional expression by hand each time -- the same specification objects could equally be reused to validate a single user or to build a database query's WHERE clause. Expected result: 1 1 array_filter($users, fn ($u) => $activePremium->isSatisfiedBy($u));
interface PricingStrategy
{
public function priceFor(int $subtotalCents): int;
}
final class RegularPricing implements PricingStrategy
{
public function priceFor(int $subtotalCents): int { return $subtotalCents; }
}
final class BulkDiscountPricing implements PricingStrategy
{
public function __construct(private float $discountRate) {}
public function priceFor(int $subtotalCents): int
{
return (int) round($subtotalCents * (1 - $this->discountRate));
}
}
final class PremiumMemberPricing implements PricingStrategy
{
public function priceFor(int $subtotalCents): int
{
return max(0, $subtotalCents - 500); // flat $5 off
}
}
final class Cart
{
public function __construct(private PricingStrategy $pricing, private int $subtotalCents) {}
public function total(): int
{
return $this->pricing->priceFor($this->subtotalCents);
}
}
$regular = new Cart(new RegularPricing(), 10000);
$bulk = new Cart(new BulkDiscountPricing(0.15), 10000);
$premium = new Cart(new PremiumMemberPricing(), 10000);
echo $regular->total() . "\n" . $bulk->total() . "\n" . $premium->total();final class UserRegistered
{
public function __construct(public readonly string $userId, public readonly string $email) {}
}
final class EventDispatcher
{
/** @var array<string, callable[]> */
private array $listeners = [];
public function listen(string $eventClass, callable $listener): void
{
$this->listeners[$eventClass][] = $listener;
}
public function dispatch(object $event): void
{
foreach ($this->listeners[get_class($event)] ?? [] as $listener) {
$listener($event);
}
}
}
$dispatcher = new EventDispatcher();
$dispatcher->listen(UserRegistered::class, function (UserRegistered $e) {
echo "Sending welcome email to {$e->email}\n";
});
$dispatcher->listen(UserRegistered::class, function (UserRegistered $e) {
echo "Starting 14-day trial for {$e->userId}\n";
});
$dispatcher->listen(UserRegistered::class, function (UserRegistered $e) {
echo "Logging signup analytics event for {$e->userId}\n";
});
// Deep inside registration logic:
$dispatcher->dispatch(new UserRegistered('u_42', '[email protected]'));interface Command
{
public function execute(): void;
}
final class SendInvoiceEmailCommand implements Command
{
public function __construct(private string $invoiceId, private string $toEmail) {}
public function execute(): void
{
echo "Sending invoice {$this->invoiceId} to {$this->toEmail}\n";
}
}
final class RecalculateReportCommand implements Command
{
public function __construct(private int $reportId) {}
public function execute(): void
{
echo "Recalculating report #{$this->reportId}\n";
}
}
final class InMemoryQueue
{
/** @var Command[] */
private array $jobs = [];
public function push(Command $command): void
{
$this->jobs[] = $command;
}
public function workOff(): void
{
while ($command = array_shift($this->jobs)) {
$command->execute();
}
}
}
$queue = new InMemoryQueue();
$queue->push(new SendInvoiceEmailCommand('INV-1001', '[email protected]'));
$queue->push(new RecalculateReportCommand(reportId: 7));
// ... later, in a separate worker process ...
$queue->workOff();interface OrderState
{
public function pay(Order $order): void;
public function ship(Order $order): void;
public function cancel(Order $order): void;
public function name(): string;
}
final class Order
{
private OrderState $state;
public function __construct()
{
$this->state = new PendingState();
}
public function setState(OrderState $state): void { $this->state = $state; }
public function pay(): void { $this->state->pay($this); }
public function ship(): void { $this->state->ship($this); }
public function cancel(): void { $this->state->cancel($this); }
public function stateName(): string { return $this->state->name(); }
}
final class PendingState implements OrderState
{
public function pay(Order $order): void { $order->setState(new PaidState()); }
public function ship(Order $order): void { throw new LogicException('Cannot ship an unpaid order.'); }
public function cancel(Order $order): void { $order->setState(new CancelledState()); }
public function name(): string { return 'pending'; }
}
final class PaidState implements OrderState
{
public function pay(Order $order): void { throw new LogicException
…abstract class ReportGenerator
{
final public function generate(): string
{
$data = $this->fetchData();
return $this->header() . $this->format($data) . $this->footer();
}
abstract protected function fetchData(): array;
abstract protected function format(array $data): string;
protected function header(): string { return "=== Report ===\n"; }
protected function footer(): string { return "=== End ===\n"; }
}
final class CsvReportGenerator extends ReportGenerator
{
protected function fetchData(): array
{
return [['id' => 1, 'total' => 42], ['id' => 2, 'total' => 17]];
}
protected function format(array $data): string
{
$lines = array_map(fn ($row) => "{$row['id']},{$row['total']}", $data);
return implode("\n", $lines) . "\n";
}
}
final class JsonReportGenerator extends ReportGenerator
{
protected function fetchData(): array
{
return [['id' => 1, 'total' => 42], ['id' => 2, 'total' => 17]];
}
protected function format(array $data): string
{
return json_encode($data) . "\n";
}
protected function header(): string { return ''; } // JSON output skips
…final class Request
{
public function __construct(public array $headers, public string $path) {}
}
final class Response
{
public function __construct(public int $status, public string $body) {}
}
interface Middleware
{
public function handle(Request $request, callable $next): Response;
}
final class AuthMiddleware implements Middleware
{
public function handle(Request $request, callable $next): Response
{
if (($request->headers['Authorization'] ?? null) === null) {
return new Response(401, 'Unauthorized');
}
return $next($request);
}
}
final class RateLimitMiddleware implements Middleware
{
public function handle(Request $request, callable $next): Response
{
echo "Checking rate limit for {$request->path}\n";
return $next($request); // assume within limit
}
}
final class LoggingMiddleware implements Middleware
{
public function handle(Request $request, callable $next): Response
{
echo "-> {$request->path}\n";
$response = $next($request);
echo "<- {$response->status}\n";
return $response;
}
}
function buildChain(array $middlewares, callable $finalHa
…final class PaginatedUsers implements Iterator
{
private int $position = 0;
private array $currentPage = [];
private int $pageNumber = 1;
public function __construct(private ApiClient $client) {}
public function rewind(): void
{
$this->pageNumber = 1;
$this->currentPage = $this->client->fetchPage($this->pageNumber);
$this->position = 0;
}
public function current(): mixed { return $this->currentPage[$this->position]; }
public function key(): mixed { return $this->position; }
public function next(): void
{
$this->position++;
if (!isset($this->currentPage[$this->position]) && $this->currentPage !== []) {
$this->pageNumber++;
$this->currentPage = $this->client->fetchPage($this->pageNumber);
$this->position = 0;
}
}
public function valid(): bool { return isset($this->currentPage[$this->position]); }
}
// The same lazy, page-by-page walk written with a Generator instead --
// no explicit position/current/next bookkeeping required.
function paginatedUsers(ApiClient $client): Generator
{
$page = 1;
while (($rows = $client->fetchPage($page)) !
…interface Specification
{
public function isSatisfiedBy(mixed $candidate): bool;
}
final class ActiveUserSpecification implements Specification
{
public function isSatisfiedBy(mixed $candidate): bool
{
return $candidate->isActive === true;
}
}
final class PremiumUserSpecification implements Specification
{
public function isSatisfiedBy(mixed $candidate): bool
{
return $candidate->plan === 'premium';
}
}
final class AndSpecification implements Specification
{
public function __construct(private Specification $left, private Specification $right) {}
public function isSatisfiedBy(mixed $candidate): bool
{
return $this->left->isSatisfiedBy($candidate) && $this->right->isSatisfiedBy($candidate);
}
}
final class NotSpecification implements Specification
{
public function __construct(private Specification $inner) {}
public function isSatisfiedBy(mixed $candidate): bool
{
return !$this->inner->isSatisfiedBy($candidate);
}
}
final class User
{
public function __construct(public bool $isActive, public string $plan) {}
}
$activePremium = new AndSpecification(new ActiveUserSpecification(), new
…Where each behavioral pattern intervenes
Each pattern relocates one specific piece of runtime decision-making -- which algorithm, who reacts, when it runs, what state allows, which step varies, who handles it, how it's walked, or whether it matches -- into its own object.
Strategy: context --> [ chosen algorithm object ] --> result
Observer: subject --> notifies --> [listener 1] [listener 2] [listener 3]
Command: caller --> wraps action as object --> [queue] --> execute() later
State: object.method() --> delegates to --> [current state object]
Template Method: skeleton() --> calls --> [fixed step] [ABSTRACT step] [hook]
Chain of Resp: request --> [handler A] -> next -> [handler B] -> next -> [final]
Iterator: foreach --> current()/next()/valid() --> hides internal storage
Specification: candidate --> isSatisfiedBy() --> [rule object, composable]Important Rules
Strategy replaces branching on 'which algorithm' with polymorphism -- the context calls one method and doesn't know which implementation it got.
Observer/event dispatch decouples an action from its reactions; keep listeners independent of each other and of the code that dispatches.
Command turns an action into a value that can be queued, retried, or logged -- use it exactly when that capability is actually needed.
State delegates behavior to the current lifecycle stage's own object, so invalid transitions can be rejected right where the rule belongs.
Template Method fixes the algorithm's sequence in a (often final) base method and defers only individual steps to subclasses or hooks.
Chain of Responsibility lets each handler decide independently to act, pass on, or both -- exactly how HTTP middleware pipelines work.
PHP arrays are already iterable for free; use Iterator/Generator when internals must stay hidden or iteration must be lazy, not for plain arrays.
Specification is worth it when a rule is reused and composed (and/or/not) across the codebase, not for a filter used in exactly one place.
Where these show up in real PHP frameworks
Laravel's and Symfony's event dispatchers are Observer as a first-class framework feature (Event::listen(), Symfony's EventDispatcher). Laravel Queue jobs and Symfony Messenger messages are Command, serialized and executed by workers. Symfony's and Laravel's HTTP middleware pipelines (Illuminate\Pipeline, Symfony's HttpKernel event chain) are Chain of Responsibility. Doctrine's and Laravel's query builders often accept Specification-like criteria objects for reusable, composable filtering. State machines for order/subscription lifecycles are common in e-commerce codebases, frequently backed by a small library rather than hand-rolled classes once the transition rules get complex enough. And PHP's own Generator (used throughout Doctrine's hydration and Symfony's finder component) is the language's built-in, idiomatic answer to Iterator for anything that shouldn't be loaded fully into memory.
Writing PricingStrategy interface plus two classes for a rule that has exactly two cases and will never grow a third.
Use a simple if/else (or match expression) inline in Cart until a genuine third case or genuine reuse elsewhere appears.
Strategy's value is in swapping/growing algorithms -- a permanently-fixed two-way branch doesn't need polymorphism to express it.
Implementing PHP's Iterator interface (five methods) on a class that just wraps a plain in-memory array.
Implement IteratorAggregate returning an ArrayIterator, or simply expose the array and let callers foreach over it directly.
PHP arrays are already fully iterable -- reimplementing that from scratch for no lazy-loading or hidden-internals reason is pure boilerplate.
Building a Specification class hierarchy for a single array_filter() callback used in exactly one controller action.
Write the condition as an inline arrow function passed to array_filter() until the same rule is genuinely needed in a second place.
Specification earns its cost through reuse and composition across the codebase -- a one-off filter doesn't need a dedicated, testable, named class.
What is the core difference between Strategy and State, given that both let an object delegate behavior to another object it holds a reference to? A. Strategy's delegate is chosen once by the caller and represents an interchangeable algorithm; State's delegate represents the object's own current lifecycle stage and can change itself as a side effect of the object's behavior. B. State can only have two possible delegate classes; Strategy can have unlimited delegate classes. C. Strategy is only usable with interfaces; State is only usable with abstract classes. D. They are the same pattern -- 'State' is just an outdated name for Strategy.
A. Strategy's delegate is chosen once by the caller and represents an interchangeable algorithm; State's delegate represents the object's own current lifecycle stage and can change itself as a side effect of the object's behavior. — Strategy is about swapping *which algorithm* runs, typically chosen externally and held fixed; State is about an object's behavior changing as *it itself* transitions between lifecycle stages, often triggered by its own methods (e.g. ship() causes a state change).
Why is Command a good fit for a background job like sending an invoice email, but likely overkill for validating a login form field synchronously? A. Command's value is treating an action as a value that can be queued, retried, or logged for later execution -- a synchronous, immediate, one-shot validation never needs any of that, so a plain function call is simpler. B. PHP does not allow objects to be passed into functions, so Command cannot be used for validation. C. Command objects cannot contain any data, only behavior. D. Validation logic is not allowed to use classes in PHP.
A. Command's value is treating an action as a value that can be queued, retried, or logged for later execution -- a synchronous, immediate, one-shot validation never needs any of that, so a plain function call is simpler. — The whole reason to wrap an action as a Command object is to defer, queue, retry, or log it independently of the calling code's lifetime -- an immediate synchronous check gets none of that benefit and is better left as a direct call.
In a Chain of Responsibility middleware pipeline, how does one handler stop the rest of the chain from running (e.g. rejecting an unauthenticated request)? A. It simply returns its own response without calling the $next callable/handler. B. It throws a fatal PHP error, which is caught by the framework. C. It sets a global variable that every other handler checks. D. PHP automatically stops a chain after the first handler unless told otherwise.
A. It simply returns its own response without calling the $next callable/handler. — Each handler decides for itself whether to call $next -- skipping that call (as AuthMiddleware does by returning a 401 directly) short-circuits everything further down the chain without any special language feature.
Why would implementing PHP's full Iterator interface (current/key/next/valid/rewind) on a class that just wraps a plain array usually be considered overengineering? A. Plain PHP arrays are already iterable with foreach for free, so hand-writing those five methods reproduces built-in behavior without hiding any real internal complexity or adding any laziness. B. The Iterator interface has been removed from PHP 8 and no longer exists. C. foreach cannot be used on any object, only on arrays. D. Iterator implementations are limited to exactly one method in PHP.
A. Plain PHP arrays are already iterable with foreach for free, so hand-writing those five methods reproduces built-in behavior without hiding any real internal complexity or adding any laziness. — Iterator (or a Generator) earns its cost when the underlying data source isn't already a simple in-memory array -- lazy fetching, hidden structure, or infinite sequences. Wrapping a plain array in it is pure unnecessary ceremony.
What makes Specification objects more reusable than an inline array_filter() callback for the same condition? A. They can be named, unit-tested independently, and composed with AndSpecification/OrSpecification/NotSpecification to build new rules without duplicating the underlying condition's logic. B. Specification objects run measurably faster than closures in every case. C. array_filter() cannot accept closures as its callback argument. D. Specification objects are required by PHP's type system for any boolean-returning check.
A. They can be named, unit-tested independently, and composed with AndSpecification/OrSpecification/NotSpecification to build new rules without duplicating the underlying condition's logic. — The advantage is entirely about reuse and composability -- a named Specification class can be combined with others and reused verbatim across filtering, validation, and query-building contexts, whereas an inline closure lives and dies at its one call site.
A ReportGenerator's generate() method is declared final in the Template Method example. Why? A. To guarantee the overall step sequence (fetch, then format, wrapped in header/footer) can never be reordered or bypassed by a subclass -- only the individual steps (fetchData, format, and the optional hooks) are meant to vary. B. PHP requires all abstract class methods to be final. C. final methods run faster than non-final methods in PHP's engine. D. It prevents the class from being instantiated directly.
A. To guarantee the overall step sequence (fetch, then format, wrapped in header/footer) can never be reordered or bypassed by a subclass -- only the individual steps (fetchData, format, and the optional hooks) are meant to vary. — Template Method's whole point is that the skeleton stays fixed while specific steps vary -- marking the orchestrating method final enforces that subclasses can only customize the designated steps/hooks, not the overall algorithm shape.
Design a DiscountSpecification hierarchy (isSatisfiedBy(Order $order): bool) with LoyalCustomerSpecification and MinimumSpendSpecification, combined with an AndSpecification, and use it inside a DiscountStrategy (Strategy pattern) that returns 10% off only when the combined specification is satisfied, 0% otherwise. Wire it so a Cart holds a DiscountStrategy and calls it to compute its final total.
final class SpecificationDiscountStrategy implements PricingStrategy { public function __construct(private Specification $eligibility, private float $rate) {} public function priceFor(Order $order, int $subtotalCents): int { if (!$this->eligibility->isSatisfiedBy($order)) { return $subtotalCents; } return (int) round($subtotalCents * (1 - $this->rate)); } } $eligible = new AndSpecification(new LoyalCustomerSpecification(), new MinimumSpendSpecification(5000)); $strategy = new SpecificationDiscountStrategy($eligible, 0.10);
DiscountStrategy's applyDiscount(Order $order, int $subtotalCents) should call $this->specification->isSatisfiedBy($order) first, and only apply the percentage reduction when it returns true -- otherwise return $subtotalCents unchanged.