[
  {
    "path": ".github/workflows/phpunit.yml",
    "content": "name: Phpunit\n\non: [push, pull_request]\n\njobs:\n  phpcs:\n    runs-on: ubuntu-latest\n\n    name: phpcs - PHP 8.4\n\n    steps:\n      - name: Checkout repo\n        uses: actions/checkout@v3\n\n      # Setup the PHP version to use.\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: '8.4'\n\n      # Dependencies needed for the shiftonelabs/codesniffer-standard package.\n      - name: Install dependencies\n        run: composer update --prefer-dist --no-interaction\n\n      # Run the phpcs tool.\n      - name: Run phpcs\n        run: ./vendor/bin/phpcs\n\n  tests:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      # Turn off fail-fast so that all jobs will run even when one fails,\n      # and the build will still get marked as failed.\n      fail-fast: false\n\n      matrix:\n        os: [ubuntu-latest]\n        php: ['8.0', '8.1', '8.2', '8.3', '8.4']\n        laravel: ['9.*', '10.*', '11.*']\n        exclude:\n          - php: '8.0'\n            laravel: '10.*'\n          - php: '8.0'\n            laravel: '11.*'\n          - php: '8.1'\n            laravel: '11.*'\n\n    name: tests - PHP ${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.os }}\n\n    steps:\n      - name: Checkout repo\n        uses: actions/checkout@v3\n        with:\n          # We need more than 1 commit to prevent the \"Failed to retrieve\n          # commit parents\" error from the ocular code coverage upload.\n          fetch-depth: 5\n\n      # Setup the PHP version to use for the test and include xdebug to generate the code coverage file.\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: ${{ matrix.php }}\n          coverage: xdebug\n\n      # Setup the required packages for the version being tested and install the packages\n      - name: Install dependencies\n        run: |\n          COMPOSER_MEMORY_LIMIT=-1 composer require \"illuminate/database:${{ matrix.laravel }}\" --no-update\n          composer update --prefer-dist --no-interaction\n\n      # Run the unit tests and generate the code coverage file.\n      - name: Run phpunit tests\n        run: |\n          PHPUNIT_CONFIG=\"\"\n          ( [[ -z \"${PHPUNIT_CONFIG}\" ]] && [[ \"${{ matrix.php }}\" == \"8.0\" ]] ) && PHPUNIT_CONFIG=\"--configuration phpunit.9.xml\"\n          ( [[ -z \"${PHPUNIT_CONFIG}\" ]] && [[ \"${{ matrix.php }}\" == \"8.4\" ]] && ( [[ \"${{ matrix.laravel }}\" == \"9.*\" ]] || [[ \"${{ matrix.laravel }}\" == \"10.*\" ]] ) ) && PHPUNIT_CONFIG=\"--configuration phpunit.10.ignore.deprecations.xml\"\n          ./vendor/bin/phpunit ${PHPUNIT_CONFIG} --coverage-clover ./clover.xml\n\n      # Send the code coverage file regardless of the tests passing or failing.\n      - name: Send coverage\n        if: success() || failure()\n        run: |\n          composer global require scrutinizer/ocular\n          ~/.composer/vendor/bin/ocular code-coverage:upload --format=php-clover ./clover.xml\n"
  },
  {
    "path": ".gitignore",
    "content": "/vendor/\ncomposer.lock"
  },
  {
    "path": ".scrutinizer.yml",
    "content": "build:\n    nodes:\n        analysis:\n            environment:\n                php: 8.3.11\n            tests:\n                override:\n                    - php-scrutinizer-run\n                    - phpcs-run\n\nfilter:\n    excluded_paths: [tests/*]\n\ntools:\n    external_code_coverage:\n        timeout: 5400\n        runs: 12\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n## [2.0.2] - 2024-12-14\n### Added\n- Added new phpunit config for older PHP version.\n- Added new phpunit config for incompatible PHP/Laravel versions.\n\n### Changed\n- Updated phpunit config to latest version.\n- Updated phpunit config to ensure tests fail on warnings, notices, and deprecations.\n- Updated Github Actions to use different phpunit configs.\n- Updated CI configs to add support for PHP 8.4.\n\n### Fixed\n- Fixed deprecation notice in PHP 8.4. ([#12](https://github.com/shiftonelabs/laravel-cascade-deletes/pull/12))\n\n## [2.0.1] - 2024-09-22\n### Changed\n- Updated CI configs to add support for Laravel 11 and PHP 8.3.\n- Updated readme with new version information.\n\n### Fixed\n- Fixed `morphMany()` relationship typo in example code in readme. ([#9](https://github.com/shiftonelabs/laravel-cascade-deletes/pull/9))\n\n## [2.0.0] - 2023-03-27\n### Removed\n- 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.\n- 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.\n\n### Changed\n- Updated package dependencies to support new minimum Laravel and PHP versions.\n- Updated CI configs to support new minimum Laravel and PHP versions.\n- Updated the README to reflect the new version changes.\n\n## [1.0.3] - 2023-03-24\n### Changed\n- Converted CI from Travis CI to Github Actions.\n- Updated CI config to stop running tests in Scrutinizer.\n\n## [1.0.2] - 2023-03-23\n### Changed\n- Updated readme to make copying the composer command easier. ([#8](https://github.com/shiftonelabs/laravel-cascade-deletes/pull/8))\n- Updated readme with new version information.\n- Updated tense in changelog.\n\n## [1.0.1] - 2020-04-02\n### Added\n- New changelog.\n\n### Changed\n- Updated tests to work with all supported Laravel versions.\n- Updated CI configs for increased test coverage across versions.\n- Small code cleanup items across the code base.\n- Updated readme with new version information.\n- Sort the packages in composer.json.\n\n### Fixed\n- Fix count of soft deleted records to work with changes in Laravel >= 5.5.\n\n## 1.0.0 - 2016-12-08\n### Added\n- Initial release!\n\n[Unreleased]: https://github.com/shiftonelabs/laravel-cascade-deletes/compare/2.0.2...HEAD\n[2.0.2]: https://github.com/shiftonelabs/laravel-cascade-deletes/compare/2.0.1...2.0.2\n[2.0.1]: https://github.com/shiftonelabs/laravel-cascade-deletes/compare/2.0.0...2.0.1\n[2.0.0]: https://github.com/shiftonelabs/laravel-cascade-deletes/compare/1.0.3...2.0.0\n[1.0.3]: https://github.com/shiftonelabs/laravel-cascade-deletes/compare/1.0.2...1.0.3\n[1.0.2]: https://github.com/shiftonelabs/laravel-cascade-deletes/compare/1.0.1...1.0.2\n[1.0.1]: https://github.com/shiftonelabs/laravel-cascade-deletes/compare/1.0.0...1.0.1\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nContributions are **welcome** and will be fully **credited**.\n\nWe accept contributions via Pull Requests on [Github](https://github.com/shiftonelabs/laravel-cascade-deletes).\n\n\n## Pull Requests\n\n- **[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).\n\n- **Add tests!** - Your patch won't be accepted if it doesn't have tests (within reason).\n\n- **Document any change in behavior** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.\n\n- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option.\n\n- **Create feature branches** - Don't ask us to pull from your master branch.\n\n- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.\n\n- **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.\n\n\n**Happy coding**!\n"
  },
  {
    "path": "LICENSE.md",
    "content": "The MIT License (MIT)\n\nCopyright (c) Patrick Carlo-Hickman\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# laravel-cascade-deletes\n\n[![Latest Version on Packagist][ico-version]][link-packagist]\n[![Software License][ico-license]](LICENSE.txt)\n[![Build Status][ico-github-actions]][link-github-actions]\n[![Coverage Status][ico-scrutinizer]][link-scrutinizer]\n[![Quality Score][ico-code-quality]][link-code-quality]\n[![Total Downloads][ico-downloads]][link-downloads]\n\nThis 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.\n\nFor 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.\n\n## Versions\n\nThis 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.\n\nThis 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.\n\n## Install\n\nVia Composer\n\n``` bash\ncomposer require shiftonelabs/laravel-cascade-deletes\n```\n\n## Usage\n\nEnabling cascading deletes can be done two ways. Either:\n- update your `Model` to extend the `\\ShiftOneLabs\\LaravelCascadeDeletes\\CascadesDeletesModel`, or\n- update your `Model` to use the `\\ShiftOneLabs\\LaravelCascadeDeletes\\CascadesDeletes` trait.\n\nOnce 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.\n\nNow, 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.\n\nAdditionally, 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.**\n\n#### Code Example\n\nUser Model:\n\n``` php\n<?php\n\nnamespace App;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse ShiftOneLabs\\LaravelCascadeDeletes\\CascadesDeletes;\n\nclass User extends Model {\n    use CascadesDeletes;\n\n    protected $cascadeDeletes = ['posts', 'profile'];\n\n    public function posts()\n    {\n        return $this->hasMany(Post::class);\n    }\n\n    public function profile()\n    {\n        return $this->hasOne(Profile::class);\n    }\n\n    public function type()\n    {\n        return $this->belongsTo(Type::class);\n    }\n}\n```\n\nProfile Model:\n\n``` php\n<?php\n\nnamespace App;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse ShiftOneLabs\\LaravelCascadeDeletes\\CascadesDeletes;\n\nclass Profile extends Model {\n    use CascadesDeletes;\n\n    protected $cascadeDeletes = ['addresses'];\n\n    public function user()\n    {\n        return $this->belongsTo(User::class);\n    }\n\n    public function addresses()\n    {\n        return $this->morphMany(Address::class, 'addressable');\n    }\n}\n```\n\nIn 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.\n\nGiven 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.\n\nIf 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.**\n\n** 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.\n\n#### SoftDeletes\n\nThis package also works with Models that are setup with `SoftDeletes`.\n\nWhen 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()`.\n\nThe 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.\n\n## Notes\n\n- 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.\n\n- 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.\n\n## Contributing\n\nContributions are welcome. Please see [CONTRIBUTING](CONTRIBUTING.md) for details.\n\n## Security\n\nIf you discover any security related issues, please email patrick@shiftonelabs.com instead of using the issue tracker.\n\n## Credits\n\n- [Patrick Carlo-Hickman][link-author]\n- [All Contributors][link-contributors]\n\n## License\n\nThe MIT License (MIT). Please see [License File](LICENSE.txt) for more information.\n\n[ico-version]: https://img.shields.io/packagist/v/shiftonelabs/laravel-cascade-deletes.svg?style=flat-square\n[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square\n[ico-github-actions]: https://img.shields.io/github/actions/workflow/status/shiftonelabs/laravel-cascade-deletes/.github/workflows/phpunit.yml?style=flat-square\n[ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/shiftonelabs/laravel-cascade-deletes.svg?style=flat-square\n[ico-code-quality]: https://img.shields.io/scrutinizer/g/shiftonelabs/laravel-cascade-deletes.svg?style=flat-square\n[ico-downloads]: https://img.shields.io/packagist/dt/shiftonelabs/laravel-cascade-deletes.svg?style=flat-square\n\n[link-packagist]: https://packagist.org/packages/shiftonelabs/laravel-cascade-deletes\n[link-github-actions]: https://github.com/shiftonelabs/laravel-cascade-deletes/actions\n[link-scrutinizer]: https://scrutinizer-ci.com/g/shiftonelabs/laravel-cascade-deletes/code-structure\n[link-code-quality]: https://scrutinizer-ci.com/g/shiftonelabs/laravel-cascade-deletes\n[link-downloads]: https://packagist.org/packages/shiftonelabs/laravel-cascade-deletes\n[link-author]: https://github.com/patrickcarlohickman\n[link-contributors]: ../../contributors\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"shiftonelabs/laravel-cascade-deletes\",\n    \"description\": \"Adds application level cascading deletes to Eloquent Models.\",\n    \"keywords\": [\"laravel\", \"lumen\", \"eloquent\", \"model\", \"cascade\", \"deletes\"],\n    \"homepage\": \"https://github.com/shiftonelabs/laravel-cascade-deletes\",\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Patrick Carlo-Hickman\",\n            \"email\": \"patrick@shiftonelabs.com\"\n        }\n    ],\n    \"support\": {\n        \"issues\": \"https://github.com/shiftonelabs/laravel-cascade-deletes/issues\",\n        \"source\": \"https://github.com/shiftonelabs/laravel-cascade-deletes\"\n    },\n    \"require\": {\n        \"php\": \">=8.0.2\",\n        \"illuminate/database\": \">=9.0\",\n        \"illuminate/events\": \">=9.0\"\n    },\n    \"require-dev\": {\n        \"mockery/mockery\": \"~1.3\",\n        \"phpunit/phpunit\": \"~9.3 || ~10.0\",\n        \"shiftonelabs/codesniffer-standard\": \"0.*\",\n        \"squizlabs/php_codesniffer\": \"3.*\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"ShiftOneLabs\\\\LaravelCascadeDeletes\\\\\": \"src/\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"ShiftOneLabs\\\\LaravelCascadeDeletes\\\\Tests\\\\\": \"tests/\"\n        },\n        \"files\": [\n            \"tests/helpers.php\"\n        ]\n    },\n    \"config\": {\n        \"sort-packages\": true,\n        \"allow-plugins\": {\n            \"kylekatarnls/update-helper\": false\n        }\n    },\n    \"minimum-stability\": \"stable\"\n}\n"
  },
  {
    "path": "phpcs.xml",
    "content": "<?xml version=\"1.0\"?>\r\n<ruleset name=\"MYPSR2\">\r\n    <description>Base PSR-2 with a few modifications.</description>\r\n\r\n    <file>src</file>\r\n    <file>tests</file>\r\n\r\n    <rule ref=\"PSR2\">\r\n        <exclude name=\"Generic.Files.LineLength\" />\r\n    </rule>\r\n\r\n    <rule ref=\"PSR1.Methods.CamelCapsMethodName.NotCamelCaps\">\r\n        <exclude-pattern>tests/*</exclude-pattern>\r\n    </rule>\r\n\r\n    <rule ref=\"PSR1.Classes.ClassDeclaration.MultipleClasses\">\r\n        <exclude-pattern>tests/*</exclude-pattern>\r\n    </rule>\r\n\r\n    <rule ref=\"vendor/shiftonelabs/codesniffer-standard/ShiftOneLabs\" />\r\n</ruleset>\r\n"
  },
  {
    "path": "phpunit.10.ignore.deprecations.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.5/phpunit.xsd\"\n         bootstrap=\"vendor/autoload.php\"\n         colors=\"true\"\n         processIsolation=\"false\"\n         stopOnFailure=\"false\"\n         failOnNotice=\"true\"\n         failOnWarning=\"true\"\n         displayDetailsOnTestsThatTriggerDeprecations=\"true\"\n         displayDetailsOnTestsThatTriggerErrors=\"true\"\n         displayDetailsOnTestsThatTriggerNotices=\"true\"\n         displayDetailsOnTestsThatTriggerWarnings=\"true\"\n         cacheResult=\"false\">\n  <source>\n    <include>\n      <directory suffix=\".php\">./src</directory>\n    </include>\n    <exclude>\n      <file>./src/CascadesDeletesModel.php</file>\n    </exclude>\n  </source>\n  <testsuites>\n    <testsuite name=\"Laravel Cascade Deletes Test Suite\">\n      <directory suffix=\"Test.php\">./tests/</directory>\n    </testsuite>\n  </testsuites>\n</phpunit>\n"
  },
  {
    "path": "phpunit.9.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/9.6/phpunit.xsd\"\n         bootstrap=\"vendor/autoload.php\"\n         convertDeprecationsToExceptions=\"true\"\n         convertNoticesToExceptions=\"true\"\n         convertWarningsToExceptions=\"true\"\n         cacheResult=\"false\">\n  <coverage>\n    <include>\n      <directory suffix=\".php\">./src</directory>\n    </include>\n    <exclude>\n      <file>./src/CascadesDeletesModel.php</file>\n    </exclude>\n  </coverage>\n  <testsuites>\n    <testsuite name=\"Laravel Cascade Deletes Test Suite\">\n      <directory suffix=\"Test.php\">./tests/</directory>\n    </testsuite>\n  </testsuites>\n  <php>\n    <ini name=\"error_reporting\" value=\"-1\" />\n  </php>\n</phpunit>\n"
  },
  {
    "path": "phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.5/phpunit.xsd\"\n         bootstrap=\"vendor/autoload.php\"\n         colors=\"true\"\n         processIsolation=\"false\"\n         stopOnFailure=\"false\"\n         failOnDeprecation=\"true\"\n         failOnNotice=\"true\"\n         failOnWarning=\"true\"\n         displayDetailsOnTestsThatTriggerDeprecations=\"true\"\n         displayDetailsOnTestsThatTriggerErrors=\"true\"\n         displayDetailsOnTestsThatTriggerNotices=\"true\"\n         displayDetailsOnTestsThatTriggerWarnings=\"true\"\n         cacheResult=\"false\">\n  <source>\n    <include>\n      <directory suffix=\".php\">./src</directory>\n    </include>\n    <exclude>\n      <file>./src/CascadesDeletesModel.php</file>\n    </exclude>\n  </source>\n  <testsuites>\n    <testsuite name=\"Laravel Cascade Deletes Test Suite\">\n      <directory suffix=\"Test.php\">./tests/</directory>\n    </testsuite>\n  </testsuites>\n</phpunit>\n"
  },
  {
    "path": "src/CascadesDeletes.php",
    "content": "<?php\n\nnamespace ShiftOneLabs\\LaravelCascadeDeletes;\n\nuse LogicException;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\Relation;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOneOrMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\n\ntrait CascadesDeletes\n{\n    /**\n     * Use the boot function to setup model event listeners.\n     *\n     * @return void\n     */\n    public static function bootCascadesDeletes()\n    {\n        // Setup the 'deleting' event listener.\n        static::deleting(function ($model) {\n\n            // Wrap all of the cascading deletes inside of a transaction to make this an\n            // all or nothing operation. Any exceptions thrown inside the transaction\n            // need to bubble up to make sure all transactions will be rolled back.\n            $model->getConnectionResolver()->transaction(function () use ($model) {\n\n                $relations = $model->getCascadeDeletesRelations();\n\n                if ($invalidRelations = $model->getInvalidCascadeDeletesRelations($relations)) {\n                    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)));\n                }\n\n                $deleteMethod = $model->isCascadeDeletesForceDeleting() ? 'forceDelete' : 'delete';\n\n                foreach ($relations as $relationName => $relation) {\n                    $expected = 0;\n                    $deleted = 0;\n\n                    if ($relation instanceof BelongsToMany) {\n                        // Process the many-to-many relationships on the model.\n                        // These relationships should not delete the related\n                        // record, but should just detach from each other.\n\n                        $expected = $model->getCascadeDeletesRelationQuery($relationName)->count();\n\n                        $deleted = $model->getCascadeDeletesRelationQuery($relationName)->detach();\n                    } elseif ($relation instanceof HasOneOrMany) {\n                        // Process the one-to-one and one-to-many relationships\n                        // on the model. These relationships should actually\n                        // delete the related records from the database.\n\n                        $children = $model->getCascadeDeletesRelationQuery($relationName)->get();\n\n                        // To protect against potential relationship defaults,\n                        // filter out any children that may not actually be\n                        // Model instances, or that don't actually exist.\n                        $children = $children->filter(function ($child) {\n                            return $child instanceof Model && $child->exists;\n                        })->all();\n\n                        $expected = count($children);\n\n                        foreach ($children as $child) {\n                            // Delete the record using the proper method.\n                            $deleted += $child->$deleteMethod();\n                        }\n                    } else {\n                        // Not all relationship types make sense for cascading. As an\n                        // example, for a BelongsTo relationship, it does not make\n                        // sense to delete the parent when the child is deleted.\n                        throw new LogicException(sprintf('[%s]: error occurred deleting [%s]. Relation type [%s] not handled.', static::class, $relationName, get_class($relation)));\n                    }\n\n                    if ($deleted < $expected) {\n                        throw new LogicException(sprintf('[%s]: error occurred deleting [%s]. Only deleted [%d] out of [%d] records.', static::class, $relationName, $deleted, $expected));\n                    }\n                }\n            });\n        });\n    }\n\n    /**\n     * Get the value of the cascadeDeletes attribute, if it exists.\n     *\n     * @return mixed\n     */\n    public function getCascadeDeletes()\n    {\n        return property_exists($this, 'cascadeDeletes') ? $this->cascadeDeletes : [];\n    }\n\n    /**\n     * Set the cascadeDeletes attribute.\n     *\n     * @param  mixed  $cascadeDeletes\n     *\n     * @return void\n     */\n    public function setCascadeDeletes($cascadeDeletes)\n    {\n        $this->cascadeDeletes = $cascadeDeletes;\n    }\n\n    /**\n     * Get an array of cascading relation names.\n     *\n     * @return array\n     */\n    public function getCascadeDeletesRelationNames()\n    {\n        $deletes = $this->getCascadeDeletes();\n\n        return array_filter(is_array($deletes) ? $deletes : [$deletes]);\n    }\n\n    /**\n     * Get an array of the cascading relation names mapped to their relation types.\n     *\n     * @return array\n     */\n    public function getCascadeDeletesRelations()\n    {\n        $names = $this->getCascadeDeletesRelationNames();\n\n        return array_combine($names, array_map(function ($name) {\n            $relation = method_exists($this, $name) ? $this->$name() : null;\n\n            return $relation instanceof Relation ? $relation : null;\n        }, $names));\n    }\n\n    /**\n     * Get an array of the invalid cascading relation names.\n     *\n     * @param  array|null  $relations\n     *\n     * @return array\n     */\n    public function getInvalidCascadeDeletesRelations(?array $relations = null)\n    {\n        // This will get the array keys for any item in the array where the\n        // value is null. If the value is null, that means that the name\n        // of the relation provided does not return a Relation object.\n        return array_keys($relations ?: $this->getCascadeDeletesRelations(), null);\n    }\n\n    /**\n     * Get the relationship query to use for the specified relation.\n     *\n     * @param  string  $relation\n     *\n     * @return \\Illuminate\\Database\\Eloquent\\Relations\\Relation\n     */\n    public function getCascadeDeletesRelationQuery($relation)\n    {\n        $query = $this->$relation();\n\n        // If this is a force delete and the related model is using soft deletes,\n        // we need to use the withTrashed() scope on the relationship query to\n        // ensure all related records, plus soft deleted, are force deleted.\n        if ($this->isCascadeDeletesForceDeleting() && !is_null($query->getMacro('withTrashed'))) {\n            $query = $query->withTrashed();\n        }\n\n        return $query;\n    }\n\n    /**\n     * Check if this cascading delete is a force delete.\n     *\n     * @return boolean\n     */\n    public function isCascadeDeletesForceDeleting()\n    {\n        return property_exists($this, 'forceDeleting') && $this->forceDeleting;\n    }\n}\n"
  },
  {
    "path": "src/CascadesDeletesModel.php",
    "content": "<?php\n\nnamespace ShiftOneLabs\\LaravelCascadeDeletes;\n\nuse Illuminate\\Database\\Eloquent\\Model;\n\nclass CascadesDeletesModel extends Model\n{\n    use CascadesDeletes;\n}\n"
  },
  {
    "path": "tests/IntegrationTest.php",
    "content": "<?php\n\nnamespace ShiftOneLabs\\LaravelCascadeDeletes\\Tests;\n\nuse LogicException;\nuse ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models\\Post;\nuse ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models\\User;\nuse ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models\\Photo;\nuse ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models\\Comment;\nuse ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models\\Profile;\nuse ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models\\SoftPost;\nuse ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models\\SoftUser;\nuse ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models\\InvalidKid;\nuse ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models\\SoftProfile;\n\nclass IntegrationTest extends TestCase\n{\n    /**\n     * Setup run before each test.\n     *\n     * @before\n     */\n    public function beforeSetup()\n    {\n        $this->setUpDatabaseConnection();\n\n        $this->createSchema();\n    }\n\n    protected function createSchema()\n    {\n        $this->schema()->create('users', function ($table) {\n            $table->increments('id');\n            $table->timestamps();\n            $table->softDeletes();\n            $table->string('name')->nullable();\n            $table->string('email');\n        });\n\n        $this->schema()->create('friends', function ($table) {\n            $table->integer('user_id');\n            $table->integer('friend_id');\n        });\n\n        $this->schema()->create('posts', function ($table) {\n            $table->increments('id');\n            $table->timestamps();\n            $table->softDeletes();\n            $table->integer('user_id');\n            $table->integer('parent_id')->nullable();\n            $table->string('name');\n        });\n\n        $this->schema()->create('comments', function ($table) {\n            $table->increments('id');\n            $table->timestamps();\n            $table->softDeletes();\n            $table->integer('post_id');\n            $table->integer('user_id');\n            $table->string('comment');\n        });\n\n        $this->schema()->create('photos', function ($table) {\n            $table->increments('id');\n            $table->timestamps();\n            $table->softDeletes();\n            $table->morphs('imageable');\n            $table->string('name');\n        });\n\n        $this->schema()->create('invalid_kids', function ($table) {\n            $table->increments('id');\n            $table->timestamps();\n            $table->softDeletes();\n            $table->morphs('invalidable');\n            $table->string('name');\n        });\n\n        $this->schema()->create('profiles', function ($table) {\n            $table->increments('id');\n            $table->timestamps();\n            $table->softDeletes();\n            $table->integer('user_id');\n        });\n    }\n\n    /**\n     * Tear down run after each test.\n     *\n     * @after\n     */\n    public function afterTearDown()\n    {\n        $this->schema()->dropIfExists('users');\n        $this->schema()->dropIfExists('friends');\n        $this->schema()->dropIfExists('posts');\n        $this->schema()->dropIfExists('comments');\n        $this->schema()->dropIfExists('photos');\n        $this->schema()->dropIfExists('invalid_kids');\n        $this->schema()->dropIfExists('profiles');\n    }\n\n    public function testInvalidRelationshipThrowsException()\n    {\n        $this->expectException(LogicException::class);\n        $this->expectExceptionMessage('invalid relationship(s) for cascading deletes');\n\n        $user = User::create(['email' => 'user@example.com']);\n        $user->setCascadeDeletes(['non_existing_relation']);\n\n        $user->delete();\n    }\n\n    public function testInvalidRelationshipTypeThrowsException()\n    {\n        $this->expectException(LogicException::class);\n        $this->expectExceptionMessageMatches('/Relation type .* not handled/');\n\n        $post = Post::create(['user_id' => 0, 'name' => 'First Post']);\n        $post->setCascadeDeletes(['user']);\n\n        $post->delete();\n    }\n\n    public function testNotAllRecordsDeletedThrowsException()\n    {\n        $this->expectException(LogicException::class);\n        $this->expectExceptionMessage('Only deleted [0] out of [1] records');\n\n        $user = User::create(['email' => 'user@example.com']);\n        $post = $user->permanentPosts()->create(['name' => 'First Post']);\n        $user->setCascadeDeletes(['permanentPosts']);\n\n        $user->delete();\n    }\n\n    public function testDeletesCascadeFirstLevel()\n    {\n        $user = User::create(['email' => 'user@example.com']);\n        $user->photos()->create(['name' => 'Avatar 1']);\n        $user->photos()->create(['name' => 'Avatar 2']);\n        $friend = $user->friends()->create(['email' => 'friend@example.com']);\n        $post = $user->posts()->create(['name' => 'First Post']);\n        $user->comments()->create(['post_id' => $post->id, 'comment' => 'First Comment']);\n        $user->comments()->create(['post_id' => $post->id, 'comment' => 'Second Comment']);\n\n        $this->assertEquals(7, User::count() + Photo::count() + Post::count() + Comment::count());\n\n        $user->delete();\n\n        $this->assertEquals(1, User::count());\n        $this->assertEquals(1, User::count() + Photo::count() + Post::count() + Comment::count());\n    }\n\n    public function testDeletesCascadeSecondLevel()\n    {\n        $user = User::create(['email' => 'user@example.com']);\n        $post = $user->posts()->create(['name' => 'First Post']);\n        $post->photos()->create(['name' => 'Hero 1']);\n        $post->photos()->create(['name' => 'Hero 2']);\n        $childPost = $post->childPosts()->create(['user_id' => $user->id, 'name' => 'First Child Post']);\n        $post->comments()->create(['user_id' => 0, 'comment' => 'First Comment']);\n        $post->comments()->create(['user_id' => 0, 'comment' => 'Second Comment']);\n\n        $this->assertEquals(7, User::count() + Photo::count() + Post::count() + Comment::count());\n\n        $user->delete();\n\n        $this->assertEquals(0, User::count() + Photo::count() + Post::count() + Comment::count());\n    }\n\n    public function testDeletesCascadeLowerLevels()\n    {\n        $user = User::create(['email' => 'user@example.com']);\n        $post = $user->posts()->create(['name' => 'First Post']);\n        $childPost = $post->childPosts()->create(['user_id' => 0, 'name' => 'First Child Post']);\n        $grandchildPost = $childPost->childPosts()->create(['user_id' => 0, 'name' => 'First Grandchild Post']);\n        $greatGrandchildPost = $grandchildPost->childPosts()->create(['user_id' => 0, 'name' => 'First Great Grandchild Post']);\n\n        $this->assertEquals(5, User::count() + Post::count());\n\n        $user->delete();\n\n        $this->assertEquals(0, User::count() + Post::count());\n    }\n\n    public function testEntireTransactionIsRolledBack()\n    {\n        $user = User::create(['email' => 'user@example.com']);\n        $post = $user->posts()->create(['name' => 'First Post']);\n        $post->photos()->create(['name' => 'Hero 1']);\n        $post->photos()->create(['name' => 'Hero 2']);\n        $invalidKid = $post->invalidKids()->create(['name' => 'First Invalid Kid']);\n        $post->comments()->create(['user_id' => 0, 'comment' => 'First Comment']);\n        $post->comments()->create(['user_id' => 0, 'comment' => 'Second Comment']);\n\n        $this->assertEquals(7, User::count() + Photo::count() + Post::count() + Comment::count() + InvalidKid::count());\n\n        try {\n            $exceptionThrown = false;\n\n            $user->delete();\n        } catch (LogicException $e) {\n            $exceptionThrown = true;\n        }\n\n        $this->assertEquals(7, User::count() + Photo::count() + Post::count() + Comment::count() + InvalidKid::count());\n        $this->assertTrue($exceptionThrown);\n    }\n\n    public function testDeletesHiddenRelatedRecords()\n    {\n        $user = User::create(['email' => 'user@example.com']);\n        $user->profile()->create([]);\n        $user->profile()->create([]);\n\n        $this->assertEquals(3, User::count() + Profile::count());\n\n        $user->delete();\n\n        $this->assertEquals(0, User::count() + Profile::count());\n    }\n\n    public function testDeletesOnlyRelatedRecords()\n    {\n        $user = User::create(['email' => 'user@example.com']);\n        $user->profile()->create([]);\n        $user->profile()->create([]);\n        $user2 = User::create(['email' => 'user2@example.com']);\n        $user2->profile()->create([]);\n\n        $this->assertEquals(5, User::count() + Profile::count());\n\n        $user->delete();\n\n        $this->assertEquals(2, User::count() + Profile::count());\n    }\n\n    public function testSoftDeletesCascade()\n    {\n        $user = SoftUser::create(['email' => 'user@example.com']);\n        $user->photos()->create(['name' => 'Avatar 1']);\n        $user->photos()->create(['name' => 'Avatar 2']);\n        $friend = $user->friends()->create(['email' => 'friend@example.com']);\n        $post = $user->posts()->create(['name' => 'First Post']);\n        $user->comments()->create(['post_id' => $post->id, 'comment' => 'First Comment']);\n        $user->comments()->create(['post_id' => $post->id, 'comment' => 'Second Comment']);\n\n        $this->assertEquals(7, SoftUser::count() + SoftPost::count() + Photo::count() + Comment::count());\n\n        $user->delete();\n\n        $this->assertEquals(1, SoftUser::count());\n        $this->assertEquals(0, SoftPost::count());\n        $this->assertEquals(1, SoftUser::count() + SoftPost::count() + Photo::count() + Comment::count());\n\n        $this->assertEquals(2, SoftUser::withTrashed()->count());\n        $this->assertEquals(1, SoftPost::withTrashed()->count());\n        $this->assertEquals(3, SoftUser::withTrashed()->count() + SoftPost::withTrashed()->count() + Photo::count() + Comment::count());\n    }\n\n    public function testForcedSoftDeletesCascade()\n    {\n        $user = SoftUser::create(['email' => 'user@example.com']);\n        $user->photos()->create(['name' => 'Avatar 1']);\n        $user->photos()->create(['name' => 'Avatar 2']);\n        $friend = $user->friends()->create(['email' => 'friend@example.com']);\n        $post = $user->posts()->create(['name' => 'First Post']);\n        $user->comments()->create(['post_id' => $post->id, 'comment' => 'First Comment']);\n        $user->comments()->create(['post_id' => $post->id, 'comment' => 'Second Comment']);\n\n        $this->assertEquals(7, SoftUser::count() + SoftPost::count() + Photo::count() + Comment::count());\n\n        $user->forceDelete();\n\n        $this->assertEquals(1, SoftUser::count());\n        $this->assertEquals(0, SoftPost::count());\n        $this->assertEquals(1, SoftUser::count() + SoftPost::count() + Photo::count() + Comment::count());\n\n        $this->assertEquals(1, SoftUser::withTrashed()->count());\n        $this->assertEquals(0, SoftPost::withTrashed()->count());\n        $this->assertEquals(1, SoftUser::withTrashed()->count() + SoftPost::withTrashed()->count() + Photo::count() + Comment::count());\n    }\n\n    public function testSoftDeletesHiddenRelatedRecords()\n    {\n        $user = SoftUser::create(['email' => 'user@example.com']);\n        $user->profile()->create([]);\n        $user->profile()->create([]);\n\n        $this->assertEquals(3, SoftUser::count() + SoftProfile::count());\n\n        $user->delete();\n\n        $this->assertEquals(0, SoftUser::count() + SoftProfile::count());\n        $this->assertEquals(3, SoftUser::withTrashed()->count() + SoftProfile::withTrashed()->count());\n    }\n\n    public function testForcedSoftDeletesHiddenRelatedRecords()\n    {\n        $user = SoftUser::create(['email' => 'user@example.com']);\n        $user->profile()->create([]);\n        $user->profile()->create([]);\n\n        $this->assertEquals(3, SoftUser::count() + SoftProfile::count());\n\n        $user->forceDelete();\n\n        $this->assertEquals(0, SoftUser::count() + SoftProfile::count());\n        $this->assertEquals(0, SoftUser::withTrashed()->count() + SoftProfile::withTrashed()->count());\n    }\n\n    public function testForcedSoftDeletesMixedRelatedRecords()\n    {\n        $user = SoftUser::create(['email' => 'user@example.com']);\n        $user->profile()->create([]);\n        $user->profile()->first()->delete();\n\n        $this->assertEquals(2, SoftUser::withTrashed()->count() + SoftProfile::withTrashed()->count());\n\n        $user->profile()->create([]);\n\n        $this->assertEquals(2, SoftUser::count() + SoftProfile::count());\n        $this->assertEquals(3, SoftUser::withTrashed()->count() + SoftProfile::withTrashed()->count());\n\n        $user->forceDelete();\n\n        $this->assertEquals(0, SoftUser::count() + SoftProfile::count());\n        $this->assertEquals(0, SoftUser::withTrashed()->count() + SoftProfile::withTrashed()->count());\n    }\n\n    public function testSoftDeletesTransactionIsRolledBack()\n    {\n        $user = SoftUser::create(['email' => 'user@example.com']);\n        $post = $user->posts()->create(['name' => 'First Post']);\n        $post->photos()->create(['name' => 'Hero 1']);\n        $post->photos()->create(['name' => 'Hero 2']);\n        $invalidKid = $post->invalidKids()->create(['name' => 'First Invalid Kid']);\n        $post->comments()->create(['user_id' => 0, 'comment' => 'First Comment']);\n        $post->comments()->create(['user_id' => 0, 'comment' => 'Second Comment']);\n\n        $this->assertEquals(7, User::count() + Photo::count() + Post::count() + Comment::count() + InvalidKid::count());\n\n        try {\n            $exceptionThrown = false;\n\n            $user->delete();\n        } catch (LogicException $e) {\n            $exceptionThrown = true;\n        }\n\n        $this->assertEquals(7, User::count() + Photo::count() + Post::count() + Comment::count() + InvalidKid::count());\n        $this->assertTrue($exceptionThrown);\n    }\n\n    public function testForcedSoftDeletesTransactionIsRolledBack()\n    {\n        $user = SoftUser::create(['email' => 'user@example.com']);\n        $post = $user->posts()->create(['name' => 'First Post']);\n        $post->photos()->create(['name' => 'Hero 1']);\n        $post->photos()->create(['name' => 'Hero 2']);\n        $invalidKid = $post->invalidKids()->create(['name' => 'First Invalid Kid']);\n        $post->comments()->create(['user_id' => 0, 'comment' => 'First Comment']);\n        $post->comments()->create(['user_id' => 0, 'comment' => 'Second Comment']);\n\n        $this->assertEquals(7, User::count() + Photo::count() + Post::count() + Comment::count() + InvalidKid::count());\n\n        try {\n            $exceptionThrown = false;\n\n            $user->forceDelete();\n        } catch (LogicException $e) {\n            $exceptionThrown = true;\n        }\n\n        $this->assertEquals(7, User::count() + Photo::count() + Post::count() + Comment::count() + InvalidKid::count());\n        $this->assertTrue($exceptionThrown);\n    }\n}\n"
  },
  {
    "path": "tests/ModelTest.php",
    "content": "<?php\n\nnamespace ShiftOneLabs\\LaravelCascadeDeletes\\Tests;\n\nuse ShiftOneLabs\\LaravelCascadeDeletes\\CascadesDeletes;\nuse ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models\\ExtendedUser;\n\nclass ModelTest extends TestCase\n{\n    public function testModelUsesCascadingDeletesTrait()\n    {\n        $this->assertContains(CascadesDeletes::class, class_uses_recursive(ExtendedUser::class));\n    }\n}\n"
  },
  {
    "path": "tests/Models/Comment.php",
    "content": "<?php\n\nnamespace ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Model;\n\nclass Comment extends Model\n{\n    protected $guarded = [];\n\n    public function user()\n    {\n        return $this->belongsTo(User::class);\n    }\n\n    public function post()\n    {\n        return $this->belongsTo(Post::class);\n    }\n}\n"
  },
  {
    "path": "tests/Models/ExtendedUser.php",
    "content": "<?php\n\nnamespace ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models;\n\nuse ShiftOneLabs\\LaravelCascadeDeletes\\CascadesDeletesModel;\n\nclass ExtendedUser extends CascadesDeletesModel\n{\n    protected $guarded = [];\n\n    protected $cascadeDeletes = ['friends', 'posts', 'photos', 'comments', 'profile'];\n\n    public function friends()\n    {\n        return $this->belongsToMany(User::class, 'friends', 'user_id', 'friend_id');\n    }\n\n    public function posts()\n    {\n        return $this->hasMany(Post::class);\n    }\n\n    public function photos()\n    {\n        return $this->morphMany(Photo::class, 'imageable');\n    }\n\n    public function comments()\n    {\n        return $this->hasMany(Comment::class);\n    }\n\n    public function profile()\n    {\n        return $this->hasOne(Profile::class);\n    }\n\n    public function permanentPosts()\n    {\n        return $this->hasMany(PermanentPost::class);\n    }\n}\n"
  },
  {
    "path": "tests/Models/InvalidKid.php",
    "content": "<?php\n\nnamespace ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse ShiftOneLabs\\LaravelCascadeDeletes\\CascadesDeletes;\n\nclass InvalidKid extends Model\n{\n    use CascadesDeletes;\n\n    protected $guarded = [];\n\n    protected $cascadeDeletes = ['invalidable'];\n\n    public function invalidable()\n    {\n        return $this->morphTo();\n    }\n}\n"
  },
  {
    "path": "tests/Models/PermanentPost.php",
    "content": "<?php\n\nnamespace ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models;\n\nclass PermanentPost extends Post\n{\n    protected $table = 'posts';\n\n    public function user()\n    {\n        return $this->belongsTo(User::class);\n    }\n\n    public function childPosts()\n    {\n        return $this->hasMany(PermanentPost::class, 'parent_id');\n    }\n\n    public function parentPost()\n    {\n        return $this->belongsTo(PermanentPost::class, 'parent_id');\n    }\n\n    public function comments()\n    {\n        return $this->hasMany(Comment::class, 'post_id');\n    }\n\n    public function delete()\n    {\n        return false;\n    }\n}\n"
  },
  {
    "path": "tests/Models/Photo.php",
    "content": "<?php\n\nnamespace ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Model;\n\nclass Photo extends Model\n{\n    protected $guarded = [];\n\n    public function imageable()\n    {\n        return $this->morphTo();\n    }\n}\n"
  },
  {
    "path": "tests/Models/Post.php",
    "content": "<?php\n\nnamespace ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse ShiftOneLabs\\LaravelCascadeDeletes\\CascadesDeletes;\n\nclass Post extends Model\n{\n    use CascadesDeletes;\n\n    protected $guarded = [];\n\n    protected $cascadeDeletes = ['photos', 'childPosts', 'comments', 'invalidKids'];\n\n    public function user()\n    {\n        return $this->belongsTo(User::class);\n    }\n\n    public function photos()\n    {\n        return $this->morphMany(Photo::class, 'imageable');\n    }\n\n    public function childPosts()\n    {\n        return $this->hasMany(Post::class, 'parent_id');\n    }\n\n    public function parentPost()\n    {\n        return $this->belongsTo(Post::class, 'parent_id');\n    }\n\n    public function comments()\n    {\n        return $this->hasMany(Comment::class);\n    }\n\n    public function invalidKids()\n    {\n        return $this->morphMany(InvalidKid::class, 'invalidable');\n    }\n}\n"
  },
  {
    "path": "tests/Models/Profile.php",
    "content": "<?php\n\nnamespace ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Model;\n\nclass Profile extends Model\n{\n    protected $guarded = [];\n\n    public function user()\n    {\n        return $this->belongsTo(User::class);\n    }\n}\n"
  },
  {
    "path": "tests/Models/SoftPost.php",
    "content": "<?php\n\nnamespace ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models;\n\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\n\nclass SoftPost extends Post\n{\n    use SoftDeletes;\n\n    protected $table = 'posts';\n\n    public function user()\n    {\n        return $this->belongsTo(SoftUser::class);\n    }\n\n    public function childPosts()\n    {\n        return $this->hasMany(SoftPost::class, 'parent_id');\n    }\n\n    public function parentPost()\n    {\n        return $this->belongsTo(SoftPost::class, 'parent_id');\n    }\n\n    public function comments()\n    {\n        return $this->hasMany(Comment::class, 'post_id');\n    }\n}\n"
  },
  {
    "path": "tests/Models/SoftProfile.php",
    "content": "<?php\n\nnamespace ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models;\n\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\n\nclass SoftProfile extends Profile\n{\n    use SoftDeletes;\n\n    protected $table = 'profiles';\n\n    public function user()\n    {\n        return $this->belongsTo(User::class);\n    }\n}\n"
  },
  {
    "path": "tests/Models/SoftUser.php",
    "content": "<?php\n\nnamespace ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models;\n\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\n\nclass SoftUser extends User\n{\n    use SoftDeletes;\n\n    protected $table = 'users';\n\n    public function friends()\n    {\n        return $this->belongsToMany(SoftUser::class, 'friends', 'user_id', 'friend_id');\n    }\n\n    public function posts()\n    {\n        return $this->hasMany(SoftPost::class, 'user_id');\n    }\n\n    public function comments()\n    {\n        return $this->hasMany(Comment::class, 'user_id');\n    }\n\n    public function profile()\n    {\n        return $this->hasOne(SoftProfile::class, 'user_id');\n    }\n}\n"
  },
  {
    "path": "tests/Models/User.php",
    "content": "<?php\n\nnamespace ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse ShiftOneLabs\\LaravelCascadeDeletes\\CascadesDeletes;\n\nclass User extends Model\n{\n    use CascadesDeletes;\n\n    protected $guarded = [];\n\n    protected $cascadeDeletes = ['friends', 'posts', 'photos', 'comments', 'profile'];\n\n    public function friends()\n    {\n        return $this->belongsToMany(User::class, 'friends', 'user_id', 'friend_id');\n    }\n\n    public function posts()\n    {\n        return $this->hasMany(Post::class);\n    }\n\n    public function photos()\n    {\n        return $this->morphMany(Photo::class, 'imageable');\n    }\n\n    public function comments()\n    {\n        return $this->hasMany(Comment::class);\n    }\n\n    public function profile()\n    {\n        return $this->hasOne(Profile::class);\n    }\n\n    public function permanentPosts()\n    {\n        return $this->hasMany(PermanentPost::class);\n    }\n}\n"
  },
  {
    "path": "tests/TestCase.php",
    "content": "<?php\n\nnamespace ShiftOneLabs\\LaravelCascadeDeletes\\Tests;\n\nuse ReflectionProperty;\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Container\\Container;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Capsule\\Manager as DB;\nuse PHPUnit\\Framework\\TestCase as PhpunitTestCase;\nuse Mockery\\Adapter\\Phpunit\\MockeryPHPUnitIntegration;\n\nclass TestCase extends PhpunitTestCase\n{\n    /**\n     * Use the integration trait so PHPUnit understands Mockery assertions.\n     */\n    use MockeryPHPUnitIntegration;\n\n    /**\n     * Setup the database connection.\n     *\n     * @return void\n     */\n    public function setUpDatabaseConnection()\n    {\n        $db = new DB();\n\n        $db->addConnection([\n            'driver'    => 'sqlite',\n            'database'  => ':memory:',\n        ]);\n\n        $db->setEventDispatcher(new Dispatcher(new Container()));\n\n        $db->bootEloquent();\n        $db->setAsGlobal();\n\n        // This is required for testing Model events. If this is not done, the\n        // events will only fire on the first test.\n        Model::clearBootedModels();\n    }\n\n    /**\n     * Get a schema builder instance.\n     *\n     * @return \\Illuminate\\Database\\Schema\\Builder\n     */\n    protected function schema($connection = 'default')\n    {\n        return $this->connection($connection)->getSchemaBuilder();\n    }\n\n    /**\n     * Get a database connection instance.\n     *\n     * @return \\Illuminate\\Database\\Connection\n     */\n    protected function connection($connection = 'default')\n    {\n        return Model::getConnectionResolver()->connection($connection);\n    }\n\n    /**\n     * Use reflection to set the value of a restricted (private/protected)\n     * property on an object.\n     *\n     * @param  object  $object\n     * @param  string  $property\n     * @param  mixed  $value\n     *\n     * @return void\n     */\n    protected function setRestrictedValue($object, $property, $value)\n    {\n        $reflectionProperty = new ReflectionProperty($object, $property);\n        $reflectionProperty->setAccessible(true);\n\n        if ($reflectionProperty->isStatic()) {\n            $reflectionProperty->setValue($value);\n        } else {\n            $reflectionProperty->setValue($object, $value);\n        }\n    }\n}\n"
  },
  {
    "path": "tests/TraitTest.php",
    "content": "<?php\n\nnamespace ShiftOneLabs\\LaravelCascadeDeletes\\Tests;\n\nuse Illuminate\\Database\\Eloquent\\SoftDeletingScope;\nuse Illuminate\\Database\\Eloquent\\Relations\\Relation;\nuse ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models\\User;\nuse ShiftOneLabs\\LaravelCascadeDeletes\\Tests\\Models\\SoftUser;\n\nclass TraitTest extends TestCase\n{\n    public function testCanGetCascadeDeletesProperty()\n    {\n        $user = new User();\n\n        $this->assertNotEmpty($user->getCascadeDeletes());\n    }\n\n    public function testCanSetCascadeDeletesProperty()\n    {\n        $user = new User();\n        $newDeletes = ['new', 'deletes'];\n\n        $user->setCascadeDeletes($newDeletes);\n\n        $this->assertEquals($newDeletes, $user->getCascadeDeletes());\n    }\n\n    public function testGetRelationNamesReturnsArrayFromArray()\n    {\n        $user = new User();\n\n        $names = $user->getCascadeDeletesRelationNames();\n\n        $this->assertIsArray($names);\n    }\n\n    public function testGetRelationNamesReturnsPopulatedArrayFromArray()\n    {\n        $user = new User();\n\n        $names = $user->getCascadeDeletesRelationNames();\n\n        $this->assertNotEmpty($names);\n    }\n\n    public function testGetRelationNamesReturnsArrayFromString()\n    {\n        $user = new User();\n        $user->setCascadeDeletes('string_value');\n\n        $names = $user->getCascadeDeletesRelationNames();\n\n        $this->assertIsArray($names);\n    }\n\n    public function testGetRelationNamesReturnsPopulatedArrayFromString()\n    {\n        $user = new User();\n        $user->setCascadeDeletes('string_value');\n\n        $names = $user->getCascadeDeletesRelationNames();\n\n        $this->assertNotEmpty($names);\n    }\n\n    public function testGetRelationNamesReturnsArrayFromNonEmptyValue()\n    {\n        $user = new User();\n        $user->setCascadeDeletes(1234);\n\n        $names = $user->getCascadeDeletesRelationNames();\n\n        $this->assertIsArray($names);\n    }\n\n    public function testGetRelationNamesReturnsPopulatedArrayFromNonEmptyValue()\n    {\n        $user = new User();\n        $user->setCascadeDeletes(1234);\n\n        $names = $user->getCascadeDeletesRelationNames();\n\n        $this->assertNotEmpty($names);\n    }\n\n    public function testGetRelationNamesReturnsArrayFromEmptyValue()\n    {\n        $user = new User();\n        $user->setCascadeDeletes(null);\n\n        $names = $user->getCascadeDeletesRelationNames();\n\n        $this->assertIsArray($names);\n    }\n\n    public function testGetRelationNamesReturnsEmptyArrayFromEmptyValue()\n    {\n        $user = new User();\n        $user->setCascadeDeletes(null);\n\n        $names = $user->getCascadeDeletesRelationNames();\n\n        $this->assertEmpty($names);\n    }\n\n    public function testGetRelationsReturnsRelationObjectsForValidNames()\n    {\n        $user = new User();\n        $user->setCascadeDeletes(['friends', 'posts', 'photos']);\n        $expected = [\n            'friends' => $user->friends(),\n            'posts' => $user->posts(),\n            'photos' => $user->photos(),\n        ];\n\n        $relations = $user->getCascadeDeletesRelations();\n\n        $this->assertEquals($expected, $relations);\n    }\n\n    public function testGetRelationsReturnsNullForInvalidNames()\n    {\n        $user = new User();\n        $user->setCascadeDeletes(['friends', 'asdf', 1234]);\n        $expected = [\n            'friends' => $user->friends(),\n            'asdf' => null,\n            1234 => null,\n        ];\n\n        $relations = $user->getCascadeDeletesRelations();\n\n        $this->assertEquals($expected, $relations);\n    }\n\n    public function testGetRelationsExcludesEmptyNames()\n    {\n        $user = new User();\n        $user->setCascadeDeletes(['friends', '', 0, null, 'posts']);\n        $expected = [\n            'friends' => $user->friends(),\n            'posts' => $user->posts(),\n        ];\n\n        $relations = $user->getCascadeDeletesRelations();\n\n        $this->assertEquals($expected, $relations);\n    }\n\n    public function testGetInvalidRelationsReturnsInvalidNames()\n    {\n        $user = new User();\n        $user->setCascadeDeletes(['friends', 'asdf', 1234]);\n        $expected = ['asdf', 1234];\n\n        $names = $user->getInvalidCascadeDeletesRelations();\n\n        $this->assertEquals($expected, $names);\n    }\n\n    public function testGetInvalidRelationsExcludesEmptyNames()\n    {\n        $user = new User();\n        $user->setCascadeDeletes(['asdf', '', 0, null, 1234]);\n        $expected = ['asdf', 1234];\n\n        $names = $user->getInvalidCascadeDeletesRelations();\n\n        $this->assertEquals($expected, $names);\n    }\n\n    public function testIsForceDeletingReturnsTrueWhenForceDeleting()\n    {\n        $user = new SoftUser();\n\n        $this->setRestrictedValue($user, 'forceDeleting', true);\n\n        $this->assertTrue($user->isCascadeDeletesForceDeleting());\n    }\n\n    public function testIsForceDeletingReturnsFalseWhenNotForceDeleting()\n    {\n        $user = new SoftUser();\n\n        $this->assertFalse($user->isCascadeDeletesForceDeleting());\n    }\n\n    public function testGetCascadeDeletesRelationQueryReturnsRelation()\n    {\n        $user = new User();\n\n        $query = $user->getCascadeDeletesRelationQuery($user->getCascadeDeletesRelationNames()[0]);\n\n        $this->assertInstanceOf(Relation::class, $query);\n    }\n\n    public function testCascadeDeletesRelationQueryExcludesTrashedWhenNotForceDeleting()\n    {\n        $user = new SoftUser();\n\n        $query = $user->getCascadeDeletesRelationQuery($user->getCascadeDeletesRelationNames()[0])->getQuery();\n\n        $this->assertNotContains(SoftDeletingScope::class, $query->removedScopes());\n    }\n\n    public function testCascadeDeletesRelationQueryIncludesTrashedWhenForceDeleting()\n    {\n        $user = new SoftUser();\n\n        $this->setRestrictedValue($user, 'forceDeleting', true);\n\n        $query = $user->getCascadeDeletesRelationQuery($user->getCascadeDeletesRelationNames()[0])->getQuery();\n\n        $this->assertContains(SoftDeletingScope::class, $query->removedScopes());\n    }\n}\n"
  },
  {
    "path": "tests/helpers.php",
    "content": "<?php\n\nif (!function_exists('class_uses_recursive')) {\n    /**\n     * Returns all traits used by a class, its subclasses and trait of their traits.\n     *\n     * @param  object|string  $class\n     *\n     * @return array\n     */\n    function class_uses_recursive($class)\n    {\n        if (is_object($class)) {\n            $class = get_class($class);\n        }\n\n        $results = [];\n\n        foreach (array_merge([$class => $class], class_parents($class)) as $class) {\n            $results += trait_uses_recursive($class);\n        }\n\n        return array_unique($results);\n    }\n}\n\nif (!function_exists('trait_uses_recursive')) {\n    /**\n     * Returns all traits used by a trait and its traits.\n     *\n     * @param  string  $trait\n     *\n     * @return array\n     */\n    function trait_uses_recursive($trait)\n    {\n        $traits = class_uses($trait);\n\n        foreach ($traits as $trait) {\n            $traits += trait_uses_recursive($trait);\n        }\n\n        return $traits;\n    }\n}\n"
  }
]