├── 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 | [](https://packagist.org/packages/illuminatech/db-safedelete)
14 | [](https://packagist.org/packages/illuminatech/db-safedelete)
15 | [](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 |
--------------------------------------------------------------------------------