
php-ecosystem
by takeokunn
takeokunn's nixos-configuration
SKILL.md
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.
<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>
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>
<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>
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> </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
// File: src/Domain/User/Entity/User.php
namespace App\Domain\User\Entity;
class User
{
// Fully qualified: App\Domain\User\Entity\User
}
</example>
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>
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>
class ServiceLocator
{
public function __construct(
private ContainerInterface $container,
) {}
public function getUserService(): UserService
{
return $this->container->get(UserService::class);
}
}
</example>
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>
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>
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>
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>
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>
// 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>
<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.2",
"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>
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>
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>
<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="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>
<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-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);
</example>
</pattern>
</php_cs_fixer>
return RectorConfig::configure()
->withPaths([
__DIR__ . '/src',
__DIR__ . '/tests',
])
->withSets([
SetList::CODE_QUALITY,
SetList::DEAD_CODE,
SetList::TYPE_DECLARATION,
])
->withPhpSets(php83: true);
</example>
<note>LevelSetList (e.g., UP_TO_PHP_83) deprecated since Rector 0.19.2. Use ->withPhpSets() instead.</note>
</pattern>
$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>
// 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>
<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>
/** @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>
<anti_patterns> Classes that do too much Split into focused single-responsibility classes
// Good
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$email]);
</example>
<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>
<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>
<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>
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
1ヶ月以内に更新
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon


