PHP 8 系は、7 系の動的な書き方を禁止していません。追加されたのは、宣言的に書きたいときに書けるようにする機能群です。使うかどうかは書き手に委ねられていますが、選択肢が増えた結果として推奨される書き方は明確に変わりました。

差分を型・データモデル・分岐の 3 点で整理します。

型: docblock から言語機能へ

7.4 では、複数の型を受ける関数はコメントで表現するしかありませんでした。

/** @param int|string $id */
function findUser($id): ?User
{
    // ...
}

@param は言語が検査しません。静的解析ツールを入れて初めて意味を持ちます。

8 系では Union 型が言語機能になりました。

function findUser(int|string $id): User|null
{
    // ...
}

宣言と違う値を渡せば TypeError になります。検査は実行時ですが、コメントと違って必ず効きます。

さらに Intersection 型と DNF 型が使えます。

function consume(Iterator&Countable $items): void
{
}

function normalize((Iterator&Countable)|array $items): array
{
    return is_array($items) ? $items : iterator_to_array($items);
}

Iterator&Countable は「両方を満たすもの」を要求します。DNF 型は積と和を組み合わせた表記で、括弧で優先順位を示します。

データモデル: 定数クラスから enum へ

7.4 での定番は、定数を並べたクラスでした。

final class UserStatus
{
    public const ACTIVE = 'active';
    public const SUSPENDED = 'suspended';
}

これは実体が文字列なので、'acitve' のようなタイプミスが型として弾かれません。渡す側も受ける側も string で扱うことになります。

8.1 以降は enum が使えます。

enum UserStatus: string
{
    case Active = 'active';
    case Suspended = 'suspended';
}

UserStatus を引数型に書けば、許される値が型で限定されます。文字列としての値も ->value で取り出せるので、DB 保存との往復も無理なく書けます。

DTO も短くなりました。引数宣言と同時にプロパティを定義するコンストラクタプロモーションと、readonly の組み合わせです。

readonly class UserData
{
    public function __construct(
        public int $id,
        public string $name,
        public UserStatus $status,
    ) {
    }
}

7.4 なら、プロパティ宣言・コンストラクタ引数・代入の 3 箇所に同じ名前を書いていた部分です。

分岐: switch から match へ

$label = match ($status) {
    UserStatus::Active => '利用中',
    UserStatus::Suspended => '停止中',
};

matchswitch と 3 点違います。式なので値を返す、break が不要、比較が === 相当。switch の代表的なバグである break 忘れと緩い比較が、構造的に発生しません。

null 安全演算子も入りました。

$country = $user?->address?->country ?? 'unknown';

?-> は左辺が null なら評価を打ち切って null を返します。null チェックの入れ子が 1 行になります。

8.4 以降の追加分

プロパティフックは、プロパティへの読み書きにロジックを差し込む仕組みです。getter と setter を明示的に書かずに検証を挟めます。

final class User
{
    public private(set) string $name {
        set {
            $value = trim($value);

            if ($value === '') {
                throw new InvalidArgumentException('Name is required');
            }

            $this->name = $value;
        }
    }

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

public private(set)非対称可視性で、読み取りは公開・書き込みはクラス内部に限る、という指定です。読み取り用の getter を書くだけの定型コードが不要になります。

8.5 のパイプ演算子は、値を関数へ順に流す記法です。

$result = $input
    |> trim(...)
    |> strtolower(...)
    |> htmlspecialchars(...);

trim(strtolower(htmlspecialchars($input))) のような内側から読む入れ子が、上から下へ読む形になります。

移行時に踏みやすいところ

7.4 からのアップグレードで挙動が変わる主な点です。

  • 数値と文字列の緩い比較の規則が変わった
  • 内部関数へ不正な型を渡すと warning ではなく TypeError になる
  • 不正な値が ValueError になるケースがある
  • 動的プロパティが非推奨になった
  • each()create_function() などが削除された
  • 波括弧による文字列・配列アクセスが削除された
  • implode() の古い引数順が削除された
  • 暗黙的な nullable(function f(int $x = null))が非推奨になった
  • 親子クラスのシグネチャ互換性の検査が厳しくなった

上 3 つが実害の出やすいところです。warning で素通りしていたコードが例外で止まるようになるため、テストが薄い箇所ほど本番で顕在化します。

まとめ

  • PHP 8 系は動的な書き方を禁止していません。宣言的に書く選択肢が増えたと捉えるのが正確です。
  • docblock の @param int|string は Union 型として言語機能になり、実行時に必ず検査されます。
  • 定数クラスは enum へ、手書き DTO は readonly とコンストラクタプロモーションへ置き換わりました。
  • match は式で break 不要、厳密比較。switch の典型的なバグが構造的に消えます。
  • プロパティフックと非対称可視性により、getter/setter の定型コードが不要になりました。
  • 移行時の実害は、warning で通っていたコードが TypeErrorValueError で止まる点に集中します。