Design Patterns And Data Architecture

Factory Pattern

A factory is an object or function that creates other objects. That definition is simple, but the pattern is useful only when construction has a real decision, validation rule, dependency choice, or setup sequence that should not be repeated throughout the codebase. In PHP applications, factories commonly appear around payment clients, mailers, export writers, discount strategies, value objects, framework services, HTTP clients, and objects built from configuration.

A factory should not exist just to hide every new keyword. Creating a simple value object directly can be perfectly clear. The goal is to give construction logic one owner when scattered construction would make the application brittle. If three controllers each know how to choose between Stripe, PayPal, and a fake payment gateway, the payment selection rule is duplicated. A factory can centralize that rule and give the rest of the application one stable way to request the correct implementation.

Factories also help when object creation needs guardrails. A money object should not be built from a floating-point number in one place and an integer number of pence in another. An HTTP client should not be created without timeout and base URI settings. A CSV writer should not let callers forget encoding or delimiter rules. A factory can protect those invariants at the boundary where objects are born.

Factory Is A Boundary, Not Decoration

A factory should make a decision or enforce a rule. A class named UserFactory that only returns new User() without adding clarity may not be worth the extra file. A factory becomes useful when it answers questions such as:

  • Which concrete implementation should be used for this configuration?
  • Which dependencies are required to build a valid object?
  • Which input values need normalization or validation before construction?
  • Which defaults should be applied consistently?
  • Which object graph should be assembled for this runtime environment?

This is why factories often sit near application edges. They translate configuration, request input, command-line options, feature flags, tenant settings, or framework container definitions into usable objects.

The factory itself should not become a dumping ground for business behavior. It creates the object that performs work; it should not perform the whole use case.

A Simple Strategy Factory

This example chooses a discount strategy from a code. The caller does not need to know every concrete class.

PHP example
<?php

declare(strict_types=1);

interface DiscountStrategy
{
    public function discountFor(int $subtotalPence): int;
}

final class NoDiscount implements DiscountStrategy
{
    public function discountFor(int $subtotalPence): int
    {
        return 0;
    }
}

final class StudentDiscount implements DiscountStrategy
{
    public function discountFor(int $subtotalPence): int
    {
        return (int) round($subtotalPence * 0.15);
    }
}

final class DiscountFactory
{
    public function fromCode(string $code): DiscountStrategy
    {
        return match (strtolower(trim($code))) {
            'student' => new StudentDiscount(),
            '', 'none' => new NoDiscount(),
            default => throw new InvalidArgumentException('Unknown discount code.'),
        };
    }
}

$factory = new DiscountFactory();
$discount = $factory->fromCode('student');

echo $discount->discountFor(2000) . PHP_EOL;

// Prints:
// 300

The factory owns the mapping from external code to implementation. That mapping is now testable without running checkout, HTTP routing, or database code. Adding a new discount still requires a deliberate change, but the change is localized.

Factories And Dependency Injection

Factories and dependency injection work together. Dependency injection supplies ready dependencies to classes that need them. A factory creates objects when creation requires runtime input or a selection rule.

A service container can act as a factory for application services, but not all factories should become invisible container wiring. If the construction rule is part of the domain or application workflow, an explicit factory class may be clearer than a container binding hidden in configuration.

For example, a framework container can create CheckoutController because its dependencies are known at startup. A PaymentGatewayFactory may still be useful because the selected gateway depends on merchant account settings, feature flags, or a checkout region known only at runtime.

Avoid passing the whole service container into ordinary services and calling it a factory. That is usually a service locator. It hides dependencies and makes tests harder because the class can fetch anything. Prefer a narrow factory interface that exposes only the creation operation the caller needs.

Named Constructors And Value Objects

Not every factory needs its own class. A value object can use named constructors when creation rules belong directly with the value.

PHP example
<?php

declare(strict_types=1);

final readonly class Money
{
    private function __construct(
        public int $amountPence,
        public string $currency,
    ) {
        if ($amountPence < 0) {
            throw new InvalidArgumentException('Amount must not be negative.');
        }
    }

    public static function pounds(string $amount): self
    {
        if (!preg_match('/^\d+\.\d{2}$/', $amount)) {
            throw new InvalidArgumentException('Use pounds with two decimal places.');
        }

        [$pounds, $pence] = explode('.', $amount);

        return new self(((int) $pounds * 100) + (int) $pence, 'GBP');
    }
}

