DEV Community

Jack Miras
Jack Miras

Posted on • Updated on

Laravel: Delete Actions Simplified

In my last publishing, I've mentioned controller operations that are frequently duplicated and how we could leverage what we've learned so far to make those operations simpler to use. Now, I would like to discuss another controller operation that frequently gets duplicated as well, and that is the delete operation.

Content

Conventional way

Before we jump into the simplification, let’s take a look at how a delete 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 destroy(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->delete() === false) {
            return response(
                "Couldn't delete the user with id {$request->id}",
                Response::HTTP_BAD_REQUEST
            );
        }

        return response(["id" => $request->id, "deleted" => true], Response::HTTP_OK);
    }
}
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->delete() this function returns true or false to let us know if the data got successfully deleted. In the event it returns false, an error message with status 400 will be returned.

Finally, if the user got correctly deleted, we render a response with the user's id and a property deleted with the value true.

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 destroy 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 destroy(Request $request): Response
    {
        $user = User::findOrFail($request->id);

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

        return response(["id" => $request->id, "deleted" => true], Response::HTTP_OK);
    }
}
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 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 just about the same.

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

Same as before, if the user was deleted, we render its id and the property deleted with the value true.

The deleteOrFail 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 delete action on Laravel, let’s see how we can implement this using the deleteOrFail() function.

<?php

namespace App\Http\Controllers;

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

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

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

        return response(["id" => $request->id, "deleted" => true], Response::HTTP_OK);
    }
}
Enter fullscreen mode Exit fullscreen mode

As before, we are querying a user by its ID using the findOrFail() function.

Next, we have a second conditional where we call $user->deleteOrFail() this function returns true or false to let us know if the data got successfully deleted. In the event it returns false, an error message with status 400 will be returned.

Once more, if the user got correctly deleted, we render its id and deleted as with the value true.

NOTE: Notice that the deleteOrFail() check if the model itself exists, open a database connection, then a transaction and call the delete() 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 deleteOrFail() 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 a destroy 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 a good portion of my controllers end up having a destroy action. In my context, this is something highly reused.

Trait abstraction

<?php

namespace App\Helpers;

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

trait DeleteOrThrow
{
    /**
     * Instantiate a new model instance from the model implementing this trait.
     *
     * @return Model
     */
    private static function model(): Model
    {
        return new (get_class());
    }

    /**
     * Find a model by id, remove the model into the database,
     * otherwise it throws an exception.
     *
     * @param  int  $id
     * @return Model
     *
     * @throws \App\Exceptions\ModelDeletionException
     */
    public static function deleteOrThrow(int $id): Model
    {
        $model = self::model()->findOrFail($id);

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

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

Our trait is composed of two functions: model() which is responsible for returning an instance of the model implementing the trait, and deleteOrThrow() which is responsible for deleting the model or throwing an exception in case delete fails.

Here we are simply implementing the behavior that I belive that would be the expected behavior for the new native deleteOrFail() function, in fact, I used to call the deleteOrThrow() function deleteOrFail() but I had to rename it to not conflict with the deleteOrFail() 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 deleteOrThrow function
/**
 * Find a model by id, remove the model into the database,
 * otherwise it throws an exception.
 *
 * @param  int  $id
 * @return Model
 *
 * @throws \App\Exceptions\ModelDeletionException
 */
public static function deleteOrThrow(int $id): Model
{
    $model = self::model()->findOrFail($id);

    if ($model->delete() === false) {
        throw new ModelDeletionException($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.

Subsequently, we have a conditional where we are calling $user->delete(), in case the function returns false, a custom exception gets thrown.

Finally, after a successful delete, we return the deleted 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 ModelDeletionException extends ApplicationException
{
    private int $id;
    private string $model;

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

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

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

    public function error(): string
    {
        return trans('exception.model_not_deleted.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 deleting parameter 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 deleted.

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

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 DeleteOrThrow Trait in the models that we want to have this simplified delete behavior.

To achieve that, we just have to put in our models the use DeleteOrThrow; 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\DeleteOrThrow;
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 DeleteOrThrow;

    ...
}
Enter fullscreen mode Exit fullscreen mode

Implementing it

As a final result, we end up with an API call that looks like User::deleteOrThrow($id) leaving us with a destroy action in our controllers that has a single line of implementation, and it 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 destroy(Request $request): Response
    {
        return response(User::deleteOrThrow($request->id));
    }
}
Enter fullscreen mode Exit fullscreen mode

Happy coding!

Latest comments (0)