DEV Community

Cover image for Using PHP Backed Enums in Laravel with Testing
Ivan Mykhavko
Ivan Mykhavko

Posted on

Using PHP Backed Enums in Laravel with Testing

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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],
        ]
    ]);
}
Enter fullscreen mode Exit fullscreen mode

The Quirk with assertJson

If we skip ->value in the assertJson assertion:

$response->assertJson([
    'data' => [
        'item_status' => ['id' => OrderItemStatusEnum::REFUSAL_CLIENT],
    ]
]);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

The failure message is misleading:

Unable to find JSON:

[{
    "data": {
        "item_status": {
            "id": 3
        }
    }
}]

within response JSON:

[{
    "data": {
        "item_status": {
            "id": 3
        }
    }
}].
Enter fullscreen mode Exit fullscreen mode

Even though the values look identical, Laravel's assertJson calls json_encode on the enum, converting it to a string and causing a mismatch.

More on This Issue

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!

Image of Datadog

Create and maintain end-to-end frontend tests

Learn best practices on creating frontend tests, testing on-premise apps, integrating tests into your CI/CD pipeline, and using Datadog’s testing tunnel.

Download The Guide

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs