dirnbauer

php-modernization

3
0
# Install this skill:
npx skills add dirnbauer/webconsulting-skills --skill "php-modernization"

Install specific skill from multi-skill repository

# Description

PHP 8.x modernization patterns. Use when upgrading to PHP 8.2/8.3/8.4, implementing type safety, or achieving PHPStan level 10.

# SKILL.md


name: php-modernization
description: PHP 8.x modernization patterns. Use when upgrading to PHP 8.2/8.3/8.4, implementing type safety, or achieving PHPStan level 10.
version: 1.0.0
triggers:
- php
- modernization
- phpstan
- rector
- type safety
- dto
- enum


PHP Modernization Skill

Modernize PHP applications to PHP 8.x with type safety, PSR compliance, and static analysis.

Expertise Areas

  • PHP 8.x: Constructor promotion, readonly, enums, match, attributes, union types
  • PSR/PER Compliance: Active PHP-FIG standards
  • Static Analysis: PHPStan (level 9+), PHPat, Rector, PHP-CS-Fixer
  • Type Safety: DTOs/VOs over arrays, generics via PHPDoc

PHP 8.x Features

Constructor Property Promotion (PHP 8.0+)

// ❌ OLD
class UserService
{
    private UserRepository $userRepository;
    private LoggerInterface $logger;

    public function __construct(
        UserRepository $userRepository,
        LoggerInterface $logger
    ) {
        $this->userRepository = $userRepository;
        $this->logger = $logger;
    }
}

// βœ… NEW
final class UserService
{
    public function __construct(
        private readonly UserRepository $userRepository,
        private readonly LoggerInterface $logger,
    ) {}
}

Readonly Classes (PHP 8.2+)

// βœ… All properties are implicitly readonly
final readonly class UserDTO
{
    public function __construct(
        public int $id,
        public string $name,
        public string $email,
    ) {}
}

Enums (PHP 8.1+)

// ❌ OLD - String constants
class Status
{
    public const DRAFT = 'draft';
    public const PUBLISHED = 'published';
    public const ARCHIVED = 'archived';
}

// βœ… NEW - Backed enum
enum Status: string
{
    case Draft = 'draft';
    case Published = 'published';
    case Archived = 'archived';

    public function label(): string
    {
        return match($this) {
            self::Draft => 'Draft',
            self::Published => 'Published',
            self::Archived => 'Archived',
        };
    }
}

// Usage
public function setStatus(Status $status): void
{
    $this->status = $status;
}

$item->setStatus(Status::Published);

Match Expression (PHP 8.0+)

// ❌ OLD - Switch
switch ($type) {
    case 'a':
        $result = 'Type A';
        break;
    case 'b':
        $result = 'Type B';
        break;
    default:
        $result = 'Unknown';
}

// βœ… NEW - Match
$result = match($type) {
    'a' => 'Type A',
    'b' => 'Type B',
    default => 'Unknown',
};

Named Arguments (PHP 8.0+)

// βœ… Clearer and order-independent
$this->doSomething(
    name: 'value',
    options: ['key' => 'value'],
    enabled: true,
);

Null Safe Operator (PHP 8.0+)

// ❌ OLD
$country = null;
if ($user !== null && $user->getAddress() !== null) {
    $country = $user->getAddress()->getCountry();
}

// βœ… NEW
$country = $user?->getAddress()?->getCountry();

Union Types (PHP 8.0+)

public function process(string|int $value): string|null
{
    // ...
}

Intersection Types (PHP 8.1+)

public function handle(Countable&Iterator $collection): void
{
    // $collection must implement both interfaces
}

Attributes (PHP 8.0+)

use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(identifier: 'myext/my-listener')]
final class MyListener
{
    public function __invoke(SomeEvent $event): void
    {
        // Handle event
    }
}

DTOs and Value Objects

Never Use Arrays for Structured Data

// ❌ BAD - Array passing
public function createUser(array $data): array
{
    // What fields are expected? What types?
}

// βœ… GOOD - DTO pattern
public function createUser(CreateUserDTO $dto): UserDTO
{
    // Type-safe, documented, IDE-friendly
}

Data Transfer Object

<?php

declare(strict_types=1);

namespace Vendor\MyExtension\DTO;

final readonly class CreateUserDTO
{
    public function __construct(
        public string $name,
        public string $email,
        public ?string $phone = null,
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(
            name: $data['name'] ?? throw new \InvalidArgumentException('Name required'),
            email: $data['email'] ?? throw new \InvalidArgumentException('Email required'),
            phone: $data['phone'] ?? null,
        );
    }
}

Value Object

<?php

declare(strict_types=1);

namespace Vendor\MyExtension\ValueObject;

final readonly class EmailAddress
{
    private function __construct(
        public string $value,
    ) {}

    public static function fromString(string $email): self
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException('Invalid email address');
        }

        return new self($email);
    }

    public function equals(self $other): bool
    {
        return $this->value === $other->value;
    }

    public function __toString(): string
    {
        return $this->value;
    }
}

PSR/PER Compliance

Active Standards

