Best use case
php-quality is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
PHP code quality: PSR standards, strict types, framework idioms.
Teams using php-quality should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/php-quality/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How php-quality Compares
| Feature / Agent | php-quality | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
PHP code quality: PSR standards, strict types, framework idioms.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
Related Guides
SKILL.md Source
# PHP Quality Skill
## Strict Types Declaration
Every PHP file must begin with `declare(strict_types=1)`. This enforces scalar type coercion rules, catching type errors at call time instead of silently converting values.
```php
<?php
declare(strict_types=1);
// Without strict_types: strlen(123) silently returns 3
// With strict_types: strlen(123) throws TypeError
```
This is non-negotiable. Omitting it is a code quality defect.
## PSR-12 Coding Standard
PSR-12 extends PSR-1 and PSR-2 as the accepted PHP coding style.
```php
<?php
declare(strict_types=1);
namespace App\Service;
use App\Repository\UserRepository;
use Psr\Log\LoggerInterface;
class UserService
{
// Visibility required on all properties, methods, constants
private const MAX_RETRIES = 3;
public function __construct(
private readonly UserRepository $repository,
private readonly LoggerInterface $logger,
) {
}
public function findActiveUsers(int $limit = 50): array
{
// Opening braces on same line for control structures
if ($limit <= 0) {
throw new \InvalidArgumentException('Limit must be positive');
}
// Opening braces on next line for classes and methods (shown above)
return $this->repository->findActive($limit);
}
}
```
Key rules: 4-space indentation, no trailing whitespace, one class per file, `use` statements after namespace with a blank line before and after, visibility on everything.
## Type Declarations
### Union Types (PHP 8.0)
```php
function parseId(int|string $id): User
{
return match (true) {
is_int($id) => $this->findById($id),
is_string($id) => $this->findBySlug($id),
};
}
```
### Intersection Types (PHP 8.1)
```php
function processItem(Countable&Iterator $collection): void
{
// $collection must implement BOTH interfaces
foreach ($collection as $item) {
// ...
}
}
```
### DNF Types — Disjunctive Normal Form (PHP 8.2)
```php
function handle((Countable&Iterator)|null $items): void
{
// Combines union and intersection: (A&B)|C
if ($items === null) {
return;
}
// ...
}
```
## Enums (PHP 8.1)
### Backed Enums
```php
enum Status: string
{
case Active = 'active';
case Inactive = 'inactive';
case Pending = 'pending';
// Enums can have methods
public function label(): string
{
return match ($this) {
self::Active => 'Active',
self::Inactive => 'Inactive',
self::Pending => 'Pending Review',
};
}
// And implement interfaces
public function isTerminal(): bool
{
return $this === self::Inactive;
}
}
// Usage
$status = Status::from('active'); // throws ValueError if invalid
$status = Status::tryFrom('unknown'); // returns null if invalid
$value = Status::Active->value; // 'active'
```
Backed enums can be `string` or `int`. Use them instead of class constants for fixed value sets.
## Readonly Properties and Classes (PHP 8.1 / 8.2)
```php
// Readonly properties (PHP 8.1) — set once, immutable after
class Money
{
public function __construct(
public readonly int $amount,
public readonly string $currency,
) {
}
}
// Readonly classes (PHP 8.2) — all properties are implicitly readonly
readonly class Coordinate
{
public function __construct(
public float $latitude,
public float $longitude,
) {
}
}
$c = new Coordinate(37.7749, -122.4194);
// $c->latitude = 0; // Error: Cannot modify readonly property
```
## Named Arguments (PHP 8.0)
Named arguments improve readability for functions with many parameters or boolean flags.
```php
// Before: positional arguments — what does true mean?
$user = createUser('Alice', 'alice@example.com', true, false);
// After: named arguments — intent is clear
$user = createUser(
name: 'Alice',
email: 'alice@example.com',
isAdmin: true,
sendWelcomeEmail: false,
);
// Named arguments can skip optional parameters
htmlspecialchars($string, encoding: 'UTF-8');
```
## Match Expressions (PHP 8.0)
`match` is a stricter alternative to `switch`. It uses strict comparison (`===`), returns a value, and throws `UnhandledMatchError` for missing cases.
```php
// switch — loose comparison, fall-through risk, verbose
switch ($statusCode) {
case 200:
$text = 'OK';
break;
case 404:
$text = 'Not Found';
break;
default:
$text = 'Unknown';
}
// match — strict comparison, no fall-through, expression
$text = match ($statusCode) {
200 => 'OK',
301 => 'Moved Permanently',
404 => 'Not Found',
500 => 'Internal Server Error',
default => 'Unknown',
};
// match with no subject — replaces if/elseif chains
$category = match (true) {
$age < 13 => 'child',
$age < 18 => 'teen',
$age < 65 => 'adult',
default => 'senior',
};
```
## Null Safe Operator (PHP 8.0)
The `?->` operator short-circuits to `null` when the left side is null, eliminating nested null checks.
```php
// Before: defensive null checking
$country = null;
if ($user !== null) {
$address = $user->getAddress();
if ($address !== null) {
$country = $address->getCountry();
}
}
// After: nullsafe chaining
$country = $user?->getAddress()?->getCountry();
// Combine with null coalescing for defaults
$countryCode = $user?->getAddress()?->getCountry()?->code ?? 'US';
```
## Laravel Idioms
### Eloquent
```php
// Scopes for reusable query constraints
class User extends Model
{
public function scopeActive(Builder $query): Builder
{
return $query->where('active', true);
}
}
// Usage: User::active()->where('role', 'admin')->get();
```
### Collections
```php
// Prefer collection methods over raw loops
$names = collect($users)
->filter(fn (User $u) => $u->isActive())
->map(fn (User $u) => $u->fullName())
->sort()
->values()
->all();
```
### Service Container
```php
// Bind interface to implementation in a ServiceProvider
$this->app->bind(PaymentGateway::class, StripeGateway::class);
// Contextual binding
$this->app->when(OrderService::class)
->needs(PaymentGateway::class)
->give(StripeGateway::class);
```
## Symfony Idioms
### Dependency Injection with Attributes
```php
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class ReportGenerator
{
public function __construct(
private readonly ReportRepository $repository,
#[Autowire('%kernel.project_dir%/var/reports')]
private readonly string $outputDir,
) {
}
}
```
### Event Dispatcher
```php
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
#[AsEventListener(event: OrderPlaced::class)]
class SendOrderConfirmation
{
public function __invoke(OrderPlaced $event): void
{
// Send confirmation email for $event->orderId
}
}
```
## Quality Enforcement
Run these tools in CI:
```bash
# PHP-CS-Fixer — auto-fix PSR-12 violations
./vendor/bin/php-cs-fixer fix --dry-run --diff
# PHPStan — static analysis (level 0-9, aim for 6+)
./vendor/bin/phpstan analyse src --level=6
# Psalm — alternative static analysis
./vendor/bin/psalm --show-info=true
# Rector — automated refactoring and upgrades
./vendor/bin/rector process src --dry-run
```
| Tool | Purpose | Config File |
|---|---|---|
| PHP-CS-Fixer | Code style enforcement | `.php-cs-fixer.dist.php` |
| PHPStan | Static analysis, type checking | `phpstan.neon` |
| Psalm | Static analysis, taint analysis | `psalm.xml` |
| Rector | Automated refactoring | `rector.php` |Related Skills
universal-quality-gate
Multi-language code quality gate with auto-detection and linters.
python-quality-gate
Python quality checks: ruff, pytest, mypy, bandit in deterministic order.
comment-quality
Review and fix temporal references in code comments.
x-api
Post tweets, build threads, upload media via the X API.
worktree-agent
Mandatory rules for agents in git worktree isolation.
workflow
Structured multi-phase workflows: review, debug, refactor, deploy, create, research, and more.
workflow-help
Interactive guide to workflow system: agents, skills, routing, execution patterns.
wordpress-uploader
WordPress REST API integration for posts and media uploads.
wordpress-live-validation
Validate published WordPress posts in browser via Playwright.
with-anti-rationalization
Anti-rationalization enforcement for maximum-rigor task execution.
voice-writer
Unified voice content generation pipeline with mandatory validation and joy-check. 8-phase pipeline: LOAD, GROUND, GENERATE, VALIDATE, REFINE, JOY-CHECK, OUTPUT, CLEANUP. Use when writing articles, blog posts, or any content that uses a voice profile. Use for "write article", "blog post", "write in voice", "generate content", "draft article", "write about".
voice-validator
Critique-and-rewrite loop for voice fidelity validation.