あなたの知らないPHPのスプレッド構文の色々な使い方
はじめに
皆さんはPHPのスプレッド構文を使ったことがありますか? スプレッド構文とは ... で表され、配列や引数を展開するために使われます。
JavaScriptでも同様の機能がありますが、PHPのスプレッド構文はJavaScriptのそれとは少し異なる使い方ができます。
今回この記事を書いたきっかけは、現場でPHPのスプレッド構文を使っている人が意外と少なく、その使いどころが分かりにくいと感じたからです。
この記事では、PHPのスプレッド構文の使い方をいくつか紹介し、その使いどころを紹介します。
可変長引数 php5.6から
PHPのスプレッド構文は、可変長引数を持つ関数に渡す際に非常に便利です。
可変長引数の定義
例えば、次のような関数を考えてみましょう。
function join_names(string ...$names): string {
    return implode(', ', $names);
}
この関数は、可変長引数 ...$names を受け取ります。... は、スプレッド構文を使うことで、可変長引数として複数の値を配列として受け取ることを示しています。
この関数では、文字列を結合することができます。
$result = join_names('太郎', '二郎', '三郎'); // $result = '太郎, 二郎, 三郎'
スプレッド構文を使わない場合、次のように書く必要があります。
function join_names(): string {
    // 配列の型は指定できないので、引数の型をチェックする必要がある
    $names = func_get_args();
    return implode(', ', $names);
}
スプレッド構文を使うことで、可変長引数に型を指定することができます。
これにより、可変長引数をより安全に扱うことができます。
このように、スプレッド構文を使うことで、可変長引数を簡潔に書くことができます。
配列展開 php7.4から
PHPのスプレッド構文は、配列を展開して、別の配列に結合したり、関数の引数に渡したりする際に便利です。
JavaScriptのスプレッド構文に馴染みがある人は、PHPのスプレッド構文も使いやすいと感じるかもしれません。
配列の結合/マージ
例えば、次のような配列を考えましょう。
$numbers1 = [1, 2, 3];
$numbers2 = [4, 5, 6];
これらの配列を結合するには、スプレッド構文を使います。
$numbers = [...$numbers1, ...$numbers2]; // $numbers = [1, 2, 3, 4, 5, 6]
// 色々な結合方法がある
$numbers = [...$numbers1, 4, 5, 6]; // $numbers = [1, 2, 3, 4, 5, 6]
$numbers = [1, 2, 3, ...$numbers2]; // $numbers = [1, 2, 3, 4, 5, 6]
$numbers = [...[...$numbers1, ...$numbers2]]; // $numbers = [1, 2, 3, 4, 5, 6]
この例では、...$numbers1 と ...$numbers2 がそれぞれ展開されて、新しい配列 $numbers に結合されています。
これは、array_merge 関数を使っても同じ結果が得られます。
$numbers = array_merge($numbers1, $numbers2); // $numbers = [1, 2, 3, 4, 5, 6]
ただ、array_merge関数よりもスプレッド構文を使った方が簡潔に書くことができます。
配列のコピー
スプレッド構文を使うことで、配列をコピーすることもできます。
$numbers1 = [1, 2, 3];
$numbers2 = [...$numbers1]; // $numbers2 = [1, 2, 3]
この例では、$numbers1 を ...$numbers1 で展開して、新しい配列 $numbers2 にコピーしています。
このように、スプレッド構文を使うことで、配列を簡単に結合/コピー/展開することができます。
配列展開と可変長引数の組み合わせ
スプレッド構文は、可変長引数と配列展開を組み合わせて使うこともできます。
例えば、次のような関数を考えてみましょう。
function sum(int ...$numbers): int {
  return array_sum($numbers);
}
$numbers1 = [1, 2, 3];
$numbers2 = [4, 5, 6];
$result = sum(...$numbers1, ...$numbers2); // $result = 21
// 色々な書き方ができる
$result = sum(...$numbers1, 4, 5, 6); // $result = 21
$result = sum(1, 2, 3, ...$numbers2); // $result = 21
$result = sum(...[...$numbers1, ...$numbers2]); // $result = 21
この例では、sum 関数に ...$numbers1 と ...$numbers2 を渡しています。... は、$numbers1 と $numbers2 の要素をそれぞれ展開して、sum 関数に渡します。
普通の引数で配列展開を使う
スプレッド構文は、可変長引数以外の引数でも使うことができます。
例えば、次のような関数を考えてみましょう。
function join_abc(int $a, int $b, string $c)
{
    return "$a, $b, $c";
}
$result = join_abc(...[1, 2, 'three']); // $result = '1, 2, three'
この例では、join_abc 関数に ...[1, 2, 'three'] を渡しています。... は、配列 [1, 2, 'three'] の要素をそれぞれ展開して、join_abc 関数に渡します。
このように、スプレッド構文を使うことで、配列を関数の引数に展開して渡すことができます。
Named Argumentsと組み合わせる
配列展開とNamed Argumentsと組み合わせて使うことで、関数の引数の順番を気にせずに、引数を渡すことができます。
$result = join_abc(...['a' => 1, 'c' => 'three', 'b' => 2]); // $result = '1, 2, three'
この例では、join_abc 関数に ...['a' => 1, 'c' => 'three', 'b' => 2] を渡しています。
... は、連想配列 ['a' => 1, 'c' => 'three', 'b' => 2] の要素をそれぞれ展開して、join_abc 関数に渡します。
このように、スプレッド構文を使うことで、Named Argumentsを使った関数の呼び出しをより柔軟に行うことができます。
可変長引数と配列展開のスプレッド構文の関係
可変長引数と配列展開のスプレッド構文は、一見すると同じように見えるかもしれませんが、異なる働きをします。
可変長引数は、関数の引数として複数の値を受け取るための仕組みです。
一方、配列展開は、複数の値を展開して別の配列に展開/結合するための仕組みです。
可変長引数と配列展開の違いを理解しておくことで、より柔軟なコードを書くことができるようになります。
第一級callable php8.1から
PHP 8.1から、スプレッド構文は第一級callableの生成にも使用できるようになりました。
第一級callableとは、関数やメソッドをクロージャ(無名関数)として扱うことができる機能です。
例えば、次のような関数を考えてみましょう。
function display(string $name, int $age) {
  return "Name: $name, Age: $age";
}
この関数をスプレッド構文を使ってクロージャにすることができます。
$display = display(...);
$result = $display('太郎', 20); // $result = 'Name: 太郎, Age: 20'
この例では、display 関数をスプレッド構文を使ってクロージャにしています。... は、display 関数をクロージャに変換することを示しています。
$display は、display 関数をクロージャとして保持しています。
$display を呼び出すことで、display 関数を実行することができます。
スプレッド構文を使わない場合、次のように書く必要があります。
$display = Closure::fromCallable('display');
$result = $display('太郎', 20); // $result = 'Name: 太郎, Age: 20'
オブジェクトのメソッドやクラスメソッドも同様にクロージャに変換することができます。
readonly class User
{
    public function __construct(
        private string $name = '',
        private int $age = 0,
    ) {
        //
    }
    public function display(): string
    {
        return "Name: $this->name, Age: $this->age";
    }
    public static function displayStatic(string $name, int $age): string
    {
        return (new self($name, $age))->display();
    }
}
$user = new User('太郎', 20);
$display = $user->display(...);
$result = $display(); // $result = 'Name: 太郎, Age: 20'
$displayStatic = User::displayStatic(...);
$result = $displayStatic('太郎', 20); // $result = 'Name: 太郎, Age: 20'
この例では、User クラスの display メソッドと displayStatic メソッドをスプレッド構文を使ってクロージャにしています。$display と $displayStatic は、それぞれ display メソッドと displayStatic メソッドをクロージャとして保持しています。
$display と $displayStatic を呼び出すことで、display メソッドと displayStatic メソッドを実行することができます。
スプレッド構文を使わない場合、次のように書く必要があります。
$user = new User('太郎', 20);
$display = Closure::fromCallable([$user, 'display']);
$result = $display(); // $result = 'Name: 太郎, Age: 20'
$displayStatic = Closure::fromCallable([User::class, 'displayStatic']);
$result = $displayStatic('太郎', 20); // $result = 'Name: 太郎, Age: 20'
このように、スプレッド構文を使うことで、関数やメソッド、クラスメソッドをクロージャに変換することができます。
おすすめの使い方:型安全なコレクションクラス
PHPには現状、ジェネリクスがサポートされていないため、型安全なコレクションクラスを実現するのは難しいです。
しかし、可変長引数と配列展開でのスプレッド構文の使い方を組み合わせることで、型安全なコレクションクラスを実現できます。
例えば、次のようなコレクションクラスを考えてみましょう。
/**
 * @implements IteratorAggregate<array-key,string>
 */
