php74-expert について
php74-expertは、フレームワークを前提としないPHP 7.4開発向けのコーディングスキルです。互換性のある構文や既存の規約に沿い、入力検証とパフォーマンスにも配慮した実装を案内します。
主な機能
- 01PHP 7.4の構文と機能の指針
- 02名前空間とインポートの規約
- 03過剰な検証の回避
- 04コードベースと依存関係の確認
- 05パフォーマンスとメモリの指針
- 06障害時の処理
活用シーン
- 01PHP 7.4プロジェクトでの機能実装
- 02PHP 8以降の構文の混入チェック
- 03スキーマに沿ったリポジトリ設計
- 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 でよくあるメモリーリーク:
- メモリー上の大きな配列
- 循環参照(PHP 7.4 では GC によりまれですが、それでも発生する可能性があります)
- リソースを閉じない(ファイルハンドル、データベースカーソル)
- セッションにデータを入れすぎる
悪い例 — すべてをメモリーに読み込む:
<?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 の形式が正しくない
- ❌ 配列に必須のフィールドがない
- ❌ サードパーティサービスが停止している
必ず確認してください。
- ここで失敗する可能性があるのは何ですか?
- どのように対処すべきですか?
- 再試行、ログ記録、例外のスロー、エラーの返却のどれが必要ですか?
- ユーザーには何を表示すべきですか?
基本ルール(必須)
- すべての 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=1PDO::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.phpApp\Services\UserService→src/Services/UserService.phpApp\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を使用する@でエラーを抑制する- コントローラーにビジネスロジックを記述する
- あらゆる処理を行う神クラスを作成する
- コンポジションを使うべき場面で継承する
- エラーハンドリングを省略する。例外を決して黙って握りつぶさない