Laravel 10 – Updating my projects

Image showing two notifications on a phone saying "You have new software updates"

I have numerous projects which are built utilising the Laravel Framework. Each time there is a new major release, you need to ask yourself the same question; how long until I update my project? There are two follow-on questions; in what order should I upgrade? And is it worth upgrading this one?

I try to ensure all my projects get updated to the latest version of Laravel. However, I am not one of those eager users who updates their Apps and website on release day. When it comes to updates, I am conservative with my own projects. It is typically worth waiting for at least some minor releases which catch the inevitable bugs.

Most of the time, the order that I update is simple. The Apps which are in active development and any which could or will benefit from any of the new features go first.

Laravel 10 is different

Laravel 10 is a different release. The major benefit is not the new features, and it isn’t immediately obvious. The major benefit will come with time, all the user land code is now strictly typed.

Unlike previous Laravel releases, for Laravel 10 you should not simply update your composer file and run update. To benefit from the changes in Laravel 10, you need to update all your user land code to be strictly typed. If you simply run composer update you are not benefiting from the new typing. You need to ensure your user land code is strict.

There are services and tools that can help you with upgrading your projects, the obvious one being Laravel Shift. Though if you have several projects, this can get expensive quickly. In addition to the financial costs and time cost, you need to consider whether a project will have any future development. Is the project a personal passion project that is likely to just fizzle out?

Updates have begun

To date, two of my projects are using Laravel 10. I upgraded one with Shift and a little TLC, the other I upgraded manually. At release time, Budget Pro was still in the preliminary stages of development. It had minimal user land code which I needed to type.

With my other umpteen active projects, their time will come. The two most urgent are my API and a large client App for a customer. The updates for these projects are going to be complicated tasks that I will manage manually. I don’t want to hand over the job to Shift because as much as I like Laravel Shift, these projects are too critical. I expect I will start the update when I want to use a new feature introduced in Laravel 10 or a later point release.

Being a solo developer, it can be hard to justify the time. Any time that I dedicate to updating and validating is time I could be using to refactor, add new features, marketing, or anything else.

Action class usage in my Laravel Apps

Action class usage in my Laravel Apps

What is an action class?

I define an action class as exactly that – a class that performs a single action. Examples include addUser, deleteUser, assignRole etc.

In my applications I typically make these classes invokable. However, there will be the odd case where I choose to add a method to call, it depends on usage and how the code reads.

Action class structure

The structure of my classes very much depends on the structure of the App itself. Is the App a standard app with its own database or does it pass everything off to a REST API and effectively acts as the front-end?

Action class usage in a typical App

In a typical App my action classes return a Boolean. I keep things simple, the action either succeeds or doesn’t. Error handling is either handled by the controller or internally within the action, it depends on how exceptions are being handled and whether actions need to be retried, this is typically a business decision.

Below is a simple example which sets the salesperson for an order.

class AssignSalesPerson
{
    public function __invoke(
        int $customer_id,
        string $order_reference,
        array $input
    ): bool {
        Validator::make($input, [
            'sales_person_code' => [
                'required',
                Rule::exists(User::class, 'sales_person_code')
            ]
        ])->validate();

        $order = (new Order())->getOrder($customer_id, $reference);
        if ($order === null) {
            return false;
        }

        try {
            DB::transaction(function () use ($order, $input) {
                // Update the order
                $order->sales_person_code = $input['sales_person_code'];
                $order->save();
                
                // Log the order change
                // ...   
            });

            return true;
            
        } catch (\Exception $e) {
            // Log the exception the error
            //...
            return false;
        }
    }
}

Action class usage with a REST API

If the App is a frontend for a REST API, the structure of my action classes changes. Instead of returning a Boolean, they return an integer, specifically the http status code returned from the API request. Additionally, these action classes will extend a base class which will contain several helper methods and properties.

Below is an example of the parent for API action classes.

abstract class Action
{
    // The message to return to the user, this is usually the message
    // from the the API when the request didn't succeed
    protected string $message;

    // Store any validation errors from the API
    // Our App can then pass these on as necessary
    protected array $validation_errors = [];

    // Store any parameters that need to be passed to the view
    // For example the id for a new resource 
    protected array $parameters = [];

    public function getMessage(): string
    {
        return $this->message;
    }

    public function getParameters(): array
    {
        return $this->parameters;
    }

    public function getValidationErrors(): array
    {
        return $this->validation_errors;
    }
}

And this is an example of a class which creates a budget item.

class CreateBudgetItem extends Action
{
    public function __invoke(
        Service $api,
        string $resource_type_id,
        string $resource_id,
        array $input
    ): int {
        // Handle any validation which needs to be 
        // done locally before passing the data to the API

        if (count($this->validation_errors) > 0) {
            return 422;
        }

        // Create the payload to send to the API        
        $payload = [
            ...
        ];

        $create_response = $api->budgetItemCreate(
            $resource_type_id,
            $resource_id,
            $payload
        );

        if ($create_response['status'] === 201) {
            return $create_response['status'];
        }

        if ($create_response['status'] === 422) {
            $this->message = $create_response['content'];
            $this->validation_errors = $create_response['fields'];

            return $create_response['status'];
        }

        // Unexpected failure, store the message
        $this->message = $create_response['content'];
        return $create_response['status'];
    }
}

The names of my classes depend on the complexity of the App. The class names shown above are simply examples, typically I try to organise my code by namespace and keep the names simple. If you see \App\Actions\BudgetItem\Create you know exactly what the action class is doing, there is no way to misinterpret the desired action.

Benefits

There are several benefits to using action classes – I’m going to talk about two for now, in the future I might do a full deep dive into all the positives and negatives.

Encapsulation

The class encapsulates all the code for the function, if you are strict in how you use actions this is a massive plus, there is a single class that performs the action, and it is easily testable.

Reusability

Action classes are reusable, it is easy to call an action class in a console command or job, I’ve several actions that need to be called on command as well as based on a user action. If I didn’t use action classes I would have had to create something similar or worse, duplicate the relevant code.

Continue to evolve

My action classes have evolved over time and I’m sure I will make more tweaks in the future. Much of my client work is creating B2B Apps and typically they require event logging, transactions, retrying. My real-life action classes are much more complex than the examples detailed above.

As much as I love action classes, they are not always the best solution. I try to ensure my action classes stay small. In cases where that is not possible and you still want to use the same idea, invokable controllers can help.

This post is part of a series about my development process. Please check out my typical project setup and my Git workflow.