PHP Ecosystem

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.

$ Installieren

git clone https://github.com/takeokunn/nixos-configuration /tmp/nixos-configuration && cp -r /tmp/nixos-configuration/home-manager/programs/claude-code/skills/php-ecosystem ~/.claude/skills/nixos-configuration

// tip: Run this command in your terminal to install the skill


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 =&gt; 'Draft',
        self::Published =&gt; 'Published',
        self::Archived =&gt; 'Archived',
    };
}

}

// Usage $status = Status::from('published'); $value = $status->value; // 'published'

public function color(): string
{
    return match($this) {
        self::Hearts, self::Diamonds =&gt; 'red',
        self::Clubs, self::Spades =&gt; 'black',
    };
}

}

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-&gt;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

// File: src/Domain/User/Entity/User.php namespace App\Domain\User\Entity;

class User { // Fully qualified: App\Domain\User\Entity\User }

class UserService { public function __construct( private LoggerInterface $logger, ) {}

public function create(array $data): User
{
    $this-&gt;logger-&gt;info('Creating user', ['email' =&gt; $data['email']]);

    try {
        $user = new User($data);
        $this-&gt;logger-&gt;debug('User created', ['id' =&gt; $user-&gt;getId()]);
        return $user;
    } catch (\Exception $e) {
        $this-&gt;logger-&gt;error('Failed to create user', [
            'exception' =&gt; $e,
            'data' =&gt; $data,
        ]);
        throw $e;
    }
}

}

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
    -&gt;withStatus(200)
    -&gt;withHeader('Content-Type', 'application/json');

}

class ServiceLocator { public function __construct( private ContainerInterface $container, ) {}

public function getUserService(): UserService
{
    return $this-&gt;container-&gt;get(UserService::class);
}

}

class AuthMiddleware implements MiddlewareInterface { public function process( ServerRequestInterface $request, RequestHandlerInterface $handler ): ResponseInterface { $token = $request->getHeaderLine('Authorization');

    if (!$this-&gt;validateToken($token)) {
        return new Response(401);
    }

    return $handler-&gt;handle($request);
}

}

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-&gt;streamFactory-&gt;createStream($json);

    return $this-&gt;responseFactory-&gt;createResponse($status)
        -&gt;withHeader('Content-Type', 'application/json')
        -&gt;withBody($body);
}

}

class ApiClient { public function __construct( private ClientInterface $httpClient, private RequestFactoryInterface $requestFactory, ) {}

public function get(string $url): array
{
    $request = $this-&gt;requestFactory-&gt;createRequest('GET', $url);
    $response = $this-&gt;httpClient-&gt;sendRequest($request);

    return json_decode(
        $response-&gt;getBody()-&gt;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-&gt;currency !== $other-&gt;currency) {
        throw new InvalidArgumentException('Currency mismatch');
    }
    return new self($this-&gt;amount + $other-&gt;amount, $this-&gt;currency);
}

public function equals(Money $other): bool
{
    return $this-&gt;amount === $other-&gt;amount
        &amp;&amp; $this-&gt;currency === $other-&gt;currency;
}

}

