PHPのマジックメソッド__invoke()解説。単一機能のカプセル化に便利

PHPマジックメソッド__invoke()解説 マジックメソッド
※当サイトはアフィリエイト広告を掲載しています。

PHPにはマジックメソッドという機能があります。オブジェクトに対し、特定の動作を行った際に動作するメソッドのことです。

本ページでは、__invoke()という、オブジェクトを関数のように呼び出せるマジックメソッドについてまとめました。

PHPの新たな使い方を知ることで、より良いコーディングに繋がれば幸いです。

__invoke()の概要

まずは__invoke()を全くご存じ無い方向けに、概要をご紹介します。一般的なクラスを使った例を通じて、どんなものかを知っていただければと思います。

すでにご存じの方は次項に飛ばしてください。

通常のクラスを使った例

通常、PHPのクラスを考えるとき、そのクラスのオブジェクト(インスタンス)を作成し、そのオブジェクトから->(アロー演算子)でメソッドを呼び出します。

以下が簡単な例です。

class Greet
{
    public function sayHello($name)
    {
        return "Hello, {$name}!";
    }
}

$greeting = new Greet();
echo $greeting->sayHello('John');  // "Hello, John!" と出力される

何の変哲もないクラスですね。GreetクラスのsayHello()を呼び出すことで、メッセージを表示させています。

この分かりやすいクラスをベースに、__invoke()メソッドを解説していきたいと思います。

__invoke()を使った例

最初に軽く触れた通り、PHPにはオブジェクトを関数のように呼び出すことができる機能が存在します。それが、__invoke() マジックメソッドです。

先ほどのコードを、__invoke()を使って表現したのが以下の例です。

class Greet
{
    public function __invoke($name)
    {
        return "Hello, {$name}!";
    }
}

$greeting = new Greet();
echo $greeting('John');  // "Hello, John!" と出力される

普通のPHPの文法に慣れ親しんだ人にとっては、脳がバグったような気持ちになるかもしれませんね。このようにして使えるのが__invoke()です。

文法と基本的な例

文法

文法自体は、通常のメソッド定義と異なるわけではありません。メソッド名として __invoke を使用し、必要な引数(パラメータ)を定義します。

class クラス名
{
    public function __invoke(引数1, 引数2, ...)
    {
        // 処理内容
    }
}

上記のように、引数は自由に決められます。ここでは書いていませんが、コンストラクタやプロパティ、メソッドなども自由に書けます。

要は__invoke()を使いたい場合、同名のメソッドを定義にしておけば良い……ということになりますね。

例:配列の合計値を計算するクラス

以下の例では、ArraySum クラスのインスタンス $sumCalculator を、__invoke()で呼び出しています。

class ArraySum {
    public function __invoke(array $values) {
        return array_sum($values);
    }
}

$sumCalculator = new ArraySum();
echo $sumCalculator([1, 2, 3, 4, 5]); // 15

結果、引数で指定した配列内の合計値が出力されます。

例:文字列に接頭語を付けるクラス例

以下の例では、Prefixer クラスを使用して、与えられた文字列の前に接頭語を付けています。

class Prefixer
{
    private $prefix;

    public function __construct($prefix)
    {
        $this->prefix = $prefix;
    }

    public function __invoke($string)
    {
        return $this->prefix . $string;
    }
}

$prefixer = new Prefixer("Hello, ");
echo $prefixer("John");  // "Hello, John" と出力される

通常のクラスと同じように、コンストラクタやプロパティを活かすコードもOKです。

使い所ってあるの!?

これまでご紹介したとおり、文法自体は簡単。そのため、日常的にクラスを利用している方であれば__invoke()はすぐに使うことが可能です。

しかしながら、こうは思いませんか?

何が便利か分からない。使い所がわからない。

私も最初はそう思いました。その疑問に対して、私なりの意見を書きます。

処理を明確にできる

クラスが特定のアクションや役割に特化している場合、__invoke() はそれを明確&直感的に表現することが可能です。

例えば以下は、Multiplier(乗数)というクラスです。

class Multiplier
{
    private $factor;

    public function __construct($factor)
    {
        $this->factor = $factor;
    }

    public function __invoke($value)
    {
        return $value * $this->factor;
    }
}

Multiplier.phpというファイル名で保存しておけば、「そういうクラスなんだな」と分かりやすい。しかも単一の機能なのでコードも短いので読みやすく、使い方も分かりやすいです。

以下の様に使えます。

$double = new Multiplier(2);
echo $double(5); // 10

実際はこんな簡単なクラスは作らないかもしれません。

しかしながらここで言いたいのは、1クラス1機能のこれ以上無い明確なクラスを作れるということ。つまりはうまく使えばコードの見通しがよくなり、ミスを減らせそうということです。

カプセル化できる

__invoke() を使用することで、関連するデータと処理を一つのクラス内にカプセル化することができます。

