Use when you have a written implementation plan to execute in a separate session with review checkpoints
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
- [ ]
finalon classes not designed for inheritance - [ ]
readonlyon immutable classes - [ ] No
@varannotations 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.