Standard Purpose Status
PSR-1 Basic Coding Required
PSR-4 Autoloading Required
PER CS Coding Style Required (supersedes PSR-12)
PSR-3 Logger Interface Use for logging
PSR-6/16 Cache Use for caching
PSR-7/17/18 HTTP Use for HTTP clients
PSR-11 Container Use for DI
PSR-14 Events Use for event dispatching
PSR-15 Middleware Use for HTTP middleware
PSR-20 Clock Use for time-dependent code

PER Coding Style

<?php

declare(strict_types=1);

namespace Vendor\Package;

use Vendor\Package\SomeClass;

final class MyClass
{
    public function __construct(
        private readonly SomeClass $dependency,
    ) {}

    public function doSomething(
        string $param1,
        int $param2,
    ): string {
        return match ($param2) {
            1 => $param1,
            2 => $param1 . $param1,
            default => '',
        };
    }
}

Static Analysis Tools

PHPStan (Level 9+)

# phpstan.neon
includes:
    - vendor/phpstan/phpstan-strict-rules/rules.neon
    - vendor/saschaegerer/phpstan-typo3/extension.neon

parameters:
    level: 10
    paths:
        - Classes
        - Tests
    excludePaths:
        - Classes/Domain/Model/*

Level Guide:
- Level 0-5: Basic checks
- Level 6-8: Type checking
- Level 9: Strict mixed handling
- Level 10: Maximum strictness (recommended)

PHP-CS-Fixer

<?php
// .php-cs-fixer.dist.php
$config = new PhpCsFixer\Config();

return $config
    ->setRules([
        '@PER-CS' => true,
        '@PER-CS:risky' => true,
        'declare_strict_types' => true,
        'no_unused_imports' => true,
        'ordered_imports' => ['sort_algorithm' => 'alpha'],
        'single_line_empty_body' => true,
        'trailing_comma_in_multiline' => [
            'elements' => ['arguments', 'arrays', 'match', 'parameters'],
        ],
    ])
    ->setRiskyAllowed(true)
    ->setFinder(
        PhpCsFixer\Finder::create()
            ->in(__DIR__ . '/Classes')
            ->in(__DIR__ . '/Tests')
    );

Rector

<?php
// rector.php
declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;

return RectorConfig::configure()
    ->withPaths([
        __DIR__ . '/Classes',
        __DIR__ . '/Tests',
    ])
    ->withSets([
        LevelSetList::UP_TO_PHP_83,
        SetList::CODE_QUALITY,
        SetList::TYPE_DECLARATION,
        SetList::DEAD_CODE,
    ]);

PHPat (Architecture Testing)

<?php

declare(strict_types=1);

namespace Vendor\MyExtension\Tests\Architecture;

use PHPat\Selector\Selector;
use PHPat\Test\Builder\Rule;
use PHPat\Test\PHPat;

final class ArchitectureTest
{
    public function testDomainDoesNotDependOnInfrastructure(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::inNamespace('Vendor\MyExtension\Domain'))
            ->shouldNotDependOn()
            ->classes(Selector::inNamespace('Vendor\MyExtension\Infrastructure'));
    }
}

Type Safety Patterns

Typed Arrays with PHPDoc Generics

/**
 * @return array<int, User>
 */
public function getUsers(): array
{
    return $this->users;
}

/**
 * @param array<string, mixed> $config
 */
public function configure(array $config): void
{
    // ...
}

/**
 * @return \Generator<int, Item, mixed, void>
 */
public function iterateItems(): \Generator
{
    foreach ($this->items as $item) {
        yield $item;
    }
}

Strict Comparison

// ❌ Loose comparison
if ($value == '1') {}

// βœ… Strict comparison
if ($value === '1') {}
if ($value === 1) {}

Early Returns

// ❌ Nested conditions
public function process(?User $user): ?Result
{
    if ($user !== null) {
        if ($user->isActive()) {
            return $this->doProcess($user);
        }
    }
    return null;
}

// βœ… Early returns
public function process(?User $user): ?Result
{
    if ($user === null) {
        return null;
    }

    if (!$user->isActive()) {
        return null;
    }

    return $this->doProcess($user);
}

Migration Checklist

  • [ ] declare(strict_types=1) in all files
  • [ ] PSR-4 autoloading in composer.json
  • [ ] PER Coding Style enforced via PHP-CS-Fixer
  • [ ] PHPStan level 9+ (level 10 for new projects)
  • [ ] All methods have return types
  • [ ] All parameters have type declarations
  • [ ] All properties have type declarations
  • [ ] DTOs for data transfer, Value Objects for domain concepts
  • [ ] Enums for fixed sets of values (not string constants)
  • [ ] Constructor property promotion used
  • [ ] final on classes not designed for inheritance
  • [ ] readonly on immutable classes
  • [ ] No @var annotations when type is declared
  • [ ] PHPat architecture tests for layer dependencies

Resources

  • PHP-FIG: https://www.php-fig.org/
  • PHPStan: https://phpstan.org/
  • Rector: https://getrector.com/
  • PHP-CS-Fixer: https://cs.symfony.com/
  • PHPat: https://www.phpat.dev/

Credits & Attribution

This skill is based on the excellent work by
Netresearch DTT GmbH.

Original repository: https://github.com/netresearch/php-modernization-skill

Copyright (c) Netresearch DTT GmbH - Methodology and best practices
Adapted by webconsulting.at for this skill collection

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.