Implementing the Abstract Factory Pattern in PHP: Creating Families of Related Objects Cleanly
AI generated
<?php
8.4
PHP · OOP · Design Patterns · Creational Patterns
Implementing the Abstract Factory Pattern in PHP
Creating Families of Related Objects Cleanly

As soon as a system needs to interchangeably create multiple objects that belong together, for instance payment providers with a matching client, validator and logger, a simple factory method is no longer enough. The Abstract Factory pattern encapsulates exactly this creation of entire product families behind a shared, type-safe interface.

20 min read Abstract Factory · interfaces · dependency injection PHP 8.x

1. What the Abstract Factory Pattern Actually Solves

The Abstract Factory pattern belongs to the family of creational patterns and solves a specific problem: client code should be able to create entire families of mutually compatible objects without knowing the concrete classes of those objects. The decisive difference from a simple factory method is the emphasis on "families": not a single object is created, but a cohesive set of several objects that must fit together and must never be mixed.

A classic example illustrates the problem: an e-commerce system supports multiple payment providers like Stripe and PayPal. Each provider needs its own API client, its own request validator, and its own response parser. These three objects must match, combining a Stripe client with a PayPal validator does not produce working code. The Abstract Factory pattern ensures that exactly this consistency stays guaranteed, by having a single factory instance be responsible for the entire family.

In this article we build the Abstract Factory pattern step by step in PHP, from the basic structure through a complete payment provider example to integration with dependency injection containers. By the end you will know exactly when the extra structural effort is worthwhile and when a simpler solution suffices.

2. Basic Structure: Products, Factories, and Interfaces

The structure of the Abstract Factory pattern consists of three layers. First, abstract product interfaces that define which operations every product in the family must support, independent of the concrete implementation. Second, concrete product classes that implement these interfaces for a specific family, for instance all Stripe-related classes. Third, an abstract factory interface with one method per product type, plus concrete factory classes that instantiate the matching concrete products for each family.

The client code works exclusively with the abstract factory interface and the abstract product interfaces, never with concrete classes. This strict separation is the core of the Abstract Factory pattern: the client only knows it has a PaymentProviderFactory that delivers a PaymentClient, a PaymentValidator, and a PaymentResponseParser, but not whether Stripe, PayPal, or a test double is behind it. This decoupling is exactly what makes swapping entire product families at runtime or configuration time possible without having to adapt the client code.


<?php

declare(strict_types=1);

// Abstract product interfaces — the "family" contract
interface PaymentClient
{
    public function charge(int $amountInCents, string $currency): PaymentResult;
}

interface PaymentValidator
{
    public function validate(array $requestPayload): bool;
}

interface PaymentResponseParser
{
    public function parse(string $rawResponse): PaymentResult;
}

// Abstract factory interface — one creation method per product type
interface PaymentProviderFactory
{
    public function createClient(): PaymentClient;
    public function createValidator(): PaymentValidator;
    public function createResponseParser(): PaymentResponseParser;
}

3. Practical Example: Interchangeable Payment Providers

With the interfaces from the previous section, the Abstract Factory pattern can now be filled with concrete implementations. Each concrete factory, for instance StripeProviderFactory and PayPalProviderFactory, encapsulates exactly the products that belong together. The decisive advantage shows up in the calling code: an order service that works with the abstract PaymentProviderFactory interface does not have to change a single line of code when a new payment provider is added or an existing one is swapped out.

This interchangeability is the practical core of the Abstract Factory pattern: the decision of which concrete factory to use is made in exactly one central place, for instance during application bootstrapping based on a configuration setting. The rest of the application code stays completely independent of that decision and works exclusively with the abstract interfaces.


<?php

declare(strict_types=1);

final class StripeProviderFactory implements PaymentProviderFactory
{
    public function __construct(private readonly string $apiKey)
    {
    }

    public function createClient(): PaymentClient
    {
        return new StripeClient($this->apiKey);
    }

    public function createValidator(): PaymentValidator
    {
        return new StripeSignatureValidator($this->apiKey);
    }

    public function createResponseParser(): PaymentResponseParser
    {
        return new StripeResponseParser();
    }
}

final class PayPalProviderFactory implements PaymentProviderFactory
{
    public function __construct(
        private readonly string $clientId,
        private readonly string $clientSecret,
    ) {
    }

    public function createClient(): PaymentClient
    {
        return new PayPalClient($this->clientId, $this->clientSecret);
    }

    public function createValidator(): PaymentValidator
    {
        return new PayPalWebhookValidator($this->clientSecret);
    }

