mcpField
P
php-development

php-development

github.com/practicalswan開発2366 回インストール2 週間前に更新
リポジトリ

php-development について

php-development は、PHP 開発者向けのガイダンススキルです。PHP 8.x、PDO によるデータアクセス、REST API 設計を扱い、セキュリティ実践、検証手順、XAMPP の設定も案内します。

主な機能

  1. 01PHP 8.x の開発ガイダンス
  2. 02PDO とプリペアドステートメント
  3. 03REST API と認証の設計
  4. 04入力検証と XSS・CSRF 対策
  5. 05XAMPP 環境の設定ガイド

活用シーン

  1. 01PHP REST API の構築
  2. 02PDO による安全なデータベース操作
  3. 03XAMPP 開発環境の設定
  4. 04PHP の変更レビューと境界条件の検証

MCP 対応

MCP サーバーは必須ではありません。README には、ローカルの手順と検証を使う方法が記載されています。

インストール

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

$npx skills add https://github.com/practicalswan/agent-skills --skill php-development

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

CHANGELOG.md
LICENSE.txt
examples/
pdo-database-patterns.php
references/
php-8-4-api-patterns-2026.md
scripts/
xampp-setup.ps1

詳細

PHP 開発

現行の PHP 8.x リリース、PHPUnit 11+、Composer 2.x、および PDO を利用する MySQL または MariaDB アプリ向けに最適化されています。

PHP 8.0+、安全なデータベースアクセスのための PDO、RESTful API 設計、XAMPP 環境の設定について、https://php.net の公式 PHP ドキュメントに沿った、高品質な PHP アプリケーションを構築するための専門的なガイダンスです。

  • 利用可能な場合は、ネイティブの並列サブエージェントディスパッチと 200k+ のコンテキストウィンドウを活用します。

クライアント間の移植性

このスキルは、GitHub Copilot、Claude Code、Codex で利用できるように作られています。

  • GitHub Copilot:フォルダーを Copilot から参照できるスキルパスに配置するか、フォルダーを検出できない場合はワークフローをプロジェクトの指示に組み込みます。
  • Claude Code:フォルダーをローカルのスキルディレクトリまたは互換性のあるプラグインソースに配置します。
  • Codex:フォルダーを $CODEX_HOME/skills/php-development にインストールまたは同期し、大きな変更を加えた後に Codex を再起動します。

MCP の利用可否とフォールバック

優先 MCP サーバー:不要

  • フォールバックプロンプト:「MCP を使わずに PHP Development スキルを使用してください。ローカルの指示、同梱リソース、標準のシェルまたはエディターツール、直接的な検証を活用してください。結論を出す前に、使用した証拠を提示してください。」
  • アクティブなホストが MCP 操作を提供していない場合は、その操作を使用したと主張しないでください。
  • フォールバックの証拠には、ローカルファイル、テスト、レンダリング結果、ログ、スクリーンショットを使用してください。

アンチパターン

  • SQL の直接補間:PHP のデータアクセスにおいて、正確性とセキュリティの基準となるのはプリペアドステートメントです。
  • リクエストの解析、ビジネスルール、レンダリングの混在:密結合したスクリプトはテストしにくく、API への発展も困難です。
  • バリデーションだけで XSS を防げると考えること:ユーザーが制御するコンテンツを HTML に再表示する際には、出力エンコーディングも重要です。

検証プロトコル

「スキルが正常に適用されました」と主張する前に、次を確認してください。

  1. 合否:PHP Development の実装で、対象ランタイム、フレームワークのバージョン、影響を受けるファイルが明示されています。
  2. 合否:変更箇所に対して、ビルド、Lint、テスト、または同等のローカル検証が実行されています。
  3. 合否:エラー、依存関係のドリフト、環境の違いに関するエッジケースに対処しているか、明示的に対象外としています。
  4. 負荷テストシナリオ:正常系のテストには合格するものの、境界条件を 1 つ満たさない変更にワークフローを適用します。
  5. 成功指標:未検証の成功主張がゼロであること。実装に関するすべての主張を、コマンドまたは成果物に対応付けます。

適用前と適用後の例

<?php
// Before
$stmt = $pdo->query("SELECT * FROM users WHERE email = '$email'");
$user = $stmt->fetch();

// After
$stmt = $pdo->prepare('SELECT id, email, password_hash FROM users WHERE email = :email LIMIT 1');
$stmt->execute(['email' => $email]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);

