| name | PHP Ecosystem |
| description | This skill should be used when the user asks to "write php", "php 8", "composer", "phpunit", "pest", "phpstan", "psalm", "psr", or works with modern PHP language patterns and configuration. Provides comprehensive modern PHP ecosystem patterns and best practices. |
| version | 2.1.0 |
Provide comprehensive patterns for modern PHP (8.5+) language features, PSR standards, testing, static analysis, and package development in a framework-agnostic approach.
Read - Analyze composer.json and PHP source files
Edit - Modify PHP code and Composer configuration
Bash - Run composer, phpunit, phpstan, php-cs-fixer commands
mcp__plugin_claude-code-home-manager_context7__query-docs - Fetch latest PHP documentation
Enable declare(strict_types=1) in all PHP files for type safety
Follow PSR-1, PSR-4, PER-CS for interoperability
Inject dependencies through constructor; avoid service locator pattern
Use readonly classes for immutable domain values (PHP 8.2+)
Use property hooks for validation and computed properties (PHP 8.4+)
<php_version>
<version_mapping>
PHP version-specific feature availability and support timeline
<support_timeline>
Active support until Dec 2027, security until Dec 2029
Still supported
Maintenance/upgrade runway
Expected late 2026
</support_timeline>
Pipe operator (|>)
Clone with modified properties (clone $obj with {...})
#[\NoDiscard] attribute
Property hooks (get/set)
Asymmetric visibility (public private(set))
#[Deprecated] attribute
Lazy objects
new MyClass()->method() without parentheses
array_find(), array_find_key(), array_any(), array_all()
mb_trim(), mb_ltrim(), mb_rtrim()
Typed class constants
json_validate() function
Randomizer::getFloat() and nextFloat()
Deep cloning of readonly properties
Override attribute
Granular DateTime exceptions
Readonly classes
DNF types (Disjunctive Normal Form)
null, false, true as standalone types
Constants in traits
Deprecate dynamic properties
Enums (backed and unit)
Readonly properties
Fibers (cooperative multitasking)
Intersection types
never return type
First-class callable syntax
New in initializers
Named arguments
Attributes
Constructor property promotion
Union types
Match expression
Nullsafe operator
mixed type
</version_mapping>
<recommended_config>
php.ini recommended settings for development
error_reporting = E_ALL
display_errors = On
log_errors = On
opcache.enable = 1
opcache.validate_timestamps = 1
</recommended_config>
<version_upgrade_coherence>
A PHP major/minor bump is a cross-cutting change, not a one-line edit to composer.json. The language version is pinned in several independent places, and if any one lags, static analysis and CI validate against a different language level than the code actually runs on — a silent, confidence-eroding split. Bump every pin in one pass and verify with the full check suite.
<pattern name="align_all_version_pins">
<description>Enumerate and update every location that encodes the PHP version together.</description>
<checklist>
<item>composer.json: the "php" constraint (e.g. "^8.5")</item>
<item>Local dev environment: the toolchain pin (Nix devenv php85, asdf/mise, or equivalent)</item>
<item>Docker base images: both the CLI and FPM variants</item>
<item>CI: the setup-php (or equivalent) php-version input</item>
<item>Static analysis: PHPStan phpVersion (e.g. 80500)</item>
<item>Automated refactoring: Rector ->withPhpSets() / PhpVersion target</item>
</checklist>
</pattern>
<pattern name="regenerate_baseline_after_toolchain_bump">
<description>After upgrading the static-analysis toolchain itself (e.g. a PHPStan major, or its framework extension), regenerate the baseline (phpstan-baseline.neon) for the new toolchain rather than hand-editing it. The new version surfaces and reclassifies errors differently; a stale baseline hides real regressions.</description>
<note>New rules that would require broad mechanical changes to pre-existing code (for example, banning inline ignore comments across a legacy codebase) can be disabled deliberately and tracked as separate technical debt, rather than blocking the upgrade.</note>
</pattern>
<pattern name="track_accepted_advisories">
<description>composer audit may report a low-severity advisory that is pinned by a transitive dependency you do not control. Record the advisory and why it is deferred (blocking constraint) instead of silently ignoring the audit, so the decision is auditable and revisited when the constraint lifts.</description>
</pattern>
</version_upgrade_coherence>
</php_version>
<type_system>
<union_types>
Multiple types for parameter or return
function process(string|int $value): string|null
{
return is_string($value) ? $value : (string) $value;
}
</union_types>
<intersection_types>
Value must satisfy all types (PHP 8.1+)
function process(Countable&Iterator $collection): int
{
return count($collection);
}
</intersection_types>
<dnf_types>
Combine union and intersection types (PHP 8.2+)
function handle((Countable&Iterator)|null $items): void
{
if ($items === null) {
return;
}
foreach ($items as $item) {
// process
}
}
</dnf_types>
Enum with scalar backing (PHP 8.1+)
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
$status = Status::from('published');
$value = $status->value; // 'published'
</example>
</pattern>
<pattern name="unit-enum">
<description>Enum without backing value</description>
<example>
enum Suit
{
case Hearts;
case Diamonds;
case Clubs;
case Spades;
public function color(): string
{
return match($this) {
self::Hearts, self::Diamonds => 'red',
self::Clubs, self::Spades => 'black',
};
}
}
</example>
</pattern>
Immutable property (PHP 8.1+)
class User
{
public function __construct(
public readonly string $id,
public readonly string $email,
) {}
}
<pattern name="readonly-class">
<description>All properties become readonly (PHP 8.2+)</description>
<example>
readonly class ValueObject
{
public function __construct(
public string $name,
public int $value,
) {}
}
</example>
</pattern>
Define and use custom attributes (PHP 8.0+)
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
class Route
{
public function __construct(
public string $path,
public string $method = 'GET',
) {}
}
class UserController
{
#[Route('/users', 'GET')]
public function index(): array
{
return [];
}
}
// Reading attributes via reflection
$method = new ReflectionMethod(UserController::class, 'index');
$attributes = $method->getAttributes(Route::class);
foreach ($attributes as $attribute) {
$route = $attribute->newInstance();
echo $route->path; // '/users'
}
</example>
</pattern>
<constructor_promotion>
Declare and assign properties in constructor (PHP 8.0+)
class Product
{
public function __construct(
private string $name,
private float $price,
private int $quantity = 0,
) {}
public function getName(): string
{
return $this->name;
}
}
</example>
</pattern>
</constructor_promotion>
<named_arguments>
Pass arguments by name (PHP 8.0+)
function createUser(
string $name,
string $email,
bool $active = true,
?string $role = null,
): User {
// ...
}
// Usage with named arguments
$user = createUser(
email: 'user@example.com',
name: 'John Doe',
role: 'admin',
);
</example>
<decision_tree name="when_to_use">
<question>Are you skipping optional parameters or improving readability?</question>
<if_yes>Use named arguments</if_yes>
<if_no>Use positional arguments for simple calls</if_no>
</decision_tree>
</pattern>
</named_arguments>
<typed_class_constants>
Type declarations for class constants (PHP 8.3+)
class Config
{
public const string VERSION = '1.0.0';
public const int MAX_RETRIES = 3;
public const array ALLOWED_METHODS = ['GET', 'POST', 'PUT', 'DELETE'];
}
</typed_class_constants>
<property_hooks>
Property hooks for validation and transformation (PHP 8.4+)
class User
{
public string $email {
get => $this->email;
set {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email');
}
$this->email = strtolower($value);
}
}
}
<decision_tree name="when_to_use">
Do you need validation or transformation on property access?
<if_yes>Use property hooks instead of getters/setters</if_yes>
<if_no>Use simple public properties or readonly</if_no>
</decision_tree>
<pattern name="virtual-property">
<description>Computed property without backing storage (PHP 8.4+)</description>
<example>
class User
{
public string $fullName {
get => $this->firstName . ' ' . $this->lastName;
}
public function __construct(
public string $firstName,
public string $lastName,
) {}
}
</example>
</pattern>
<pattern name="old_vs_modern">
<description>Migration from getters/setters to property hooks</description>
<old_way>
// Before PHP 8.4: Boilerplate getters/setters
class Product
{
private int $price;
public function getPrice(): int
{
return $this->price;
}
public function setPrice(int $price): void
{
if ($price < 0) {
throw new InvalidArgumentException('Price cannot be negative');
}
$this->price = $price;
}
}
// Usage
$product->setPrice(100);
echo $product->getPrice();
</old_way>
<modern_way>
// PHP 8.4+: Property hooks
class Product
{
public int $price {
set {
if ($value < 0) {
throw new InvalidArgumentException('Price cannot be negative');
}
$this->price = $value;
}
}
}
// Usage - direct property access
$product->price = 100;
echo $product->price;
</modern_way>
</pattern>
</property_hooks>
<php_85_features>
The pipe operator (|>) enables functional-style chaining for cleaner, more readable code (PHP 8.5).
$result = $input
|> fn($x) => trim($x)
|> fn($x) => strtolower($x)
|> fn($x) => str_replace(' ', '-', $x);
Clone an object while modifying selected properties in a single expression (PHP 8.5).
$newUser = clone $user with {
name: 'New Name',
email: 'new@example.com',
};
#[\NoDiscard] attribute emits warnings when important return values are ignored (PHP 8.5).
#[\NoDiscard]
function computeChecksum(string $data): string {
return hash('sha256', $data);
}
// Warning: Return value of computeChecksum() is not used
computeChecksum($data);
</php_85_features>
<asymmetric_visibility>
Public read access with private write (PHP 8.4+)
class Order
{
public private(set) string $status = 'pending';
public private(set) DateTimeImmutable $createdAt;
public private(set) ?DateTimeImmutable $processedAt = null;
public function __construct()
{
$this->createdAt = new DateTimeImmutable();
}
public function process(): void
{
$this->status = 'processed';
$this->processedAt = new DateTimeImmutable();
}
}
// Usage
$order = new Order();
echo $order->status; // OK: public read
$order->status = 'failed'; // Error: private write
</example>
<decision_tree name="when_to_use">
<question>Do you need public read but controlled write access?</question>
<if_yes>Use asymmetric visibility (public private(set))</if_yes>
<if_no>Use readonly for immutable or regular visibility for mutable</if_no>
</decision_tree>
</pattern>
</asymmetric_visibility>
<deprecated_attribute>
Mark functions/methods/constants as deprecated (PHP 8.4+)
class ApiService
{
#[Deprecated(
message: 'Use fetchV2() instead',
since: '2.0.0'
)]
public function fetch(): array
{
return $this->fetchV2();
}
public function fetchV2(): array
{
// New implementation
}
}
// Usage triggers E_USER_DEPRECATED automatically
$service->fetch();
</example>
</pattern>
</deprecated_attribute>
<lazy_objects>
Create lazy-initialized objects (PHP 8.4+)
class ExpensiveService
{
public function __construct()
{
// Heavy initialization...
}
}
// Create lazy ghost - initializes only when accessed
$reflector = new ReflectionClass(ExpensiveService::class);
$service = $reflector->newLazyGhost(
function (ExpensiveService $instance) {
// Called when first property/method accessed
$instance->__construct();
}
);
// $service not initialized yet
$service->doSomething(); // Triggers initialization
</example>
<decision_tree name="when_to_use">
<question>Do you need to defer expensive object creation?</question>
<if_yes>Use lazy ghost for same-class or lazy proxy for interface wrapping</if_yes>
<if_no>Use regular instantiation</if_no>
</decision_tree>
</pattern>
</lazy_objects>
PHP Fibers (8.1+) enable cooperative multitasking for async I/O without callbacks.
$fiber = new Fiber(function (): void {
$value = Fiber::suspend('fiber started');
echo "Fiber resumed with: $value";
});
$result = $fiber->start(); // 'fiber started'
$fiber->resume('hello'); // 'Fiber resumed with: hello'
</example>
<note>Fibers are low-level primitives. Prefer async libraries (ReactPHP, AMPHP, Revolt) that use Fibers internally.</note>
</pattern>
<psr_standards>
Basic coding standards for PHP files
Files MUST use only <?php and <?= tags
Files MUST use only UTF-8 without BOM
Class names MUST be declared in StudlyCaps
Class constants MUST be declared in UPPER_CASE
Method names MUST be declared in camelCase
Autoloading classes from file paths
// composer.json
{
"autoload": {
"psr-4": {
"App\\": "src/",
"App\\Tests\\": "tests/"
}
}
}
// File: src/Domain/User/Entity/User.php
namespace App\Domain\User\Entity;
class User
{
// Fully qualified: App\Domain\User\Entity\User
}
</example>
Common interface for logging libraries
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
class UserService
{
public function __construct(
private LoggerInterface $logger,
) {}
public function create(array $data): User
{
$this->logger->info('Creating user', ['email' => $data['email']]);
try {
$user = new User($data);
$this->logger->debug('User created', ['id' => $user->getId()]);
return $user;
} catch (\Exception $e) {
$this->logger->error('Failed to create user', [
'exception' => $e,
'data' => $data,
]);
throw $e;
}
}
}
</example>
Common interfaces for HTTP messages
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
function handleRequest(ServerRequestInterface $request): ResponseInterface
{
$method = $request->getMethod();
$uri = $request->getUri();
$body = $request->getParsedBody();
$query = $request->getQueryParams();
// PSR-7 messages are immutable
$response = new Response();
return $response
->withStatus(200)
->withHeader('Content-Type', 'application/json');
}
</example>
Common interface for dependency injection containers
use Psr\Container\ContainerInterface;
class ServiceLocator
{
public function __construct(
private ContainerInterface $container,
) {}
public function getUserService(): UserService
{
return $this->container->get(UserService::class);
}
}
</example>
Extends PSR-1 with detailed formatting rules
Code MUST follow PSR-1
Code MUST use 4 spaces for indenting
Lines SHOULD be 80 characters or less
There MUST be one blank line after namespace declaration
Opening braces for classes MUST go on next line
Opening braces for methods MUST go on next line
Visibility MUST be declared on all properties and methods
Interfaces for HTTP server request handlers and middleware
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Server\MiddlewareInterface;
class AuthMiddleware implements MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
$token = $request->getHeaderLine('Authorization');
if (!$this->validateToken($token)) {
return new Response(401);
}
return $handler->handle($request);
}
}
</example>
Factory interfaces for creating PSR-7 objects
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
class JsonResponder
{
public function __construct(
private ResponseFactoryInterface $responseFactory,
private StreamFactoryInterface $streamFactory,
) {}
public function respond(array $data, int $status = 200): ResponseInterface
{
$json = json_encode($data, JSON_THROW_ON_ERROR);
$body = $this->streamFactory->createStream($json);
return $this->responseFactory->createResponse($status)
->withHeader('Content-Type', 'application/json')
->withBody($body);
}
}
</example>
Common interface for HTTP clients
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
class ApiClient
{
public function __construct(
private ClientInterface $httpClient,
private RequestFactoryInterface $requestFactory,
) {}
public function get(string $url): array
{
$request = $this->requestFactory->createRequest('GET', $url);
$response = $this->httpClient->sendRequest($request);
return json_decode(
$response->getBody()->getContents(),
true,
512,
JSON_THROW_ON_ERROR
);
}
}
</example>
<design_patterns>
Immutable objects representing a value
readonly class Money
{
public function __construct(
public int $amount,
public string $currency,
) {
if ($amount < 0) {
throw new InvalidArgumentException('Amount cannot be negative');
}
}
public function add(Money $other): self
{
if ($this->currency !== $other->currency) {
throw new InvalidArgumentException('Currency mismatch');
}
return new self($this->amount + $other->amount, $this->currency);
}
public function equals(Money $other): bool
{
return $this->amount === $other->amount
&& $this->currency === $other->currency;
}
}
</example>
Abstract data persistence behind an interface
interface UserRepositoryInterface
{
public function find(UserId $id): ?User;
public function findByEmail(Email $email): ?User;
public function save(User $user): void;
public function remove(User $user): void;
}
class PdoUserRepository implements UserRepositoryInterface
{
public function __construct(
private PDO $pdo,
) {}
public function find(UserId $id): ?User
{
$stmt = $this->pdo->prepare(
'SELECT * FROM users WHERE id = :id'
);
$stmt->execute(['id' => $id->toString()]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ? $this->hydrate($row) : null;
}
public function save(User $user): void
{
$stmt = $this->pdo->prepare(
'INSERT INTO users (id, email, name)
VALUES (:id, :email, :name)
ON DUPLICATE KEY UPDATE email = :email, name = :name'
);
$stmt->execute([
'id' => $user->getId()->toString(),
'email' => $user->getEmail()->toString(),
'name' => $user->getName(),
]);
}
}
</example>
<decision_tree name="when_to_use">
<question>Do you need to abstract persistence details from domain logic?</question>
<if_yes>Use Repository pattern</if_yes>
<if_no>Direct database access may be sufficient for simple CRUD</if_no>
</decision_tree>
Coordinate use cases and transactions
class CreateUserHandler
{
public function __construct(
private UserRepositoryInterface $userRepository,
private PasswordHasherInterface $passwordHasher,
private EventDispatcherInterface $eventDispatcher,
) {}
public function handle(CreateUserCommand $command): UserId
{
$email = new Email($command->email);
if ($this->userRepository->findByEmail($email) !== null) {
throw new UserAlreadyExistsException($email);
}
$user = User::create(
UserId::generate(),
$email,
$command->name,
$this->passwordHasher->hash($command->password),
);
$this->userRepository->save($user);
$this->eventDispatcher->dispatch(new UserCreatedEvent($user));
return $user->getId();
}
}
</example>
Inject dependencies through constructor
// Interface for abstraction
interface CacheInterface
{
public function get(string $key): mixed;
public function set(string $key, mixed $value, int $ttl = 3600): void;
}
// Concrete implementation
class RedisCache implements CacheInterface
{
public function __construct(
private \Redis $redis,
) {}
public function get(string $key): mixed
{
$value = $this->redis->get($key);
return $value !== false ? unserialize($value) : null;
}
public function set(string $key, mixed $value, int $ttl = 3600): void
{
$this->redis->setex($key, $ttl, serialize($value));
}
}
// Service depending on abstraction
class ProductService
{
public function __construct(
private ProductRepositoryInterface $repository,
private CacheInterface $cache,
) {}
}
</example>
<framework_conventions>
Application frameworks (Laravel and peers) reward a small set of conventions that keep domain logic testable and correct as a codebase grows. These are framework-general disciplines: push persistence behind an abstraction, keep values strongly typed, and never trust a lazy relation load in a loop.
declare(strict_types=1) at the top of every file, and prefer final class when a type is not designed for extension. Finality makes the intended extension points explicit and keeps mocking honest (mock the interface, not a concrete class).
Interact with the database through a Repository rather than calling the ORM/query builder directly from controllers or services. The repository is the seam for testing and for swapping persistence, and it keeps query concerns out of domain code.
Load relations explicitly with eager loading (with() / loadMissing()) instead of touching a relation inside a loop, which triggers one query per iteration. N+1 is the most common performance regression in ORM-backed code and is invisible until the collection grows.
Prefer collection pipeline methods (map, filter, each, reduce) over manual foreach accumulation. The intent reads more directly and the transformations compose.
Represent exact monetary amounts as integers in minor units, never floats — floating point cannot represent decimal currency exactly. A workable rule set: prices are strict integers; discounts round down (consistent with how consumption tax is handled); cost figures may permit float where they are estimates rather than charged amounts.
Back enums with descriptive string values and store those strings in the database, not integers. String values keep stored data self-describing and stable across enum reordering; integer backing couples the schema to declaration order.
Add production dependencies
composer require psr/log
composer require guzzlehttp/guzzle
composer require symfony/http-foundation
<pattern name="require-dev">
<description>Add development dependencies</description>
<example>
composer require --dev phpunit/phpunit
composer require --dev phpstan/phpstan
composer require --dev friendsofphp/php-cs-fixer
</example>
</pattern>
<pattern name="version-constraints">
<description>Specify version requirements</description>
<example>
{
"require": {
"php": "^8.5",
"psr/log": "^3.0",
"guzzlehttp/guzzle": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "^10.0 || ^11.0",
"phpstan/phpstan": "^1.10"
}
}
</example>
<note>^ allows minor version updates, ~ allows patch updates only</note>
</pattern>
</package_management>
<package_development>
Standard library package structure
my-package/
├── src/
│ └── MyClass.php
├── tests/
│ └── MyClassTest.php
├── composer.json
├── phpunit.xml.dist
├── phpstan.neon
├── .php-cs-fixer.dist.php
├── LICENSE
└── README.md
<pattern name="composer-json">
<description>Complete composer.json for library</description>
<example>
{
"name": "vendor/my-package",
"description": "My awesome PHP package",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Your Name",
"email": "you@example.com"
}
],
"require": {
"php": "^8.2"
},
"require-dev": {
"phpunit/phpunit": "^11.0",
"phpstan/phpstan": "^1.10"
},
"autoload": {
"psr-4": {
"Vendor\\MyPackage\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Vendor\\MyPackage\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit",
"analyse": "phpstan analyse",
"cs-fix": "php-cs-fixer fix"
},
"config": {
"sort-packages": true
}
}
</example>
</pattern>
<pattern name="scripts">
<description>Automate common tasks with Composer scripts</description>
<example>
{
"scripts": {
"test": "phpunit --colors=always",
"test:coverage": "phpunit --coverage-html coverage",
"analyse": "phpstan analyse --memory-limit=512M",
"cs-check": "php-cs-fixer fix --dry-run --diff",
"cs-fix": "php-cs-fixer fix",
"ci": [
"@cs-check",
"@analyse",
"@test"
]
}
}
</example>
</pattern>
</package_development>
Basic PHPUnit test structure
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\DataProvider;
class CalculatorTest extends TestCase
{
private Calculator $calculator;
protected function setUp(): void
{
$this->calculator = new Calculator();
}
#[Test]
public function itAddsNumbers(): void
{
$result = $this->calculator->add(2, 3);
$this->assertSame(5, $result);
}
#[Test]
#[DataProvider('additionProvider')]
public function itAddsVariousNumbers(int $a, int $b, int $expected): void
{
$this->assertSame($expected, $this->calculator->add($a, $b));
}
public static function additionProvider(): array
{
return [
'positive numbers' => [1, 2, 3],
'negative numbers' => [-1, -2, -3],
'mixed numbers' => [-1, 2, 1],
'zeros' => [0, 0, 0],
];
}
}
</example>
</pattern>
<pattern name="mocking">
<description>Create test doubles with PHPUnit</description>
<example>
use PHPUnit\Framework\TestCase;
class UserServiceTest extends TestCase
{
#[Test]
public function itCreatesUser(): void
{
// Arrange
$repository = $this->createMock(UserRepositoryInterface::class);
$repository
->expects($this->once())
->method('save')
->with($this->isInstanceOf(User::class));
$hasher = $this->createMock(PasswordHasherInterface::class);
$hasher
->method('hash')
->willReturn('hashed_password');
$service = new UserService($repository, $hasher);
// Act
$userId = $service->create('test@example.com', 'password');
// Assert
$this->assertInstanceOf(UserId::class, $userId);
}
}
</example>
</pattern>
<pattern name="config">
<description>PHPUnit configuration file</description>
<example>
<!-- phpunit.xml.dist -->
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
cacheDirectory=".phpunit.cache">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Integration">
<directory>tests/Integration</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>src</directory>
</include>
</source>
</phpunit>
</example>
</pattern>
Pest PHP test syntax
// tests/Unit/CalculatorTest.php
use App\Calculator;
beforeEach(function () {
$this->calculator = new Calculator();
});
test('it adds numbers', function () {
expect($this->calculator->add(2, 3))->toBe(5);
});
test('it subtracts numbers', function () {
expect($this->calculator->subtract(5, 3))->toBe(2);
});
it('throws on division by zero', function () {
$this->calculator->divide(10, 0);
})->throws(DivisionByZeroError::class);
</example>
</pattern>
<pattern name="datasets">
<description>Pest datasets for parameterized tests</description>
<example>
dataset('addition', [
'positive' => [1, 2, 3],
'negative' => [-1, -2, -3],
'mixed' => [-1, 2, 1],
]);
test('it adds numbers correctly', function (int $a, int $b, int $expected) {
expect($this->calculator->add($a, $b))->toBe($expected);
})->with('addition');
</example>
</pattern>
<pattern name="expectations">
<description>Pest expectation API</description>
<example>
test('user properties', function () {
$user = new User('john@example.com', 'John Doe');
expect($user)
->toBeInstanceOf(User::class)
->email->toBe('john@example.com')
->name->toBe('John Doe')
->isActive()->toBeTrue();
});
</example>
</pattern>
<pest_v3>
<description>Pest v3 (2024+) adds architecture testing, mutation testing, and type coverage.</description>
<example>
// Architecture testing
arch()->expect('App\Models')->toExtend('Illuminate\Database\Eloquent\Model');
arch()->expect('App')->toUseStrictTypes();
// Mutation testing
// pest --mutate
</example>
</pest_v3>
<static_analysis>
PHPStan configuration
# phpstan.neon
parameters:
level: 8
paths:
- src
- tests
excludePaths:
- vendor
checkMissingIterableValueType: true
checkGenericClassInNonGenericObjectType: true
reportUnmatchedIgnoredErrors: true
<pattern name="levels">
<description>PHPStan strictness levels (0-9)</description>
<levels>
<level number="0">Basic checks</level>
<level number="1">Possibly undefined variables</level>
<level number="2">Unknown methods on $this</level>
<level number="3">Wrong return types</level>
<level number="4">Dead code</level>
<level number="5">Argument types</level>
<level number="6">Missing type hints</level>
<level number="7">Partial union types</level>
<level number="8">No mixed types</level>
<level number="9">Mixed type operations</level>
<level number="10">Stricter implicit mixed (PHPStan 2.0+)</level>
</levels>
<note>Start at level 5-6 for existing projects, level 9-10 for new projects. Use --level max for highest available.</note>
</pattern>
<pattern name="level-9-10-config">
<description>Maximum strictness configuration for new projects</description>
<example>
# phpstan.neon
includes:
- vendor/phpstan/phpstan-strict-rules/rules.neon
parameters:
level: 10 # PHPStan 2.0+ (use 9 for PHPStan 1.x)
paths:
- src
- tests
checkUninitializedProperties: true
checkBenevolentUnionTypes: true
reportPossiblyNonexistentGeneralArrayOffset: true
</example>
<note>Level 10 treats implicit mixed (missing types) as strictly as explicit mixed. Use phpstan-strict-rules for additional checks like === enforcement.</note>
</pattern>
<pattern name="generics">
<description>Generic types with PHPStan annotations</description>
<example>
/**
* @template T
* @param class-string<T> $class
* @return T
*/
public function create(string $class): object
{
return new $class();
}
/**
* @template T of object
* @param T $entity
* @return T
*/
public function save(object $entity): object
{
// persist
return $entity;
}
</example>
</pattern>
Psalm configuration
<!-- psalm.xml -->
<?xml version="1.0"?>
<psalm
errorLevel="1"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="src" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
</psalm>
<pattern name="annotations">
<description>Psalm-specific annotations</description>
<example>
/**
* @psalm-immutable
*/
readonly class ImmutableValue
{
public function __construct(
public string $value,
) {}
}
/**
* @psalm-assert-if-true User $user
*/
function isActiveUser(?User $user): bool
{
return $user !== null && $user->isActive();
}
</example>
</pattern>
<php_cs_fixer>
PHP CS Fixer configuration
<?php
// .php-cs-fixer.dist.php
$finder = PhpCsFixer\Finder::create()
->in(DIR . '/src')
->in(DIR . '/tests');
return (new PhpCsFixer\Config())
->setRules([
'@PER-CS' => true,
'@PHP85Migration' => true,
'strict_types' => true,
'declare_strict_types' => true,
'array_syntax' => ['syntax' => 'short'],
'no_unused_imports' => true,
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'trailing_comma_in_multiline' => true,
])
->setFinder($finder)
->setRiskyAllowed(true);
</example>
</pattern>
</php_cs_fixer>
Rector automated refactoring configuration
<?php
// rector.php
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\SetList;
return RectorConfig::configure()
->withPaths([
__DIR__ . '/src',
__DIR__ . '/tests',
])
->withSets([
SetList::CODE_QUALITY,
SetList::DEAD_CODE,
SetList::TYPE_DECLARATION,
])
->withPhpSets(php85: true);
</example>
<note>LevelSetList (e.g., UP_TO_PHP_85) deprecated since Rector 0.19.2. Use ->withPhpSets() instead.</note>
</pattern>
PDO database connection
$dsn = 'mysql:host=localhost;dbname=myapp;charset=utf8mb4';
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
$pdo = new PDO($dsn, $username, $password, $options);
</example>
</pattern>
<pattern name="prepared-statements">
<description>Secure parameterized queries</description>
<example>
// Named parameters
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);
$user = $stmt->fetch();
// Positional parameters
$stmt = $pdo->prepare('INSERT INTO users (email, name) VALUES (?, ?)');
$stmt->execute([$email, $name]);
$id = $pdo->lastInsertId();
</example>
<warning>Never concatenate user input into SQL queries</warning>
</pattern>
<pattern name="transactions">
<description>Database transactions with PDO</description>
<example>
try {
$pdo->beginTransaction();
$stmt = $pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?');
$stmt->execute([$amount, $fromAccount]);
$stmt = $pdo->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?');
$stmt->execute([$amount, $toAccount]);
$pdo->commit();
} catch (\Exception $e) {
$pdo->rollBack();
throw $e;
}
</example>
</pattern>
OPcache settings for production
; php.ini production settings
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0
opcache.save_comments=1
opcache.enable_file_override=1
Set validate_timestamps=0 in production, clear cache on deploy
JIT compiler settings (PHP 8.0+)
; php.ini JIT settings
opcache.jit=tracing
opcache.jit_buffer_size=128M
JIT provides most benefit for CPU-intensive tasks, less for I/O-bound web apps
Preload classes at startup (PHP 7.4+)
<?php
// preload.php
require __DIR__ . '/vendor/autoload.php';
// Preload commonly used classes
$classesToPreload = [
App\Domain\User\User::class,
App\Domain\Order\Order::class,
];
foreach ($classesToPreload as $class) {
class_exists($class);
}
</example>
<config>
; php.ini
opcache.preload=/path/to/preload.php
opcache.preload_user=www-data
</config>
</pattern>
Read-heavy, non-critical endpoints (dashboards, homepages, rankings) are the highest-leverage caching targets: they run the same expensive read on every request and tolerate slightly stale data. Wrapping them in a remembered cache can turn hundreds of milliseconds of database work per request into single-digit milliseconds and cut query load by orders of magnitude. The design work is in the invalidation and failure behavior, not the cache call itself.
<pattern name="remember_with_ttl_flag_and_namespace">
<description>Wrap the read in a remember()-style helper with an explicit TTL, gate it behind a config feature flag (so it can be turned off without a code change), and namespace the keys by prefix. The flag and namespace make the cache observable and reversible in production.</description>
<example>
return Cache::remember(
"homepage:daily_ranking",
300, // seconds
fn () => $this->repository->dailyRanking(),
);
</example>
</pattern>
<pattern name="cache_stampede_evaluation">
<description>A stampede happens when a hot key expires and many concurrent requests all recompute it at once. Evaluate the risk along four axes before choosing mitigations.</description>
<axes>
<axis name="concurrency">How many requests hit the key within the recompute window?</axis>
<axis name="data_size">How heavy is a single recomputation (query cost, payload size)?</axis>
<axis name="ttl">Shorter TTL means fresher data but more frequent expiry events.</axis>
<axis name="criticality">Presentational data tolerates brief staleness; transactional data does not.</axis>
</axes>
<mitigations>
<mitigation>Tune the TTL so expiry lands in lower-traffic periods; lengthen it if a stampede is observed.</mitigation>
<mitigation>Use a lock (Cache::lock) around the recompute so only one request rebuilds while others wait or serve stale.</mitigation>
<mitigation>Warm the cache shortly before expiry so the key never actually goes cold under load.</mitigation>
<mitigation>Wrap the cache backend in try/catch so a cache outage degrades to a direct read instead of an error.</mitigation>
</mitigations>
</pattern>
<pattern name="observe_hit_ratio">
<description>Monitor the cache hit ratio in production and alert when it drops below an expected threshold. A falling hit ratio is the leading indicator of a misconfigured key, an unexpectedly short effective TTL, or a stampede — visible before latency regresses.</description>
</pattern>
<error_handling>
Custom exception hierarchy
// Base domain exception
abstract class DomainException extends \Exception {}
// Specific exceptions
class EntityNotFoundException extends DomainException
{
public static function forClass(string $class, string $id): self
{
return new self(sprintf('%s with id "%s" not found', $class, $id));
}
}
class ValidationException extends DomainException
{
public function __construct(
string $message,
public readonly array $errors = [],
) {
parent::__construct($message);
}
}
// Usage
throw EntityNotFoundException::forClass(User::class, $userId);
</example>
Result type for error handling without exceptions
/**
* @template T
* @template E
*/
readonly class Result
{
private function __construct(
private bool $success,
private mixed $value,
) {}
/** @return self<T, never> */
public static function ok(mixed $value): self
{
return new self(true, $value);
}
/** @return self<never, E> */
public static function error(mixed $error): self
{
return new self(false, $error);
}
public function isSuccess(): bool { return $this->success; }
public function isError(): bool { return !$this->success; }
public function getValue(): mixed { return $this->value; }
}
// Usage
function divide(int $a, int $b): Result
{
if ($b === 0) {
return Result::error('Division by zero');
}
return Result::ok($a / $b);
}
</example>
<array_functions>
<php84_functions>
Find first element matching predicate (PHP 8.4+)
$users = [
['id' => 1, 'active' => false],
['id' => 2, 'active' => true],
['id' => 3, 'active' => true],
];
$firstActive = array_find($users, fn($user) => $user['active']);
// ['id' => 2, 'active' => true]
</example>
</function>
<function name="array_find_key">
<description>Find key of first element matching predicate (PHP 8.4+)</description>
<example>
$users = [
'john' => ['active' => false],
'jane' => ['active' => true],
];
$firstActiveKey = array_find_key($users, fn($user) => $user['active']);
// 'jane'
</example>
</function>
<function name="array_any">
<description>Check if any element matches predicate (PHP 8.4+)</description>
<example>
$hasAdmin = array_any($users, fn($u) => $u['role'] === 'admin');
</example>
</function>
<function name="array_all">
<description>Check if all elements match predicate (PHP 8.4+)</description>
<example>
$allActive = array_all($users, fn($u) => $u['active']);
</example>
</function>
</php84_functions>
</array_functions>
<context7_integration>
<library_id>/php/doc-en</library_id>
<trust_score>9.4</trust_score>
<usage_pattern>
For PHP core documentation, use library ID /php/doc-en
Workflow guidance
Step completed
Fetch specific topic documentation with get-library-docs
Workflow guidance
Step completed
</usage_pattern>
<common_queries>
PHP attribute syntax and usage
Enumeration types and patterns
Readonly properties and classes
Property hooks syntax (PHP 8.4+)
Pipe operator syntax (PHP 8.5)
</common_queries>
</context7_integration>
<best_practices>
Enable strict_types in all PHP files
Use prepared statements for all database queries
Use PHPStan level 9+ for type safety
Use readonly classes for value objects
Follow PSR-12 coding style
Use enums instead of string/int constants
Inject dependencies through constructor
Use named arguments for complex function calls
Create custom exceptions for domain errors
Use attributes for metadata instead of docblock annotations
</best_practices>
<anti_patterns>
Classes that do too much
Split into focused single-responsibility classes
Global service container access
Use constructor injection for explicit dependencies
Using arrays instead of typed objects
Create value objects or DTOs with typed properties
Using mixed to avoid proper typing
Use union types, generics, or proper type narrowing
Overusing static methods making testing difficult
Use instance methods with dependency injection
Returning null for error conditions
Throw exceptions or use Result type
Building SQL with string concatenation
Always use prepared statements with parameters
// Bad
$sql = "SELECT * FROM users WHERE email = '$email'";
// Good
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$email]);
</example>
Enable declare(strict_types=1) in all PHP files
Use prepared statements for all database queries
Run PHPStan level 9+ before committing
Never suppress errors with @ operator
Follow PER-CS coding style
Use named arguments for functions with 3+ parameters
Create custom exceptions for domain errors
Use readonly classes for value objects
Prefer property hooks over getters/setters (PHP 8.4+)
Use pipe operator for functional-style chaining (PHP 8.5+)
Understand PHP code requirements
1. Check PHP version constraints in composer.json
Workflow guidance
Step completed
2. Review existing type patterns in project
Workflow guidance
Step completed
3. Identify PSR standards in use
Workflow guidance
Step completed
Write type-safe PHP code
1. Add declare(strict_types=1) at file start
Workflow guidance
Step completed
2. Define interfaces before implementations
Workflow guidance
Step completed
3. Use constructor property promotion
Workflow guidance
Step completed
4. Add return types to all methods
Workflow guidance
Step completed
Verify PHP correctness
1. Run PHPStan for type checking
Workflow guidance
Step completed
2. Run PHP CS Fixer for style
Workflow guidance
Step completed
3. Run PHPUnit/Pest for tests
Workflow guidance
Step completed
<error_escalation inherits="core-patterns#error_escalation">
Minor coding style issue
PHPStan error or missing type
Breaking API change or security issue
SQL injection or authentication bypass
</error_escalation>
Add declare(strict_types=1) to all PHP files
Use prepared statements for database queries
Define explicit return types on all methods
Follow PSR-12 coding style
Using mixed type without justification
Suppressing PHPStan errors without documentation
Using @ error suppression operator
<related_skills>
Symbol-level navigation for class and interface definitions
Fetch latest PHP and library documentation
Test strategy and coverage patterns
PDO patterns and query optimization
</related_skills>
<related_agents>
Locate code patterns and references in this skill domain
Review implementation quality against this skill guidance
Analyze code complexity and suggest refactoring improvements
</related_agents>