PHP
Purpose
Write PHP that behaves like a typed language: strict types on, enums instead of string constants, readonly value objects, and PHPStan at a level high enough to catch real defects.
When to Use
- Writing or reviewing PHP 8.2+.
- Working in Laravel or Symfony applications.
- Introducing static analysis to a legacy PHP codebase.
- Modeling domain values and states.
- Fixing performance problems in ORM-heavy code.
Capabilities
- Strict typing, union and intersection types,
never and readonly.
- Enums with backing values and interfaces.
- Attributes for routing, validation, and DI metadata.
- PSR-4 autoloading, PSR-12 style, PSR-3 logging.
- PHPStan configuration and incremental adoption via baselines.
Inputs
- Source tree,
composer.json, framework and version.
- Existing analysis configuration and baseline, if any.
Outputs
- Files opening with
declare(strict_types=1);.
- Typed properties, parameters, and return types throughout.
- A PHPStan configuration at level 8 (or a baseline plus a plan to reach it).
Workflow
- Turn on strictness —
declare(strict_types=1) in every file; PHPStan with a baseline to freeze existing debt.
- Replace magic with types — String constants become enums; array shapes become value objects or DTOs.
- Implement — Constructor promotion, readonly properties, named arguments at call sites.
- Eliminate ORM traps — Eager-load relations; never query inside a loop.
- Gate — PHPStan, PHP-CS-Fixer, PHPUnit or Pest.
Best Practices
- Never use
array as a domain type. An untyped array is a shape that no tool can check.
- Enums replace class constants and give you exhaustive
match.
- Readonly promoted constructor properties are the shortest path to immutable value objects.
- In Eloquent,
with() your relations. An N+1 query in a list endpoint is the single most common PHP performance defect.
- Do not catch
\Exception broadly. Catch the specific type, or let it reach the handler.
- Keep framework types out of the domain layer — a domain service should not know what an HTTP request is.
Examples
Enum plus readonly value object:
<?php
declare(strict_types=1);
enum SubscriptionState: string
{
case Trialing = 'trialing';
case Active = 'active';
case PastDue = 'past_due';
case Cancelled = 'cancelled';
public function isBillable(): bool
{
return match ($this) {
self::Active, self::PastDue => true,
self::Trialing, self::Cancelled => false,
};
}
}
final readonly class Subscription
{
public function __construct(
public string $id,
public SubscriptionState $state,
public \DateTimeImmutable $renewsAt,
) {}
}
Notes
- PHPStan level 8 adds null-safety checks; it is the level where the tool starts finding real bugs rather than style issues.
readonly classes (8.2) make every property readonly implicitly — a cleaner default for DTOs.
- Laravel's
lazy() and chunk() prevent memory exhaustion on large result sets; get() on an unbounded query will eventually take the process down.
1---2name: php3description: Use when writing PHP 8.2+ or working in Laravel and Symfony codebases. Covers strict types, enums, readonly classes, attributes, PSR standards, and static analysis with PHPStan.4---56# PHP78## Purpose910Write PHP that behaves like a typed language: strict types on, enums instead of string constants, readonly value objects, and PHPStan at a level high enough to catch real defects.1112## When to Use1314- Writing or reviewing PHP 8.2+.15- Working in Laravel or Symfony applications.16- Introducing static analysis to a legacy PHP codebase.17- Modeling domain values and states.18- Fixing performance problems in ORM-heavy code.1920## Capabilities2122- Strict typing, union and intersection types, `never` and `readonly`.23- Enums with backing values and interfaces.24- Attributes for routing, validation, and DI metadata.25- PSR-4 autoloading, PSR-12 style, PSR-3 logging.26- PHPStan configuration and incremental adoption via baselines.2728## Inputs2930- Source tree, `composer.json`, framework and version.31- Existing analysis configuration and baseline, if any.3233## Outputs3435- Files opening with `declare(strict_types=1);`.36- Typed properties, parameters, and return types throughout.37- A PHPStan configuration at level 8 (or a baseline plus a plan to reach it).3839## Workflow40411. **Turn on strictness** — `declare(strict_types=1)` in every file; PHPStan with a baseline to freeze existing debt.422. **Replace magic with types** — String constants become enums; array shapes become value objects or DTOs.433. **Implement** — Constructor promotion, readonly properties, named arguments at call sites.444. **Eliminate ORM traps** — Eager-load relations; never query inside a loop.455. **Gate** — PHPStan, PHP-CS-Fixer, PHPUnit or Pest.4647## Best Practices4849- Never use `array` as a domain type. An untyped array is a shape that no tool can check.50- Enums replace class constants and give you exhaustive `match`.51- Readonly promoted constructor properties are the shortest path to immutable value objects.52- In Eloquent, `with()` your relations. An N+1 query in a list endpoint is the single most common PHP performance defect.53- Do not catch `\Exception` broadly. Catch the specific type, or let it reach the handler.54- Keep framework types out of the domain layer — a domain service should not know what an HTTP request is.5556## Examples5758**Enum plus readonly value object:**5960```php61<?php62declare(strict_types=1);6364enum SubscriptionState: string65{66 case Trialing = 'trialing';67 case Active = 'active';68 case PastDue = 'past_due';69 case Cancelled = 'cancelled';7071 public function isBillable(): bool72 {73 return match ($this) {74 self::Active, self::PastDue => true,75 self::Trialing, self::Cancelled => false,76 };77 }78}7980final readonly class Subscription81{82 public function __construct(83 public string $id,84 public SubscriptionState $state,85 public \DateTimeImmutable $renewsAt,86 ) {}87}88```8990## Notes9192- PHPStan level 8 adds null-safety checks; it is the level where the tool starts finding real bugs rather than style issues.93- `readonly` classes (8.2) make every property readonly implicitly — a cleaner default for DTOs.94- Laravel's `lazy()` and `chunk()` prevent memory exhaustion on large result sets; `get()` on an unbounded query will eventually take the process down.