文字列補間をプリペアドステートメントに置き換え、取得する結果の形を絞り込みます。

適用条件

症状 -> アクションのトリガーを使用します。該当するものがあれば、このスキルを適用し、以下のプロトコルに従って検証してください。

PHP 開発の基本:

  • 適切な HTTP メソッドを使用した PHP RESTful API の構築
  • XAMPP(Apache + MySQL + PHP)環境での作業
  • PDO を使用した安全なデータベース操作の実装
  • 認証およびセッション管理システムの作成
  • ファイルアップロードとフォーム送信の処理

データベースとデータレイヤー:

  • PDO を使用した PHP と MySQL/MariaDB の接続
  • SQL インジェクションを防ぐプリペアドステートメントの記述
  • データ整合性を保つトランザクション処理の実装
  • データアクセスのためのリポジトリパターンの作成
  • MySQLi と PDO の比較

セキュリティとベストプラクティス:

  • パスワードハッシュ化(password_hash、password_verify)の実装
  • XSS、CSRF、SQL インジェクションへの対策
  • ユーザー入力の検証とサニタイズ
  • セッションと認証トークンの管理
  • API アクセス用の CORS ヘッダーの設定

API 開発:

  • 適切な HTTP ステータスコードを使った RESTful エンドポイントの設計
  • JSON リクエストとレスポンスの処理
  • 認証と認可のためのミドルウェアの実装
  • エラー処理とログ記録
  • レート制限と API バージョニング

パート 1:PHP 8.0+ の基礎

最新の PHP 機能
<?php
// Named arguments (PHP 8.0+)
function createUser(string $name, string $email, bool $isAdmin = false): User {
    return new User($name, $email, $isAdmin);
}

// Call with named arguments
$user = createUser(email: '[email protected]', name: 'John Doe');

// Union types (PHP 8.0+)
function processValue(string|int|float $value): string {
    return (string)$value;
}

// Nullsafe operator (PHP 8.0+)
$country = $session?->user?->address?->country ?? 'Unknown';

// Constructor property promotion (PHP 8.0+)
class User {
    public function __construct(
        public string $name,
        public string $email,
        private string $passwordHash
    ) {}
}
型宣言と厳密な型
<?php
declare(strict_types=1); // Enforce type safety

// Typed properties and return types
class Recipe {
    private int $id;
    private string $title;
    private ?DateTime $createdAt;

    public function __construct(int $id, string $title) {
        $this->id = $id;
        $this->title = $title;
    }

    public function getTitle(): string {
        return $this->title;
    }

    public function setCreatedAt(?DateTime $date): void {
        $this->createdAt = $date;
    }
}

// Union and intersection types
function processData(string|array $data): string|int {
    return is_array($data) ? count($data) : strlen($data);
}

パート 2:PDO データベース連携

データベース接続クラス
<?php
class Database {
    private static ?PDO $instance = null;

    public static function getInstance(): PDO {
        if (self::$instance === null) {
            $host = $_ENV['DB_HOST'] ?? 'localhost';
            $dbname = $_ENV['DB_NAME'] ?? 'recipe_sharing_system';
            $username = $_ENV['DB_USER'] ?? 'root';
            $password = $_ENV['DB_PASSWORD'] ?? '';
            $charset = 'utf8mb4';

            $dsn = "mysql:host=$host;dbname=$dbname;charset=$charset";

            try {
                self::$instance = new PDO($dsn, $username, $password, [
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                    PDO::ATTR_EMULATE_PREPARES => false,
                ]);
            } catch (PDOException $e) {
                error_log("Database connection failed: " . $e->getMessage());
                throw new RuntimeException("Database connection error");
            }
        }

        return self::$instance;
    }
}
セキュリティのためのプリペアドステートメント
<?php
class UserRepository {
    private PDO $db;

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

    // Find user by email with prepared statement
    public function findByEmail(string $email): ?array {
        $stmt = $this->db->prepare(
            "SELECT id, email, password_hash, role, status
             FROM user
             WHERE email = :email LIMIT 1"
        );

        $stmt->bindParam(':email', $email, PDO::PARAM_STR);
        $stmt->execute();

        $user = $stmt->fetch();
        return $user ?: null;
    }