    public function createResponseParser(): PaymentResponseParser
    {
        return new PayPalResponseParser();
    }
}

// Client code depends only on the abstract factory interface
final class CheckoutService
{
    private PaymentClient $client;
    private PaymentValidator $validator;
    private PaymentResponseParser $parser;

    public function __construct(PaymentProviderFactory $factory)
    {
        $this->client = $factory->createClient();
        $this->validator = $factory->createValidator();
        $this->parser = $factory->createResponseParser();
    }

    public function processPayment(int $amountInCents, string $currency): PaymentResult
    {
        return $this->client->charge($amountInCents, $currency);
    }
}

4. Abstract Factory for Multi-Tenant Systems

Another important application area for the Abstract Factory pattern is multi-tenant systems, where each tenant needs its own configuration of mutually compatible services. One tenant might use a certain email provider, a certain SMS gateway, and a certain invoice format, while another tenant needs a completely different combination. The Abstract Factory pattern maps these tenant-specific combinations exactly, by having a separate concrete factory exist per tenant or be configured at runtime.

In practice, the Abstract Factory pattern is frequently combined in such scenarios with a registry or a simple factory-of-factories mechanism that returns the matching concrete factory based on a tenant ID. This combination keeps the tenant-specific configuration logic in a single, well-testable place, instead of scattering it across the entire application code, where at every point it would have to be checked again which tenant is currently active.


<?php

declare(strict_types=1);

interface TenantServiceFactory
{
    public function createMailer(): MailerInterface;
    public function createInvoiceFormatter(): InvoiceFormatterInterface;
}

final class TenantServiceFactoryRegistry
{
    /** @var array<string, TenantServiceFactory> */
    private array $factories = [];

    public function register(string $tenantId, TenantServiceFactory $factory): void
    {
        $this->factories[$tenantId] = $factory;
    }

    public function resolve(string $tenantId): TenantServiceFactory
    {
        return $this->factories[$tenantId]
            ?? throw new RuntimeException("No factory registered for tenant: {$tenantId}");
    }
}

// Bootstrapping: register concrete factories once at startup
$registry = new TenantServiceFactoryRegistry();
$registry->register('acme-corp', new AcmeTenantServiceFactory());
$registry->register('globex-inc', new GlobexTenantServiceFactory());

// Runtime resolution: application code stays tenant-agnostic
$factory = $registry->resolve($currentTenantId);
$mailer = $factory->createMailer();

5. Integration With Dependency Injection Containers

In modern PHP applications, the Abstract Factory pattern is rarely wired manually, but configured through a dependency injection container. Instead of having the client code directly instantiate a concrete factory, you register a binding in the container from the abstract PaymentProviderFactory interface to the concrete implementation that should currently be active. The container then handles resolution, and the client code simply requests an instance of the interface.

This approach combines the strengths of the Abstract Factory pattern with those of dependency injection: the product family stays consistent and type-safe, while the decision of which concrete family to use is fully delegated to the container configuration. Switching the payment provider for the entire application then reduces to a single line in the container configuration, without touching any other code. This exact property makes the Abstract Factory pattern a popular building block in Symfony and PSR-11-compatible applications.

6. Adding New Product Families Without Changing Existing Code

A central promise of the Abstract Factory pattern is honoring the open-closed principle: open for extension, closed for modification. Adding a new product family, for instance support for a third payment provider like Klarna, only requires implementing the three product interfaces and the factory class for this new provider. No existing code needs to change, neither the CheckoutService class nor other factories are affected.

This property clearly distinguishes the Abstract Factory pattern from a large switch statement that instantiates the matching objects based on a provider string. With such a switch, every new product family would have to be added at exactly that central location, which leads to an ever-growing, unwieldy method as the number of providers increases. The Abstract Factory pattern instead distributes this responsibility across separate, self-contained classes, each of which knows exactly one product family.

7. Testability: Fake Factories for Unit Tests

An often underestimated advantage of the Abstract Factory pattern shows up when writing unit tests. Since the client code works exclusively with the abstract factory interface, you can implement a FakePaymentProviderFactory for tests that returns test doubles instead of real API clients. The CheckoutService from the practical example does not need any Stripe or PayPal credentials for its tests, it simply gets the fake factory injected during testing.

This testability is a direct result of the strict separation between abstract interfaces and concrete implementations that the Abstract Factory pattern enforces. Without this pattern, a test would often have to mock real network calls, scattered across many individual method calls. With the Abstract Factory pattern, a single swap of the factory instance is enough to replace the entire product family with test doubles, which makes tests significantly faster and more stable.

