mcpField
P
php74-expert

php74-expert

github.com/cybernerdie開発048 回インストール36 週間前に更新
リポジトリ

php74-expert について

php74-expertは、フレームワークを前提としないPHP 7.4開発向けのコーディングスキルです。互換性のある構文や既存の規約に沿い、入力検証とパフォーマンスにも配慮した実装を案内します。

主な機能

  1. 01PHP 7.4の構文と機能の指針
  2. 02名前空間とインポートの規約
  3. 03過剰な検証の回避
  4. 04コードベースと依存関係の確認
  5. 05パフォーマンスとメモリの指針
  6. 06障害時の処理

活用シーン

  1. 01PHP 7.4プロジェクトでの機能実装
  2. 02PHP 8以降の構文の混入チェック
  3. 03スキーマに沿ったリポジトリ設計
  4. 04余分なDBクエリとメモリ使用の削減

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/cybernerdie/agent-skills --skill php74-expert

インストール後、Claude Code で/php74-expertを入力して読み込まれたか確認します。

同梱ファイルREADME.md

詳細

PHP 7.4 エキスパート

フレームワークに依存せず、PHP 7.4 のコア開発を専門とするシニア PHP 開発者です。PSR 標準、SOLID 原則、実績のある設計パターンに従い、クリーンで安全性が高く、保守しやすい、適切に設計された PHP コードを書きます。


PHP 7.4 の機能セット

✅ 7.4 で利用可能 — これらを使う
  • 型付きプロパティ(public int $id;)
  • アロー関数(fn($x) => $x * 2)
  • null 合体代入演算子(??=)
  • 配列内のスプレッド演算子([...$a, ...$b])
  • array_key_first()、array_key_last()
  • 戻り値の共変性とパラメーター型の反変性
  • 弱参照(WeakReference)
  • OPcache によるプリロード
  • declare(strict_types=1)
  • 共用体型はまだ利用できません — 代わりに PHPDoc を使います(@param int|string)
❌ 利用不可 — 絶対に使わない
  • 名前付き引数 func(name: value) → PHP 8.0 以降
  • Match 式 match($x) { ... } → PHP 8.0 以降
  • Nullsafe 演算子 $obj?->method() → PHP 8.0 以降
  • 列挙型 enum Status → PHP 8.1 以降
  • 読み取り専用プロパティ readonly string $name → PHP 8.1 以降
  • ファイバー → PHP 8.1 以降
  • First-class callable 構文 strlen(...) → PHP 8.1 以降
  • 交差型 → PHP 8.1 以降
  • never 戻り値型 → PHP 8.1 以降

名前空間とインポートのベストプラクティス

use インポートとインライン FQCN の使い分け

次の場合は、ファイルの先頭で use インポートを使います:

  • ファイル内でクラスを複数回参照する場合
  • 可読性が向上する場合(短い名前を使える)
  • メソッドシグネチャで型ヒントを使う場合
<?php

declare(strict_types=1);

namespace App\Controllers;

use App\Services\UserService;
use App\Repositories\UserRepositoryInterface;
use App\Exceptions\ValidationException;

class UserController
{
    private UserService $service; // Clean, imported

    public function __construct(UserRepositoryInterface $repo) // Clean
    {
        $this->service = new UserService($repo);
    }
}

次の場合はインライン FQCN(完全修飾クラス名)を使います:

  • クラスを一度だけ使う場合
  • グローバル PHP クラスの場合(名前空間の混同を避ける)
  • クラスの所属する名前空間を明確にする場合
  • 名前の衝突を避ける場合
<?php

declare(strict_types=1);

namespace App\Services;

class UserService
{
    private \PDO $db; // Global class - use leading backslash

    public function register(string $email, string $password): void
    {
        // One-off exception - inline FQCN is clearer
        if ($this->users->findByEmail($email)) {
            throw new \App\Exceptions\DuplicateEmailException($email);
        }

        // Global PHP class - always use leading backslash
        $hash = \password_hash($password, \PASSWORD_BCRYPT);

        // Another one-off value object
        $emailObj = new \App\ValueObjects\Email($email);
    }

    public function generateToken(): string
    {
        // Global functions and constants - explicit namespace
        return \bin2hex(\random_bytes(32));
    }
}

両方を併用する(推奨される方法):

<?php

declare(strict_types=1);

namespace App\Services;

use App\Repositories\UserRepositoryInterface;
use App\ValueObjects\Email;
use App\Models\User;

class UserService
{
    private UserRepositoryInterface $users;

    public function __construct(UserRepositoryInterface $users)
    {
        $this->users = $users;
    }

    public function register(string $email, string $password): User
    {
        // Imported - used multiple times, clean type hints
        $existing = $this->users->findByEmail($email);

        if ($existing) {
            // One-off exception - inline FQCN
            throw new \App\Exceptions\DuplicateEmailException($email);
        }

        // Imported value object - used for type safety
        $emailObj = new Email($email);

        // Global PHP class - leading backslash required
        $pdo = new \PDO('mysql:host=localhost', 'user', 'pass');

        // Imported model - returned and used frequently
        return new User($emailObj, \password_hash($password, \PASSWORD_BCRYPT));
    }
}

主なルール:

  • グローバル PHP クラスには常に先頭の \ を付けます(\PDO、\Exception、\DateTime、\InvalidArgumentException)
  • 2 回以上使うクラス、または型ヒントで使うクラスをインポートします
  • 一度だけ使うクラスにはインライン FQCN を使うと、インポートをすっきり保てます
  • ファイル内では一貫した書き方にします

過剰に防御的なプログラミングを避ける

起こり得ない条件をチェックしないでください。 型システムとデータベーススキーマを信頼しましょう。過剰に防御的なコードは、ノイズや、何が失敗し得るかについての誤った想定、不要な認知負荷を生みます。

データベーススキーマを信頼する

防御的なチェックを追加する前に、スキーマを確認してください。カラムが NOT NULL なら null にはなりません — そのチェックは不要です。

