LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

PHP: The Comeback Kid

phplaravelweb-developmentbackendlivewireperformance

PHP is the language everyone has an opinion about and most people stopped evaluating a decade ago. The criticisms of 2012-era PHP—inconsistent function signatures, no types, register_globals, magic quotes—were valid. The language earned its reputation.

Then it changed. PHP 7 added scalar type declarations and killed the performance gap with Python. PHP 8.0 added JIT compilation, union types, named arguments, and match expressions. PHP 8.1 introduced enums and fibers. PHP 8.4 shipped property hooks and asymmetric visibility. Laravel went from a scrappy Rails clone to the dominant web framework. The language that developers dismissed quietly powered 79% of the web the whole time.

This is a look at what PHP actually is in 2026, not what it was.


The Reality of PHP’s Market Position

PHP still runs approximately 79% of websites with a known server-side language. That number has remained stable for years, which tells you something: it’s not stagnating and it’s not shrinking. WordPress alone powers 43% of all websites. But the interesting development isn’t WordPress—it’s the serious modern PHP that exists alongside it.

Shopify built its e-commerce platform on PHP before migrating critical paths to Ruby and Go. Laravel powers over 1.5 million websites, from small SaaS applications to large e-commerce platforms. Symfony is the component backbone of multiple major frameworks including Drupal 10 and API Platform. Statamic is eating CMS market share with a modern headless-capable PHP CMS.

The talent pool argument is real: PHP developers are available everywhere, at every experience level, at competitive salaries. For companies that care about hiring speed, this matters.


PHP 8.x: Version by Version

PHP 8.0: The Foundation

Named Arguments make complex function calls self-documenting:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Before: what does true mean here?
User::create('john@example.com', 'John Doe', true, false);

// After: obvious
User::create(
    email: 'john@example.com',
    name: 'John Doe',
    active: true,
    admin: false
);

Named arguments also let you skip optional parameters without needing to know their position—a frequent frustration with PHP’s standard library.

Union Types replace docblock lies with enforced declarations:

1
2
3
4
function processId(int|string $id): User|null
{
    return User::find($id);
}

Match Expressions replace verbose switch statements with a type-safe alternative that doesn’t fall through:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// switch: verbose, fall-through risk, loose comparison
switch ($status) {
    case 'pending':
        return 'Awaiting';
    case 'done':
        return 'Complete';
    default:
        return 'Unknown';
}

// match: concise, strict comparison, exhaustive with default
$label = match($status) {
    'pending' => 'Awaiting',
    'done'    => 'Complete',
    default   => 'Unknown',
};

Nullsafe Operator eliminates null-check chains:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Before: defensive nesting
$street = null;
if ($user !== null) {
    if ($user->address !== null) {
        $street = $user->address->street;
    }
}

// After
$street = $user?->address?->street;

Constructor Property Promotion eliminates the most tedious PHP boilerplate:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Before: declare, accept, assign — three places for every property
class OrderService
{
    private OrderRepository $repository;
    private EventDispatcher $dispatcher;

    public function __construct(
        OrderRepository $repository,
        EventDispatcher $dispatcher,
    ) {
        $this->repository = $repository;
        $this->dispatcher = $dispatcher;
    }
}

// After: one place
class OrderService
{
    public function __construct(
        private readonly OrderRepository $repository,
        private readonly EventDispatcher $dispatcher,
    ) {}
}

PHP 8.1: Enums and Fibers

Enums are the biggest quality-of-life improvement in recent PHP history. PHP developers previously modeled finite states as string or integer constants—completely unchecked:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Before: constants, no type safety
class OrderStatus
{
    const PENDING  = 'pending';
    const APPROVED = 'approved';
    const SHIPPED  = 'shipped';
}

// Anything could be passed as "status"
function ship(Order $order, string $status): void { }
ship($order, 'SHIPPED');     // typo — passes silently
ship($order, 'nonsense');    // passes silently

Backed enums bind a string or integer value to each case and enable serialization:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
enum OrderStatus: string
{
    case PENDING  = 'pending';
    case APPROVED = 'approved';
    case SHIPPED  = 'shipped';
    case REJECTED = 'rejected';

    public function label(): string
    {
        return match($this) {
            self::PENDING  => 'Awaiting Approval',
            self::APPROVED => 'Ready to Ship',
            self::SHIPPED  => 'On the Way',
            self::REJECTED => 'Cancelled',
        };
    }

    public function isFinal(): bool
    {
        return $this === self::SHIPPED || $this === self::REJECTED;
    }