$price = Money::pounds('12.99');

echo $price->amountPence . ' ' . $price->currency . PHP_EOL;

// Prints:
// 1299 GBP

Here Money::pounds() is a factory method. It gives callers a safe way to build the value without exposing a constructor that accepts ambiguous floats. This is a good use of a factory idea without adding a separate MoneyFactory class.

Use a separate factory when creation depends on external services, configuration, environment, or multiple concrete types. Use a named constructor when the rule is intrinsic to the value itself.

Abstract Factory Versus Simple Factory

You may see terms such as factory method, simple factory, static factory, and abstract factory. Do not start by memorizing a catalogue. Start with the problem.

A simple factory usually has one method that chooses or builds an object. A factory method is a method that subclasses or implementers can override to decide what gets created. An abstract factory groups related creation methods behind an interface, often when a family of objects must be compatible.

In PHP application work, simple factories and named constructors are far more common than textbook abstract factories. Abstract factories can be useful when a system has interchangeable families, such as a UI toolkit, storage provider package, or multi-provider integration where client, signer, webhook parser, and event mapper must match the same provider.

Do not introduce an abstract factory just because several classes are created. Introduce it when the family compatibility rule matters and would otherwise be duplicated or easy to violate.

Factories For External Providers

External integrations often benefit from factories because provider setup includes credentials, base URLs, timeouts, retry policy, API versions, and environment selection.

A payment client built in one controller with a five-second timeout and another controller with no timeout is a production defect waiting to happen. A factory can enforce a complete provider configuration and reject invalid configuration at startup or at the first controlled boundary.

However, be careful not to put secrets or raw provider configuration into logs or exceptions. A factory can throw PaymentProviderConfigurationMissing, but the message should not include the API key. It can log the provider name and missing setting name through safe operational logs.

Factories should also avoid doing expensive network checks during object creation unless that is an intentional startup validation step. Creating a client object should usually be cheap. Calling the provider should happen when the application operation needs it.

Factories And Testing

Factories make tests easier when they isolate construction decisions. You can test that DiscountFactory::fromCode('student') returns a student discount and rejects an unknown code. You can test a payment factory with fake configuration and verify that timeouts, environment, and provider choices are applied.

For classes that depend on created objects, prefer depending on an interface or narrow factory interface. In tests, pass a fake factory that returns a fake implementation. That avoids booting the full framework container or real external SDK.

Be careful with factories that read global state directly. A factory that calls getenv() inside every method can be awkward to test and can behave differently across processes. Prefer injecting configuration as a value object or array that was loaded by the application bootstrap. Then the factory remains deterministic.

Common Failure Modes

One failure mode is factory overuse. If every simple object has a factory, code becomes harder to follow. Direct construction is fine when the constructor is clear, stable, and has no branching setup rule.

Another failure mode is the giant factory. A class named ApplicationFactory that creates users, invoices, HTTP clients, reports, commands, repositories, and cache adapters has no focused responsibility. Split factories by concept or let the dependency injection container assemble ordinary services.

A third failure mode is hiding dependencies. If a service receives a generic factory or container and asks it for arbitrary classes, the service's real dependencies disappear from the constructor. This makes code harder to review and test.

A fourth failure mode is mixing creation with work. A ReportExporterFactory should build an exporter; it should not run the export, write files, send email, and return a response. Keep creation and execution separate unless the method name and contract make the combined behavior explicit.

Review Criteria

When reviewing a factory, ask what rule it owns. If the answer is only "it creates the class," direct construction may be clearer. If the answer is "it maps provider configuration to a compatible client family" or "it validates money input before construction," the factory has a real reason to exist.

Check that the factory returns an interface or meaningful type when callers should not know the concrete class. Check that invalid input fails early with a useful exception. Check that defaults such as timeouts, base URLs, encoding, currency, and feature flags are applied consistently.

Check that tests cover each branch. A factory with a match over provider names should have tests for known providers and unknown providers. A named constructor should have tests for valid input, malformed input, and boundary values.

What To Check

