Named Constructor Pattern
debt(d7/e3/b3/t5)
Closest to 'only careful code review or runtime testing' (d7). PHPStan is listed as a detection tool but it cannot automatically flag that a class is missing named constructors or that a public constructor should be private — these are design-level decisions. A reviewer must inspect the API surface to notice that both `new Money(500, 'cents')` and `Money::fromCents(500)` are available, or that construction intent is ambiguous. The common mistake of not making the constructor private goes completely undetected by automated analysis.
Closest to 'simple parameterised fix' (e3). The quick_fix describes adding static named constructor methods and making the regular constructor private — this is a small, localised refactor within one class file. Call sites using `new` must be updated to the named constructor, touching multiple call sites but with a straightforward search-and-replace pattern. Not a single-line patch (e1) because constructor visibility change and call-site updates are required, but not a multi-file architectural change.
Closest to 'localised tax' (b3). The pattern applies at the individual class level — each value object or domain entity that adopts named constructors carries its own small tax (private constructor, static methods, validation logic). The rest of the codebase is mostly unaffected unless the class is widely used. It does not impose a cross-cutting constraint, but callers must use the named constructor API rather than `new`, which is a localised constraint.
Closest to 'notable trap' (t5). The misconception field explicitly states the trap: developers think named constructors are 'just static methods' and miss that they should enforce all construction through validated paths, can return subtypes, and replace the public constructor entirely. The common mistake of not making the constructor private confirms this is a documented, frequently encountered gotcha — developers add the static method but leave the direct `new` path open, negating the intent-clarity and validation enforcement benefits.
Also Known As
TL;DR
Explanation
PHP constructors have a single name (__construct) and cannot be overloaded. Named constructors are static methods that communicate intent: Money::fromCents(500), Money::fromDollars(5.00), DateRange::lastMonth(), User::createGuest(). They can return different subtypes, enforce invariants, or accept different input formats. Combined with a private constructor, they become the only way to instantiate the class — enforcing all construction passes through validated paths.
Common Misconception
Why It Matters
Common Mistakes
- Not making the constructor private when named constructors are the intended API — both paths remain available.
- Named constructors that accept the same parameters as the constructor — adds verbosity without clarity.
- Omitting validation in named constructors — they are the perfect place to enforce class invariants.
- Using static methods for everything instead of identifying which cases genuinely need named constructors.
Code Examples
// Ambiguous constructor — is 500 cents or dollars?
class Money {
public function __construct(private int $amount, private string $currency) {}
}
new Money(500, 'USD'); // $500 or 500 cents? Who knows
// Named constructors — intent is explicit:
class Money {
private function __construct(
private readonly int $cents,
private readonly string $currency,
) {}
public static function fromCents(int $cents, string $currency): self {
if ($cents < 0) throw new InvalidArgumentException('Amount cannot be negative');
return new self($cents, $currency);
}
public static function fromDollars(float $dollars, string $currency): self {
return new self((int) round($dollars * 100), $currency);
}
public static function zero(string $currency): self {
return new self(0, $currency);
}
}