robotsinside/laravel-deletable

Gracefully handle deletion of Eloquent models with related entities.

1.5.0 2022-07-26 21:23 UTC

This package is auto-updated.

Last update: 2024-04-09 08:03:55 UTC


README

Latest Version on Packagist Total Downloads CI License: MIT

This package can be used to gracefully handle the deletion of Eloquent models which are related to other models through HasOne, HasMany, BelongsTo, BelongsToMany or Morph* relationships.

It provides a number of helpful additions:

  1. Validate delete requests with the provided DeletableRequest class
  2. Check for the existence of related models before soft deleting a model instance
  3. Emulate the cascade behaviour provided at the DB layer

Table of contents

Installation

  1. Run composer require robotsinside/laravel-deletable.

  2. Optionally register the service provider in config/app.php

/*
* Package Service Providers...
*/
\RobotsInside\DeletableServiceProvider::class,

Auto-discovery is enabled, so this step can be skipped.

Usage

Use the RobotsInside\Deletable\Deletable trait in your models. You must also define a protected deletableConfig() method which returns the configuration array.

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use RobotsInside\Deletable\Deletable;

class Post extends Model
{
    use Deletable, SoftDeletes;

    protected function deletableConfig(): array
    {
        return [
            'relations' => [
                'authors',
            ]
        ]
    }

    public function authors()
    {
        return $this->belongsToMany(Author::class);
    }
}

Use cases

1. Avoid SQLSTATE[23000]: Integrity constraint violation

A Post implements a HasMany relation with a Like model.

<?php

$post = Post::create(['title' => 'My post']);

$like = new Like;
$like->post()->associate($post);
$like->save()

$post->delete(); // SQLSTATE[23000]: Integrity constraint violation

To avoid this error and provide the user with some more helpful feedback, we can use the DeletableRequest class.

<?php

namespace App\Http\Controllers;

use App\Post;
use RobotsInside\Deletable\Requests\DeletableRequest;

class PostController extends Controller
{
    /**
     * Remove the specified resource from storage.
     *
     * @param DeletableRequest $request
     * @param  Post $post
     * @return \Illuminate\Http\Response
     */
    public function destroy(DeletableRequest $request, Post $post)
    {
        $post->delete();

        return redirect()->route('posts.index');
    }
}

Now we can display the Integrity contraint violation as validation errors instead..

<div>
    @if ($errors->any())
        <div class="alert alert-danger">
            <ul>
                @foreach ($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
        </div>
    @endif
</div>

// Output: This Post has one or more Likes.

2. Check if a model is deletable

This feature supports all relation types. It's particularly helpful when Laravel's soft deletes are in use, since soft-deleting always succeeds without throwing an Integrity Constraint Violation error.

<?php

$post = Post::create(['title' => 'My post']);

$author = Author::create(['name' => 'Billy Bob']);

$post->authors()->save($author);

if($post->deletable()) {
    $post->delete();
}

3. Validate deletes

To validate delete requests, you can type-hint the provided RobotsInside\Deletable\Requests\DeletableRequest class in your controller method.

This class will attempt to automatically resolve the model's route binding, however it currently only supports a single URI route binding.

+-----------+--------------+---------------+----------------------------------------------
| Method    | URI          | Name          | Action
+-----------+--------------+---------------+----------------------------------------------
| DELETE    | posts/{post} | posts.destroy | App\Http\Controllers\PostController@destroy

If your route has more than one binding, such as authors/{author}/posts/{post}, you'll need to create your own form request, which extends DeletableRequest and define a getRouteModel method which returns the models' route binding.

Below is an example for routes with more than one route binding. This is all that is required for validation to kick in.

<?php

namespace App\Http\Requests;

use RobotsInside\Deletable\Requests\DeletableRequest;

class DeletePostRequest extends DeletableRequest
{
    protected function getRouteModel()
    {
        return 'post';
    }
}

As before, type-hint the extended form request in your controller.

<?php

namespace App\Http\Controllers;

use App\Http\Requests\DeletePostRequest;

class PostContoller extends Controller
{
    ...

    /**
    * Remove the specified resource from storage.
    *
    * @param  App\Http\Requests\DeletePostRequest;
    * @param  App\Post $post
    * @return \Illuminate\Http\Response
    */
    public function destroy(DeletePostRequest $request, Post $post)
    {
        $post->delete();

        return back();
    }
}

4. Customising the validation messages

If you don't want to rely on the default validation messages, you can define a deletableValidationMessage method on your model. You are free to add custom messages for each related model that is preventing a delete.

<?php

namespace App\Models\Post;

use App\Models\Author;
use App\Models\Like;
use Illuminate\Database\Eloquent\Model;
use RobotsInside\Deletable\Deletable;

class Post extends Model
{
    use Deletable;

    public function deletableValidationMessage($model)
    {
        switch ($model) {
            case Like::class:
                return 'Posts with likes cannot be deleted.';
                break;
            case Author::class:
                return 'Posts written by authors cannot be deleted.';
                break;
            default:
                return 'This model cannot be deleted.';
                break;
        }
    }

    ...

Supported safeDelete modes (use when soft deleting)

When using the safeDelete method, you have the option of defining a mode to be used when deleting a record. The mode can be set on the model's deletableConfig array.

  • exception (default) (optional)
  • cascade
  • custom

Note that the mode configuration key can be left empty in exception mode, but must be set for cascade and custom modes.

Exception mode (default)

Soft deleting a model in this situation will fail. If the model in question is referenced by another model, an UnsafeDeleteException will be thrown.

<?php

$post = Post::create(['title' => 'My post']);
$author = Author::create(['name' => 'Billy Bob']);
$post->authors()->save($author);

Post::find(1)->safeDelete(); // UnsafeDeleteException

Cascade mode

In this mode related models will also be deleted.

<?php

use App\Post;

$post = Post::create(['title' => 'My post']);
$author = Author::create(['name' => 'Billy Bob']);
$post->authors()->save($author);

Post::find(1)->safeDelete(); // My Post and Billy Bob will be deleted.

Custom mode

  1. Set mode to custom.
  2. Set the handler method
  3. Define the handler method on the model

If soft deleting fails, the handler method is called.

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use RobotsInside\Deletable\Deletable;

class Post extends Model
{
    use Deletable, SoftDeletes;

    protected function deletableConfig()
    {
        return [
            'mode' => 'custom',
            'handler' => 'myHandler',
            'relations' => [
                'authors'
            ]
        ];
    }

    public function authors()
    {
        return $this->belongsToMany(Author::class);
    }

    public function myHandler()
    {
        app('log')->info('Unsafe delete of ' . __CLASS__);
    }
}

Testing

Run the provided tests:

composer test

Security

If you discover any vulnerabilities, please email robertfrancken@gmail.com instead of using the issue tracker.

Coffee Time

Will work for ☕☕☕

Buy Me A Coffee

License

The MIT License (MIT). Please see License File for more information.