PHP は別の言語に生まれ変わったわけではない。
ただ、昔つらくなりやすかった場所には、いまは具体的な手当てがある。この記事では、新機能を並べるのではなく、どのつらさに何が効くか を before / after で整理する。
この記事で扱わないもの:
- Laravel / Symfony / Slim の比較
- PHP 8.x の全機能一覧
- 既存コードベースの大規模移行手順
- ベンチマークや性能比較
1. まず結論
モダンPHPの変化をひとことで言うなら、値の形 分岐の条件 null の扱い 壊れ方 をコード上に出しやすくなった、ということになる。
昔の PHP でも Web アプリは作れた。問題になりやすかったのは、次の変更でどこを見ればよいかが見えにくい場面だ。
- 配列にいろいろな値を詰め込み、どんな形のデータかが読み取りにくい
- 文字列の状態管理が散らばり、typo や分岐漏れが埋もれやすい
nullチェックが深くなり、正常系と異常系の境目が曖昧になりやすい- 実行して初めて型のずれに気づく
いまは、このあたりの曖昧さを言語機能と周辺ツールで前に出しやすい。readonly match enum nullsafe strict_types に PHPStan を合わせると、昔よりずっと見通しを作りやすくなる。
2. まず、どこが変わったのか
細部に入る前に、今回の見取り図を置いておく。
| 昔つらくなりやすかった点 | いま取りやすい主な手当て | 変わること |
|---|---|---|
| 配列に寄りすぎて、値の形が見えにくい | 型宣言、constructor property promotion、readonly | 入力値や戻り値の責務を小さなクラスで切り出しやすい |
| 文字列の状態管理が広がる | enum、match | 取りうる値を閉じやすく、分岐の抜け漏れを減らしやすい |
深い 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));
これでも動く。ただ、name と email が揃ったあとに何が有効な値なのか、どこで整形したいのか、返り値の形は何かが分散しやすい。
同じ題材を、入力用の小さなクラスで受けると次のようになる。
<?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. 文字列の状態管理は enum と match で閉じやすい
昔の 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 へ流れる。壊れ方が静かなので、気づくのが遅れやすい。
enum と match を使うと、まず 取りうる状態 を先に閉じられる。
<?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. いま学び直すなら、この順番で十分
全部の機能を一気に覚える必要はない。学び直しなら、次の順番で触るだけでも感触は変わる。
declare(strict_types=1)と引数型、戻り値型に慣れる- 配列で流している入力や設定の一部を、小さな
readonlyクラスへ寄せる - ステータスや区分を
enumに切り出し、分岐をmatchへ寄せる - オプショナルな関連をたどる場面で nullsafe を使う
PHPStanを入れて、いまのコードでどこが曖昧かを機械に見つけてもらう
既存コードベースを全部書き換える必要はない。新しいクラス、新しい機能追加、新しく触る境界から始めれば十分だ。局所導入でも、この値は何か この状態は何種類あるか ここで null は許容か が見え始める。
言い換えると、再入門でやるべきことは 最新機能を全部知る ことではない。昔つらかった場所に対して、いまはどの道具を置けるかを知ることにある。
8. まとめ
モダンPHPは、昔の PHP を完全に否定するための言葉ではない。昔つらかった点に対して、いまは小さな型、readonly、enum、match、nullsafe、静的解析を置きやすくなった、という整理のほうが実感に近い。
配列に寄りすぎるなら型付きの小さなクラス、文字列の状態管理が散るなら enum と match、深い null チェックには nullsafe、実行時まで埋もれる型のずれには strict_types と PHPStan が効く。機能名だけを追うより、どの悩みに何が効くかで見るほうが学び直しやすい。
数年前の印象で PHP を止めていたなら、小さなコードで触り直してみるとよい。触り直してみると、昔のPHPの延長線上にありながら、値の形や分岐、null の扱いをずっと整理しやすくなっているのが分かるはずだ。