    // Create new user with password hashing
    public function create(string $name, string $email, string $password): int {
        $passwordHash = password_hash($password, PASSWORD_DEFAULT);

        $stmt = $this->db->prepare(
            "INSERT INTO user (name, email, password_hash, role, status, created_at, updated_at)
             VALUES (:name, :email, :password_hash, 'user', 'active', NOW(), NOW())"
        );

        $stmt->bindParam(':name', $name, PDO::PARAM_STR);
        $stmt->bindParam(':email', $email, PDO::PARAM_STR);
        $stmt->bindParam(':password_hash', $passwordHash, PDO::PARAM_STR);

        $stmt->execute();

        return (int) $this->db->lastInsertId();
    }

    // Authentication with password verification
    public function authenticate(string $email, string $password): ?array {
        $user = $this->findByEmail($email);

        if ($user === null) {
            return null;
        }

        if (!password_verify($password, $user['password_hash'])) {
            return null;
        }

        // Check if password needs rehash
        if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) {
            $newHash = password_hash($password, PASSWORD_DEFAULT);
            $this->updatePasswordHash($user['id'], $newHash);
        }

        unset($user['password_hash']); // Remove sensitive data
        return $user;
    }

    private function updatePasswordHash(int $userId, string $hash): void {
        $stmt = $this->db->prepare(
            "UPDATE user SET password_hash = :hash WHERE id = :id"
        );
        $stmt->execute([':hash' => $hash, ':id' => $userId]);
    }
}
トランザクション管理
<?php
class RecipeService {
    private PDO $db;

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

    // Create recipe with ingredients, instructions, and images in a transaction
    public function createRecipeWithDetails(array $recipeData, array $ingredients, array $instructions): int {
        try {
            $this->db->beginTransaction();

            // Insert recipe
            $stmt = $this->db->prepare(
                "INSERT INTO recipe (title, description, category, difficulty, prep_time, cook_time, servings, author_id, status, created_at, updated_at)
                 VALUES (:title, :description, :category, :difficulty, :prep_time, :cook_time, :servings, :author_id, 'pending', NOW(), NOW())"
            );

            $stmt->execute([
                ':title' => $recipeData['title'],
                ':description' => $recipeData['description'],
                ':category' => $recipeData['category'],
                ':difficulty' => $recipeData['difficulty'],
                ':prep_time' => $recipeData['prepTime'],
                ':cook_time' => $recipeData['cookTime'],
                ':servings' => $recipeData['servings'],
                ':author_id' => $recipeData['authorId'],
            ]);

            $recipeId = (int) $this->db->lastInsertId();

            // Insert ingredients
            $ingredientStmt = $this->db->prepare(
                "INSERT INTO ingredient (recipe_id, name, quantity, unit, sort_order, created_at, updated_at)
                 VALUES (:recipe_id, :name, :quantity, :unit, :sort_order, NOW(), NOW())"
            );

            foreach ($ingredients as $index => $ingredient) {
                $ingredientStmt->execute([
                    ':recipe_id' => $recipeId,
                    ':name' => $ingredient['name'],
                    ':quantity' => $ingredient['quantity'],
                    ':unit' => $ingredient['unit'],
                    ':sort_order' => $index,
                ]);
            }

            // Insert instructions
            $instructionStmt = $this->db->prepare(
                "INSERT INTO instruction (recipe_id, step_number, instruction_text, created_at, updated_at)
                 VALUES (:recipe_id, :step_number, :instruction_text, NOW(), NOW())"
            );

            foreach ($instructions as $index => $instruction) {
                $instructionStmt->execute([
                    ':recipe_id' => $recipeId,
                    ':step_number' => $index + 1,
                    ':instruction_text' => $instruction['text'],
                ]);
            }

            $this->db->commit();

            return $recipeId;

        } catch (Exception $e) {
            $this->db->rollBack();
            error_log("Failed to create recipe: " . $e->getMessage());
            throw $e;
        }
    }
}

パート 3:RESTful API の開発

JSON レスポンスヘルパー
<?php
class Response {
    public static function json(mixed $data, int $statusCode = 200): never {
        http_response_code($statusCode);
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
        exit;
    }

    public static function error(string $message, int $statusCode = 400): never {
        self::json([
            'success' => false,
            'error' => $message,
        ], $statusCode);
    }

