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. 02使用 PDO 与预处理语句
  3. 03REST API 与身份验证设计
  4. 04输入验证及 XSS、CSRF 防护
  5. 05XAMPP 环境配置指导

适用场景

  1. 01构建 PHP REST API
  2. 02通过 PDO 实现安全数据库操作
  3. 03配置 XAMPP 开发环境
  4. 04检查 PHP 更改并验证边界情况

MCP 相关

此技能不要求 MCP 服务;README 提供不使用 MCP 的本地指导与验证流程。

安装

复制下面的命令,在终端中运行。

$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 应用进行了优化。

遵循 https://php.net 上的 PHP 官方文档,提供构建高质量 PHP 应用的专家指导,涵盖 PHP 8.0+、用于安全数据库访问的 PDO、RESTful API 设计以及 XAMPP 环境配置。

  • 在可用时,利用原生并行子 Agent 调度和 200k+ 上下文窗口。

跨客户端可移植性

此技能旨在兼容 GitHub Copilot、Claude Code 和 Codex。

  • GitHub Copilot:将文件夹放在 Copilot 可见的技能路径中;如果无法发现文件夹,则将工作流封装在项目说明中。
  • Claude Code:将文件夹放在本地技能目录或兼容的插件源中。
  • Codex:将文件夹安装或同步到 $CODEX_HOME/skills/php-development,并在进行重大更改后重启 Codex。

MCP 可用性与回退方案

首选 MCP 服务:无需

  • 回退提示词:“不使用 MCP,使用 PHP Development 技能。依靠其本地说明、内置资源、标准 Shell 或编辑器工具,以及直接验证。得出结论前,展示所依据的证据。”
  • 如果当前宿主未提供 MCP 操作,不要声称使用了该操作。
  • 将本地文件、测试、渲染输出、日志或屏幕截图作为回退证据。

反模式

  • 直接插入 SQL:在 PHP 数据访问中,预处理语句是正确性和安全性的基准。
  • 混合请求解析、业务规则和渲染:紧密耦合的脚本更难测试,也难以演进为 API。
  • 认为仅靠验证就能防止 XSS:将用户控制的内容渲染回 HTML 时,输出编码仍然重要。

验证协议

在声称“技能已成功应用”之前:

  1. 通过/未通过:PHP Development 实现需说明目标运行时、框架版本和受影响的文件。
  2. 通过/未通过:针对已更改部分运行构建、Lint、测试或同等的本地验证。
  3. 通过/未通过:处理错误、依赖漂移和环境差异等边界情况,或明确说明这些情况不在范围内。
  4. 压力测试场景:将工作流应用于一项能通过正常路径测试、但会在某个边界条件下失败的更改。
  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:当工作流还需要两阶段审查(先检查规范符合性,再检查代码质量)、可维护性和重构指导时使用。
  • systematic-debugging:当工作流还需要在提出修复方案前进行根因调试时使用。
  • development-workflow:当工作流还需要规划、质量关卡和交付跟踪时使用。

评论

支持 Markdown