    public function canTransitionTo(self $next): bool
    {
        return match($this) {
            self::PENDING  => in_array($next, [self::APPROVED, self::REJECTED]),
            self::APPROVED => $next === self::SHIPPED,
            default        => false,
        };
    }
}

// Type-safe — only OrderStatus values accepted
function ship(Order $order, OrderStatus $status): void
{
    if (!$order->status->canTransitionTo($status)) {
        throw new InvalidStatusTransition();
    }
}

// Serialization
$status = OrderStatus::from($record['status']);  // throws if invalid
$status->value;  // 'approved'

Enums can implement interfaces, have constants, and contain static factory methods. They’re not just syntactic sugar—they’re a genuine modeling tool.

Readonly Properties make value objects practical:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Money
{
    public function __construct(
        public readonly int $amount,
        public readonly string $currency,
    ) {}
}

$price = new Money(1000, 'USD');
$price->amount;      // 1000
$price->amount = 2;  // Fatal error — readonly

PHP 8.2: Readonly Classes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Every property in a readonly class is automatically readonly
readonly class Coordinate
{
    public function __construct(
        public float $latitude,
        public float $longitude,
    ) {}

    public function distanceTo(self $other): float
    {
        // Haversine formula
        $lat = deg2rad($other->latitude - $this->latitude);
        $lon = deg2rad($other->longitude - $this->longitude);
        // ...
    }
}

Readonly classes are the PHP equivalent of records—immutable value objects without manual enforcement.

PHP 8.3: Typed Class Constants

1
2
3
4
5
6
class Config
{
    const string APP_NAME    = 'MyApp';
    const int    MAX_RETRIES = 3;
    const array  ENVIRONMENTS = ['dev', 'staging', 'prod'];
}

Also added json_validate() for checking JSON validity without decoding—useful for validating webhook payloads before processing.

PHP 8.4: Property Hooks and Asymmetric Visibility

PHP 8.4 (November 2024) brings two features that change how properties work.

Property Hooks let you define get/set logic directly on a property, replacing verbose getter/setter methods:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class User
{
    // Hook with validation
    public string $email {
        set {
            if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
                throw new InvalidArgumentException("Invalid email: $value");
            }
            $this->email = strtolower($value);
        }
    }

    // Computed property (no backing storage)
    public string $displayName {
        get => "{$this->firstName} {$this->lastName}";
    }

    // Both hooks
    private float $_price = 0;
    public float $price {
        get => $this->_price;
        set {
            if ($value < 0) throw new \ValueError("Price must be non-negative");
            $this->_price = round($value, 2);
        }
    }

    public function __construct(
        public string $firstName,
        public string $lastName,
    ) {}
}

$user = new User('Alice', 'Smith');
$user->email = 'ALICE@EXAMPLE.COM';  // stored as alice@example.com
echo $user->displayName;             // "Alice Smith"

Property hooks work with interfaces, inheritance, and static analysis tools—they’re not runtime magic but a first-class language feature.

Asymmetric Visibility allows different access levels for reading and writing:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Order
{
    // Public to read, private to write
    private(set) string $id;
    private(set) OrderStatus $status;

    public function __construct()
    {
        $this->id = uniqid('order_');
        $this->status = OrderStatus::PENDING;
    }

    public function approve(): void
    {
        $this->status = OrderStatus::APPROVED;  // allowed — same class
    }
}

$order = new Order();
echo $order->id;        // Works — public read
$order->id = 'hack';    // Fatal error — private write

Fibers: Async PHP Without Callbacks

PHP 8.1 added Fibers—cooperative coroutines that enable async-style programming without the callback pyramid:

1
2
3
4
5
6
7
8
// A Fiber is a pausable function
$fiber = new Fiber(function(): void {
    $value = Fiber::suspend('first pause');  // Pauses, returns 'first pause' to caller
    echo "Resumed with: $value\n";           // Continues when resumed
});

$result = $fiber->start();          // Runs until suspend — $result = 'first pause'
$fiber->resume('hello');            // Resumes with 'hello'

In practice, you rarely use Fibers directly—frameworks and libraries build on them. The key ecosystem libraries:

RevoltPHP is the low-level event loop built specifically for PHP 8.1+ Fibers:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use Revolt\EventLoop;

EventLoop::queue(function() {
    // These two HTTP requests run concurrently
    $response1 = \Amp\Http\Client\fetch('https://api.example.com/users');
    $response2 = \Amp\Http\Client\fetch('https://api.example.com/orders');

    // Both are awaited, but only one fiber blocks at a time
    echo $response1->getBody()->buffer();
    echo $response2->getBody()->buffer();
});

EventLoop::run();

FrankenPHP worker mode keeps your application in memory between requests—eliminating PHP’s traditional “bootstrap on every request” cost:

