0

Magic methods: __call, __callStatic (method overloading)

Advanced5 min read·php-07-013
interviewlaravel-src

Concept

Method overloading magic methods intercept calls to inaccessible methods — methods that don't exist, or exist but are private/protected and called from outside. They enable dynamic dispatch and proxy patterns.

__call($name, $args): Called when an undefined or inaccessible instance method is called. $args is an array of all passed arguments. Returns whatever value the method call should evaluate to.

__callStatic($name, $args): Same, but for static method calls (ClassName::undefinedMethod(...)).

Laravel Facades use __callStatic to forward static calls to the underlying service container instance. When you call Cache::get('key'), it triggers __callStatic('get', ['key']) on the Cache facade, which resolves the CacheInterface implementation from the container and calls get on it.

Macros in Laravel also use __call: any class using the Macroable trait registers user-defined callables by name, and __call / __callStatic look up and invoke them. This lets packages add methods to Laravel's core classes without modifying their source.

Proxy pattern: A class wrapping another class can forward all method calls to the wrapped object using __call with call_user_func_array, transparently proxying behavior (adding logging, timing, auth checks) without knowing the wrapped class's full interface.

Code Example

php
<?php
declare(strict_types=1);

// Fluent query builder via __call
class QueryBuilder
{
    private array $clauses = [];
    private string $table  = '';

    public function table(string $t): static
    {
        $this->table = $t;
        return $this;
    }

    // Dynamic where methods: whereStatus(), whereName(), whereEmail()...
    public function __call(string $name, array $args): static
    {
        if (str_starts_with($name, 'where')) {
            $column = lcfirst(substr($name, 5)); // "whereStatus" → "status"
            $this->clauses[] = "$column = ?";
        }
        return $this;
    }

    // Static: QueryBuilder::forTable('users')
    public static function __callStatic(string $name, array $args): static
    {
        if (str_starts_with($name, 'for')) {
            $table = strtolower(substr($name, 3)); // "forUsers" → "users"
            return (new static())->table($table);
        }
        throw new \BadMethodCallException("Unknown static method: $name");
    }
}

$query = QueryBuilder::forUsers()  // __callStatic
    ->whereStatus('active')        // __call
    ->whereName('Alice');          // __call

// Proxy / decorator via __call
class TimingProxy
{
    public function __construct(private object $target) {}

    public function __call(string $name, array $args): mixed
    {
        $start  = hrtime(true);
        $result = $this->target->$name(...$args);
        $ns     = (hrtime(true) - $start) / 1_000_000;
        printf("%s() took %.2fms\n", $name, $ns);
        return $result;
    }
}

class SlowService
{
    public function process(string $data): string
    {
        usleep(10_000); // simulate work
        return strtoupper($data);
    }
}

$timed = new TimingProxy(new SlowService());
echo $timed->process('hello'); // "HELLO", prints timing

// Laravel Macroable trait pattern
trait Macroable
{
    private static array $macros = [];

    public static function macro(string $name, callable $fn): void
    {
        static::$macros[$name] = $fn;
    }

    public function __call(string $name, array $args): mixed
    {
        if (!isset(static::$macros[$name])) {
            throw new \BadMethodCallException("Method $name not found");
        }
        return (static::$macros[$name])(...$args);
    }
}