skip to content

In PHP, when do __call and __callStatic run, and which one handles self::missing() inside an instance method?

level: middleimportance: should knowfreq 52%

answer

  1. inaccessible method, not only missing
  2. __callStatic must itself be static
  3. arguments arrive as one array
  4. static-style call with $this: __call wins
  5. method_exists false, is_callable true

basics

~20 s

__call runs when an inaccessible method is called on an object; __callStatic when one is called statically, like Settings::missing(). A static-style call such as self::missing() from inside an instance method goes to __call, because an object is available.

solid answer

~40 s

Both intercept calls to **inaccessible** methods: undeclared ones, or private/protected ones called from outside. `__call(string $name, array $arguments)` handles object calls like `$settings->getTimeout()`; `__callStatic` must be declared `static` and handles `Settings::fromEnv()` when no such method is visible. The arguments arrive as one array; named arguments keep their names as string keys. A subtle rule: a static-style call (`self::missing()`, `static::missing()` or `Settings::missing()`) made from inside an instance method of that class goes to `__call`, not `__callStatic`, because `$this` is available. Without either method, PHP throws `Error: Call to undefined method`. Tooling sees none of this: `method_exists()` returns `false` for magic methods, while `is_callable([$obj, 'anything'])` returns `true` once `__call` exists.

code

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

final class Settings
{
    public function __construct(private array $values) {}

    public function __call(string $name, array $arguments): mixed
    {
        if (str_starts_with($name, 'get')) {
            $key = lcfirst(substr($name, 3));
            if (array_key_exists($key, $this->values)) {
                return $this->values[$key];
            }
        }
        throw new BadMethodCallException("Unknown method: $name");
    }

}

$s = new Settings(['timeout' => 30]);
echo $s->getTimeout(), PHP_EOL;                      // 30
var_dump(method_exists($s, 'getTimeout'));          // bool(false)
var_dump(is_callable([$s, 'getTimeout']));          // bool(true)

go deeper

for a junior

Recall that __call handles unknown object method calls and __callStatic handles unknown static calls.

for a middle

Explain the inaccessible rule, the arguments array with named keys, and why a static-style call with $this goes to __call.

for a senior

Keep magic call surfaces narrow: validate names, throw BadMethodCallException, and document them so tools and reviewers can follow them.

for a principal

Judge whether a framework-style magic API is worth the lost discoverability, and where explicit interfaces should replace it.

## Two hooks for method calls PHP lets a class intercept calls to methods it does not expose: | Hook | Triggered by | Declaration | |---|---|---| | `__call` | `$obj->name(...)` for an inaccessible method | `public function __call(string $name, array $arguments): mixed` | | `__callStatic` | `Klass::name(...)` for an inaccessible method, in static context | `public static function __callStatic(string $name, array $arguments): mixed` | `__call` must not be static and `__callStatic` must be; declaring either the wrong way is a compile error in PHP 8. Both should be public; a narrower visibility draws a warning. **Inaccessible** has the same meaning as for properties: the method is not declared, or it is declared `private`/`protected` and called from a scope that cannot see it. The second case surprises people: calling a private method from outside a class that has `__call` does not raise "Call to private method"; it silently routes to `__call`. ## What the hook receives - `$name` is the method name as the caller wrote it. PHP method names are case-insensitive, but `$name` preserves the caller's spelling, so compare it deliberately. - `$arguments` is an array of the passed values. Positional arguments get integer keys; named arguments such as `$obj->find(id: 5)` arrive with string keys, which is why forwarding with `...$arguments` keeps the names intact. - Whatever the hook returns becomes the call's result. ## Which hook runs for a static-style call The rule people get wrong: 1. `Settings::fromEnv()` called from outside any `Settings` instance goes to `__callStatic`. 2. `self::fromEnv()`, `static::fromEnv()` or `Settings::fromEnv()` called from **inside an instance method**, where `$this` is an instance of `Settings`, goes to `__call`, if the class defines one. The engine prefers the object-context hook whenever an object is available. 3. If only `__callStatic` exists, it handles the call in both situations. So a class that defines both hooks with different behaviour can take the "wrong" branch when refactored code moves a static-style call into an instance method. ## What callers and tools see Magic methods do not exist as methods: - `method_exists($obj, 'getTimeout')` returns `false`; the manual states it cannot detect methods reached through `__call`. - `is_callable([$obj, 'anything'])` returns `true` for every name once the class defines `__call`, and a class name is callable for any static name once it defines `__callStatic`. - Reflection lists no such method and IDEs cannot autocomplete it. Static analysers either accept any name on a class with `__call` and type the result as `mixed`, or, in stricter settings, report it as undefined; `@method` docblock tags give them the real signatures. ## Typical uses - **Forwarding** every unknown call to a wrapped object, for example to add logging around a client library. - **Name-derived methods**, such as a settings object answering `getTimeout()` by looking up `timeout`. - **Static entry points** that resolve an instance and forward to it, a style some frameworks use. Each use trades explicit API for brevity. A good `__call` validates `$name` against a known list and throws `BadMethodCallException` for anything else; otherwise a typo becomes a silent no-op or returns `null`. ## Designing a safe `__call` A catch-all method hook turns every typo into "valid" code, so the discipline has to live inside the hook: 1. **Whitelist names.** Map known method names (or a strict prefix plus a known key) to behaviour, and throw `BadMethodCallException` for everything else, so `$settings->getTimout()` fails at the call. 2. **Keep it narrow in base classes.** A `__call` in a parent class also catches every misspelt call on every subclass, and hides private methods that outside code should not reach. 3. **Do not use it on hot paths.** Each magic call costs an extra method dispatch and array packing of the arguments; a declared method is cheaper and visible to every tool. 4. **Document the surface** with `@method` docblock tags so IDEs, analysers and reviewers can see which names exist. 5. **Prefer explicit methods** once the set of names stops changing; magic is a tool for open-ended name sets, not for saving a few lines. ## Summary - `__call` for object calls, `__callStatic` for static calls, both only for inaccessible methods. - A static-style call with `$this` available goes to `__call`. - Arguments arrive as one array, with string keys for named arguments. - Tooling cannot see magic methods; document or replace them.

  • In PHP, what happens when outside code calls a private method on an object whose class defines __call?
    The private method is inaccessible from that scope, so PHP routes the call to `__call` instead of throwing `Call to private method`. That can hide a visibility mistake, which is one reason a `__call` should reject names it does not recognise.
  • In PHP 8, how do named arguments reach __call?
    They land in the `$arguments` array under string keys, next to integer keys for positional arguments. Forwarding with `$target->$name(...$arguments)` therefore passes them on as named arguments.

saying these in an interview costs you the question

  • Says __call only runs for methods that are not declared at all
  • Declares __callStatic as an instance method
  • Expects self::missing() inside an instance method to reach __callStatic when __call exists
  • Uses method_exists() to detect methods served by __call
  • Lets __call return null for unknown names instead of throwing