1. 命名の極意:脳のスタック消費を最小限に抑える
変数や関数の命名は可読性だけでなく、重大なエラーの発生時に脳のメモリを効率よく使うために非常に重要な点です。
その場しのぎの適当な命名を行うと、考えるのに時間がかかったり、誤解をしてバグを誘発したりすることがあります。
1.1 否定形の命名を避ける
〇〇でない場合を判定するために否定の意味のある変数を使ってしまうことがあるが、後々混乱を招くのでやめましょう。
- Why: 人間の脳は論理構造を「肯定」の形で認識します。
isNotUserのような命名をすると、その否定が必要になった際に 二重否定(!$isNotUser) が発生し、読み手に「反転の反転」という脳内ステップを強いるため、バグを誘発します。 - Good: 変数は常に肯定形(例:
isUser)で定義し、否定が必要な場合は評価側で!を用いる。
// ❌ NG:二重否定が発生し、直感に反する
if (!$isNotUser) { ... }
// 👍 Good:直感的に理解できる
if ($isUser) { ... }
1.2 共通語彙(動詞の使い分け)
「取得」という意味でなんでもget...というメソッド名を使いがちであるが、スコープの小さな動詞をうまく使うことで、補足的な言葉を追加しなくても理解しやすくなる。
ただし、これは、命名規則としてある程度共通理解がないと、慣れない英単語で躓くこともあるので、選定は十分に注意が必要。
- Why: すべてを
get...で始めると、それがDBからの重い取得なのか、メモリ内の単純な参照なのかが曖昧になります。 - Rule: スコープの小さい動詞をプロジェクト内で辞書として定義し、使い分けます。
fetch...: リモート(APIやDB)からIO負荷を伴って取得する。find...: 大量データの中から特定の条件で探し、なければ null を返す。calculate...: 数式やロジックを用いて値を算出する。show...: 取得した結果を画面に表示する。
2. 関数設計の原則:単一責任原則(SRP)の実践
関数の設計も、バグを誘発させたり誤解を与えたりさせないための重要なポイントがあります。
2.1 Fat Function(巨大な関数)の解体
関数の機能は、基本として「1関数1機能」を原則にしましょう。 バリデーションしてDBに登録して、画面にも表示して、...と何個もの機能を詰め込みすぎるとテストが難しくなったり、修正時の影響が大きくなることがあります。
- Why: 1つの関数でバリデーション、DB保存、メール送信まで行うと、テストが不可能になり、変更時の影響範囲が読めなくなります。
- Solution: 単一責任原則(SRP)に基づき、「この関数を変更する理由は1つだけか?」を自問します。具体的な処理は Actionクラス や Serviceクラスに委譲し、呼び出し側は「目次」のように読める状態にします。
2.2 引数オブジェクト(DTO)と名前付き引数
引数がいっぱいあって、呼び出し側でfunc(null,true,false,false,$data);なんていう実装を見たことはないだろうか?
引数が多すぎるというのは設計のミスの可能性もある。設計を見直すチャンスと考えることもできます。
- Why: 引数が4つも5つもある関数は、呼び出し側の可読性を著しく下げ、その関数が責任を持ちすぎているサインです。
- Good: 引数同士の結びつきが強い場合は DTO(データ転送オブジェクト) にまとめます。また、PHP 8.0以降の Named Arguments(名前付き引数) を活用して、引数の意味を明示します。
3. モダンなクラス設計:カプセル化と型システムによる保護
バリデーションもモダンなプロパティフックを使ったり、引数もプリミティブ型ではなく、独自の型によってバリデーションを行ったりすることも検討してみよう。
なお、PHP8.xではPHP7.x時代のようにすべての引数をPHPDocに書かずに、オブジェクトなど補足が必要なものに限るのが一般的になった。PHP8.x時代では、PHPDocのコメントと実装の乖離を避けるために、実装側に型定義を書くのが通常になってきている。
3.1 PHP 8.4 Property Hooksによるカプセル化
PHP8.4からgetterやsetterではなく代入時にバリデーションを行うロジックを搭載できるようになりました。
- Why: 従来の機械的な getter/setter は、データの加工ロジックがクラス外に漏れ出す原因となります。
- Good: Property Hooks を使い、プロパティ自体にバリデーションや加工(トリミング等)を隠蔽し、クラスの自律性を保ちます。
// 👍 Good:PHP 8.4 Property Hooks で加工を隠蔽
class User {
public string $email {
set (string $value) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid format");
}
$this->email = trim(strtolower($value)); // 加工をクラス内で保証
}
}
}
3.2 プリミティブ型への執着を捨てる
電話番号やメールアドレスなど決まった型のあるものは従来のstring型などのプリミティブ型ではなく独自の型を定義してしまうことで、同じバリエーションを何度も書かなくて済むようになります。
- Why: 電話番号やメールアドレスを単なる
stringとして扱うと、至る所で同じバリデーションを書く「防御的プログラミング」が蔓延します。 - Good: 値オブジェクト(Value Object) を導入します。専用の型を作ることで、「そのオブジェクトが存在する=正しい形式である」ことを型システムで保証します。
4. 継承とポリモーフィズム:多態性を壊さないための厳格なルール
クラスの継承を正しく使えていますか。改めて確認してみましょう。
4.1 $this / self / parent の使い分け
とりあえず、self::やparend::でメソッドをコールしていませんか?
- Rule: インスタンスメソッドの呼び出しは、原則として一律
$this->method()を使用します。 - Why:
self::は記述したクラス(コンパイル時)に固定されるため、子クラスでのオーバーライドを無視し、ポリモーフィズムを破壊します。$this->を使うことで、実行時のクラスに基づいた正しい挙動(遅延静的束縛)が保証されます。
4.2 Liskovの置換原則(LSP)とPHP 8.xの互換性
PHPのバージョンを上げるときに特に注意が必要です。子クラスでオーバーライドしたメソッドに引数を追加するときはオプション引数の追加、またはDTOを用いた引数で数を合わさないとエラーになります。
- Rule: 子クラスでオーバーライドする際、親クラスにない 必須引数を勝手に追加してはいけません。
- Why: PHP 8.xでは型互換性が崩れると Fatal Error が発生し、システムがクラッシュします。拡張が必要な場合は、引数をオプショナルにするか、DTOを用いて型を維持したまま情報を増やします。
4.3 継承の抑制(final キーワード)
子クラスでオーバーライドさせたくない場合はfinalキーワードを使って定義しましょう。
- Why: 継承はクラス間の最も強固な結合であり、安易な継承は予期せぬ挙動破壊を招きます。
- Good: 「継承よりも委譲」を優先し、これ以上拡張されるべきではないコアロジックや具象クラスには
finalを付与します。
5. ロジックの清流化:条件分岐とエラー処理のアンチパターン
5.1 ガード節(早期リターン)によるハッピーパスの維持
ネストが深くなってくると、可読性が落ちます。ガード節の利用などで可読性を保ちすっきりとしたコードにしましょう。
- Why:
ifのネストが深い「アローヘッド(矢印型)」は、読み手のワーキングメモリを浪費させ、バグの見落としを生みます。 - Good: 例外系を先頭で即座に
returnやthrowして弾き、正常系をインデントのない一本道(ハッピーパス)として記述します。
5.2 例外の握り潰し(ポケモン・ハンドリング)の禁止
「エラー、ゲットだぜ!」と捕まえるだけのエラーハンドリングのことを「ポケモンエクセプション」とか「ポケモンハンドリング」というらしいです(この記事を書くために調べたときに出てきたワードです)
エラーは捕まえたら、適切に処理をしましょう。
- Why: 空の
catchブロックはシステムの異常を隠蔽し、後続の処理で原因不明のエラーを引き起こします。 - Good: 例外をキャッチした場合は、適切に上位レイヤーに再スローするか、スタックトレースを明記したエラーログを出力します。
6. アーキテクチャ規約:フレームワークの機能を活かした関心の分離
フレームワークの中で定められた機能を果たすように実装しましょう。
6.1 スマートUI(Fat Controller)の回避
- Why: コントローラーの中にビジネスロジックを書くと、API化やコードの再利用ができなくなります。
- Good: コントローラーの役割を「交通整理」に限定し、ロジックは Model や Action クラスへ分離します。
6.2 クエリビルダの隠蔽(Local Scopeの活用)
- Why: 「公開済みデータ」などのビジネスルール(
where条件)をコントローラーに直書きすると、仕様変更時に修正漏れが発生します。 - Good: Laravelの Local Scope を定義し、仕様に名前をつけてModelで一元管理します。
// 👍 Good:Model側に仕様を定義
class Post extends Model {
public function scopePublished(Builder $query): void {
$query->where('status', Status::PUBLISHED)->where('published_at', '<=', now());
}
}
// コントローラー側:意味のある名前で呼び出す
$posts = Post::published()->get();