| 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. |
Provide comprehensive patterns for modern PHP (8.1+) language features, PSR standards, testing, static analysis, and package development in a framework-agnostic approach.
<php_version>
<version_mapping>
PHP version-specific feature availability
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
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>
</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'
Enum without backing value
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',
};
}
}
Immutable property (PHP 8.1+)
class User
{
public function __construct(
public readonly string $id,
public readonly string $email,
) {}
}
All properties become readonly (PHP 8.2+)
readonly class ValueObject
{
public function __construct(
public string $name,
public int $value,
) {}
}
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'
}
<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;
}
}
</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',
);
<decision_tree name="when_to_use">
Are you skipping optional parameters or improving readability?
<if_yes>Use named arguments</if_yes>
<if_no>Use positional arguments for simple calls</if_no>
</decision_tree>
</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>
</type_system>
<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
}
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;
}
}
}
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');
}
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);
}
}
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);
}
}
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);
}
}
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
);
}
}
</psr_standards>
<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;
}
}
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(),
]);
}
}
<decision_tree name="when_to_use">
Do you need to abstract persistence details from domain logic?
<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();
}
}
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,
) {}
}
</design_patterns>
Add production dependencies
composer require psr/log
composer require guzzlehttp/guzzle
composer require symfony/http-foundation
Add development dependencies
composer require --dev phpunit/phpunit
composer require --dev phpstan/phpstan
composer require --dev friendsofphp/php-cs-fixer
Specify version requirements
{
"require": {
"php": "^8.2",
"psr/log": "^3.0",
"guzzlehttp/guzzle": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "^10.0 || ^11.0",
"phpstan/phpstan": "^1.10"
}
}
^ allows minor version updates, ~ allows patch updates only
<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
Complete composer.json for library
{
"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
}
}
Automate common tasks with Composer scripts
{
"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"
]
}
}
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],
];
}
}
Create test doubles with PHPUnit
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);
}
}
PHPUnit configuration file
<!-- 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>
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);
Pest datasets for parameterized tests
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');
Pest expectation API
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();
});
<static_analysis>
PHPStan configuration
phpstan.neon
parameters:
level: 8
paths: - src - tests
excludePaths: - vendor
checkMissingIterableValueType: true
checkGenericClassInNonGenericObjectType: true
reportUnmatchedIgnoredErrors: true
PHPStan strictness levels (0-9)
Basic checks
Possibly undefined variables
Unknown methods on $this
Wrong return types
Dead code
Argument types
Missing type hints
Partial union types
No mixed types
Mixed type operations
Stricter implicit mixed (PHPStan 2.0+)
Start at level 5-6 for existing projects, level 9-10 for new projects. Use --level max for highest available.
Generic types with PHPStan annotations
/**
* @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;
}
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>
Psalm-specific annotations
/**
* @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();
}
<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-CS2.0' => true,
'@PHP82Migration' => 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);
</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(php83: true);
LevelSetList (e.g., UP_TO_PHP_83) deprecated since Rector 0.19.2. Use ->withPhpSets() instead.
</static_analysis>
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);
Secure parameterized queries
// 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();
Never concatenate user input into SQL queries
Database transactions with PDO
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;
}
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=1255
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);
}
; php.ini
opcache.preload=/path/to/preload.php
opcache.preload_user=www-data
<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);
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);
}
</error_handling>
<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]);
</anti_patterns>
<best_practices>
Enable strict_types in all PHP files
Use prepared statements for all database queries
Use PHPStan level 6+ 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>
Understand PHP code requirements
1. Check PHP version constraints in composer.json
2. Review existing type patterns in project
3. Identify PSR standards in use
Write type-safe PHP code
1. Add declare(strict_types=1) at file start
2. Define interfaces before implementations
3. Use constructor property promotion
4. Add return types to all methods
Verify PHP correctness
1. Run PHPStan for type checking
2. Run PHP CS Fixer for style
3. Run PHPUnit/Pest for tests
<error_escalation>
Minor coding style issue
Auto-fix with PHP CS Fixer
PHPStan error or missing type
Fix type, verify with static analysis
Breaking API change or security issue
Stop, present options to user
SQL injection or authentication bypass
Block operation, require immediate fix
</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_agents>
API design, architecture, and module structure planning
PHP implementation with strict typing and PSR compliance
PHPStan validation, type safety, and best practices
PHPUnit and Pest test creation and coverage
SQL injection, XSS, and authentication vulnerabilities
</related_agents>
<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>