Help us improve
Share bugs, ideas, or general feedback.
From ensemble-development
Guides modern PHP 8.x development with type system, constructor promotion, enums, attributes, null safety, OOP patterns, Handler/Service, Map/DTO, PDO access, Composer, error handling, PHPUnit testing, arrays, and security.
npx claudepluginhub fortiumpartners/ensemble --plugin ensemble-developmentHow this skill is triggered — by the user, by Claude, or both
Slash command
/ensemble-development:developing-with-phpThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Core PHP language patterns for modern development. For Laravel-specific patterns, see the Laravel skill.
README.mdREFERENCE.mdVALIDATION.mdexamples/handler_architecture.example.phptemplates/class.template.phptemplates/composer.template.jsontemplates/enum.template.phptemplates/handler.template.phptemplates/interface.template.phptemplates/map_data.template.phptemplates/pdo_repository.template.phptemplates/phpunit.template.xmltemplates/trait.template.phpGuides use of modern PHP features like typed properties, union types, match expressions, named arguments, attributes, enums, and readonly properties for type-safe, expressive code in PHP 7.4+.
Builds PHP 8.3+ applications with Laravel/Symfony, strict typing, PHPStan level 9, and PSR standards. Creates typed DTOs, controllers, migrations, tests, and REST/GraphQL APIs.
Builds modern PHP 8.3+ applications with Laravel or Symfony, enforcing strict typing, PHPStan level 9, Swoole async patterns, and PSR standards. Creates controllers, middleware, migrations, PHPUnit/Pest tests, DTOs, DI, and REST/GraphQL APIs.
Share bugs, ideas, or general feedback.
Core PHP language patterns for modern development. For Laravel-specific patterns, see the Laravel skill.
Progressive Disclosure: This is the quick reference. See REFERENCE.md for comprehensive patterns, advanced examples, and deep-dives.
<?php
// Union types (8.0+)
function process(string|int $value): string|false { }
// Intersection types (8.1+)
function handle(Countable&Iterator $collection): void { }
// Nullable types
function find(int $id): ?User { }
// Never return type (8.1+)
function fail(): never {
throw new Exception('Fatal error');
}
// True, false, null as standalone types (8.2+)
function isValid(): true { return true; }
<?php
// Promoted properties (8.0+) - replaces boilerplate
class User {
public function __construct(
private string $name,
private string $email,
private bool $active = true,
) {}
}
// Readonly class (8.2+) - all properties are readonly
readonly class ValueObject {
public function __construct(
public string $value,
public DateTimeImmutable $createdAt,
) {}
}
<?php
// Backed enum with values (8.1+)
enum Status: string {
case Draft = 'draft';
case Published = 'published';
case Archived = 'archived';
public function label(): string {
return match($this) {
self::Draft => 'Draft',
self::Published => 'Published',
self::Archived => 'Archived',
};
}
}
// Usage
$status = Status::Published;
$value = $status->value; // 'published'
$all = Status::cases(); // Array of all cases
$fromValue = Status::from('draft'); // Status::Draft
$tryFrom = Status::tryFrom('invalid'); // null (no exception)
See REFERENCE.md for EnumTrait pattern and advanced enum usage.
<?php
// Match returns value, uses strict comparison
$result = match($status) {
Status::Draft => 'Not ready',
Status::Published => 'Live',
Status::Archived => 'Hidden',
};
// Multiple conditions
$category = match($code) {
200, 201, 204 => 'success',
400, 422 => 'client_error',
500, 502, 503 => 'server_error',
default => 'unknown',
};
// Named arguments (skip defaults, any order)
$user = createUser(
email: 'john@example.com',
name: 'John Doe',
role: 'admin',
);
<?php
#[Attribute]
class Route {
public function __construct(
public string $path,
public string $method = 'GET',
) {}
}
class UserController {
#[Route('/users', 'GET')]
public function index(): array { }
}
// Reading attributes via reflection
$reflection = new ReflectionMethod(UserController::class, 'index');
$attributes = $reflection->getAttributes(Route::class);
$route = $attributes[0]->newInstance();
echo $route->path; // '/users'
<?php
// Null coalescing
$name = $user->name ?? 'Anonymous';
// Null coalescing assignment
$data['count'] ??= 0;
// Nullsafe operator (8.0+)
$country = $user?->address?->country?->name;
// Combined
$timezone = $user?->settings?->timezone ?? 'UTC';
<?php
interface RepositoryInterface {
public function find(int $id): ?object;
public function save(object $entity): void;
}
abstract class BaseHandler {
public function __construct(protected PDO $db) {}
abstract public function handle(array $data): mixed;
}
<?php
trait Timestamps {
protected ?DateTimeImmutable $createdAt = null;
public function setCreatedAt(): void {
$this->createdAt = new DateTimeImmutable();
}
}
class User {
use Timestamps;
}
See REFERENCE.md for trait conflict resolution, late static binding, and magic methods.
<?php
class UserHandler {
public function __construct(
private readonly PDO $db,
private readonly CacheInterface $cache,
) {}
public function get(int $id): ?array {
$cacheKey = "user:{$id}";
if ($cached = $this->cache->get($cacheKey)) {
return $cached;
}
$stmt = $this->db->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user) {
$this->cache->set($cacheKey, $user, 3600);
}
return $user ?: null;
}
}
See REFERENCE.md for CRUD handlers, aggregation patterns, and collection mapping.
<?php
readonly class UserMap {
public function __construct(
public int $id,
public string $name,
public string $email,
) {}
public static function fromRow(array $row): self {
return new self(
id: (int) $row['id'],
name: trim($row['first_name'] . ' ' . $row['last_name']),
email: $row['email'],
);
}
public function toArray(): array {
return ['id' => $this->id, 'name' => $this->name, 'email' => $this->email];
}
}
See REFERENCE.md for nested mapping and collection patterns.
<?php
$dsn = 'mysql:host=localhost;dbname=myapp;charset=utf8mb4';
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
$pdo = new PDO($dsn, 'username', 'password', $options);
<?php
// Named parameters
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);
$user = $stmt->fetch();
// Positional parameters
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);
<?php
try {
$pdo->beginTransaction();
// Multiple operations...
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
throw $e;
}
See REFERENCE.md for stored procedures, batch operations, and SQL file execution.
{
"require": {
"php": "^8.2"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
}
}
| Constraint | Meaning |
|---|---|
^1.0 | >=1.0.0 <2.0.0 |
~1.2 | >=1.2.0 <1.3.0 |
1.0.* | >=1.0.0 <1.1.0 |
<?php
class AppException extends Exception {
public function __construct(
string $message,
int $code = 0,
public readonly array $context = [],
) {
parent::__construct($message, $code);
}
}
class NotFoundException extends AppException {
public function __construct(string $resource, int|string $id) {
parent::__construct("{$resource} not found", 404, ['id' => $id]);
}
}
<?php
try {
$user = $handler->findOrFail($id);
} catch (NotFoundException $e) {
return ['error' => $e->getMessage(), 'code' => 404];
} catch (AppException $e) {
$this->logger->error($e->getMessage(), $e->context);
return ['error' => 'An error occurred', 'code' => 500];
}
See REFERENCE.md for global exception handlers and validation exceptions.
<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Test;
class UserHandlerTest extends TestCase
{
private UserHandler $handler;
protected function setUp(): void
{
$pdo = new PDO('sqlite::memory:');
$this->handler = new UserHandler($pdo);
}
#[Test]
public function it_creates_a_user(): void
{
$id = $this->handler->create(['name' => 'John', 'email' => 'john@example.com']);
$this->assertIsInt($id);
$this->assertGreaterThan(0, $id);
}
}
See REFERENCE.md for data providers, mocking, and integration testing.
<?php
// Mapping
$names = array_map(fn($u) => $u['name'], $users);
// Filtering
$active = array_filter($users, fn($u) => $u['active']);
// Reducing
$total = array_reduce($items, fn($sum, $i) => $sum + $i['price'], 0);
// Column extraction
$emails = array_column($users, 'email');
$byId = array_column($users, null, 'id'); // Index by 'id'
// Sorting
usort($users, fn($a, $b) => $a['name'] <=> $b['name']);
See REFERENCE.md for generators and advanced collection patterns.
<?php
$hash = password_hash($password, PASSWORD_DEFAULT);
if (password_verify($password, $hash)) { /* valid */ }
<?php
// NEVER: $sql = "SELECT * FROM users WHERE id = {$_GET['id']}";
// ALWAYS: Use prepared statements
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$_GET['id']]);
<?php
$email = filter_var($_POST['email'], FILTER_VALIDATE_EMAIL);
$cleanHtml = htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');
See REFERENCE.md for CSRF protection and comprehensive security patterns.
| Type | PHP Version | Example |
|---|---|---|
?Type (nullable) | 7.1+ | ?int |
void | 7.1+ | function f(): void |
mixed | 8.0+ | function f(mixed $x) |
Type1|Type2 (union) | 8.0+ | int|string |
never | 8.1+ | function f(): never |
Type1&Type2 (intersection) | 8.1+ | A&B |
true, false, null | 8.2+ | function f(): true |
| Mode | Returns |
|---|---|
PDO::FETCH_ASSOC | Associative array |
PDO::FETCH_OBJ | stdClass object |
PDO::FETCH_CLASS | Instance of specified class |