PHP 8.0: Class magic method signatures are strictly enforced

Version8.0
TypeChange

Magic methods in PHP are special class method names that, if declared, brings special functionality to the class.

There are several magic methods in PHP. __constuct() magic method is called when a new class object is instantiated with new Foo() pattern, the __get() magic method is called when non-existing class property is requested, __toString() magic method is called when a class object is coerced to a string, and so on.

Although all these magic methods carried a semantic meaning, they were not programmatically enforced until PHP 8.0. The __toString() method, for example, semantically expects to return a string, but it was possible to return any data type, including ones that cannot be coerced to strings.

class Foo {
    public function __toString(): object {
    }
}

In PHP 8.0 and later, if a magic method is declared, it must adhere to the function signature and return type if declared too.

A few signature checks are enforced for return types since their introduction in PHP 7.0. Namely, __construct, __destruct, and __clone are were not allowed to declare a return type at all, not even void (void was added in PHP 7.1).

Types are optional

Unlike a PHP class interface, that enforces the parameter and return types to be present in all implementations, magic method enforcement allows to not declare a type at all.

This allows existing classes that implement magic methods to continue to run without having to declare explicit types.

However, if a type is declared, it must follow the signature.

Fatal Errors on Signature Mismatches

PHP 8.0 throws fatal errors when interface implementations and class inheritance violates LSP. Magic methods follow the same pattern.

  • Return types are allowed to narrow the return type.
  • Method parameters are allowed to widen the parameter type.

Variance is allowed

Similar to standard class/interface inheritance, magic methods variance is allowed too.

For example, a __get() method which should return a mixed type can declare that its return type is bool because bool is already included in the mixed type.

Further, the __set() magic method can widen its method parameter from __set(string $name, $value) to __set(string|CacheEntry $name, $value) if the function can handle CacheEntry objects too.

Magic Method Signatures

class GoodFoo {

    public function __isset(string $name): bool {}
    public function __get(string $name): mixed {}
    public function __set(string $name, mixed $value): void {}
    public function __unset(string $name): void {}
    public function __set_state(array $properties): object {}

    public function __call(string $name, array $arguments): mixed {}
    public function __callStatic(string $name, array $arguments): mixed {}
    public function __invoke(mixed $arguments): mixed {}

    public function __clone(): void {}

    public function __serialize(): array {}
    public function __unserialize(array $data): void {}

    public function __sleep(): array {}
    public function __wakeup(): void {}

    public function __debugInfo(): ?array {}
}

Any magic method implementation must take all parameters in their signature, and if declares a return type, it must be of the same type of a sub type too.

Any declarations otherwise triggers a fatal error:

class BadFoo {
    public function __isset(string $name): array {}
    //                            mismatch ^^^^^ 
}

// Fatal error: BadFoo::__isset(): Return type must be bool when declared in ... on line ...
class BadBar {
    public function __call(array $name, array $arguments): mixed {}
    //           mismatch  ^^^^^
}

// Fatal error: BadBar::__call(): Parameter #1 ($name) must be of type string when declared in /in/JpKPd on line 4

Magic methods with no return types allowed

Magic methods that did not allow declaring any return type prior to PHP 8.0 continue to enforce this.

class BadBar {
    public function __construct(): void {}
    public function __destruct(): void {}
}

Neither of the declarations are allowed in PHP 8.0 and prior versions. They result in a fatal error:

Fatal error: Method BadBar::__construct() cannot declare a return type in ... on line ...
Fatal error: Method BadBar::__destruct() cannot declare a return type in ... on line ...

Stringable interface

PHP 8.0 introduces a new Stringable that is added automatically to any class that implements __toString magic methods. If the Stringable interface is explicitly declared (e.g Foo implements Stringable), the __toString() method signature is enforced with interface rules.

private Magic Methods

PHP 8.0 relaxes signature enforcement when a private method is re-declared in a child class. Magic methods must follow the signature even if they are declared private.

class Foo {
    private function __get(): mixed {}
}
// Fatal error: Method Foo::__get() must take exactly 1 argument in ... on line ...

Backwards Compatibility Impact

This is backwards-compatibility breaking change. However, the enforced signatures are semantically correct, and allows variance, mitigates potential compatibility issues.

  • If there are no types enforced, the their types will not be checked. However, the methods must accept same number of parameters.
  • mixed type is includes all types in PHP except void, which makes any return type fulfills return type signature.

Related Changes


RFC Discussion Implementation