Cascading deletes for Eloquent models that implement soft deletes

Overview

Cascading soft deletes for the Laravel PHP Framework

Build Status Latest Stable Version Total Downloads License Buy us a tree

Introduction

In scenarios when you delete a parent record - say for example a blog post - you may want to also delete any comments associated with it as a form of self-maintenance of your data.

Normally, you would use your database's foreign key constraints, adding an ON DELETE CASCADE rule to the foreign key constraint in your comments table.

It may be useful to be able to restore a parent record after it was deleted. In those instances, you may reach for Laravel's soft deleting functionality.

In doing so, however, you lose the ability to use the cascading delete functionality that your database would otherwise provide. That is where this package aims to bridge the gap in functionality when using the SoftDeletes trait.

Code Samples

<?php

namespace App;

use App\Comment;
use Dyrynda\Database\Support\CascadeSoftDeletes;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Post extends Model
{
    use SoftDeletes, CascadeSoftDeletes;

    protected $cascadeDeletes = ['comments'];

    protected $dates = ['deleted_at'];

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}

Now you can delete an App\Post record, and any associated App\Comment records will be deleted. If the App\Comment record implements the CascadeSoftDeletes trait as well, it's children will also be deleted and so on.

$post = App\Post::find($postId)
$post->delete(); // Soft delete the post, which will also trigger the delete() method on any comments and their children.

Note: It's important to know that when you cascade your soft deleted child records, there is no way to know which were deleted by the cascading operation, and which were deleted prior to that. This means that when you restore the blog post, the associated comments will not be.

Because this trait hooks into the deleting Eloquent model event, we can prevent the parent record from being deleted as well as any child records, if any exception is triggered. A LogicException will be triggered if the model does not use the Illuminate\Database\Eloquent\SoftDeletes trait, or if any of the defined cascadeDeletes relationships do not exist, or do not return an instance of Illuminate\Database\Eloquent\Relations\Relation.

Additional Note: If you already have existing event listeners in place for a model that is going to cascade soft deletes, you can adjust the priority or firing order of events to have CascadeSoftDeletes fire after your event. To do this you can set the priority of your deleting event listener to be 1.

MODEL::observe( MODELObserver::class, 1 ); The second param is the priority.

MODEL::deleting( MODELObserver::class, 1 );

As of right now this is not documented in the Larvel docs, but just know that the default priority is 0 for all listeners, and that 0 is the lowest priority. Passing a param of greater than 0 to your listener will cause your listener to fire before listeners with default priority of 0

Installation

This trait is installed via Composer. To install, simply add to your composer.json file:

$ composer require dyrynda/laravel-cascade-soft-deletes

Support

If you are having general issues with this package, feel free to contact me on Twitter.

If you believe you have found an issue, please report it using the GitHub issue tracker, or better yet, fork the repository and submit a pull request.

If you're using this package, I'd love to hear your thoughts. Thanks!

Treeware

You're free to use this package, but if it makes it to your production environment you are required to buy the world a tree.

It’s now common knowledge that one of the best tools to tackle the climate crisis and keep our temperatures from rising above 1.5C is to plant trees. If you support this package and contribute to the Treeware forest you’ll be creating employment for local families and restoring wildlife habitats.

You can buy trees here

Read more about Treeware at treeware.earth