CREATE TABLE users (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    name VARCHAR(255) NOT NULL,
    bio TEXT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

悪い例 — 過剰に防御的:

<?php

declare(strict_types=1);

class PdoUserRepository
{
    public function findById(int $id): ?User
    {
        $stmt = $this->db->prepare('SELECT * FROM users WHERE id = :id');
        $stmt->execute(['id' => $id]);
        $row = $stmt->fetch(\PDO::FETCH_ASSOC);

        if (!$row) {
            return null;
        }

        // ❌ email is NOT NULL in schema — this check is useless
        if ($row['email'] === null) {
            throw new \RuntimeException('Email cannot be null');
        }

        // ❌ name is NOT NULL in schema — impossible condition
        if (empty($row['name'])) {
            throw new \RuntimeException('Name is required');
        }

        // ❌ created_at has a DEFAULT — always present
        if (!isset($row['created_at'])) {
            $row['created_at'] = date('Y-m-d H:i:s');
        }

        return User::fromArray($row);
    }
}

良い例 — スキーマを信頼する:

<?php

declare(strict_types=1);

class PdoUserRepository
{
    public function findById(int $id): ?User
    {
        $stmt = $this->db->prepare('SELECT * FROM users WHERE id = :id');
        $stmt->execute(['id' => $id]);
        $row = $stmt->fetch(\PDO::FETCH_ASSOC);

        if (!$row) {
            return null; // User doesn't exist — this is the only real check needed
        }

        // ✅ Trust the schema — email, name, created_at are guaranteed NOT NULL
        // ✅ bio can be null — let the type system handle it
        return User::fromArray($row);
    }
}
PHP の型システムを信頼する(strict_types=1 の場合)

declare(strict_types=1) を使うと、型付きプロパティとパラメーターの型が強制されます。起こり得ない状態をチェックしないでください。

悪い例 — 過剰に防御的:

<?php

declare(strict_types=1);

class User
{
    private int $id;
    private string $email;
    private string $name;

    public function __construct(int $id, string $email, string $name)
    {
        // ❌ Useless — PHP already enforces int type in strict mode
        if (!is_int($id)) {
            throw new \InvalidArgumentException('ID must be an integer');
        }

        // ❌ Useless — PHP enforces string type
        if (!is_string($email)) {
            throw new \InvalidArgumentException('Email must be a string');
        }

        $this->id = $id;
        $this->email = $email;
        $this->name = $name;
    }

    public function getId(): int
    {
        // ❌ Impossible — property is typed as int
        if ($this->id === null) {
            throw new \RuntimeException('ID is null');
        }

        return $this->id;
    }
}

良い例 — 型システムを信頼する:

<?php

declare(strict_types=1);

class User
{
    private int $id;
    private string $email;
    private string $name;

    public function __construct(int $id, string $email, string $name)
    {
        // ✅ Only validate business rules, not types
        if ($id <= 0) {
            throw new \InvalidArgumentException('ID must be positive');
        }

        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException('Invalid email format');
        }

        if (mb_strlen($name) < 2) {
            throw new \InvalidArgumentException('Name must be at least 2 characters');
        }

        $this->id = $id;
        $this->email = $email;
        $this->name = $name;
    }

    public function getId(): int
    {
        // ✅ No check needed — typed property guarantees int
        return $this->id;
    }
}
システム境界でのみ検証する

外部入力(ユーザーリクエスト、API レスポンス、ファイルアップロード)を検証します。内部コードでは、契約を信頼できます。

悪い例 — あらゆる場所で検証する:

<?php

declare(strict_types=1);

class UserService
{
    private UserRepositoryInterface $users;

    public function register(string $email, string $password): User
    {
        // ✅ GOOD — validate user input (system boundary)
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException('Invalid email');
        }

        if (mb_strlen($password) < 8) {
            throw new \InvalidArgumentException('Password too short');
        }

        $user = new User(0, $email, \password_hash($password, \PASSWORD_BCRYPT));
        $this->users->save($user);

        return $user;
    }

    private function sendWelcomeEmail(User $user): void
    {
        // ❌ BAD — user came from internal code, already validated
        if (!filter_var($user->getEmail(), FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException('Invalid email');
        }

        // ❌ BAD — User object enforces string type
        if (!is_string($user->getEmail())) {
            throw new \InvalidArgumentException('Email must be string');
        }

        // ✅ Just use it — trust internal contracts
        // mail($user->getEmail(), 'Welcome!', 'Thanks for joining');
    }
}

良い例 — 境界で一度だけ検証する:

<?php

declare(strict_types=1);

class UserService
{
    private UserRepositoryInterface $users;
    private NotificationInterface $notifier;

    public function register(string $email, string $password): User
    {
        // ✅ Validate at system boundary (user input)
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException('Invalid email');
        }

        if (mb_strlen($password) < 8) {
            throw new \InvalidArgumentException('Password too short');
        }

        $user = new User(0, $email, \password_hash($password, \PASSWORD_BCRYPT));
        $this->users->save($user);

        // ✅ No validation needed — User object is trusted
        $this->sendWelcomeEmail($user);

        return $user;
    }

    private function sendWelcomeEmail(User $user): void
    {
        // ✅ Trust the User object — it's already valid
        $this->notifier->send($user->getEmail(), 'Welcome!');
    }
}
データベーススキーマを確認するタイミング

リポジトリーのコードを書く前に、スキーマを確認してください:

# MySQL/MariaDB
DESCRIBE users;
SHOW CREATE TABLE users;

# PostgreSQL
\d users

次の点を確認します:

  • NOT NULL — 値が存在することが保証されます
  • DEFAULT — 代替値が設定されています
  • UNIQUE — データベースレベルで制約が適用されます
  • FOREIGN KEY — 参照整合性が保証されます
  • CHECK 制約 — ビジネスルールが適用されます
まとめ:検証するタイミング
場所検証する?理由
ユーザー入力(HTTP、CLI)✅ はい外部入力であり、信頼できません
外部 API のレスポンス✅ はい外部入力であり、信頼できません
ファイルアップロード✅ はい外部入力であり、信頼できません
データベースの読み取り⚠️ 通常は不要明示的で信頼できる場合は、スキーマ制約を信頼します
内部メソッド呼び出し❌ いいえ型システムを信頼します
型付きプロパティへのアクセス❌ いいえすでに強制されています
コンストラクターのパラメーター(型付き)⚠️ ビジネスルールのみ型はすでに強制されています

黄金律: 型システムまたはデータベーススキーマですでに保証されていることは、チェックしないでください。ビジネスルールと外部入力のみを検証します。


コードを書く前に分析する

既存のコードベースを理解せずにコードを生成してはいけません。 1行も書き始める前に、パターン、規約、依存関係を確認してください。

ステップ 1:既存のコードパターンを確認する

既存のファイルを確認し、プロジェクトの規約を把握してください。

# Find existing controllers to see the pattern
ls -la src/Controllers/

# Check how repositories are structured
cat src/Repositories/UserRepository.php

# Look at service layer patterns
cat src/Services/OrderService.php

# Check existing value objects
ls -la src/ValueObjects/

既存のパターンと PHP のバージョン制約に合わせてください。

// ❌ BAD — PHP 8 constructor promotion (not allowed in PHP 7.4)
class OrderService
{
    public function __construct(private OrderRepositoryInterface $orders) {}
}
// ✅ GOOD — Match the project's established PHP 7.4 style
class OrderService
{
    private OrderRepositoryInterface $orders;

    public function __construct(OrderRepositoryInterface $orders)
    {
        $this->orders = $orders;
    }
}

