advanced php language
Enums
Enums let you model a fixed set of valid values as real PHP types. They are useful when a value must be one of a known list, such as an order status, user role, payment method, export format, or notification channel.
Before enums, PHP projects often used strings or class constants for this. Strings are easy to mistype, and constants do not force a function parameter to accept only one of the allowed values. Enums make the allowed values explicit.
Pure Enums
A pure enum has named cases but no string or integer value attached to each case.
<?php
declare(strict_types=1);
enum DeliverySpeed
{
case Standard;
case Express;
case SameDay;
}
function labelFor(DeliverySpeed $speed): string
{
return match ($speed) {
DeliverySpeed::Standard => 'Standard delivery',
DeliverySpeed::Express => 'Express delivery',
DeliverySpeed::SameDay => 'Same-day delivery',
};
}
echo labelFor(DeliverySpeed::Express) . PHP_EOL;
// Prints:
// Express delivery
The function cannot be called with an arbitrary string. It requires a DeliverySpeed case.
Backed Enums
A backed enum gives each case a string or integer value. This is useful when the enum must cross a boundary such as a database column, API payload, form field, or queue message.
<?php
declare(strict_types=1);
enum OrderStatus: string
{
case Draft = 'draft';
case Paid = 'paid';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
}
$status = OrderStatus::Paid;
echo $status->name . PHP_EOL;
echo $status->value . PHP_EOL;
// Prints:
// Paid
// paid
name is the PHP case name. value is the backed value stored or sent outside PHP.
from() And tryFrom()
Backed enums can be created from stored values.
from() returns the enum case or throws ValueError if the value is invalid. tryFrom() returns the enum case or null.
<?php
declare(strict_types=1);
enum OrderStatus: string
{
case Draft = 'draft';
case Paid = 'paid';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
}
$status = OrderStatus::from('paid');
echo $status->name . PHP_EOL;
$missing = OrderStatus::tryFrom('refunded');
echo $missing === null ? 'unknown status' : $missing->name;
echo PHP_EOL;
// Prints:
// Paid
// unknown status
Use tryFrom() when handling untrusted input such as form values, query strings, JSON, or webhooks. Use from() when invalid data should be treated as a programming or data-integrity error.
Enums Can Have Methods
Enums can contain methods, which is useful when behaviour belongs directly to the set of cases.
<?php
declare(strict_types=1);
enum OrderStatus: string
{
case Draft = 'draft';
case Paid = 'paid';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
public function canBeCancelled(): bool
{
return match ($this) {
self::Draft, self::Paid => true,
self::Shipped, self::Cancelled => false,
};
}
public function customerLabel(): string
{
return match ($this) {
self::Draft => 'Not checked out',
self::Paid => 'Payment received',
self::Shipped => 'On the way',
self::Cancelled => 'Cancelled',
};
}
}
$status = OrderStatus::Paid;
echo $status->customerLabel() . PHP_EOL;
echo $status->canBeCancelled() ? 'can cancel' : 'cannot cancel';
echo PHP_EOL;
// Prints:
// Payment received
// can cancel
This keeps status-related logic near the status definition instead of scattering string comparisons through services and controllers.
cases()
Every enum has a cases() method that returns all cases.
<?php
declare(strict_types=1);
enum ExportFormat: string
{
case Csv = 'csv';
case Json = 'json';
case Xml = 'xml';
}
foreach (ExportFormat::cases() as $format) {
echo $format->value . PHP_EOL;
}
// Prints:
// csv
// json
// xml
This is useful for building select options, validation lists, documentation output, and tests that cover every case.
Enums At Boundaries
Backed enum values often enter and leave the application as strings.
<?php
declare(strict_types=1);
enum ExportFormat: string
{
case Csv = 'csv';
case Json = 'json';
}
function parseFormat(string $input): ExportFormat
{
$format = ExportFormat::tryFrom(strtolower(trim($input)));
if ($format === null) {
throw new InvalidArgumentException('Unsupported export format.');
}
return $format;
}
echo parseFormat(' CSV ')->value . PHP_EOL;
// Prints:
// csv
Convert external strings into enums at the boundary. Inside the application, pass the enum type instead of passing the raw string around.
Enums Versus Constants
Constants are still fine for values that do not need type safety or behaviour. Enums are better when the value represents a closed set and functions should require one valid option.
Use an enum for:
- order statuses
- user roles
- payment states
- export formats
- notification channels
- feature modes
Do not use an enum for values that change often in the database, such as product categories managed by admins. Those should usually be records, not code cases.
What You Should Be Able To Do
After this lesson, you should be able to create pure and backed enums, convert input with tryFrom(), use match with enum cases, add simple methods to enums, and decide whether an enum or database table is the better model.
For junior work, this matters because enums remove a common source of bugs: loose strings that are misspelled, undocumented, or accepted in the wrong place.
Practice
Practice: Model Order Status With An Enum
Create a small PHP example using a backed enum for order status.
Task
Build an OrderStatus enum with these cases:
DraftPaidShippedCancelled
Each case should have a string value suitable for storage. Add:
- a
customerLabel()method - a
canBeCancelled()method - a function that parses an external string with
tryFrom()
Use strict types. Keep the expected output in the PHP code block as printed lines or comments.
Check Your Work
Run cases for:
- parsing a valid stored value
- parsing an invalid stored value
- printing a customer label
- checking whether a shipped order can be cancelled
Afterward, explain why passing OrderStatus inside the application is safer than passing raw strings.
Show solution
This solution converts external strings into an enum at the boundary, then uses the enum type inside the application.
<?php
declare(strict_types=1);
enum OrderStatus: string
{
case Draft = 'draft';
case Paid = 'paid';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
public function customerLabel(): string
{
return match ($this) {
self::Draft => 'Not checked out',
self::Paid => 'Payment received',
self::Shipped => 'On the way',
self::Cancelled => 'Cancelled',
};
}
public function canBeCancelled(): bool
{
return match ($this) {
self::Draft, self::Paid => true,
self::Shipped, self::Cancelled => false,
};
}
}
function parseOrderStatus(string $value): OrderStatus
{
$status = OrderStatus::tryFrom(strtolower(trim($value)));
if ($status === null) {
throw new InvalidArgumentException('Unknown order status.');
}
return $status;
}
$paid = parseOrderStatus(' paid ');
echo $paid->customerLabel() . PHP_EOL;
echo OrderStatus::Shipped->canBeCancelled() ? 'can cancel' : 'cannot cancel';
echo PHP_EOL;
try {
parseOrderStatus('refunded');
} catch (InvalidArgumentException $exception) {
echo $exception->getMessage() . PHP_EOL;
}
// Prints:
// Payment received
// cannot cancel
// Unknown order status.
Passing OrderStatus inside the application is safer because a function can only receive one of the defined cases. Raw strings can be misspelled or invented accidentally.