Comments
  • Recursive deleting

    Recursive deleting

    Hello. I have this situation:

    One category can have subcategories(they use the same model. The difference is that a subcategory have the ID of it's father, while the father category have the field 'FatherID' as NULL), and both can have posts on them. Something like this:

    Category Father posts Subcategory Subcategory Posts

    When I delete a father category, I need to softdelete it's posts, it's subcategories and their posts. However, I'm not being able to softdelete the subcategories posts. Can you help me with this ?

    Thanks in advance. This package is awesome =).

    opened by mahezer 9
  • Issue in pivot soft delete cascading

    Issue in pivot soft delete cascading

    the package works perfect for one-to-many relationship, but when it comes to many-to-many relationship it's not working here is my code :

        class Product extends Model
        {
            use HasFactory, SoftDeletes,CascadeSoftDeletes;
            protected $table = 'products';
            protected $cascadeDeletes  = ['product_categories_2'];
        
            public function product_categories_2(){
                return $this->belongsToMany(Category::class,'product_filter_categories')->using(ProductFilterCategory::class);
            }
        }
    

    is it a wrong in my code or this package don't support many-to-many relation ?

    opened by AliSuliman01 8
  • (suggestion) Add cascade restore

    (suggestion) Add cascade restore

    <?php
    
    declare(strict_types=1);
    
    namespace App\Traits;
    
    // https://github.com/michaeldyrynda/laravel-cascade-soft-deletes
    
    use App\Exceptions\CascadeSoftDeleteException;
    use Illuminate\Database\Eloquent\Relations\Relation;
    
    trait CascadeSoftDeletes
    {
        /**
         * Boot the trait.
         *
         * Listen for the deleting event of a soft deleting model, and run
         * the delete operation for any configured relationship methods.
         *
         * @throws \LogicException
         */
        protected static function bootCascadeSoftDeletes()
        {
            static::deleting(function ($model) {
                /** @var CascadeSoftDeletes $model */
                $model->validateCascadingSoftDelete();
    
                $model->runCascadingDeletes();
            });
            static::restoring(function ($model) {
                /** @var CascadeSoftDeletes $model */
                $model->validateCascadingRestore();
    
                $model->runCascadingRestores();
            });
        }
    
        /**
         * Validate that the calling model is correctly setup for cascading soft deletes.
         *
         * @throws \App\Exceptions\CascadeSoftDeleteException
         */
        protected function validateCascadingSoftDelete()
        {
            if (! $this->implementsSoftDeletes()) {
                throw CascadeSoftDeleteException::softDeleteNotImplemented(get_called_class());
            }
    
            if ($invalidCascadingDeletesRelationships = $this->hasInvalidCascadingDeletesRelationships()) {
                throw CascadeSoftDeleteException::invalidRelationships($invalidCascadingDeletesRelationships);
            }
        }
    
        /**
         * Validate that the calling model is correctly setup for cascading restores.
         *
         * @throws \App\Exceptions\CascadeSoftDeleteException
         */
        protected function validateCascadingRestore()
        {
            if (! $this->implementsSoftDeletes()) {
                throw CascadeSoftDeleteException::softDeleteNotImplemented(get_called_class());
            }
    
            if ($invalidCascadingRestoresRelationships = $this->hasInvalidCascadingRestoresRelationships()) {
                throw CascadeSoftDeleteException::invalidRelationships($invalidCascadingRestoresRelationships);
            }
        }
    
        /**
         * Run the cascading soft delete for this model.
         *
         * @return void
         */
        protected function runCascadingDeletes()
        {
            foreach ($this->getActiveCascadingDeletes() as $relationship) {
                $this->cascadeSoftDeletes($relationship);
            }
        }
    
        /**
         * Run the cascading restore for this model.
         *
         * @return void
         */
        protected function runCascadingRestores()
        {
            foreach ($this->getActiveCascadingRestores() as $relationship) {
                $this->cascadeRestores($relationship);
            }
        }
    
        /**
         * Cascade delete the given relationship on the given mode.
         *
         * @param  string  $relationship
         * @return void
         */
        protected function cascadeSoftDeletes($relationship)
        {
            $delete = $this->forceDeleting ? 'forceDelete' : 'delete';
    
            foreach ($this->{$relationship}()->get() as $model) {
                $model->pivot ? $model->pivot->{$delete}() : $model->{$delete}();
            }
        }
    
        /**
         * Cascade restore the given relationship.
         *
         * @param  string  $relationship
         * @return void
         */
        protected function cascadeRestores($relationship)
        {
            foreach ($this->{$relationship}()->onlyTrashed()->get() as $model) {
                /** @var \Illuminate\Database\Eloquent\SoftDeletes $model */
                $model->pivot ? $model->pivot->restore() : $model->restore();
            }
        }
    
        /**
         * Determine if the current model implements soft deletes.
         *
         * @return bool
         */
        protected function implementsSoftDeletes()
        {
            return method_exists($this, 'runSoftDelete');
        }
    
        /**
         * Determine if the current model has any invalid cascading deletes relationships defined.
         *
         * A relationship is considered invalid when the method does not exist, or the relationship
         * method does not return an instance of Illuminate\Database\Eloquent\Relations\Relation.
         *
         * @return array
         */
        protected function hasInvalidCascadingDeletesRelationships()
        {
            return array_filter($this->getCascadingDeletes(), function ($relationship) {
                return ! method_exists($this, $relationship) || ! $this->{$relationship}() instanceof Relation;
            });
        }
    
        /**
         * Determine if the current model has any invalid cascading restores relationships defined.
         *
         * A relationship is considered invalid when the method does not exist, or the relationship
         * method does not return an instance of Illuminate\Database\Eloquent\Relations\Relation.
         *
         * @return array
         */
        protected function hasInvalidCascadingRestoresRelationships()
        {
            return array_filter($this->getCascadingRestores(), function ($relationship) {
                return ! method_exists($this, $relationship) || ! $this->{$relationship}() instanceof Relation;
            });
        }
    
        /**
         * Fetch the defined cascading soft deletes for this model.
         *
         * @return array
         */
        protected function getCascadingDeletes()
        {
            return isset($this->cascadeDeletes) ? (array) $this->cascadeDeletes : [];
        }
    
        /**
         * Fetch the defined cascading restores for this model.
         *
         * @return array
         */
        protected function getCascadingRestores()
        {
            return isset($this->cascadeRestores) ? (array) $this->cascadeRestores : [];
        }
    
        /**
         * For the cascading deletes defined on the model, return only those that are not null.
         *
         * @return array
         */
        protected function getActiveCascadingDeletes()
        {
            return array_filter($this->getCascadingDeletes(), function ($relationship) {
                return ! is_null($this->{$relationship});
            });
        }
    
        /**
         * For the cascading restores defined on the model, return only those that are not null.
         *
         * @return array
         */
        protected function getActiveCascadingRestores()
        {
            return array_filter($this->getCascadingRestores(), function ($relationship) {
                return ! is_null($this->{$relationship}()->onlyTrashed());
            });
        }
    }
    
    
    opened by musapinar 6
  • Why restrcting usage to soft delete only ?

    Why restrcting usage to soft delete only ?

    Hi! (me again :3)

    here: https://github.com/michaeldyrynda/laravel-cascade-soft-deletes/blob/master/src/CascadeSoftDeletes.php#L22L27

    You restrict usage to models implementing soft delete.

    In my case I hard delete records but I want to use your trait to trigger deleting events on children models

    thanks

    question 
    opened by nicolas-t 5
  • Call to a member function delete() on null

    Call to a member function delete() on null

    I'm receiving the following error when deleting a model using this trait.

    FatalThrowableError in CascadeSoftDeletes.php line 40: Call to a member function delete() on null

    Full stack trace:

    in CascadeSoftDeletes.php line 40
    at Post::Iatstuti\Database\Support\{closure}(object(Post))
    at call_user_func_array(object(Closure), array(object(Post))) in Dispatcher.php line 221
    at Dispatcher->fire('eloquent.deleting: App\Post', array(object(Post)), true) in Dispatcher.php line 164
    at Dispatcher->until('eloquent.deleting: App\Post', object(Post)) in Model.php line 1686
    at Model->fireModelEvent('eloquent.deleting: App\Post') in Model.php line 1122
    at Model->delete() in PostsController.php line 142
    

    The $child variable here in the trait is returning null.

    $delete = $model->forceDeleting ? 'forceDelete' : 'delete';
    
                foreach ($model->getCascadingDeletes() as $relationship) {
                    foreach ($model->{$relationship} as $child) {
                        $child->{$delete}();
                    }
                }
    

    I must be missing something obvious because testing it like so, it appears that $child is not being set when the for loop runs.

    dd($model->getCascadingDeletes()); // returns ['website'] (the model relationship to be deleted)
    
     foreach ($model->getCascadingDeletes() as $relationship) {
                    foreach ($model->{$relationship} as $child) {
                        dd($child); // returns Null
                        dd($model->{$relationship}); // returns the cascading model to be deleted. 
                        $child->{$delete}();
                    }
                }
    

    If anyone can point me in the right direction what I or this package is doing wrong, that would be much appreciated :)

    opened by kyranb 5
  • Add support for forceDelete()

    Add support for forceDelete()

    Hi Michael,

    I have found an issue where package couldn't handle situation, when you want to perform an actual DELETE statement to your model.

    That's because you want to delete record that is associated with records from other tables with foreign keys so you get a SQL error like this:

    SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails (`dbname`.`comments`, CONSTRAINT `comments_review_id_foreign` FOREIGN KEY (`review_id`) REFERENCES `reviews` (`id`)) (SQL: delete from `reviews` where `id` = 1)
    

    This little change handle makes this possible now.

    opened by adriandmitroca 5
  • What if onDelete is restrict instead of cascade?

    What if onDelete is restrict instead of cascade?

    I have model which uses soft delete but the action for onDelete is restrict instead of cascade. I have gonna through your code and you haven't handling this case.

    Can you be able to provide any solution to check the type of onDelete action if the type restrict then do not delete and show error?

    question 
    opened by rohanvakharia 5
  • Issue in composer install laravel-cascade-soft-deletes

    Issue in composer install laravel-cascade-soft-deletes

    Hi

    I am using laravel 7.3 when i run this command 'composer require dyrynda/laravel-cascade-soft-deletes'

    I have get this error : Problem 1 - Root composer.json requires dyrynda/laravel-cascade-soft-deletes ^4.0 -> satisfiable by dyrynda/laravel-cascade-soft-deletes[4.0.0]. - dyrynda/laravel-cascade-soft-deletes 4.0.0 requires illuminate/database ^8.0 -> found illuminate/database[v8.0.0, ..., v8.13.0] but it conflicts with another require.

    I am unable to found root casue can you please check this.

    opened by manoj12345678 4
  • Order of 'deleting' listeners

    Order of 'deleting' listeners

    First your package is great. works perfect. The issue that I am having is that I have my own event listeners for a model for 'deleting' your trait is firing before my deleting listener, I was wondering if you knew of a way to have it so my local model listener fired before your trait deleting listener.

    Just to give you an example. I have a Model called Runs which has many Runners attached to it. I have an event setup for when a Run gets deleted to send a notification to all Runners that the run was deleted. Since your trait is cascading down to runners first, when i go to grab a collection of runners $run->runners()->get(); It returns 0 because they were already soft deleted.

    question 
    opened by mattpramschufer 4
  • It Doesn't  work

    It Doesn't work

    Hi Actually Its not properly working for me. I have the following two models: Menu SubMenu

    -->I also added the code on the model but its doesn't works. `class Menu extends Model { use HasFactory; use SoftDeletes; use CascadeSoftDeletes;

    protected $cascadeDeletes = ['subMenusFromMenu'];
    protected $dates = ['deleted_at'];
    
    
    public function subMenusFromMenu(): HasMany
    {
        return $this->hasMany(SubMenu::class, 'menu_id', 'id');
    }
    

    }`

    I tried to delete Menu , But after deleting menu the related child submenu records doesn't delete.

    opened by AnuragckMeridianSolutions 3
  • Compatability with polymorphic relationships?

    Compatability with polymorphic relationships?

    I installed the package successfully with one->many relationship child. Cant seem to get it going with a polymorphic setup. Is it supported?

    PS. Good work on the Laravel News podcast. It's great to hear a familiar accent in the Laravel world.

    opened by tyler36 3
  • Undefined type 'Dyrynda\Database\Support\CascadeSoftDeletes'

    Undefined type 'Dyrynda\Database\Support\CascadeSoftDeletes'

    Hello, thanks for sharing your knowledge. I installed the code via composer and the system is showing the message: Undefined type 'Dyrynda\Database\Support\CascadeSoftDeletes' . Can you help me?

    opened by bbroger1 4
Releases(4.2.1)
  • 4.2.1(Nov 22, 2022)

    What's Changed

    • Fixes issue with Laravel's new strict mode on models by @totov in https://github.com/michaeldyrynda/laravel-cascade-soft-deletes/pull/56
    • Update README to remove event priority note by @decaylala in https://github.com/michaeldyrynda/laravel-cascade-soft-deletes/pull/57

    New Contributors

    • @totov made their first contribution in https://github.com/michaeldyrynda/laravel-cascade-soft-deletes/pull/56
    • @decaylala made their first contribution in https://github.com/michaeldyrynda/laravel-cascade-soft-deletes/pull/57

    Full Changelog: https://github.com/michaeldyrynda/laravel-cascade-soft-deletes/compare/4.2.0...4.2.1

    Source code(tar.gz)
    Source code(zip)
  • 4.2.0(Jan 26, 2022)

  • 4.1.2(Jan 5, 2022)

    What's Changed

    • Call relationship directly to allow deletes with lazy loading disabled by @lukasleitsch in https://github.com/michaeldyrynda/laravel-cascade-soft-deletes/pull/50

    New Contributors

    • @lukasleitsch made their first contribution in https://github.com/michaeldyrynda/laravel-cascade-soft-deletes/pull/50

    Full Changelog: https://github.com/michaeldyrynda/laravel-cascade-soft-deletes/compare/4.1.0...4.1.2

    Source code(tar.gz)
    Source code(zip)
  • 4.1.0(Dec 1, 2020)

  • 4.0.0(Sep 8, 2020)

  • 3.0.0(Mar 6, 2020)

  • 2.0.0(Sep 6, 2019)

  • 1.5.0(Nov 1, 2018)

  • 1.4.0(Feb 14, 2018)

  • 1.3.0(Aug 31, 2017)

  • 1.2.1(Jul 4, 2017)

  • 1.2.0(Jan 28, 2017)

  • 1.1.0(Aug 24, 2016)

  • 1.0.5(Aug 7, 2016)

  • 1.0.4(Jul 22, 2016)

  • 1.0.3(Jun 9, 2016)

    Per #7, when using forceDelete in combination with foreign key constraints, the forceDelete would fail ont he parent record due to child records still existing as the cascade would only ever use delete, updating the child records' deleted_at column leaving them in place.

    Source code(tar.gz)
    Source code(zip)
  • 1.0.2(May 27, 2016)

    If you happen to want all of your models to implement SoftDeletes, you can now also add the CascadeSoftDeletes to your base model and the functionality will be correctly inherited through your child models.

    Source code(tar.gz)
    Source code(zip)
  • 1.0.1(May 4, 2016)

    Code changes

    • Update docblocks
    • Check that the cascade relationship method exists and that it returns the appropriate type
      • A LogicException is now thrown if the method does not exist, as well as if it does exist and does not implement Illuminate\Database\Eloquent\Relations\Relation
    • Move iterating over the cascadeDeletes property to a getter method, to ensure the trait functions even without the property defined
    • Switch collect to array_filter (minor speed improvement)

    Tests

    • Add expected message regular expressions for expected exceptions
    • Ensure we can accept a string in the cascadeDeletes property
    • Ensure we handle relationship methods that don’t exist without triggering a fatal error
    • Extract attaching of comments to a private method
    Source code(tar.gz)
    Source code(zip)
  • 1.0.0(May 3, 2016)

Owner
Michael Dyrynda
Michael Dyrynda
An index of Laravel's cascading comments

Cascading Comments An index of Laravel's cascading comments. Visit the site at cascading-comments.sjorso.com. About This is a cascading comment: casca

Sjors Ottjes 4 Apr 28, 2022
e-soft.uz source code

About Laravel Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experie

Elbek Khamdullaev 4 Dec 6, 2022
A package to implement repository pattern for laravel models

Laravel Model UUID A simple package to use Repository Pattern approach for laravel models . Repository pattern Repositories are classes or components

null 26 Dec 21, 2022
An Eloquent Way To Filter Laravel Models And Their Relationships

Eloquent Filter An Eloquent way to filter Eloquent Models and their relationships Introduction Lets say we want to return a list of users filtered by

Eric Tucker 1.5k Jan 7, 2023
Easy creation of slugs for your Eloquent models in Laravel

Eloquent-Sluggable Easy creation of slugs for your Eloquent models in Laravel. NOTE: These instructions are for the latest version of Laravel. If you

Colin Viebrock 3.6k Dec 30, 2022
Sortable behaviour for Eloquent models

Sortable behaviour for Eloquent models This package provides a trait that adds sortable behaviour to an Eloquent model. The value of the order column

Spatie 1.2k Dec 22, 2022
This package gives Eloquent models the ability to manage their friendships.

Laravel 5 Friendships This package gives Eloquent models the ability to manage their friendships. You can easily design a Facebook like Friend System.

Alex Kyriakidis 690 Nov 27, 2022
Automatically validating Eloquent models for Laravel

Validating, a validation trait for Laravel Validating is a trait for Laravel Eloquent models which ensures that models meet their validation criteria

Dwight Watson 955 Dec 25, 2022
Laravel Ban simplify blocking and banning Eloquent models.

Laravel Ban Introduction Laravel Ban simplify management of Eloquent model's ban. Make any model bannable in a minutes! Use case is not limited to Use

cybercog 879 Dec 30, 2022
cybercog 996 Dec 28, 2022
Create presenters for Eloquent Models

Laravel Presentable This package allows the information to be presented in a different way by means of methods that can be defined in the model's pres

The Hive Team 67 Dec 7, 2022
A small package for adding UUIDs to Eloquent models.

A small package for adding UUIDs to Eloquent models. Installation You can install the package via composer: composer require ryangjchandler/laravel-uu

Ryan Chandler 40 Jun 5, 2022
Use auto generated UUID slugs to identify and retrieve your Eloquent models.

Laravel Eloquent UUID slug Summary About Features Requirements Installation Examples Compatibility table Alternatives Tests About By default, when get

Khalyomede 25 Dec 14, 2022
Record created by, updated by and deleted by on Eloquent models automatically.

quarks/laravel-auditors Record created by, updated by and deleted by (if SoftDeletes added) on Eloquent models automatically. Installation composer re

Quarks 3 Jun 13, 2022
Observe (and react to) attribute changes made on Eloquent models.

Laravel Attribute Observer Requirements PHP: 7.4+ Laravel: 7+ Installation You can install the package via composer: composer require alexstewartja/la

Alex Stewart 55 Jan 4, 2023
Tag support for Laravel Eloquent models - Taggable Trait

Laravel Taggable Trait This package is not meant to handle javascript or html in any way. This package handles database storage and read/writes only.

Rob 859 Dec 11, 2022
Preferences for Laravel Eloquent models

Preferences for Laravel Eloquent models Use this library to bind multiple key/value pair preferences to your application's Eloquent models. Preference

Kevin Laude 32 Oct 30, 2022
Laravel package to search through multiple Eloquent models.

Laravel package to search through multiple Eloquent models. Supports sorting, pagination, scoped queries, eager load relationships and searching through single or multiple columns.

Protone Media 845 Jan 1, 2023
Eloquent Befriended brings social media-like features like following, blocking and filtering content based on following or blocked models.

Laravel Befriended Eloquent Befriended brings social media-like features like following, blocking and filtering content based on following or blocked

Renoki Co. 720 Jan 3, 2023