基礎

PHPの__callメソッド|未定義メソッドの呼び出しをキャッチする

PHPの__call()は、アクセスできないメソッド(未定義またはprivate/protected)がオブジェクトに対して呼び出されたときに自動的に実行されるマジックメソッドです。メソッド名と引数を受け取り、動的な処理の振り分けに使えます。

メソッドの委譲(デリゲーション)、動的なメソッド生成、メソッドチェーンの自動生成など、柔軟なAPIを設計する際に活用されます。

基本的な使い方

PHP
<?php
class MagicMethod {
    public function __call(string $name, array $arguments): mixed {
        echo "メソッド '{$name}' が呼ばれました\n";
        echo "引数: " . implode(", ", $arguments) . "\n";
        return null;
    }
}

$obj = new MagicMethod();
$obj->hello("田中", "佐藤");
$obj->calculate(10, 20, 30);
実行結果
メソッド 'hello' が呼ばれました
引数: 田中, 佐藤
メソッド 'calculate' が呼ばれました
引数: 10, 20, 30

定義されていないメソッドを呼んでもエラーにならず、__call() がメソッド名と引数を受け取って処理します。

動的なgetter/setter

__call() を使って、getName()setName() のようなgetter/setterを動的に生成できます。

PHP
<?php
class FlexibleEntity {
    private array $data = [];

    public function __call(string $name, array $args): mixed {
        // getXxx() パターン
        if (str_starts_with($name, "get")) {
            $prop = lcfirst(substr($name, 3));
            return $this->data[$prop] ?? null;
        }

        // setXxx() パターン
        if (str_starts_with($name, "set") && count($args) === 1) {
            $prop = lcfirst(substr($name, 3));
            $this->data[$prop] = $args[0];
            return $this;  // メソッドチェーン対応
        }

        throw new BadMethodCallException("未定義のメソッド: {$name}");
    }
}

$user = new FlexibleEntity();
$user->setName("田中")
     ->setAge(30)
     ->setEmail("tanaka@example.com");

echo "名前: " . $user->getName() . "\n";
echo "年齢: " . $user->getAge() . "\n";
echo "メール: " . $user->getEmail() . "\n";
実行結果
名前: 田中
年齢: 30
メール: tanaka@example.com

メソッドの委譲(デリゲーション)

他のオブジェクトへメソッド呼び出しを転送するパターンです。

PHP
<?php
class Logger {
    public function info(string $msg): void { echo "[INFO] {$msg}\n"; }
    public function warn(string $msg): void { echo "[WARN] {$msg}\n"; }
    public function error(string $msg): void { echo "[ERROR] {$msg}\n"; }
}

class Application {
    private Logger $logger;

    public function __construct() {
        $this->logger = new Logger();
    }

    // Loggerのメソッドを委譲
    public function __call(string $name, array $args): mixed {
        if (method_exists($this->logger, $name)) {
            return $this->logger->$name(...$args);
        }
        throw new BadMethodCallException("メソッド {$name} は存在しません");
    }

    public function run(): void {
        $this->info("アプリケーション開始");
        $this->warn("設定ファイルが見つかりません。デフォルトを使用");
        $this->info("処理完了");
    }
}

$app = new Application();
$app->run();
実行結果
[INFO] アプリケーション開始
[WARN] 設定ファイルが見つかりません。デフォルトを使用
[INFO] 処理完了

実用的な例:クエリビルダー

PHP
<?php
class QueryBuilder {
    private string $table = "";
    private array $wheres = [];
    private ?int $limitVal = null;

    // whereXxx() を動的に処理
    public function __call(string $name, array $args): self {
        if (str_starts_with($name, "where") && count($args) === 1) {
            $column = strtolower(preg_replace('/[A-Z]/', '_$0', lcfirst(substr($name, 5))));
            $this->wheres[] = "{$column} = '{$args[0]}'";
            return $this;
        }
        throw new BadMethodCallException("Unknown method: {$name}");
    }

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

    public function limit(int $n): self {
        $this->limitVal = $n;
        return $this;
    }

    public function toSQL(): string {
        $sql = "SELECT * FROM {$this->table}";
        if ($this->wheres) {
            $sql .= " WHERE " . implode(" AND ", $this->wheres);
        }
        if ($this->limitVal) {
            $sql .= " LIMIT {$this->limitVal}";
        }
        return $sql;
    }
}

$sql = (new QueryBuilder())
    ->from("users")
    ->whereStatus("active")
    ->whereRole("admin")
    ->limit(10)
    ->toSQL();

echo $sql . "\n";
実行結果
SELECT * FROM users WHERE status = 'active' AND role = 'admin' LIMIT 10
ポイント

__call() はインスタンスメソッドの呼び出しに対して動作します。静的メソッドの呼び出しには __callStatic() を使います。IDEの補完が効かなくなるため、@method PHPDocアノテーションで仮想メソッドを宣言しておくと便利です。

注意

__call() を多用するとコードの追跡が困難になり、IDEの支援も受けにくくなります。明確なインターフェースを持つクラスの方が保守しやすいため、本当に動的なメソッド名が必要な場面に限定して使用しましょう。

まとめ

  • __call() はアクセスできないメソッドが呼ばれたときに自動実行される
  • メソッド名と引数の配列を受け取り、動的な処理の振り分けができる
  • 動的なgetter/setter、メソッド委譲、クエリビルダーなどに活用できる
  • IDEの補完が効かなくなるため @method PHPDocの活用が推奨される
  • 過度な使用は保守性を下げるため、必要な場面に限定する