デザインワン・ジャパン Tech Blog

DesignOne Japan | Activate the World.

【PHP】タイプヒンティングをより強力にするArrayShape

はじめに

こんにちは!株式会社デザインワン・ジャパンで口コミサービス エキテンのリニューアルを担当しているサービス開発部の鈴木セシル(@suzuki_cecil_)です。

弊社では現在、15年間運用され続けている口コミサービス エキテン のリニューアルプロジェクトに取り組んでおります。

エキテンはリニューアル前後ともにPHPで開発をしているのですがリニューアル前は一部古いアプリケーションがPHP5系で、リニューアル後は全体がPHP8.1に統一されリニューアルによってレガシーPHPからモダンPHPに進化を遂げることになります。(※本記事投稿時点におけるPHPの最新リリースは8.1です)

PHP7系では タイプヒンティング で定義できる型の拡張、PHP8.1では 名前付き引数コンストラクタプロパティプロモーション などの新機能の採用により、ソースコードの堅牢性・可読性ともに向上できる言語仕様へと成長しました。

今回はその中でもタイプヒンティングおよびタイプヒンティングに関連して、PHPStormのArrayShapeに関して記事を書かさせていただきます。

タイプヒンティングとは

まずタイプヒンティングをご存知でない方に向けてタイプヒンティングの解説を行います。ArrayShapeに関して知りたい方は次の段落からお読みください。

タイプヒンティングとは型宣言とも呼ばれ、公式ドキュメントでは以下のように説明されております。

関数のパラメータや戻り値、 クラスのプロパティ (PHP 7.4.0 以降) に対して型を宣言することができます。 これによって、その値が特定の型であることを保証できます。 その型でない場合は、TypeError がスローされます。

以下のソースコード1を例に説明しますと、

ソースコード1

<?php
declare(strict_types=1); // 強い型付けとすることで型が異なる場合はTypeErrorをスローできる

function displayName(string $name) {
    echo "{$name}\n";
}
displayName("Suzuki"); // Suzukiと表示される
displayName(1); // TypeErrorがスローされる

引数をstring型でタイプヒンティングされた関数 displayName に対して、

  • displayName("Suzuki"); とコールした場合
    • 宣言通りの型の変数が引数に渡されるため正常に関数が実行される
  • displayName(1); とコールした場合
    • int型の変数が引数に渡されるためTypeErrorがスローされる

といった挙動になります。

また関数の戻り値に対してもタイプヒンティングが可能です。

ソースコード2

<?php
declare(strict_types=1); // 強い型付けとすることで型が異なる場合はTypeErrorをスローできる

function square(int $num): int {
    return $num * $num; // $numの二乗が返される
}
function invalidSquare(int $num): int {
    return "hoge"; // TypeErrorがスローされる
}
echo square(2);
echo invalidSquare(2);

戻り値をint型でタイプヒンティングされた関数 square および invalidSquare に関して、

  • square
    • int型の変数が戻り値に該当するため正常に関数が実行される
  • invalidSquare
    • string型の変数が戻り値に該当するためTypeErrorがスローされる

といった挙動になります。

タイプヒンティングを行うことで、想定される引数・戻り値が可視化されることによりソースコードの可読性が向上し、また想定される型を違反した際に処理が終了するためソースコードの堅牢性が向上されるといった恩恵を受けることが可能です。

強い型付けと弱い型付けについて

宣言した型と異なる型の変数が引数・戻り値に設定された場合の挙動として強い型付けと弱い型付けの2つが存在します。

強い型付けでは型が異なる時点でTypeErrorがスローされますが、弱い型付けでは宣言した型にキャストが可能な場合はキャストを行い、キャストが不可能な場合はTypeErrorがスローされます。

配列のタイプヒンティング

タイプヒンティングにより宣言できる型にarray型が存在するため、配列に関してもタイプヒンティングが可能です。

ただしPHP8.1時点における、PHPのタイプヒンティングの弱点としてarray型としてのみタイプヒンティングが可能であるため配列の要素に対してタイプヒンティングを行うことができません。

ソースコード3

<?php
function displayNames(array $names) {
    foreach ($names as $name) {
        echo "{$name}\n";
    }
}

displayNames(["Suzuki", "Sato", "Takahashi"]);
displayNames("Suzuki");
displayNames([1, 2, 3]);

引数にstring型を要素に持つ配列が渡されることを想定した関数 displayNames に対して、

  • displayNames(["Suzuki", "Sato", "Takahashi"]); とコールした場合
    • string型を要素に持つ配列が引数に渡されるため正常に関数が実行される
  • displayNames("Suzuki"); とコールした場合
    • string型の変数が引数に渡されるためTypeErrorがスローされる
  • displayNames([1, 2, 3]); とコールした場合
    • 要素がint型ではあるものの配列が引数に渡されるため正常に関数が実行される

