Structural Design Patterns
Understand the five classic structural patterns -- Adapter, Decorator, Facade, Proxy, and Composite -- well enough to recognize when object composition (rather than inheritance) is the right tool for reshaping, extending, simplifying, guarding, or unifying access to objects in real PHP web-application code, and to spot when the added indirection is not worth it.
Structural patterns answer a different question than creational ones: not 'how does this object get built?' but 'how do existing objects fit together?'. They're about composing objects and interfaces so that mismatched, oversized, sensitive, or expensive things can be worked with cleanly -- wrapping an incompatible third-party client so your app never sees its ugly API, layering optional behavior onto a response without subclassing every combination, hiding a tangle of subsystems behind one simple call, delaying expensive work until it's truly needed, and treating a single item and a group of items the same way. Every one of these patterns trades a small amount of extra indirection for a real reduction in coupling or complexity elsewhere -- and every one of them can be misapplied to something that was already simple.
Adapter -- making an incompatible interface fit
An Adapter wraps an object with one interface and exposes a different interface that your code actually expects, translating calls (and often data shapes) between the two. The classic real-world trigger is a third-party SDK or API client whose method names, parameter order, or data format don't match the interface your application's domain code was designed around -- and which you don't control, so you can't just change it.
The payoff is that your application code depends only on its own clean interface, never on the third party's quirks, so swapping providers later means writing one new adapter, not rewriting every call site. It becomes overengineering when there is genuinely only one implementation ever, the interfaces already match closely, and the 'adapter' would just forward every call through unchanged -- at that point it's a no-op wrapper.
Decorator -- adding behavior by wrapping, not subclassing
A Decorator implements the same interface as the object it wraps, forwards calls to that wrapped object, and adds behavior before, after, or around the forwarded call. Because a decorator implements the same interface it decorates, decorators stack: you can wrap a decorator in another decorator, composing behaviors at runtime instead of pre-declaring every combination as a subclass.
This solves the 'subclass explosion' problem directly -- if a response can optionally be cached, compressed, and logged, subclassing would need CachedResponse, CompressedResponse, LoggedResponse, CachedCompressedResponse, and so on. Decorators let you compose exactly the combination each call site needs. It becomes overengineering when there are only one or two fixed combinations that never change -- at that point a single class with an if-branch is more readable than a decorator stack.
Facade -- one simple entry point over a complex subsystem
A Facade provides a small, purpose-built interface in front of a larger set of subsystem classes, hiding their coordination logic behind one or two high-level method calls. The subsystem classes still exist and still have their own full interfaces for code that genuinely needs fine-grained control -- the facade is an additional, simpler door, not a replacement for the building.
It earns its place when a common task genuinely requires coordinating several subsystems in a specific order with specific error handling, and most callers only need that common task done, not the underlying flexibility. It becomes overengineering when it's placed in front of a single class -- a facade with one method that calls one other method's one method adds a layer without hiding any real complexity.
Proxy -- controlling or deferring access to another object
A Proxy implements the same interface as a 'real' object it stands in for, and controls access to it -- most commonly by deferring expensive construction until the object is actually used (a *virtual proxy*, for lazy loading), but also by checking permissions before delegating (a *protection proxy*) or adding remote-call plumbing (a *remote proxy*). Callers hold a reference to the proxy and are none the wiser that the real object isn't there yet.
It earns its place when constructing the real object is measurably expensive (a large file, a slow query, a remote call) and it's common for that cost to go unpaid because the object ends up unused on some code paths. It becomes overengineering when the wrapped object is cheap to construct anyway -- at that point the proxy adds indirection to save microseconds that were never a problem.
Composite -- treating a single item and a group uniformly
A Composite defines one common interface shared by both individual ('leaf') objects and containers of them ('composite' objects that hold children of the same interface), so client code can call the same method on a single item or an entire tree without checking which kind it has. This is the natural fit for anything tree-shaped: a navigation menu with links and submenus, a permission set with individual rights and grouped roles, a filesystem with files and directories.
The payoff is that operations like render(), totalCost(), or isAllowed() recurse naturally: a composite's implementation just calls the same method on each child and combines the results, with zero type-checking. It becomes overengineering when the structure is never actually nested -- a flat list of items dressed up as a one-level-deep 'tree' gains nothing from Composite over a plain array and a foreach.
Choosing a structural pattern -- and knowing when it's just indirection
All five patterns reshape how objects relate to each other without changing what any single object fundamentally does. The shared risk across all of them is the same: composition itself is not free. Every wrapper is one more object allocation, one more layer to step through in a debugger, one more hop for a reader to trace. The patterns pay for that cost by removing a real problem -- an interface mismatch, a combinatorial explosion of subclasses, a tangled subsystem, an unpaid-for expensive object, or type-checking scattered through client code.
A practical test before adding any of these: can you point at the specific mismatch, explosion, tangle, cost, or type-check the pattern removes? If the honest answer is 'not really, but it felt like the professional thing to do,' the plain, unwrapped version was the better choice.
• Adapter: use for a genuine interface mismatch with an external/third-party type you don't control; skip when interfaces already match. • Decorator: use when behaviors combine independently and would otherwise multiply subclasses; skip for one or two fixed, unchanging combinations. • Facade: use to give most callers a simple door onto a real multi-class subsystem; skip in front of a single class. • Proxy: use when the wrapped object is genuinely expensive and sometimes goes unused; skip when construction is already cheap. • Composite: use for genuinely recursive/tree-shaped data; skip for structures that are actually flat.
Structural pattern skeletons
Both Adapter and Decorator implement an existing interface and hold an inner object -- the difference is intent: Adapter *translates* a mismatched interface, Decorator *adds* to a matching one. A Proxy and the real object it stands in for share the SAME interface, so callers can't tell them apart -- that's what makes the laziness or access-control invisible to client code.
// Adapter
interface WeatherProvider { public function forecastFor(string $city): array; }
final class ThirdPartyWeatherAdapter implements WeatherProvider {
public function __construct(private ThirdPartyWeatherClient $client) {}
public function forecastFor(string $city): array {
$raw = $this->client->getData($city); // third-party shape
return ['tempC' => $raw['temp_c']]; // translated to our shape
}
}
// Decorator
interface ResponseInterface { public function getBody(): string; public function getHeaders(): array; }
abstract class ResponseDecorator implements ResponseInterface {
public function __construct(protected ResponseInterface $inner) {}
public function getBody(): string { return $this->inner->getBody(); }
public function getHeaders(): array { return $this->inner->getHeaders(); }
}
// Facade
final class OrderFacade {
public function __construct(
private InventoryService $inventory,
private PaymentService $payment,
private ShippingService $shipping,
) {}
public function placeOrder(Cart $cart): Order { /* coordinates all three */ }
}
// Proxy
interface Image { public function render(): stri
…Code Examples
printForecast() and WeatherProvider never see getData(), temp_c, or the third party's lat/lon-as-separate-floats signature -- the adapter absorbs all of that translation. Swapping to a different weather API later means writing one new adapter class, with zero changes to printForecast() or Forecast. Expected result: Austin: 21.5C, clear
$raw = $this->client->getData($city->lat, $city->lon); return new Forecast(temperatureC: $raw['temp_c'], ...);
Each decorator still implements ResponseInterface, so they nest freely: JsonResponse is wrapped in gzip, which is wrapped in cache headers, and the final $response can be passed anywhere a ResponseInterface is expected. Adding a LoggingResponseDecorator later needs no changes to the classes already written. Expected result: Array ( [Content-Type] => application/json [Content-Encoding] => gzip [Cache-Control] => max-age=3600) new CacheHeaderDecorator(new GzipResponseDecorator(new JsonResponse([...])), maxAgeSeconds: 3600)
A checkout controller calls one method, placeOrder(), instead of knowing the exact order to call four different subsystems in, or duplicating that coordination logic at every place an order can be placed (web checkout, admin panel, API). The subsystem classes are still fully usable directly for anything that needs finer control. Expected result: Reserved 2x SKU-42 Charged cust_1 4999c Shipping SKU-42 to 1 Main St Notified cust_1 of ch_123 public function placeOrder(...): void { $this->inventory->reserve(...);...; $this->notifications->orderConfirmed(...); }
Two LazyReportProxy objects are created up front and cost almost nothing -- the expensive RealReport, and its slow construction work, is deferred until render() is actually called, and only report #1 ever pays that cost in this example because report #2's proxy is never used. Expected result: Proxies created, nothing built yet. Building expensive report 1... <html>report #1</html> return ($this->real ??= new RealReport($this->reportId))->render();
render() is called on the root MenuGroup, but MenuLink and MenuGroup both implement MenuComponent, so the exact same method call recurses through the whole tree without $root ever needing to check 'is this child a leaf or another group?' -- that uniformity is Composite's entire value. Expected result: + Dashboard - Home + Settings - Profile - Billing foreach ($this->children as $child) { $out.= $child->render($depth + 1); }
// Third-party SDK -- we don't control this shape.
final class ThirdPartyWeatherClient
{
public function getData(float $lat, float $lon): array
{
return ['temp_c' => 21.5, 'conditions' => 'clear', 'wind_kph' => 12.0];
}
}
// Our application's own clean interface.
interface WeatherProvider
{
public function forecastFor(City $city): Forecast;
}
final class Forecast
{
public function __construct(public float $temperatureC, public string $summary) {}
}
final class City
{
public function __construct(public string $name, public float $lat, public float $lon) {}
}
final class ThirdPartyWeatherAdapter implements WeatherProvider
{
public function __construct(private ThirdPartyWeatherClient $client) {}
public function forecastFor(City $city): Forecast
{
$raw = $this->client->getData($city->lat, $city->lon);
return new Forecast(
temperatureC: $raw['temp_c'],
summary: $raw['conditions'],
);
}
}
function printForecast(WeatherProvider $provider, City $city): void
{
$forecast = $provider->forecastFor($city);
echo "{$city->name}: {$forecast->temperatureC}C, {$forecast->summary}\n";
}
pri
…interface ResponseInterface
{
public function getBody(): string;
public function getHeaders(): array;
}
final class JsonResponse implements ResponseInterface
{
public function __construct(private array $data) {}
public function getBody(): string { return json_encode($this->data); }
public function getHeaders(): array { return ['Content-Type' => 'application/json']; }
}
abstract class ResponseDecorator implements ResponseInterface
{
public function __construct(protected ResponseInterface $inner) {}
public function getBody(): string { return $this->inner->getBody(); }
public function getHeaders(): array { return $this->inner->getHeaders(); }
}
final class GzipResponseDecorator extends ResponseDecorator
{
public function getBody(): string
{
return gzencode(parent::getBody());
}
public function getHeaders(): array
{
return parent::getHeaders() + ['Content-Encoding' => 'gzip'];
}
}
final class CacheHeaderDecorator extends ResponseDecorator
{
public function __construct(ResponseInterface $inner, private int $maxAgeSeconds) {
parent::__construct($inner);
}
public function getHeaders(): array
…final class InventoryService
{
public function reserve(string $sku, int $qty): void { echo "Reserved {$qty}x {$sku}\n"; }
}
final class PaymentService
{
public function charge(string $customerId, int $cents): string { echo "Charged {$customerId} {$cents}c\n"; return 'ch_123'; }
}
final class ShippingService
{
public function schedule(string $sku, string $address): void { echo "Shipping {$sku} to {$address}\n"; }
}
final class NotificationService
{
public function orderConfirmed(string $customerId, string $chargeId): void { echo "Notified {$customerId} of {$chargeId}\n"; }
}
final class OrderFacade
{
public function __construct(
private InventoryService $inventory,
private PaymentService $payment,
private ShippingService $shipping,
private NotificationService $notifications,
) {}
public function placeOrder(string $customerId, string $sku, int $qty, int $cents, string $address): void
{
$this->inventory->reserve($sku, $qty);
$chargeId = $this->payment->charge($customerId, $cents);
$this->shipping->schedule($sku, $address);
$this->notifications->orderConfirmed($customerId, $chargeId);
…interface Report
{
public function render(): string;
}
final class RealReport implements Report
{
private string $renderedHtml;
public function __construct(private int $reportId)
{
// Imagine this runs a slow aggregation query.
echo "Building expensive report {$reportId}...\n";
$this->renderedHtml = "<html>report #{$reportId}</html>";
}
public function render(): string
{
return $this->renderedHtml;
}
}
final class LazyReportProxy implements Report
{
private ?RealReport $real = null;
public function __construct(private int $reportId) {}
public function render(): string
{
return ($this->real ??= new RealReport($this->reportId))->render();
}
}
$reports = [new LazyReportProxy(1), new LazyReportProxy(2)];
echo "Proxies created, nothing built yet.\n";
echo $reports[0]->render() . "\n"; // only report #1 is actually built hereinterface MenuComponent
{
public function render(int $depth = 0): string;
public function isVisibleTo(string $role): bool;
}
final class MenuLink implements MenuComponent
{
public function __construct(
private string $label,
private string $requiredRole = 'guest',
) {}
public function render(int $depth = 0): string
{
return str_repeat(' ', $depth) . "- {$this->label}\n";
}
public function isVisibleTo(string $role): bool
{
return $role === 'admin' || $role === $this->requiredRole;
}
}
final class MenuGroup implements MenuComponent
{
/** @var MenuComponent[] */
private array $children = [];
public function __construct(private string $label) {}
public function add(MenuComponent $child): void
{
$this->children[] = $child;
}
public function render(int $depth = 0): string
{
$out = str_repeat(' ', $depth) . "+ {$this->label}\n";
foreach ($this->children as $child) {
$out .= $child->render($depth + 1);
}
return $out;
}
public function isVisibleTo(string $role): bool
{
foreach ($this->children as $child) {
…How structural patterns compose objects
Adapter changes the interface; Decorator and Proxy keep it identical but change what happens behind it; Facade collapses many interfaces into one; Composite makes one and many indistinguishable.
client code
|
v
[ Adapter ] wraps a MISMATCHED interface --> presents YOUR interface
|
[ Decorator ] wraps a MATCHING interface --> adds behavior, still same interface
|
[ Facade ] wraps MANY subsystem objects --> presents ONE simple method
|
[ Proxy ] wraps the SAME interface --> defers/guards access to the real object
|
[ Composite ] leaf and group SHARE one interface --> client can't tell them apartImportant Rules
Adapter translates a mismatched (usually third-party) interface into the one your application actually wants to depend on.
Decorator and the object it wraps implement the SAME interface, so decorators can stack arbitrarily deep at runtime.
Facade simplifies the common case without removing the subsystem classes' own full interfaces for callers that need them.
A Proxy and its real object share the same interface -- that's what makes lazy loading or access control invisible to callers.
Composite requires leaf and container types to implement one shared interface, so recursive operations need no type-checking.
Where these show up in real PHP frameworks
PSR-7 HTTP message decorators (middleware that wraps a ResponseInterface to add headers, compress, or buffer output) are Decorator in its purest modern PHP form. Laravel's facades (Route::get(...), Cache::remember(...)) are literally named after this pattern, though they resolve from the container behind static-looking syntax. Doctrine's proxy objects for lazy-loaded entity associations are a textbook virtual Proxy -- $order->getCustomer() returns a proxy that only queries the database the moment a real property on it is accessed. Symfony's and Guzzle's HTTP client wrappers around cURL are Adapters. And Composite is how most PHP menu-builder and permission-tree packages represent nested navigation or role hierarchies.
Writing an Adapter that just forwards every call unchanged because the wrapped interface already matches yours.
Skip the adapter and depend on the third-party type directly (or its own interface, if it exposes one) until a real mismatch appears.
A pass-through adapter is pure indirection -- it isn't translating anything, so it isn't earning its cost.
Subclassing every combination of optional response behavior: CachedGzipResponse, LoggedCachedResponse, LoggedCachedGzipResponse...
Use stackable Decorators so each behavior is written once and combined at the call site as needed.
Subclass combinations grow multiplicatively with each new behavior; decorators grow additively.
Building a Facade in front of a single service class with a single method.
Call that one class's one method directly -- there is no coordination complexity to hide.
A facade only pays for itself when it's hiding real multi-object coordination, not renaming one call.
What is the key structural difference between Adapter and Decorator, given that both wrap an inner object? A. Adapter exposes a DIFFERENT interface than the object it wraps; Decorator exposes the SAME interface as the object it wraps. B. Adapter can only wrap final classes; Decorator cannot. C. Decorator is always abstract; Adapter is always final. D. There is no real difference -- they are interchangeable names for the same pattern.
A. Adapter exposes a DIFFERENT interface than the object it wraps; Decorator exposes the SAME interface as the object it wraps. — Adapter's job is translating between two different interfaces; Decorator's job is adding behavior while staying in the same interface as what it wraps, which is exactly what lets decorators stack.
Why can Decorators be stacked (e.g. wrapping a GzipResponseDecorator in a CacheHeaderDecorator) but a typical Adapter usually isn't stacked the same way? A. Because each Decorator still implements the same interface it decorates, so any decorator can itself be wrapped by another; an Adapter changes the interface, so chaining adapters would require each one to accept the previous adapter's already-different interface. B. PHP forbids nesting Adapter instances at the language level. C. Decorators are always abstract classes and Adapters are always interfaces, which is a hard PHP rule. D. Stacking has nothing to do with interfaces; it's just a naming convention.
A. Because each Decorator still implements the same interface it decorates, so any decorator can itself be wrapped by another; an Adapter changes the interface, so chaining adapters would require each one to accept the previous adapter's already-different interface. — Because a Decorator's output type matches its input type, decorators compose freely. An Adapter's whole purpose is bridging two different interfaces, so it naturally sits at the boundary rather than in a repeatable chain.
A Proxy and the real object it stands in for must: A. Implement the same interface, so client code cannot tell them apart. B. Live in the same file for performance reasons. C. Never both exist at the same time in memory. D. Share the same constructor signature as each other.
A. Implement the same interface, so client code cannot tell them apart. — A Proxy is only useful because it's a drop-in stand-in: callers hold a reference typed to the shared interface and have no idea whether they're talking to the real object or a proxy deferring/guarding access to it.
Why is Composite a good fit for a permission tree with individual permissions and grouped roles, but a poor fit for a genuinely flat list of ten unrelated settings? A. Composite's value comes from letting recursive, tree-shaped operations run without type-checking leaf vs. container -- a flat list has no nesting to recurse over, so a plain array and a loop already do the job. B. PHP's array functions cannot iterate over more than ten items. C. Composite requires exactly two levels of nesting, never more or fewer. D. Flat lists cannot implement interfaces in PHP.
A. Composite's value comes from letting recursive, tree-shaped operations run without type-checking leaf vs. container -- a flat list has no nesting to recurse over, so a plain array and a loop already do the job. — Composite pays off specifically when structure is recursive/tree-shaped; applied to something that is actually flat, it just adds an unused container abstraction over what a simple array already handled.
A Facade placed in front of a single service class with a single method is best described as: A. Overengineering -- there's no real multi-object coordination being hidden, so the facade adds a layer for no benefit. B. A required step before using dependency injection. C. The correct minimal use of the Facade pattern. D. A security improvement because it hides the underlying class name.
A. Overengineering -- there's no real multi-object coordination being hidden, so the facade adds a layer for no benefit. — Facade earns its keep by hiding genuine coordination across several subsystem classes; wrapping one class's one method just renames a call without removing any complexity.
Design a LoggingResponseDecorator that wraps any ResponseInterface, logs the response body's length via an injected Logger every time getBody() is called, and otherwise forwards getHeaders() unchanged. Then design a PermissionNode Composite (interface with isGrantedFor(string $userRole): bool) with a leaf Permission and a container PermissionGroup, and build a tree representing 'Admin' containing 'Billing' and 'UserManagement'.
final class LoggingResponseDecorator extends ResponseDecorator { public function __construct(ResponseInterface $inner, private Logger $logger) { parent::__construct($inner); } public function getBody(): string { $body = parent::getBody(); $this->logger->log('response length: ' . strlen($body)); return $body; } } final class Permission implements PermissionNode { public function __construct(private string $name, private string $role) {} public function isGrantedFor(string $userRole): bool { return $userRole === $this->role; } } final class PermissionGroup implements PermissionNode { private array $children = []; public function add(PermissionNode $n): void { $this->children[] = $n; } public function isGrantedFor(string $userRole): bool { foreach ($this->children as $c) { if ($c->isGrantedFor($userRole)) return true; } return false; } }
LoggingResponseDecorator should extend a base ResponseDecorator (or implement ResponseInterface directly) and call $this->logger->log() inside its own getBody() before delegating to the inner response. PermissionGroup's isGrantedFor() should foreach its children and return true if ANY child grants the role (or ALL, depending on whether you want it to model 'any of' vs 'all of').