[PHP]__callStatic()とは?未定義の静的メソッド呼び出しを動的に処理する方法を徹底解説

PHP

はじめに

前回の記事では、インスタンスメソッドとして未定義のメソッドが呼び出された際に動的に処理する __call() を解説しました。今回取り上げる __callStatic() は、その静的メソッド版にあたるマジックメソッドです。

ClassName::undefinedMethod() のように、クラス名を直接指定して静的にメソッドを呼び出した際、そのメソッドがクラスに定義されていない場合、通常は致命的なエラーになります。しかし、クラスに __callStatic() が定義されていると、その呼び出しが自動的に __callStatic() に転送されます。この仕組みは、LaravelのFacade(ファサード)やEloquentのモデルクラスなど、多くの人気フレームワークが提供する、簡潔で読みやすい静的インターフェースの土台となっています。本記事では基本的な使い方から実践的な活用例まで詳しく解説します。


メソッド概要

項目内容
メソッド名__callStatic()
種別マジックメソッド(静的メソッドとして定義する必要がある)
シグネチャpublic static function __callStatic(string $name, array $arguments): mixed
引数1$name — 呼び出された静的メソッド名
引数2$arguments — 呼び出し時に渡された引数の配列
呼び出しタイミングアクセス可能な範囲内に定義がない静的メソッドが呼び出された時
戻り値任意(呼び出し元に返したい値を返す)
対応バージョンPHP 5.3.0以降
関連するマジックメソッド__call()(インスタンスメソッド版)

__call()との使い分け(イメージ図)

  【インスタンスメソッドとして呼び出した場合】
  $obj->undefinedMethod('arg');
              │
              ▼
      __call('undefinedMethod', ['arg'])  が呼ばれる
      (前回記事)


  【静的メソッドとして呼び出した場合】
  ClassName::undefinedMethod('arg');
              │
              ▼
      __callStatic('undefinedMethod', ['arg'])  が呼ばれる
      ★この記事の対象
      ※ static function として定義する必要がある

ポイントは、呼び出し方(-> か :: か)によって、__call() と __callStatic() のどちらが発動するかが決まる点です。両方を定義しておけば、インスタンス経由でも静的経由でも、未定義メソッド呼び出しを動的に処理できます。ただし、__callStatic() はその名の通り静的コンテキストで実行されるため、$this を使えない点に注意が必要です。


実践サンプル7選

例1:基本的な使い方

<?php

class BasicStaticCallDemo
{
    public static function __callStatic(string $name, array $arguments): mixed
    {
        // 呼び出された静的メソッド名と引数を確認する
        $argsString = implode(', ', $arguments);
        return "静的メソッド '{$name}' が引数({$argsString})で呼ばれました";
    }
}

echo BasicStaticCallDemo::doSomething('a', 'b') . PHP_EOL;
echo BasicStaticCallDemo::anotherMethod(123) . PHP_EOL;

例2:Facadeパターンによる静的インターフェースの提供

<?php

class RealCache
{
    private array $store = [];

    public function get(string $key): mixed
    {
        return $this->store[$key] ?? null;
    }

    public function set(string $key, mixed $value): void
    {
        $this->store[$key] = $value;
    }
}

class Cache
{
    private static ?RealCache $instance = null;

    /**
     * 静的なCache::get()のような呼び出しを、
     * 内部の実インスタンスのメソッドに転送する(Facadeパターン)
     */
    public static function __callStatic(string $name, array $arguments): mixed
    {
        self::$instance ??= new RealCache();
        return self::$instance->$name(...$arguments);
    }
}

Cache::set('greeting', 'こんにちは');
echo Cache::get('greeting') . PHP_EOL;

例3:名前付きコンストラクタ(ファクトリ)を動的に生成するクラス

<?php

class Color
{
    private function __construct(public readonly string $hex)
    {
    }

    /**
     * Color::red() / Color::blue() のような呼び出しを、
     * 対応する色コードのインスタンス生成に動的に変換する
     */
    public static function __callStatic(string $name, array $arguments): self
    {
        $palette = [
            'red'   => '#FF0000',
            'green' => '#00FF00',
            'blue'  => '#0000FF',
        ];

        if (!isset($palette[$name])) {
            throw new BadMethodCallException("色 '{$name}' は定義されていません");
        }

        return new self($palette[$name]);
    }
}

$red = Color::red();
$blue = Color::blue();
echo "{$red->hex} / {$blue->hex}" . PHP_EOL;

例4:静的なバリデーターを動的に生成するクラス

<?php

class Validate
{
    /**
     * Validate::isEmail() / Validate::isNumeric() のような
     * "isXxx" 形式の静的呼び出しを、対応する検証処理に変換する
     */
    public static function __callStatic(string $name, array $arguments): bool
    {
        $value = $arguments[0] ?? null;

        return match ($name) {
            'isEmail'   => filter_var($value, FILTER_VALIDATE_EMAIL) !== false,
            'isNumeric' => is_numeric($value),
            'isUrl'     => filter_var($value, FILTER_VALIDATE_URL) !== false,
            default     => throw new BadMethodCallException("検証ルール '{$name}' は存在しません"),
        };
    }
}

var_dump(Validate::isEmail('test@example.com'));
var_dump(Validate::isNumeric('abc'));

例5:__call()と__callStatic()を両方定義して呼び出し方の違いを吸収する