といった挙動になり、配列の要素が想定と異なる場合にTypeErrorはスローされません。

これでは可読性・堅牢性の向上に繋がりませんが、対策としてPHPDocによる補完を行うことで品質を担保することが可能です。

ソースコード4

<?php
/**
 * @param string[] $names // string型の配列であることを明示的にする
 */
function displayNames(array $names) {
    foreach ($names as $name) {
        echo "{$name}\n";
    }
}

※PHPDocを記述しても要素が想定と異なる場合にTypeErrorはスローされませんが、静的解析により配列の要素まで精査が可能なため結果的に堅牢性の向上に繋がります

上記の通りPHPDocを記述することで、配列のタイプヒンティングの場合でもソースコードの品質を向上することが可能となりましたが、ただPHPDocを書くだけでは連想配列のような複雑な配列の定義を行うことができません。そこで登場するのがArrayShapeです。

ArrayShapeとは

ArrayShape とはPHPDocのアノテーションの一つでオブジェクト上の配列(連想配列)に対して、その構造を定義可能とします。

PHPStormでは2022.2のバージョンにてリリースされました。

ただしArrayShapeはPHPDocのアノテーションであり、PHPの仕様とは異なり配列のタイプヒンティング同様、定義とは異なる値が引数・戻り値に設定されたとしてもTypeErrorはスローされないため静的解析にて堅牢性を担保する必要があります。

ArrayShapeによる連想配列の定義

ArrayShapeで連想配列を定義する場合は、PHPDocに array{キー名: 型名, キー名: 型名} と記述します。

ソースコード5

<?php

class UserId
{
    public function __construct(public readonly string $value)
    {
    }
}

class UserName
{
    public function __construct(public readonly string $value)
    {
    }
}

/**
 * @return array
 */
function getUserWithoutArrayShape(): array
{
    return [
        "id" => new UserId("1"),
        "name" => new UserName("Suzuki"),
    ];
}

/**
 * @return array{"id": UserId, "name": UserName}
 */
function getUserWithArrayShape(): array
{
    return [
        "id" => new UserId("1"),
        "name" => new UserName("Suzuki"),
    ];
}

$user1 = getUserWithoutArrayShape();
echo $user1["name"]->value . "\n";

$user2 = getUserWithArrayShape();
echo $user2["name"]->value . "\n";

ソースコード5ではArrayShapeで戻り値を定義していない getUserWithoutArrayShape 関数と、ArrayShapeで戻り値を定義した getUserWithArrayShape 関数の2つが存在します。これらの関数はArrayShapeによる戻り値の定義の有無以外は全て一致しています。

戻り値の連想配列id キーでUserIdクラスのインスタンスを、 name キーでUserNameクラスのインスタンスを保持しているためArrayShapeは array{"id": UserId, "name": UserName} となります。

ArrayShapeを用いて定義することにより連想配列が保持するキーおよび各キーが保持する変数の型が明示的になります。

またPHPStormではArrayShapeによる定義を基にコード補完を行なってくれます。

ArrayShapeによる配列の定義

ArrayShapeで配列の定義を行うことも可能です。その場合は array<型名> と記述します。

またこれを応用して要素に連想配列を持つ配列を定義することも可能です。

ソースコード6

<?php

class UserId
{
    public function __construct(public readonly string $value)
    {
    }
}

class UserName
{
    public function __construct(public readonly string $value)
    {
    }
}

/**
 * @return array<array{"id": UserId, "name": UserName}>
 */
function getUsers(): array
{
    return [
        [
            "id" => new UserId("1"),
            "name" => new UserName("Suzuki"),
        ],
        [
            "id" => new UserId("2"),
            "name" => new UserName("Sato"),
        ],
        [
            "id" => new UserId("3"),
            "name" => new UserName("Takahashi"),
        ],
    ];
}

$users = getUsers();
echo $users[0]["name"]->value;

PHPStormではArrayShapeによる連想配列の配列の定義も解析してくれ、コード補完を行なってくれます。

まとめ

今回はArrayShapeによるソースコードの可読性、(静的解析と併用した)堅牢性の担保について解説させていただきました。

ArrayShapeを用いることで配列定義の表現が一段と広がります。

ただしPHPの仕様ではなく定義と異なる値が設定されてもTypeErrorがスローされないため、個人的には今後のPHPのバージョンアップによるタイプヒンティングによる配列定義の表現が広がることに期待します。

仲間を募集しております

募集中の職種については以下を御覧ください。

www.wantedly.com