├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src └── CascadeUpdates.php └── tests ├── CascadeUpdateIntegrationTest.php └── Entities ├── Author.php ├── Comment.php └── Post.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jeff Madsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Cascade Updates 2 | 3 | ## v1.0.0 4 | 5 | This is basically a fork of https://github.com/michaeldyrynda/laravel-cascade-soft-deletes by Michael Dyrynda. I had the idea to make this for sometime, and after planning out how I wanted it to work I searched around to see what was similar. Michael's library was already well established and worked very similarly to what I had in mind, so in the interest of getting it done quickly over the holidays and sticking to a known format, I just based mine on his work. 6 | 7 | This Trait allows you to update all lower entities in a hierarchy when a top level record is changed. For example, if I have Authors with multiple Posts, and each Post has multiple Comments, I can set a `is_active` flag to false on an Author and have all of their Posts and the related Comments deactivate at the same time. 8 | 9 | ## Code Samples 10 | 11 | ```php 12 | ['is_active']]; 25 | 26 | public function comments() 27 | { 28 | return $this->hasMany(Comment::class); 29 | } 30 | } 31 | ``` 32 | 33 | Now you can update an `App\Post` record, and any associated `App\Comment` records will be update. If the `App\Comment` record implements the `CascadeUpdates` trait as well, it's children will also be updated and so on. 34 | 35 | ##### IMPORTANT: This will only update the same field on all entities. In other words, it is not possible to tell it to update `myField` whenever the `is_active` is changed. 36 | 37 | ```php 38 | $post = App\Post::find($postId) 39 | $post->update(['is_active' => 0]); // Updates the post, which will also trigger the update() method on any comments and their children. 40 | ``` 41 | 42 | ## Installation 43 | 44 | This trait is installed via [Composer](http://getcomposer.org/). To install, simply add to your `composer.json` file: 45 | 46 | ``` 47 | $ composer require jrmadsen67/laravel-cascade-updates 48 | ``` 49 | 50 | ## Support 51 | 52 | If you are having general issues with this package, feel free to contact me on [Twitter](https://twitter.com/codebyjeff). 53 | 54 | If you believe you have found an issue, please report it using the [GitHub issue tracker](https://github.com/jrmadsen67/laravel-cascade-updates/issues), or better yet, fork the repository and submit a pull request. 55 | 56 | If you're using this package, I'd love to hear your thoughts. Thanks! 57 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jrmadsen67/laravel-cascade-updates", 3 | "description": "Cascading model updates for Laravel.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Jeff Madsen", 8 | "email": "jrmadsen67@gmail.com", 9 | "homepage": "http://codebyjeff.com" 10 | }, 11 | { 12 | "name": "Michael Dyrynda (original work this is based on)", 13 | "email": "michael@dyrynda.com.au", 14 | "homepage": "https://dyrynda.com.au" 15 | } 16 | ], 17 | "autoload": { 18 | "psr-4": { 19 | "jrmadsen67\\Database\\Support\\": "src/" 20 | } 21 | }, 22 | "autoload-dev": { 23 | "psr-4": { 24 | "Tests\\": "tests/" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | ./tests/ 13 | 14 | 15 | 16 | 17 | app/ 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/CascadeUpdates.php: -------------------------------------------------------------------------------- 1 | hasInvalidCascadingRelationships()) { 25 | throw new LogicException(sprintf( 26 | '%s [%s] must exist and return an object of type Illuminate\Database\Eloquent\Relations\Relation', 27 | str_plural('Relationship', count($invalidCascadingRelationships)), 28 | join(', ', $invalidCascadingRelationships) 29 | )); 30 | } 31 | 32 | foreach ($model->getActiveCascadingUpdates() as $relationship) { 33 | if ($model->{$relationship} instanceof Model) { 34 | $model->{$relationship}->update($model->hydrateUpdate($relationship)); //@TODO: test this 35 | } else { 36 | foreach ($model->{$relationship} as $child) { 37 | $child->update($model->hydrateUpdate($relationship)); 38 | } 39 | } 40 | } 41 | 42 | }); 43 | } 44 | 45 | 46 | /** 47 | * Determine if the current model has any invalid cascading relationships defined. 48 | * 49 | * A relationship is considered invalid when the method does not exist, or the relationship 50 | * method does not return an instance of Illuminate\Database\Eloquent\Relations\Relation. 51 | * 52 | * @return array 53 | */ 54 | protected function hasInvalidCascadingRelationships() 55 | { 56 | return array_filter($this->getCascadingUpdates(), function ($relationship) { 57 | return ! method_exists($this, $relationship) || ! $this->{$relationship}() instanceof Relation; 58 | }); 59 | } 60 | 61 | /** 62 | * Fetch the defined cascading updates for this model. 63 | * 64 | * @return array 65 | */ 66 | protected function getCascadingUpdates() 67 | { 68 | return isset($this->cascadeUpdates) ? (array) array_keys($this->cascadeUpdates) : []; 69 | } 70 | 71 | /** 72 | * For the cascading updates defined on the model, return only those that are not null. 73 | * 74 | * @return array 75 | */ 76 | protected function getActiveCascadingUpdates() 77 | { 78 | return array_filter($this->getCascadingUpdates(), function ($relationship) { 79 | return ! is_null($this->{$relationship}); 80 | }); 81 | } 82 | 83 | 84 | /** 85 | * @return array 86 | */ 87 | function hydrateUpdate($relationship): array{ 88 | $fieldsForUpdate = $this->cascadeUpdates[$relationship]; 89 | return array_only($this->attributes, $fieldsForUpdate); 90 | } 91 | 92 | // @TODO 93 | public function turnOffCascade(){ 94 | $this->updateCascadeActive = true; 95 | return $this; 96 | } 97 | } -------------------------------------------------------------------------------- /tests/CascadeUpdateIntegrationTest.php: -------------------------------------------------------------------------------- 1 | addConnection([ 13 | 'driver' => 'sqlite', 14 | 'database' => ':memory:', 15 | ]); 16 | $manager->setEventDispatcher(new Dispatcher(new Container())); 17 | $manager->setAsGlobal(); 18 | $manager->bootEloquent(); 19 | $manager->schema()->create('authors', function($table){ 20 | $table->increments('id'); 21 | $table->string('name'); 22 | $table->boolean('is_active')->default(1); 23 | $table->timestamps(); 24 | $table->softDeletes(); 25 | }); 26 | $manager->schema()->create('posts', function($table){ 27 | $table->increments('id'); 28 | $table->integer('author_id')->unsigned()->nullable(); 29 | $table->string('title'); 30 | $table->string('body'); 31 | $table->boolean('is_active')->default(1); 32 | $table->timestamps(); 33 | $table->softDeletes(); 34 | }); 35 | $manager->schema()->create('comments', function($table){ 36 | $table->increments('id'); 37 | $table->integer('post_id')->unsigned(); 38 | $table->string('body'); 39 | $table->boolean('is_active')->default(1); 40 | $table->timestamps(); 41 | }); 42 | } 43 | 44 | /** @test */ 45 | public function it_cascades_updates_when_updating_a_parent_model() 46 | { 47 | $author = Tests\Entities\Author::create([ 48 | 'name' => 'Eli Dyrynda', 49 | 'is_active' => 1 50 | ]); 51 | 52 | $author = $this->attachPostsAndCommentsToAuthor($author); 53 | $this->assertCount(2, $author->posts); 54 | $this->assertCount(2, Tests\Entities\Post::all()); 55 | 56 | $author->update(['is_active' => 0]); 57 | $author->load('posts'); 58 | 59 | $author->posts->each(function($post){ 60 | $post->load('comments'); 61 | $post->each(function($comment){ 62 | $this->assertEquals(0, $comment->is_active); 63 | }); 64 | }); 65 | } 66 | 67 | /** 68 | * Attach some dummy posts (w/ comments) to the given author. 69 | * 70 | * @return void 71 | */ 72 | private function attachPostsAndCommentsToAuthor($author) 73 | { 74 | $author->posts()->saveMany([ 75 | $this->attachCommentsToPost( 76 | Tests\Entities\Post::create([ 77 | 'title' => 'First post', 78 | 'body' => 'This is the first test post', 79 | 'is_active' => 1 80 | ]) 81 | ), 82 | $this->attachCommentsToPost( 83 | Tests\Entities\Post::create([ 84 | 'title' => 'Second post', 85 | 'body' => 'This is the second test post', 86 | 'is_active' => 1 87 | ]) 88 | ), 89 | ]); 90 | 91 | return $author; 92 | } 93 | 94 | /** 95 | * Attach some dummy comments to the given post. 96 | * 97 | * @return void 98 | */ 99 | private function attachCommentsToPost($post) 100 | { 101 | $post->comments()->saveMany([ 102 | new Tests\Entities\Comment(['body' => 'This is the first test comment', 'is_active' => 1]), 103 | new Tests\Entities\Comment(['body' => 'This is the second test comment', 'is_active' => 1]), 104 | new Tests\Entities\Comment(['body' => 'This is the third test comment', 'is_active' => 1]), 105 | ]); 106 | 107 | return $post; 108 | } 109 | 110 | /** @test */ 111 | public function check(){ 112 | $this->assertTrue(true); 113 | } 114 | } -------------------------------------------------------------------------------- /tests/Entities/Author.php: -------------------------------------------------------------------------------- 1 | ['is_active'], 15 | ]; 16 | 17 | protected $fillable = ['name','is_active']; 18 | 19 | public function posts() 20 | { 21 | return $this->hasMany('Tests\Entities\Post'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Entities/Comment.php: -------------------------------------------------------------------------------- 1 | belongsTo('Tests\Entities\Post'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/Entities/Post.php: -------------------------------------------------------------------------------- 1 | ['is_active'], 15 | ]; 16 | 17 | protected $fillable = ['title', 'body', 'is_active']; 18 | 19 | public function comments() 20 | { 21 | return $this->hasMany('Tests\Entities\Comment'); 22 | } 23 | 24 | } 25 | --------------------------------------------------------------------------------