<?php

class FlexibleDispatcher
{
    private function handle(string $name, array $arguments): string
    {
        return "処理: {$name}(" . implode(', ', $arguments) . ")";
    }

    /**
     * インスタンス経由の呼び出し($obj->method())
     */
    public function __call(string $name, array $arguments): string
    {
        return '[インスタンス] ' . $this->handle($name, $arguments);
    }

    /**
     * 静的経由の呼び出し(ClassName::method())
     */
    public static function __callStatic(string $name, array $arguments): string
    {
        $instance = new self();
        return '[静的] ' . $instance->handle($name, $arguments);
    }
}

$dispatcher = new FlexibleDispatcher();
echo $dispatcher->anything('x') . PHP_EOL;
echo FlexibleDispatcher::anything('x') . PHP_EOL;

例6:サービスロケーターとしての簡易実装

<?php

class Config
{
    private static array $values = [
        'app_name' => 'MyApp',
        'timezone' => 'Asia/Tokyo',
    ];

    /**
     * Config::appName() のようなキャメルケースの静的呼び出しを、
     * スネークケースの設定キー("app_name")に変換して値を取得する
     */
    public static function __callStatic(string $name, array $arguments): mixed
    {
        $key = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $name));
        return self::$values[$key] ?? ($arguments[0] ?? null);
    }
}

echo Config::appName() . PHP_EOL;
echo Config::timezone() . PHP_EOL;
echo Config::nonExistent('デフォルト値') . PHP_EOL;

例7:static::を使った遅延静的束縛との組み合わせ

<?php

abstract class BaseModel
{
    protected static string $table = '';

    /**
     * 子クラスごとに異なるテーブル名を参照しつつ、
     * "findByXxx" 形式の静的呼び出しを動的に処理する
     */
    public static function __callStatic(string $name, array $arguments): string
    {
        if (str_starts_with($name, 'findBy')) {
            $field = strtolower(substr($name, 6));
            return sprintf(
                "SELECT * FROM %s WHERE %s = '%s'",
                static::$table,
                $field,
                $arguments[0]
            );
        }

        throw new BadMethodCallException("メソッド '{$name}' は存在しません");
    }
}

class User extends BaseModel
{
    protected static string $table = 'users';
}

class Product extends BaseModel
{
    protected static string $table = 'products';
}

echo User::findByEmail('taro@example.com') . PHP_EOL;
echo Product::findByName('ノート') . PHP_EOL;

関連機能との比較

機能役割__callStatic()との違い
__callStatic()未定義の静的メソッド呼び出しを処理本記事の対象。ClassName::method()形式の呼び出しが対象
__call()未定義のインスタンスメソッド呼び出しを処理$obj->method()形式の呼び出しが対象(前回記事を参照)
__get() / __set()未定義プロパティへのアクセスを処理メソッド呼び出しではなく、プロパティアクセスを対象とする
staticファクトリーメソッド明示的に定義された静的メソッドで生成を制御__callStatic()は名前を事前に列挙せず、動的にメソッド名を解釈する点が異なる
Facadeパターン(Laravel等)静的インターフェースを通じてサービスを提供__callStatic()を内部実装として利用する、設計パターンとしての応用例

よくある落とし穴(注意点)

  1. staticキーワードをつけ忘れると致命的エラーになる __callStatic() は必ず public static function として定義する必要があります。static を付け忘れると、警告またはエラーが発生します。
  2. $thisが使えない(静的コンテキスト) __callStatic() は静的メソッドであるため、メソッド内で $this を参照することはできません。インスタンスの状態にアクセスしたい場合は、静的プロパティや、メソッド内で生成したインスタンス経由で操作する必要があります(例2・例5を参照)。
  3. IDEの補完やコード解析ツールが効きにくくなる __call() と同様、動的に処理されるメソッドはIDEが認識できません。PHPDocの @method static アノテーションを使って、補完情報を提供する工夫が推奨されます。
  4. 静的なグローバル状態を持ちやすく、テストが困難になる Facadeパターンのように、__callStatic() の内部で静的なインスタンスを保持する設計は、テスト時のモック化や状態のリセットが難しくなる傾向があります。依存性注入との使い分けを意識しましょう。
  5. 存在しないメソッド呼び出しをサイレントに無視しない __callStatic() 内でエラーハンドリングを行わないと、タイプミスなどのバグが静かに失敗してしまいます。想定外のメソッド名には BadMethodCallException をスローする設計が望ましいです(例3・例4を参照)。

まとめ

観点まとめ
何をするメソッドかクラスに定義されていない静的メソッドが呼び出された際に動的に処理する
主な用途Facadeパターン、動的なファクトリメソッド、名前規則ベースの静的ヘルパーの提供
定義の必須要件public static functionとして定義する必要がある
対になるメソッド__call()(インスタンスメソッド版)
注意点$thisが使えないこと、IDE補完の効きにくさ、静的状態によるテストの難しさ

__callStatic() は、LaravelのFacadeに代表されるような、簡潔で読みやすい静的インターフェースを提供するための強力な仕組みです。__call() と組み合わせて使うことで、インスタンス経由・静的経由のどちらの呼び出しにも柔軟に対応できますが、テスト容易性や保守性とのバランスを意識して、適切な場面で活用しましょう。

タイトルとURLをコピーしました