DEV Community

Jack Miras
Jack Miras

Posted on • Edited on

Laravel: Update Actions Simplified

When building APIs, we often come across CRUD operations. Even though these operations are one of the first things we learn when we start working with the backend, some of them can have significantly less code.

Besides that, this code frequently gets duplicated across controllers. Through my journey working with Laravel, I've noticed that this happens typically in update functions, and that's why I decided to share how to simplify this implementation.

Content

Conventional way

Before we jump into the simplification, let’s take a look at how an update operation looks like when conventionally implemented.

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class UsersController extends Controller
{
    public function update(Request $request): Response
    {
        $user = User::find($request->id);

        if ($user === null) {
            return response(
                "User with id {$request->id} not found",
                Response::HTTP_NOT_FOUND
            );
        }

        if ($user->update($request->all()) === false) {
            return response(
                "Couldn't update the user with id {$request->id}",
                Response::HTTP_BAD_REQUEST
            );
        }

        return response($user);
    }
}
Enter fullscreen mode Exit fullscreen mode

On the first line, we are querying a user by its ID and storing the result in the $user object.

Then, at the first conditional, we check if the $user is null; if it is, it means that no record with the given ID got found, and an error message with status 404 will be returned.

Thereafter, we have a second conditional where we call $user->update() with the data that we want to update. This function returns true or false to let us know if the data was successfully updated. In the event it returns false, an error message with status 400 will be returned.

Finally, if the data is successfully updated, we render the updated user as a response.

Shortening it

Why not use the findOrFail() helper function to shorten our code? When using this approach, it would remove at least five lines from the update action of our controller, as shown in the code example down below.

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class UsersController extends Controller
{
    public function update(Request $request): Response
    {
        $user = User::findOrFail($request->id);

        if ($user->update($request->all()) === false) {
            return response(
                "Couldn't update the user with id {$request->id}",
                Response::HTTP_BAD_REQUEST
            );
        }

        return response($user);
    }
}
Enter fullscreen mode Exit fullscreen mode

On the first line, we are querying a user by its ID using the findOrFail() function. This function has a special behavior where an exception gets thrown in case the data for the given ID doesn't get found.

To make the most out of this change, we need to know how to automate the handling of the exception thrown by the findOrFail(). Otherwise, it would be necessary to use a try/catch block, and the number of lines would be mostly the same.

As in the previous example, we have a conditional where we are calling $user->update() with the data that we want to update. In the event it returns false, an error message with status 400 will be sent.

Finally, if the user was correctly updated, we render the updated user as a response.

The updateOrFail function

NOTE: This function will only be available if you are using Laravel on version 8.58.0 or newer.

Now that we’ve seen two different ways of implementing the update action on Laravel, let’s see how we can implement this using the updateOrFail() function.

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Exceptions\ModelUpdatingException;

class Users extends Controller
{
    public function update(Request $request): Response
    {
        $user = User::findOrFail($request->id);

        if ($user->updateOrFail($request->all()) === false) {
            return response(
                "Couldn't update the user with id {$request->id}",
                Response::HTTP_BAD_REQUEST
            );
        }

        return response($user);
    }
}
Enter fullscreen mode Exit fullscreen mode

On the first line, we are querying a user by its ID using the findOrFail() function.

Next, we have a second conditional where we call $user->updateOrFail() with the data that we want to update. This function returns true or false to let us know if the data was successfully updated. In the event it returns false, an error message with status 400 will be returned.

And again, if the user was correctly updated, we render the updated user as a response.

NOTE: Notice that the updateOrFail() function doesn't have any significant difference from the update() function, and the resultant usage is exactly the same as the one presented in the previous section.

This is a very odd implementation in the framework, since most of the *orFail() functions usually throw an exception. If that was the case, we could just automate the handling of the exception thrown by the updateOrFail() function the same way we did in the last post in my series about exceptions.

If we had a standard *orFail() function, this function would be the simplest way of implementing an update action in a controller, completely removing the need for the approach that will be explained in the next section.