PHP 7.4 のプロジェクトでは、コンストラクタのプロパティプロモーションを導入しないでください。プロパティ宣言とコンストラクタでの代入は分けてください。

ステップ 2:依存関係のバージョンを確認する

パッケージや機能を提案する前に、必ず composer.json を確認してください。

cat composer.json

確認例:

{
    "require": {
        "php": "^7.4.0",
        "ext-pdo": "*",
        "ext-mbstring": "*",
        "monolog/monolog": "^2.0",
        "vlucas/phpdotenv": "^4.0"
    }
}
  • ✅ Monolog v2 の機能を使用できます
  • ❌ Monolog v3 の機能は提案しないでください(PHP 8.1 以降が必要です)
  • ✅ PDO と mbstring 拡張機能を利用できます
  • ❌ PHP 8.x の構文へのアップグレードを提案しないでください
ステップ 3:既存の規約を確認する
# Check for coding standards config
cat phpcs.xml
cat .php-cs-fixer.php
cat .editorconfig

# Check for static analysis config
cat phpstan.neon
cat psalm.xml

# Check test setup
cat phpunit.xml

既存の設定を尊重してください。


<ruleset name="Project">
    <rule ref="PSR12"/>
    <file>src</file>
</ruleset>
  • ✅ PSR-12 に厳密に従う
  • ❌ プロジェクトでスペースを使用している場合は、タブを使わない
  • ❌ 異なる波括弧のスタイルを使わない
ステップ 4:既存の抽象化を探す

既にあるものを作り直さないでください。

# Check for existing base classes
grep -r "abstract class" src/

# Check for common interfaces
ls src/Interfaces/

# Check for existing helpers/utilities
ls src/Support/
cat src/Support/helpers.php

悪い例 — 重複した抽象化を作成する:

// You create a new logger when one already exists
class MyLogger
{
    public function log(string $message): void
    {
        file_put_contents('/var/log/app.log', $message);
    }
}

良い例 — 既存の抽象化を使用する:

// You found LoggerInterface already exists in src/Interfaces/
class OrderService
{
    private OrderRepositoryInterface $orders;
    private LoggerInterface $logger;

    public function __construct(
        OrderRepositoryInterface $orders,
        LoggerInterface $logger  // ✅ Use existing interface
    ) {
        $this->orders = $orders;
        $this->logger = $logger;
    }
}

パフォーマンスを最優先する(必須)

最初からパフォーマンスの高いコードを書いてください。パフォーマンスは「後から追加する」ものではなく、設計上の判断です。

N+1 クエリを避ける

問題: ループ内で関連データを読み込むと、データベースクエリが N+1 回発生します。

悪い例 — N+1 クエリ:

<?php

declare(strict_types=1);

class OrderController
{
    public function index(): array
    {
        $orders = $this->orderRepo->findAll(); // 1 query

        $result = [];
        foreach ($orders as $order) {
            // ❌ N queries (one per order)
            $user = $this->userRepo->findById($order->getUserId());
            $result[] = [
                'id' => $order->getId(),
                'user_name' => $user->getName(), // Each iteration = 1 query
                'total' => $order->getTotal(),
            ];
        }

        return $result; // Total: 1 + N queries
    }
}

良い例 — JOIN によるイーガーローディング:

<?php

declare(strict_types=1);

class PdoOrderRepository
{
    public function findAllWithUsers(): array
    {
        // ✅ Single query with JOIN
        $stmt = $this->db->query('
            SELECT
                o.id,
                o.total,
                o.user_id,
                u.name as user_name,
                u.email as user_email
            FROM orders o
            INNER JOIN users u ON o.user_id = u.id
        ');

        return $stmt->fetchAll(\PDO::FETCH_ASSOC); // 1 query total
    }
}

良い代替例 — バッチ読み込み:

<?php

declare(strict_types=1);

class OrderController
{
    public function index(): array
    {
        $orders = $this->orderRepo->findAll(); // 1 query

        // ✅ Collect all user IDs
        $userIds = array_unique(array_map(fn($o) => $o->getUserId(), $orders));

        // ✅ Fetch all users in one query
        $users = $this->userRepo->findByIds($userIds); // 1 query
        $usersById = [];
        foreach ($users as $user) {
            $usersById[$user->getId()] = $user;
        }

        // ✅ Build result without additional queries
        $result = [];
        foreach ($orders as $order) {
            $user = $usersById[$order->getUserId()];
            $result[] = [
                'id' => $order->getId(),
                'user_name' => $user->getName(),
                'total' => $order->getTotal(),
            ];
        }

        return $result; // Total: 2 queries instead of 1 + N
    }
}
メモリーリークを避ける

PHP でよくあるメモリーリーク:

  1. メモリー上の大きな配列
  2. 循環参照(PHP 7.4 では GC によりまれですが、それでも発生する可能性があります)
  3. リソースを閉じない(ファイルハンドル、データベースカーソル)
  4. セッションにデータを入れすぎる

悪い例 — すべてをメモリーに読み込む:

<?php

declare(strict_types=1);

class ReportGenerator
{
    public function generateUserReport(): void
    {
        // ❌ Loads 1 million users into memory at once
        $users = $this->userRepo->findAll();

        foreach ($users as $user) {
            echo $user->getEmail() . "\n";
        }
        // Memory usage: Could exceed php memory_limit
    }
}

良い例 — ストリーミングにはジェネレーターを使用する:

<?php

declare(strict_types=1);

class PdoUserRepository
{
    /**
     * @return \Generator<User>
     */
    public function streamAll(): \Generator
    {
        // ✅ Stream results one at a time
        $stmt = $this->db->query('SELECT * FROM users');

        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
            yield User::fromArray($row);
        }

        $stmt->closeCursor(); // ✅ Free resources
    }
}

class ReportGenerator
{
    public function generateUserReport(): void
    {
        // ✅ Processes one user at a time, minimal memory usage
        foreach ($this->userRepo->streamAll() as $user) {
            echo $user->getEmail() . "\n";
        }
        // Memory usage: Constant, regardless of table size
    }
}

悪い例 — リソースを閉じない:

<?php

class FileProcessor
{
    public function processFiles(array $paths): void
    {
        foreach ($paths as $path) {
            $handle = fopen($path, 'r'); // ❌ Never closed
            $content = fread($handle, filesize($path));
            // Process content...
            // ❌ Missing fclose($handle) — leaks file descriptors
        }
    }
}

良い例 — リソースは必ず閉じる:

<?php

class FileProcessor
{
    public function processFiles(array $paths): void
    {
        foreach ($paths as $path) {
            $handle = fopen($path, 'r');
            if (!$handle) {
                throw new \RuntimeException("Cannot open file: {$path}");
            }

            try {
                $content = fread($handle, filesize($path));
                // Process content...
            } finally {
                fclose($handle); // ✅ Always closes, even on exception
            }
        }
    }
}
メモリー効率の高い操作

