├── .gitignore
├── src
├── CascadesDeletesModel.php
└── CascadesDeletes.php
├── tests
├── Models
│ ├── Photo.php
│ ├── Profile.php
│ ├── SoftProfile.php
│ ├── Comment.php
│ ├── InvalidKid.php
│ ├── SoftPost.php
│ ├── PermanentPost.php
│ ├── SoftUser.php
│ ├── ExtendedUser.php
│ ├── Post.php
│ └── User.php
├── ModelTest.php
├── helpers.php
├── TestCase.php
├── TraitTest.php
└── IntegrationTest.php
├── .scrutinizer.yml
├── phpcs.xml
├── phpunit.9.xml
├── phpunit.10.ignore.deprecations.xml
├── phpunit.xml
├── LICENSE.md
├── CONTRIBUTING.md
├── composer.json
├── .github
└── workflows
│ └── phpunit.yml
├── CHANGELOG.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | composer.lock
--------------------------------------------------------------------------------
/src/CascadesDeletesModel.php:
--------------------------------------------------------------------------------
1 | morphTo();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tests/Models/Profile.php:
--------------------------------------------------------------------------------
1 | belongsTo(User::class);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tests/Models/SoftProfile.php:
--------------------------------------------------------------------------------
1 | belongsTo(User::class);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | build:
2 | nodes:
3 | analysis:
4 | environment:
5 | php: 8.3.11
6 | tests:
7 | override:
8 | - php-scrutinizer-run
9 | - phpcs-run
10 |
11 | filter:
12 | excluded_paths: [tests/*]
13 |
14 | tools:
15 | external_code_coverage:
16 | timeout: 5400
17 | runs: 12
18 |
--------------------------------------------------------------------------------
/tests/Models/Comment.php:
--------------------------------------------------------------------------------
1 | belongsTo(User::class);
14 | }
15 |
16 | public function post()
17 | {
18 | return $this->belongsTo(Post::class);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/ModelTest.php:
--------------------------------------------------------------------------------
1 | assertContains(CascadesDeletes::class, class_uses_recursive(ExtendedUser::class));
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tests/Models/InvalidKid.php:
--------------------------------------------------------------------------------
1 | morphTo();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Base PSR-2 with a few modifications.
4 |
5 | src
6 | tests
7 |
8 |
9 |
10 |
11 |
12 |
13 | tests/*
14 |
15 |
16 |
17 | tests/*
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/tests/Models/SoftPost.php:
--------------------------------------------------------------------------------
1 | belongsTo(SoftUser::class);
16 | }
17 |
18 | public function childPosts()
19 | {
20 | return $this->hasMany(SoftPost::class, 'parent_id');
21 | }
22 |
23 | public function parentPost()
24 | {
25 | return $this->belongsTo(SoftPost::class, 'parent_id');
26 | }
27 |
28 | public function comments()
29 | {
30 | return $this->hasMany(Comment::class, 'post_id');
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tests/Models/PermanentPost.php:
--------------------------------------------------------------------------------
1 | belongsTo(User::class);
12 | }
13 |
14 | public function childPosts()
15 | {
16 | return $this->hasMany(PermanentPost::class, 'parent_id');
17 | }
18 |
19 | public function parentPost()
20 | {
21 | return $this->belongsTo(PermanentPost::class, 'parent_id');
22 | }
23 |
24 | public function comments()
25 | {
26 | return $this->hasMany(Comment::class, 'post_id');
27 | }
28 |
29 | public function delete()
30 | {
31 | return false;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tests/Models/SoftUser.php:
--------------------------------------------------------------------------------
1 | belongsToMany(SoftUser::class, 'friends', 'user_id', 'friend_id');
16 | }
17 |
18 | public function posts()
19 | {
20 | return $this->hasMany(SoftPost::class, 'user_id');
21 | }
22 |
23 | public function comments()
24 | {
25 | return $this->hasMany(Comment::class, 'user_id');
26 | }
27 |
28 | public function profile()
29 | {
30 | return $this->hasOne(SoftProfile::class, 'user_id');
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/phpunit.9.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 | ./src
12 |
13 |
14 | ./src/CascadesDeletesModel.php
15 |
16 |
17 |
18 |
19 | ./tests/
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/tests/Models/ExtendedUser.php:
--------------------------------------------------------------------------------
1 | belongsToMany(User::class, 'friends', 'user_id', 'friend_id');
16 | }
17 |
18 | public function posts()
19 | {
20 | return $this->hasMany(Post::class);
21 | }
22 |
23 | public function photos()
24 | {
25 | return $this->morphMany(Photo::class, 'imageable');
26 | }
27 |
28 | public function comments()
29 | {
30 | return $this->hasMany(Comment::class);
31 | }
32 |
33 | public function profile()
34 | {
35 | return $this->hasOne(Profile::class);
36 | }
37 |
38 | public function permanentPosts()
39 | {
40 | return $this->hasMany(PermanentPost::class);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/phpunit.10.ignore.deprecations.xml:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 | ./src
18 |
19 |
20 | ./src/CascadesDeletesModel.php
21 |
22 |
23 |
24 |
25 | ./tests/
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 | ./src
19 |
20 |
21 | ./src/CascadesDeletesModel.php
22 |
23 |
24 |
25 |
26 | ./tests/
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/tests/Models/Post.php:
--------------------------------------------------------------------------------
1 | belongsTo(User::class);
19 | }
20 |
21 | public function photos()
22 | {
23 | return $this->morphMany(Photo::class, 'imageable');
24 | }
25 |
26 | public function childPosts()
27 | {
28 | return $this->hasMany(Post::class, 'parent_id');
29 | }
30 |
31 | public function parentPost()
32 | {
33 | return $this->belongsTo(Post::class, 'parent_id');
34 | }
35 |
36 | public function comments()
37 | {
38 | return $this->hasMany(Comment::class);
39 | }
40 |
41 | public function invalidKids()
42 | {
43 | return $this->morphMany(InvalidKid::class, 'invalidable');
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/Models/User.php:
--------------------------------------------------------------------------------
1 | belongsToMany(User::class, 'friends', 'user_id', 'friend_id');
19 | }
20 |
21 | public function posts()
22 | {
23 | return $this->hasMany(Post::class);
24 | }
25 |
26 | public function photos()
27 | {
28 | return $this->morphMany(Photo::class, 'imageable');
29 | }
30 |
31 | public function comments()
32 | {
33 | return $this->hasMany(Comment::class);
34 | }
35 |
36 | public function profile()
37 | {
38 | return $this->hasOne(Profile::class);
39 | }
40 |
41 | public function permanentPosts()
42 | {
43 | return $this->hasMany(PermanentPost::class);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Patrick Carlo-Hickman
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 |
--------------------------------------------------------------------------------
/tests/helpers.php:
--------------------------------------------------------------------------------
1 | $class], class_parents($class)) as $class) {
20 | $results += trait_uses_recursive($class);
21 | }
22 |
23 | return array_unique($results);
24 | }
25 | }
26 |
27 | if (!function_exists('trait_uses_recursive')) {
28 | /**
29 | * Returns all traits used by a trait and its traits.
30 | *
31 | * @param string $trait
32 | *
33 | * @return array
34 | */
35 | function trait_uses_recursive($trait)
36 | {
37 | $traits = class_uses($trait);
38 |
39 | foreach ($traits as $trait) {
40 | $traits += trait_uses_recursive($trait);
41 | }
42 |
43 | return $traits;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Contributions are **welcome** and will be fully **credited**.
4 |
5 | We accept contributions via Pull Requests on [Github](https://github.com/shiftonelabs/laravel-cascade-deletes).
6 |
7 |
8 | ## Pull Requests
9 |
10 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer).
11 |
12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests (within reason).
13 |
14 | - **Document any change in behavior** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.
15 |
16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option.
17 |
18 | - **Create feature branches** - Don't ask us to pull from your master branch.
19 |
20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
21 |
22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting.
23 |
24 |
25 | **Happy coding**!
26 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shiftonelabs/laravel-cascade-deletes",
3 | "description": "Adds application level cascading deletes to Eloquent Models.",
4 | "keywords": ["laravel", "lumen", "eloquent", "model", "cascade", "deletes"],
5 | "homepage": "https://github.com/shiftonelabs/laravel-cascade-deletes",
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "Patrick Carlo-Hickman",
10 | "email": "patrick@shiftonelabs.com"
11 | }
12 | ],
13 | "support": {
14 | "issues": "https://github.com/shiftonelabs/laravel-cascade-deletes/issues",
15 | "source": "https://github.com/shiftonelabs/laravel-cascade-deletes"
16 | },
17 | "require": {
18 | "php": ">=8.0.2",
19 | "illuminate/database": ">=9.0",
20 | "illuminate/events": ">=9.0"
21 | },
22 | "require-dev": {
23 | "mockery/mockery": "~1.3",
24 | "phpunit/phpunit": "~9.3 || ~10.0",
25 | "shiftonelabs/codesniffer-standard": "0.*",
26 | "squizlabs/php_codesniffer": "3.*"
27 | },
28 | "autoload": {
29 | "psr-4": {
30 | "ShiftOneLabs\\LaravelCascadeDeletes\\": "src/"
31 | }
32 | },
33 | "autoload-dev": {
34 | "psr-4": {
35 | "ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\": "tests/"
36 | },
37 | "files": [
38 | "tests/helpers.php"
39 | ]
40 | },
41 | "config": {
42 | "sort-packages": true,
43 | "allow-plugins": {
44 | "kylekatarnls/update-helper": false
45 | }
46 | },
47 | "minimum-stability": "stable"
48 | }
49 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | addConnection([
30 | 'driver' => 'sqlite',
31 | 'database' => ':memory:',
32 | ]);
33 |
34 | $db->setEventDispatcher(new Dispatcher(new Container()));
35 |
36 | $db->bootEloquent();
37 | $db->setAsGlobal();
38 |
39 | // This is required for testing Model events. If this is not done, the
40 | // events will only fire on the first test.
41 | Model::clearBootedModels();
42 | }
43 |
44 | /**
45 | * Get a schema builder instance.
46 | *
47 | * @return \Illuminate\Database\Schema\Builder
48 | */
49 | protected function schema($connection = 'default')
50 | {
51 | return $this->connection($connection)->getSchemaBuilder();
52 | }
53 |
54 | /**
55 | * Get a database connection instance.
56 | *
57 | * @return \Illuminate\Database\Connection
58 | */
59 | protected function connection($connection = 'default')
60 | {
61 | return Model::getConnectionResolver()->connection($connection);
62 | }
63 |
64 | /**
65 | * Use reflection to set the value of a restricted (private/protected)
66 | * property on an object.
67 | *
68 | * @param object $object
69 | * @param string $property
70 | * @param mixed $value
71 | *
72 | * @return void
73 | */
74 | protected function setRestrictedValue($object, $property, $value)
75 | {
76 | $reflectionProperty = new ReflectionProperty($object, $property);
77 | $reflectionProperty->setAccessible(true);
78 |
79 | if ($reflectionProperty->isStatic()) {
80 | $reflectionProperty->setValue($value);
81 | } else {
82 | $reflectionProperty->setValue($object, $value);
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/.github/workflows/phpunit.yml:
--------------------------------------------------------------------------------
1 | name: Phpunit
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | phpcs:
7 | runs-on: ubuntu-latest
8 |
9 | name: phpcs - PHP 8.4
10 |
11 | steps:
12 | - name: Checkout repo
13 | uses: actions/checkout@v3
14 |
15 | # Setup the PHP version to use.
16 | - name: Setup PHP
17 | uses: shivammathur/setup-php@v2
18 | with:
19 | php-version: '8.4'
20 |
21 | # Dependencies needed for the shiftonelabs/codesniffer-standard package.
22 | - name: Install dependencies
23 | run: composer update --prefer-dist --no-interaction
24 |
25 | # Run the phpcs tool.
26 | - name: Run phpcs
27 | run: ./vendor/bin/phpcs
28 |
29 | tests:
30 | runs-on: ${{ matrix.os }}
31 | strategy:
32 | # Turn off fail-fast so that all jobs will run even when one fails,
33 | # and the build will still get marked as failed.
34 | fail-fast: false
35 |
36 | matrix:
37 | os: [ubuntu-latest]
38 | php: ['8.0', '8.1', '8.2', '8.3', '8.4']
39 | laravel: ['9.*', '10.*', '11.*']
40 | exclude:
41 | - php: '8.0'
42 | laravel: '10.*'
43 | - php: '8.0'
44 | laravel: '11.*'
45 | - php: '8.1'
46 | laravel: '11.*'
47 |
48 | name: tests - PHP ${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.os }}
49 |
50 | steps:
51 | - name: Checkout repo
52 | uses: actions/checkout@v3
53 | with:
54 | # We need more than 1 commit to prevent the "Failed to retrieve
55 | # commit parents" error from the ocular code coverage upload.
56 | fetch-depth: 5
57 |
58 | # Setup the PHP version to use for the test and include xdebug to generate the code coverage file.
59 | - name: Setup PHP
60 | uses: shivammathur/setup-php@v2
61 | with:
62 | php-version: ${{ matrix.php }}
63 | coverage: xdebug
64 |
65 | # Setup the required packages for the version being tested and install the packages
66 | - name: Install dependencies
67 | run: |
68 | COMPOSER_MEMORY_LIMIT=-1 composer require "illuminate/database:${{ matrix.laravel }}" --no-update
69 | composer update --prefer-dist --no-interaction
70 |
71 | # Run the unit tests and generate the code coverage file.
72 | - name: Run phpunit tests
73 | run: |
74 | PHPUNIT_CONFIG=""
75 | ( [[ -z "${PHPUNIT_CONFIG}" ]] && [[ "${{ matrix.php }}" == "8.0" ]] ) && PHPUNIT_CONFIG="--configuration phpunit.9.xml"
76 | ( [[ -z "${PHPUNIT_CONFIG}" ]] && [[ "${{ matrix.php }}" == "8.4" ]] && ( [[ "${{ matrix.laravel }}" == "9.*" ]] || [[ "${{ matrix.laravel }}" == "10.*" ]] ) ) && PHPUNIT_CONFIG="--configuration phpunit.10.ignore.deprecations.xml"
77 | ./vendor/bin/phpunit ${PHPUNIT_CONFIG} --coverage-clover ./clover.xml
78 |
79 | # Send the code coverage file regardless of the tests passing or failing.
80 | - name: Send coverage
81 | if: success() || failure()
82 | run: |
83 | composer global require scrutinizer/ocular
84 | ~/.composer/vendor/bin/ocular code-coverage:upload --format=php-clover ./clover.xml
85 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6 |
7 | ## [Unreleased]
8 |
9 | ## [2.0.2] - 2024-12-14
10 | ### Added
11 | - Added new phpunit config for older PHP version.
12 | - Added new phpunit config for incompatible PHP/Laravel versions.
13 |
14 | ### Changed
15 | - Updated phpunit config to latest version.
16 | - Updated phpunit config to ensure tests fail on warnings, notices, and deprecations.
17 | - Updated Github Actions to use different phpunit configs.
18 | - Updated CI configs to add support for PHP 8.4.
19 |
20 | ### Fixed
21 | - Fixed deprecation notice in PHP 8.4. ([#12](https://github.com/shiftonelabs/laravel-cascade-deletes/pull/12))
22 |
23 | ## [2.0.1] - 2024-09-22
24 | ### Changed
25 | - Updated CI configs to add support for Laravel 11 and PHP 8.3.
26 | - Updated readme with new version information.
27 |
28 | ### Fixed
29 | - Fixed `morphMany()` relationship typo in example code in readme. ([#9](https://github.com/shiftonelabs/laravel-cascade-deletes/pull/9))
30 |
31 | ## [2.0.0] - 2023-03-27
32 | ### Removed
33 | - Removed support for Laravel 4.1 - Laravel 8.x. These are all EOL and will never change, so version 1.0.3 will always work for them.
34 | - Removed support for PHP 5.5 - PHP 7.4. These are all EOL and will never change, so version 1.0.3 will always work for them.
35 |
36 | ### Changed
37 | - Updated package dependencies to support new minimum Laravel and PHP versions.
38 | - Updated CI configs to support new minimum Laravel and PHP versions.
39 | - Updated the README to reflect the new version changes.
40 |
41 | ## [1.0.3] - 2023-03-24
42 | ### Changed
43 | - Converted CI from Travis CI to Github Actions.
44 | - Updated CI config to stop running tests in Scrutinizer.
45 |
46 | ## [1.0.2] - 2023-03-23
47 | ### Changed
48 | - Updated readme to make copying the composer command easier. ([#8](https://github.com/shiftonelabs/laravel-cascade-deletes/pull/8))
49 | - Updated readme with new version information.
50 | - Updated tense in changelog.
51 |
52 | ## [1.0.1] - 2020-04-02
53 | ### Added
54 | - New changelog.
55 |
56 | ### Changed
57 | - Updated tests to work with all supported Laravel versions.
58 | - Updated CI configs for increased test coverage across versions.
59 | - Small code cleanup items across the code base.
60 | - Updated readme with new version information.
61 | - Sort the packages in composer.json.
62 |
63 | ### Fixed
64 | - Fix count of soft deleted records to work with changes in Laravel >= 5.5.
65 |
66 | ## 1.0.0 - 2016-12-08
67 | ### Added
68 | - Initial release!
69 |
70 | [Unreleased]: https://github.com/shiftonelabs/laravel-cascade-deletes/compare/2.0.2...HEAD
71 | [2.0.2]: https://github.com/shiftonelabs/laravel-cascade-deletes/compare/2.0.1...2.0.2
72 | [2.0.1]: https://github.com/shiftonelabs/laravel-cascade-deletes/compare/2.0.0...2.0.1
73 | [2.0.0]: https://github.com/shiftonelabs/laravel-cascade-deletes/compare/1.0.3...2.0.0
74 | [1.0.3]: https://github.com/shiftonelabs/laravel-cascade-deletes/compare/1.0.2...1.0.3
75 | [1.0.2]: https://github.com/shiftonelabs/laravel-cascade-deletes/compare/1.0.1...1.0.2
76 | [1.0.1]: https://github.com/shiftonelabs/laravel-cascade-deletes/compare/1.0.0...1.0.1
77 |
--------------------------------------------------------------------------------
/tests/TraitTest.php:
--------------------------------------------------------------------------------
1 | assertNotEmpty($user->getCascadeDeletes());
17 | }
18 |
19 | public function testCanSetCascadeDeletesProperty()
20 | {
21 | $user = new User();
22 | $newDeletes = ['new', 'deletes'];
23 |
24 | $user->setCascadeDeletes($newDeletes);
25 |
26 | $this->assertEquals($newDeletes, $user->getCascadeDeletes());
27 | }
28 |
29 | public function testGetRelationNamesReturnsArrayFromArray()
30 | {
31 | $user = new User();
32 |
33 | $names = $user->getCascadeDeletesRelationNames();
34 |
35 | $this->assertIsArray($names);
36 | }
37 |
38 | public function testGetRelationNamesReturnsPopulatedArrayFromArray()
39 | {
40 | $user = new User();
41 |
42 | $names = $user->getCascadeDeletesRelationNames();
43 |
44 | $this->assertNotEmpty($names);
45 | }
46 |
47 | public function testGetRelationNamesReturnsArrayFromString()
48 | {
49 | $user = new User();
50 | $user->setCascadeDeletes('string_value');
51 |
52 | $names = $user->getCascadeDeletesRelationNames();
53 |
54 | $this->assertIsArray($names);
55 | }
56 |
57 | public function testGetRelationNamesReturnsPopulatedArrayFromString()
58 | {
59 | $user = new User();
60 | $user->setCascadeDeletes('string_value');
61 |
62 | $names = $user->getCascadeDeletesRelationNames();
63 |
64 | $this->assertNotEmpty($names);
65 | }
66 |
67 | public function testGetRelationNamesReturnsArrayFromNonEmptyValue()
68 | {
69 | $user = new User();
70 | $user->setCascadeDeletes(1234);
71 |
72 | $names = $user->getCascadeDeletesRelationNames();
73 |
74 | $this->assertIsArray($names);
75 | }
76 |
77 | public function testGetRelationNamesReturnsPopulatedArrayFromNonEmptyValue()
78 | {
79 | $user = new User();
80 | $user->setCascadeDeletes(1234);
81 |
82 | $names = $user->getCascadeDeletesRelationNames();
83 |
84 | $this->assertNotEmpty($names);
85 | }
86 |
87 | public function testGetRelationNamesReturnsArrayFromEmptyValue()
88 | {
89 | $user = new User();
90 | $user->setCascadeDeletes(null);
91 |
92 | $names = $user->getCascadeDeletesRelationNames();
93 |
94 | $this->assertIsArray($names);
95 | }
96 |
97 | public function testGetRelationNamesReturnsEmptyArrayFromEmptyValue()
98 | {
99 | $user = new User();
100 | $user->setCascadeDeletes(null);
101 |
102 | $names = $user->getCascadeDeletesRelationNames();
103 |
104 | $this->assertEmpty($names);
105 | }
106 |
107 | public function testGetRelationsReturnsRelationObjectsForValidNames()
108 | {
109 | $user = new User();
110 | $user->setCascadeDeletes(['friends', 'posts', 'photos']);
111 | $expected = [
112 | 'friends' => $user->friends(),
113 | 'posts' => $user->posts(),
114 | 'photos' => $user->photos(),
115 | ];
116 |
117 | $relations = $user->getCascadeDeletesRelations();
118 |
119 | $this->assertEquals($expected, $relations);
120 | }
121 |
122 | public function testGetRelationsReturnsNullForInvalidNames()
123 | {
124 | $user = new User();
125 | $user->setCascadeDeletes(['friends', 'asdf', 1234]);
126 | $expected = [
127 | 'friends' => $user->friends(),
128 | 'asdf' => null,
129 | 1234 => null,
130 | ];
131 |
132 | $relations = $user->getCascadeDeletesRelations();
133 |
134 | $this->assertEquals($expected, $relations);
135 | }
136 |
137 | public function testGetRelationsExcludesEmptyNames()
138 | {
139 | $user = new User();
140 | $user->setCascadeDeletes(['friends', '', 0, null, 'posts']);
141 | $expected = [
142 | 'friends' => $user->friends(),
143 | 'posts' => $user->posts(),
144 | ];
145 |
146 | $relations = $user->getCascadeDeletesRelations();
147 |
148 | $this->assertEquals($expected, $relations);
149 | }
150 |
151 | public function testGetInvalidRelationsReturnsInvalidNames()
152 | {
153 | $user = new User();
154 | $user->setCascadeDeletes(['friends', 'asdf', 1234]);
155 | $expected = ['asdf', 1234];
156 |
157 | $names = $user->getInvalidCascadeDeletesRelations();
158 |
159 | $this->assertEquals($expected, $names);
160 | }
161 |
162 | public function testGetInvalidRelationsExcludesEmptyNames()
163 | {
164 | $user = new User();
165 | $user->setCascadeDeletes(['asdf', '', 0, null, 1234]);
166 | $expected = ['asdf', 1234];
167 |
168 | $names = $user->getInvalidCascadeDeletesRelations();
169 |
170 | $this->assertEquals($expected, $names);
171 | }
172 |
173 | public function testIsForceDeletingReturnsTrueWhenForceDeleting()
174 | {
175 | $user = new SoftUser();
176 |
177 | $this->setRestrictedValue($user, 'forceDeleting', true);
178 |
179 | $this->assertTrue($user->isCascadeDeletesForceDeleting());
180 | }
181 |
182 | public function testIsForceDeletingReturnsFalseWhenNotForceDeleting()
183 | {
184 | $user = new SoftUser();
185 |
186 | $this->assertFalse($user->isCascadeDeletesForceDeleting());
187 | }
188 |
189 | public function testGetCascadeDeletesRelationQueryReturnsRelation()
190 | {
191 | $user = new User();
192 |
193 | $query = $user->getCascadeDeletesRelationQuery($user->getCascadeDeletesRelationNames()[0]);
194 |
195 | $this->assertInstanceOf(Relation::class, $query);
196 | }
197 |
198 | public function testCascadeDeletesRelationQueryExcludesTrashedWhenNotForceDeleting()
199 | {
200 | $user = new SoftUser();
201 |
202 | $query = $user->getCascadeDeletesRelationQuery($user->getCascadeDeletesRelationNames()[0])->getQuery();
203 |
204 | $this->assertNotContains(SoftDeletingScope::class, $query->removedScopes());
205 | }
206 |
207 | public function testCascadeDeletesRelationQueryIncludesTrashedWhenForceDeleting()
208 | {
209 | $user = new SoftUser();
210 |
211 | $this->setRestrictedValue($user, 'forceDeleting', true);
212 |
213 | $query = $user->getCascadeDeletesRelationQuery($user->getCascadeDeletesRelationNames()[0])->getQuery();
214 |
215 | $this->assertContains(SoftDeletingScope::class, $query->removedScopes());
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/src/CascadesDeletes.php:
--------------------------------------------------------------------------------
1 | getConnectionResolver()->transaction(function () use ($model) {
27 |
28 | $relations = $model->getCascadeDeletesRelations();
29 |
30 | if ($invalidRelations = $model->getInvalidCascadeDeletesRelations($relations)) {
31 | throw new LogicException(sprintf('[%s]: invalid relationship(s) for cascading deletes. Relationship method(s) [%s] must return an object of type Illuminate\Database\Eloquent\Relations\Relation.', static::class, implode(', ', $invalidRelations)));
32 | }
33 |
34 | $deleteMethod = $model->isCascadeDeletesForceDeleting() ? 'forceDelete' : 'delete';
35 |
36 | foreach ($relations as $relationName => $relation) {
37 | $expected = 0;
38 | $deleted = 0;
39 |
40 | if ($relation instanceof BelongsToMany) {
41 | // Process the many-to-many relationships on the model.
42 | // These relationships should not delete the related
43 | // record, but should just detach from each other.
44 |
45 | $expected = $model->getCascadeDeletesRelationQuery($relationName)->count();
46 |
47 | $deleted = $model->getCascadeDeletesRelationQuery($relationName)->detach();
48 | } elseif ($relation instanceof HasOneOrMany) {
49 | // Process the one-to-one and one-to-many relationships
50 | // on the model. These relationships should actually
51 | // delete the related records from the database.
52 |
53 | $children = $model->getCascadeDeletesRelationQuery($relationName)->get();
54 |
55 | // To protect against potential relationship defaults,
56 | // filter out any children that may not actually be
57 | // Model instances, or that don't actually exist.
58 | $children = $children->filter(function ($child) {
59 | return $child instanceof Model && $child->exists;
60 | })->all();
61 |
62 | $expected = count($children);
63 |
64 | foreach ($children as $child) {
65 | // Delete the record using the proper method.
66 | $deleted += $child->$deleteMethod();
67 | }
68 | } else {
69 | // Not all relationship types make sense for cascading. As an
70 | // example, for a BelongsTo relationship, it does not make
71 | // sense to delete the parent when the child is deleted.
72 | throw new LogicException(sprintf('[%s]: error occurred deleting [%s]. Relation type [%s] not handled.', static::class, $relationName, get_class($relation)));
73 | }
74 |
75 | if ($deleted < $expected) {
76 | throw new LogicException(sprintf('[%s]: error occurred deleting [%s]. Only deleted [%d] out of [%d] records.', static::class, $relationName, $deleted, $expected));
77 | }
78 | }
79 | });
80 | });
81 | }
82 |
83 | /**
84 | * Get the value of the cascadeDeletes attribute, if it exists.
85 | *
86 | * @return mixed
87 | */
88 | public function getCascadeDeletes()
89 | {
90 | return property_exists($this, 'cascadeDeletes') ? $this->cascadeDeletes : [];
91 | }
92 |
93 | /**
94 | * Set the cascadeDeletes attribute.
95 | *
96 | * @param mixed $cascadeDeletes
97 | *
98 | * @return void
99 | */
100 | public function setCascadeDeletes($cascadeDeletes)
101 | {
102 | $this->cascadeDeletes = $cascadeDeletes;
103 | }
104 |
105 | /**
106 | * Get an array of cascading relation names.
107 | *
108 | * @return array
109 | */
110 | public function getCascadeDeletesRelationNames()
111 | {
112 | $deletes = $this->getCascadeDeletes();
113 |
114 | return array_filter(is_array($deletes) ? $deletes : [$deletes]);
115 | }
116 |
117 | /**
118 | * Get an array of the cascading relation names mapped to their relation types.
119 | *
120 | * @return array
121 | */
122 | public function getCascadeDeletesRelations()
123 | {
124 | $names = $this->getCascadeDeletesRelationNames();
125 |
126 | return array_combine($names, array_map(function ($name) {
127 | $relation = method_exists($this, $name) ? $this->$name() : null;
128 |
129 | return $relation instanceof Relation ? $relation : null;
130 | }, $names));
131 | }
132 |
133 | /**
134 | * Get an array of the invalid cascading relation names.
135 | *
136 | * @param array|null $relations
137 | *
138 | * @return array
139 | */
140 | public function getInvalidCascadeDeletesRelations(?array $relations = null)
141 | {
142 | // This will get the array keys for any item in the array where the
143 | // value is null. If the value is null, that means that the name
144 | // of the relation provided does not return a Relation object.
145 | return array_keys($relations ?: $this->getCascadeDeletesRelations(), null);
146 | }
147 |
148 | /**
149 | * Get the relationship query to use for the specified relation.
150 | *
151 | * @param string $relation
152 | *
153 | * @return \Illuminate\Database\Eloquent\Relations\Relation
154 | */
155 | public function getCascadeDeletesRelationQuery($relation)
156 | {
157 | $query = $this->$relation();
158 |
159 | // If this is a force delete and the related model is using soft deletes,
160 | // we need to use the withTrashed() scope on the relationship query to
161 | // ensure all related records, plus soft deleted, are force deleted.
162 | if ($this->isCascadeDeletesForceDeleting() && !is_null($query->getMacro('withTrashed'))) {
163 | $query = $query->withTrashed();
164 | }
165 |
166 | return $query;
167 | }
168 |
169 | /**
170 | * Check if this cascading delete is a force delete.
171 | *
172 | * @return boolean
173 | */
174 | public function isCascadeDeletesForceDeleting()
175 | {
176 | return property_exists($this, 'forceDeleting') && $this->forceDeleting;
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # laravel-cascade-deletes
2 |
3 | [![Latest Version on Packagist][ico-version]][link-packagist]
4 | [![Software License][ico-license]](LICENSE.txt)
5 | [![Build Status][ico-github-actions]][link-github-actions]
6 | [![Coverage Status][ico-scrutinizer]][link-scrutinizer]
7 | [![Quality Score][ico-code-quality]][link-code-quality]
8 | [![Total Downloads][ico-downloads]][link-downloads]
9 |
10 | This Laravel/Lumen package provides application level cascading deletes for the Laravel's Eloquent ORM. When referential integrity is not, or cannot be, enforced at the data storage level, this package makes it easy to set this up at the application level.
11 |
12 | For example, if you are using `SoftDeletes`, or are using polymorphic relationships, these are situations where foreign keys in the database cannot enforce referential integrity, and the application needs to step in. This package can help.
13 |
14 | ## Versions
15 |
16 | This package has been tested on Laravel 4.1 through Laravel 11.x, though it may continue to work on later versions as they are released. This section will be updated to reflect the versions on which the package has actually been tested.
17 |
18 | This readme has been updated to show information for the most currently supported versions (9.x - 11.x). For Laravel 4.1 through Laravel 8.x, view the 1.x branch.
19 |
20 | ## Install
21 |
22 | Via Composer
23 |
24 | ``` bash
25 | composer require shiftonelabs/laravel-cascade-deletes
26 | ```
27 |
28 | ## Usage
29 |
30 | Enabling cascading deletes can be done two ways. Either:
31 | - update your `Model` to extend the `\ShiftOneLabs\LaravelCascadeDeletes\CascadesDeletesModel`, or
32 | - update your `Model` to use the `\ShiftOneLabs\LaravelCascadeDeletes\CascadesDeletes` trait.
33 |
34 | Once that is done, define the `$cascadeDeletes` property on the `Model`. The `$cascadeDeletes` property should be set to an array of the relationships that should be deleted when a parent record is deleted.
35 |
36 | Now, when a parent record is deleted, the defined child records will also be deleted. Furthermore, in the case where a child record also has cascading deletes defined, the delete will cascade down and delete the related records of the child, as well. This will continue on until all children, grandchildren, great grandchildren, etc. are deleted.
37 |
38 | Additionally, all cascading deletes are performed within a transaction. This makes the delete an "all or nothing" event. If, for any reason, a child record could not be deleted, the transaction will rollback and no records will be deleted at all. The `Exception` that caused the child not to be deleted will bubble up to where the `delete()` originally started, and will need to be caught and handled.**
39 |
40 | #### Code Example
41 |
42 | User Model:
43 |
44 | ``` php
45 | hasMany(Post::class);
60 | }
61 |
62 | public function profile()
63 | {
64 | return $this->hasOne(Profile::class);
65 | }
66 |
67 | public function type()
68 | {
69 | return $this->belongsTo(Type::class);
70 | }
71 | }
72 | ```
73 |
74 | Profile Model:
75 |
76 | ``` php
77 | belongsTo(User::class);
92 | }
93 |
94 | public function addresses()
95 | {
96 | return $this->morphMany(Address::class, 'addressable');
97 | }
98 | }
99 | ```
100 |
101 | In the example above, the `CascadesDeletes` trait has been added to the `User` model to enable cascading deletes. Since the user is considered a parent of posts and profiles, these relationships have been added to the `$cascadeDeletes` property. Additionally, the `Profile` model has been set up to delete its related address records.
102 |
103 | Given this setup, when a user record is deleted, all related posts and profile records will be deleted. The delete will also cascade down into the profile record, and it will delete all the addresses related to the profile, as well.
104 |
105 | If any one of the posts, profiles, or addresses fails to be deleted, the transaction will roll back and no records will be deleted, including the original user record.**
106 |
107 | ** Transaction rollback will only occur if the database being used actually supports transactions. Most do, but some do not. For example, the MySQL `InnoDB` engine supports transactions, but the MySQL `MyISAM` engine does not.
108 |
109 | #### SoftDeletes
110 |
111 | This package also works with Models that are setup with `SoftDeletes`.
112 |
113 | When using `SoftDeletes`, the delete method being used will cascade to the rest of the deletes, as well. That is, if you `delete()` a record, all the child records will also use `delete()`; if you `forceDelete()` a record, all the child records will also use `forceDelete()`.
114 |
115 | The deletes will also cross the boundary between soft deletes and hard deletes. In the code example above, the the `User` record was setup to soft delete, but the `Profile` record was not, then when a user is deleted, the `User` record would be soft deleted, but the child `Profile` record would be hard deleted, and vice versa.
116 |
117 | ## Notes
118 |
119 | - The functionality in this package is provided through the `deleting` event on the `Model`. Therefore, in order for the cascading deletes to work, `delete()` must be called on a model instance. Deletes will not cascade if a delete is performed through the query builder. For example, `App\User::where('active', 0)->delete();` will only delete those user records, and will not perform any cascading deletes, since the `delete()` was performed on the query builder and not on a model instance.
120 |
121 | - Do not add a `BelongsTo` relationship to the `$cascadeDeletes` array. This will cause a `LogicException`, and no records will be deleted. This is done as a `BelongsTo` typically represents a child record, and it usually does not make sense to delete a parent record from a child record.
122 |
123 | ## Contributing
124 |
125 | Contributions are welcome. Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
126 |
127 | ## Security
128 |
129 | If you discover any security related issues, please email patrick@shiftonelabs.com instead of using the issue tracker.
130 |
131 | ## Credits
132 |
133 | - [Patrick Carlo-Hickman][link-author]
134 | - [All Contributors][link-contributors]
135 |
136 | ## License
137 |
138 | The MIT License (MIT). Please see [License File](LICENSE.txt) for more information.
139 |
140 | [ico-version]: https://img.shields.io/packagist/v/shiftonelabs/laravel-cascade-deletes.svg?style=flat-square
141 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square
142 | [ico-github-actions]: https://img.shields.io/github/actions/workflow/status/shiftonelabs/laravel-cascade-deletes/.github/workflows/phpunit.yml?style=flat-square
143 | [ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/shiftonelabs/laravel-cascade-deletes.svg?style=flat-square
144 | [ico-code-quality]: https://img.shields.io/scrutinizer/g/shiftonelabs/laravel-cascade-deletes.svg?style=flat-square
145 | [ico-downloads]: https://img.shields.io/packagist/dt/shiftonelabs/laravel-cascade-deletes.svg?style=flat-square
146 |
147 | [link-packagist]: https://packagist.org/packages/shiftonelabs/laravel-cascade-deletes
148 | [link-github-actions]: https://github.com/shiftonelabs/laravel-cascade-deletes/actions
149 | [link-scrutinizer]: https://scrutinizer-ci.com/g/shiftonelabs/laravel-cascade-deletes/code-structure
150 | [link-code-quality]: https://scrutinizer-ci.com/g/shiftonelabs/laravel-cascade-deletes
151 | [link-downloads]: https://packagist.org/packages/shiftonelabs/laravel-cascade-deletes
152 | [link-author]: https://github.com/patrickcarlohickman
153 | [link-contributors]: ../../contributors
154 |
--------------------------------------------------------------------------------
/tests/IntegrationTest.php:
--------------------------------------------------------------------------------
1 | setUpDatabaseConnection();
26 |
27 | $this->createSchema();
28 | }
29 |
30 | protected function createSchema()
31 | {
32 | $this->schema()->create('users', function ($table) {
33 | $table->increments('id');
34 | $table->timestamps();
35 | $table->softDeletes();
36 | $table->string('name')->nullable();
37 | $table->string('email');
38 | });
39 |
40 | $this->schema()->create('friends', function ($table) {
41 | $table->integer('user_id');
42 | $table->integer('friend_id');
43 | });
44 |
45 | $this->schema()->create('posts', function ($table) {
46 | $table->increments('id');
47 | $table->timestamps();
48 | $table->softDeletes();
49 | $table->integer('user_id');
50 | $table->integer('parent_id')->nullable();
51 | $table->string('name');
52 | });
53 |
54 | $this->schema()->create('comments', function ($table) {
55 | $table->increments('id');
56 | $table->timestamps();
57 | $table->softDeletes();
58 | $table->integer('post_id');
59 | $table->integer('user_id');
60 | $table->string('comment');
61 | });
62 |
63 | $this->schema()->create('photos', function ($table) {
64 | $table->increments('id');
65 | $table->timestamps();
66 | $table->softDeletes();
67 | $table->morphs('imageable');
68 | $table->string('name');
69 | });
70 |
71 | $this->schema()->create('invalid_kids', function ($table) {
72 | $table->increments('id');
73 | $table->timestamps();
74 | $table->softDeletes();
75 | $table->morphs('invalidable');
76 | $table->string('name');
77 | });
78 |
79 | $this->schema()->create('profiles', function ($table) {
80 | $table->increments('id');
81 | $table->timestamps();
82 | $table->softDeletes();
83 | $table->integer('user_id');
84 | });
85 | }
86 |
87 | /**
88 | * Tear down run after each test.
89 | *
90 | * @after
91 | */
92 | public function afterTearDown()
93 | {
94 | $this->schema()->dropIfExists('users');
95 | $this->schema()->dropIfExists('friends');
96 | $this->schema()->dropIfExists('posts');
97 | $this->schema()->dropIfExists('comments');
98 | $this->schema()->dropIfExists('photos');
99 | $this->schema()->dropIfExists('invalid_kids');
100 | $this->schema()->dropIfExists('profiles');
101 | }
102 |
103 | public function testInvalidRelationshipThrowsException()
104 | {
105 | $this->expectException(LogicException::class);
106 | $this->expectExceptionMessage('invalid relationship(s) for cascading deletes');
107 |
108 | $user = User::create(['email' => 'user@example.com']);
109 | $user->setCascadeDeletes(['non_existing_relation']);
110 |
111 | $user->delete();
112 | }
113 |
114 | public function testInvalidRelationshipTypeThrowsException()
115 | {
116 | $this->expectException(LogicException::class);
117 | $this->expectExceptionMessageMatches('/Relation type .* not handled/');
118 |
119 | $post = Post::create(['user_id' => 0, 'name' => 'First Post']);
120 | $post->setCascadeDeletes(['user']);
121 |
122 | $post->delete();
123 | }
124 |
125 | public function testNotAllRecordsDeletedThrowsException()
126 | {
127 | $this->expectException(LogicException::class);
128 | $this->expectExceptionMessage('Only deleted [0] out of [1] records');
129 |
130 | $user = User::create(['email' => 'user@example.com']);
131 | $post = $user->permanentPosts()->create(['name' => 'First Post']);
132 | $user->setCascadeDeletes(['permanentPosts']);
133 |
134 | $user->delete();
135 | }
136 |
137 | public function testDeletesCascadeFirstLevel()
138 | {
139 | $user = User::create(['email' => 'user@example.com']);
140 | $user->photos()->create(['name' => 'Avatar 1']);
141 | $user->photos()->create(['name' => 'Avatar 2']);
142 | $friend = $user->friends()->create(['email' => 'friend@example.com']);
143 | $post = $user->posts()->create(['name' => 'First Post']);
144 | $user->comments()->create(['post_id' => $post->id, 'comment' => 'First Comment']);
145 | $user->comments()->create(['post_id' => $post->id, 'comment' => 'Second Comment']);
146 |
147 | $this->assertEquals(7, User::count() + Photo::count() + Post::count() + Comment::count());
148 |
149 | $user->delete();
150 |
151 | $this->assertEquals(1, User::count());
152 | $this->assertEquals(1, User::count() + Photo::count() + Post::count() + Comment::count());
153 | }
154 |
155 | public function testDeletesCascadeSecondLevel()
156 | {
157 | $user = User::create(['email' => 'user@example.com']);
158 | $post = $user->posts()->create(['name' => 'First Post']);
159 | $post->photos()->create(['name' => 'Hero 1']);
160 | $post->photos()->create(['name' => 'Hero 2']);
161 | $childPost = $post->childPosts()->create(['user_id' => $user->id, 'name' => 'First Child Post']);
162 | $post->comments()->create(['user_id' => 0, 'comment' => 'First Comment']);
163 | $post->comments()->create(['user_id' => 0, 'comment' => 'Second Comment']);
164 |
165 | $this->assertEquals(7, User::count() + Photo::count() + Post::count() + Comment::count());
166 |
167 | $user->delete();
168 |
169 | $this->assertEquals(0, User::count() + Photo::count() + Post::count() + Comment::count());
170 | }
171 |
172 | public function testDeletesCascadeLowerLevels()
173 | {
174 | $user = User::create(['email' => 'user@example.com']);
175 | $post = $user->posts()->create(['name' => 'First Post']);
176 | $childPost = $post->childPosts()->create(['user_id' => 0, 'name' => 'First Child Post']);
177 | $grandchildPost = $childPost->childPosts()->create(['user_id' => 0, 'name' => 'First Grandchild Post']);
178 | $greatGrandchildPost = $grandchildPost->childPosts()->create(['user_id' => 0, 'name' => 'First Great Grandchild Post']);
179 |
180 | $this->assertEquals(5, User::count() + Post::count());
181 |
182 | $user->delete();
183 |
184 | $this->assertEquals(0, User::count() + Post::count());
185 | }
186 |
187 | public function testEntireTransactionIsRolledBack()
188 | {
189 | $user = User::create(['email' => 'user@example.com']);
190 | $post = $user->posts()->create(['name' => 'First Post']);
191 | $post->photos()->create(['name' => 'Hero 1']);
192 | $post->photos()->create(['name' => 'Hero 2']);
193 | $invalidKid = $post->invalidKids()->create(['name' => 'First Invalid Kid']);
194 | $post->comments()->create(['user_id' => 0, 'comment' => 'First Comment']);
195 | $post->comments()->create(['user_id' => 0, 'comment' => 'Second Comment']);
196 |
197 | $this->assertEquals(7, User::count() + Photo::count() + Post::count() + Comment::count() + InvalidKid::count());
198 |
199 | try {
200 | $exceptionThrown = false;
201 |
202 | $user->delete();
203 | } catch (LogicException $e) {
204 | $exceptionThrown = true;
205 | }
206 |
207 | $this->assertEquals(7, User::count() + Photo::count() + Post::count() + Comment::count() + InvalidKid::count());
208 | $this->assertTrue($exceptionThrown);
209 | }
210 |
211 | public function testDeletesHiddenRelatedRecords()
212 | {
213 | $user = User::create(['email' => 'user@example.com']);
214 | $user->profile()->create([]);
215 | $user->profile()->create([]);
216 |
217 | $this->assertEquals(3, User::count() + Profile::count());
218 |
219 | $user->delete();
220 |
221 | $this->assertEquals(0, User::count() + Profile::count());
222 | }
223 |
224 | public function testDeletesOnlyRelatedRecords()
225 | {
226 | $user = User::create(['email' => 'user@example.com']);
227 | $user->profile()->create([]);
228 | $user->profile()->create([]);
229 | $user2 = User::create(['email' => 'user2@example.com']);
230 | $user2->profile()->create([]);
231 |
232 | $this->assertEquals(5, User::count() + Profile::count());
233 |
234 | $user->delete();
235 |
236 | $this->assertEquals(2, User::count() + Profile::count());
237 | }
238 |
239 | public function testSoftDeletesCascade()
240 | {
241 | $user = SoftUser::create(['email' => 'user@example.com']);
242 | $user->photos()->create(['name' => 'Avatar 1']);
243 | $user->photos()->create(['name' => 'Avatar 2']);
244 | $friend = $user->friends()->create(['email' => 'friend@example.com']);
245 | $post = $user->posts()->create(['name' => 'First Post']);
246 | $user->comments()->create(['post_id' => $post->id, 'comment' => 'First Comment']);
247 | $user->comments()->create(['post_id' => $post->id, 'comment' => 'Second Comment']);
248 |
249 | $this->assertEquals(7, SoftUser::count() + SoftPost::count() + Photo::count() + Comment::count());
250 |
251 | $user->delete();
252 |
253 | $this->assertEquals(1, SoftUser::count());
254 | $this->assertEquals(0, SoftPost::count());
255 | $this->assertEquals(1, SoftUser::count() + SoftPost::count() + Photo::count() + Comment::count());
256 |
257 | $this->assertEquals(2, SoftUser::withTrashed()->count());
258 | $this->assertEquals(1, SoftPost::withTrashed()->count());
259 | $this->assertEquals(3, SoftUser::withTrashed()->count() + SoftPost::withTrashed()->count() + Photo::count() + Comment::count());
260 | }
261 |
262 | public function testForcedSoftDeletesCascade()
263 | {
264 | $user = SoftUser::create(['email' => 'user@example.com']);
265 | $user->photos()->create(['name' => 'Avatar 1']);
266 | $user->photos()->create(['name' => 'Avatar 2']);
267 | $friend = $user->friends()->create(['email' => 'friend@example.com']);
268 | $post = $user->posts()->create(['name' => 'First Post']);
269 | $user->comments()->create(['post_id' => $post->id, 'comment' => 'First Comment']);
270 | $user->comments()->create(['post_id' => $post->id, 'comment' => 'Second Comment']);
271 |
272 | $this->assertEquals(7, SoftUser::count() + SoftPost::count() + Photo::count() + Comment::count());
273 |
274 | $user->forceDelete();
275 |
276 | $this->assertEquals(1, SoftUser::count());
277 | $this->assertEquals(0, SoftPost::count());
278 | $this->assertEquals(1, SoftUser::count() + SoftPost::count() + Photo::count() + Comment::count());
279 |
280 | $this->assertEquals(1, SoftUser::withTrashed()->count());
281 | $this->assertEquals(0, SoftPost::withTrashed()->count());
282 | $this->assertEquals(1, SoftUser::withTrashed()->count() + SoftPost::withTrashed()->count() + Photo::count() + Comment::count());
283 | }
284 |
285 | public function testSoftDeletesHiddenRelatedRecords()
286 | {
287 | $user = SoftUser::create(['email' => 'user@example.com']);
288 | $user->profile()->create([]);
289 | $user->profile()->create([]);
290 |
291 | $this->assertEquals(3, SoftUser::count() + SoftProfile::count());
292 |
293 | $user->delete();
294 |
295 | $this->assertEquals(0, SoftUser::count() + SoftProfile::count());
296 | $this->assertEquals(3, SoftUser::withTrashed()->count() + SoftProfile::withTrashed()->count());
297 | }
298 |
299 | public function testForcedSoftDeletesHiddenRelatedRecords()
300 | {
301 | $user = SoftUser::create(['email' => 'user@example.com']);
302 | $user->profile()->create([]);
303 | $user->profile()->create([]);
304 |
305 | $this->assertEquals(3, SoftUser::count() + SoftProfile::count());
306 |
307 | $user->forceDelete();
308 |
309 | $this->assertEquals(0, SoftUser::count() + SoftProfile::count());
310 | $this->assertEquals(0, SoftUser::withTrashed()->count() + SoftProfile::withTrashed()->count());
311 | }
312 |
313 | public function testForcedSoftDeletesMixedRelatedRecords()
314 | {
315 | $user = SoftUser::create(['email' => 'user@example.com']);
316 | $user->profile()->create([]);
317 | $user->profile()->first()->delete();
318 |
319 | $this->assertEquals(2, SoftUser::withTrashed()->count() + SoftProfile::withTrashed()->count());
320 |
321 | $user->profile()->create([]);
322 |
323 | $this->assertEquals(2, SoftUser::count() + SoftProfile::count());
324 | $this->assertEquals(3, SoftUser::withTrashed()->count() + SoftProfile::withTrashed()->count());
325 |
326 | $user->forceDelete();
327 |
328 | $this->assertEquals(0, SoftUser::count() + SoftProfile::count());
329 | $this->assertEquals(0, SoftUser::withTrashed()->count() + SoftProfile::withTrashed()->count());
330 | }
331 |
332 | public function testSoftDeletesTransactionIsRolledBack()
333 | {
334 | $user = SoftUser::create(['email' => 'user@example.com']);
335 | $post = $user->posts()->create(['name' => 'First Post']);
336 | $post->photos()->create(['name' => 'Hero 1']);
337 | $post->photos()->create(['name' => 'Hero 2']);
338 | $invalidKid = $post->invalidKids()->create(['name' => 'First Invalid Kid']);
339 | $post->comments()->create(['user_id' => 0, 'comment' => 'First Comment']);
340 | $post->comments()->create(['user_id' => 0, 'comment' => 'Second Comment']);
341 |
342 | $this->assertEquals(7, User::count() + Photo::count() + Post::count() + Comment::count() + InvalidKid::count());
343 |
344 | try {
345 | $exceptionThrown = false;
346 |
347 | $user->delete();
348 | } catch (LogicException $e) {
349 | $exceptionThrown = true;
350 | }
351 |
352 | $this->assertEquals(7, User::count() + Photo::count() + Post::count() + Comment::count() + InvalidKid::count());
353 | $this->assertTrue($exceptionThrown);
354 | }
355 |
356 | public function testForcedSoftDeletesTransactionIsRolledBack()
357 | {
358 | $user = SoftUser::create(['email' => 'user@example.com']);
359 | $post = $user->posts()->create(['name' => 'First Post']);
360 | $post->photos()->create(['name' => 'Hero 1']);
361 | $post->photos()->create(['name' => 'Hero 2']);
362 | $invalidKid = $post->invalidKids()->create(['name' => 'First Invalid Kid']);
363 | $post->comments()->create(['user_id' => 0, 'comment' => 'First Comment']);
364 | $post->comments()->create(['user_id' => 0, 'comment' => 'Second Comment']);
365 |
366 | $this->assertEquals(7, User::count() + Photo::count() + Post::count() + Comment::count() + InvalidKid::count());
367 |
368 | try {
369 | $exceptionThrown = false;
370 |
371 | $user->forceDelete();
372 | } catch (LogicException $e) {
373 | $exceptionThrown = true;
374 | }
375 |
376 | $this->assertEquals(7, User::count() + Photo::count() + Post::count() + Comment::count() + InvalidKid::count());
377 | $this->assertTrue($exceptionThrown);
378 | }
379 | }
380 |
--------------------------------------------------------------------------------