Coding with PHP 8.0: Attributes and the Nullsafe Operator
PHP 8.0 is out. I share how I've been using attributes and the nullsafe operator in real projects, and what they've actually delivered.
PHP 8.0 shipped last month. In my November post I took prep notes on union types, match, and named arguments. This month I’m running PHP 8.0 in a real project, and two features have found their way into my daily code more than I expected: attributes and the nullsafe operator.
Attributes
For years we’ve used PHPDoc comments to attach metadata in PHP — @Route, @Column, @ORM\Entity and the like. These comments are invisible to the compiler; they’re parsed at runtime via reflection. The syntax was never standardised, IDE support has been inconsistent, and errors surface late.
PHP 8.0’s Attribute feature brings this up to a language-level construct.
First you define an attribute class:
#[Attribute]
class Route
{
public function __construct(
public string $path,
public string $method = 'GET',
) {}
}
Then you apply that attribute to a class or method:
class UserController
{
#[Route('/users', method: 'GET')]
public function index(): JsonResponse
{
// ...
}
#[Route('/users', method: 'POST')]
public function store(Request $request): JsonResponse
{
// ...
}
}
Reading them via reflection:
$reflection = new ReflectionMethod(UserController::class, 'index');
$attributes = $reflection->getAttributes(Route::class);
foreach ($attributes as $attr) {
$route = $attr->newInstance();
echo $route->path; // /users
echo $route->method; // GET
}
Framework layers can use this mechanism to read routes, validation rules, and serialisation behaviour straight from class definitions. It’s the same job PHPDoc has always done — but now with language-level guarantees.
The most concrete advantage over PHPDoc is type safety. When you write @Route('/users'), PHP sees it as a comment and won’t catch a typo. When you write #[Route('/users')], Route is a real PHP class — the IDE offers autocompletion and the compiler throws an error on a bad parameter.
Laravel hasn’t adopted this as its primary API yet, but the ecosystem is moving in that direction. I’ve started using attributes in my own small framework components and CLI tools.
The Nullsafe Operator (?->)
This one is simple, but it cleans up code noticeably. Null-checking chained object access used to look like this:
// PHP 7 — cascading null checks
$country = null;
if ($user !== null) {
$address = $user->getAddress();
if ($address !== null) {
$country = $address->getCountry();
}
}
// or shorter but still messy
$country = $user ? ($user->getAddress() ? $user->getAddress()->getCountry() : null) : null;
With PHP 8.0:
$country = $user?->getAddress()?->getCountry();
If any link in the chain returns null, the entire expression evaluates to null. No exception is thrown — it short-circuits silently.
A real-world scenario: fetching the expiry date of a user’s active subscription.
// PHP 7
$expiry = null;
$sub = $user->activeSubscription();
if ($sub) {
$plan = $sub->plan();
if ($plan) {
$expiry = $plan->expiresAt();
}
}
// PHP 8.0
$expiry = $user->activeSubscription()?->plan()?->expiresAt();
Line count drops, intent stays clear.
One caveat: the nullsafe operator swallows null silently. That’s the right behaviour sometimes — and the wrong behaviour other times. If $user->getAddress() should never return null in a given context, using ?-> there hides a bug rather than exposing it. Reserve nullsafe for chain steps that are genuinely optional; covering everything with ?-> lets nulls propagate silently throughout the codebase.
Named Arguments and Constructor Promotion Together
Constructor property promotion is another PHP 8.0 feature I’ve been using heavily for the past two weeks:
// PHP 7 — repetitive boilerplate
class CreatePostRequest
{
public string $title;
public string $body;
public ?int $categoryId;
public function __construct(string $title, string $body, ?int $categoryId = null)
{
$this->title = $title;
$this->body = $body;
$this->categoryId = $categoryId;
}
}
// PHP 8.0 — constructor promotion
class CreatePostRequest
{
public function __construct(
public string $title,
public string $body,
public ?int $categoryId = null,
) {}
}
Combined with named arguments:
$request = new CreatePostRequest(
title: 'Coding with PHP 8.0',
body: $content,
categoryId: 3,
);
With long parameter lists this directly improves readability.
Constructor promotion shines especially in DTOs (Data Transfer Objects) and value objects. A constructor that previously took five lines collapses to one. The only downside: if the constructor also needs to contain logic, things get muddled. For pure data-carrying classes it’s a perfect fit.
A Note on JIT
The big technical headline for PHP 8.0 is the JIT compiler. The performance gains in web applications are measurable but not dramatic; JIT’s real wins come in CPU-intensive workloads. In a typical Laravel API the bottleneck is database queries, and JIT doesn’t touch those. It’s worth keeping expectations in check.
After a month with PHP 8.0, my take is this: attributes and the nullsafe operator deliver value immediately. Constructor promotion genuinely reduces boilerplate. match, union types, and named arguments require more deliberate application — powerful in the right place, unnecessary complexity in the wrong one. I’m not rushing to put this version into production, but for new projects I plan to start on PHP 8.0 from day one.
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.