Abstracting it

Let’s see how we can abstract this implementation in such a way that we get high reusability with simple usage out of this abstraction.

Since this abstraction interacts with models, I wanted to avoid using inheritance because it would be a coupling too high for an abstraction as simple as this one.

Furthermore, I want to leave the inheritance in the models open for usage, whether by a team member's decision or by some specific use case.

For that reason, I’ve chosen to implement the abstraction as a trait. Differently from C++, where we can use multiple inheritance, in PHP, a trait is the mechanism to reduce limitations around single inheritance.

Besides that, I have a personal rule where I use traits only when an implementation gets highly reused. Since most of my controllers end up having an update action, in my context, this is something highly reused.

Trait abstraction

<?php

namespace App\Helpers;

use Illuminate\Database\Eloquent\Model;
use App\Exceptions\ModelUpdatingException;

trait UpdateOrThrow
{
    /**
     * Instantiate the model implementing this trait by the model's class name.
     *
     * @return Model
     */
    private static function model(): Model
    {
        return new (get_class());
    }

    /**
     * Find a model by id, fill the model with an array of attributes, update
     * the model into the database, otherwise it throws an exception.
     *
     * @param  int  $id
     * @param  array  $attributes
     * @return Model
     *
     * @throws \App\Exceptions\ModelUpdatingException
     */
    public static function updateOrThrow(int $id, array $attributes): Model
    {
        $model = self::model()->findOrFail($id)->fill($attributes);

        if ($model->update() === false) {
            throw new ModelUpdatingException($id, get_class());
        }

        return $model;
    }
}
Enter fullscreen mode Exit fullscreen mode

Our trait will be a compound of two functions: model() which is responsible for returning an instance of the model implementing the trait, and updateOrThrow() which is responsible for updating the model or throwing an exception in case the update fails.

Here we are simply implementing the behavior that I belive that would be the expected behavior for the new native updateOrFail() function, in fact, I used to call the updateOrThrow() function updateOrFail() but I had to rename it to not conflict with the updateOrFail() function implemented in Laravel 8.58.0.

The model function
/**
 * Instantiate the model implementing this trait by the model's class name.
 *
 * @return Model
 */
private static function model(): Model
{
    return new (get_class());
}
Enter fullscreen mode Exit fullscreen mode

As mentioned, this function is responsible for returning an instance of the model implementing the trait, and since PHP allows us to use meta-programming to instantiate classes, let's take advantage of that and instantiate the model by its class name.

In this function, we have a single line with a return statement that instantiates a new object out of the get_class() function. To fully understand how this function works, let's assume that this trait was implemented by the User model. When evaluating the result of the function, we would get the string "App\Models\User".

When evaluated by the interpreter, this line would be the equivalent of return new ("App\Models\User");, but the get_class() gives us the dynamism of getting the right class name for each model implementing the trait.

The updateOrThrow function
/**
 * Find a model by id, fill the model with an array of attributes, update
 * the model into the database, otherwise it throws an exception.
 *
 * @param  int  $id
 * @param  array  $attributes
 * @return Model
 *
 * @throws \App\Exceptions\ModelUpdatingException
 */
public static function updateOrThrow(int $id, array $attributes): Model
{
    $model = self::model()->findOrFail($id)->fill($attributes);

    if ($model->update() === false) {
        throw new ModelUpdatingException($id, get_class());
    }

    return $model;
}
Enter fullscreen mode Exit fullscreen mode

In the first line, the self:: call indicates that we want to interact with the trait itself, and then we are chaining the model() function to it, which means we are calling the function previously defined.

Then we chain into the model() function call the findOrFail() function, passing the ID of the record we would like to retrieve from the database. Once the record gets found and a populated model gets returned, we chain the fill() function call, passing the data that we want to update.

Subsequently, we have a conditional where we call $user->update() this function returns true or false to let us know if the data got successfully updated. In case it returns false, a custom exception gets thrown.

Finally, after a successful update, we returned the updated model.

Custom exception

