モダンPHP(8.4 / 8.5 系)を前提とした、実務で使えるベストプラクティス集です。


目次

  1. バージョンと実行環境
  2. コーディング規約
  3. 型を徹底する
  4. モダンな言語機能を使う
  5. 設計の原則
  6. エラー処理
  7. セキュリティ
  8. 依存関係の管理(Composer)
  9. テストと静的解析
  10. パフォーマンス
  11. 本番環境の設定
  12. 避けるべきアンチパターン
  13. チェックリスト

1. バージョンと実行環境

  • サポート中のバージョンを使う。 セキュリティサポートが切れたバージョン(8.1 以前)は使わない。新規プロジェクトは最新の安定版(8.5 系)を選ぶ。
  • 対応バージョンは composer.json で明示する。
{
    "require": {
        "php": "^8.4"
    }
}
  • 開発・CI・本番で 同じマイナーバージョン を使う(Docker などで揃える)。
  • パッチリリース(例: 8.5.x)はセキュリティ修正を含むため、こまめに適用する。

2. コーディング規約

  • PSR-12 / PER Coding Style に準拠する。手作業で揃えず、ツールで自動整形する。
  • オートロードは PSR-4 に従う。名前空間とディレクトリ構造を一致させる。
{
    "autoload": {
        "psr-4": { "App\\": "src/" }
    },
    "autoload-dev": {
        "psr-4": { "Tests\\": "tests/" }
    }
}
  • 1ファイル1クラス。PHPのみのファイルでは閉じタグ ?> を書かない(末尾の空白出力事故を防ぐ)。
  • 命名規則
    対象規則例
    クラス・インターフェース・EnumPascalCaseUserRepository
    メソッド・変数camelCasefindById(), $userId
    定数・Enum caseUPPER_SNAKE_CASE / PascalCaseMAX_RETRY, Status::Active

3. 型を徹底する

strict_types を必ず宣言する

<?php
 
declare(strict_types=1);

暗黙の型変換("5" → 5 など)を防ぎ、バグを早期に発見できる。

引数・戻り値・プロパティすべてに型を付ける

// ❌ 悪い例
function calc($price, $qty) {
    return $price * $qty;
}
 
// ✅ 良い例
function calc(int $price, int $qty): int
{
    return $price * $qty;
}
  • Union型 int|string、Nullable型 ?User、never、void、static などを適切に使う。
  • mixed は最後の手段。配列の中身は PHPDoc(@param list<User> $users、@return array<string, int>)で静的解析ツールに伝える。

配列より値オブジェクト・DTOを使う

// ❌ 連想配列はキーのタイポや型の不一致に気付けない
$user = ['name' => 'Taro', 'email' => 'taro@example.com'];
 
// ✅ readonly クラスで構造を保証する
final readonly class UserData
{
    public function __construct(
        public string $name,
        public string $email,
    ) {}
}

4. モダンな言語機能を使う

コンストラクタプロモーション + readonly

final class OrderService
{
    public function __construct(
        private readonly OrderRepository $orders,
        private readonly Mailer $mailer,
    ) {}
}

Enum(文字列定数・マジックナンバーの代替)

enum OrderStatus: string
{
    case Pending   = 'pending';
    case Paid      = 'paid';
    case Cancelled = 'cancelled';
 
    public function label(): string
    {
        return match ($this) {
            self::Pending   => '保留中',
            self::Paid      => '支払済',
            self::Cancelled => 'キャンセル',
        };
    }
}
 
$status = OrderStatus::from($row['status']);      // 不正値は例外
$status = OrderStatus::tryFrom($input) ?? OrderStatus::Pending;

match 式(switch の代替)

match は厳密比較(===)で、値を返し、該当なしの場合は UnhandledMatchError を投げるため安全。

$fee = match (true) {
    $amount >= 10000 => 0,
    $amount >= 5000  => 300,
    default          => 500,
};

その他の便利な構文

// nullsafe 演算子
$city = $user?->getAddress()?->city;
 
// 名前付き引数(引数の意味が明確になる)
$str = str_pad(string: $id, length: 8, pad_string: '0', pad_type: STR_PAD_LEFT);
 
// ファーストクラスcallable
$upper = array_map(strtoupper(...), $names);
 
// 8.4: new の括弧省略
$name = new User('Taro')->name;
 
// 8.4: 配列検索関数
$admin = array_find($users, fn(User $u) => $u->isAdmin());

プロパティフック・非対称可視性(8.4〜)

getter/setter の定型コードを減らせる。

final class Product
{
    // 外部からは読み取りのみ、内部からは書き込み可
    public private(set) int $stock = 0;
 
    public string $name {
        set(string $value) {
            if ($value === '') {
                throw new InvalidArgumentException('商品名は必須です');
            }
            $this->name = $value;
        }
    }
}

