# 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.

- Published: 2020-12-06
- Category: Languages
- Tags: PHP
- Reading time: 3 min read
- Source: https://muhammetsafak.com/blog/coding-with-php-8-0-attributes-and-nullsafe-operator/
- Language: en-US
- Author: Muhammet Şafak

---
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](/blog/preparing-for-php-8-union-types-match-named-arguments/) 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:

```php
#[Attribute]
class Route
{
    public function __construct(
        public string $path,
        public string $method = 'GET',
    ) {}
}
```

Then you apply that attribute to a class or method:

```php
class UserController
{
    #[Route('/users', method: 'GET')]
    public function index(): JsonResponse
    {
        // ...
    }

    #[Route('/users', method: 'POST')]
    public function store(Request $request): JsonResponse
    {
        // ...
    }
}
```

Reading them via reflection:

```php
$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 a missing class or bad argument raises an error when the attribute is instantiated with `newInstance()`.

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
// 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:

```php
$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
// 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 — the natural continuation of [typed properties](/blog/php-7-4-typed-properties-and-arrow-functions/):

```php
// 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:

```php
$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.
