From php-tomes
Use this skill when writing or reviewing PHP code for style, naming, types, or documentation. Covers PSR-12/PER-CS formatting, naming conventions, strict typing, union/intersection/DNF types, PHPStan (level 0-9) and Psalm configuration, baseline management, PHPDoc (when to write vs skip), generics via @template, and PHP-CS-Fixer/Pint setup.
How this skill is triggered — by the user, by Claude, or both
Slash command
/php-tomes:php-code-qualityThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Apply these rules to every PHP file you write, review, or refactor. When generating new code, follow them by default.
Apply these rules to every PHP file you write, review, or refactor. When generating new code, follow them by default. When reviewing existing code, flag violations.
Every PHP file must follow this exact structure:
<?php
declare(strict_types=1);
namespace App\Domain\Order;
use App\Contracts\Repository;
use RuntimeException;
class OrderService implements Repository
{
public function __construct(
private readonly OrderRepository $orders,
private readonly EventDispatcher $events,
) {
}
public function create(array $data): Order
{
// ...
}
}
Rules:
<?php tag only — never <? short tagsdeclare(strict_types=1) on every file, after a blank lineuse imports grouped and sorted alphabetically| Construct | Convention | Example |
|---|---|---|
| Classes/interfaces/traits/enums | StudlyCaps | OrderProcessor, Cacheable |
| Methods/functions | camelCase | findByEmail(), processOrder() |
| Variables/properties | camelCase | $userId, $orderItems |
| Constants | UPPER_SNAKE_CASE | MAX_RETRY_COUNT |
| Enum cases | StudlyCaps | OrderStatus::Pending |
| Namespaces | StudlyCaps segments | App\Domain\Order |
// ❌ Bad
class order_processor {}
interface IUserRepository {}
class HTTPSHandler {} // all-caps acronym
public function create_order(): Order {}
$arr_users = []; // Hungarian notation
// ✅ Good
class OrderProcessor {}
interface UserRepository {}
class HttpsHandler {}
public function createOrder(): Order {}
$users = [];
Start boolean-returning methods with is, has, can, should, or was:
// ❌ Bad
public function active(): bool {}
// ✅ Good
public function isActive(): bool {}
public function hasPermission(string $perm): bool {}
public function canProcessRefunds(): bool {}
First word is a verb. Do not repeat the class name in the method:
// ❌ Bad — redundant class name in method
class OrderService
{
public function getOrderById(int $id): Order {}
}
// ✅ Good
class OrderService
{
public function findById(int $id): Order {}
}
// ❌ Bad — exceeds line limit
$result = $this->orderRepository->findByUserAndStatus($userId, OrderStatus::Pending, true);
// ✅ Good
$result = $this->orderRepository->findByUserAndStatus(
userId: $userId,
status: OrderStatus::Pending,
includeArchived: true,
);
// ❌ Bad
if ($condition)
doSomething();
if ($condition)
{
doSomething();
}
// ✅ Good
if ($condition) {
doSomething();
}
Always use trailing commas in multi-line arrays, arguments, and parameters:
// ✅ Good — trailing comma on last element
$config = [
'driver' => 'mysql',
'host' => 'localhost',
'port' => 3306,
];
abstract/final then public/protected/private then static:
final public static function create(): static {}
abstract protected function validate(array $data): bool;
declare(strict_types=1)?Type instead of mixed when null is the only alternativenever for functions that always throw or exitvoid for methods that return no valuereadonly class for value objects (PHP 8.2+)// ❌ Bad — missing types, using mixed unnecessarily
function findUser($id) {
// ...
}
public function save($entity): mixed {
// ...
}
// ✅ Good
function findUser(int $id): ?User {
// ...
}
public function save(Entity $entity): void {
// ...
}
// ✅ Good — readonly class for immutable value objects
readonly class Money
{
public function __construct(
public int $amount,
public string $currency,
) {
}
}
// Union type
function parseId(string|int $id): int
{
return is_string($id) ? (int) $id : $id;
}
// Intersection type (PHP 8.1+)
function persist(Loggable&Serializable $entity): void {}
// DNF type (PHP 8.2+)
function process((Countable&Iterator)|array $items): void {}
static Return Type// ✅ Good — preserves subtype in fluent chains
public function where(string $column, mixed $value): static
{
$this->wheres[] = [$column, $value];
return $this;
}
Target level 9. Use Larastan for Laravel projects.
# phpstan.neon
includes:
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
- vendor/nunomaduro/larastan/extension.neon # Laravel only
- phpstan-baseline.neon
parameters:
level: 9
paths:
- app
- src
checkModelProperties: true # Laravel only
phpstan-baseline.neon — it is a contract, not a todo list# Generate baseline
vendor/bin/phpstan analyse --generate-baseline phpstan-baseline.neon
Always include a reason when suppressing:
// ❌ Bad — no explanation
/** @phpstan-ignore-next-line */
$roles = $this->roles->pluck('name');
// ✅ Good
/** @phpstan-ignore-next-line (Eloquent magic: $this->roles is a Collection) */
$roles = $this->roles->pluck('name');
Start at level 0, increment one level per sprint. At each level:
Only write PHPDoc when it adds information beyond native type declarations:
Does the method have native type declarations for all params and return?
├── Yes → Does it also need @template, @throws, or array shapes?
│ ├── Yes → Add PHPDoc for those tags only
│ └── No → Skip PHPDoc entirely
└── No → Add native types first, then apply the above
// ✅ Generics — no native PHP syntax for this
/**
* @template T of Model
* @param class-string<T> $modelClass
* @return T
*/
public function find(string $modelClass, int $id): Model {}
// ✅ Array shapes — native `array` loses structure
/**
* @param array<string, int|float> $metrics
* @return array{min: float, max: float, avg: float}
*/
public function summarize(array $metrics): array {}
// ✅ Throws — PHP has no checked exceptions
/**
* @throws DatabaseException if the connection fails
* @throws ValidationException if constraints are violated
*/
public function save(Entity $entity): void {}
// ✅ Callable shapes
/**
* @param callable(User, int): bool $callback
* @return list<User>
*/
public function filterUsers(callable $callback): array {}
// ❌ Bad — duplicates native types
/**
* @param string $name
* @param int $age
* @return User
*/
public function create(string $name, int $age): User {}
// ✅ Good — native types are sufficient
public function create(string $name, int $age): User {}
/**
* @template TValue
* @implements IteratorAggregate<int, TValue>
*/
class TypedCollection implements IteratorAggregate
{
/** @var list<TValue> */
private array $items = [];
/** @param TValue $item */
public function add(mixed $item): void
{
$this->items[] = $item;
}
/** @return TValue|null */
public function first(): mixed
{
return $this->items[0] ?? null;
}
}
/** @extends TypedCollection<User> */
class UserCollection extends TypedCollection {}
// .php-cs-fixer.dist.php
$finder = PhpCsFixer\Finder::create()
->in([__DIR__ . '/src', __DIR__ . '/tests'])
->exclude('vendor');
return (new PhpCsFixer\Config())
->setRules([
'@PER-CS2.0' => true,
'declare_strict_types' => true,
'trailing_comma_in_multiline' => [
'elements' => ['arrays', 'arguments', 'parameters'],
],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
'single_quote' => true,
])
->setFinder($finder);
{
"preset": "per",
"rules": {
"declare_strict_types": true,
"ordered_imports": { "sort_algorithm": "alpha" }
}
}
root = true
[*.php]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
Run these checks on every PR:
vendor/bin/php-cs-fixer fix --dry-run --diff (or vendor/bin/pint --test)vendor/bin/phpstan analyse --memory-limit=512MDo not auto-fix in CI — fail the build and require local fixes. Auto-fixing on CI can cause divergence between the branch and remote.
See the reference files for deeper details:
npx claudepluginhub councilofwizards/wizards --plugin php-tomesProvides Nette-specific PHP coding standards including TAB indentation, single quotes, strict_types=1, PSR-12 modifications, and use statement ordering. Invoke before writing, modifying, or refactoring any PHP code.
Analyzes PHP code for PSR-12 compliance and style issues including brace placement, line length, indentation, blank lines, trailing whitespace, use statements, array syntax, and operator spacing.