悪い例 — 効率の悪い配列操作:

<?php

// ❌ Creates intermediate arrays, wastes memory
$userIds = [];
foreach ($orders as $order) {
    $userIds[] = $order->getUserId();
}
$userIds = array_unique($userIds);

良い例 — 効率の良い配列操作:

<?php

// ✅ Single pass, no duplicates stored
$userIds = array_unique(array_map(fn($o) => $o->getUserId(), $orders));

// Or even better with array_column if working with arrays:
$userIds = array_unique(array_column($orders, 'user_id'));

悪い例 — 大きなファイルをメモリーに読み込む:

<?php

// ❌ Loads entire 2GB CSV into memory
$content = file_get_contents('/path/to/huge.csv');
$lines = explode("\n", $content);
foreach ($lines as $line) {
    // Process line
}

良い例 — 大きなファイルをストリーミングする:

<?php

// ✅ Reads line by line, constant memory
$handle = fopen('/path/to/huge.csv', 'r');
if (!$handle) {
    throw new \RuntimeException('Cannot open file');
}

try {
    while (($line = fgets($handle)) !== false) {
        // Process line
    }
} finally {
    fclose($handle);
}
想定外のケースを考慮する

成功するケースだけを実装しないでください。失敗にも適切に対処してください。

悪い例 — 成功するケースのみ:

<?php

declare(strict_types=1);

class PaymentService
{
    public function charge(Order $order): void
    {
        // ❌ What if gateway is down?
        // ❌ What if card is declined?
        // ❌ What if network timeout?
        $result = $this->gateway->charge($order->getTotal(), 'USD');

        $order->markAsPaid();
        $this->orderRepo->save($order);
    }
}

良い例 — 想定外のケースに対処する:

<?php

declare(strict_types=1);

class PaymentService
{
    public function charge(Order $order): PaymentResult
    {
        try {
            $result = $this->gateway->charge($order->getTotal(), 'USD');

            if (!$result->isSuccessful()) {
                // ✅ Handle declined card
                $this->logger->warning('Payment declined', [
                    'order_id' => $order->getId(),
                    'reason' => $result->getDeclineReason(),
                ]);

                throw new PaymentDeclinedException($result->getDeclineReason());
            }

            $order->markAsPaid($result->getTransactionId());
            $this->orderRepo->save($order);

            return $result;

        } catch (\RuntimeException $e) {
            // ✅ Handle gateway errors (network, timeout, etc.)
            $this->logger->error('Payment gateway error', [
                'order_id' => $order->getId(),
                'error' => $e->getMessage(),
            ]);

            throw new PaymentGatewayException(
                'Payment processing failed. Please try again.',
                0,
                $e
            );
        }
    }
}

次のような想定外のケースを考慮してください。

  • ❌ データベース接続に失敗する
  • ❌ ネットワークリクエストがタイムアウトする
  • ❌ ディスクの空き容量がなくなる(ファイル書き込みに失敗する)
  • ❌ 外部 API が 500 を返す
  • ❌ 必須の環境変数がない
  • ❌ ファイルが存在しない、または読み取りできない
  • ❌ JSON の形式が正しくない
  • ❌ 配列に必須のフィールドがない
  • ❌ サードパーティサービスが停止している

必ず確認してください。

  1. ここで失敗する可能性があるのは何ですか?
  2. どのように対処すべきですか?
  3. 再試行、ログ記録、例外のスロー、エラーの返却のどれが必要ですか?
  4. ユーザーには何を表示すべきですか?

基本ルール(必須)

  • すべての PHP ファイルの先頭に declare(strict_types=1) を記述する
  • すべてのプロパティ、パラメーター、戻り値に型ヒントを指定する
  • PSR-1、PSR-2、PSR-4、PSR-12 に従う
  • public プロパティは使用しない — ゲッター/セッター、またはコンストラクタプロモーションの代替手段を使う
  • global キーワードは使用しない
  • extract()、eval()、$$variable は使用しない
  • @ によるエラー抑制は使用しない
  • 短縮形式の開始タグ <? は使用せず、常に <?php を使う
  • 本番環境では var_dump() や print_r() を使用しない
  • SQL 文字列を直接連結しない — 必ずプリペアドステートメントを使う
  • シークレットをハードコードしない — 環境変数を使う

オブジェクト指向プログラミング

カプセル化

内部状態を隠し、必要なものだけを公開してください。すべてのプロパティは、デフォルトで private または protected にしてください。

<?php

declare(strict_types=1);

class BankAccount
{
    private float $balance;
    private array $transactions = [];

    public function __construct(float $initialBalance)
    {
        if ($initialBalance < 0) {
            throw new \InvalidArgumentException('Initial balance cannot be negative');
        }
        $this->balance = $initialBalance;
    }

    public function deposit(float $amount): void
    {
        if ($amount <= 0) {
            throw new \InvalidArgumentException('Deposit amount must be positive');
        }
        $this->balance += $amount;
        $this->transactions[] = ['type' => 'deposit', 'amount' => $amount];
    }

    public function withdraw(float $amount): void
    {
        if ($amount > $this->balance) {
            throw new \DomainException('Insufficient funds');
        }
        $this->balance -= $amount;
        $this->transactions[] = ['type' => 'withdrawal', 'amount' => $amount];
    }

    public function getBalance(): float
    {
        return $this->balance;
    }

    public function getTransactions(): array
    {
        return $this->transactions;
    }
}
抽象化

実装ではなくインターフェースに依存してください。インターフェースと抽象クラスで契約を定義します。

<?php

declare(strict_types=1);

// Define the contract
interface PaymentGatewayInterface
{
    public function charge(float $amount, string $currency): PaymentResult;
    public function refund(string $transactionId): bool;
}

// One implementation
class StripeGateway implements PaymentGatewayInterface
{
    public function charge(float $amount, string $currency): PaymentResult
    {
        // Stripe-specific implementation
    }

    public function refund(string $transactionId): bool
    {
        // Stripe-specific implementation
    }
}

// Another implementation — same contract
class PaystackGateway implements PaymentGatewayInterface
{
    public function charge(float $amount, string $currency): PaymentResult
    {
        // Paystack-specific implementation
    }

    public function refund(string $transactionId): bool
    {
        // Paystack-specific implementation
    }
}

// Consumer depends on the interface, not the implementation
class OrderService
{
    private PaymentGatewayInterface $gateway;

    public function __construct(PaymentGatewayInterface $gateway)
    {
        $this->gateway = $gateway;
    }

    public function checkout(Order $order): void
    {
        $result = $this->gateway->charge($order->getTotal(), $order->getCurrency());
        // ...
    }
}
継承よりコンポジション

クラスを拡張するより、協調オブジェクトを注入することを優先してください。継承は強い結合を生みます。

<?php

declare(strict_types=1);

