公開日 2026-06-07

モダンPHP再入門

PHP 5 / 7 時代のつらさが、型宣言や enum、PHPStan でどう変わったかを再入門向けに整理する。

目次

  1. 1. まず結論
  2. 2. まず、どこが変わったのか
  3. 3. 配列に寄りすぎるつらさは、型付きの小さなクラスで減らせる
  4. 4. 文字列の状態管理は enum と match で閉じやすい
  5. 5. 深い null チェックは nullsafe で短くできる
  6. 6. 実行して初めて壊れる を、型宣言と PHPStan で前に出す
  7. 7. いま学び直すなら、この順番で十分
  8. 8. まとめ

PHP は別の言語に生まれ変わったわけではない。
ただ、昔つらくなりやすかった場所には、いまは具体的な手当てがある。この記事では、新機能を並べるのではなく、どのつらさに何が効くか を before / after で整理する。

この記事で扱わないもの:

  • Laravel / Symfony / Slim の比較
  • PHP 8.x の全機能一覧
  • 既存コードベースの大規模移行手順
  • ベンチマークや性能比較

1. まず結論

モダンPHPの変化をひとことで言うなら、値の形 分岐の条件 null の扱い 壊れ方 をコード上に出しやすくなった、ということになる。

昔の PHP でも Web アプリは作れた。問題になりやすかったのは、次の変更でどこを見ればよいかが見えにくい場面だ。

  • 配列にいろいろな値を詰め込み、どんな形のデータかが読み取りにくい
  • 文字列の状態管理が散らばり、typo や分岐漏れが埋もれやすい
  • null チェックが深くなり、正常系と異常系の境目が曖昧になりやすい
  • 実行して初めて型のずれに気づく

いまは、このあたりの曖昧さを言語機能と周辺ツールで前に出しやすい。readonly match enum nullsafe strict_typesPHPStan を合わせると、昔よりずっと見通しを作りやすくなる。

2. まず、どこが変わったのか

細部に入る前に、今回の見取り図を置いておく。

昔つらくなりやすかった点いま取りやすい主な手当て変わること
配列に寄りすぎて、値の形が見えにくい型宣言、constructor property promotion、readonly入力値や戻り値の責務を小さなクラスで切り出しやすい
文字列の状態管理が広がるenummatch取りうる値を閉じやすく、分岐の抜け漏れを減らしやすい
深い null チェックが増えるnullsafe 演算子 ?->??オプショナルな関連をたどるコードを短く保ちやすい
実行して初めて壊れるdeclare(strict_types=1)、引数型、戻り値型、PHPStan早い段階で型のずれや戻り値の違和感に気づきやすい

昔のPHPは全部だめだった という話ではない。いまは、昔ならコメントや慣習で支えていた部分を、コードそのものへ寄せやすくなったということだ。

3. 配列に寄りすぎるつらさは、型付きの小さなクラスで減らせる

昔の PHP を見返すと、入力値や設定値を array に詰めて、その場で整形しながら流しているコードに出会いやすい。もちろん配列自体は今も便利だが、この値は何者か が曖昧なまま流れると、次の変更で苦しくなりやすい。

たとえば、ユーザー登録の入力をそのまま配列で持つとこうなりやすい。

<?php

$userInput = [
    'name' => 'Yuki',
    'email' => ' yuki@example.com ',
    'newsletter' => '1',
];

function registerUser(array $userInput): array
{
    $name = trim((string) ($userInput['name'] ?? ''));
    $email = trim((string) ($userInput['email'] ?? ''));
    $newsletter = ($userInput['newsletter'] ?? '0') === '1';

    if ($name === '' || $email === '') {
        return ['ok' => false, 'message' => 'invalid'];
    }

    return [
        'ok' => true,
        'user' => [
            'name' => $name,
            'email' => $email,
            'newsletter' => $newsletter,
        ],
    ];
}

var_export(registerUser($userInput));

これでも動く。ただ、nameemail が揃ったあとに何が有効な値なのか、どこで整形したいのか、返り値の形は何かが分散しやすい。

同じ題材を、入力用の小さなクラスで受けると次のようになる。

<?php

declare(strict_types=1);

final class UserRegistrationInput
{
    public function __construct(
        public readonly string $name,
        public readonly string $email,
        public readonly bool $newsletter,
    ) {
    }

