php-quality

PHP code quality: PSR standards, strict types, framework idioms.

290 stars

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

$curl -o ~/.claude/skills/php-quality/SKILL.md --create-dirs "https://raw.githubusercontent.com/notque/claude-code-toolkit/main/skills/php-quality/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/php-quality/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How php-quality Compares

Feature / Agentphp-qualityStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/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

290
from notque/claude-code-toolkit

Multi-language code quality gate with auto-detection and linters.

python-quality-gate

290
from notque/claude-code-toolkit

Python quality checks: ruff, pytest, mypy, bandit in deterministic order.

comment-quality

290
from notque/claude-code-toolkit

Review and fix temporal references in code comments.

x-api

290
from notque/claude-code-toolkit

Post tweets, build threads, upload media via the X API.

worktree-agent

290
from notque/claude-code-toolkit

Mandatory rules for agents in git worktree isolation.

workflow

290
from notque/claude-code-toolkit

Structured multi-phase workflows: review, debug, refactor, deploy, create, research, and more.

workflow-help

290
from notque/claude-code-toolkit

Interactive guide to workflow system: agents, skills, routing, execution patterns.

wordpress-uploader

290
from notque/claude-code-toolkit

WordPress REST API integration for posts and media uploads.

wordpress-live-validation

290
from notque/claude-code-toolkit

Validate published WordPress posts in browser via Playwright.

with-anti-rationalization

290
from notque/claude-code-toolkit

Anti-rationalization enforcement for maximum-rigor task execution.

voice-writer

290
from notque/claude-code-toolkit

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

290
from notque/claude-code-toolkit

Critique-and-rewrite loop for voice fidelity validation.