はじめに
前回の記事では、オブジェクトを文字列として扱う __toString() を解説しました。今回取り上げる __invoke() は、それとよく似た発想で、オブジェクトを関数のように「呼び出し可能(callable)」にするマジックメソッドです。
$obj() のように、変数名の直後に括弧を付けてオブジェクトを呼び出すと、通常は「Object of class X is not callable」というエラーになります。しかし、クラスに __invoke() が定義されていると、この呼び出しが自動的に __invoke() に転送されます。この仕組みは、array_map() などのコールバックを受け取る関数に、状態を持った「関数オブジェクト」を渡したい場合や、ミドルウェアパターンの実装などで頻繁に活用されます。本記事では基本的な使い方から、実践的な活用例まで詳しく解説します。
メソッド概要
| 項目 | 内容 |
|---|---|
| メソッド名 | __invoke() |
| 種別 | マジックメソッド |
| シグネチャ | public function __invoke(...$args): mixed |
| 引数 | 呼び出し時に渡された任意の引数(可変長) |
| 呼び出しタイミング | オブジェクトが $obj(...) の形式で呼び出された時 |
| 戻り値 | 任意(呼び出し元に返したい値) |
| 対応バージョン | PHP 5.3.0以降 |
| 判定関数 | is_callable() が __invoke() を持つオブジェクトに対して true を返す |
| 用途の分類 | 「関数オブジェクト(Functor)」や「呼び出し可能なオブジェクト」の実現 |
呼び出しの仕組み(イメージ図)
$multiplier = new Multiplier(3);
$multiplier(5);
│
▼
┌───────────────────────────┐
│ PHPが$multiplierの直後の()を検知し、 │
│ オブジェクトの呼び出しとして解釈する │
└───────────┬───────────────┘
▼
__invoke(5) が自動的に呼ばれる
★この記事の対象
│
▼
戻り値がそのまま呼び出し結果となる
(この例では 5 * 3 = 15 のような計算結果)
ポイントは、__invoke() を持つオブジェクトが通常の関数やクロージャと同じように callable 型として扱えるという点です。これにより、オブジェクトの持つ状態(プロパティ)を活かしながら、関数のように振る舞わせることができます。
実践サンプル7選
例1:基本的な使い方
<?php
class Multiplier
{
public function __construct(private int $factor)
{
}
public function __invoke(int $value): int
{
return $value * $this->factor;
}
}
$triple = new Multiplier(3);
echo $triple(5) . PHP_EOL; // 15(関数のように呼び出せる)
echo $triple(10) . PHP_EOL; // 30
例2:array_map()などのコールバックとして使う
<?php
class CurrencyFormatter
{
public function __construct(private string $currency)
{
}
/**
* __invoke()を持つオブジェクトは、
* array_map()などのcallable引数にそのまま渡せる
*/
public function __invoke(int $amount): string
{
return number_format($amount) . ' ' . $this->currency;
}
}
$formatter = new CurrencyFormatter('JPY');
$prices = [1000, 25000, 300000];
$formatted = array_map($formatter, $prices);
print_r($formatted);
例3:ミドルウェアパターンの実装
<?php
class LoggingMiddleware
{
/**
* __invoke()を使うことで、ミドルウェアを
* シンプルな関数のように連鎖させて呼び出せる
*/
public function __invoke(string $request, callable $next): string
{
echo "[LOG] リクエスト受信: {$request}" . PHP_EOL;
$response = $next($request);
echo "[LOG] レスポンス送信: {$response}" . PHP_EOL;
return $response;
}
}
$middleware = new LoggingMiddleware();
$result = $middleware('GET /users', function (string $req) {
return "200 OK ({$req})";
});
echo $result . PHP_EOL;
例4:バリデーションルールを関数オブジェクトとして表現する
<?php
class RangeValidator
{
public function __construct(
private int $min,
private int $max
) {
}
public function __invoke(mixed $value): bool
{
return is_numeric($value) && $value >= $this->min && $value <= $this->max;
}
}
$ageValidator = new RangeValidator(0, 120);
var_dump($ageValidator(30)); // true
var_dump($ageValidator(150)); // false
// array_filter()と組み合わせて、条件に合う値だけを抽出する例
$values = [10, 50, 150, -5, 80];
$validValues = array_filter($values, $ageValidator);
print_r($validValues);
例5:キャッシュ付きの計算をカプセル化する関数オブジェクト
<?php
class MemoizedFibonacci
{
private array $cache = [];
/**
* 内部に状態(キャッシュ)を持ちながら、
* 関数のように呼び出せるオブジェクトを作る
* (通常の関数ではこうした状態の保持は難しい)
*/
public function __invoke(int $n): int
{
if (isset($this->cache[$n])) {
return $this->cache[$n];
}
$result = $n <= 1 ? $n : $this($n - 1) + $this($n - 2);
$this->cache[$n] = $result;
return $result;
}
}
$fib = new MemoizedFibonacci();
echo $fib(10) . PHP_EOL; // 55
echo $fib(20) . PHP_EOL; // 6765(内部でキャッシュが効いている)
例6:is_callable()とcallable型宣言での扱いを確認するデモ
<?php
class SimpleCallable
{
public function __invoke(): string
{
return '呼び出されました';
}
}
function executeCallback(callable $callback): string
{
// __invoke()を持つオブジェクトは、callable型の引数として受け取れる
return $callback();
}
$obj = new SimpleCallable();
var_dump(is_callable($obj)); // true
echo executeCallback($obj) . PHP_EOL;
echo executeCallback(fn () => 'クロージャも同様に動作する') . PHP_EOL;
例7:イベントリスナーを表現する関数オブジェクト
<?php
class EventLogger
{
private array $log = [];
public function __invoke(string $eventName, array $payload): void
{
$this->log[] = [
'event' => $eventName,
'payload' => $payload,
'time' => date('H:i:s'),
];
}
public function getLog(): array
{
return $this->log;
}
}
class SimpleEventDispatcher
{
private array $listeners = [];
public function listen(string $eventName, callable $listener): void
{
$this->listeners[$eventName][] = $listener;
}
public function dispatch(string $eventName, array $payload = []): void
{
foreach ($this->listeners[$eventName] ?? [] as $listener) {
$listener($eventName, $payload);
}
}
}
$logger = new EventLogger();
$dispatcher = new SimpleEventDispatcher();
$dispatcher->listen('user.registered', $logger); // __invoke()を持つオブジェクトをリスナーとして登録
$dispatcher->dispatch('user.registered', ['id' => 1, 'name' => '太郎']);
print_r($logger->getLog());
関連機能との比較
| 機能 | 役割 | __invoke()との違い |
|---|---|---|
__invoke() | オブジェクトを関数のように呼び出し可能にする | 本記事の対象。$obj()形式の呼び出しに対応する |
クロージャ(Closure) | 無名関数としての呼び出し可能な値 | 状態(プロパティ)を明示的な形で保持しにくいが、構文がより簡潔 |
__call() | 未定義のメソッド呼び出しを処理する | メソッド名を伴う呼び出し($obj->method())が対象で、$obj()とは異なる |
Closure::fromCallable() | callableをClosureオブジェクトに変換する | __invoke()を持つオブジェクトをクロージャとして扱いたい場合に利用できる |
| 通常の関数 | グローバルスコープで定義される関数 | 状態を持てない点が、__invoke()を使うオブジェクトとの大きな違い |
よくある落とし穴(注意点)
callable型として扱われることを意識した設計__invoke()を実装すると、そのクラスのインスタンスはis_callable()がtrueを返す「呼び出し可能な値」になります。この性質を活かして、コールバックを期待する関数やメソッドに自然に渡せる設計を意識しましょう(例2・例4を参照)。- 引数の型や数を明確にドキュメント化する
__invoke()は可変長引数としても定義できますが、呼び出し側から見て「このオブジェクトはどんな引数を受け取るのか」が分かりにくくなりがちです。型宣言やPHPDocコメントで明確にしておくことが推奨されます。 - 状態を持つことのメリットとデメリットを理解する
__invoke()を使うオブジェクトは、クロージャのuse句よりも柔軟に内部状態(プロパティ)を保持・変更できます(例5を参照)。一方で、単純な処理であれば、通常のクロージャの方がコード量が少なく済む場合も多いため、状態管理が本当に必要かどうかを見極めましょう。 - 再帰呼び出し時は
$thisを使う__invoke()内から自分自身を再帰的に呼び出す場合、$this(...)という記法を使います(例5を参照)。これは一見見慣れない書き方に思えるかもしれませんが、__invoke()が定義されたオブジェクトとして正しく動作します。 - デバッグ時にスタックトレースが読みにくくなることがある
__invoke()を多用した処理のチェーンは、エラー発生時のスタックトレースにおいて、通常のメソッド呼び出しよりも文脈が追いにくくなることがあります。複雑なロジックを組む場合は、適度にコメントや명확なクラス名を付けることを心がけましょう。
まとめ
| 観点 | まとめ |
|---|---|
| 何をするメソッドか | オブジェクトを$obj()という関数のような構文で呼び出せるようにする |
| 主な用途 | コールバックとしての利用、ミドルウェアパターン、状態を持つ関数オブジェクト(Functor) |
| 判定方法 | is_callable()がtrueを返し、callable型として扱える |
| クロージャとの違い | プロパティとして状態を明示的に保持しやすい |
| 注意点 | 引数仕様の明確化、状態管理の必要性の見極め、再帰呼び出し時の$thisの使用 |
__invoke() は、オブジェクト指向の柔軟性と関数型的な「呼び出し可能な値」という性質を組み合わせられる、強力なマジックメソッドです。状態を持つコールバックやミドルウェアの実装など、クロージャだけでは表現しにくい場面で、その真価を発揮します。