class StringCollection implements IteratorAggregate, Countable
{
    /**
     * @param string[] $strings
     */
    private array $strings;
    /**
     * スプレッド構文を使用して、文字列の配列のみを受け取る
     *
     * @param string ...$strings
     */
    public function __construct(string ...$strings)
    {
        // 配列が空でないか検証する
        if (empty($strings)) {
            throw new InvalidArgumentException('The strings must not be empty.');
        }
        $this->strings = $strings;
    }
    /**
     * スプレッド構文を使用して、文字列の配列のみを追加する
     *
     * @param string ...$strings
     * @return void
     */
    public function add(string ...$strings): void
    {
        // スプレッド構文を使って配列を結合する
        $this->strings = [
            ...$this->strings,
            ...$strings,
        ];
    }
    /**
     * {@inheritDoc}
     *
     * @return int
     */
    public function count(): int
    {
        return count($this->strings);
    }
    /**
     * {@inheritDoc}
     *
     * @return Traversable<array-key,string>
     */
    public function getIterator(): Traversable
    {
        return new ArrayIterator($this->strings);
    }
}
// 使用例
$collection = new StringCollection('太郎', '二郎', '三郎');
$collection->add('四郎', '五郎', '六郎');
foreach ($collection as $string) {
    echo "$string, "; // 太郎, 二郎, 三郎, 四郎, 五郎, 六郎,
}
この例では、StringCollection クラスを定義しています。このクラスは、文字列のコレクションを表すクラスです。
コンストラクタには、可変長引数 string ...$strings を受け取り、文字列の配列 $strings に代入しています。
add メソッドでは、可変長引数 string ...$strings を受け取り、文字列の配列 $strings を追加しています。
一応CountableとIteratorAggregateを実装することで、コレクションとしての振る舞いを実現しています。
このクラスを使うことで、文字列のみを受け取るコレクションを作成することができます。
もちろん、文字列だけではなく、他の型のコレクションも同様に作成することができるので、ドメインオブジェクトを表すコレクションクラス(ファーストクラスコレクションとか)など、様々な用途で活用することができます。
まとめ
PHPのスプレッド構文は、可変長引数や配列展開、コレクションクラスなど、様々な使い方ができます。
この記事では、スプレッド構文の基本的な使い方から、応用的な使い方までを紹介しました。是非、この記事を参考にして、スプレッド構文を使ったコードを書いてみてください。
スプレッド構文は、コードをより簡潔かつ柔軟にする強力なツールです。積極的に活用することで、より洗練されたコードを書くことができます。

Discussion