長くPHP 5を使ってきた人がPHP 8.2のコードを見ると、「同じPHPとは思えない」「記号が多く、何をしているのか分からない」と感じることがあります。これは決して理解力の問題ではありません。PHP 5からPHP 8.2までの間に、言語仕様だけでなく、開発現場で好まれる設計やツールの使い方も大きく変わったためです。
この記事では、PHP 5でよく使われた書き方とPHP 8.2の書き方を、同じ処理ごとに並べて比較します。最新構文を暗記するのではなく、「新しい一行は、昔のどの処理を短くしたものなのか」を理解することが目的です。
- 最初に結論:難しく見える原因は3つ
- 比較1:関数の引数と戻り値
- 比較2:nullを許可する型と複数の型
- 比較3:値が存在しない場合の処理
- 比較4:null安全演算子 ?->
- 比較5:クラスのプロパティとコンストラクタ
- 比較6:readonlyで変更を禁止する
- 比較7:switchとmatch
- 比較8:無名関数とアロー関数
- 比較9:文字列や数値で表していた状態をEnumにする
- 比較10:コメントから属性 #[…] へ
- 比較11:requireとComposer・名前空間
- 比較12:クラス内で部品を作るか、外から渡すか
- PHP 8.2では宣言していないプロパティが問題になる
- PHP 8.2のコードを読む実践手順
- おすすめの学習順序
- 最新構文を全部使うことが良いコードではない
- PHP 5からPHP 8.2へ移行するときの注意
- まとめ
- 参考資料
最初に結論:難しく見える原因は3つ
- 変数や引数、戻り値に型を書くようになった
- 長い処理を一行にまとめる省略構文が増えた
- 手続き型中心から、クラス、Composer、依存性注入を使う設計へ移った
PHP 8.2がPHP 5より本質的に難解になったというより、以前はコメントや開発者の頭の中にあった決まりを、コード上へ明示するようになったと考えると理解しやすくなります。
| PHP 5で多かった考え方 | PHP 8.2で多い考え方 |
|---|---|
| 値の種類は実行時にPHPが判断 | 引数や戻り値の型を明示 |
| 配列で多くのデータを表現 | クラスやEnumで意味を表現 |
| requireでファイルを読み込む | Composerで自動読み込み |
| 長いが上から順番に読める | 短いが記号の知識が必要 |
| 曖昧な値を自動変換 | 不正な値は早めにエラー |
比較1:関数の引数と戻り値
PHP 5でよくある書き方
function add($a, $b)
{
return $a + $b;
}短くて分かりやすい反面、整数、文字列、nullなど、どの値を渡す前提なのかは関数の中身を読むまで分かりません。
PHP 8.2の書き方
function add(int $a, int $b): int
{
return $a + $b;
}int $aとint $bは「整数を受け取る」、最後の: intは「整数を返す」という意味です。型は処理ではなく、この関数の入力と出力を説明する仕様書です。
function findUser(int $id): Userなら、「整数のIDを渡すとUserオブジェクトが返る関数」と日本語へ置き換えて読めば十分です。
比較2:nullを許可する型と複数の型
function findUser(int $id): ?User
{
// 見つかればUser、見つからなければnull
}?Userは「Userまたはnull」です。次のUser|nullと同じ意味になります。
function findUser(int $id): User|null
{
}|は「または」と読みます。例えばint|stringなら「整数または文字列」、User|falseなら「Userまたはfalse」です。複雑な記号として覚えるより、日本語へ機械的に置き換えるのが近道です。
比較3:値が存在しない場合の処理
PHP 5の書き方
if (isset($_POST['name'])) {
$name = $_POST['name'];
} else {
$name = '';
}PHP 8.2でよく使う書き方
$name = $_POST['name'] ?? '';??は「左側に値があれば使い、なければ右側を使う」という意味です。PHP 7で導入された構文ですが、PHP 8系のコードでは日常的に登場します。短いコードが読みにくい場合は、頭の中で元のif文に戻してみましょう。
比較4:null安全演算子 ?->
従来の書き方
$user = getUser();
if ($user !== null) {
$company = $user->getCompany();
if ($company !== null) {
$name = $company->getName();
} else {
$name = null;
}
} else {
$name = null;
}PHP 8.2の書き方
$name = getUser()?->getCompany()?->getName();?->は「左側がnullなら、その先を実行せずnullにする」という記号です。上の一行は、Userが存在するか、Companyが存在するかを順番に確認しています。
さらに次のように書かれていれば、「名前を取得できなければ名称未設定を使う」と読めます。
$name = getUser()?->getCompany()?->getName() ?? '名称未設定';比較5:クラスのプロパティとコンストラクタ
PHP 5の書き方
class User
{
private $name;
private $email;
public function __construct($name, $email)
{
$this->name = $name;
$this->email = $email;
}
}PHP 8.2の書き方
class User
{
public function __construct(
private string $name,
private string $email
) {
}
}PHP 8のコンストラクタ・プロパティ・プロモーションでは、プロパティの宣言、コンストラクタ引数、プロパティへの代入を一か所にまとめられます。
private string $nameを見たら、「文字列型のprivateプロパティを作り、コンストラクタで受け取った値を保存する」と展開します。コード量は減りますが、裏側で行われる処理はPHP 5の例とほぼ同じです。
比較6:readonlyで変更を禁止する
class User
{
public function __construct(
public readonly int $id,
public readonly string $name
) {
}
}readonlyは、最初に設定した値を後から変更できないようにします。
$user = new User(1, '田中');
$user->id = 2; // エラーPHP 5では、変更してはいけない値も運用上の約束で守ることがありました。PHP 8.2では、その約束を言語側で強制できます。つまり、readonlyは難しい処理ではなく「作成後は変更禁止」という注意書きです。
比較7:switchとmatch
PHP 5のswitch
switch ($status) {
case 1:
$message = '受付中';
break;
case 2:
$message = '完了';
break;
default:
$message = '不明';
break;
}PHP 8.2のmatch
$message = match ($status) {
1 => '受付中',
2 => '完了',
default => '不明',
};matchは「値を返すswitch」と考えると理解できます。$statusが1なら「受付中」、2なら「完了」、それ以外なら「不明」を$messageへ代入しています。breakが不要で、比較も厳密です。
比較8:無名関数とアロー関数
従来の無名関数
$prices = array_map(function ($price) {
return $price * 1.1;
}, $items);アロー関数
$prices = array_map(
fn($price) => $price * 1.1,
$items
);fn($price) => $price * 1.1は、「$priceを受け取り、$price × 1.1を返す短い関数」です。複数行の処理には通常の無名関数を使ったほうが読みやすくなります。新構文を使うこと自体が目的ではありません。
比較9:文字列や数値で表していた状態をEnumにする
PHP 5でよくある状態管理
$status = 'paid';
if ($status === 'paid') {
// 支払済みの処理
}単純ですが、padiのようにスペルを間違えても、ただの文字列として成立してしまいます。
PHP 8.2のEnum
enum PaymentStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
case Cancelled = 'cancelled';
}
$status = PaymentStatus::Paid;Enumは、「指定された候補以外を入れられない値」です。難しいオブジェクトとして捉えず、入力可能な選択肢を先に決めたものと理解するとよいでしょう。
比較10:コメントから属性 #[…] へ
コメントで設定を書く例
/**
* @Route("/users", methods={"GET"})
*/
public function index()
{
}PHP 8の属性
#[Route('/users', methods: ['GET'])]
public function index(): Response
{
}#[...]は属性(Attribute)です。クラスやメソッドへ、フレームワークなどが読み取れる追加情報を付けます。上の例では「/usersへのGETアクセスをこのメソッドで処理する」という設定です。
属性を見たら、まず「ここは通常の処理本体ではなく、付加設定である」と切り離しましょう。その属性が何をするかはPHP共通ではなく、LaravelやSymfonyなど利用している仕組みによって変わる場合があります。
比較11:requireとComposer・名前空間
PHP 5でよくある書き方
require_once 'UserRepository.php';
$repository = new UserRepository();現在よく見る書き方
namespace App\Service;
use App\Repository\UserRepository;
$repository = new UserRepository();namespaceはクラスの所属先を表し、同名クラスの衝突を防ぎます。use App\Repository\UserRepository;は、その長いクラス名を、このファイル内ではUserRepositoryという短い名前で使う宣言です。
実際のファイル読み込みはComposerのオートローダーが担当することが一般的です。最初はnamespaceとuseを処理本体から外し、「クラス名の住所と略称」として読んでも問題ありません。
比較12:クラス内で部品を作るか、外から渡すか
PHP 5時代に多かった形
class UserController
{
public function show($id)
{
$repository = new UserRepository();
$logger = new Logger();
$logger->info('ユーザーを取得します');
return $repository->find($id);
}
}現代的な依存性注入
class UserController
{
public function __construct(
private UserRepository $repository,
private LoggerInterface $logger
) {
}
public function show(int $id): ?User
{
$this->logger->info('ユーザーを取得します');
return $this->repository->find($id);
}
}後者では、必要なRepositoryやLoggerをクラス内で直接作らず、外から渡してもらいます。これが依存性注入です。用語は難しく聞こえますが、「このクラスで使用する道具を外から受け取る」と考えれば十分です。
部品を交換しやすく、テストもしやすくなるため、現在のフレームワークで広く使われています。PHP 8.2の文法というより、現代的な設計方法の違いが、コードを別物に見せている大きな原因です。
PHP 8.2では宣言していないプロパティが問題になる
PHP 5では動いていた書き方
class User
{
}
$user = new User();
$user->name = '田中';このように、クラスで宣言していないプロパティを後から追加する方法を動的プロパティと呼びます。PHP 8.2では原則として非推奨です。
PHP 8.2で明示する
class User
{
public string $name;
}
$user = new User();
$user->name = '田中';宣言が必要になったのは、例えば$user->nmaeというスペルミスを新しいプロパティとして見逃さないためです。面倒が増えたように見えますが、不具合を早い段階で発見するための変更です。
PHP 8.2のコードを読む実践手順
1.記号を日本語に置き換える
| 記述 | 読み替え |
|---|---|
?User | Userまたはnull |
User|false | Userまたはfalse |
: int | 整数を返す |
: void | 戻り値を使わない |
?-> | nullならそこで止める |
?? | 値がなければ右側を使う |
fn() => | 一つの式を返す短い関数 |
#[...] | 付加設定 |
readonly | 作成後は変更禁止 |
match | 値を返すswitch |
2.短い一行を昔のif文へ戻す
$name = $user?->getProfile()?->getName() ?? '名称未設定';これを見て分からなければ、Userがnullか、Profileがnullか、名前がnullかを一つずつ確認するif文へ書き戻します。何度か展開すると、短い構文のまま処理を追えるようになります。
3.PHPの文法とフレームワークの機能を分ける
- PHPそのものの文法か
- LaravelやSymfonyなどの機能か
- Composerで入れたライブラリの機能か
- そのシステム独自のクラスか
例えば#[Route(...)]の#[...]はPHPの構文ですが、Routeの具体的な動作はフレームワーク側の機能です。この切り分けができると、何を調べればよいかが明確になります。
おすすめの学習順序
最初に覚えるもの
- 引数と戻り値の型
?、|、null??と?->- クラスのプロパティ型
- コンストラクタの短縮記法
namespaceとuse
次に覚えるもの
- Composerと自動読み込み
- 例外処理
- 依存性注入
match- アロー関数
- Enum
Attributes、Intersection Type、Fibers、Reflectionなどは、必要になってから学べば十分です。通常のWebシステムを読むために、PHP 8.2の全機能を最初から覚える必要はありません。
最新構文を全部使うことが良いコードではない
PHP 8.2でも、次のような素直なコードは有効です。
function getDisplayName(?User $user): string
{
if ($user === null) {
return 'ゲスト';
}
$name = $user->getName();
if ($name === '') {
return '名称未設定';
}
return $name;
}型は明示しながらも、処理の流れは上から順番に追えます。一行へ詰め込んで読みにくくするより、適切に改行し、処理を分けるほうが保守しやすいコードになります。
- 型は積極的に付ける
- 省略して分かりにくくなるなら省略しない
- 一行に複数の判断を詰め込まない
- 目的のない高度な設計パターンを導入しない
- コメントより分かりやすい関数名と変数名を優先する
PHP 5からPHP 8.2へ移行するときの注意
書き方を新しくすることと、既存プログラムをPHP 8.2で動作させることは別の作業です。古いプログラムでは、削除された関数、引数や戻り値の扱い、警告からエラーへ変わった処理、動的プロパティ、古いライブラリなども確認する必要があります。
いきなり全面的に書き直すより、まずPHP 8.2でエラーや非推奨警告を確認し、動作を維持したまま問題を解消します。その後、型宣言やクラス設計を少しずつ追加するほうが安全です。
まとめ
PHP 5は、処理を順番に書き、値の解釈をPHPへ任せる柔軟な言語でした。PHP 8.2は、どの値を受け取り、何を返し、何を変更できるのかをコード上で明確にする方向へ進んでいます。
最新のコードに慣れる一番の方法は、構文を丸暗記することではありません。「この短い一行をPHP 5風に書くとどうなるか」と展開してみることです。型は説明書、?はnullの可能性、??は代替値、?->はnullなら停止、と日本語へ置き換えるだけでも、読める範囲は大きく広がります。
そして何より、PHP 8.2を使うからといって、すべてを短く、複雑に書く必要はありません。新しいPHPの安全性を活かしながら、処理の流れが追いやすい書き方を選ぶことが、実務では最も重要です。



コメント