├── .php-cs-fixer.php ├── LICENSE ├── README.md ├── composer.json └── src ├── Relations ├── BelongsToManyCustom.php └── MorphToManyCustom.php └── Traits ├── ExtendFireModelEventTrait.php ├── ExtendRelationsTrait.php ├── FiresPivotEventsTrait.php └── PivotEventTrait.php /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | in(__DIR__); 5 | 6 | $config = new PhpCsFixer\Config(); 7 | return $config->setRules([ 8 | '@PSR2' => true, 9 | '@Symfony' => true, 10 | ]) 11 | ->setFinder($finder); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Filip Horvat 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 Pivot 2 | 3 | This package introduces new eloquent events for sync(), attach(), detach() or updateExistingPivot() methods on BelongsToMany relation. 4 | 5 | ## Laravel Problems 6 | 7 | In Laravel events are not dispatched when BelongsToMany relation (pivot table) is updated with sync(), attach(), detach() or updateExistingPivot() methods, but this package will help with that. 8 | 9 | ## Version Compatibility 10 | 11 | | Laravel Version | Package Tag | Supported | Development Branch 12 | |-----------------|-------------|-----------| -----------| 13 | | >= 5.5.0 | 3.* | yes | master 14 | | < 5.5.0 | - | no | - 15 | 16 | * you still can use inactive branches for laravel 5.4.x or older 17 | 18 | ## Install 19 | 20 | 1.Install package with composer 21 | ``` 22 | composer require fico7489/laravel-pivot 23 | ``` 24 | With this statement, a composer will install highest available package version for your current laravel version. 25 | 26 | 2.Use Fico7489\Laravel\Pivot\Traits\PivotEventTrait trait in your base model or only in particular models. 27 | 28 | ```php 29 | use Fico7489\Laravel\Pivot\Traits\PivotEventTrait; 30 | use Illuminate\Database\Eloquent\Model; 31 | 32 | abstract class BaseModel extends Model 33 | { 34 | use PivotEventTrait; 35 | ... 36 | ``` 37 | 38 | and that's it, enjoy. 39 | 40 | ## New eloquent events 41 | 42 | You can check all eloquent events here: https://laravel.com/docs/5.5/eloquent#events) 43 | 44 | New events are : 45 | 46 | ``` 47 | pivotAttaching, pivotAttached 48 | pivotDetaching, pivotDetached, 49 | pivotUpdating, pivotUpdated 50 | ``` 51 | 52 | The best way to catch events is with this model functions: 53 | 54 | ```php 55 | public static function boot() 56 | { 57 | parent::boot(); 58 | 59 | static::pivotAttaching(function ($model, $relationName, $pivotIds, $pivotIdsAttributes) { 60 | // 61 | }); 62 | 63 | static::pivotAttached(function ($model, $relationName, $pivotIds, $pivotIdsAttributes) { 64 | // 65 | }); 66 | 67 | static::pivotDetaching(function ($model, $relationName, $pivotIds) { 68 | // 69 | }); 70 | 71 | static::pivotDetached(function ($model, $relationName, $pivotIds) { 72 | // 73 | }); 74 | 75 | static::pivotUpdating(function ($model, $relationName, $pivotIds, $pivotIdsAttributes) { 76 | // 77 | }); 78 | 79 | static::pivotUpdated(function ($model, $relationName, $pivotIds, $pivotIdsAttributes) { 80 | // 81 | }); 82 | 83 | static::updating(function ($model) { 84 | //this is how we catch standard eloquent events 85 | }); 86 | } 87 | ``` 88 | 89 | You can also see those events here: 90 | 91 | ```php 92 | \Event::listen('eloquent.*', function ($eventName, array $data) { 93 | echo $eventName; //e.g. 'eloquent.pivotAttached' 94 | }); 95 | ``` 96 | 97 | ## Suported relations 98 | 99 | **BelongsToMany** and **MorphToMany** 100 | 101 | ## Which events are dispatched and when they are dispatched 102 | 103 | Four BelongsToMany methods dispatches events from this package: 104 | 105 | **attach()** 106 | Dispatches **one** **pivotAttaching** and **one** **pivotAttached** event. 107 | Even when more rows are added only **one** event is dispatched for all rows but in that case, you can see all changed row ids in the $pivotIds variable, and the changed row ids with attributes in the $pivotIdsAttributes variable. 108 | 109 | **detach()** 110 | Dispatches **one** **pivotDetaching** and **one** **pivotDetached** event. 111 | Even when more rows are deleted only **one** event is dispatched for all rows but in that case, you can see all changed row ids in the $pivotIds variable. 112 | 113 | **updateExistingPivot()** 114 | Dispatches **one** **pivotUpdating** and **one** **pivotUpdated** event. 115 | You can change only one row in the pivot table with updateExistingPivot. 116 | 117 | **sync()** 118 | Dispatches **more** **pivotAttaching** and **more** **pivotAttached** events, depending on how many rows are added in the pivot table. These events are not dispatched if nothing is attached. 119 | Dispatches **one** **pivotDetaching** and **one** **pivotDetached** event, but you can see all deleted ids in the $pivotIds variable. This event is not dispatched if nothing is detached. 120 | Dispatches **more** **pivotUpdating** and **more** **pivotUpdated** events, depending on how many rows are updated in the pivot table. These events are not dispatched if nothing is attached. 121 | 122 | E.g. when you call sync() if two rows are added and two are deleted **two** **pivotAttaching** and **two** **pivotAttached** events and **one** **pivotDetaching** and **one** **pivotDetached** event will be dispatched. 123 | If sync() is called but rows are not added or deleted events are not dispatched. 124 | 125 | 126 | ## Usage 127 | 128 | We have three tables in database users(id, name), roles(id, name), role_user(user_id, role_id). 129 | We have two models : 130 | 131 | ```php 132 | 133 | class User extends Model 134 | { 135 | use PivotEventTrait; 136 | .... 137 | 138 | public function roles() 139 | { 140 | return $this->belongsToMany(Role::class); 141 | } 142 | 143 | static::pivotAttached(function ($model, $relationName, $pivotIds, $pivotIdsAttributes) { 144 | echo 'pivotAttached'; 145 | echo get_class($model); 146 | echo $relationName; 147 | print_r($pivotIds); 148 | print_r($pivotIdsAttributes); 149 | }); 150 | 151 | static::pivotUpdated(function ($model, $relationName, $pivotIds, $pivotIdsAttributes) { 152 | echo 'pivotUpdated'; 153 | echo get_class($model); 154 | echo $relationName; 155 | print_r($pivotIds); 156 | print_r($pivotIdsAttributes); 157 | }); 158 | 159 | static::pivotDetached(function ($model, $relationName, $pivotIds) { 160 | echo 'pivotDetached'; 161 | echo get_class($model); 162 | echo $relationName; 163 | print_r($pivotIds); 164 | }); 165 | ``` 166 | 167 | ```php 168 | class Role extends Model 169 | { 170 | .... 171 | ``` 172 | 173 | ### Attaching 174 | 175 | For attach() or detach() one event is dispatched for both pivot ids. 176 | 177 | #### Attaching with int 178 | Running this code 179 | ```php 180 | $user = User::first(); 181 | $user->roles()->attach(1); 182 | ``` 183 | You will see this output 184 | ``` 185 | pivotAttached 186 | App\Models\User 187 | roles 188 | [1] 189 | [1 => []] 190 | ``` 191 | 192 | 193 | #### Attaching with array 194 | Running this code 195 | ```php 196 | $user = User::first(); 197 | $user->roles()->attach([1]); 198 | ``` 199 | You will see this output 200 | ``` 201 | pivotAttached 202 | App\Models\User 203 | roles 204 | [1] 205 | [1 => []] 206 | ``` 207 | 208 | 209 | #### Attaching with model 210 | Running this code 211 | ```php 212 | $user = User::first(); 213 | $user->roles()->attach(Role::first()); 214 | ``` 215 | You will see this output 216 | ``` 217 | pivotAttached 218 | App\Models\User 219 | roles 220 | [1] 221 | [1 => []] 222 | ``` 223 | 224 | 225 | #### Attaching with collection 226 | Running this code 227 | ```php 228 | $user = User::first(); 229 | $user->roles()->attach(Role::get()); 230 | ``` 231 | You will see this output 232 | ``` 233 | pivotAttached 234 | App\Models\User 235 | roles 236 | [1, 2] 237 | [1 => [], 2 => []] 238 | ``` 239 | 240 | 241 | #### Attaching with array (id => attributes) 242 | Running this code 243 | ```php 244 | $user = User::first(); 245 | $user->roles()->attach([1, 2 => ['attribute' => 'test']], ['attribute2' => 'test2']); 246 | ``` 247 | You will see this output 248 | ``` 249 | pivotAttached 250 | App\Models\User 251 | roles 252 | [1, 2] 253 | [1 => [], 2 => ['attribute' => 'test', 'attribute2' => 'test2']] 254 | ``` 255 | 256 | 257 | ### Syncing: 258 | 259 | For sync() method event is dispatched for each pivot row. 260 | 261 | Running this code 262 | ```php 263 | $user = User::first(); 264 | $user->roles()->sync([1, 2]); 265 | ``` 266 | 267 | You will see this output 268 | 269 | ``` 270 | pivotAttached 271 | App\Models\User 272 | roles 273 | [1] 274 | [1 => []] 275 | 276 | pivotAttached 277 | App\Models\User 278 | roles 279 | [2] 280 | [2 => []] 281 | ``` 282 | 283 | ### Detaching: 284 | 285 | Running this code 286 | ```php 287 | $user = User::first(); 288 | $user->roles()->detach([1, 2]); 289 | ``` 290 | You will see this output 291 | ``` 292 | pivotDetached 293 | App\Models\User 294 | roles 295 | [1, 2] 296 | ``` 297 | 298 | ### Updating: 299 | 300 | Running this code 301 | ```php 302 | $user = User::first(); 303 | $user->roles()->updateExistingPivot(1, ['attribute' => 'test']); 304 | ``` 305 | You will see this output 306 | ``` 307 | pivotUpdated 308 | App\Models\User 309 | roles 310 | [1] 311 | [1 => ['attribute' => 'test']] 312 | ``` 313 | 314 | License 315 | ---- 316 | 317 | MIT 318 | 319 | 320 | **Free Software, Hell Yeah!** 321 | 322 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fico7489/laravel-pivot", 3 | "description": "This package introduces new eloquent events for sync(), attach(), detach() or updateExistingPivot() methods on BelongsToMany relation.", 4 | "keywords": [ 5 | "laravel BelongsToMany events", 6 | "eloquent extra events", 7 | "laravel pivot events", 8 | "laravel sync events", 9 | "eloquent events" 10 | ], 11 | "homepage": "https://github.com/fico7489/laravel-pivot", 12 | "support": { 13 | "issues": "https://github.com/fico7489/laravel-pivot/issues", 14 | "source": "https://github.com/fico7489/laravel-pivot" 15 | }, 16 | "license": "MIT", 17 | "authors": [ 18 | { 19 | "name": "Filip Horvat", 20 | "email": "filip.horvat@am2studio.hr", 21 | "homepage": "http://am2studio.hr", 22 | "role": "Developer" 23 | } 24 | ], 25 | "require": { 26 | "illuminate/database": "5.5.*|6.*|7.*|8.*|9.*|10.*|11.*|12.*" 27 | }, 28 | "require-dev": { 29 | "orchestra/testbench": "*", 30 | "friendsofphp/php-cs-fixer": "*" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Fico7489\\Laravel\\Pivot\\": "src" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Fico7489\\Laravel\\Pivot\\Tests\\": "tests/" 40 | } 41 | }, 42 | "minimum-stability": "dev", 43 | "prefer-stable": true 44 | } 45 | -------------------------------------------------------------------------------- /src/Relations/BelongsToManyCustom.php: -------------------------------------------------------------------------------- 1 | filterModelEventResults( 27 | $this->fireCustomModelEvent($event, $method) 28 | ); 29 | 30 | if (false === $result) { 31 | return false; 32 | } 33 | 34 | $payload = [$this, $relationName, $ids, $idsAttributes]; 35 | 36 | return !empty($result) ? $result : static::$dispatcher->{$method}( 37 | "eloquent.{$event}: ".static::class, $payload 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Traits/ExtendRelationsTrait.php: -------------------------------------------------------------------------------- 1 | getIdsWithAttributes($ids, $attributes); 19 | 20 | if (false === $this->parent->fireModelEvent('pivotAttaching', true, $this->getRelationName(), $idsOnly, $idsAttributes)) { 21 | return false; 22 | } 23 | 24 | $parentResult = parent::attach($ids, $attributes, $touch); 25 | $this->parent->fireModelEvent('pivotAttached', false, $this->getRelationName(), $idsOnly, $idsAttributes); 26 | 27 | return $parentResult; 28 | } 29 | 30 | /** 31 | * Detach models from the relationship. 32 | * 33 | * @param mixed $ids 34 | * @param bool $touch 35 | * 36 | * @return int 37 | */ 38 | public function detach($ids = null, $touch = true) 39 | { 40 | if (is_null($ids)) { 41 | $ids = $this->query->pluck($this->query->qualifyColumn($this->relatedKey))->toArray(); 42 | } 43 | 44 | list($idsOnly) = $this->getIdsWithAttributes($ids); 45 | 46 | if (false === $this->parent->fireModelEvent('pivotDetaching', true, $this->getRelationName(), $idsOnly)) { 47 | return false; 48 | } 49 | 50 | $parentResult = parent::detach($ids, $touch); 51 | $this->parent->fireModelEvent('pivotDetached', false, $this->getRelationName(), $idsOnly); 52 | 53 | return $parentResult; 54 | } 55 | 56 | /** 57 | * Update an existing pivot record on the table. 58 | * 59 | * @param mixed $id 60 | * @param bool $touch 61 | * 62 | * @return int 63 | */ 64 | public function updateExistingPivot($id, array $attributes, $touch = true) 65 | { 66 | list($idsOnly, $idsAttributes) = $this->getIdsWithAttributes($id, $attributes); 67 | 68 | if (false === $this->parent->fireModelEvent('pivotUpdating', true, $this->getRelationName(), $idsOnly, $idsAttributes)) { 69 | return false; 70 | } 71 | 72 | $parentResult = parent::updateExistingPivot($id, $attributes, $touch); 73 | $this->parent->fireModelEvent('pivotUpdated', false, $this->getRelationName(), $idsOnly, $idsAttributes); 74 | 75 | return $parentResult; 76 | } 77 | 78 | /** 79 | * Cleans the ids and ids with attributes 80 | * Returns an array with and array of ids and array of id => attributes. 81 | * 82 | * @param mixed $id 83 | * @param array $attributes 84 | * 85 | * @return array 86 | */ 87 | private function getIdsWithAttributes($id, $attributes = []) 88 | { 89 | $ids = []; 90 | 91 | if ($id instanceof Model) { 92 | $ids[$id->getKey()] = $attributes; 93 | } elseif ($id instanceof Collection) { 94 | foreach ($id as $model) { 95 | $ids[$model->getKey()] = $attributes; 96 | } 97 | } elseif (is_array($id)) { 98 | foreach ($id as $key => $attributesArray) { 99 | if (is_array($attributesArray)) { 100 | $ids[$key] = array_merge($attributes, $attributesArray); 101 | } else { 102 | $ids[$attributesArray] = $attributes; 103 | } 104 | } 105 | } elseif (is_int($id) || is_string($id)) { 106 | $ids[$id] = $attributes; 107 | } 108 | 109 | $idsOnly = array_keys($ids); 110 | 111 | return [$idsOnly, $ids]; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Traits/PivotEventTrait.php: -------------------------------------------------------------------------------- 1 |