class PdoUserRepository implements UserRepositoryInterface { public function __construct( private PDO $pdo, ) {}

public function find(UserId $id): ?User
{
    $stmt = $this-&gt;pdo-&gt;prepare(
        'SELECT * FROM users WHERE id = :id'
    );
    $stmt-&gt;execute(['id' =&gt; $id-&gt;toString()]);
    $row = $stmt-&gt;fetch(PDO::FETCH_ASSOC);

    return $row ? $this-&gt;hydrate($row) : null;
}

public function save(User $user): void
{
    $stmt = $this-&gt;pdo-&gt;prepare(
        'INSERT INTO users (id, email, name)
         VALUES (:id, :email, :name)
         ON DUPLICATE KEY UPDATE email = :email, name = :name'
    );
    $stmt-&gt;execute([
        'id' =&gt; $user-&gt;getId()-&gt;toString(),
        'email' =&gt; $user-&gt;getEmail()-&gt;toString(),
        'name' =&gt; $user-&gt;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>

public function handle(CreateUserCommand $command): UserId
{
    $email = new Email($command-&gt;email);

    if ($this-&gt;userRepository-&gt;findByEmail($email) !== null) {
        throw new UserAlreadyExistsException($email);
    }

    $user = User::create(
        UserId::generate(),
        $email,
        $command-&gt;name,
        $this-&gt;passwordHasher-&gt;hash($command-&gt;password),
    );

    $this-&gt;userRepository-&gt;save($user);
    $this-&gt;eventDispatcher-&gt;dispatch(new UserCreatedEvent($user));

    return $user-&gt;getId();
}

}

// Concrete implementation class RedisCache implements CacheInterface { public function __construct( private \Redis $redis, ) {}

public function get(string $key): mixed
{
    $value = $this-&gt;redis-&gt;get($key);
    return $value !== false ? unserialize($value) : null;
}

public function set(string $key, mixed $value, int $ttl = 3600): void
{
    $this-&gt;redis-&gt;setex($key, $ttl, serialize($value));
}

}

// Service depending on abstraction class ProductService { public function __construct( private ProductRepositoryInterface $repository, private CacheInterface $cache, ) {} } </design_patterns>

<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

class CalculatorTest extends TestCase { private Calculator $calculator;

protected function setUp(): void
{
    $this-&gt;calculator = new Calculator();
}

#[Test]
public function itAddsNumbers(): void
{
    $result = $this-&gt;calculator-&gt;add(2, 3);

    $this-&gt;assertSame(5, $result);
}

#[Test]
#[DataProvider('additionProvider')]
public function itAddsVariousNumbers(int $a, int $b, int $expected): void
{
    $this-&gt;assertSame($expected, $this-&gt;calculator-&gt;add($a, $b));
}

public static function additionProvider(): array
{
    return [
        'positive numbers' =&gt; [1, 2, 3],
        'negative numbers' =&gt; [-1, -2, -3],
        'mixed numbers' =&gt; [-1, 2, 1],
        'zeros' =&gt; [0, 0, 0],
    ];
}

}

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-&gt;createMock(PasswordHasherInterface::class);
    $hasher
        -&gt;method('hash')
        -&gt;willReturn('hashed_password');

    $service = new UserService($repository, $hasher);

    // Act
    $userId = $service-&gt;create('test@example.com', 'password');

    // Assert
    $this-&gt;assertInstanceOf(UserId::class, $userId);
}

}

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);

test('it adds numbers correctly', function (int $a, int $b, int $expected) { expect($this->calculator->add($a, $b))->toBe($expected); })->with('addition');

expect($user)
    -&gt;toBeInstanceOf(User::class)
    -&gt;email-&gt;toBe('john@example.com')
    -&gt;name-&gt;toBe('John Doe')
    -&gt;isActive()-&gt;toBeTrue();

});

<static_analysis> PHPStan configuration

phpstan.neon

parameters: level: 8 paths: - src - tests excludePaths: - vendor checkMissingIterableValueType: true checkGenericClassInNonGenericObjectType: true reportUnmatchedIgnoredErrors: true

/**

  • @template T of object
  • @param T $entity
  • @return T */ public function save(object $entity): object { // persist return $entity; }

/**

  • @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>

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 = new PDO($dsn, $username, $password, $options);

// 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

$stmt = $pdo-&gt;prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?');
$stmt-&gt;execute([$amount, $fromAccount]);

$stmt = $pdo-&gt;prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?');
$stmt-&gt;execute([$amount, $toAccount]);

$pdo-&gt;commit();

} catch (\Exception $e) { $pdo->rollBack(); throw $e; }

// 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);

/** @return self&lt;T, never&gt; */
public static function ok(mixed $value): self
{
    return new self(true, $value);
}

/** @return self&lt;never, E&gt; */
public static function error(mixed $error): self
{
    return new self(false, $error);
}

public function isSuccess(): bool { return $this-&gt;success; }
public function isError(): bool { return !$this-&gt;success; }
public function getValue(): mixed { return $this-&gt;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

// 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>

<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_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>

Repository

takeokunn
takeokunn
Author
takeokunn/nixos-configuration/home-manager/programs/claude-code/skills/php-ecosystem
52
Stars
0
Forks
Updated5d ago
Added6d ago