Introduction to PHP Enums
PHP 8.1 introduced enums, providing a structured way to define a set of named values. Laravel integrates well with enums, allowing their use in models, validation rules, and query conditions. However, when it comes to testing, some quirks can lead to unexpected failures.
Example: Order Item Status Enum
Here's an example of a backed enum for order item statuses:
namespace App\Support\Enums\Order;
enum OrderItemStatusEnum: int
{
case ACCEPTED = 1;
case SUSPENDED = 2;
case REFUSAL_CLIENT = 3;
case SUPPLIER_REJECTION = 4;
case PRICE_EXCEEDED = 5;
case IN_TRANSIT = 6;
case IN_STOCK = 7;
case READY_FOR_SHIPMENT = 8;
case SHIPPED = 9;
case DAMAGED = 10;
case SENT_TO_SUPPLIER = 11;
case IN_ASSEMBLY = 12;
case PURCHASED = 13;
}
Using Enums in Laravel Actions
The OrderItemCancelAction
updates an order item status using the enum:
final class OrderItemCancelAction implements Actionable
{
public function handle(string $orderItemUuId, int $userId, int $managerId = null): OrderItem
{
$orderItem = OrderItem::query()->where('is_canceled', false)->findOrFail($orderItemUuId);
DB::transaction(function () use ($orderItem) {
$orderItem->update(['status_id' => OrderItemStatusEnum::REFUSAL_CLIENT->value]);
});
return $orderItem;
}
}
Testing with Backed Enums
Here's a feature test that verifies the status update:
public function test_admin_cancel_order_item(): void
{
$user = $this->getDefaultUser();
$admin = $this->getAdmin();
$this->actingAsAdmin($admin);
$order = $this->createOrder($user);
$orderItem = OrderItemFactory::new()->for($order)->for($user)->create();
$data = ['user_id' => $user->id];
$response = $this->put(route('api-admin:order.order-items.cancel', ['uuid' => $orderItem->uuid]), $data);
$response->assertOk();
$response->assertJson([
'data' => [
'item_status' => ['id' => OrderItemStatusEnum::REFUSAL_CLIENT->value],
]
]);
}
The Quirk with assertJson
If we skip ->value
in the assertJson
assertion:
$response->assertJson([
'data' => [
'item_status' => ['id' => OrderItemStatusEnum::REFUSAL_CLIENT],
]
]);
The test will fail, even though Laravel allows enums without ->value
in many places, such as:
$order = Order::query()->whereIn('status_id', [OrderStatusEnum::PROCESSING, OrderStatusEnum::SUSPENDED])->findOrFail($orderItem->order_id);
The failure message is misleading:
Unable to find JSON:
[{
"data": {
"item_status": {
"id": 3
}
}
}]
within response JSON:
[{
"data": {
"item_status": {
"id": 3
}
}
}].
Even though the values look identical, Laravel's assertJson
calls json_encode
on the enum, converting it to a string and causing a mismatch.
For a deeper dive, check out An unhelpful error message with enums and assertJson, where Joel Clermont explains why this happens due to PHPUnit's assertArraySubset
treating enums differently.
Conclusion
When working with PHP backed enums in Laravel, always use ->value
to avoid confusions. While Laravel allows direct enum usage in many areas, PHPUnit tests require explicit conversion. Don't rely on PHP or Laravel's magic - be explicit with your values!
Top comments (0)