├── .github └── workflows │ └── run-tests.yml ├── .styleci.yml ├── LICENSE.md ├── README.md ├── UPGRADING.md ├── composer.json ├── config └── f9web-laravel-deletable.php ├── resources └── lang │ └── en │ └── messages.php └── src ├── Exceptions └── NoneDeletableModel.php ├── LaravelDeletableServiceProvider.php └── Traits └── RestrictsDeletion.php /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-tests-laravel-8-9 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | tests: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | fail-fast: true 13 | matrix: 14 | php: [8.3, 8.2] 15 | laravel: ["^12.0", "^11.0", "^10.0"] 16 | dependency-version: [prefer-stable] 17 | include: 18 | - laravel: ^12.0 19 | testbench: 10.* 20 | - laravel: ^11.0 21 | testbench: 9.* 22 | - laravel: ^10.0 23 | testbench: 8.* 24 | 25 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} 26 | 27 | steps: 28 | - name: Checkout code 29 | uses: actions/checkout@v4 30 | 31 | - name: Cache dependencies 32 | uses: actions/cache@v4 33 | with: 34 | path: ~/.composer/cache/files 35 | key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 36 | 37 | - name: Setup PHP 38 | uses: shivammathur/setup-php@v2 39 | with: 40 | php-version: ${{ matrix.php }} 41 | extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv 42 | coverage: none 43 | 44 | - name: Install dependencies 45 | run: | 46 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" "mockery/mockery:^1.3.2" --no-interaction --no-update 47 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction 48 | 49 | - name: Display PHP version 50 | run: php -v | grep ^PHP | cut -d' ' -f2 51 | 52 | - name: Execute tests 53 | run: vendor/bin/phpunit --color=always tests 54 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr12 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) F9WebLtd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://banners.beyondco.de/Laravel%20Deletable.png?theme=light&packageManager=composer+require&packageName=f9webltd%2Flaravel-deletable&pattern=architect&style=style_1&description=Gracefully+restrict+deletion+of+Laravel+Eloquent+models&md=1&showWatermark=0&fontSize=100px&images=trash) 2 | 3 | [![Packagist Version](https://img.shields.io/packagist/v/f9webltd/laravel-deletable?style=flat-square)](https://packagist.org/packages/f9webltd/laravel-deletable) 4 | [![run-tests](https://github.com/f9webltd/laravel-deletable/actions/workflows/run-tests.yml/badge.svg)](https://github.com/f9webltd/laravel-deletable/actions/workflows/run-tests.yml)[![StyleCI Status](https://github.styleci.io/repos/278581318/shield)](https://github.styleci.io/repos/278581318) 5 | [![Packagist License](https://img.shields.io/packagist/l/f9webltd/laravel-deletable?style=flat-square)](https://packagist.org/packages/f9webltd/laravel-deletable) 6 | [![PHP ^8.2](https://img.shields.io/badge/php-%5E8.2-brightgreen)]() 7 | 8 | # Laravel Deletable 9 | 10 | Gracefully handle deletion restrictions on your [Eloquent models](https://laravel.com/docs/7.x/eloquent), as featured on [Laravel News](https://laravel-news.com/laravel-deletable-trait) 11 | 12 | ## Requirements 13 | 14 | * PHP `^8.2` 15 | * Laravel `^10.0` / `^11.0` / `^12.0`. 16 | 17 | For older versions of PHP / Laravel use version [1.0.6](https://github.com/f9webltd/laravel-deletable/tree/1.0.6) 18 | 19 | ## Installation 20 | 21 | ``` bash 22 | composer require f9webltd/laravel-deletable 23 | ``` 24 | 25 | The package will automatically register itself. 26 | 27 | Optionally publish the configuration file by running: `php artisan vendor:publish` and selecting the appropriate package. 28 | 29 | ## Documentation 30 | 31 | ### Usage 32 | 33 | Within an Eloquent model use the `RestrictsDeletion` trait: 34 | 35 | ```php 36 | namespace App; 37 | 38 | use F9Web\LaravelDeletable\Traits\RestrictsDeletion; 39 | use Illuminate\Database\Eloquent\Model; 40 | 41 | class User extends Model 42 | { 43 | use RestrictsDeletion; 44 | } 45 | ``` 46 | 47 | The trait overrides calls to Eloquent's `delete()` method. 48 | 49 | Implement the `isDeletable()` method within the model in question. 50 | 51 | This method should return `true` to allow deletion and `false` to deny deletion: 52 | 53 | ```php 54 | namespace App; 55 | 56 | use F9Web\LaravelDeletable\Traits\RestrictsDeletion; 57 | use Illuminate\Database\Eloquent\Model; 58 | 59 | class User extends Model 60 | { 61 | use RestrictsDeletion; 62 | 63 | public function isDeletable() : bool 64 | { 65 | return $this->orders()->doesntExist(); 66 | } 67 | } 68 | ``` 69 | 70 | The above denies deletion of users with orders. 71 | 72 | None deletable models throw an exception when the `isDeletable()` method returns `false`: 73 | 74 | ```php 75 | namespace App\Controllers; 76 | 77 | use F9Web\LaravelDeletable\Exceptions\NoneDeletableModel; 78 | use App\User; 79 | 80 | class UsersController 81 | { 82 | public function destroy(User $user) : bool 83 | { 84 | try { 85 | $user->delete(); 86 | } catch (NoneDeletableModel $e) { 87 | // dd($ex->getMessage()); 88 | } 89 | } 90 | } 91 | ``` 92 | 93 | #### Eloquent Base Model 94 | 95 | As the default `isDeletable()` method returns `true`, a base Eloquent model can be optionally defined from which all models extend. Each model can then optionally implement the `isDeletable()` method as needed. 96 | 97 | ### Customising Messages 98 | 99 | The default exception message is defined within the config `f9web-laravel-deletable.messages.default` and is simply `The model cannot be deleted`. 100 | 101 | By setting `f9web-laravel-deletable.messages.default` to `null` a more detailed message is automatically generated i.e. `Restricted deletion: App\User - 1 is not deletable`. 102 | 103 | Custom messages can be set within the `isDeletable()` method: 104 | 105 | ```php 106 | namespace App; 107 | 108 | use F9Web\LaravelDeletable\Traits\RestrictsDeletion; 109 | use Illuminate\Database\Eloquent\Model; 110 | use Illuminate\Support\Str; 111 | 112 | class User extends Model 113 | { 114 | use RestrictsDeletion; 115 | 116 | public function isDeletable() : bool 117 | { 118 | if (Str::endsWith($this->email, 'f9web.co.uk')) { 119 | return $this->denyDeletionReason('Users with f9web.co.uk company email addresses cannot be deleted'); 120 | } 121 | 122 | return true; 123 | } 124 | } 125 | ``` 126 | 127 | The `denyDeletionReason()` method can be used to specify the exception message. 128 | 129 | In the above case, the exception message is `Users with f9web.co.uk company email addresses cannot be deleted`. 130 | 131 | ### Multiple Checks 132 | 133 | Multiple checks can be performed within `isDeletable()` if necessary, each of which returning a different exception message: 134 | 135 | ```php 136 | namespace App; 137 | 138 | use F9Web\LaravelDeletable\Traits\RestrictsDeletion; 139 | use Illuminate\Database\Eloquent\Model; 140 | use Illuminate\Support\Str; 141 | 142 | class User extends Model 143 | { 144 | use RestrictsDeletion; 145 | 146 | public function isDeletable() : bool 147 | { 148 | if (Str::endsWith($this->email, 'f9web.co.uk')) { 149 | return $this->denyDeletionReason('Users with f9web.co.uk company email addresses cannot be deleted'); 150 | } 151 | 152 | if ($this->orders->isNotEmpty()) { 153 | return false; 154 | } 155 | 156 | if ($this->purchaseOrders->isNotEmpty()) { 157 | return $this->denyDeletionReason('This user has active purchase orders and cannot be deleted'); 158 | } 159 | 160 | if ($this->overdueInvoices->isNotEmpty()) { 161 | return $this->denyDeletionReason('Users with overdue invoices cannot be deleted'); 162 | } 163 | 164 | return true; 165 | } 166 | } 167 | ``` 168 | 169 | ## Contribution 170 | 171 | Any ideas are welcome. Feel free to submit any issues or pull requests. 172 | 173 | ## Testing 174 | 175 | ``` bash 176 | composer test 177 | ``` 178 | 179 | ## Security 180 | 181 | If you discover any security related issues, please email rob@f9web.co.uk instead of using the issue tracker. 182 | 183 | ## Credits 184 | 185 | - [Rob Allport](https://github.com/ultrono) for [F9 Web Ltd.](https://www.f9web.co.uk) 186 | 187 | ## License 188 | 189 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 190 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading 2 | 3 | ## 1.0.6 to 2.0.0 6.4.0 - 2024-02-28 4 | 5 | * Dropped support for PHP < `8.0` and Laravel < `10.0` 6 | * No changes to the package API have been made 7 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "f9webltd/laravel-deletable", 3 | "description": "Gracefully restrict deletion of Laravel Eloquent models", 4 | "keywords": [ 5 | "laravel", 6 | "laravel eloquent", 7 | "laravel delete", 8 | "laravel destroy model", 9 | "eloquent", 10 | "soft deletes" 11 | ], 12 | "homepage": "https://github.com/f9webltd/laravel-deletable", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Rob Allport", 17 | "email": "rob@f9web.co.uk", 18 | "homepage": "https://www.f9web.co.uk", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php": "^8.2", 24 | "illuminate/config": "^10.0|^11.0|^12.0", 25 | "illuminate/container": "^10.0|^11.0|^12.0", 26 | "illuminate/contracts": "^10.0|^11.0|^12.0", 27 | "illuminate/database": "^10.0|^11.0|^12.0" 28 | }, 29 | "require-dev": { 30 | "orchestra/testbench": "^8.0|^9.0|^10.0", 31 | "phpunit/phpunit": "^10.1|^11.5.3" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "F9Web\\LaravelDeletable\\": "src" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "F9Web\\LaravelDeletable\\Tests\\": "tests" 41 | } 42 | }, 43 | "scripts": { 44 | "test": "vendor/bin/phpunit" 45 | }, 46 | "config": { 47 | "sort-packages": true 48 | }, 49 | "extra": { 50 | "laravel": { 51 | "providers": [ 52 | "F9Web\\LaravelDeletable\\LaravelDeletableServiceProvider" 53 | ] 54 | } 55 | }, 56 | "minimum-stability": "dev", 57 | "prefer-stable": true 58 | } 59 | -------------------------------------------------------------------------------- /config/f9web-laravel-deletable.php: -------------------------------------------------------------------------------- 1 | [ 6 | 'default' => 'The model cannot be deleted', 7 | ], 8 | 9 | ]; 10 | -------------------------------------------------------------------------------- /resources/lang/en/messages.php: -------------------------------------------------------------------------------- 1 | '[:model #:id] is a core record and therefore not deletable. This indicates the record is used programmatically within the system.', 5 | ]; 6 | -------------------------------------------------------------------------------- /src/Exceptions/NoneDeletableModel.php: -------------------------------------------------------------------------------- 1 | publishes( 17 | [ 18 | __DIR__ . '/../config/f9web-laravel-deletable.php' => config_path('f9web-laravel-deletable.php'), 19 | ], 20 | 'config' 21 | ); 22 | 23 | $this->mergeConfigFrom(__DIR__ . '/../config/f9web-laravel-deletable.php', 'f9web-laravel-deletable'); 24 | 25 | $this->loadTranslationsFrom(__DIR__ . '/../resources/lang/', 'f9web-laravel-deletable'); 26 | 27 | $this->publishes( 28 | [ 29 | __DIR__ . '/../resources/lang' => resource_path('lang/vendor/f9web-laravel-deletable'), 30 | ] 31 | ); 32 | } 33 | 34 | public function register() 35 | { 36 | // 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Traits/RestrictsDeletion.php: -------------------------------------------------------------------------------- 1 | isDeletable()) { 26 | return parent::delete(); 27 | } 28 | 29 | throw new NoneDeletableModel( 30 | $this->getNoneDeletableMessage() 31 | ); 32 | } 33 | 34 | /** 35 | * @return bool 36 | */ 37 | public function isDeletable(): bool 38 | { 39 | return true; 40 | } 41 | 42 | /** 43 | * @return string|null 44 | */ 45 | public function getNoneDeletableMessage(): ?string 46 | { 47 | return $this->notDeletableMessage ?? $this->getFallbackMessage(); 48 | } 49 | 50 | /** 51 | * If a default message is omitted from the config, use a default 52 | * 53 | * @return string 54 | */ 55 | public function getFallbackMessage(): string 56 | { 57 | if ($message = config('f9web-laravel-deletable.messages.default')) { 58 | return $message; 59 | } 60 | 61 | return sprintf( 62 | 'Restricted deletion: %s - %s is not deletable', 63 | get_class($this), 64 | $this->getKey() 65 | ); 66 | } 67 | 68 | /** 69 | * Set a custom deletion restriction message and stop deletion 70 | * 71 | * @param string $message 72 | * @return bool 73 | */ 74 | public function denyDeletionReason(?string $message = null): bool 75 | { 76 | $this->notDeletableMessage = $message; 77 | 78 | return false; 79 | } 80 | 81 | /** 82 | * Set a custom deletion restriction message for core models 83 | * 84 | * @return bool 85 | */ 86 | public function isCoreEntity(): bool 87 | { 88 | $this->notDeletableMessage = trans( 89 | 'f9web-laravel-deletable::messages.core', 90 | [ 91 | 'model' => get_class($this), 92 | 'id' => $this->getKey(), 93 | ] 94 | ); 95 | 96 | return false; 97 | } 98 | 99 | /** 100 | * Get the value of the model's primary key. 101 | * 102 | * @return mixed 103 | */ 104 | abstract public function getKey(); 105 | } 106 | --------------------------------------------------------------------------------