1
2
3
4
5
6
# Start with worker mode
frankenphp run --config Caddyfile

# FrankenPHP keeps Laravel/Symfony booted in memory
# ~15,000 req/s vs ~14,600 req/s for PHP-FPM
# More dramatic gains under concurrent load

Swoole (compiled C++ extension) provides the highest performance ceiling: ~24,000 req/s versus ~14,600 for PHP-FPM under load. The trade-off is compilation complexity and careful state management—Swoole processes are long-lived, so anything you put in static variables persists between requests.

For most Laravel applications, PHP-FPM with OPcache is sufficient. Fibers and worker mode are tools for high-concurrency requirements, not defaults.


Laravel 11: Slim by Default

Laravel 11 (March 2024) restructured the default application to reduce complexity for the common case.

The Slim Application Structure

The entire application configuration lives in one file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// bootstrap/app.php — replaces Http/Kernel.php, Console/Kernel.php
return Application::configure(basePath: __DIR__ . '/..')
    ->withRouting(
        web: __DIR__ . '/../routes/web.php',
        api: __DIR__ . '/../routes/api.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->api(prepend: [
            \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
        ]);

        $middleware->alias([
            'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->report(function (InvalidOrderException $e) {
            Log::channel('orders')->error($e->getMessage());
        });
    })
    ->create();

The default config reduced from 11 files to 2. Service providers dropped from 5 to 1. The HTTP and Console kernels were removed.

Built-In Authentication

1
php artisan make:auth

Generates:

  • User model with has_secure_password
  • Session model tracking IP address, user agent, and token
  • Sign-in, sign-out, and password reset controllers
  • No external gem or package required
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class SessionController extends Controller
{
    public function store(Request $request): RedirectResponse
    {
        $request->validate(['email' => 'required|email', 'password' => 'required']);

        if (! Auth::attempt($request->only('email', 'password'), $request->boolean('remember'))) {
            return back()->withErrors(['email' => 'Invalid credentials.']);
        }

        $request->session()->regenerate();
        return redirect()->intended();
    }
}

Per-Second Rate Limiting

1
2
3
4
5
6
7
8
9
// routes/api.php
Route::middleware('throttle:api')->group(function () {
    Route::get('/users', [UserController::class, 'index']);
});

// AppServiceProvider
RateLimiter::for('api', function (Request $request) {
    return Limit::perSecond(10)->by($request->user()?->id ?: $request->ip());
});

Livewire: Full-Stack Components in PHP

Livewire lets you build reactive UI components without writing JavaScript. Components are PHP classes with Blade templates:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
// app/Livewire/UserSearch.php
namespace App\Livewire;

use Livewire\Component;
use App\Models\User;

class UserSearch extends Component
{
    public string $search = '';
    public string $role = 'all';

    public function render()
    {
        $users = User::query()
            ->when($this->search, fn($q) => $q->where('name', 'like', "%{$this->search}%"))
            ->when($this->role !== 'all', fn($q) => $q->where('role', $this->role))
            ->paginate(20);

        return view('livewire.user-search', compact('users'));
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{{-- resources/views/livewire/user-search.blade.php --}}
<div>
    <div class="filters">
        <input wire:model.live="search" type="text" placeholder="Search users...">
        <select wire:model.live="role">
            <option value="all">All Roles</option>
            <option value="admin">Admin</option>
            <option value="viewer">Viewer</option>
        </select>
    </div>

    <table>
        @foreach($users as $user)
            <tr>
                <td>{{ $user->name }}</td>
                <td>{{ $user->email }}</td>
                <td>{{ $user->role }}</td>
            </tr>
        @endforeach
    </table>

    {{ $users->links() }}
</div>

wire:model.live updates on every keystroke. wire:model updates on blur. The table re-renders server-side; Livewire diffs and patches only changed DOM nodes. No JavaScript, no API endpoints, no state management library.

Livewire Volt (single-file components) puts class and template together:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{{-- resources/views/livewire/create-post.blade.php --}}

<form wire:submit="save">
    <input type="text" wire:model="title" placeholder="Title">
    <textarea wire:model="body" placeholder="Content..."></textarea>
    @error('title') <span class="error">{{ $message }}</span> @enderror
    <button type="submit">Publish</button>
</form>

<?php

use Livewire\Volt\Component;
use App\Models\Post;

new class extends Component {
    public string $title = '';
    public string $body = '';

    protected $rules = [
        'title' => 'required|min:3|max:255',
        'body'  => 'required|min:10',
    ];

    public function save(): void
    {
        $this->validate();

        Post::create([
            'title'   => $this->title,
            'body'    => $this->body,
            'user_id' => auth()->id(),
        ]);

        $this->redirect('/posts', navigate: true);
    }
} ?>

The navigate: true flag uses wire:navigate for SPA-style transitions—no full page reload.


Laravel Reverb: WebSockets Without Third Parties

Laravel Reverb is a first-party WebSocket server. Previously, real-time features required Pusher or Ably subscriptions. Reverb runs on your infrastructure:

1
php artisan reverb:start
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Broadcast an event
class OrderShipped implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(public Order $order) {}

    public function broadcastOn(): array
    {
        return [new PrivateChannel('orders.' . $this->order->user_id)];
    }
}

// Trigger from a controller
broadcast(new OrderShipped($order));
1
2
3
4
5
// Frontend receives in real-time
Echo.private(`orders.${userId}`)
    .listen('OrderShipped', (e) => {
        updateOrderStatus(e.order.id, e.order.status);
    });

Performance Reality

OPcache is the single most impactful configuration change—and it should always be enabled:

1
2
3
4
5
6
7
; php.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.revalidate_freq=0    ; 0 = never revalidate in production
opcache.save_comments=1

OPcache caches the compiled bytecode of PHP files, eliminating parsing and compilation on every request.

JIT Compiler benefits depend heavily on workload:

Workload OPcache Only OPcache + JIT Improvement
Web app (typical CRUD) Baseline +10–15% Marginal
CPU-intensive computation Baseline +80–100% Significant
Image processing Baseline +50–70% Meaningful

For typical Laravel CRUD applications, JIT’s benefit is real but not dramatic. Enable it for CPU-heavy work:

1
2
opcache.jit_buffer_size=100M
opcache.jit=tracing    ; or 'function' for simpler workloads

Benchmark numbers for reference (requests/second under load):

