├── .github └── workflows │ └── php.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── config └── auto-hard-deleter.php ├── cover.jpg ├── phpunit.xml ├── src ├── AutoHardDeleteServiceProvider.php └── HardDeleteExpiredCommand.php └── tests ├── AutoHardDeleterTest.php ├── HardDeleteExpiredTestCommand.php ├── Models └── SampleModel.php └── database ├── factories └── SampleModelFactory.php └── migrations └── 0000_00_00_000000_create_sample_models_test_table.php /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP Composer 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Validate composer.json and composer.lock 14 | run: composer validate 15 | 16 | - name: Install dependencies 17 | run: composer install --prefer-dist --no-progress --no-suggest 18 | 19 | - name: Run test suite 20 | run: composer run-script test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | /vendor/ 3 | /.idea/ 4 | /.phpunit.result.cache 5 | cover.psd -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.2 5 | - 7.3 6 | - 7.4 7 | 8 | before_script: 9 | - composer self-update 10 | - composer install --no-interaction 11 | 12 | script: 13 | - composer run-script test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Siavash Bamshadnia 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Auto Hard Deleter 2 | 3 | ![PHP Composer](https://github.com/SiavashBamshadnia/Laravel-Auto-Hard-Deleter/workflows/PHP%20Composer/badge.svg) 4 | [![Build Status](https://travis-ci.org/SiavashBamshadnia/Laravel-Auto-Hard-Deleter.svg?branch=master)](https://travis-ci.org/SiavashBamshadnia/Laravel-Auto-Hard-Deleter) 5 | [![StyleCI](https://github.styleci.io/repos/241140247/shield?branch=master)](https://github.styleci.io/repos/241140247) 6 | [![Latest Stable Version](https://poser.pugx.org/sbamtr/Laravel-Auto-Hard-Deleter/version)](https://packagist.org/packages/sbamtr/laravel-auto-hard-deleter) 7 | [![License](https://poser.pugx.org/sbamtr/Laravel-Auto-Hard-Deleter/license)](https://github.com/SiavashBamshadnia/Laravel-Auto-Hard-Deleter) 8 | ![PHP from Travis config](https://img.shields.io/travis/php-v/SiavashBamshadnia/Laravel-Auto-Hard-Deleter.svg) 9 | [![CodeFactor](https://www.codefactor.io/repository/github/siavashbamshadnia/laravel-auto-hard-deleter/badge)](https://www.codefactor.io/repository/github/siavashbamshadnia/laravel-auto-hard-deleter) 10 | 11 | ![](cover.jpg) 12 | 13 | This package deletes soft deleted rows automatically after a time interval that you define. 14 | 15 | *For Laravel and Lumen 6, 7, 8, 9* 16 | 17 | * [Installation](#installation) 18 | * [Usage](#usage) 19 | * [Auto Hard Delete Command](#auto-hard-delete-command) 20 | 21 | ## Installation 22 | ### Step 1 23 | Require the package with composer using the following command: 24 | ```bash 25 | composer require sbamtr/laravel-auto-hard-deleter 26 | ``` 27 | ### Step 2 28 | #### For Laravel 29 | The service provider will automatically get registered. Or you may manually add the service provider in your `config/app.php` file: 30 | ```php 31 | 'providers' => [ 32 | // ... 33 | \sbamtr\LaravelAutoHardDeleter\AutoHardDeleteServiceProvider::class, 34 | ]; 35 | ``` 36 | 37 | #### For Lumen 38 | Add this line of code under the `Register Service Providers` section of your `bootstrap/app.php`: 39 | ```php 40 | $app->register(\sbamtr\LaravelAutoHardDeleter\AutoHardDeleteServiceProvider::class); 41 | ``` 42 | 43 | ### Step 3 44 | Now its the time for scheduling the command. 45 | in you `app/Console/Kernel.php` file, paste this code in `schedule()` function: 46 | ```php 47 | protected function schedule(Schedule $schedule) 48 | { 49 | // ... 50 | $schedule->command(\sbamtr\LaravelAutoHardDeleter\HardDeleteExpiredCommand::class)->hourly(); 51 | // ... 52 | } 53 | ``` 54 | In the code above, the command scheduled to run hourly. you can change it. For more information, please read [this](https://laravel.com/docs/scheduling#scheduling-artisan-commands) page. 55 | 56 | ### Step 4 (Optional) 57 | You can publish the config file with this following command: 58 | ```bash 59 | php artisan vendor:publish --provider="sbamtr\LaravelAutoHardDeleter\AutoHardDeleteServiceProvider" --tag=config 60 | ``` 61 | **Note:** If you are using Lumen, you have to use [this package](https://github.com/laravelista/lumen-vendor-publish). 62 | 63 | Also you can set the `AUTO_HARD_DELETE_AFTER` value in `.env` file. like the following code: 64 | 65 | ```.env 66 | ... 67 | AUTO_HARD_DELETE_AFTER='1 day' 68 | ... 69 | ``` 70 | 71 | ## Usage 72 | in your models that used `SoftDeletes` trait, you can enable Auto Hard Delete with this code: 73 | ```php 74 | class SampleModel extends Model 75 | { 76 | use SoftDeletes; 77 | const AUTO_HARD_DELETE_ENABLED = true; 78 | } 79 | ``` 80 | Just write `const AUTO_HARD_DELETE_ENABLED = true` in your models! 81 | Also you can set expiration time for your deleted entities using the following line: 82 | ```php 83 | const AUTO_HARD_DELETE_AFTER = '5 months'; 84 | ``` 85 | In the code above, expiration time for your soft deleted entity model is 5 months. 86 | The final code is: 87 | ```php 88 | class SampleModel extends Model 89 | { 90 | use SoftDeletes; 91 | const AUTO_HARD_DELETE_ENABLED = true; 92 | const AUTO_HARD_DELETE_AFTER = '5 months'; 93 | } 94 | ``` 95 | You can set any other values for `AUTO_HARD_DELETE_AFTER` like `5`(means 5 days), `2 hours`, `45 days`, `2.5 months`, `1 year`, etc. 96 | 97 | **Note:** If you don't set any value for `AUTO_HARD_DELETE_AFTER` in your model, the soft deleted models with `AUTO_HARD_DELETE_ENABLED = true` will be hard deleted after the time defined in config file named `auto-hard-deleter.php`. 98 | 99 | ## Auto Hard Delete Command 100 | Also you can hard delete expired rows manually using this artisan command: 101 | ```bash 102 | php artisan hard-delete-expired 103 | ``` 104 | 105 | Written with ♥ by Siavash Bamshadnia 106 | 107 | Please support me by staring this repository. 108 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sbamtr/laravel-auto-hard-deleter", 3 | "keywords": [ 4 | "laravel", 5 | "lumen", 6 | "softdelete", 7 | "database" 8 | ], 9 | "type": "library", 10 | "require": { 11 | "php": ">=7.2.5", 12 | "illuminate/console": "^6|^7|^8|^9", 13 | "illuminate/database": "^6.20.12|^7.30.4|^8.22.1|^9", 14 | "illuminate/support": "^6|^7|^8|^9" 15 | }, 16 | "require-dev": { 17 | "orchestra/testbench": "^3|^4|^5|^6|^7" 18 | }, 19 | "description": "Laravel Auto Hard Deleter", 20 | "license": "MIT", 21 | "authors": [ 22 | { 23 | "name": "Siavash Bamshadnia", 24 | "email": "sbamtr@gmail.com" 25 | } 26 | ], 27 | "autoload": { 28 | "psr-4": { 29 | "sbamtr\\LaravelAutoHardDeleter\\": "src" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "sbamtr\\LaravelAutoHardDeleter\\Tests\\": "tests" 35 | } 36 | }, 37 | "config": { 38 | "optimize-autoloader": true, 39 | "sort-packages": true 40 | }, 41 | "extra": { 42 | "laravel": { 43 | "providers": [ 44 | "sbamtr\\LaravelAutoHardDeleter\\AutoHardDeleteServiceProvider" 45 | ] 46 | } 47 | }, 48 | "scripts": { 49 | "test": "vendor/bin/phpunit -c phpunit.xml --testdox --verbose" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /config/auto-hard-deleter.php: -------------------------------------------------------------------------------- 1 | env('AUTO_HARD_DELETE_AFTER', '60 days'), 15 | ]; 16 | -------------------------------------------------------------------------------- /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiavashBamshadnia/Laravel-Auto-Hard-Deleter/36ff6528167c00a09396ce060e910591e360ed46/cover.jpg -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/AutoHardDeleteServiceProvider.php: -------------------------------------------------------------------------------- 1 | commands([ 27 | HardDeleteExpiredCommand::class, 28 | ]); 29 | } 30 | 31 | /** 32 | * Bootstrap services. 33 | * 34 | * @return void 35 | */ 36 | public function boot() 37 | { 38 | // Publish config file 39 | $configPath = __DIR__.'/../config/auto-hard-deleter.php'; 40 | if (function_exists('config_path')) { 41 | $publishPath = config_path('auto-hard-deleter.php'); 42 | } else { 43 | $publishPath = base_path('config/auto-hard-deleter.php'); 44 | } 45 | $this->publishes([$configPath => $publishPath], 'config'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/HardDeleteExpiredCommand.php: -------------------------------------------------------------------------------- 1 | withEloquent(); 46 | } 47 | 48 | // Autoload classes. this is needed for finding latest model classes 49 | $process = new Process(['composer', 'dump-autoload', '-o']); 50 | $process->run(); 51 | 52 | if (!$process->isSuccessful()) { 53 | throw new ProcessFailedException($process); 54 | } 55 | 56 | // Include all of classes 57 | $classes = include_once base_path('vendor/composer/autoload_classmap.php'); 58 | $classes = array_keys($classes); 59 | $classes2 = []; 60 | 61 | // Exclude classes that not support soft delete 62 | foreach ($classes as $class) { 63 | if (Str::startsWith($class, 'App') && (new ReflectionClass($class))->hasMethod('runSoftDelete')) { 64 | $classes2[] = $class; 65 | } 66 | } 67 | 68 | foreach ($classes2 as $class) { 69 | $object = new $class(); 70 | $deletedAtColumn = $object->getDeletedAtColumn(); 71 | 72 | // If auto hard delete is not enabled, do not delete anything 73 | if (!defined("$class::AUTO_HARD_DELETE_ENABLED") || $class::AUTO_HARD_DELETE_ENABLED != true) { 74 | continue; 75 | } 76 | 77 | if (defined("$class::AUTO_HARD_DELETE_AFTER")) { 78 | $autoHardDeleteAfter = $class::AUTO_HARD_DELETE_AFTER; 79 | } else { 80 | $autoHardDeleteAfter = null; 81 | } 82 | 83 | if (!$autoHardDeleteAfter || blank($autoHardDeleteAfter)) { 84 | $autoHardDeleteAfter = config('auto-hard-deleter.auto_hard_delete_after', '60 days'); 85 | } 86 | if (is_numeric($autoHardDeleteAfter)) { 87 | $autoHardDeleteAfter .= ' days'; 88 | } 89 | 90 | // Hard delete expired rows 91 | $count = $class::onlyTrashed()->where($deletedAtColumn, '<=', Carbon::now()->sub($autoHardDeleteAfter))->forceDelete(); 92 | 93 | if ($count) { 94 | $this->line("Deleted $count rows from ".$object->getTable().' table.'); 95 | } 96 | } 97 | 98 | $this->info('Auto hard deleting completed successfully.'); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/AutoHardDeleterTest.php: -------------------------------------------------------------------------------- 1 | faker->numberBetween(1, 100))->create(['del' => Carbon::now()->subDays($this->faker->numberBetween(1, 14))]); 19 | factory(SampleModel::class, $this->faker->numberBetween(1, 100))->create(['del' => Carbon::now()->subDays(15)]); 20 | factory(SampleModel::class, $this->faker->numberBetween(1, 100))->create(['del' => Carbon::now()->subDays($this->faker->numberBetween(16, 100))]); 21 | 22 | $app = new Application(realpath(__DIR__.'/../')); 23 | $command = $this->app->make(HardDeleteExpiredTestCommand::class); 24 | $command->setLaravel($app); 25 | $tester = new CommandTester($command); 26 | $tester->execute([]); 27 | 28 | self::assertEquals($numberOfNotExpiredRows, SampleModel::withTrashed()->count()); 29 | } 30 | 31 | protected function getEnvironmentSetUp($app) 32 | { 33 | $app['config']->set('database.default', 'testbench'); 34 | $app['config']->set('database.connections.testbench', [ 35 | 'driver' => 'sqlite', 36 | 'database' => ':memory:', 37 | 'prefix' => '', 38 | ]); 39 | } 40 | 41 | protected function setUp(): void 42 | { 43 | parent::setUp(); 44 | $this->loadMigrationsFrom(__DIR__.'/database/migrations'); 45 | $this->withFactories(__DIR__.'/database/factories'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/HardDeleteExpiredTestCommand.php: -------------------------------------------------------------------------------- 1 | withEloquent(); 44 | } 45 | 46 | // Autoload classes. this is needed for finding latest model classes 47 | $process = new Process(['composer', 'dump-autoload', '-o']); 48 | $process->run(); 49 | 50 | if (!$process->isSuccessful()) { 51 | throw new ProcessFailedException($process); 52 | } 53 | 54 | // Include all of classes 55 | $classes = include_once /** @scrutinizer ignore-call */ base_path('vendor/composer/autoload_classmap.php'); 56 | $classes = array_keys($classes); 57 | $classes2 = []; 58 | 59 | // Exclude classes that not support soft delete 60 | foreach ($classes as $class) { 61 | if (Str::startsWith($class, 'sbamtr\LaravelAutoHardDeleter\Tests\Models') && (new ReflectionClass($class))->hasMethod('runSoftDelete')) { 62 | $classes2[] = $class; 63 | } 64 | } 65 | 66 | foreach ($classes2 as $class) { 67 | $object = new $class(); 68 | $deletedAtColumn = $object->getDeletedAtColumn(); 69 | 70 | // If auto hard delete is not enabled, do not delete anything 71 | if (!defined("$class::AUTO_HARD_DELETE_ENABLED") || $class::AUTO_HARD_DELETE_ENABLED != true) { 72 | continue; 73 | } 74 | 75 | if (defined("$class::AUTO_HARD_DELETE_AFTER")) { 76 | $autoHardDeleteAfter = $class::AUTO_HARD_DELETE_AFTER; 77 | } else { 78 | $autoHardDeleteAfter = null; 79 | } 80 | 81 | if (!$autoHardDeleteAfter || blank($autoHardDeleteAfter)) { 82 | $autoHardDeleteAfter = config('auto-hard-deleter.auto_hard_delete_after', '60 days'); 83 | } 84 | if (is_numeric($autoHardDeleteAfter)) { 85 | $autoHardDeleteAfter .= ' days'; 86 | } 87 | 88 | // Hard delete expired rows 89 | $count = $class::onlyTrashed()->where($deletedAtColumn, '<=', Carbon::now()->sub($autoHardDeleteAfter))->forceDelete(); 90 | 91 | if ($count) { 92 | $this->line("Deleted $count rows from ".$object->getTable().' table.'); 93 | } 94 | } 95 | 96 | $this->info('Auto hard deleting completed successfully.'); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/Models/SampleModel.php: -------------------------------------------------------------------------------- 1 | define(SampleModel::class, function (Faker $faker) { 10 | return [ 11 | 'foo' => $faker->numberBetween(), 12 | 'bar' => $faker->text, 13 | ]; 14 | }); 15 | -------------------------------------------------------------------------------- /tests/database/migrations/0000_00_00_000000_create_sample_models_test_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 15 | $table->integer('foo'); 16 | $table->string('bar'); 17 | $table->softDeletes('del'); 18 | }); 19 | } 20 | } 21 | --------------------------------------------------------------------------------