    public static function success(mixed $data = null, string $message = 'Success'): never {
        self::json([
            'success' => true,
            'message' => $message,
            'data' => $data,
        ]);
    }
}
CORS ミドルウェア
<?php
// Handle CORS headers
$allowedOrigins = [
    'http://localhost:5173', // Vite dev server
    'http://localhost:3000',  // Alternative dev server
];

$origin = $_SERVER['HTTP_ORIGIN'] ?? '';

if (in_array($origin, $allowedOrigins, true)) {
    header("Access-Control-Allow-Origin: $origin");
}

header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Allow-Credentials: true');

// Handle preflight requests
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(200);
    exit;
}
認証ミドルウェア
<?php
function requireAuth(): array {
    $authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';

    if (!preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
        Response::error('Unauthorized: Missing or invalid token', 401);
    }

    $token = $matches[1];

    // Validate token (example withJWT)
    try {
        $payload = JWT::decode($token, $_ENV['JWT_SECRET'], ['HS256']);
        return (array) $payload;
    } catch (Exception $e) {
        Response::error('Unauthorized: Invalid token', 401);
    }
}

function requireAdmin(): array {
    $user = requireAuth();

    if ($user['role'] !== 'admin') {
        Response::error('Forbidden: Admin access required', 403);
    }

    return $user;
}
API コントローラーの例
<?php
require_once '../../config/database.php';
require_once '../../middleware/cors.php';
require_once '../../utils/response.php';

class RecipeController {
    private PDO $db;

    public function __construct() {
        $this->db = Database::getInstance();
    }

    // GET /api/recipes - Get all published recipes
    public function index(): void {
        $category = $_GET['category'] ?? null;
        $difficulty = $_GET['difficulty'] ?? null;
        $search = $_GET['search'] ?? null;
        $limit = (int)($_GET['limit'] ?? 20);
        $offset = (int)($_GET['offset'] ?? 0);

        $query = "SELECT r.*, u.name as author_name,
                         COUNT(DISTINCT rv.id) as view_count,
                         COUNT(DISTINCT lr.id) as like_count,
                         AVG(rev.rating) as average_rating
                  FROM recipe r
                  JOIN user u ON r.author_id = u.id
                  LEFT JOIN recipe_view rv ON r.id = rv.recipe_id
                  LEFT JOIN like_record lr ON r.id = lr.recipe_id
                  LEFT JOIN review rev ON r.id = rev.recipe_id
                  WHERE r.status = 'published'";

        $params = [];

        if ($category !== null) {
            $query .= " AND r.category = :category";
            $params[':category'] = $category;
        }

        if ($difficulty !== null) {
            $query .= " AND r.difficulty = :difficulty";
            $params[':difficulty'] = $difficulty;
        }

        if ($search !== null) {
            $query .= " AND (r.title LIKE :search OR r.description LIKE :search)";
            $searchTerm = "%$search%";
            $params[':search'] = $searchTerm;
            $params[':search2'] = $searchTerm;
        }

        $query .= " GROUP BY r.id ORDER BY r.created_at DESC LIMIT :limit OFFSET :offset";

        $stmt = $this->db->prepare($query);
        $stmt->execute($params);

        $recipes = $stmt->fetchAll();

        Response::success($recipes);
    }

    // GET /api/recipes/:id - Get recipe by ID
    public function show(int $id): void {
        $stmt = $this->db->prepare(
            "SELECT r.*, u.name as author_name, u.email as author_email,
                    GROUP_CONCAT(CONCAT(i.name, ' (', i.quantity, ' ', i.unit, ')') SEPARATOR ', ') as ingredients
             FROM recipe r
             JOIN user u ON r.author_id = u.id
             LEFT JOIN ingredient i ON r.id = i.recipe_id
             WHERE r.id = :id
             GROUP BY r.id"
        );

        $stmt->execute([':id' => $id]);
        $recipe = $stmt->fetch();

        if ($recipe === false) {
            Response::error('Recipe not found', 404);
        }

        // Fetch instructions
        $instStmt = $this->db->prepare(
            "SELECT step_number, instruction_text
             FROM instruction
             WHERE recipe_id = :recipe_id
             ORDER BY step_number"
        );

        $instStmt->execute([':recipe_id' => $id]);
        $recipe['instructions'] = $instStmt->fetchAll();

        Response::success($recipe);
    }

