From php-foundation
Modern PHP idioms for any PHP project (PHP 8.1+): readonly properties, enums, match expressions, constructor property promotion, typed properties, named arguments, nullsafe operator, first-class callable syntax, strict_types, PSR-12 style, and null discipline. Apply when the project is a PHP 8.1+ project. Stack-agnostic — referenced by every PHP plugin in the marketplace. Use this skill to: - Write strongly-typed, immutable-by-default value objects with readonly properties and enums. - Replace switch ladders and magic constants with match expressions and backed enums. - Cut constructor boilerplate with property promotion and express optionality with typed nullable returns. - Keep code PSR-12 compliant with declare(strict_types=1) in every file. Do NOT use this skill for: - Framework-specific idioms (Eloquent, Doctrine, Symfony services, Laravel facades — those live in framework plugin skills). - Composer / autoloading / dependency management — see php-foundation:composer-tooling. - Testing patterns — see php-foundation:php-testing.
How this skill is triggered — by the user, by Claude, or both
Slash command
/php-foundation:php-conventionsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill encodes idioms that reduce bugs and improve readability in any PHP codebase. Apply alongside the active framework plugin's conventions skill (e.g., `laravel:laravel-conventions`, `symfony:symfony-conventions`).
This skill encodes idioms that reduce bugs and improve readability in any PHP codebase. Apply alongside the active framework plugin's conventions skill (e.g., laravel:laravel-conventions, symfony:symfony-conventions).
Read composer.json first to learn the target PHP version:
"require": { "php": "^8.1" } (or higher) → all idioms below apply."config": { "platform": { "php": "8.x" } } pins the resolver version.Match the project's minimum version — do not use PHP 8.2+ features (e.g. readonly classes, DNF types) on an 8.1 project.
Every .php file with logic starts with:
<?php
declare(strict_types=1);
namespace App\Domain;
Strict typing turns silent coercions into TypeErrors at the boundary, catching bugs early. The declaration must be the first statement.
Collapse the declare/assign boilerplate into the signature.
// Prefer
final class Money
{
public function __construct(
public readonly int $amount,
public readonly Currency $currency,
) {
}
}
// Avoid — manual field + assignment
final class Money
{
private int $amount;
private Currency $currency;
public function __construct(int $amount, Currency $currency)
{
$this->amount = $amount;
$this->currency = $currency;
}
}
final class OrderLine
{
public function __construct(
public readonly string $sku,
public readonly int $quantity,
) {
}
public function withQuantity(int $quantity): self
{
return new self($this->sku, $quantity); // return a new instance, never mutate
}
}
readonly properties can be assigned once (from within the declaring scope) and never again. Use them for value objects and DTOs. On PHP 8.2+ a whole class may be marked readonly.
Use backed enums for closed sets of values; attach behaviour with methods.
enum Status: string
{
case Draft = 'draft';
case Active = 'active';
case Archived = 'archived';
public function isPublished(): bool
{
return $this === self::Active;
}
public function label(): string
{
return match ($this) {
self::Draft => 'Draft',
self::Active => 'Active',
self::Archived => 'Archived',
};
}
}
$status = Status::from('active'); // throws ValueError on unknown
$maybe = Status::tryFrom($input); // null on unknown
Never model a closed set with bare string constants or class const lists when a backed enum fits.
$discount = match (true) {
$total >= 1000 => 0.15,
$total >= 500 => 0.10,
$total >= 100 => 0.05,
default => 0.0,
};
match uses strict comparison (===), has no fallthrough, and throws UnhandledMatchError if nothing matches and no default is present — far safer than switch.
final class UserService
{
public function findById(int $id): ?User // explicit nullable return
{
return $this->repository->find($id);
}
/** @return list<User> */
public function active(): array // PHPDoc narrows array element type
{
return $this->repository->whereActive();
}
}
?T for nullable, union types (int|string) where genuinely needed.list<T>, array<K, V>) so static analysers (PHPStan/Psalm) can check element types.$country = $order?->customer?->address?->country; // short-circuits to null
// Validate at boundaries instead of propagating null
public function charge(Customer $customer): void
{
$card = $customer->defaultCard()
?? throw new NoPaymentMethodException($customer->id);
// ...
}
null to signal errors from public methods — throw a domain exception or return an empty collection.$response = $client->request(
method: 'POST',
uri: '/orders',
options: ['json' => $payload],
);
Use named arguments to clarify boolean/optional parameters at the call site. Do not reorder — pass them in declaration order for readability.
$names = array_map(strtoupper(...), $words);
$ids = array_map($user->id(...), $users);
Prefer $callable(...) over string callable names or array [$obj, 'method'] forms.
<?php (no closing ?>).final by default; open for extension only when designed for it.@PSR12 / @Symfony ruleset). Do not hand-tune whitespace the formatter owns.private/protected by default, public only when needed.interface; depend on abstractions, not concretions.npx claudepluginhub aratkruglik/claude-sdlc --plugin php-foundationUse when writing or reviewing modern PHP (8.1–8.5) outside a framework — property hooks, asymmetric visibility, the pipe operator, native attributes, enums, readonly, lazy objects. Covers language features that LLMs frequently get wrong or write in a pre-8.0 style. Do NOT use for coding style/PSR/autoloading (use php-standards) or Laravel-specific syntax (use the laravel plugin).
Writes modern PHP 8.x code with constructor promotion, enums, readonly classes/properties, match expressions, named arguments, union/intersection types, fibers, attributes. For Magento or PHP 8.x projects.