パイプ演算子・clone with(8.5〜)

// 8.5: パイプ演算子で処理を左から右へ読めるようにする
$slug = $title
    |> trim(...)
    |> strtolower(...)
    |> (fn(string $s) => preg_replace('/[^a-z0-9]+/', '-', $s));
 
// 8.5: readonly オブジェクトの一部を変えたコピーを作る
$updated = clone($user, ['email' => 'new@example.com']);

使用する機能はプロジェクトの最低対応バージョンに合わせること。


5. 設計の原則

  • 依存性注入(DI)を使う。 クラス内で new やグローバル関数・静的呼び出しで依存を生成しない。テスト時に差し替えられなくなる。
// ❌ 依存がハードコードされている
class ReportService
{
    public function send(): void
    {
        $mailer = new SmtpMailer();   // 差し替え不能
    }
}
 
// ✅ インターフェースに依存し、外から注入する
final class ReportService
{
    public function __construct(private readonly MailerInterface $mailer) {}
}
  • デフォルトで final にする。 継承前提でないクラスは final。拡張はインターフェースとコンポジションで行う。
  • イミュータブルを優先する。 readonly プロパティ / readonly class を使い、変更時は新しいインスタンスを返す。
  • 単一責任。 1クラス・1メソッドの責務を小さく保つ。巨大な「Util」「Helper」「Manager」クラスを作らない。
  • 早期リターン でネストを浅くする。
public function process(?Order $order): void
{
    if ($order === null) {
        return;
    }
    if (!$order->isPayable()) {
        throw new DomainException('支払い不可の注文です');
    }
    // 本処理
}
  • グローバル変数・static な可変状態を避ける。
  • 日時は DateTimeImmutable を使い、DateTime(ミュータブル)は避ける。現在時刻はクロックを注入してテスト可能にする(PSR-20 ClockInterface)。

6. エラー処理

  • 例外を使う。 エラー時に false や null を返して呼び出し側に判定させる設計は避ける。
  • ドメイン固有の例外クラス を作り、意味のある単位で捕捉する。
final class InsufficientStockException extends DomainException
{
    public static function forProduct(int $productId): self
    {
        return new self("在庫不足: product_id={$productId}");
    }
}
  • 例外を握りつぶさない。catch (Exception $e) {} は禁止。再スローする場合は previous を渡して原因を残す。
try {
    $this->gateway->charge($amount);
} catch (GatewayException $e) {
    throw new PaymentFailedException('決済に失敗しました', previous: $e);
}
  • エラー抑制演算子 @ を使わない。
  • 開発環境では error_reporting(E_ALL) とし、警告・Deprecated もすべて潰す。
  • ログは PSR-3(Monolog など)で構造化して出力し、パスワード・トークン・個人情報をログに含めない。
  • 戻り値を無視してはいけない関数には #[\NoDiscard](8.5〜)を付ける。

7. セキュリティ

SQLインジェクション対策:プリペアドステートメント

// ❌ 絶対に文字列連結しない
$pdo->query("SELECT * FROM users WHERE email = '$email'");
 
// ✅ プレースホルダを使う
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);

PDO の推奨設定:

$pdo = new PDO($dsn, $user, $pass, [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
]);

※ テーブル名・カラム名・ORDER BY はプレースホルダにできないため、ホワイトリスト で検証する。

XSS対策:出力時にエスケープ

function h(string $s): string
{
    return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
  • テンプレートエンジン(Twig、Blade)の自動エスケープを有効にする。
  • Content-Security-Policy ヘッダーを設定する。

パスワード

$hash = password_hash($password, PASSWORD_DEFAULT);   // または PASSWORD_ARGON2ID
 
if (password_verify($input, $hash)) {
    if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
        // 新しいアルゴリズムで再ハッシュして保存
    }
}

md5() / sha1() / 独自ハッシュは使わない。

その他

項目対策
乱数・トークンrandom_bytes() / random_int() を使う(rand() / mt_rand() / uniqid() は不可)
CSRFフォームにトークンを埋め込み hash_equals() で比較。Cookie に SameSite を設定
セッションログイン時に session_regenerate_id(true)。Cookie は Secure / HttpOnly / SameSite=Lax 以上
入力検証すべての外部入力($_GET、$_POST、ヘッダー、Cookie、ファイル)を信用しない。filter_var() やバリデーションライブラリで検証
デシリアライズ信頼できないデータに unserialize() を使わない。json_decode() を使う
ファイルアップロードMIMEタイプをサーバー側で検証(finfo)、ファイル名は再生成、公開ディレクトリ外に保存
コマンド実行exec() 等は極力避ける。必要なら escapeshellarg() で引数をエスケープ
機密情報APIキー・DBパスワードはコードに書かず環境変数やシークレット管理で扱う。.env は Git にコミットしない
依存パッケージcomposer audit で既知の脆弱性を定期チェック

