| 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. |
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'
public function color(): string
{
return match($this) {
self::Hearts, self::Diamonds => 'red',
self::Clubs, self::Spades => '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' }
public function getName(): string
{
return $this->name;
}
}
// Usage with named arguments $user = createUser( email: 'user@example.com', name: 'John Doe', role: 'admin', );
// 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->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;
}
}
}
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');
}
class ServiceLocator { public function __construct( private ContainerInterface $container, ) {}
public function getUserService(): UserService
{
return $this->container->get(UserService::class);
}
}
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);
}
}
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);
}
}
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
);
}
}
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;
}
}
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(),
]);
}
}
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();
}
}
// 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, ) {} }
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],
];
}
}
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);
}
}
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)
->toBeInstanceOf(User::class)
->email->toBe('john@example.com')
->name->toBe('John Doe')
->isActive()->toBeTrue();
});
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(); }
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);
return RectorConfig::configure() ->withPaths([ DIR . '/src', DIR . '/tests', ]) ->withSets([ SetList::CODE_QUALITY, SetList::DEAD_CODE, SetList::TYPE_DECLARATION, ]) ->withPhpSets(php83: true);
$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();
$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; }
// Preload commonly used classes $classesToPreload = [ App\Domain\User\User::class, App\Domain\Order\Order::class, ];
foreach ($classesToPreload as $class) {
class_exists($class);
}
// 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<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); }
// Good $stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?'); $stmt->execute([$email]);