skip to content

In a PHP model base class, how do you write a static factory such as User::create() that returns the subclass, and what can break it?

level: seniorimportance: should knowfreq 44%

answer

  1. instantiate the called class
  2. new self returns the base
  3. : static return type since 8.0
  4. child constructor may need other arguments
  5. final or abstract constructor pins the signature

basics

~20 s

Write create() as return new static(...) with a : static return type, so User::create() builds a User. Guard the constructor signature (final, abstract or interface-declared), because a subclass constructor with different parameters makes new static throw.

solid answer

~40 s

`new self()` always builds the class that contains the method, so `User::create()` would return a bare `Model`, and with a `: static` return type PHP throws `TypeError` (`Model::create(): Return value must be of type User, Model returned`). `new static()` uses late static binding to instantiate the **called** class, and the `static` return type (PHP 8.0+) tells callers and tools they get that subclass. The risk is the constructor: PHP does not check constructor signatures across inheritance unless the parent's constructor is abstract or comes from an interface, so a subclass can demand extra arguments and `new static()` then throws `ArgumentCountError`. Declare the base constructor `final`, abstract, or in an interface so every subclass accepts what the factory passes. Calling `Model::create()` on an abstract base throws `Error: Cannot instantiate abstract class Model`.

code

php · 22 lines
php
<?php
declare(strict_types=1);

abstract class Model
{
    final public function __construct(protected array $attributes = []) {}

    public static function create(array $attributes): static
    {
        return new static($attributes);
    }

    abstract public static function table(): string;
}

final class User extends Model
{
    public static function table(): string { return 'users'; }
}

$user = User::create(['name' => 'Ada']);
echo $user::class, ' ', $user::table(), PHP_EOL; // User users

go deeper

for a junior

Know that new static() builds the class a static method was called on, while new self() builds the class the code is in.

for a middle

Explain the static return type, the TypeError it raises for new self, and why an abstract base cannot be instantiated.

for a senior

Show that unchecked constructor signatures make new static fragile, and fix it with a final or abstract constructor or an interface.

for a principal

Decide whether a model hierarchy should rely on inherited static factories at all, or on composition and explicit factories that are easier to replace.

## The goal A common base-class design gives every model a **static named constructor**: ```php $user = User::create(['name' => 'Ada']); $post = Post::create(['title' => 'Hello']); ``` `create()` is written once, in the base class `Model`, but each call should return an instance of the class it was called on. That requires the base class to refer to "whichever subclass was called", which is what **late static binding** (the `static` keyword) provides. ## `new self` versus `new static` | Expression inside `Model::create()` | `User::create()` returns | Notes | |---|---|---| | `new self(...)` | a `Model` | `self` is the class where the code is written | | `new Model(...)` | a `Model` | same as `self`, but hard-coded | | `new static(...)` | a `User` | `static` is the called class, resolved at runtime | With `new self`, the subclass-specific methods are missing from the returned object. If the method also declares `: static` as its return type, PHP catches the mismatch at the return: `Model::create(): Return value must be of type User, Model returned` is a `TypeError`. ## The `static` return type PHP 8.0 added `static` as a **return type** (it is not allowed for parameters or properties). It means "an instance of the called class". It documents the contract, is enforced at runtime, and lets static analysers and IDEs infer `User` from `User::create()` without a docblock. PHP 8.5 also allows a `final` subclass to narrow an inherited `: static` return type to `self` or to its own class name when it overrides the method. ## What breaks `new static` 1. **Constructor signatures are not inherited contracts.** PHP checks that an overriding method is compatible with its parent, but it skips that check for constructors unless the parent constructor is `abstract` or declared in an interface. A subclass can therefore write `__construct(private string $title)`. `new static($attributes)` then passes the wrong arguments, and PHP throws `ArgumentCountError` ("Too few arguments to function Post::__construct() …") or a `TypeError`. 2. **An abstract base.** If `Model` is abstract, `Model::create()` makes `Model` the called class and `new static` throws `Error: Cannot instantiate abstract class Model`. Usually that is the right outcome; the factory is meant to be called on concrete subclasses. 3. **Abstract static hooks.** A base class often reads subclass details through `static::`, for example `static::table()`. If `table()` is declared `abstract public static` in the base and someone calls it on the base, PHP throws `Error: Cannot call abstract method Model::table()`. ## Making the constructor safe There are three ways to guarantee every subclass accepts what the factory passes: - **Declare the base constructor `final`.** Subclasses cannot override it, so `new static($attributes)` always matches. Subclass-specific setup moves into a protected hook the constructor calls. - **Declare it `abstract`, or in an interface the base implements.** PHP then applies the normal signature-compatibility check to subclass constructors. - **Make the class `final`** when it needs no subclasses at all. Then `static` and `self` name the same class and `new static` cannot meet a foreign constructor. PHPStan reports `Unsafe usage of new static()` for exactly this risk and stays quiet when the class is final, the constructor is final or abstract, or the constructor comes from an interface. ## Why not override `create()` in every subclass The alternative to late static binding is to write `create()` again in each subclass with `return new User(...)`. It works, but it multiplies identical code, and every new model must remember to add it. Worse, a subclass that forgets inherits the parent's version and quietly returns the wrong type unless a `: static` return type catches it. One `new static` in the base removes the duplication, and the return type turns a forgotten detail into an immediate `TypeError` instead of a subtle bug. ## Putting it together A robust version of the pattern: - the base constructor is `final` (or abstract with a fixed signature); - `create()` returns `new static(...)` and declares `: static`; - per-class details come from `static::` members such as a table-name method or constant; - the base class is `abstract`, so it cannot be instantiated by accident. The same technique powers fluent "with-er" methods that return `static` on immutable value objects, so a subclass's `withX()` keeps returning the subclass. Whether a class should have a single shared instance (the singleton pattern) is a separate design question; this pattern creates a new object on every call.

  • In PHP, why does PHP allow a subclass constructor to take different parameters from its parent's?
    Constructors are exempt from the method signature-compatibility check unless the parent constructor is abstract or declared in an interface. Construction is treated as class-specific, so a subclass may add required parameters. That freedom is what makes `new static()` in a parent unsafe unless the constructor is pinned.
  • In PHP, what happens if a method declared : static returns new self() and is called on a subclass?
    The return type check fails: PHP throws `TypeError` with a message such as `Model::create(): Return value must be of type User, Model returned`. The `static` return type is enforced at runtime against the called class.
  • In PHP 8.5, can a final subclass override a method declared : static with a return type of self?
    Yes. PHP 8.5 allows a final subclass to substitute `static` with `self` or its concrete class name, because no further subclass can exist for `static` to mean. Earlier versions rejected that narrowing as incompatible.

saying these in an interview costs you the question

  • Uses new self() in a base factory and expects the subclass back
  • Assumes PHP enforces the parent constructor's signature on every subclass
  • Believes the static return type is only a docblock hint with no runtime check
  • Thinks new static() creates a shared single instance per class
  • Calls the factory on the abstract base and expects a usable object