DEV Community

Cover image for Factory Method in PHP: When Refactoring Leads to a Pattern
CodeCraft Diary
CodeCraft Diary

Posted on • Originally published at codecraftdiary.com

Factory Method in PHP: When Refactoring Leads to a Pattern

Factory Method is one of those patterns that many PHP developers encounter early, but fully understand much later. It is often introduced as a “creational pattern” with diagrams and inheritance hierarchies, yet in real projects it rarely starts that way.

In practice, Factory Method is almost never a design decision made upfront. It is a destination reached through refactoring—usually when simple object creation starts to interfere with otherwise stable business logic.

This article explains how Factory Method emerges naturally, what problems it actually solves, and where developers frequently misuse it.

Factory Method is often described as the “younger sibling” of Abstract Factory.

Here you can find article about Abstract factory: https://codecraftdiary.com/2025/12/07/abstract-factory-pattern-php/

The Starting Point: Simple Object Creation

Every PHP project begins with direct instantiation:

$exporter = new CsvExporter();
$exporter->export($data);
Enter fullscreen mode Exit fullscreen mode

This is clean, readable, and perfectly correct.
No abstraction is needed because there is no variability yet.

The mistake many developers make is assuming that patterns should exist before problems do. In reality, the absence of patterns here is a sign of healthy code.

The First Smell: Conditional Creation

As requirements grow, object creation often becomes conditional:

if ($format === 'csv') {
    $exporter = new CsvExporter();
} elseif ($format === 'xlsx') {
    $exporter = new XlsxExporter();
} else {
    throw new RuntimeException('Unsupported format');
}

$exporter->export($data);
Enter fullscreen mode Exit fullscreen mode

This code still works, but several problems appear:

  • Business logic is mixed with creation logic
  • Adding a new exporter requires modifying this method
  • The method now has multiple reasons to change

However, this situation still does not justify Factory Method.

At this stage, the problem is not patterns—it is responsibility.

The Correct First Move: Extract Creation

The right response is not introducing a GoF pattern, but performing a simple refactoring:

protected function createExporter(string $format): Exporter
{
    if ($format === 'csv') {
        return new CsvExporter();
    }

    if ($format === 'xlsx') {
        return new XlsxExporter();
    }

    throw new RuntimeException('Unsupported format');
}
Enter fullscreen mode Exit fullscreen mode

This step is critical. It separates what the code does from what it creates.

At this point, many developers stop—and often they should. If object creation remains stable and parameter-driven, a Strategy, a map, or even a simple switch is usually enough.

Factory Method only becomes relevant if creation itself must vary by context.

The Key Question

Before going any further, ask:

Will different variants of this class need to create different implementations, while the overall algorithm stays the same?

If the answer is no, Factory Method is the wrong tool.

If the answer is yes, Factory Method is likely the simplest correct solution.

When the Pattern Emerges

Consider a scenario where you now have multiple export services, each following the same workflow:

  • Validate data
  • Create exporter
  • Export
  • Log result

The workflow is identical. Only the exporter differs.

This is where Factory Method naturally appears.

Defining the Stable Algorithm

abstract class ExportService
{
    public function export(array $data): void
    {
        $this->validate($data);

        $exporter = $this->createExporter();
        $exporter->export($data);

        $this->log();
    }

    abstract protected function createExporter(): Exporter;

    protected function validate(array $data): void
    {
        // shared validation logic
    }

    protected function log(): void
    {
        // shared logging logic
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice what happened:

  • The algorithm is fixed
  • The creation step is deferred
  • Subclasses decide what to instantiate

This is Factory Method—not because the pattern was chosen, but because the structure demanded it.

Concrete Implementations

class CsvExportService extends ExportService
{
    protected function createExporter(): Exporter
    {
        return new CsvExporter();
    }
}

class XlsxExportService extends ExportService
{
    protected function createExporter(): Exporter
    {
        return new XlsxExporter();
    }
}
Enter fullscreen mode Exit fullscreen mode

Each subclass controls creation, while the base class controls behavior.

This separation is the essence of Factory Method.

Why This Works Better Than Conditionals

1. Stable Core Logic
The export process is written once. It does not care which exporter exists, only that one exists.

2. Open for Extension
New exporters require new subclasses, not changes to existing logic.

3. Improved Testability
You can override createExporter() in tests and inject test doubles without touching production code.

Common Misunderstandings

“Factory Method replaces conditionals”
It does not.
If the choice depends on runtime data, Factory Method is usually the wrong abstraction.

“Factory Method is just a factory class”
Factory Method is about inheritance and variation, not about standalone factory objects.

“It should be public”
In almost all cases, the factory method should be protected. Making it public leaks internal construction details.

Factory Method vs Other Options

Factory Method sits in the middle: more flexible than direct
instantiation, simpler than full factory hierarchies.

When NOT to Use Factory Method

Avoid Factory Method if:

  • There is only one concrete implementation
  • Object creation depends purely on input values
  • You need to assemble complex object graphs
  • A simple data-driven approach is enough

Patterns add structure—but also weight.

Final Thought

Factory Method is not something you “apply”.
It is something your code grows into.

If you extract object creation, notice that subclasses need to customize it, and want to keep the main algorithm untouched—you are already there.

Used this way, Factory Method becomes one of the most practical and low-risk patterns in everyday PHP development.

Not because it is clever—but because it respects how software actually evolves.

Top comments (0)