  • PHP-FPM + OPcache: ~14,600 req/s
  • FrankenPHP worker mode: ~15,400 req/s
  • Swoole async server: ~24,400 req/s

Swoole’s gains are most pronounced under high concurrency (hundreds of simultaneous requests)—it keeps the application in memory rather than bootstrapping on each request. For most Laravel apps, PHP-FPM + OPcache is perfectly adequate.


Static Analysis: Approaching TypeScript Safety

Modern PHP with PHPStan at level 9 catches a significant class of bugs at analysis time rather than runtime.

PHPStan runs on 11 levels (0–10):

  • Level 0: Basic syntax and undefined variables
  • Level 5: Missing properties, wrong argument types
  • Level 9: Strict type checking, no dynamic access
1
2
3
4
5
6
7
8
# Install
composer require --dev phpstan/phpstan

# Run
vendor/bin/phpstan analyse src --level=9

# With Laravel plugin
composer require --dev nunomaduro/larastan

Psalm adds security-focused taint analysis—tracking untrusted data flows to catch SQL injection, XSS, and command injection:

1
2
composer require --dev vimeo/psalm
vendor/bin/psalm --taint-analysis

The honest comparison with TypeScript: PHP’s type system covers maybe 85% of the same ground. Generics are the main gap—PHP has no List<T> or Map<K,V>. You can document generic types in docblocks and PHPStan understands them, but it’s not a language-level feature.


Testing with Pest

Pest is a testing framework built on PHPUnit with a function-based API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// tests/Feature/OrderTest.php
use App\Models\Order;
use App\Enums\OrderStatus;

describe('Order creation', function () {
    it('creates a pending order with correct defaults', function () {
        $order = Order::factory()->create();

        expect($order->status)->toBe(OrderStatus::PENDING)
            ->and($order->id)->toBeString()
            ->and($order->created_at)->not->toBeNull();
    });

    it('cannot transition from shipped to pending', function () {
        $order = Order::factory()->state(['status' => OrderStatus::SHIPPED])->create();

        expect(fn() => $order->transitionTo(OrderStatus::PENDING))
            ->toThrow(InvalidStatusTransition::class);
    });
});

describe('Order approval', function () {
    it('sends notification email on approval', function () {
        Mail::fake();
        $order = Order::factory()->create();

        $order->approve();

        Mail::assertSent(OrderApprovedMail::class, fn($mail) =>
            $mail->hasTo($order->user->email)
        );
    });
});

Pest’s expect() API reads naturally and produces clear failure messages. Architecture rules are a standout feature:

1
2
3
4
5
6
7
8
// tests/ArchTest.php
test('controllers have no direct model queries')
    ->expect('App\Http\Controllers')
    ->not->toUse('App\Models');

test('all commands are in the Console namespace')
    ->expect('App\Console\Commands')
    ->toExtend('Illuminate\Console\Command');

A Complete Modern Laravel Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?php
// app/Http/Controllers/OrderController.php
declare(strict_types=1);

namespace App\Http\Controllers;

use App\Enums\OrderStatus;
use App\Http\Requests\UpdateOrderRequest;
use App\Models\Order;
use App\Services\OrderService;

readonly class OrderController
{
    public function __construct(
        private OrderService $service,
    ) {}

    public function update(UpdateOrderRequest $request, Order $order): \Illuminate\Http\JsonResponse
    {
        $status = OrderStatus::from($request->validated('status'));

        $updated = $this->service->transition(
            order: $order,
            to: $status,
            notes: $request->validated('notes'),
        );

        return response()->json($updated);
    }
}

// app/Services/OrderService.php
readonly class OrderService
{
    public function __construct(
        private \Illuminate\Contracts\Events\Dispatcher $events,
    ) {}

    public function transition(Order $order, OrderStatus $to, ?string $notes = null): Order
    {
        if (!$order->status->canTransitionTo($to)) {
            throw new \DomainException(
                "Cannot transition from {$order->status->value} to {$to->value}"
            );
        }

        $order->update(['status' => $to, 'notes' => $notes]);

        $this->events->dispatch(new \App\Events\OrderStatusChanged($order));

        return $order->fresh();
    }
}

// app/Http/Requests/UpdateOrderRequest.php
class UpdateOrderRequest extends \Illuminate\Foundation\Http\FormRequest
{
    public function rules(): array
    {
        return [
            'status' => ['required', \Illuminate\Validation\Rule::enum(OrderStatus::class)],
            'notes'  => ['nullable', 'string', 'max:1000'],
        ];
    }
}

This is idiomatic modern PHP: declare(strict_types=1) at the top, constructor property promotion, readonly classes, enums, named arguments, and type declarations throughout. PHPStan level 9 passes on this code.


When to Choose PHP and Laravel

PHP and Laravel excel at:

Rapid product delivery. Laravel’s conventions (Eloquent ORM, Blade templates, queues, mail, notifications, storage) eliminate boilerplate. A team that knows Laravel ships features fast.

The WordPress ecosystem. If you’re building anything adjacent to WordPress—themes, plugins, custom admin interfaces, headless CMS—PHP is the only sensible choice.

CRUD-heavy web applications. Dashboards, admin panels, SaaS billing interfaces, content platforms. Laravel was designed for this.

Large talent pools. PHP developers are abundant globally. If hiring speed matters, this is a real advantage.

Where to look elsewhere:

CPU-intensive processing. Image transformations, video encoding, heavy cryptography. Offload to Rust or Go. Laravel’s queue system makes dispatching to separate workers straightforward.

Sub-millisecond real-time requirements. Swoole helps, but for genuine low-latency systems (trading, gaming, telemetry), use Go or Rust.

Applications requiring generics. PHP’s type system is missing them. If your domain modeling requires Collection<User> as a first-class type, TypeScript, Java, or Kotlin is a better fit.

Teams committed to full static typing. PHPStan at level 9 is genuinely good, but it requires discipline and doesn’t cover every edge. If TypeScript-level safety is non-negotiable, use TypeScript.


Quick Reference: Modern PHP 8.4

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php declare(strict_types=1);

// Constructor property promotion
readonly class User
{
    public function __construct(
        public string $name,
        public string $email,
        public UserRole $role = UserRole::VIEWER,
    ) {}
}

// Backed enum with methods
enum UserRole: string
{
    case ADMIN  = 'admin';
    case VIEWER = 'viewer';

    public function canDelete(): bool => $this === self::ADMIN;
}

// Named arguments
$user = new User(name: 'Alice', email: 'alice@example.com');

// Nullsafe operator
$city = $user?->address?->city ?? 'Unknown';

// Match expression
$access = match($user->role) {
    UserRole::ADMIN  => 'full',
    UserRole::VIEWER => 'read-only',
};

// Property hooks (PHP 8.4)
class Product
{
    private(set) string $sku;

    public float $price {
        set => $value > 0 ? $value : throw new \ValueError('Price must be positive');
    }
}

// Union types
function find(int|string $id): User|null { /* ... */ }

The PHP of 2026 is not the PHP of 2010. The function signature inconsistencies are still there (a legacy of 35 years of accumulated standard library), but the language itself—types, enums, readonly, fibers, property hooks—has become genuinely modern. Laravel is a world-class framework. The talent pool is enormous. The question isn’t whether PHP is a viable choice. It’s whether it’s the right choice for your specific constraints.

For a lot of web applications, it is.

Comments