    public static function fromArray(array $input): self
    {
        return new self(
            name: trim((string) ($input['name'] ?? '')),
            email: trim((string) ($input['email'] ?? '')),
            newsletter: ($input['newsletter'] ?? '0') === '1',
        );
    }

    public function isValid(): bool
    {
        return $this->name !== '' && $this->email !== '';
    }
}

function validateUserRegistrationInput(UserRegistrationInput $input): UserRegistrationInput
{
    if (! $input->isValid()) {
        throw new InvalidArgumentException('name と email は必須です。');
    }

    return $input;
}

$input = UserRegistrationInput::fromArray([
    'name' => 'Yuki',
    'email' => ' yuki@example.com ',
    'newsletter' => '1',
]);

$validatedInput = validateUserRegistrationInput($input);
var_dump($validatedInput->email);

ここでやっていることは、難しい設計ではない。

  • constructor property promotion で、値の受け取りとプロパティ定義をまとめられる
  • readonly で、生成後に不用意に書き換えない前提をコードで示せる
  • 引数が array ではなく UserRegistrationInput になるので、検証対象の責務が見えやすい

配列を全面禁止する必要はない。外部入力を受ける入口や、一時的なマッピングでは今も配列は使う。ただ、このデータをアプリ内で何として扱うか が見えたほうがよい箇所では、小さな型付きクラスが効く。

なお、readonly は「生成後にそのプロパティを再代入しない」ことを示すもので、オブジェクト全体の完全な不変性を保証するものではない。とはいえ、値の扱いを雑にしにくくするには十分効く。

4. 文字列の状態管理は enummatch で閉じやすい

昔の PHP では、状態を文字列で持ち、そのまま switch するコードがよくあった。いまでも簡単なスクリプトなら十分だが、状態が増えてくると typo や分岐漏れが目に入りにくい。

<?php

function statusLabel(string $status): string
{
    switch ($status) {
        case 'draft':
            return '下書き';
        case 'review':
            return 'レビュー中';
        case 'published':
            return '公開済み';
        default:
            return '不明';
    }
}

echo statusLabel('publsihed') . PHP_EOL;

この例では typo した 'publsihed'default へ流れる。壊れ方が静かなので、気づくのが遅れやすい。

enummatch を使うと、まず 取りうる状態 を先に閉じられる。

<?php

declare(strict_types=1);

enum ArticleStatus: string
{
    case Draft = 'draft';
    case Review = 'review';
    case Published = 'published';
}

function statusLabel(ArticleStatus $status): string
{
    return match ($status) {
        ArticleStatus::Draft => '下書き',
        ArticleStatus::Review => 'レビュー中',
        ArticleStatus::Published => '公開済み',
    };
}

$status = ArticleStatus::from('published');
echo statusLabel($status) . PHP_EOL;

enum が効くのは、状態や区分が有限集合である場面だ。記事ステータス、配送方法、権限、支払い方法のような値には相性がよい。match は値を返す式なので、代入や変換と相性がよく、switch より分岐の意図を狭く保ちやすい。

外部入力を受けるときは from() で例外にするか、tryFrom()null に落とすかを選べる。どちらにしても、ただの文字列をあちこちへ配る より、想定外の値が紛れ込む場所を減らせる。

5. 深い null チェックは nullsafe で短くできる

オプショナルな関連先をたどるコードも、昔の PHP では if が増えやすかった。値が存在しないこと自体が珍しくない場面では、枝分かれの多さがそのまま読みにくさにつながる。

<?php

declare(strict_types=1);

final class Company
{
    public function __construct(
        public readonly string $name,
    ) {
    }
}

final class Team
{
    public function __construct(
        public readonly ?Company $company,
    ) {
    }
}

final class User
{
    public function __construct(
        public readonly ?Team $team,
    ) {
    }
}

function companyNameBefore(?User $user): string
{
    if ($user === null) {
        return '未所属';
    }

    if ($user->team === null) {
        return '未所属';
    }

    if ($user->team->company === null) {
        return '未所属';
    }

    return $user->team->company->name;
}

echo companyNameBefore(new User(new Team(new Company('Acme Inc.')))) . PHP_EOL;

同じ意図を nullsafe 演算子で書くと、どこかが無ければ未所属 という条件がそのまま出てくる。

