SEO Metadata
SEO Title Options
- Design Patterns in PHP: Factory, Strategy, Observer
- Design Patterns in PHP: Factory, Strategy: Practical 2026
- Architecture Playbook: Design Patterns in PHP: Factory
Meta Description Options
- Learn Design Patterns in PHP: Factory, Strategy, Observer & Decorator with a practical Architecture framework, expert mistakes, implementation steps.
- Code-heavy tutorial implementing GoF design patterns in modern PHP 8.x with interface-first design and practical use cases.
URL Slug
design-patterns-php-factory-strategy-observer-decorator
Focus Keyword
Design Patterns in PHP: Factory, Strategy, Observer & Decorator
Additional LSI Keywords
- Architecture
- PHP
- Design Patterns
- OOP
- SOLID
- Design Patterns in PHP: Factory, Strategy, Observer & Decorator
- production checklist
- implementation guide
- best practices
- architecture decisions
- testing strategy
- performance impact
Table of Contents
- Article overview
- What Design Patterns in PHP: Factory, Strategy, Observer & Decorator means
- Why it matters now
- Implementation framework
- Practical comparison
- Expert workflow
- Common mistakes
- Media and link plan
- Original technical deep dive
- FAQ
- Structured data
- Conclusion
Article overview
Design Patterns in PHP: Factory, Strategy, Observer & Decorator is the kind of topic that looks simple until it reaches production. Teams usually discover the real cost late: unclear boundaries, weak defaults, hidden maintenance work, and decisions that seemed harmless when the codebase was small.
The problem gets worse when the article, tutorial, or implementation guide only explains the happy path. This guide closes that gap with a practical framework, a comparison table, common mistakes, and a deep technical section you can use while planning real work.
Keep reading for the non-obvious part: the safest implementation is rarely the most impressive-looking one. It is the one your team can debug, test, document, and evolve without turning every future change into archaeology.
Key Takeaways
- Design Patterns in PHP: Factory, Strategy, Observer & Decorator should be evaluated as a production decision, not only as a syntax or tooling choice.
- The best implementation keeps responsibilities visible, with clear ownership, tests, documentation, and rollback paths.
- Search visibility improves when practical depth, structured answers, and expert examples live on the same page.
[IMAGE: A mobile-first technical article layout showing the main concept, decision table, implementation checklist, and FAQ blocks. Alt: Design Patterns in PHP: Factory, Strategy, Observer & Decorator expert guide for Architecture]
What Design Patterns in PHP: Factory, Strategy, Observer & Decorator means
Design Patterns in PHP: Factory, Strategy, Observer & Decorator means applying architecture knowledge to a concrete engineering decision, then turning that decision into reliable code, documentation, and operational behavior. In practice, it combines the topic's core concepts with trade-off analysis, implementation boundaries, testing strategy, and maintenance discipline.
This is the definition worth optimizing for featured snippets because it avoids hype. It tells the reader what the topic does and what a professional implementation must include.
Why it matters now
The technical web is more crowded than it was a few years ago. Thin tutorials can still get indexed, but they rarely earn trust from senior developers, buyers, AI answer systems, or teams that need production guidance.
For architecture topics, the strongest content now has three layers:
- a clear answer for fast scanning
- a practical framework for implementation
- expert context that explains what breaks later
That same structure helps search engines understand the page. It also helps readers decide whether the advice fits their project.
Implementation framework
Use this framework before adopting the approach described in this article.
- Define the user problem and the production risk.
- Identify the smallest reliable implementation boundary.
- Keep configuration, secrets, and environment-specific behavior outside the article's core logic.
- Add tests for the behavior that would hurt if it regressed.
- Document the trade-off, not only the final code.
- Measure the result with logs, metrics, or user-facing outcomes.
- Revisit the decision after real usage exposes edge cases.
The sequence is deliberately conservative. It keeps the work grounded in outcomes instead of novelty.
[IMAGE: A seven-step implementation framework with discovery, boundary design, configuration, tests, documentation, measurement, and iteration. Alt: Design Patterns in PHP: Factory, Strategy, Observer & Decorator implementation framework]
Practical comparison
| Decision area | Strong approach | Weak approach | Why it matters |
|---|---|---|---|
| Scope | Solve one clear problem | Mix unrelated concerns | Focus improves testing and search intent |
| Architecture | Put logic in explicit classes or documented boundaries | Hide behavior in templates or incidental callbacks | Future changes stay easier to review |
| Data flow | Pass prepared data into the view or endpoint | Query or compute in presentation code | Reduces regressions and performance surprises |
| Testing | Cover the risky behavior directly | Test only the happy path | Catches production failures earlier |
| Documentation | Explain trade-offs and limits | Repeat generic definitions | Builds E-E-A-T and reader trust |
| Operations | Track logs, metrics, and rollback steps | Ship without measurement | Makes the decision reversible |
This table is intentionally practical. It gives a reviewer something to check before the implementation becomes expensive to change.
Expert workflow
Expert tip: "Treat Design Patterns in PHP: Factory, Strategy, Observer & Decorator as a system boundary. If the next developer cannot find where the decision lives, how it is tested, and when it should be avoided, the implementation is not finished."
A useful workflow is simple:
- Start with the smallest working example.
- Add the constraints that exist in your real project.
- Remove anything that only demonstrates cleverness.
- Write down the failure modes.
- Add links to related decisions so future readers can navigate the topic cluster.
That last point matters for both humans and search systems. A single article can answer a question; a cluster proves authority.
Common mistakes
Mistake 1: Copying a pattern without its context
A pattern that works in a small demo can fail in a real application. The missing context is usually data volume, team experience, deployment process, security requirements, or observability.
Before copying the pattern, ask what assumption made it safe in the original example.
Mistake 2: Putting business logic in the wrong layer
This is the fastest way to make future debugging expensive. In Laravel, PHP, and server-rendered websites, presentation should receive prepared data, not discover rules on its own.
Keep decision logic in models, actions, services, policies, requests, jobs, or documented helpers where it can be tested directly.
Mistake 3: Optimizing for novelty instead of maintainability
Newer tools and language features can be valuable. They can also hide simple behavior behind unfamiliar syntax.
Use the option that makes the next production incident easier to understand.
Mistake 4: Publishing without a measurement plan
If the article describes a performance, SEO, security, or architecture improvement, define how success will be checked. Logs, tests, crawl diagnostics, analytics, and user behavior are all stronger than assumptions.
[IMAGE: A common-mistakes board with context loss, wrong layer, novelty bias, and missing measurement highlighted. Alt: Design Patterns in PHP: Factory, Strategy, Observer & Decorator common mistakes]
Media and link plan
Image placeholders
- [IMAGE: A concept diagram for Design Patterns in PHP: Factory, Strategy, Observer & Decorator with input, decision boundary, implementation, tests, and production feedback. Alt: Design Patterns in PHP: Factory, Strategy, Observer & Decorator concept diagram]
- [IMAGE: A mobile screenshot-style checklist for Design Patterns in PHP: Factory, Strategy, Observer & Decorator. Alt: Design Patterns in PHP: Factory, Strategy, Observer & Decorator mobile checklist]
- [IMAGE: A comparison table visualization for strong versus weak implementation choices. Alt: Design Patterns in PHP: Factory, Strategy, Observer & Decorator comparison table]
Video placeholder
[VIDEO: Insert a 5-8 minute YouTube walkthrough that demonstrates the main decision, the implementation boundary, the test strategy, and the production caveats for Design Patterns in PHP: Factory, Strategy, Observer & Decorator.]
Trustworthy outbound links
- PHP manual - use this as the trust reference for language-level reference.
- Google Search quality guidance - use this as the trust reference for people-first content and E-E-A-T alignment.
Internal linking opportunities
- Internal guide: SOLID Principles in PHP: Practical Examples - use this when readers need a related Architecture follow-up.
- Internal guide: PHP Refactoring Handbook: How to Modernize - use this when readers need a related Architecture follow-up.
Original Technical Deep Dive
Patterns are tools, not goals
Design patterns help when code changes in predictable ways.
Use them when they remove conditionals, isolate third-party code, make policies swappable, or keep side effects away from core business logic. Do not use them to make simple code look architectural.
This guide uses one checkout domain and four patterns:
| Pattern | Use it when |
|---|---|
| Factory | Object creation depends on runtime input or configuration. |
| Strategy | A business rule has multiple interchangeable implementations. |
| Observer | Something happened and several independent reactions may follow. |
| Decorator | You need to add behavior around an existing service without changing its public contract. |
The practical rule is simple: start with an interface only when more than one implementation is real or likely. Otherwise, write the simple class first.
Shared value objects
The examples use small typed objects instead of arrays.
declare(strict_types=1);
final readonly class Money
{
public function __construct(
public int $cents,
public string $currency,
) {
if ($cents < 0) {
throw new InvalidArgumentException('Money cannot be negative.');
}
if ($currency === '') {
throw new InvalidArgumentException('Currency is required.');
}
}
public static function zero(string $currency): self
{
return new self(0, $currency);
}
public function add(self $other): self
{
$this->assertSameCurrency($other);
return new self($this->cents + $other->cents, $this->currency);
}
public function subtract(self $other): self
{
$this->assertSameCurrency($other);
if ($other->cents > $this->cents) {
throw new InvalidArgumentException('Resulting amount cannot be negative.');
}
return new self($this->cents - $other->cents, $this->currency);
}
public function multiply(float $ratio): self
{
return new self((int) round($this->cents * $ratio), $this->currency);
}
private function assertSameCurrency(self $other): void
{
if ($this->currency !== $other->currency) {
throw new InvalidArgumentException('Currency mismatch.');
}
}
}
final readonly class CartLine
{
public function __construct(
public string $sku,
public int $quantity,
public Money $unitPrice,
) {
if ($sku === '') {
throw new InvalidArgumentException('SKU is required.');
}
if ($quantity < 1) {
throw new InvalidArgumentException('Quantity must be positive.');
}
}
public function total(): Money
{
return $this->unitPrice->multiply($this->quantity);
}
}
final readonly class Cart
{
/**
* @param list<CartLine> $lines
*/
public function __construct(
public array $lines,
public string $currency,
public bool $customerIsVip = false,
) {
if ($lines === []) {
throw new InvalidArgumentException('Cart cannot be empty.');
}
}
public function subtotal(): Money
{
$total = Money::zero($this->currency);
foreach ($this->lines as $line) {
$total = $total->add($line->total());
}
return $total;
}
}
The readonly keyword fits value objects because they should be created valid and then left alone. If the project still supports PHP 8.1, use readonly properties instead of readonly class.
Factory: hide construction policy
A factory creates objects when the choice of concrete class is a policy decision.
Do not create a factory just to write new User(). Create one when the caller should not know which implementation is needed.
Payment gateway selection is a good example:
declare(strict_types=1);
enum PaymentProvider: string
{
case Stripe = 'stripe';
case BankTransfer = 'bank_transfer';
case Fake = 'fake';
}
interface PaymentGateway
{
public function charge(Money $amount, string $reference): ChargeResult;
}
final readonly class ChargeResult
{
public function __construct(
public string $providerReference,
public bool $paid,
) {}
}
Concrete gateways can depend on different infrastructure:
declare(strict_types=1);
interface HttpClient
{
/**
* @param array<string, mixed> $payload
* @return array<string, mixed>
*/
public function postJson(string $url, array $payload): array;
}
final readonly class StripeGateway implements PaymentGateway
{
public function __construct(
private HttpClient $http,
private string $apiKey,
) {}
public function charge(Money $amount, string $reference): ChargeResult
{
$response = $this->http->postJson('https://api.stripe.test/charges', [
'api_key' => $this->apiKey,
'amount' => $amount->cents,
'currency' => $amount->currency,
'reference' => $reference,
]);
return new ChargeResult(
providerReference: (string) $response['id'],
paid: (bool) $response['paid'],
);
}
}
final readonly class BankTransferGateway implements PaymentGateway
{
public function __construct(
private string $iban,
) {}
public function charge(Money $amount, string $reference): ChargeResult
{
return new ChargeResult(
providerReference: sprintf('%s:%s:%d', $this->iban, $reference, $amount->cents),
paid: false,
);
}
}
final class FakeGateway implements PaymentGateway
{
public function charge(Money $amount, string $reference): ChargeResult
{
return new ChargeResult(
providerReference: 'fake_'.$reference,
paid: true,
);
}
}
The factory owns the construction switch:
declare(strict_types=1);
final readonly class PaymentGatewayFactory
{
/**
* @param array{
* stripe_api_key: string,
* bank_iban: string
* } $config
*/
public function __construct(
private HttpClient $http,
private array $config,
) {}
public function make(PaymentProvider $provider): PaymentGateway
{
return match ($provider) {
PaymentProvider::Stripe => new StripeGateway(
http: $this->http,
apiKey: $this->config['stripe_api_key'],
),
PaymentProvider::BankTransfer => new BankTransferGateway(
iban: $this->config['bank_iban'],
),
PaymentProvider::Fake => new FakeGateway(),
};
}
}
The checkout service receives the factory, not a pile of payment classes:
declare(strict_types=1);
final readonly class CheckoutService
{
public function __construct(
private PaymentGatewayFactory $gateways,
) {}
public function pay(Cart $cart, PaymentProvider $provider, string $orderNumber): ChargeResult
{
$gateway = $this->gateways->make($provider);
return $gateway->charge($cart->subtotal(), $orderNumber);
}
}
Factory trade-off:
- Good: keeps object creation and configuration in one place.
- Good: lets the caller ask for behavior by enum or runtime setting.
- Bad: can become a service locator if every object in the app is pulled from it.
- Bad: often unnecessary when dependency injection can wire one fixed implementation.
Strategy: swap business rules
Strategy is for interchangeable algorithms behind the same interface.
A discount rule is a better Strategy candidate than a gateway factory because the checkout flow does not care how the discount is calculated.
declare(strict_types=1);
interface DiscountPolicy
{
public function discountFor(Cart $cart): Money;
}
final class NoDiscount implements DiscountPolicy
{
public function discountFor(Cart $cart): Money
{
return Money::zero($cart->currency);
}
}
final readonly class PercentageDiscount implements DiscountPolicy
{
public function __construct(
private float $percent,
) {
if ($percent < 0 || $percent > 100) {
throw new InvalidArgumentException('Discount percent must be between 0 and 100.');
}
}
public function discountFor(Cart $cart): Money
{
return $cart->subtotal()->multiply($this->percent / 100);
}
}
final class VipDiscount implements DiscountPolicy
{
public function discountFor(Cart $cart): Money
{
if (! $cart->customerIsVip) {
return Money::zero($cart->currency);
}
return $cart->subtotal()->multiply(0.15);
}
}
The service depends on the interface:
declare(strict_types=1);
final readonly class PriceQuote
{
public function __construct(
public Money $subtotal,
public Money $discount,
public Money $total,
) {}
}
final readonly class QuoteCalculator
{
public function __construct(
private DiscountPolicy $discounts,
) {}
public function quote(Cart $cart): PriceQuote
{
$subtotal = $cart->subtotal();
$discount = $this->discounts->discountFor($cart);
return new PriceQuote(
subtotal: $subtotal,
discount: $discount,
total: $subtotal->subtract($discount),
);
}
}
Selection can be a separate resolver:
declare(strict_types=1);
enum DiscountCode: string
{
case None = 'none';
case Launch = 'launch';
case Vip = 'vip';
}
final class DiscountPolicyResolver
{
public function resolve(DiscountCode $code): DiscountPolicy
{
return match ($code) {
DiscountCode::None => new NoDiscount(),
DiscountCode::Launch => new PercentageDiscount(10),
DiscountCode::Vip => new VipDiscount(),
};
}
}
The important part is not the match. The important part is that QuoteCalculator does not change when a new discount policy is added.
[IMAGE: Supporting visual 1 for Design Patterns in PHP: Factory, Strategy, Observer & Decorator, showing Design Patterns in PHP: Factory, Strategy, Observer & Decorator decisions, examples, and PHP, Design Patterns, Architecture. Alt: Design Patterns in PHP: Factory, Strategy, Observer & Decorator design-patterns-php-factory-strategy-observer-decorator visual 1]
[IMAGE: Supporting visual 1 for Design Patterns in PHP: Factory, Strategy, Observer & Decorator, showing Design Patterns in PHP: Factory, Strategy, Observer & Decorator decisions, examples, and PHP, Design Patterns, Architecture. Alt: Design Patterns in PHP: Factory, Strategy, Observer & Decorator design-patterns-php-factory-strategy-observer-decorator visual 1]
Strategy trade-off:
- Good: removes large conditional blocks from the caller.
- Good: makes each rule testable on its own.
- Good: lets you compose runtime behavior.
- Bad: too many tiny strategies can hide simple logic.
- Bad: if every strategy needs completely different input, the interface is probably wrong.
Observer: react to domain events
Observer is for one-to-many reactions.
Do not send emails, update analytics, write audit logs, and notify warehouses directly inside CheckoutService. Publish an event and let independent listeners react.
Start with a small event contract:
declare(strict_types=1);
interface DomainEvent
{
public function occurredAt(): DateTimeImmutable;
}
final readonly class OrderPlaced implements DomainEvent
{
public function __construct(
public string $orderNumber,
public string $customerEmail,
public Money $total,
private DateTimeImmutable $occurredAt = new DateTimeImmutable(),
) {}
public function occurredAt(): DateTimeImmutable
{
return $this->occurredAt;
}
}
Then add a minimal dispatcher:
declare(strict_types=1);
final class EventDispatcher
{
/**
* @var array<class-string<DomainEvent>, list<callable(DomainEvent): void>>
*/
private array $listeners = [];
/**
* @template T of DomainEvent
* @param class-string<T> $eventClass
* @param callable(T): void $listener
*/
public function listen(string $eventClass, callable $listener): void
{
$this->listeners[$eventClass][] = $listener;
}
public function dispatch(DomainEvent $event): void
{
foreach ($this->listeners[$event::class] ?? [] as $listener) {
$listener($event);
}
}
}
Listeners stay focused:
declare(strict_types=1);
final readonly class SendOrderReceipt
{
public function __construct(
private Mailer $mailer,
) {}
public function __invoke(OrderPlaced $event): void
{
$this->mailer->send(
to: $event->customerEmail,
subject: 'Your order '.$event->orderNumber,
body: 'Thanks for your order.',
);
}
}
final readonly class RecordOrderMetric
{
public function __construct(
private Metrics $metrics,
) {}
public function __invoke(OrderPlaced $event): void
{
$this->metrics->increment('orders.placed');
$this->metrics->timing('orders.total_cents', $event->total->cents);
}
}
Wire the observers once:
declare(strict_types=1);
$events = new EventDispatcher();
$events->listen(OrderPlaced::class, new SendOrderReceipt($mailer));
$events->listen(OrderPlaced::class, new RecordOrderMetric($metrics));
Now checkout publishes one event:
declare(strict_types=1);
final readonly class PlaceOrder
{
public function __construct(
private CheckoutService $checkout,
private EventDispatcher $events,
) {}
public function handle(Cart $cart, PaymentProvider $provider, string $email): ChargeResult
{
$orderNumber = bin2hex(random_bytes(8));
$result = $this->checkout->pay($cart, $provider, $orderNumber);
if ($result->paid) {
$this->events->dispatch(new OrderPlaced(
orderNumber: $orderNumber,
customerEmail: $email,
total: $cart->subtotal(),
));
}
return $result;
}
}
PHP also ships SplObserver and SplSubject. They are useful for generic observer examples, but their method signatures are broad. For domain events, a small typed dispatcher is usually clearer.
Observer trade-off:
- Good: removes unrelated side effects from the command handler.
- Good: lets new reactions be added without editing checkout logic.
- Good: maps cleanly to queues later.
- Bad: control flow becomes less obvious.
- Bad: listener failures need a policy: fail the command, retry, log, or queue.
- Bad: synchronous listeners can quietly slow down the original request.
Decorator: add behavior around a service
Decorator wraps an object that implements the same interface. The caller still depends on the interface, but extra behavior runs before or after the wrapped service.
Take the quote calculator interface:
declare(strict_types=1);
interface PriceCalculator
{
public function quote(Cart $cart): PriceQuote;
}
final readonly class BasePriceCalculator implements PriceCalculator
{
public function quote(Cart $cart): PriceQuote
{
$subtotal = $cart->subtotal();
return new PriceQuote(
subtotal: $subtotal,
discount: Money::zero($cart->currency),
total: $subtotal,
);
}
}
Add discounts as a decorator:
declare(strict_types=1);
final readonly class DiscountingPriceCalculator implements PriceCalculator
{
public function __construct(
private PriceCalculator $next,
private DiscountPolicy $discounts,
) {}
public function quote(Cart $cart): PriceQuote
{
$quote = $this->next->quote($cart);
$discount = $this->discounts->discountFor($cart);
return new PriceQuote(
subtotal: $quote->subtotal,
discount: $quote->discount->add($discount),
total: $quote->total->subtract($discount),
);
}
}
Add tax as another decorator:
declare(strict_types=1);
final readonly class TaxingPriceCalculator implements PriceCalculator
{
public function __construct(
private PriceCalculator $next,
private float $taxRate,
) {}
public function quote(Cart $cart): PriceQuote
{
$quote = $this->next->quote($cart);
$tax = $quote->total->multiply($this->taxRate);
return new PriceQuote(
subtotal: $quote->subtotal,
discount: $quote->discount,
total: $quote->total->add($tax),
);
}
}
Add logging without touching pricing rules:
declare(strict_types=1);
interface Logger
{
/**
* @param array<string, mixed> $context
*/
public function info(string $message, array $context = []): void;
}
final readonly class LoggingPriceCalculator implements PriceCalculator
{
public function __construct(
private PriceCalculator $next,
private Logger $logger,
) {}
public function quote(Cart $cart): PriceQuote
{
$quote = $this->next->quote($cart);
$this->logger->info('Price quoted.', [
'subtotal_cents' => $quote->subtotal->cents,
'discount_cents' => $quote->discount->cents,
'total_cents' => $quote->total->cents,
'currency' => $quote->total->currency,
]);
return $quote;
}
}
Composition order is explicit:
declare(strict_types=1);
$calculator = new LoggingPriceCalculator(
next: new TaxingPriceCalculator(
next: new DiscountingPriceCalculator(
next: new BasePriceCalculator(),
discounts: new VipDiscount(),
),
taxRate: 0.21,
),
logger: $logger,
);
$quote = $calculator->quote($cart);
The order matters. Discount before tax is not the same as tax before discount. Decorators make that order visible.
Decorator trade-off:
- Good: adds cross-cutting behavior without changing the core service.
- Good: composes well with caching, logging, metrics, retries, and authorization checks.
- Good: keeps each wrapper small.
- Bad: deep decorator stacks can be hard to debug.
- Bad: order-dependent decorators need tests around composition.
- Bad: a decorator must preserve the wrapped interface honestly.
[IMAGE: Supporting visual 2 for Design Patterns in PHP: Factory, Strategy, Observer & Decorator, showing Design Patterns in PHP: Factory, Strategy, Observer & Decorator decisions, examples, and PHP, Design Patterns, Architecture. Alt: Design Patterns in PHP: Factory, Strategy, Observer & Decorator design-patterns-php-factory-strategy-observer-decorator visual 2]
How the patterns fit together
In one checkout flow:
- Factory chooses the
PaymentGateway. - Strategy calculates the discount policy.
- Decorator layers discount, tax, and logging around price calculation.
- Observer publishes
OrderPlacedand lets independent listeners react.
That gives each source of change its own place:
[IMAGE: Supporting visual 2 for Design Patterns in PHP: Factory, Strategy, Observer & Decorator, showing Design Patterns in PHP: Factory, Strategy, Observer & Decorator decisions, examples, and PHP, Design Patterns, Architecture. Alt: Design Patterns in PHP: Factory, Strategy, Observer & Decorator design-patterns-php-factory-strategy-observer-decorator visual 2]
| Change | File that should change |
|---|---|
| Add PayPal | PaymentGatewayFactory and a new gateway class. |
| Add student discount | New DiscountPolicy implementation and resolver entry. |
| Add price logging | New PriceCalculator decorator. |
| Send Slack alert after order | New OrderPlaced listener. |
If a change requires editing five unrelated classes, the boundaries are probably wrong.
Testing the patterns
Do not test that a pattern exists. Test the behavior it protects.
Factory test:
declare(strict_types=1);
$factory = new PaymentGatewayFactory($http, [
'stripe_api_key' => 'test-key',
'bank_iban' => 'LT000000000000000000',
]);
assert($factory->make(PaymentProvider::Stripe) instanceof StripeGateway);
assert($factory->make(PaymentProvider::BankTransfer) instanceof BankTransferGateway);
Strategy test:
declare(strict_types=1);
$cart = new Cart([
new CartLine('book', 1, new Money(2000, 'EUR')),
], 'EUR', customerIsVip: true);
$discount = (new VipDiscount())->discountFor($cart);
assert($discount->cents === 300);
Observer test:
declare(strict_types=1);
$events = new EventDispatcher();
$seen = [];
$events->listen(OrderPlaced::class, function (OrderPlaced $event) use (&$seen): void {
$seen[] = $event->orderNumber;
});
$events->dispatch(new OrderPlaced(
orderNumber: 'A1001',
customerEmail: 'buyer@example.com',
total: new Money(5000, 'EUR'),
));
assert($seen === ['A1001']);
Decorator test:
declare(strict_types=1);
$calculator = new DiscountingPriceCalculator(
next: new BasePriceCalculator(),
discounts: new PercentageDiscount(10),
);
$quote = $calculator->quote(new Cart([
new CartLine('desk', 1, new Money(10000, 'EUR')),
], 'EUR'));
assert($quote->discount->cents === 1000);
assert($quote->total->cents === 9000);
These tests are small because each pattern creates a narrow seam around one kind of change.
Common mistakes
Do not start with abstract classes.
Most of these patterns work better with interfaces. Use an abstract class only when shared implementation is real and stable.
Do not put business rules in factories.
A factory chooses what object to build. It should not calculate discounts, send emails, or decide if an order is valid.
Do not turn every if into Strategy.
Some conditionals are clearer than a directory full of one-method classes. Use Strategy when the rule is independently testable, reused, configured, or expected to grow.
Do not hide critical side effects behind observers.
If payment capture must happen before the order is valid, it is part of the command, not an observer. Observers are better for follow-up reactions.
Do not stack decorators without composition tests.
Caching before authorization, retrying around non-idempotent commands, and logging the wrong response can all produce real bugs.
Practical checklist
Before adding a pattern, ask:
- What is changing?
- How often does it change?
- Does the caller need to know the concrete class?
- Can the behavior be tested through an interface?
- Will this remove a real conditional or just move it?
- Does this make failure behavior clearer?
- Can a new implementation be added without editing core flow?
[IMAGE: Supporting visual 3 for Design Patterns in PHP: Factory, Strategy, Observer & Decorator, showing Design Patterns in PHP: Factory, Strategy, Observer & Decorator decisions, examples, and PHP, Design Patterns, Architecture. Alt: Design Patterns in PHP: Factory, Strategy, Observer & Decorator design-patterns-php-factory-strategy-observer-decorator visual 3]
Good PHP architecture is not a pattern catalog. It is code where changes land in predictable places.
FAQ
What is Design Patterns in PHP: Factory, Strategy, Observer & Decorator?
Design Patterns in PHP: Factory, Strategy, Observer & Decorator is a practical architecture topic that should be evaluated through implementation scope, production risk, testing, documentation, and long-term maintainability.
When should a team use Design Patterns in PHP: Factory, Strategy, Observer & Decorator?
Use Design Patterns in PHP: Factory, Strategy, Observer & Decorator when it solves a real project constraint, improves clarity, or reduces operational risk. Avoid it when it only adds novelty or hides behavior from future maintainers.
What is the biggest risk with Design Patterns in PHP: Factory, Strategy, Observer & Decorator?
The biggest risk is copying a pattern without its context. Production systems need clear boundaries, rollback options, tests, and observability before a technique becomes dependable.
How do you test Design Patterns in PHP: Factory, Strategy, Observer & Decorator?
Test the smallest unit that owns the behavior, then add integration coverage for the path users or systems actually rely on. Include failure cases, configuration differences, and regression checks.
How does Design Patterns in PHP: Factory, Strategy, Observer & Decorator affect SEO and AI search visibility?
It improves visibility when the article gives a direct answer, expert context, structured headings, internal links, trustworthy references, and FAQ content that matches the visible page.
Conclusion
Design Patterns in PHP: Factory, Strategy, Observer & Decorator is worth doing when the implementation improves clarity, reliability, or delivery speed. It is not worth doing when it hides ownership, increases operational risk, or makes the system harder to explain.
Use the framework above as a review checklist. Then connect this topic to the rest of the project documentation so readers can move from concept to implementation without losing context.