Here we are using the same technique explained in the Laravel custom exceptions post. If you didn’t read the post yet, take a moment to read it, so you can make sense out of this section.

<?php

namespace App\Exceptions;

use Illuminate\Support\Str;
use Illuminate\Http\Response;

class ModelUpdatingException extends ApplicationException
{
    public function __construct(private int $id, private string $model)
    {
        $this->model = Str::afterLast($model, '\\');
    }

    public function status(): int
    {
        return Response::HTTP_BAD_REQUEST;
    }

    public function help(): string
    {
        return trans('exception.model_not_updated.help');
    }

    public function error(): string
    {
        return trans('exception.model_not_updated.error', [
            'id' => $this->id,
            'model' => $this->model,
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

At the class definition, we are extending the ApplicationException which is an abstract class used to enforce the implementation of the status(), help() and error() functions, and guaranteeing that Laravel will be able to handle this exception automatically.

Following the class definition, we have the constructor, where property promotion is being used to make the code cleaner. As parameters, we have $id, which contains the ID of the record we want to query from the database at our trait, and $model where the full class name of the model can be found.

Inside the constructor, we are extracting the model name out of the full class name; the full name would be something like App\Models\User, and we want just the User part. This is getting done, so we can automate the error message into something that makes sense to the person interacting with our API in case it’s not possible to find the record for a given ID.

Next, we have the implementation of the status() function, where we are returning the 400 HTTP status.

Thereafter, we have the help() function, where we return a translated string that indicates a possible solution to the error. In case you are wondering, the translated string would be evaluated to Check your update parameters and try again.

Finally, we have the error() function, where the error that happened gets specified. As in the previous function, we are using a translated string, but differently from before, here we are using the replace parameters feature from trans().

This approach was chosen to give us a dynamic error message with context. Here, the translated string would be evaluated to something like User with id 1 not updated.

With this structure, if the target model changes, the message emitted by the exception would change as well since the model name gets dynamically defined.

As a secondary example, imagine that now, you are interacting with a Sale model; in this case, the message would automatically change to Sale with id 2 not updated.

Using the abstraction

Now that our abstraction has been defined, and we have guaranteed that the error handling is in place, we need to use our UpdateOrThrow trait in the models that we want to have this simplified update behavior.

To achieve that, we just have to put in our models the use UpdateOrThrow; exactly like the other traits that normally Laravel brings in the models, you can see it with more details in the code example down below.

<?php

namespace App\Models;

use App\Helpers\UpdateOrThrow;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasFactory;
    use Notifiable;
    use HasApiTokens;
    use UpdateOrThrow;

    ...
}
Enter fullscreen mode Exit fullscreen mode

Implementing it

As a final result, we end up with an API call that looks like User::updateOrThrow($id, $params) leaving us with an update action in our controllers that has a single line of implementation, and is highly reusable.

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class Users extends Controller
{

    public function update(Request $request): Response
    {
        return response(User::updateOrThrow($request->id, $request->all()));
    }
}
Enter fullscreen mode Exit fullscreen mode

Happy coding!

Top comments (4)

Collapse
 
usernotnull profile image
John F

Unfortunately, using the traits as such is now deprecated, see
wiki.php.net/rfc/deprecations_php_...

Calling static trait member directly is deprecated. It should only be accessed on a class using the trait.

Collapse
 
jackmiras profile image
Jack Miras

@usernotnull Thanks for letting me know, I'll figure an alternative for the implementation presented at this post and when I find it, I let you know through the comments.

Collapse
 
jackmiras profile image
Jack Miras • Edited

@usernotnull I've figured an alternative to the problem, the Trait has now a model() function that returns the model instance without the usage of the static:: call.

As you may notice, I've added some extra information into the post because I've noticed that some things changed in the framework and a some information was missing in this post.

If you have anything to add from the previous or the current version of the post, please, let me know.

Collapse
 
casperbraske profile image
Luis

Elegant. I'll try this in the future [if Laravel hasn't already implemented something like that].