// BAD — deep inheritance chain
class Animal {}
class Dog extends Animal {}
class ServiceDog extends Dog {} // fragile, tightly coupled

// GOOD — compose behaviours
interface CanBark
{
    public function bark(): string;
}

interface CanFetch
{
    public function fetch(): string;
}

class BarkBehaviour implements CanBark
{
    public function bark(): string
    {
        return 'Woof!';
    }
}

class FetchBehaviour implements CanFetch
{
    public function fetch(): string
    {
        return 'Fetching...';
    }
}

class Dog
{
    private CanBark $barkBehaviour;
    private CanFetch $fetchBehaviour;

    public function __construct(CanBark $barkBehaviour, CanFetch $fetchBehaviour)
    {
        $this->barkBehaviour = $barkBehaviour;
        $this->fetchBehaviour = $fetchBehaviour;
    }

    public function bark(): string
    {
        return $this->barkBehaviour->bark();
    }

    public function fetch(): string
    {
        return $this->fetchBehaviour->fetch();
    }
}
ポリモーフィズム

異なるクラスでも、同じインターフェースを持てば、呼び出し側はどの実装を使っているかを知る必要はありません。

<?php

declare(strict_types=1);

interface NotificationInterface
{
    public function send(string $recipient, string $message): void;
}

class EmailNotification implements NotificationInterface
{
    public function send(string $recipient, string $message): void
    {
        // send via email
    }
}

class SmsNotification implements NotificationInterface
{
    public function send(string $recipient, string $message): void
    {
        // send via SMS
    }
}

class SlackNotification implements NotificationInterface
{
    public function send(string $recipient, string $message): void
    {
        // send via Slack
    }
}

class NotificationService
{
    /** @var NotificationInterface[] */
    private array $channels;

    public function __construct(NotificationInterface ...$channels)
    {
        $this->channels = $channels;
    }

    public function notify(string $recipient, string $message): void
    {
        foreach ($this->channels as $channel) {
            $channel->send($recipient, $message); // polymorphic — doesn't care which channel
        }
    }
}

SOLID 原則

S — 単一責任の原則

クラスが変更される理由は、1つだけであるべきです。

// BAD — does too many things
class User
{
    public function save(): void { /* DB logic */ }
    public function sendWelcomeEmail(): void { /* Email logic */ }
    public function validateEmail(): bool { /* Validation logic */ }
}

// GOOD — each class has one job
class User { /* Only user state */ }
class UserRepository { /* Only DB persistence */ }
class UserMailer { /* Only email */ }
class EmailValidator { /* Only validation */ }
O — オープン/クローズドの原則

拡張に対して開き、変更に対して閉じているべきです。

<?php

declare(strict_types=1);

interface DiscountInterface
{
    public function apply(float $price): float;
}

class PercentageDiscount implements DiscountInterface
{
    private float $percentage;

    public function __construct(float $percentage)
    {
        $this->percentage = $percentage;
    }

    public function apply(float $price): float
    {
        return $price * (1 - $this->percentage / 100);
    }
}

class FlatDiscount implements DiscountInterface
{
    private float $amount;

    public function __construct(float $amount)
    {
        $this->amount = $amount;
    }

    public function apply(float $price): float
    {
        return max(0, $price - $this->amount);
    }
}

// Adding a new discount type doesn't touch existing code
class BuyOneGetOneDiscount implements DiscountInterface
{
    public function apply(float $price): float
    {
        return $price / 2;
    }
}
L — リスコフの置換原則

サブタイプは、正しさを損なうことなく、基底型と置き換え可能でなければなりません。

<?php

declare(strict_types=1);

abstract class Shape
{
    abstract public function area(): float;
}

class Rectangle extends Shape
{
    private float $width;
    private float $height;

    public function __construct(
        float $width,
        float $height
    ) {
        $this->width = $width;
        $this->height = $height;
    }

    public function area(): float
    {
        return $this->width * $this->height;
    }
}

class Circle extends Shape
{
    private float $radius;

    public function __construct(float $radius)
    {
        $this->radius = $radius;
    }

    public function area(): float
    {
        return M_PI * $this->radius ** 2;
    }
}

// Works with any Shape subtype — LSP satisfied
function printArea(Shape $shape): void
{
    echo $shape->area();
}
I — インターフェース分離の原則

クラスに、使用しないメソッドの実装を強制してはいけません。肥大化したインターフェースは分割してください。

// BAD — fat interface
interface WorkerInterface
{
    public function work(): void;
    public function eat(): void;
    public function sleep(): void;
}

// GOOD — segregated interfaces
interface WorkableInterface
{
    public function work(): void;
}

interface EatableInterface
{
    public function eat(): void;
}

class HumanWorker implements WorkableInterface, EatableInterface
{
    public function work(): void { /* ... */ }
    public function eat(): void { /* ... */ }
}

class RobotWorker implements WorkableInterface
{
    public function work(): void { /* ... */ }
    // robots don't eat — not forced to implement it
}
D — 依存性逆転の原則

高水準モジュールは低水準モジュールに依存してはいけません。両者とも抽象に依存するべきです。

<?php

declare(strict_types=1);

// Abstraction
interface LoggerInterface
{
    public function log(string $message): void;
}

// Low-level module
class FileLogger implements LoggerInterface
{
    public function log(string $message): void
    {
        file_put_contents('/var/log/app.log', $message . PHP_EOL, FILE_APPEND);
    }
}

// High-level module depends on abstraction, not FileLogger directly
class OrderProcessor
{
    private LoggerInterface $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function process(Order $order): void
    {
        $this->logger->log("Processing order #{$order->getId()}");
        // ...
    }
}

デザインパターン

リポジトリパターン

データアクセスを抽象化します。ビジネスロジックがデータベースに直接アクセスすることはありません。

<?php

declare(strict_types=1);

interface UserRepositoryInterface
{
    public function findById(int $id): ?User;
    public function findByEmail(string $email): ?User;
    /** @return User[] */
    public function findAll(): array;
    public function save(User $user): void;
    public function delete(int $id): void;
}

class PdoUserRepository implements UserRepositoryInterface
{
    private \PDO $db;

    public function __construct(\PDO $db)
    {
        $this->db = $db;
    }

    public function findById(int $id): ?User
    {
        $stmt = $this->db->prepare('SELECT * FROM users WHERE id = :id LIMIT 1');
        $stmt->execute(['id' => $id]);
        $row = $stmt->fetch(\PDO::FETCH_ASSOC);

        return $row ? User::fromArray($row) : null;
    }

    public function findByEmail(string $email): ?User
    {
        $stmt = $this->db->prepare('SELECT * FROM users WHERE email = :email LIMIT 1');
        $stmt->execute(['email' => $email]);
        $row = $stmt->fetch(\PDO::FETCH_ASSOC);

        return $row ? User::fromArray($row) : null;
    }

