Back to Blog
·14 min read

PHP 8 Features Every Laravel Developer Should Use Daily

PHPLaravel

PHP 8.x added features that genuinely change how you write Laravel applications. If you are still using class constants for statuses, mutable DTOs, and long switch statements, you are leaving type safety and readability on the table.

Here are the PHP 8 features I use in every new Laravel project.

Enums for domain states

Before PHP 8.1, we used constants or magic strings:

// ❌ No type safety, easy typos
class Order
{
    const STATUS_PENDING = 'pending';
    const STATUS_PAID = 'paid';
    const STATUS_SHIPPED = 'shipped';
}

$order->status = 'pendng'; // typo goes unnoticed until production

Backed enums give you type safety with database persistence:

enum OrderStatus: string
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Shipped = 'shipped';
    case Cancelled = 'cancelled';

    public function label(): string
    {
        return match ($this) {
            self::Pending => 'Pending Payment',
            self::Paid => 'Paid',
            self::Shipped => 'Shipped',
            self::Cancelled => 'Cancelled',
        };
    }

    public function canTransitionTo(self $next): bool
    {
        return match ($this) {
            self::Pending => in_array($next, [self::Paid, self::Cancelled]),
            self::Paid => in_array($next, [self::Shipped, self::Cancelled]),
            self::Shipped, self::Cancelled => false,
        };
    }
}

Cast on your Eloquent model:

class Order extends Model
{
    protected $casts = [
        'status' => OrderStatus::class,
    ];
}

// Usage
$order->status = OrderStatus::Paid;
$order->status->label(); // "Paid"

if ($order->status->canTransitionTo(OrderStatus::Shipped)) {
    $order->status = OrderStatus::Shipped;
    $order->save();
}

Validate enum input in Form Requests:

'status' => ['required', Rule::enum(OrderStatus::class)],

Readonly properties for immutable value objects

PHP 8.1 readonly classes are perfect for DTOs passed between layers:

readonly class CreateOrderData
{
    public function __construct(
        public int $customerId,
        public array $items,
        public OrderStatus $status = OrderStatus::Pending,
    ) {}
}
// In controller
$data = new CreateOrderData(
    customerId: $request->integer('customer_id'),
    items: $request->input('items'),
);

$order = $this->orderService->create($data);

Once constructed, the object cannot be mutated—fewer bugs in service layer code.

Match expressions over switch

match is strict (===), returns values, and throws on no match:

// ❌ Verbose switch with fall-through risks
switch ($status) {
    case 'pending':
        $color = 'yellow';
        break;
    case 'paid':
        $color = 'green';
        break;
    default:
        $color = 'gray';
}

// ✅ Exhaustive match
$color = match ($order->status) {
    OrderStatus::Pending => 'yellow',
    OrderStatus::Paid => 'green',
    OrderStatus::Shipped => 'blue',
    OrderStatus::Cancelled => 'red',
};

PHPStan and Psalm can verify enum matches are exhaustive—if you add a new case, static analysis catches missing branches.

Named arguments for clarity

When a function has many optional parameters, named arguments document intent:

// ❌ What does true, false, 30 mean?
sendNotification($user, 'order_shipped', true, false, 30);

// ✅ Self-documenting
sendNotification(
    user: $user,
    template: 'order_shipped',
    queue: true,
    retry: false,
    delaySeconds: 30,
);

Laravel uses named arguments internally in many places. Adopt them in your service methods—especially for configuration-heavy calls.

Attributes for metadata and routing

PHP 8 attributes replace docblock annotations with first-class syntax:

#[ObservedBy([OrderObserver::class])]
class Order extends Model
{
    //
}

Custom attributes for your own conventions:

#[Attribute(Attribute::TARGET_METHOD)]
class RateLimit
{
    public function __construct(
        public int $maxAttempts = 60,
        public int $decaySeconds = 60,
    ) {}
}
class PaymentController extends Controller
{
    #[RateLimit(maxAttempts: 10, decaySeconds: 60)]
    public function process(Request $request)
    {
        // ...
    }
}

Read attributes in middleware via reflection to apply rate limits dynamically—cleaner than maintaining a separate config map.

Constructor property promotion

Cut boilerplate in DTOs, events, and jobs:

// Before PHP 8
class OrderShipped
{
    public Order $order;
    public string $trackingNumber;

    public function __construct(Order $order, string $trackingNumber)
    {
        $this->order = $order;
        $this->trackingNumber = $trackingNumber;
    }
}

// PHP 8+
class OrderShipped
{
    public function __construct(
        public Order $order,
        public string $trackingNumber,
    ) {}
}

Same pattern works for Jobs, Events, Listeners, and Value Objects.

Null-safe operator

Stop chaining ugly null checks:

// ❌
$city = null;
if ($user !== null && $user->address !== null) {
    $city = $user->address->city;
}

// ✅
$city = $user?->address?->city;

Especially useful in Blade and API Resources:

'company_name' => $this->user?->company?->name,

Union and intersection types

Document methods that accept multiple types:

public function notify(User|Admin $recipient): void
{
    Mail::to($recipient->email)->send(new AlertMail());
}

public function process(Countable&Iterator $collection): void
{
    // Must implement both interfaces
}

Use union types in Form Request helpers and service interfaces for precise contracts.

Practical migration strategy

You do not need to rewrite your entire codebase. Migrate incrementally:

  1. New code only — use enums, readonly, and match for all new features
  2. Status columns first — replace string statuses with backed enums (biggest win)
  3. DTOs next — introduce readonly classes at service boundaries
  4. Static analysis — add PHPStan level 6+ to enforce enum exhaustiveness
composer require --dev larastan/larastan
# phpstan.neon
includes:
    - vendor/larastan/larastan/extension.neon

parameters:
    level: 6
    paths:
        - app

PHP 8 features are not optional extras for Laravel developers—they are the foundation of type-safe, maintainable PHP in 2025. Start with enums on your most-used status columns and feel the difference immediately.