Enums
Class constants for statuses look tidy: PaymentStatus::CAPTURED instead of 'captured'. But the constant is still a string. A method that takes string $status will take 'captured', 'CAPTURED', 'capturd', or a customer's surname. The constants are a convention, not a type.
Enums are types
enum PaymentStatus: string { case Authorised = 'authorised'; case Captured = 'captured'; case Refunded = 'refunded'; case Failed = 'failed'; }
Now function refund(PaymentStatus $status) accepts exactly four values, and PHP rejects anything else. Cases are singletons, so === compares them safely.
A backed enum also has a scalar value for storage. That's the boundary:
PaymentStatus::from('captured')gives youPaymentStatus::Captured, and throws aValueErrorfor anything that isn't a case.PaymentStatus::tryFrom('capturd')gives younullinstead, for when bad input is expected.$status->valueis the string to store.
Convert once, when the row comes in, and the rest of the code never sees a status string again.
Rules live with the cases
Enums can have methods and implement interfaces, so the rules about each status can sit with the status rather than in an if in some controller:
public function canRefund(): bool { return match ($this) { self::Captured => true, self::Authorised, self::Refunded, self::Failed => false, }; }
List every case and PHPStan checks the match is exhaustive. Add Disputed next year and every match that forgot it is reported, rather than throwing an UnhandledMatchError in production.
Power-up
<?php declare(strict_types=1); final class PaymentStatus { public const AUTHORISED = 'authorised'; public const CAPTURED = 'captured'; public const REFUNDED = 'refunded'; public const FAILED = 'failed'; } // Somewhere far away: if ($status === 'captured' || $status === 'Captured') { ...
<?php declare(strict_types=1); enum PaymentStatus: string { case Authorised = 'authorised'; case Captured = 'captured'; case Refunded = 'refunded'; case Failed = 'failed'; public function canRefund(): bool { return match ($this) { self::Captured => true, self::Authorised, self::Refunded, self::Failed => false, }; } }