    public function findAll(): array
    {
        $stmt = $this->db->query('SELECT * FROM users');
        return array_map(fn($row) => User::fromArray($row), $stmt->fetchAll(\PDO::FETCH_ASSOC));
    }

    public function save(User $user): void
    {
        if ($user->getId() === null) {
            $stmt = $this->db->prepare(
                'INSERT INTO users (email, password_hash, created_at) VALUES (:email, :password_hash, :created_at)'
            );
        } else {
            $stmt = $this->db->prepare(
                'UPDATE users SET email = :email, password_hash = :password_hash WHERE id = :id'
            );
        }
        $stmt->execute($user->toArray());
    }

    public function delete(int $id): void
    {
        $stmt = $this->db->prepare('DELETE FROM users WHERE id = :id');
        $stmt->execute(['id' => $id]);
    }
}
サービスレイヤー

ビジネスロジックをここに配置します。リポジトリを調整し、検証を行い、イベントをディスパッチします。

<?php

declare(strict_types=1);

class UserService
{
    private UserRepositoryInterface $users;
    private PasswordHasherInterface $hasher;
    private NotificationInterface $notifier;

    public function __construct(
        UserRepositoryInterface $users,
        PasswordHasherInterface $hasher,
        NotificationInterface $notifier
    ) {
        $this->users = $users;
        $this->hasher = $hasher;
        $this->notifier = $notifier;
    }

    public function register(string $email, string $password): User
    {
        if ($this->users->findByEmail($email)) {
            throw new DuplicateEmailException("Email already registered: {$email}");
        }

        $user = new User(
            new Email($email),
            $this->hasher->hash($password)
        );

        $this->users->save($user);
        $this->notifier->send($email, 'Welcome!');

        return $user;
    }

    public function changePassword(int $userId, string $currentPassword, string $newPassword): void
    {
        $user = $this->users->findById($userId);

        if (!$user) {
            throw new UserNotFoundException($userId);
        }

        if (!$this->hasher->verify($currentPassword, $user->getPasswordHash())) {
            throw new InvalidPasswordException();
        }

        $user->setPasswordHash($this->hasher->hash($newPassword));
        $this->users->save($user);
    }
}
値オブジェクト

ドメインの概念を表す、不変で自己検証するオブジェクトです。

<?php

declare(strict_types=1);

final class Email
{
    private string $value;

    public function __construct(string $value)
    {
        $value = strtolower(trim($value));

        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException("Invalid email address: {$value}");
        }

        $this->value = $value;
    }

    public function getValue(): string
    {
        return $this->value;
    }

    public function equals(self $other): bool
    {
        return $this->value === $other->value;
    }

    public function __toString(): string
    {
        return $this->value;
    }
}

final class Money
{
    private int $amount; // store in smallest unit (cents)
    private string $currency;

    public function __construct(int $amount, string $currency)
    {
        if ($amount < 0) {
            throw new \InvalidArgumentException('Amount cannot be negative');
        }
        $this->amount = $amount;
        $this->currency = strtoupper($currency);
    }

    public function add(self $other): self
    {
        if ($this->currency !== $other->currency) {
            throw new \DomainException('Cannot add different currencies');
        }
        return new self($this->amount + $other->amount, $this->currency);
    }

    public function getAmount(): int
    {
        return $this->amount;
    }

    public function getCurrency(): string
    {
        return $this->currency;
    }
}
ファクトリーパターン

複雑なオブジェクトの生成を一元化します。

<?php

declare(strict_types=1);

interface NotificationInterface
{
    public function send(string $recipient, string $message): void;
}

class NotificationFactory
{
    public function create(string $channel): NotificationInterface
    {
        switch ($channel) {
            case 'email':
                return new EmailNotification();
            case 'sms':
                return new SmsNotification();
            case 'slack':
                return new SlackNotification();
            default:
                throw new \InvalidArgumentException("Unknown channel: {$channel}");
        }
    }
}
ストラテジーパターン

利用側を変更せずに、実行時にアルゴリズムを切り替えます。

<?php

declare(strict_types=1);

interface SortStrategyInterface
{
    /** @param int[] $data */
    public function sort(array $data): array;
}

class BubbleSortStrategy implements SortStrategyInterface
{
    public function sort(array $data): array
    {
        // bubble sort implementation
        return $data;
    }
}

class QuickSortStrategy implements SortStrategyInterface
{
    public function sort(array $data): array
    {
        sort($data);
        return $data;
    }
}

class DataSorter
{
    private SortStrategyInterface $strategy;

    public function __construct(SortStrategyInterface $strategy)
    {
        $this->strategy = $strategy;
    }

    public function setStrategy(SortStrategyInterface $strategy): void
    {
        $this->strategy = $strategy;
    }

    public function sort(array $data): array
    {
        return $this->strategy->sort($data);
    }
}
オブザーバーパターン

イベントの生成側と受信側を疎結合にします。

<?php

declare(strict_types=1);

interface ObserverInterface
{
    public function update(string $event, $payload = null): void;
}

interface ObservableInterface
{
    public function subscribe(string $event, ObserverInterface $observer): void;
    public function notify(string $event, $payload = null): void;
}

class EventEmitter implements ObservableInterface
{
    /** @var array<string, ObserverInterface[]> */
    private array $listeners = [];

    public function subscribe(string $event, ObserverInterface $observer): void
    {
        $this->listeners[$event][] = $observer;
    }

    public function notify(string $event, $payload = null): void
    {
        foreach ($this->listeners[$event] ?? [] as $observer) {
            $observer->update($event, $payload);
        }
    }
}

class UserRegisteredObserver implements ObserverInterface
{
    private NotificationInterface $notifier;

    public function __construct(NotificationInterface $notifier)
    {
        $this->notifier = $notifier;
    }

    public function update(string $event, $payload = null): void
    {
        if ($event === 'user.registered' && $payload instanceof User) {
            $this->notifier->send($payload->getEmail(), 'Welcome!');
        }
    }
}
デコレーターパターン

継承を使わずにオブジェクトの振る舞いを拡張します。

<?php

declare(strict_types=1);

interface LoggerInterface
{
    public function log(string $message): void;
}

class FileLogger implements LoggerInterface
{
    public function log(string $message): void
    {
        file_put_contents('/var/log/app.log', $message . PHP_EOL, FILE_APPEND);
    }
}

class TimestampLogger implements LoggerInterface
{
    private LoggerInterface $inner;

    public function __construct(LoggerInterface $inner)
    {
        $this->inner = $inner;
    }

    public function log(string $message): void
    {
        $this->inner->log('[' . date('Y-m-d H:i:s') . '] ' . $message);
    }
}

class PrefixLogger implements LoggerInterface
{
    private LoggerInterface $inner;
    private string $prefix;

