--- name: php-development version: "2.0" last_updated: 2026-09-08 tags: [php, development, testing, quality, automation] description: "PHP 8.0+ development — XAMPP, RESTful APIs, PDO/MySQL/MariaDB, and authentication. Use when building PHP backends, creating API endpoints, configuring XAMPP, or integrating PHP with databases." --- # PHP Development > Optimized for current PHP 8.x releases, PHPUnit 11+, Composer 2.x, and PDO-backed MySQL or MariaDB apps. Expert guidance for building high-quality PHP applications with PHP 8.0+, PDO for secure database access, RESTful API design, and XAMPP environment configuration following official PHP documentation at https://php.net. - Leverage native parallel subagent dispatch and 200k+ context windows where available. ## Cross-Client Portability This skill is written to stay usable across GitHub Copilot, Claude Code, and Codex. - GitHub Copilot: keep the folder in a Copilot-visible skill path or wrap the workflow in project instructions when folder discovery is unavailable. - Claude Code: keep the folder in a local skills directory or a compatible plugin source. - Codex: install or sync the folder into `$CODEX_HOME/skills/php-development` and restart Codex after major changes. ## MCP Availability And Fallback Preferred MCP Server: None required - Fallback prompt: "Use the PHP Development skill without MCP. Rely on its local instructions, bundled resources, standard shell or editor tools, and direct verification. Show the evidence used before concluding." - Do not claim an MCP operation was used when the active host does not expose it. - Treat local files, tests, rendered outputs, logs, or screenshots as the fallback evidence path. ## Anti-Patterns - Interpolating SQL directly: Prepared statements are the baseline for correctness and security in PHP data access. - Mixing request parsing, business rules, and rendering: Tightly coupled scripts are harder to test and evolve into APIs. - Assuming validation alone prevents XSS: Output encoding still matters when user-controlled content is rendered back to HTML. ## Verification Protocol Before claiming "skill applied successfully": 1. Pass/fail: The PHP Development implementation names the target runtime, framework version, and affected files. 2. Pass/fail: Build, lint, test, or equivalent local validation is run for the changed surface. 3. Pass/fail: Edge cases for errors, dependency drift, and environment differences are addressed or explicitly out of scope. 4. Pressure-test scenario: Apply the workflow to a change that passes happy-path tests but fails one boundary condition. 5. Success metric: Zero untested success claims; every implementation claim maps to a command or artifact. ## Before and After Example ```php 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); ``` Replaces string interpolation with a prepared statement and a narrower result shape. ## Activation Conditions Use symptom -> action triggers: when one matches, apply this skill and verify with the protocol below. **Core PHP Development:** - Building PHP RESTful APIs with proper HTTP methods - Working with XAMPP (Apache + MySQL + PHP) environment - Implementing secure database operations with PDO - Creating authentication and session management systems - Handling file uploads and form submissions **Database & Data Layer:** - Connecting PHP to MySQL/MariaDB with PDO - Writing prepared statements to prevent SQL injection - Implementing transaction handling for data integrity - Creating repository patterns for data access - Working with MySQLi vs PDO comparisons **Security & Best Practices:** - Implementing password hashing (password_hash, password_verify) - Securing against XSS, CSRF, and SQL injection - Validating and sanitizing user input - Managing sessions and authentication tokens - Configuring CORS headers for API access **API Development:** - Designing RESTful endpoints with proper HTTP status codes - Handling JSON requests and responses - Implementing middleware for authentication and authorization - Error handling and logging - Rate limiting and API versioning --- ## Part 1: PHP 8.0+ Fundamentals ### Modern PHP Features ```php user?->address?->country ?? 'Unknown'; // Constructor property promotion (PHP 8.0+) class User { public function __construct( public string $name, public string $email, private string $passwordHash ) {} } ``` ### Type Declarations & Strict Types ```php 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); } ``` --- ## Part 2: PDO Database Integration ### Database Connection Class ```php 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; } } ``` ### Prepared Statements for Security ```php 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]); } } ``` ### Transaction Management ```php 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; } } } ``` --- ## Part 3: RESTful API Development ### JSON Response Helpers ```php false, 'error' => $message, ], $statusCode); } public static function success(mixed $data = null, string $message = 'Success'): never { self::json([ 'success' => true, 'message' => $message, 'data' => $data, ]); } } ``` ### CORS Middleware ```php 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); } } } ``` --- ## Part 4: Input Validation & Sanitization ### Validation Functions ```php = $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'); } } ``` ### Validation Example ```php $value) { if (is_string($value)) { $data[$key] = Validator::sanitize($value); } } return ['errors' => $errors, 'data' => $data]; } ``` --- ## Part 5: Security Best Practices ### Password Management ```php ]/', $password)) { $errors[] = 'Password must contain at least one special character'; } return $errors; } } ``` ### Session Management ```php "; } } ``` --- ## Part 6: XAMPP Configuration ### `.htaccess` for URL Rewriting ```apache 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 Configuration (php.ini) ```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 Development Best Practices ### Code Style (PSR-12) - [ ] Use strict types (`declare(strict_types=1)`) - [ ] Follow PSR-12 coding standards - [ ] Use type hints for all functions and methods - [ ] Use namespaces for autoloading classes - [ ] Exception handling with try-catch blocks ### Security - [ ] Always use prepared statements with PDO - [ ] Hash passwords with `password_hash()` - [ ] Validate all user input - [ ] Sanitize output for XSS prevention - [ ] Use HTTPS in production - [ ] Implement CSRF protection ### API Design - [ ] Use proper HTTP status codes (200, 201, 400, 401, 403, 404, 500) - [ ] Return JSON responses - [ ] Handle CORS headers - [ ] Implement authentication middleware - [ ] Rate limit endpoints ### Database - [ ] Use PDO for database connections - [ ] Implement transactions for multi-step operations - [ ] Use named parameters in prepared statements - [ ] Handle connection errors gracefully - [ ] Close connections properly --- ## Common Pitfalls - Interpolating SQL directly: Prepared statements are the baseline for correctness and security in PHP data access. - Mixing request parsing, business rules, and rendering: Tightly coupled scripts become difficult to test or migrate into APIs. - Ignoring output encoding: Input validation alone does not protect against XSS when data is rendered back to users. ## References & Resources ### Documentation - [PHP 8.4+ API Patterns](./references/php-8-4-api-patterns-2026.md) — Modern PHP API development patterns ### Examples - [PDO Database Patterns](./examples/pdo-database-patterns.php) — PHP PDO database integration examples ### Scripts - [XAMPP Setup Script](./scripts/xampp-setup.ps1) — PowerShell script to configure XAMPP for PHP development ### Official Documentation - [PHP Manual](https://www.php.net/manual/en/) — Complete PHP reference - [ PDO for MySQL](https://www.php.net/manual/en/pdo_mysql.php) — PDO MySQL driver documentation - [Password Hashing](https://www.php.net/manual/en/book.password.php) — Secure password functions - [REST API Best Practices](https://restfulapi.net/) — API design principles ### PHP Standards - [PSR-12: Extended Coding Style](https://www.php-fig.org/psr/psr-12/) — Modern PHP coding style - [XAMPP Documentation](https://www.apachefriends.org/) — XAMPP setup and configuration ### Security Resources - [OWASP PHP Security](https://owasp.org/www-community/attacks/xss/) — XSS prevention - [SQL Injection Prevention](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html) — SQL injection prevention - [PHP Security Guide](https://www.php.net/manual/en/security.php) — Official PHP security considerations --- ## Related Skills - [sql-development](../sql-development/SKILL.md): Use it when the workflow also needs SQL query, schema, and performance tuning work. - [code-quality](../code-quality/SKILL.md): Use it when the workflow also needs two-stage review (spec compliance first, then code quality), maintainability, and refactoring guidance. - [systematic-debugging](../systematic-debugging/SKILL.md): Use it when the workflow also needs root-cause debugging before proposing fixes. - [development-workflow](../development-workflow/SKILL.md): Use it when the workflow also needs planning, quality gates, and delivery tracking.