    // POST /api/recipes - Create new recipe
    public function store(): void {
        $user = requireAuth();
        $data = json_decode(file_get_contents('php://input'), true);

        // Validate required fields
        if (empty($data['title']) || empty($data['description'])) {
            Response::error('Title and description are required');
        }

        $recipeData = [
            'title' => $data['title'],
            'description' => $data['description'],
            'category' => $data['category'] ?? 'Uncategorized',
            'difficulty' => $data['difficulty'] ?? 'Medium',
            'prepTime' => (int)($data['prepTime'] ?? 0),
            'cookTime' => (int)($data['cookTime'] ?? 0),
            'servings' => (int)($data['servings'] ?? 1),
            'authorId' => $user['id'],
        ];

        $recipeService = new RecipeService($this->db);

        try {
            $recipeId = $recipeService->createRecipeWithDetails(
                $recipeData,
                $data['ingredients'] ?? [],
                $data['instructions'] ?? []
            );

            Response::success(['id' => $recipeId], 'Recipe created successfully', 201);
        } catch (Exception $e) {
            Response::error('Failed to create recipe: ' . $e->getMessage(), 500);
        }
    }
}

パート 4:入力の検証とサニタイズ

検証関数
<?php
class Validator {
    public static function email(string $email): bool {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }

    public static function string(string $value, int $min = 1, int $max = 255): bool {
        $length = strlen($value);
        return $length >= $min && $length <= $max;
    }

    public static function integer(int $value, int $min = PHP_INT_MIN, int $max = PHP_INT_MAX): bool {
        return $value >= $min && $value <= $max;
    }

    public static function enum(string $value, array $allowed): bool {
        return in_array($value, $allowed, true);
    }

    public static function required(array $data, array $fields): array {
        $errors = [];
        foreach ($fields as $field) {
            if (empty($data[$field])) {
                $errors[] = "$field is required";
            }
        }
        return $errors;
    }

    public static function sanitize(string $input): string {
        return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
    }
}
検証の例
<?php
function validateRecipeData(array $data): array {
    $errors = [];

    // Validate title
    if (empty($data['title'])) {
        $errors[] = 'Title is required';
    } elseif (!Validator::string($data['title'], 3, 200)) {
        $errors[] = 'Title must be between 3 and 200 characters';
    }

    // Validate email
    if (!empty($data['email']) && !Validator::email($data['email'])) {
        $errors[] = 'Invalid email address';
    }

    // Validate rating
    if (isset($data['rating']) && !Validator::integer((int)$data['rating'], 1, 5)) {
        $errors[] = 'Rating must be between 1 and 5';
    }

    // Validate difficulty
    if (!empty($data['difficulty']) &&
        !Validator::enum($data['difficulty'], ['Easy', 'Medium', 'Hard'])) {
        $errors[] = 'Difficulty must be Easy, Medium, or Hard';
    }

    // Sanitize all string inputs
    foreach ($data as $key => $value) {
        if (is_string($value)) {
            $data[$key] = Validator::sanitize($value);
        }
    }

    return ['errors' => $errors, 'data' => $data];
}

パート 5:セキュリティのベストプラクティス

パスワード管理
<?php
class PasswordManager {
    public static function hash(string $password): string {
        // Use algorithm recommended by PHP
        return password_hash($password, PASSWORD_DEFAULT);
    }

    public static function verify(string $password, string $hash): bool {
        return password_verify($password, $hash);
    }

    public static function needsRehash(string $hash): bool {
        return password_needs_rehash($hash, PASSWORD_DEFAULT);
    }

    public static function validateStrength(string $password): array {
        $errors = [];

        if (strlen($password) < 8) {
            $errors[] = 'Password must be at least 8 characters';
        }

        if (!preg_match('/[A-Z]/', $password)) {
            $errors[] = 'Password must contain at least one uppercase letter';
        }

        if (!preg_match('/[a-z]/', $password)) {
            $errors[] = 'Password must contain at least one lowercase letter';
        }

        if (!preg_match('/[0-9]/', $password)) {
            $errors[] = 'Password must contain at least one number';
        }

        if (!preg_match('/[!@#$%^&*(),.?":{}|<>]/', $password)) {
            $errors[] = 'Password must contain at least one special character';
        }

        return $errors;
    }
}
セッション管理
<?php
class Session {
    public static function start(): void {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
    }

    public static function set(string $key, mixed $value): void {
        $_SESSION[$key] = $value;
    }