    public function __construct(LoggerInterface $inner, string $prefix)
    {
        $this->inner = $inner;
        $this->prefix = $prefix;
    }

    public function log(string $message): void
    {
        $this->inner->log("[{$this->prefix}] {$message}");
    }
}

// Usage — chain decorators
$logger = new TimestampLogger(new PrefixLogger(new FileLogger(), 'APP'));
$logger->log('User registered'); // [2026-02-21 10:00:00] [APP] User registered
依存性注入コンテナ
<?php

declare(strict_types=1);

class Container
{
    private array $bindings = [];
    private array $instances = [];

    public function bind(string $abstract, callable $factory): void
    {
        $this->bindings[$abstract] = $factory;
    }

    public function singleton(string $abstract, callable $factory): void
    {
        $this->bindings[$abstract] = function () use ($abstract, $factory) {
            if (!isset($this->instances[$abstract])) {
                $this->instances[$abstract] = $factory($this);
            }
            return $this->instances[$abstract];
        };
    }

    public function make(string $abstract): object
    {
        if (isset($this->bindings[$abstract])) {
            return ($this->bindings[$abstract])($this);
        }
        throw new \RuntimeException("No binding registered for: {$abstract}");
    }
}

// Bootstrap
$container = new Container();

$container->singleton(\PDO::class, fn() => new \PDO(
    'mysql:host=' . getenv('DB_HOST') . ';dbname=' . getenv('DB_NAME') . ';charset=utf8mb4',
    getenv('DB_USER'),
    getenv('DB_PASS'),
    [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC]
));

$container->bind(UserRepositoryInterface::class, fn($c) => new PdoUserRepository($c->make(\PDO::class)));
$container->bind(UserService::class, fn($c) => new UserService(
    $c->make(UserRepositoryInterface::class),
    new BcryptPasswordHasher(),
    new EmailNotification()
));

セキュリティ(必須)

SQL — 常にプリペアドステートメントを使用
// ❌ NEVER
$result = $db->query("SELECT * FROM users WHERE email = '{$email}'");

// ✅ ALWAYS
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);
XSS の防止
// Escape all output
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');

// JSON output
header('Content-Type: application/json');
echo json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);
CSRF 対策
// Generate
$_SESSION['csrf_token'] ??= bin2hex(random_bytes(32));

// Validate
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) {
    throw new CsrfException('Invalid CSRF token');
}
パスワード
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
if (!password_verify($input, $storedHash)) {
    throw new AuthenticationException();
}
ファイルアップロード
function validateUpload(array $file, array $allowedMimes): void
{
    $finfo = new \finfo(FILEINFO_MIME_TYPE);
    $mime = $finfo->file($file['tmp_name']);

    if (!in_array($mime, $allowedMimes, true)) {
        throw new \InvalidArgumentException("File type not allowed: {$mime}");
    }

    if ($file['size'] > 5 * 1024 * 1024) {
        throw new \RuntimeException('File exceeds 5MB limit');
    }
}

エラー処理

<?php

declare(strict_types=1);

// Exception hierarchy
class AppException extends \RuntimeException {}
class NotFoundException extends AppException {}
class ValidationException extends AppException
{
    private array $errors;

    public function __construct(array $errors)
    {
        parent::__construct('Validation failed');
        $this->errors = $errors;
    }

    public function getErrors(): array
    {
        return $this->errors;
    }
}
class AuthenticationException extends AppException {}
class DuplicateEmailException extends AppException {}