例えばLaravel10ではFormRequestクラスにおいてafter()メソッドというものがあります。このメソッドからCallableの配列を返せば、追加バリデーションの機能を実装できるというものです。

バリデーション用のコールバック(関数等)を、配列で定義するものとお考えください。

以下例では、new ValidateShippingTimenew ValidateUserStatusとしておけば、それぞれのクラスの__invoke()メソッドが呼び出されます。11~12行目です。

use App\Validation\ValidateShippingTime;
use App\Validation\ValidateUserStatus;

/**
 * Callableの配列を返すことで、
 * 追加バリデーションを行う
 */
public function after(): array
{
    return [
        new ValidateShippingTime,
        new ValidateUserStatus,
    ];
}

ValidateShippingTimeクラスで出荷予定日を。ValidateUserStatusクラスでユーザーのステータスをバリデーション定義することで、処理をカプセル化できています。

ファイル毎にそれ専用のクラスにカプセルすることで、責務を閉じ込めることが可能です。

使い所が難しい!?

これまでの解説の通り、うまく__invoke()を使えば洗練されたコードになる反面、実践では使い所が難しいです。かなりPHPプログラミングに慣れていないと難しいでしょう。

一方、Laravel等のフレームワークとして、使い方のレールが敷いてあれば乗るのは簡単です。

ですので__invoke()を難しいと感じる方は、フレームワークなどで学び&利用させてもらいつつ親しんでいく。そして慣れてきたら、その他のプロジェクトでも取り入れていくのも良いのではないでしょうか。

Lara
Lara

実際、私もLaravelから__invoke()を導入したくちです。

その他の利用例

ここではもう少し追加で利用例をいくつか挙げますので、参考にしてください。

コールバック関数の代わりに

コールバックとして__invoke() を利用する例です。

class StringReverser
{
    public function __invoke(string $string): string
    {
        return strrev($string);
    }
}

$reverser = new StringReverser();
$result = array_map($reverser, ['apple', 'banana', 'cherry']);
echo implode(', ', $result);
// 出力: elppa, ananab, yrrehc

無名クラスにしても動きます。

$reverser = new class {
    public function __invoke(string $string): string
    {
        return strrev($string);
    }
};

$result = array_map($reverser, ['apple', 'banana', 'cherry']);
echo implode(', ', $result);
// 出力: elppa, ananab, yrrehc

このような単純な例は無名関数にした方が簡潔に表記できます。が、もしもコンストラクタやプロパティが必要な際等には活きてくるかもしれません。

メールの送信に利用する

メール送信を処理を実行するような例です。

class EmailNotification
{
    private $email;

    public function __construct($email)
    {
        $this->email = $email;
    }

    public function __invoke($message)
    {
        // ここで $this->email 宛にメッセージを送信するロジック
        echo "Email sent to {$this->email} with message: $message";
    }
}

$notification = new EmailNotification('test@example.com');
$notification('Hello there!');  // test@example.comに Hello there! というメールを送る

テキストをフィルタリングする

NGワードを指定し、それを伏せ字にするような例です。

class TextFilter {
    private $forbiddenWords;

    public function __construct(array $forbiddenWords) {
        $this->forbiddenWords = $forbiddenWords;
    }

    public function __invoke($text) {
        foreach ($this->forbiddenWords as $word) {
            $replacement = str_repeat('*', strlen($word));
            $text = str_replace($word, $replacement, $text);
        }

        return $text;
    }
}

$filter = new TextFilter(["maji", "koi"]);
echo $filter("majiでkoiする5秒前"); // "****で***する5秒前"

インターフェースを実装する

インターフェースを実装し、異なる振る舞いを実現する例です。

interface Comparator
{
    public function __invoke($a, $b): int;
}

class AscendingComparator implements Comparator
{
    public function __invoke($a, $b): int
    {
        return $a <=> $b;
    }
}

class DescendingComparator implements Comparator
{
    public function __invoke($a, $b): int
    {
        return $b <=> $a;
    }
}

function sortNumbers(array $numbers, Comparator $comparator): array
{
    usort($numbers, $comparator);
    return $numbers;
}

$numbers = [3, 1, 4, 1, 5];
$sortedAsc = sortNumbers($numbers, new AscendingComparator());  // [1, 1, 3, 4, 5]
$sortedDesc = sortNumbers($numbers, new DescendingComparator());  // [5, 4, 3, 1, 1]

まとめ

私もPHPは長年書いていますが、なかなかマジックメソッドを使いこなすのは難しいと感じます。

__invoke()メソッドも、頭では使い方が分かっていても実践で活かすのは経験が必要になりそうです。使えるところではどんどん使い、しっかりと身につけたいものですね。

マジックメソッドは他にもたくさんありますので、少しずつ記事を書いていきたいと思います。

コメント

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