Laravel Typed Enums: Casts, Rules &amp; PHP 8.3 Patterns | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://msaied.com/public) - [Home](https://msaied.com/public)
- [Projects](https://msaied.com/public/projects)
- [Articles](https://msaied.com/public/articles)
- [Certificates](https://msaied.com/public/certificates)
- [About](https://msaied.com/public#about)

           [  Contact](https://msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://msaied.com/public)
- [ProjectsCase studies](https://msaied.com/public/projects)
- [ArticlesEngineering notes](https://msaied.com/public/articles)
- [CertificatesCredentials](https://msaied.com/public/certificates)
- [AboutHow I work](https://msaied.com/public#about)
- [ContactGet in touch](https://msaied.com/public#contact)

  [Start a conversation](https://msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://msaied.com/public)
2. /
3. [Articles](https://msaied.com/public/articles)
4. /
5. Laravel Typed Enums as First-Class Citizens: Casts, Rules, and PHP 8.3 Features

 Laravel Typed Enums as First-Class Citizens: Casts, Rules, and PHP 8.3 Features
================================================================================

 PHP 8.1 enums changed how we model domain state. This article shows how to wire backed enums into Eloquent casts, form validation rules, and route binding — with PHP 8.3 typed constants making everything tighter.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://msaied.com/public#person) Published 29 Jul 2026 · Updated 29 Jul 2026 · 3 min read

ShareCopy linkCopied

 ![Laravel Typed Enums as First-Class Citizens: Casts, Rules, and PHP 8.3 Features](https://cdn.msaied.com/483/0b80ca4c39b98b48f8d3475ac949ea0f.png) 

  On this page +1. [Why Enums Deserve More Than a CastsAttributes Afterthought](#why-enums-deserve-more-than-a-codecastsattributescode-afterthought)
2. [1. Backed Enums as Eloquent Casts](#1-backed-enums-as-eloquent-casts)
3. [2. Enum-Aware Validation Rules](#2-enum-aware-validation-rules)
4. [3. Route Model Binding with Enums](#3-route-model-binding-with-enums)
5. [4. PHP 8.3 Typed Class Constants](#4-php-83-typed-class-constants)
6. [5. Serialising Enums in API Resources](#5-serialising-enums-in-api-resources)
7. [Takeaways](#takeaways)

 Why Enums Deserve More Than a `CastsAttributes` Afterthought
------------------------------------------------------------

Most teams adopt backed enums, slap `->casts` on the model, and call it done. That leaves a lot of safety on the table. Enums can own their own validation logic, drive route resolution, and — with PHP 8.3 typed class constants — become genuinely self-documenting domain primitives.

---

1. Backed Enums as Eloquent Casts
---------------------------------

Laravel resolves any `BackedEnum` class directly in the `$casts` array:

```php
// app/Enums/OrderStatus.php
enum OrderStatus: string
{
    case Pending   = 'pending';
    case Confirmed = 'confirmed';
    case Shipped   = 'shipped';
    case Cancelled = 'cancelled';

    public function label(): string
    {
        return match($this) {
            self::Pending   => 'Awaiting Payment',
            self::Confirmed => 'Order Confirmed',
            self::Shipped   => 'On Its Way',
            self::Cancelled => 'Cancelled',
        };
    }

    public function isTerminal(): bool
    {
        return in_array($this, [self::Shipped, self::Cancelled], true);
    }
}

```

```php
// app/Models/Order.php
protected $casts = [
    'status' => OrderStatus::class,
];

```

Now `$order->status` is always an `OrderStatus` instance — never a raw string. Calling `$order->status->label()` is safe everywhere without defensive checks.

---

2. Enum-Aware Validation Rules
------------------------------

Laravel ships `Rule::enum()`, but you can push logic into the enum itself for reuse across HTTP and CLI contexts:

```php
use Illuminate\Validation\Rules\Enum;

// In a Form Request
public function rules(): array
{
    return [
        'status' => ['required', new Enum(OrderStatus::class)],
    ];
}

```

For transitions, a custom rule keeps the policy inside the enum:

```php
enum OrderStatus: string
{
    // ... cases above ...

    /** @return self[] */
    public function allowedTransitions(): array
    {
        return match($this) {
            self::Pending   => [self::Confirmed, self::Cancelled],
            self::Confirmed => [self::Shipped,   self::Cancelled],
            default         => [],
        };
    }
}

```

```php
// app/Rules/ValidStatusTransition.php
class ValidStatusTransition implements ValidationRule
{
    public function __construct(private readonly OrderStatus $current) {}

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $next = OrderStatus::tryFrom($value);

        if ($next === null || ! in_array($next, $this->current->allowedTransitions(), true)) {
            $fail("Cannot transition from {$this->current->value} to {$value}.");
        }
    }
}

```

The rule is instantiated with the *current* status, so the transition matrix lives in one place.

---

3. Route Model Binding with Enums
---------------------------------

Bind an enum directly in a route without a custom resolver:

```php
// routes/api.php
Route::get('/orders/status/{status}', [OrderController::class, 'byStatus']);

```

```php
// app/Http/Controllers/OrderController.php
public function byStatus(OrderStatus $status): JsonResponse
{
    $orders = Order::where('status', $status)->paginate();
    return response()->json($orders);
}

```

Laravel automatically calls `OrderStatus::from($routeValue)` and returns a 404 if the value is invalid — zero boilerplate.

---

4. PHP 8.3 Typed Class Constants
--------------------------------

PHP 8.3 allows typed constants on enums, which is perfect for associating metadata without a separate config file:

```php
enum OrderStatus: string
{
    case Pending   = 'pending';
    case Confirmed = 'confirmed';
    case Shipped   = 'shipped';
    case Cancelled = 'cancelled';

    // Typed constant — enforced by the engine
    const array TERMINAL = [self::Shipped, self::Cancelled];
    const string DEFAULT  = self::Pending->value;
}

```

Now `OrderStatus::TERMINAL` is a typed `array` constant — no docblock needed, and static analysis tools understand it without plugins.

---

5. Serialising Enums in API Resources
-------------------------------------

Avoid leaking raw values or accidentally serialising the enum object:

```php
// app/Http/Resources/OrderResource.php
public function toArray(Request $request): array
{
    return [
        'id'     => $this->id,
        'status' => [
            'value'      => $this->status->value,
            'label'      => $this->status->label(),
            'terminal'   => $this->status->isTerminal(),
        ],
    ];
}

```

Frontend consumers get a stable contract with both machine and human-readable representations.

---

Takeaways
---------

- Use `$casts` with the enum class directly — Laravel handles `BackedEnum` natively.
- Push transition logic into the enum itself; validation rules just delegate to it.
- Route model binding resolves backed enums automatically with a 404 on invalid values.
- PHP 8.3 typed constants let enums carry structured metadata without external config.
- Serialise enums explicitly in API resources to keep frontend contracts stable.

- [laravel](https://msaied.com/public/articles?search=laravel)
- [php](https://msaied.com/public/articles?search=php)
- [enums](https://msaied.com/public/articles?search=enums)
- [eloquent](https://msaied.com/public/articles?search=eloquent)
- [api](https://msaied.com/public/articles?search=api)

 Frequently asked questions 
---------------------------

  Does Laravel automatically validate enum values when casting?No. The cast silently returns null for invalid values when using `tryFrom` internally. You still need `Rule::enum()` or a custom validation rule in your Form Request to reject bad input before it reaches the model.

   Can I use a pure (non-backed) enum as an Eloquent cast?Not directly. Eloquent's built-in enum cast requires a `BackedEnum` because it needs a scalar value to store in the database. For pure enums you must implement a custom `CastsAttributes` class.

   Are PHP 8.3 typed constants on enums supported by PHPStan and Psalm?Yes. Both PHPStan (level 6+) and Psalm understand typed class constants on enums as of their current stable releases, giving you full static analysis coverage without extra stubs.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://msaied.com/public#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleBlade Formatting in Laravel Pint 1.30.0](https://msaied.com/public/articles/blade-formatting-in-laravel-pint-1300) [Next articleLaravel Queues at Scale: Backpressure, Dead-Letter Patterns, and Job Observability](https://msaied.com/public/articles/laravel-queues-at-scale-backpressure-dead-letter-patterns-and-job-observability)  

   On this page
-------------

1. [Why Enums Deserve More Than a CastsAttributes Afterthought](#why-enums-deserve-more-than-a-codecastsattributescode-afterthought)
2. [1. Backed Enums as Eloquent Casts](#1-backed-enums-as-eloquent-casts)
3. [2. Enum-Aware Validation Rules](#2-enum-aware-validation-rules)
4. [3. Route Model Binding with Enums](#3-route-model-binding-with-enums)
5. [4. PHP 8.3 Typed Class Constants](#4-php-83-typed-class-constants)
6. [5. Serialising Enums in API Resources](#5-serialising-enums-in-api-resources)
7. [Takeaways](#takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/745/8744e1be5136b430da52e9fca3ed3964.png)  · 3 min read### Service Container Deep Dive: Contextual Binding, Tagging, and Method Injection

6 Oct 2026 ](https://msaied.com/public/articles/service-container-deep-dive-contextual-binding-tagging-and-method-injection-1) [ ![](https://cdn.msaied.com/743/8998fac3a41451ab3fe1588194e17a43.png) Filament · 3 min read### Securing Filament Plugins with Plumb: Automated Security Scoring for PHP Packages

5 Oct 2026 ](https://msaied.com/public/articles/securing-filament-plugins-with-plumb-automated-security-scoring-for-php-packages) [ ![](https://cdn.msaied.com/742/2d02018669cdeedccb5de2efb898f0ee.png) Filament · 3 min read### Filament v3.3.56 Released: File Hash Names and Livewire Upload Fix

5 Oct 2026 ](https://msaied.com/public/articles/filament-v3356-released-file-hash-names-and-livewire-upload-fix) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://msaied.com/public)
- [Articles](https://msaied.com/public/articles)
- [Certificates](https://msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://msaied.com/public/sitemap.xml)