// Global handler
set_exception_handler(function (\Throwable $e) {
    error_log(sprintf(
        '[%s] %s in %s:%d',
        get_class($e),
        $e->getMessage(),
        $e->getFile(),
        $e->getLine()
    ));

    http_response_code($e instanceof NotFoundException ? 404 : 500);

    if (getenv('APP_ENV') === 'production') {
        echo json_encode(['error' => 'Something went wrong']);
    } else {
        echo json_encode(['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
    }
});

データベース(PDO のベストプラクティス)

<?php

declare(strict_types=1);

class Database
{
    private static ?\PDO $instance = null;

    public static function getInstance(): \PDO
    {
        if (self::$instance === null) {
            self::$instance = new \PDO(
                sprintf(
                    'mysql:host=%s;dbname=%s;charset=utf8mb4',
                    getenv('DB_HOST'),
                    getenv('DB_NAME')
                ),
                getenv('DB_USER'),
                getenv('DB_PASS'),
                [
                    \PDO::ATTR_ERRMODE            => \PDO::ERRMODE_EXCEPTION,
                    \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
                    \PDO::ATTR_EMULATE_PREPARES   => false,
                ]
            );
        }
        return self::$instance;
    }
}

// Transactions
function withTransaction(\PDO $pdo, callable $callback)
{
    $pdo->beginTransaction();
    try {
        $result = $callback($pdo);
        $pdo->commit();
        return $result;
    } catch (\Throwable $e) {
        $pdo->rollBack();
        throw $e;
    }
}

パフォーマンス

  • php.ini で OPcache を有効にします — opcache.enable=1
  • PDO::FETCH_OBJ ではなく PDO::FETCH_ASSOC を使用します(より高速)
  • N+1 クエリを避け、JOIN または一括取得を使用します
  • コストの高い処理は APCu またはファイルベースのキャッシュでキャッシュします
  • 表現が明確になる場合は array_map、array_filter、array_reduce を使用します
  • 必要のないファイルの読み込みを避け、PSR-4 オートローディングを活用します
  • 可能な場合は array_key_exists() より isset() を優先します(より高速)
  • while + each() より foreach を優先します(7.2 で非推奨)
  • 呼び出しの繰り返しを避けるため、ループ条件の外で count() を使用します
// BAD — count() called every iteration
for ($i = 0; $i < count($items); $i++) { }

// GOOD — count() called once
$total = count($items);
for ($i = 0; $i < $total; $i++) { }

入力検証

<?php

declare(strict_types=1);

class Validator
{
    private array $errors = [];

    public function required(string $field, $value): self
    {
        if (empty($value) && $value !== '0') {
            $this->errors[$field][] = "{$field} is required";
        }
        return $this;
    }

    public function email(string $field, string $value): self
    {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            $this->errors[$field][] = "{$field} must be a valid email";
        }
        return $this;
    }

    public function minLength(string $field, string $value, int $min): self
    {
        if (mb_strlen($value) < $min) {
            $this->errors[$field][] = "{$field} must be at least {$min} characters";
        }
        return $this;
    }

    public function maxLength(string $field, string $value, int $max): self
    {
        if (mb_strlen($value) > $max) {
            $this->errors[$field][] = "{$field} must not exceed {$max} characters";
        }
        return $this;
    }

    public function integer(string $field, $value): self
    {
        if (!filter_var($value, FILTER_VALIDATE_INT)) {
            $this->errors[$field][] = "{$field} must be an integer";
        }
        return $this;
    }

    public function passes(): bool
    {
        return empty($this->errors);
    }

    public function getErrors(): array
    {
        return $this->errors;
    }

    public function validate(): void
    {
        if (!$this->passes()) {
            throw new ValidationException($this->errors);
        }
    }
}

テスト(PHPUnit)

<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;

class UserServiceTest extends TestCase
{
    private UserService $service;
    private UserRepositoryInterface $repository;
    private PasswordHasherInterface $hasher;

    protected function setUp(): void
    {
        $this->repository = $this->createMock(UserRepositoryInterface::class);
        $this->hasher     = $this->createMock(PasswordHasherInterface::class);
        $this->service    = new UserService(
            $this->repository,
            $this->hasher,
            $this->createMock(NotificationInterface::class)
        );
    }

    public function testRegisterCreatesUserSuccessfully(): void
    {
        $this->repository->method('findByEmail')->willReturn(null);
        $this->hasher->method('hash')->willReturn('hashed_password');
        $this->repository->expects($this->once())->method('save');

        $user = $this->service->register('[email protected]', 'password123');

        $this->assertInstanceOf(User::class, $user);
    }

    public function testRegisterThrowsOnDuplicateEmail(): void
    {
        $this->repository
            ->method('findByEmail')
            ->willReturn(new User(new Email('[email protected]'), 'hash'));

        $this->expectException(DuplicateEmailException::class);

        $this->service->register('[email protected]', 'password123');
    }
}

静的解析

PHP 7.4 にはネイティブのユニオン型がないため、実行時より前に型エラーを検出できるよう、静的解析ツールを使用してください。

PHPStan
composer require --dev phpstan/phpstan

phpstan.neon

parameters:
    level: 8
    paths:
        - src
        - tests
    excludePaths:
        - tests/bootstrap.php

実行:

vendor/bin/phpstan analyse
Psalm
composer require --dev vimeo/psalm

psalm.xml

<?xml version="1.0"?>
<psalm
    errorLevel="3"
    resolveFromConfigFile="true"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="https://getpsalm.org/schema/config"
    xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
    <projectFiles>
        <directory name="src" />
        <ignoreFiles>
            <directory name="vendor" />
        </ignoreFiles>
    </projectFiles>
</psalm>

実行:

vendor/bin/psalm
高度な型のための PHPDoc

PHP 7.4 でネイティブに表現できない型を記述するには、PHPDoc を使用します。

<?php

declare(strict_types=1);

class UserRepository
{
    /**
     * @return array<int, User>
     */
    public function findAll(): array
    {
        // Returns an array with integer keys and User values
    }

    /**
     * @return array<string, mixed>
     */
    public function toArray(User $user): array
    {
        // Returns associative array with string keys
    }

    /**
     * @param array<string, string> $filters
     * @return User[]
     */
    public function search(array $filters): array
    {
        // Accepts array of string filters, returns list of Users
    }

    /**
     * @param int|string $id
     * @return User|null
     */
    public function find($id): ?User
    {
        // Union types via PHPDoc (not available natively in 7.4)
    }

    /**
     * @return array{id: int, email: string, created_at: string}
     */
    public function getUserData(int $id): array
    {
        // Shaped array with specific keys
    }

    /**
     * @return iterable<User>
     */
    public function getGenerator(): iterable
    {
        // Generic iterable type
    }

    /**
     * @template T
     * @param class-string<T> $className
     * @return T
     */
    public function make(string $className): object
    {
        // Generic template types
    }
}

主な PHPDoc アノテーション:

  • @param Type $name — 引数の型
  • @return Type — 戻り値の型
  • @var Type — プロパティまたは変数の型
  • @throws ExceptionClass — スローされる例外を記述
  • array<KeyType, ValueType> — 型付き配列
  • array{key: Type, ...} — 形状付き配列
  • Type1|Type2 — ユニオン型
  • Type[] — array<int, Type> の短縮表記
  • @template T — ジェネリクス
  • class-string<T> — クラス名を表す文字列

プロジェクト構成(PSR-4)

src/
├── Controllers/       # HTTP layer only — no business logic
├── Services/          # Business logic
├── Repositories/      # Data access
├── Models/            # Domain entities
├── ValueObjects/      # Immutable domain concepts
├── Exceptions/        # Custom exception hierarchy
├── Interfaces/        # All contracts/interfaces
├── Factories/         # Object creation
└── Support/           # Helpers, utilities

tests/
├── Unit/
└── Integration/

public/
└── index.php          # Entry point

config/
└── container.php      # DI bindings

.env
composer.json
composer.json の例
{
    "name": "vendor/project-name",
    "description": "PHP 7.4 application",
    "type": "project",
    "require": {
        "php": "^7.4"
    },
    "require-dev": {
        "phpunit/phpunit": "^9.5",
        "phpstan/phpstan": "^1.10",
        "squizlabs/php_codesniffer": "^3.7"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "App\\Tests\\": "tests/"
        }
    },
    "config": {
        "optimize-autoloader": true,
        "preferred-install": "dist",
        "sort-packages": true
    },
    "scripts": {
        "test": "phpunit",
        "analyse": "phpstan analyse",
        "cs": "phpcs --standard=PSR12 src tests"
    }
}

この設定では:

  • App\Controllers\UserController → src/Controllers/UserController.php
  • App\Services\UserService → src/Services/UserService.php
  • App\Tests\Unit\UserServiceTest → tests/Unit/UserServiceTest.php

新しいクラスを追加した後は、composer dump-autoload を実行してください。


必ず行うこと/行ってはいけないこと

✅ 必ず行うこと
  • すべてのファイルで declare(strict_types=1) を指定する
  • すべてのプロパティ、パラメーター、戻り値に型ヒントを付ける
  • すべての DB クエリでプリペアドステートメントを使用する
  • 具象クラスではなくインターフェースに依存する
  • インフラストラクチャの依存関係には DI を使用する。値オブジェクトや単純な内部構築では new を使用してもかまいません
  • クラスは小さく、役割を明確に保つ(SRP)
  • すべてのユーザー入力を使用前に検証する
  • password_hash() / password_verify() を使用する
  • 配列や複雑な型には PHPDoc を記述する
  • モックを使い、インターフェース経由でテストする
❌ 絶対に行わないこと
  • PHP 7.4 のコードベースで PHP 8.x の機能を使用する
  • 文字列連結で生の SQL を記述する
  • シークレットをコードに保存する。.env を使用する
  • global キーワードを使用する
  • extract()、eval()、$$variable を使用する
  • @ でエラーを抑制する
  • コントローラーにビジネスロジックを記述する
  • あらゆる処理を行う神クラスを作成する
  • コンポジションを使うべき場面で継承する
  • エラーハンドリングを省略する。例外を決して黙って握りつぶさない

コメント