8. Common Mistakes and Avoiding Overengineering

The most common mistake when using the Abstract Factory pattern is applying it to situations where no genuine product family exists, but only a single object needs to be created. For a single product, a simple factory method or even a direct constructor call is completely sufficient. The Abstract Factory pattern only justifies its extra structural effort when multiple related objects genuinely need to be swapped consistently.

A second mistake is splitting the product family too granularly. Adding products to the factory that in practice are never swapped independently of others makes the interface grow unnecessarily and complicates implementing new factories. A third problem arises when factories themselves hold state that is meant to be shared between multiple creation calls, the Abstract Factory pattern is designed for stateless, repeatedly callable creation, not for singleton-like state management, for which other patterns are better suited.

9. Abstract Factory Compared to Factory Method and Builder

The Abstract Factory pattern is frequently confused with related creational patterns. The following table summarizes the key differences and helps choose the right pattern for a given use case.

Criterion Factory Method Abstract Factory Builder
Creates A single object Family of several related objects A complex object step by step
Focus Which class to instantiate Ensuring consistency between products Construction steps and order
Typical implementation One method, often with Late Static Binding Interface with multiple creation methods Fluent interface with with-methods
Interchangeability One implementation per call Entire family swappable at once Not the pattern's focus
Complexity Low Medium to high Medium

This comparison shows why the Abstract Factory pattern is specifically suited for situations with multiple related products, while the simpler factory method is enough for a single object, and the builder tends to be used for complex, step-by-step construction of a single object. The choice between these patterns should always be made based on the actual requirement, not on personal preference for a particular pattern.

Mironsoft

PHP architecture, design patterns, and multi-tenant systems

Building interchangeable payment providers or tenant configurations?

We design Abstract Factory structures for interchangeable provider families, integrate them cleanly into your dependency injection configuration, and ensure fully testable, extensible code without overengineering.

Architecture Design

Identifying product families and choosing the right creational pattern

DI Integration

Configuring Abstract Factory bindings cleanly in PSR-11 containers

Test Coverage

Fake factories and test doubles for fast, stable unit tests

10. Summary

The Abstract Factory pattern solves exactly one problem: creating entire families of related objects consistently and interchangeably, without the client code having to know concrete classes. The structure of abstract product interfaces, concrete product implementations, and an abstract factory with multiple creation methods enforces exactly this consistency, where a simple factory method would fail because it only considers a single object.

The greatest strength of the Abstract Factory pattern lies in the combination of extensibility, testability, and clean integration with dependency injection containers. The greatest danger lies in overengineering: applying the pattern to single, independent objects creates unnecessary structural effort without real added value. Anyone who makes the decision based on the actual product family structure, rather than out of pure pattern affection, uses the Abstract Factory pattern exactly where it plays to its real strengths.

Abstract Factory in PHP — The Key Points at a Glance

Core Idea

A factory creates several related products as a consistent, interchangeable family.

Use Case

Interchangeable payment providers, multi-tenant systems, any setup with consistent product families.

Advantage

New families can be added without changing existing code, fully mockable factories for tests.

Limitation

Do not use for single, independent objects, a simple factory method is enough for those.

11. FAQ: Abstract Factory in PHP

1Difference to Factory Method?
Factory Method creates a single object, Abstract Factory an entire family of related objects through an interface with multiple methods.
2When to use it?
When multiple objects must fit together and must never be mixed, such as the client and validator of a payment provider.
3New product types harder?
Yes, a new product type requires changes in all concrete factories. Adding new product families, however, stays simple.
4How do you test it?
With a fake factory implementation returning test doubles. The client code stays unchanged, only the factory instance is swapped.
5Compatible with DI containers?
Very well. Register a binding from the factory interface to a concrete implementation in the container, client just requests the interface.
6Most common mistake?
Using it without a genuine product family, that is for a single object. A simple factory method is completely sufficient for that.
7Good for multi-tenant setups?
Yes, each tenant gets its own concrete factory, often combined with a registry that resolves based on a tenant ID.
8Is switch instead of Abstract Factory bad?
Yes, every new family requires a change at the central switch location. Abstract Factory distributes this responsibility across separate classes.
9Can a factory hold state?
Configuration values like API keys yes, mutable state shared between calls better not. Other patterns are better suited for that.
10Builder instead of Abstract Factory?
When a single, complex object needs step-by-step construction with many optional parameters, rather than creating several objects consistently.