Before moving on, make sure you can:

  • explain when a factory is better than direct new
  • distinguish a factory from a service locator
  • use a named constructor for value-object creation rules
  • use a factory class for runtime selection or object graph setup
  • keep business execution out of construction code
  • test factory branches and invalid input
  • spot overused, giant, or dependency-hiding factories

After this lesson, you should be able to look at object creation in a PHP project and decide whether it is already clear, should become a named constructor, should move to a focused factory, or should remain dependency-injection container wiring.

Practice

Task: Choose A Factory Boundary

For each case, decide whether direct construction, a named constructor, a focused factory class, or dependency-injection container wiring is the best default.

Cases

  1. Creating new DateTimeImmutable('now') in a one-off script.
  2. Building a Money value from a submitted pounds-and-pence string.
  3. Choosing between Stripe, PayPal, and a fake payment gateway from merchant configuration.
  4. Creating a controller with known framework services at application startup.
  5. Building an HTTP client that must always use a base URI, timeout, user agent, and retry policy.

Requirements

Explain the choice for each case and identify what rule, if any, the factory would own.

Check your work

The answer should not claim that every new keyword needs a factory.

Show solution

new DateTimeImmutable('now') in a one-off script can use direct construction. There is no shared construction rule unless the project needs a clock abstraction for tests or consistent timezone policy.

Building Money from a submitted string fits a named constructor such as Money::fromPoundsString(). The parsing and validation rule belongs with the value object because it protects the value from ambiguous floats and malformed input.

Choosing between Stripe, PayPal, and a fake gateway fits a focused factory class. The factory owns the mapping from merchant configuration to a compatible gateway implementation and can validate missing provider settings.

Creating a controller with known framework services belongs to dependency-injection container wiring. The dependencies are known at startup, and the controller should simply declare them.

Building a configured HTTP client can use a focused factory or container factory depending on whether the configuration is static or runtime-specific. The factory owns base URI, timeout, user agent, retry policy, and safe defaults so callers cannot accidentally create incomplete clients.

Task: Refactor Construction Logic
PHP example
$gateway = match ($merchant->provider) {
    'stripe' => new StripeGateway($merchant->stripeKey),
    'paypal' => new PayPalGateway($merchant->paypalClientId, $merchant->paypalSecret),
    default => new FakeGateway(),
};

Propose a factory design that removes the duplication.

Requirements

Name the factory, its method, the return type, what input it accepts, how it handles unknown providers, and how tests should cover it.

Check your work

The design should centralize provider selection without making controllers depend on a generic service container.

Show solution

The factory accepts the merchant or a smaller provider configuration value object. It reads the provider name and required credentials, then returns the matching implementation. Unknown providers should throw a specific exception such as UnsupportedPaymentProvider rather than silently using the fake gateway in production.

Controllers receive PaymentGatewayFactory through dependency injection and call $factory->forMerchant($merchant) when the merchant is known. They do not need to know constructor details for each provider.

Tests should cover Stripe configuration, PayPal configuration, missing required credentials, unknown provider names, and the explicit fake-provider path if the application supports one. The tests should also verify that secrets are not included in exception messages.

Task: Review Factory Overuse

A project contains UserFactory, AddressFactory, InvoiceFactory, ReportFactory, and EverythingFactory. Most methods only return new ClassName(...) with the same arguments they received.

Review the design and recommend what to keep, remove, or split.

Requirements

Discuss direct construction, named constructors, focused factories, giant factories, service locators, and test impact.

Check your work

The answer should preserve factories that enforce real rules while removing indirection that adds no clarity.

Show solution

Factories that only pass the same arguments into new ClassName(...) without validation, defaults, selection, or setup rules can usually be removed. Direct construction is clearer when the constructor is stable and obvious.

AddressFactory or InvoiceFactory may be worth keeping only if they normalize input, enforce invariants, create compatible object families, or apply shared defaults. If a value object has intrinsic parsing rules, a named constructor such as Address::fromFormInput() may be clearer than a separate factory.

EverythingFactory should be split or removed. A giant factory has no focused responsibility and can become a service locator if callers ask it for arbitrary objects. Ordinary application services should declare dependencies directly through constructors, with the container assembling them where appropriate.

Keep focused factories for real runtime decisions, such as provider selection or configured external clients. Remove factories that hide simple construction. Tests should become simpler because most classes can be built directly, while the remaining factories get targeted tests for their actual construction rules and failure branches.