Php expert
Framework & language expert skills for Claude Code — idiomatic best practices for TypeScript, React, Vue, Svelte, Solid, Angular, Astro
npx -y skills add Akayashuu/agent-skills --skill php-expertAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when writing, reviewing, or refactoring PHP (8.1+) — modelling data with enums/readonly value objects, choosing match over switch, designing exceptions vs result types, wiring PSR interfaces & Composer autoloading, or tightening types for PHPStan.
SKILL.md
7.9 KB, as published. Nobody here has run it
PHP Expert
Overview
Modern PHP (8.1+) is statically-mindful and immutable-by-default when you let it be. The judgment is in making invalid states unconstructable — readonly value objects validated in their constructor, backed enums instead of loose constants, match instead of fall-through switch — and in depending on PSR interfaces rather than concrete libraries. The runtime is forgiving; PHPStan at level max is where the design pressure comes from. These are calls a linter alone won't make for you.
Quick Reference
| Goal | Do | Avoid |
|---|---|---|
| Closed set of values | backed/pure enum | class const soup, magic strings |
| Immutable data | public readonly promoted props | setters / mutable DTOs |
| Branch on a value, return | match (===, expression) | switch (loose ==, fall-through) |
| Expected failure (parse/lookup) | union return Ok|Err | exceptions for control flow |
| Truly exceptional condition | throw a typed \Exception | error codes / false sentinels |
| Reach a method as a callable | $obj->m(...) first-class syntax | [$obj, 'm'] / Closure::fromCallable |
| Self-documenting call site | named arguments | positional bool/null flags |
| Function that always exits | : never return type | : void + implicit return |
| Optional chain | nullsafe $a?->b?->c | nested if ($a !== null) ladders |
| Depend on a collaborator | PSR interface (LoggerInterface) | a concrete class (Monolog\Logger) |
| Fake generics | @template docblock | mixed everywhere with no @var |
| Suppress a warning | fix the cause / check first | @ error-suppression operator |
| File of definitions | declare(strict_types=1); at top | relying on type juggling |
Core Patterns
Readonly value object + enum — invalid instances can't exist. Validate in the constructor; "mutation" returns a new instance. Implement Stringable for a clean cast.
final class Money implements Stringable {
public function __construct(
public readonly int $amountMinor, // cents, never floats for money
public readonly Currency $currency, // backed enum
) {
if ($amountMinor < 0) throw new InvalidArgumentException('negative');
}
}
Runnable:
examples/value-object.php
Compatibility gotcha: public readonly promoted properties work in 8.1, but the class-level readonly class Foo {} shorthand is 8.2+. If you target 8.1, mark each property — don't reach for readonly class.
Result type for expected failures — match forces both arms. Reserve exceptions for the exceptional; a parse miss is not exceptional, so return it.
/** @return Ok<int>|Err */
function parsePositiveInt(string $raw): Ok|Err { /* … */ }
Runnable:
examples/result-type.php
match over switch. Strict ===, no fall-through, it's an expression, and an unmapped value throws \UnhandledMatchError instead of silently doing nothing. Over a pure enum, PHPStan flags a missing arm at analysis time. Use : never for the always-throwing helper so flow analysis knows the branch terminates.
$idempotent = match ($method) {
HttpMethod::Get, HttpMethod::Put, HttpMethod::Delete => true,
HttpMethod::Post => false,
};
Runnable:
examples/match-exhaustive.php
Program to a PSR interface, not an implementation. Type-hint LoggerInterface (PSR-3), RequestInterface (PSR-7), ContainerInterface (PSR-11), ClientInterface (PSR-18) — any conforming library or test double drops in. And never let fire-and-forget analytics throw: swallow \Throwable at the edge so tracking can't break the request. The first-class callable $obj->track(...) captures the method without a wrapper lambda.
public function __construct(private readonly LoggerInterface $log) {}
public function track(string $event, array $props = []): void {
try { $this->log->info("event:$event", $props); }
catch (\Throwable) { /* analytics must not affect control flow */ }
}
Runnable:
examples/psr-discovery.php
Generics via docblock. PHP has no native generics; encode them with @template so PHPStan checks element types. Named arguments document the call site.
/** @template T */
final class TypedStack {
/** @param T $item */ public function push(mixed $item): void { /* … */ }
/** @return T */ public function pop(): mixed { /* … */ }
}
/** @var TypedStack<string> $s */
Runnable:
examples/template-generics.php
Common Mistakes
- Class constants for a closed set —
const STATUS_ACTIVE = 1carries no type and no exhaustiveness. Use a backed enum; you get::cases(),::from(), and methods. switchfor value branching — loose==, accidental fall-through on a missingbreak, and it's a statement not an expression. Prefermatch.- Exceptions as control flow /
falsesentinels — a function returningUser|falseforces=== falsechecks and loses the failure reason. Return a typed result for expected failures; throw only for the exceptional. @error suppression — it hides the warning and its cause, and survives into production. Check the precondition (isset,is_file) or let it throw.mixedplus@varcasts to silence PHPStan —mixeddisables checking and/** @var X */is an unchecked claim. Narrow withinstanceof/is_*or model the type. At level max these are smells, not fixes.- No
declare(strict_types=1);— without it"7" == 7andf(int $x)coerces silently. Put it at the top of every file. - Nullsafe to paper over a null that shouldn't exist —
?->is for genuinely optional chains, not for dodging a missing-data bug. Fix the type if it's never supposed to be null. - Reaching for
readonly class— fine on 8.2+, a fatal parse error on 8.1. Know your floor.
When NOT to over-engineer
A Result type, @template generics, and deeply factored value objects have a readability cost. Reach for them when they prevent a class of real bugs — a public parser, a shared collection, money. For a private one-off transform, a plain typed function with a thrown exception is clearer than a hand-rolled Ok|Err. Don't add a PSR interface for a collaborator that will only ever have one implementation inside the same module.
Sources
- PHP Manual — Enumerations · readonly properties · match · first-class callable syntax ·
never· nullsafe operator - PHP 8.2 — readonly classes — the 8.1-vs-8.2 caveat
- PSR-4 Autoloading · PSR-12 Coding Style · PSR-3 Logger · PSR-7 HTTP Message · PSR-11 Container · PSR-18 HTTP Client
- PHPStan — Rule Levels · PHPStan Generics
- Composer — Autoloading