8. 依存関係の管理(Composer)

  • composer.lock は必ずコミットする(アプリケーションの場合)。ライブラリの場合はコミットしない運用が一般的。
  • 本番デプロイは以下で行う:
composer install --no-dev --optimize-autoloader --classmap-authoritative
  • バージョン制約はキャレット ^ を基本にし、* や dev-master を本番依存に使わない。
  • 定期的に composer outdated と composer audit を実行する(Dependabot / Renovate で自動化推奨)。
  • 自前実装の前に、PSR準拠の定評あるパッケージがないか確認する。

9. テストと静的解析

テスト

  • PHPUnit または Pest で自動テストを書く。
  • ビジネスロジックはユニットテスト、DBや外部APIとの結合部分は統合テストで検証する。
  • テストは CI で毎回実行する。
final class PriceCalculatorTest extends TestCase
{
    #[Test]
    public function 送料無料の閾値以上では送料が0円になる(): void
    {
        $calc = new PriceCalculator();
 
        self::assertSame(0, $calc->shippingFee(10000));
    }
}

静的解析・品質ツール

ツール用途
PHPStan / Psalm型エラー・バグの静的検出。レベルは段階的に上げ、新規は最大レベルを目指す
PHP-CS-Fixer / PHP_CodeSnifferコーディング規約の自動整形・チェック
Rector新しいPHPバージョンへの自動リファクタリング・移行
Infectionミューテーションテストでテストの質を測る

既存プロジェクトでは PHPStan の baseline 機能を使い、新規コードから厳格化していく。


10. パフォーマンス

  • OPcache を必ず有効化 する(本番では opcache.validate_timestamps=0 にしてデプロイ時にリセット)。
  • プリロード(opcache.preload)や JIT はベンチマークで効果を確認してから導入する。Webアプリでは I/O がボトルネックのことが多い。
  • N+1 クエリを避ける。 ループ内でクエリを発行しない。ORM ではEagerロードを使う。
  • 大量データは ジェネレーター(yield) で逐次処理し、メモリ使用量を抑える。
function readLines(string $path): Generator
{
    $handle = fopen($path, 'rb');
    try {
        while (($line = fgets($handle)) !== false) {
            yield $line;
        }
    } finally {
        fclose($handle);
    }
}
  • 重い処理はキュー(非同期ジョブ)に逃がす。
  • 推測で最適化せず、プロファイラ(Xdebug、Blackfire、SPX など)で計測してから手を入れる。
  • 頻繁に読まれるデータは APCu / Redis などでキャッシュする。

11. 本番環境の設定

php.ini の主な推奨設定:

display_errors = Off
display_startup_errors = Off
log_errors = On
error_reporting = E_ALL
expose_php = Off
 
session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = Lax
session.use_strict_mode = 1
 
opcache.enable = 1
opcache.validate_timestamps = 0
  • エラー詳細は画面に出さず、ログにのみ出力する。
  • ドキュメントルートは public/ のみにし、src/・vendor/・.env を Web から見えない場所に置く。
  • 不要な拡張モジュールは無効化する。

12. 避けるべきアンチパターン

アンチパターン代替
== による緩い比較=== を使う
extract() / 可変変数 $$name明示的な変数・配列アクセス
eval()使わない
global キーワード依存性注入
@ によるエラー抑制例外処理・事前チェック
mysql_* 関数 / SQL文字列連結PDO + プリペアドステートメント
HTMLとロジックの混在テンプレートエンジン・MVC
巨大な連想配列の受け渡しDTO / 値オブジェクト
マジックナンバー・マジック文字列定数・Enum
empty() の多用意図が明確な比較(=== ''、=== []、=== null)
何でもできる __get / __call明示的なメソッド・プロパティ
例外の握りつぶし適切に処理するかログを残して再スロー

13. チェックリスト

コードレビュー・新規プロジェクト立ち上げ時の確認用。

  • サポート中の PHP バージョンを使っている
  • 全ファイルに declare(strict_types=1); がある
  • 引数・戻り値・プロパティに型がある
  • PSR-4 オートロード、PSR-12 / PER-CS 準拠(自動整形が CI に組み込まれている)
  • PHPStan / Psalm を CI で実行している
  • 自動テストがあり CI で実行している
  • SQL はすべてプリペアドステートメント
  • 出力はすべてエスケープされている
  • パスワードは password_hash() で保存している
  • 機密情報がリポジトリに含まれていない
  • composer.lock をコミットし、composer audit を定期実行している
  • 本番で display_errors = Off、OPcache が有効
  • ドキュメントルートが public/ のみ

参考リンク

0件のページ