    public static function get(string $key, mixed $default = null): mixed {
        return $_SESSION[$key] ?? $default;
    }

    public static function remove(string $key): void {
        unset($_SESSION[$key]);
    }

    public static function destroy(): void {
        $_SESSION = [];
        session_destroy();
        if (ini_get("session.use_cookies")) {
            $params = session_get_cookie_params();
            setcookie(session_name(), '', time() - 42000,
                $params["path"], $params["domain"],
                $params["secure"], $params["httponly"]
            );
        }
    }

    public static function regenerateId(): void {
        session_regenerate_id(true);
    }
}
CSRF 保護
<?php
class CsrfProtection {
    public static function generateToken(): string {
        if (!isset($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        }
        return $_SESSION['csrf_token'];
    }

    public static function validateToken(string $token): bool {
        return isset($_SESSION['csrf_token']) &&
               hash_equals($_SESSION['csrf_token'], $token);
    }

    public static function invalidateToken(): void {
        unset($_SESSION['csrf_token']);
    }

    public static function getInputField(): string {
        $token = self::generateToken();
        return "<input type='hidden' name='csrf_token' value='$token'>";
    }
}

パート 6:XAMPP の設定

URL 書き換え用の .htaccess
RewriteEngine On

# Redirect trailing slashes
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]

# Handle API routes
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^api/(.*)$ api/index.php [QSA,L]

# Handle frontend routes (SPA)
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.html [QSA,L]
PHP の設定(php.ini)
; Enable error reporting for development
error_reporting = E_ALL
display_errors = On
display_startup_errors = On

; Log errors in production
log_errors = On
error_log = "C:/xampp/php/logs/php_error.log"

; Increase upload limits
upload_max_filesize = 10M
post_max_size = 10M

; Enable PDO extensions
extension=pdo_mysql
extension=mysqli

; Enable session handling
session.save_handler = files
session.save_path = "C:/xampp/tmp"
session.use_strict_mode = 1
session.cookie_httponly = 1
session.cookie_secure = 0  ; Set to 1 if HTTPS
session.use_only_cookies = 1

; Set timezone
date.timezone = "Asia/Bangkok"

PHP 開発のベストプラクティス

コードスタイル(PSR-12)
  • 厳密な型を使用します(declare(strict_types=1))
  • PSR-12 のコーディング規約に従います
  • すべての関数とメソッドに型ヒントを使用します
  • クラスのオートロードに名前空間を使用します
  • try-catch ブロックで例外を処理します
セキュリティ
  • PDO では常にプリペアドステートメントを使用します
  • password_hash() でパスワードをハッシュ化します
  • すべてのユーザー入力を検証します
  • XSS を防ぐために出力をサニタイズします
  • 本番環境では HTTPS を使用します
  • CSRF 保護を実装します
API 設計
  • 適切な HTTP ステータスコード(200、201、400、401、403、404、500)を使用します
  • JSON レスポンスを返します
  • CORS ヘッダーを処理します
  • 認証ミドルウェアを実装します
  • エンドポイントにレート制限を適用します
データベース
  • データベース接続に PDO を使用します
  • 複数ステップの処理にはトランザクションを実装します
  • プリペアドステートメントでは名前付きパラメーターを使用します
  • 接続エラーを適切に処理します
  • 接続を適切に閉じます

よくある落とし穴

  • SQL を直接埋め込む:PHP でデータにアクセスする際、プリペアドステートメントは正確性とセキュリティを確保するための基本です。
  • リクエストの解析、ビジネスルール、レンダリングを混在させる:密結合したスクリプトはテストや API への移行が困難になります。
  • 出力エンコーディングを無視する:データをユーザーに再表示する際、入力の検証だけでは XSS を防げません。

参考資料とリソース

ドキュメント
例
スクリプト
公式ドキュメント
PHP 標準
セキュリティリソース

関連スキル

  • sql-development:ワークフローで SQL クエリ、スキーマ、パフォーマンスのチューニング作業も必要な場合に使用します。
  • code-quality:ワークフローで 2 段階レビュー(最初に仕様への準拠、次にコード品質)、保守性、リファクタリングのガイダンスも必要な場合に使用します。
  • systematic-debugging:ワークフローで修正を提案する前に根本原因のデバッグも必要な場合に使用します。
  • development-workflow:ワークフローで計画、品質ゲート、デリバリーの追跡も必要な場合に使用します。

コメント