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', ); Are you skipping optional parameters or improving readability? Use named arguments Use positional arguments for simple calls
// 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(),
]);
}
} Do you need to abstract persistence details from domain logic? Use Repository pattern Direct database access may be sufficient for simple CRUD
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); LevelSetList (e.g., UP_TO_PHP_83) deprecated since Rector 0.19.2. Use ->withPhpSets() instead.
$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->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 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
// 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]);