- name
- php-pure
- description
- Use this skill when building PHP applications without a framework — PSR standards, routing, middleware, database access, error handling, and security. This skill enforces: PSR-4 autoloading, PSR-7 request/response, PSR-15 middleware, PSR-11 container, PSR-3 logging. Requires PHP 8.1+. Do NOT use for: Laravel, Symfony, WordPress, or framework-specific PHP development.
- version
- 1.0.0
- author
- j4flmao
- license
- MIT
- compatibility
- {"claude-code":true,"cursor":true,"codex":true,"windsurf":true}
- tags
- ["backend","php","pure","phase-4"]
# PHP (Pure/Standard)
## Purpose
Build PHP applications without a framework — PSR standards-based architecture, routing, middleware, database access, type-safe with PHP 8.x features, and security-first patterns.
## Agent Protocol
### Trigger
User request includes: `pure PHP`, `PHP without framework`, `PHP PSR`, `PHP routing`, `PHP middleware`, `PHP DI`, `PHP database`, `PHP 8 attributes`, `PHP enum`.
### Input Context
- PHP version (8.1+)
- PSR implementations (PSR-7, PSR-15, PSR-11, PSR-3)
- Database (PDO, Doctrine DBAL, MySQLi)
- Container (PHP-DI, custom)
### Output Artifact
Project structure, router class, middleware chain, controller, database repository.
### Response Format
Produce artifact directly. No preamble, no postamble, no explanations.
### Completion Criteria
- PSR-4 autoloading configured
- PSR-7 request/response handling
- PSR-15 middleware pipeline
- PSR-11 container for DI
- Route matching with attributes or configuration
- PDO with prepared statements
### Max Response Length
4096 tokens
## Architecture Decision Trees
### Micro-framework vs Full Framework vs Pure PHP
| Criterion | Pure PHP (PSR stack) | Micro-framework (Slim) | Full Framework (Laravel) |
|-----------|---------------------|----------------------|--------------------------|
| Control | Complete | High | Framework conventions |
| Setup time | High | Low | Very low |
| Performance | Best | Good | Moderate |
| Learning curve | Steep | Low | Moderate |
| Flexibility | Max | High | Constrained by conventions |
Decision: Full control + performance → Pure PHP. Quick REST API → Slim/Fat-Free. Full features → Laravel/Symfony.
### PHP 8 Features for Clean Architecture
| Feature | Use | Benefit |
|---------|-----|---------|
| Attributes | Route definitions, validation, middleware | Declarative metadata without annotations |
| Enums | Status, roles, types | Type-safe constants |
| Readonly properties | DTOs, value objects | Immutability by default |
| Named arguments | Constructor DI | Self-documenting dependencies |
| Match expression | Error mapping, event routing | Exhaustive pattern matching |
| Union types | Multi-type returns | Type-safe polymorphic returns |
| Constructor promotion | DTOs, services | Reduces boilerplate |
## Workflow
### Step 1: Project Structure
```
project/
public/
index.php # Entry point
src/
Controllers/
UserController.php
Middleware/
AuthMiddleware.php
CorsMiddleware.php
ErrorHandlerMiddleware.php
Router/
Router.php
Route.php
Request/
Request.php # PSR-7 implementation
Response/
Response.php
Container/
Container.php # PSR-11 implementation
Repository/
UserRepository.php
Service/
UserService.php
Exception/
NotFoundException.php
ValidationException.php
Enum/
UserRole.php
config/
routes.php
services.php
composer.json
```
### Step 2: Composer Setup and PSR-4
```json
{
"name": "app/api",
"require": {
"php": ">=8.1",
"psr/http-message": "^1.0",
"psr/container": "^2.0",
"psr/log": "^3.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
```
### Step 3: Router with PHP 8 Attributes
```php
<?php
// src/Router/Route.php
namespace App\Router;
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class Route
{
public function __construct(
public readonly string $method,
public readonly string $path,
) {}
}
// src/Router/Router.php
namespace App\Router;
use App\Exception\NotFoundException;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface;
class Router
{
private array $routes = [];
public function register(string $class): void
{
$reflection = new \ReflectionClass($class);
foreach ($reflection->getMethods() as $method) {
foreach ($method->getAttributes(Route::class) as $attribute) {
$route = $attribute->newInstance();
$this->routes[] = [
'method' => $route->method,
'path' => $route->path,
'class' => $class,
'handler' => $method->getName(),
];
}
}
}
public function dispatch(ServerRequestInterface $request, ContainerInterface $container): mixed
{
$method = $request->getMethod();
$uri = $request->getUri()->getPath();
foreach ($this->routes as $route) {
$params = $this->match($route['method'], $route['path'], $method, $uri);
if ($params !== null) {
$controller = $container->get($route['class']);
return $controller->{$route['handler']}($request, ...$params);
}
}
throw new NotFoundException('Route not found');
}
private function match(string $routeMethod, string $routePath, string $requestMethod, string $requestUri): ?array
{
if ($routeMethod !== $requestMethod) return null;
$pattern = preg_replace('/\{(\w+)\}/', '(?P<$1>[^/]+)', $routePath);
$pattern = '#^' . $pattern . '$#';
if (preg_match($pattern, $requestUri, $matches)) {
return array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
}
return null;
}
}
// src/Enum/UserRole.php
namespace App\Enum;
enum UserRole: string
{
case Admin = 'admin';
case User = 'user';
case Moderator = 'moderator';
}
// src/Controllers/UserController.php
namespace App\Controllers;
use App\Router\Route;
use App\Service\UserService;
use Psr\Http\Message\ServerRequestInterface;
class UserController
{
public function __construct(
private readonly UserService $userService,
) {}
#[Route('GET', '/users')]
public function list(ServerRequestInterface $request): array
{
$page = (int) ($request->getQueryParams()['page'] ?? 1);
return $this->userService->paginate($page);
}
#[Route('GET', '/users/{id}')]
public function getById(ServerRequestInterface $request, string $id): ?array
{
$user = $this->userService->findById($id);
if (!$user) throw new NotFoundException('User not found');
return $user;
}
#[Route('POST', '/users')]
public function create(ServerRequestInterface $request): array
{
$data = json_decode((string) $request->getBody(), true);
return $this->userService->create($data);
}
}
```
### Step 4: Middleware Pipeline (PSR-15)
```php
<?php
// src/Middleware/Pipeline.php
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class Pipeline implements RequestHandlerInterface
{
private array $middleware = [];
private int $index = 0;
public function __construct(
private readonly RequestHandlerInterface $handler,
) {}
public function add(MiddlewareInterface $middleware): void
{
$this->middleware[] = $middleware;
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
if ($this->index < count($this->middleware)) {
$middleware = $this->middleware[$this->index];
$this->index++;
return $middleware->process($request, $this);
}
return $this->handler->handle($request);
}
}
// src/Middleware/AuthMiddleware.php
namespace App\Middleware;
use Firebase\JWT\JWT;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class AuthMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly string $jwtSecret,
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$header = $request->getHeaderLine('Authorization');
if (!str_starts_with($header, 'Bearer ')) {
throw new UnauthorizedException('Missing token');
}
try {
$token = substr($header, 7);
$payload = JWT::decode($token, new Key($this->jwtSecret, 'HS256'));
$request = $request->withAttribute('user_id', $payload->sub);
$request = $request->withAttribute('user_role', $payload->role);
} catch (\Exception) {
throw new UnauthorizedException('Invalid token');
}
return $handler->handle($request);
}
}
```
### Step 5: PDO Database Access
```php
<?php
// src/Repository/UserRepository.php
namespace App\Repository;
use App\Enum\UserRole;
use PDO;
class UserRepository
{
public function __construct(
private readonly PDO $pdo,
) {}
public function findById(string $id): ?array
{
$stmt = $this->pdo->prepare('SELECT id, name, email, role, is_active, created_at FROM users WHERE id = :id');
$stmt->execute([':id' => $id]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
return $user ?: null;
}
public function findByEmail(string $email): ?array
{
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute([':email' => $email]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
return $user ?: null;
}
public function create(array $data): array
{
$stmt = $this->pdo->prepare(
'INSERT INTO users (id, name, email, password, role) VALUES (:id, :name, :email, :password, :role) RETURNING *'
);
$stmt->execute([
':id' => uuid_create(),
':name' => $data['name'],
':email' => $data['email'],
':password' => password_hash($data['password'], PASSWORD_BCRYPT),
':role' => $data['role'] ?? UserRole::User->value,
]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function paginate(int $page, int $perPage = 20): array
{
$offset = ($page - 1) * $perPage;
$stmt = $this->pdo->prepare(
'SELECT id, name, email, role, is_active, created_at FROM users ORDER BY created_at DESC LIMIT :limit OFFSET :offset'
);
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
```
### Step 6: Entry Point
```php
<?php
// public/index.php
declare(strict_types=1);
Ver en GitHub