Back to list
takeokunn

php-ecosystem

by takeokunn

takeokunn's nixos-configuration

59🍴 0📅 Jan 23, 2026

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

    // Usage
    $status = Status::from('published');
    $value = $status-&gt;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 =&gt; 'red',
                self::Clubs, self::Spades =&gt; '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-&gt;getAttributes(Route::class);
    foreach ($attributes as $attribute) {
        $route = $attribute-&gt;newInstance();
        echo $route-&gt;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-&gt;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-&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;
          }
      }
  }
</example>
  function handleRequest(ServerRequestInterface $request): ResponseInterface
  {
      $method = $request-&gt;getMethod();
      $uri = $request-&gt;getUri();
      $body = $request-&gt;getParsedBody();
      $query = $request-&gt;getQueryParams();

      // PSR-7 messages are immutable
      $response = new Response();
      return $response
          -&gt;withStatus(200)
          -&gt;withHeader('Content-Type', 'application/json');
  }
</example>
  class ServiceLocator
  {
      public function __construct(
          private ContainerInterface $container,
      ) {}

      public function getUserService(): UserService
      {
          return $this-&gt;container-&gt;get(UserService::class);
      }
  }
</example>
  class AuthMiddleware implements MiddlewareInterface
  {
      public function process(
          ServerRequestInterface $request,
          RequestHandlerInterface $handler
      ): ResponseInterface {
          $token = $request-&gt;getHeaderLine('Authorization');

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

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

          return $this-&gt;responseFactory-&gt;createResponse($status)
              -&gt;withHeader('Content-Type', 'application/json')
              -&gt;withBody($body);
      }
  }
</example>
  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
          );
      }
  }
</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-&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;
      }
  }
</example>
  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(),
          ]);
      }
  }
</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-&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();
      }
  }
</example>
  // 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,
      ) {}
  }
</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-&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],
            ];
        }
    }
  </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-&gt;createMock(UserRepositoryInterface::class);
            $repository
                -&gt;expects($this-&gt;once())
                -&gt;method('save')
                -&gt;with($this-&gt;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);
        }
    }
  </example>
</pattern>

<pattern name="config">
  <description>PHPUnit configuration file</description>
  <example>
    &lt;!-- phpunit.xml.dist --&gt;
    &lt;?xml version="1.0" encoding="UTF-8"?&gt;
    &lt;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"&gt;
        &lt;testsuites&gt;
            &lt;testsuite name="Unit"&gt;
                &lt;directory&gt;tests/Unit&lt;/directory&gt;
            &lt;/testsuite&gt;
            &lt;testsuite name="Integration"&gt;
                &lt;directory&gt;tests/Integration&lt;/directory&gt;
            &lt;/testsuite&gt;
        &lt;/testsuites&gt;
        &lt;source&gt;
            &lt;include&gt;
                &lt;directory&gt;src&lt;/directory&gt;
            &lt;/include&gt;
        &lt;/source&gt;
    &lt;/phpunit&gt;
  </example>
</pattern>
    beforeEach(function () {
        $this-&gt;calculator = new Calculator();
    });

    test('it adds numbers', function () {
        expect($this-&gt;calculator-&gt;add(2, 3))-&gt;toBe(5);
    });

    test('it subtracts numbers', function () {
        expect($this-&gt;calculator-&gt;subtract(5, 3))-&gt;toBe(2);
    });

    it('throws on division by zero', function () {
        $this-&gt;calculator-&gt;divide(10, 0);
    })-&gt;throws(DivisionByZeroError::class);
  </example>
</pattern>

<pattern name="datasets">
  <description>Pest datasets for parameterized tests</description>
  <example>
    dataset('addition', [
        'positive' =&gt; [1, 2, 3],
        'negative' =&gt; [-1, -2, -3],
        'mixed' =&gt; [-1, 2, 1],
    ]);

    test('it adds numbers correctly', function (int $a, int $b, int $expected) {
        expect($this-&gt;calculator-&gt;add($a, $b))-&gt;toBe($expected);
    })-&gt;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)
            -&gt;toBeInstanceOf(User::class)
            -&gt;email-&gt;toBe('john@example.com')
            -&gt;name-&gt;toBe('John Doe')
            -&gt;isActive()-&gt;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&lt;T&gt; $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 &amp;&amp; $user-&gt;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())
        -&gt;setRules([
            '@PER-CS2.0' =&gt; true,
            '@PHP82Migration' =&gt; true,
            'strict_types' =&gt; true,
            'declare_strict_types' =&gt; true,
            'array_syntax' =&gt; ['syntax' =&gt; 'short'],
            'no_unused_imports' =&gt; true,
            'ordered_imports' =&gt; ['sort_algorithm' =&gt; 'alpha'],
            'trailing_comma_in_multiline' =&gt; true,
        ])
        -&gt;setFinder($finder)
        -&gt;setRiskyAllowed(true);
  </example>
</pattern>

</php_cs_fixer>

    return RectorConfig::configure()
        -&gt;withPaths([
            __DIR__ . '/src',
            __DIR__ . '/tests',
        ])
        -&gt;withSets([
            SetList::CODE_QUALITY,
            SetList::DEAD_CODE,
            SetList::TYPE_DECLARATION,
        ])
        -&gt;withPhpSets(php83: true);
  </example>
  <note>LevelSetList (e.g., UP_TO_PHP_83) deprecated since Rector 0.19.2. Use -&gt;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-&gt;prepare('SELECT * FROM users WHERE email = :email');
    $stmt-&gt;execute(['email' =&gt; $email]);
    $user = $stmt-&gt;fetch();

    // Positional parameters
    $stmt = $pdo-&gt;prepare('INSERT INTO users (email, name) VALUES (?, ?)');
    $stmt-&gt;execute([$email, $name]);
    $id = $pdo-&gt;lastInsertId();
  </example>
  <warning>Never concatenate user input into SQL queries</warning>
</pattern>

<pattern name="transactions">
  <description>Database transactions with PDO</description>
  <example>
    try {
        $pdo-&gt;beginTransaction();

        $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-&gt;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&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);
  }
</example>

<anti_patterns> Classes that do too much Split into focused single-responsibility classes

  // Good
  $stmt = $pdo-&gt;prepare('SELECT * FROM users WHERE email = ?');
  $stmt-&gt;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

55/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon