Upgrading to Eloquent accessors & mutators from Laravel 9
Tags: php, laravel, refactoring, rector
Published: 2023-02-26 13:16:54

Laravel 10 is making the headlines these days. However, many apps are stuck in older versions, or at least still use the syntax and methods of older releases. One of the most significant changes introduced in Laravel 9 is the syntax for accessors and mutators.

The old syntax used a simple convention, just wrapping the attribute between 'get' or 'set' and the word 'Attribute', such as `getFullNameAttribute` and `setFullNameAttribute`, that did the trick. In the new syntax, you may define an accessor and mutator using a single, non-prefixed method by type-hinting a return type of `Illuminate\Database\Eloquent\Casts\Attribute` ([docs](https://laravel.com/docs/9.x/releases#eloquent-accessors-and-mutators)):

```php
use Illuminate\Database\Eloquent\Casts\Attribute;
 
public function name(): Attribute
{
    return new Attribute(
        get: fn ($value) => strtoupper($value),
        set: fn ($value) => $value,
    );
}
```

While the old syntax is still usable, it might get removed in the future, or might confuse Laravel newcomers, so it's best to update it sooner rather than later. You can modify those methods by hand, or you can use an automated tool, like [Rector](https://getrector.com/). It's an open-source tool to upgrade PHP projects and automate the boring stuff for you.

You can install Rector with `composer require rector/rector --dev`. Apart from the core package, we'll require another [package](https://github.com/driftingly/rector-laravel) that is specifically tailored to Laravel apps. You can install it with `composer require driftingly/rector-laravel:dev-main --dev`, and initiate a config file with `vendor/bin/rector init`.

You only need to apply one Rector rule to migrate to the new syntax automatically. Just write this in your `rector.php` file:

```php
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use RectorLaravel\Rector\ClassMethod\MigrateToSimplifiedAttributeRector;

return static function (RectorConfig $rectorConfig): void {
    $rectorConfig->paths([
        __DIR__ . '/app',
    ]);

    $rectorConfig->rule(MigrateToSimplifiedAttributeRector::class);
};
```

It will take care of migrating all your Eloquent models to use the new syntax. If you found a bug or something went wrong, let me know. This Rector rule was contributed to the library by yours truly 😊.

## There is one gotcha

The old syntax allowed you to use the accessors and mutators however you wanted. So, if you had a `getFullNameAttribute()`, you could use it either like `$user->full_name` or `$user->fullName`. Both would work. The new attributes in Laravel 9 still allow you to use both cases, however, if you append the attributes to your models using camel case, you're a bit out of luck. Suppose you have `protected $appends = ['firstName'];`, you will see this error: `Call to undefined method App\Models\User::getFirstNameAttribute()`.

The problem is that Laravel is trying to determine your attribute using the snake case name, but it can't find it (we're doing `protected function firstName(): Attribute`), so it defaults to searching for attributes in the old syntax and it fails.

To overcome that, you need to double-check your Eloquent `$appends` property or any other manual appends that you apply to your models that use `camelCase` names for your accessors or mutators. Change them to snake case. Also, if you're using your models in the front end, check to see if the front-end logic is still working. Better yet, make these changes before you migrate to the new syntax and check them beforehand. I know, it does not look that fun now, but what were you going to do on a weekend anyway?

The other hacky solution I found was to disable attribute snake casing by using this in your models: `public static $snakeAttributes = false;`. This will make all your errors go away and will properly find the attributes regardless of their casing. However, I think this is also used to [determine the names of model relationships](https://github.com/laravel/framework/blob/a47e7a21fdcdce84b17d39f49ea69523e9aef887/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php#L349), and it might have implications for them as well. I think you shouldn't take this approach, and your safest bet is to just not upgrade right away the mutators/accessors that cause this issue.

Overall, the new syntax for accessors and mutators in Laravel 9 provides a more intuitive and readable way of working with Eloquent models. By upgrading your existing code to the new syntax, you can take advantage of the latest features and enhancements, and ensure that your code is more maintainable and easy to read.