<?php

declare(strict_types=1);

final class Company
{
    public function __construct(
        public readonly string $name,
    ) {
    }
}

final class Team
{
    public function __construct(
        public readonly ?Company $company,
    ) {
    }
}

final class User
{
    public function __construct(
        public readonly ?Team $team,
    ) {
    }
}

function companyName(?User $user): string
{
    return $user?->team?->company?->name ?? '未所属';
}

echo companyName(new User(new Team(new Company('Acme Inc.')))) . PHP_EOL;
echo companyName(new User(null)) . PHP_EOL;

ここで気をつけたいのは、nullsafe を使えば全部よい わけではないことだ。関連先が無いことを許容するなら相性がよい。逆に、そこが無いなら不正データとして早く止めたい場面では、明示的なガードや例外のほうが合う。

6. 実行して初めて壊れる を、型宣言と PHPStan で前に出す

昔の PHP でつらかった印象として大きいのが、一応動くけれど、変な値が混ざっても実行するまで気づきにくい ことだった。ここは言語機能だけでなく、静的解析まで含めて見ると印象が変わる。

まず、declare(strict_types=1) と引数型、戻り値型を置く。

<?php

declare(strict_types=1);

final class CheckoutCalculator
{
    public function total(int $subtotal, int $shippingFee, int $discount = 0): int
    {
        return $subtotal + $shippingFee - $discount;
    }
}

$calculator = new CheckoutCalculator();

echo $calculator->total(
    subtotal: 1200,
    shippingFee: 500,
    discount: 100,
) . PHP_EOL;

この段階でも、何を受け取るか何を返すか はだいぶ見えやすい。さらに PHPStan を入れると、実行前に見つけられる範囲が広がる。

composer require --dev phpstan/phpstan
vendor/bin/phpstan analyse src --level=5

ここでは、最初から最厳格にせず、まず効果を感じやすい中間レベルとして level=5 を例にしている。

たとえば、int を返すはずのメソッドで array を返してしまったnullable な値を null チェックなしで使っている配列の shape と取り出し方がずれている といった問題は、コードを走らせる前に気づけることが多い。

モダンPHPを 言語機能だけ で見ると、たしかに ちょっと便利になった で終わりやすい。けれど、型宣言と静的解析まで合わせると、壊れ方を前に出しやすくなった という変化が見えてくる。再入門では、ここがいちばん印象の変わりやすいポイントだと思う。

PHPStan の最初の一歩だけを知りたいなら、PHPStan入門(最初のエラー1件を直す) もつながりやすい。

7. いま学び直すなら、この順番で十分

全部の機能を一気に覚える必要はない。学び直しなら、次の順番で触るだけでも感触は変わる。

  1. declare(strict_types=1) と引数型、戻り値型に慣れる
  2. 配列で流している入力や設定の一部を、小さな readonly クラスへ寄せる
  3. ステータスや区分を enum に切り出し、分岐を match へ寄せる
  4. オプショナルな関連をたどる場面で nullsafe を使う
  5. PHPStan を入れて、いまのコードでどこが曖昧かを機械に見つけてもらう

既存コードベースを全部書き換える必要はない。新しいクラス、新しい機能追加、新しく触る境界から始めれば十分だ。局所導入でも、この値は何か この状態は何種類あるか ここで null は許容か が見え始める。

言い換えると、再入門でやるべきことは 最新機能を全部知る ことではない。昔つらかった場所に対して、いまはどの道具を置けるかを知ることにある。

8. まとめ

モダンPHPは、昔の PHP を完全に否定するための言葉ではない。昔つらかった点に対して、いまは小さな型、readonlyenummatch、nullsafe、静的解析を置きやすくなった、という整理のほうが実感に近い。

配列に寄りすぎるなら型付きの小さなクラス、文字列の状態管理が散るなら enummatch、深い null チェックには nullsafe、実行時まで埋もれる型のずれには strict_typesPHPStan が効く。機能名だけを追うより、どの悩みに何が効くかで見るほうが学び直しやすい。

数年前の印象で PHP を止めていたなら、小さなコードで触り直してみるとよい。触り直してみると、昔のPHPの延長線上にありながら、値の形や分岐、null の扱いをずっと整理しやすくなっているのが分かるはずだ。