Modern PHP in 2026: How to Write Code You Won’t Hate in Two Years

Write modern PHP by turning on strict types in every file, typing properties and parameters, using enums instead of string constants, and keeping classes final and readonly where you can. Those small rules stop silent coercion bugs and make the code readable two years from now.
Part of the complete web development guide.
On this page
- Start with strict types. Every file.
- Type everything you can
- Enums replaced your string constants
- Common mistakes I still see in 2026
- A note on PHP version
- Testing is not optional
- Where this fits into a bigger build
- FAQ
- Should I upgrade to PHP 8.4 if my app works fine on 7.4?
- Do I need strict types everywhere, or just new files?
- Is Pest worth learning over PHPUnit?
Most PHP codebases don’t rot because the language is bad. They rot because nobody sets rules early and the framework gets blamed for it later. Here’s what I actually reach for in 2026, and the small decisions that make the difference between a codebase you maintain and one you rewrite.
Start with strict types. Every file.
This is the single cheapest upgrade you can make. Add declare(strict_types=1); to the top of every file and PHP stops silently coercing "5" into 5 for you. That silent coercion is where a huge share of weird production bugs come from.
<?php
declare(strict_types=1);
function calculateTotal(int $cents, float $taxRate): int
{
return (int) round($cents * (1 + $taxRate));
}
Without strict types, passing "100" works and hides a type error upstream. With it, you get a TypeError at the call site, which is exactly where you want to be standing when something breaks.
Type everything you can
PHP has had typed properties since 7.4, union types since 8.0, and readonly properties since 8.1. Use them. A class that declares its types is documentation that can’t lie to you.
final class Invoice
{
public function __construct(
public readonly int $id,
public readonly string $customerEmail,
public readonly int $amountCents,
public readonly DateTimeImmutable $issuedAt,
) {}
}
That constructor promotion plus readonly combo kills a lot of boilerplate. No getters, no setters, no accidental mutation three files away.
Enums replaced your string constants
If you’re still doing const STATUS_PAID = 'paid';, stop. Enums give you type safety, IDE autocomplete, and a method to hang behavior on.
enum OrderStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
case Refunded = 'refunded';
public function isFinal(): bool
{
return match($this) {
self::Refunded => true,
default => false,
};
}
}
Backed enums serialize cleanly to the database and JSON, and match forces you to handle cases instead of falling through like switch does.
Common mistakes I still see in 2026
- Ignoring the return type of
json_decode. It returnsmixed. Cast it, or use a DTO with a fromArray constructor. - Doing DB queries inside a loop. The N+1 problem never went away. Eager load or batch.
- Trusting
$_POSTdirectly. Validate and cast at the boundary, then work with typed objects inside. - Catching
Exceptionand swallowing it. Catch what you can actually handle. Let the rest bubble to your error handler. - Skipping
composer.lockin version control. It belongs in the repo. Always.
A note on PHP version
PHP 8.4 is the current stable line as of early 2026, with 8.5 in active development. If you’re still on 7.4 or 8.0, you’re running an unsupported version with known security fixes you’re not getting. The upgrade path from 8.1 onward is usually a day of work, not a month. Do it.
Testing is not optional
You don’t need 100% coverage. You need the tests that catch the things you’d otherwise catch in production. Pest has become my default over PHPUnit for new projects because the syntax reads closer to plain English.
it('marks a refunded order as final', function () {
$status = OrderStatus::Refunded;
expect($status->isFinal())->toBeTrue();
});
Run it in CI, run it before you deploy, run it when you’re scared. That’s the whole point.
Where this fits into a bigger build
All of this is table stakes for a modern PHP app, whether it’s a Laravel SaaS, a WordPress plugin, or a custom internal tool. If you’re working with someone on custom web development, these are the questions worth asking before the first line of code gets written: what PHP version, what testing story, what happens when a type fails. The answers tell you more about the project than any pitch deck.
I’ve been shipping PHP on Long Island for a long time, and the code that survives isn’t the cleverest. It’s the code where every function says what it takes and what it gives back, and nothing has to guess.
FAQ
Should I upgrade to PHP 8.4 if my app works fine on 7.4?
Yes. 7.4 stopped getting security fixes years ago. “Works fine” just means you haven’t hit the bug yet. The upgrade is usually smaller than people fear, especially if you have tests.
Do I need strict types everywhere, or just new files?
Everywhere, eventually. Start with new files so you don’t break old ones, then add it as you touch legacy code. A file without strict types is a file you can’t fully trust.
Is Pest worth learning over PHPUnit?
If you’re starting fresh, yes. It’s built on PHPUnit, so you lose nothing, and the syntax is easier to read and write. If your team already has a large PHPUnit suite, migrating isn’t urgent.


