├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src └── SafeDeletes.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Laravel Eloquent Safe Delete 2 | ============================ 3 | 4 | 1.0.6, March 6, 2025 5 | -------------------- 6 | 7 | - Enh: Added support for "illuminate/database" 12.0 (klimov-paul) 8 | 9 | 10 | 1.0.5, March 25, 2024 11 | --------------------- 12 | 13 | - Enh: Added support for "illuminate/database" 11.0 (klimov-paul) 14 | 15 | 16 | 1.0.4, February 27, 2023 17 | ------------------------ 18 | 19 | - Enh: Added support for "illuminate/database" 10.0 (klimov-paul) 20 | 21 | 22 | 1.0.3, February 9, 2022 23 | ----------------------- 24 | 25 | - Enh: Added support for "illuminate/database" 9.0 (klimov-paul) 26 | 27 | 28 | 1.0.2, September 9, 2020 29 | ------------------------ 30 | 31 | - Enh: Added support for "illuminate/database" 8.0 (klimov-paul) 32 | 33 | 34 | 1.0.1, March 4, 2020 35 | -------------------- 36 | 37 | - Enh: Added support for "illuminate/database" 7.0 (klimov-paul) 38 | 39 | 40 | 1.0.0, November 19, 2019 41 | ------------------------ 42 | 43 | - Initial release. 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This is free software. It is released under the terms of the 2 | following BSD License. 3 | 4 | Copyright © 2019 by Illuminatech (https://github.com/illuminatech) 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions 9 | are met: 10 | 11 | * Redistributions of source code must retain the above copyright 12 | notice, this list of conditions and the following disclaimer. 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in 15 | the documentation and/or other materials provided with the 16 | distribution. 17 | * Neither the name of Illuminatech nor the names of its 18 | contributors may be used to endorse or promote products derived 19 | from this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 24 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 25 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 26 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 27 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 31 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

Laravel Eloquent Safe Delete

6 |
7 |

8 | 9 | This extension provides "safe" deletion for the Eloquent model, which attempts to invoke force delete, and, if it fails - falls back to soft delete. 10 | 11 | For license information check the [LICENSE](LICENSE.md)-file. 12 | 13 | [![Latest Stable Version](https://img.shields.io/packagist/v/illuminatech/db-safedelete.svg)](https://packagist.org/packages/illuminatech/db-safedelete) 14 | [![Total Downloads](https://img.shields.io/packagist/dt/illuminatech/db-safedelete.svg)](https://packagist.org/packages/illuminatech/db-safedelete) 15 | [![Build Status](https://github.com/illuminatech/db-safedelete/workflows/build/badge.svg)](https://github.com/illuminatech/db-safedelete/actions) 16 | 17 | 18 | Installation 19 | ------------ 20 | 21 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 22 | 23 | Either run 24 | 25 | ``` 26 | php composer.phar require --prefer-dist illuminatech/db-safedelete 27 | ``` 28 | 29 | or add 30 | 31 | ```json 32 | "illuminatech/db-safedelete": "*" 33 | ``` 34 | 35 | to the require section of your composer.json. 36 | 37 | 38 | Usage 39 | ----- 40 | 41 | This extension provides "safe" deletion for the Eloquent model, which attempts to invoke force delete, and, if it fails - falls back to soft delete. 42 | It works on top of regular Laravel [model soft deleting](https://laravel.com/docs/eloquent#soft-deleting) feature. 43 | 44 | In case of usage of the relational database, which supports foreign keys, like MySQL, PostgreSQL etc., "soft" deletion 45 | is widely used for keeping foreign keys consistence. For example: if user performs a purchase at the online shop, information 46 | about this purchase should remain in the system for the future bookkeeping. The DDL for such data structure may look like the 47 | following one: 48 | 49 | ```sql 50 | CREATE TABLE `сustomers` 51 | ( 52 | `id` integer NOT NULL AUTO_INCREMENT, 53 | `name` varchar(64) NOT NULL, 54 | `address` varchar(64) NOT NULL, 55 | `phone` varchar(20) NOT NULL, 56 | PRIMARY KEY (`id`) 57 | ) ENGINE InnoDB; 58 | 59 | CREATE TABLE `purchases` 60 | ( 61 | `id` integer NOT NULL AUTO_INCREMENT, 62 | `customer_id` integer NOT NULL, 63 | `item_id` integer NOT NULL, 64 | `amount` integer NOT NULL, 65 | PRIMARY KEY (`id`) 66 | FOREIGN KEY (`customer_id`) REFERENCES `customers` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, 67 | FOREIGN KEY (`item_id`) REFERENCES `items` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, 68 | ) ENGINE InnoDB; 69 | ``` 70 | 71 | Thus, while set up a foreign key from 'purchase' to 'user', 'ON DELETE RESTRICT' mode is used. So on attempt to delete 72 | a user record, which have at least one purchase, a database error will occur. However, if user record have no external 73 | reference, it can be deleted. 74 | 75 | This extension introduces `Illuminatech\DbSafeDelete\SafeDeletes` trait, which serves as an enhanced version of standard 76 | `Illuminate\Database\Eloquent\SoftDeletes`, allowing handing foreign key constraints and custom delete allowing logic. 77 | Being attached to the model `Illuminatech\DbSafeDelete\SafeDeletes` changes model's regular `delete()` method in the way 78 | it attempts to invoke force delete, and, if it fails - falls back to soft delete. For example: 79 | 80 | ```php 81 | hasMany(Purchase::class); 95 | } 96 | 97 | // ... 98 | } 99 | 100 | // if there is a foreign key reference : 101 | $customerWithReference = Customer::query() 102 | ->whereHas('purchases') 103 | ->first(); 104 | 105 | $customerWithReference->delete(); // performs "soft" delete! 106 | 107 | // if there is NO foreign key reference : 108 | $customerWithoutReference = Customer::query() 109 | ->whereDoesntHave('purchases') 110 | ->first(); 111 | 112 | $customerWithoutReference->delete(); // performs actual delete! 113 | ``` 114 | 115 | **Heads up!** Make sure you do not attach both `Illuminate\Database\Eloquent\SoftDeletes` and `Illuminatech\DbSafeDelete\SafeDeletes` 116 | in the same model class. It will cause PHP naming conflict error since `Illuminate\Database\Eloquent\SoftDeletes` is already 117 | included into `Illuminatech\DbSafeDelete\SafeDeletes`. 118 | 119 | 120 | ### Smart deletion 121 | 122 | Usually "soft" deleting feature is used to prevent the database history loss, ensuring data, which has been in use and 123 | perhaps has a references or dependencies, is kept in the system. However, sometimes actual deleting is allowed for 124 | such data as well. 125 | For example: usually user account records should not be deleted but only marked as "trashed", however if you browse 126 | through users list and found accounts, which has been registered long ago, but don't have at least single log-in in the 127 | system, these records have no value for the history and can be removed from database to save a disk space. 128 | 129 | You can make "soft" deletion to be "smart" and detect, if the record can be removed from the database or only marked as "trashed". 130 | This can be done via `Illuminatech\DbSafeDelete\SafeDeletes::forceDeleteAllowed()`. For example: 131 | 132 | ```php 133 | last_login_at === null; 147 | } 148 | 149 | // ... 150 | } 151 | 152 | $user = User::query()->whereNull('last_login_at')->first(); 153 | $user->delete(); // removes the record!!! 154 | 155 | $user = User::query()->whereNotNull('last_login_at')->first(); 156 | $user->delete(); // marks record as "trashed" 157 | ``` 158 | 159 | 160 | ### Manual delete flow control 161 | 162 | Using `Illuminatech\DbSafeDelete\SafeDeletes` you can still manually "soft" delete or "force" delete a particular record, using 163 | following methods: 164 | 165 | - `softDelete()` - always performs "soft" deletion. 166 | - `forceDelete()` - always performs actual deletion. 167 | - `safeDelete()` - attempts to perform actual deletion, if it fails - applies "soft" one. 168 | 169 | For example: 170 | 171 | ```php 172 | whereHas('purchases') 177 | ->first(); 178 | 179 | $customerWithReference->forceDelete(); // performs actual delete (triggers a database error actually)! 180 | 181 | // if there is NO foreign key reference : 182 | $customerWithoutReference = Customer::query() 183 | ->whereDoesntHave('purchases') 184 | ->first(); 185 | 186 | $customerWithoutReference->softDelete(); // performs "soft" delete! 187 | 188 | // if there is a foreign key reference : 189 | $customerWithReference = Customer::query() 190 | ->whereHas('purchases') 191 | ->first(); 192 | 193 | $customerWithReference->safeDelete(); // performs "soft" delete! 194 | 195 | // if there is NO foreign key reference : 196 | $customerWithoutReference = Customer::query() 197 | ->whereDoesntHave('purchases') 198 | ->first(); 199 | 200 | $customerWithoutReference->safeDelete(); // performs actual delete! 201 | ``` 202 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "illuminatech/db-safedelete", 3 | "description": "Attempts to invoke force delete, if it fails - falls back to soft delete.", 4 | "keywords": ["laravel", "database", "eloquent", "model", "safe", "smart", "soft", "delete", "foreign", "key"], 5 | "license": "BSD-3-Clause", 6 | "support": { 7 | "issues": "https://github.com/illuminatech/db-safedelete/issues", 8 | "wiki": "https://github.com/illuminatech/db-safedelete/wiki", 9 | "source": "https://github.com/illuminatech/db-safedelete" 10 | }, 11 | "authors": [ 12 | { 13 | "name": "Paul Klimov", 14 | "email": "klimov.paul@gmail.com" 15 | } 16 | ], 17 | "require": { 18 | "illuminate/database": "^5.8 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0" 19 | }, 20 | "require-dev": { 21 | "illuminate/events": "*", 22 | "phpunit/phpunit": "^7.5 || ^8.0 || ^9.3 || ^10.5" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Illuminatech\\DbSafeDelete\\": "src" 27 | } 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "Illuminatech\\DbSafeDelete\\Test\\": "tests" 32 | } 33 | }, 34 | "extra": { 35 | "branch-alias": { 36 | "dev-master": "1.0.x-dev" 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/SafeDeletes.php: -------------------------------------------------------------------------------- 1 | forceDeleting) { 41 | $this->exists = false; 42 | 43 | return $this->setKeysForSaveQuery($this->newModelQuery())->forceDelete(); 44 | } 45 | 46 | if (! $this->safeDeleting || ! $this->forceDeleteAllowed()) { 47 | return $this->runSoftDelete(); 48 | } 49 | 50 | $this->getConnection()->beginTransaction(); 51 | 52 | try { 53 | $result = $this->setKeysForSaveQuery($this->newModelQuery())->forceDelete(); 54 | $this->exists = false; 55 | 56 | $this->getConnection()->commit(); 57 | 58 | return $result; 59 | } catch (QueryException $e) { 60 | $this->getConnection()->rollBack(); 61 | 62 | return $this->runSoftDelete(); 63 | } catch (\Throwable $e) { 64 | $this->getConnection()->rollBack(); 65 | 66 | throw $e; 67 | } 68 | } 69 | 70 | /** 71 | * Indicates whether this particular model is allowed to be force deleted or not. 72 | * 73 | * @return bool whether force delete is allowed for this particular model. 74 | */ 75 | public function forceDeleteAllowed(): bool 76 | { 77 | return true; 78 | } 79 | 80 | /** 81 | * Marks this model as "deleted" without actual record removal. 82 | * 83 | * @return bool|null 84 | */ 85 | public function softDelete() 86 | { 87 | $originSafeDeleting = $this->safeDeleting; 88 | 89 | $this->safeDeleting = false; 90 | 91 | return tap($this->delete(), function () use ($originSafeDeleting) { 92 | $this->safeDeleting = $originSafeDeleting; 93 | }); 94 | } 95 | 96 | /** 97 | * Attempts to invoke force delete, and, if it fails - falls back to soft delete. 98 | * 99 | * @return bool|null 100 | */ 101 | public function safeDelete() 102 | { 103 | $originSafeDeleting = $this->safeDeleting; 104 | 105 | $this->safeDeleting = true; 106 | 107 | return tap($this->delete(), function () use ($originSafeDeleting) { 108 | $this->safeDeleting = $originSafeDeleting; 109 | }); 110 | } 111 | } 112 | --------------------------------------------------------------------------------