├── .devcontainer └── devcontainer.json ├── .editorconfig ├── .php_cs ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── composer.json ├── docker-compose.yml └── src ├── Relations ├── BelongsToManyCustom.php └── MorphToManyCustom.php └── Traits ├── ExtendFireModelEventTrait.php ├── ExtendRelationsTrait.php ├── FiresPivotEventsTrait.php └── PivotEventTrait.php /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // https://aka.ms/devcontainer.json 2 | { 3 | "name": "Existing Docker Compose (Extend)", 4 | "dockerComposeFile": [ 5 | "../docker-compose.yml" 6 | ], 7 | "features": { 8 | "ghcr.io/devcontainers/features/sshd:1": { 9 | "version": "latest" 10 | } 11 | }, 12 | "service": "laravel.test", 13 | "workspaceFolder": "/var/www/html", 14 | "customizations": { 15 | "vscode": { 16 | "settings": {}, 17 | "extensions": [ 18 | "aaron-bond.better-comments", 19 | "adrianwilczynski.alpine-js-intellisense", 20 | "AlexArthurs.todo-pusher", 21 | "amiralizadeh9480.laravel-extra-intellisense", 22 | "austenc.laravel-blade-spacer", 23 | "beyondcode.tinkerwell", 24 | "bmewburn.vscode-intelephense-client", 25 | "bradlc.vscode-tailwindcss", 26 | "christian-kohler.npm-intellisense", 27 | "christian-kohler.path-intellisense", 28 | "cierra.livewire-vscode", 29 | "codecov.codecov", 30 | "codingyu.laravel-goto-view", 31 | "davidanson.vscode-markdownlint", 32 | "davidbwaters.macos-modern-theme", 33 | "eamodio.gitlens", 34 | "editorconfig.editorconfig", 35 | "ericcheng.codesongclear", 36 | "faelv.composer-companion", 37 | "file-icons.file-icons", 38 | "foxundermoon.shell-format", 39 | "georgykurian.laravel-ide-helper", 40 | "github.codespaces", 41 | "GitHub.copilot-chat", 42 | "GitHub.copilot-nightly", 43 | "GitHub.copilot", 44 | "github.vscode-github-actions", 45 | "github.vscode-pull-request-github", 46 | "Gruntfuggly.todo-tree", 47 | "heissenbergerlab.php-array-from-json", 48 | "heybourn.headwind", 49 | "huibizhang.codesnap-plus", 50 | "irongeek.vscode-env", 51 | "kencocaceo.customvscodeuicss", 52 | "m4ns0ur.base64", 53 | "maciejdems.add-to-gitignore", 54 | "mahmoudshahin.laravel-routes", 55 | "markis.code-coverage", 56 | "martybegood.single-editor-tabs", 57 | "mechatroner.rainbow-csv", 58 | "mehedidracula.php-namespace-resolver", 59 | "mhutchie.git-graph", 60 | "mikestead.dotenv", 61 | "mohamedbenhida.laravel-intellisense", 62 | "mrmlnc.vscode-duplicate", 63 | "naoray.laravel-goto-components", 64 | "oderwat.indent-rainbow", 65 | "pcbowers.alpine-intellisense", 66 | "recca0120.vscode-phpunit", 67 | "redhat.vscode-yaml", 68 | "rifi2k.format-html-in-php", 69 | "shevaua.phpcs", 70 | "shufo.vscode-blade-formatter", 71 | "sperovita.alpinejs-syntax-highlight", 72 | "streetsidesoftware.code-spell-checker", 73 | "syler.ignore", 74 | "teabyii.ayu", 75 | "usernamehw.errorlens", 76 | "vincaslt.highlight-matching-tag", 77 | "WakaTime.vscode-wakatime", 78 | "withfig.fig", 79 | "xdebug.php-debug" 80 | ] 81 | } 82 | }, 83 | "remoteUser": "sail", 84 | "postCreateCommand": "sudo chown -R 1000:1000 /var/www/html", 85 | "forwardPorts": [ 86 | 80 87 | ], 88 | "portsAttributes": { 89 | "80": { 90 | "label": "HTTP" 91 | } 92 | }, 93 | "mounts": [ 94 | "source=${localEnv:HOME}/.wakatime.cfg,target=/home/sail/.wakatime.cfg,type=bind,consistency=delegated" 95 | ] 96 | // "runServices": [], 97 | // "shutdownAction": "none", 98 | } 99 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [.blackfire.yaml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | in([ 11 | __DIR__ .'/src', 12 | __DIR__ .'/tests', 13 | ]); 14 | ; 15 | 16 | /* 17 | * Do the magic 18 | */ 19 | return Config::create() 20 | ->setUsingCache(false) 21 | ->setRules([ 22 | '@PSR2' => true, 23 | '@Symfony' => true, 24 | 25 | 'align_multiline_comment' => true, 26 | 'blank_line_after_opening_tag' => true, 27 | 'single_blank_line_before_namespace' => true, 28 | 'no_unused_imports' => true, 29 | 'binary_operator_spaces' => ['default' => null], 30 | ]) 31 | ->setFinder($finder) 32 | ; -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "aaron-bond.better-comments", 4 | "adrianwilczynski.alpine-js-intellisense", 5 | "amiralizadeh9480.laravel-extra-intellisense", 6 | "austenc.laravel-blade-spacer", 7 | "beyondcode.tinkerwell", 8 | "bmewburn.vscode-intelephense-client", 9 | "bradlc.vscode-tailwindcss", 10 | "christian-kohler.npm-intellisense", 11 | "christian-kohler.path-intellisense", 12 | "cierra.livewire-vscode", 13 | "codingyu.laravel-goto-view", 14 | "davidanson.vscode-markdownlint", 15 | "davidbwaters.macos-modern-theme", 16 | "eamodio.gitlens", 17 | "editorconfig.editorconfig", 18 | "ericcheng.codesongclear", 19 | "faelv.composer-companion", 20 | "file-icons.file-icons", 21 | "foxundermoon.shell-format", 22 | "georgykurian.laravel-ide-helper", 23 | "github.codespaces", 24 | "GitHub.copilot-chat", 25 | "GitHub.copilot-nightly", 26 | "github.vscode-github-actions", 27 | "github.vscode-pull-request-github", 28 | "heissenbergerlab.php-array-from-json", 29 | "heybourn.headwind", 30 | "huibizhang.codesnap-plus", 31 | "irongeek.vscode-env", 32 | "kencocaceo.customvscodeuicss", 33 | "m4ns0ur.base64", 34 | "maciejdems.add-to-gitignore", 35 | "mahmoudshahin.laravel-routes", 36 | "markis.code-coverage", 37 | "martybegood.single-editor-tabs", 38 | "mechatroner.rainbow-csv", 39 | "mehedidracula.php-namespace-resolver", 40 | "mhutchie.git-graph", 41 | "mikestead.dotenv", 42 | "mohamedbenhida.laravel-intellisense", 43 | "mrmlnc.vscode-duplicate", 44 | "naoray.laravel-goto-components", 45 | "oderwat.indent-rainbow", 46 | "pcbowers.alpine-intellisense", 47 | "recca0120.vscode-phpunit", 48 | "redhat.vscode-yaml", 49 | "rifi2k.format-html-in-php", 50 | "shevaua.phpcs", 51 | "shufo.vscode-blade-formatter", 52 | "sperovita.alpinejs-syntax-highlight", 53 | "streetsidesoftware.code-spell-checker", 54 | "syler.ignore", 55 | "teabyii.ayu", 56 | "usernamehw.errorlens", 57 | "vincaslt.highlight-matching-tag", 58 | "WakaTime.vscode-wakatime", 59 | "withfig.fig", 60 | "xdebug.php-debug", 61 | "codecov.codecov" 62 | ], 63 | "unwantedRecommendations": [ 64 | "ikappas.phpcs", 65 | "linyang95.phpmd" 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "alpine-intellisense.settings.languageScopes": "html,blade,php", 3 | "better-comments.tags": [ 4 | { 5 | "tag": "!", 6 | "color": "#FF2D00", 7 | "strikethrough": false, 8 | "underline": false, 9 | "backgroundColor": "transparent", 10 | "bold": false, 11 | "italic": false 12 | }, 13 | { 14 | "tag": "?", 15 | "color": "#3498DB", 16 | "strikethrough": false, 17 | "underline": false, 18 | "backgroundColor": "transparent", 19 | "bold": false, 20 | "italic": false 21 | }, 22 | { 23 | "tag": "//", 24 | "color": "#474747", 25 | "strikethrough": true, 26 | "underline": false, 27 | "backgroundColor": "transparent", 28 | "bold": false, 29 | "italic": false 30 | }, 31 | { 32 | "tag": "todo", 33 | "color": "#FF8C00", 34 | "strikethrough": false, 35 | "underline": false, 36 | "backgroundColor": "transparent", 37 | "bold": false, 38 | "italic": false 39 | }, 40 | { 41 | "tag": "*", 42 | "color": "#98C379", 43 | "strikethrough": false, 44 | "underline": false, 45 | "backgroundColor": "transparent", 46 | "bold": false, 47 | "italic": false 48 | }, 49 | { 50 | "tag": "phpcs:ignore", 51 | "color": "black", 52 | "strikethrough": false, 53 | "underline": false, 54 | "backgroundColor": "orange", 55 | "bold": false, 56 | "italic": false 57 | } 58 | ], 59 | "blade.format.enable": true, 60 | "blade.newLine": true, 61 | "bladeFormatter.format.enabled": true, 62 | "bladeFormatter.format.noMultipleEmptyLines": true, 63 | "bladeFormatter.format.sortHtmlAttributes": "alphabetical", 64 | "bladeFormatter.format.sortTailwindcssClasses": true, 65 | "bladeFormatter.format.wrapAttributes": "force-expand-multiline", 66 | "bladeFormatter.format.wrapLineLength": 100, 67 | "breadcrumbs.enabled": true, 68 | "composerCompanion.executablePath": "composer", 69 | "cSpell.spellCheckOnlyWorkspaceFiles": true, 70 | "cSpell.autoFormatConfigFile": true, 71 | "css.validate": false, 72 | "debug.allowBreakpointsEverywhere": true, 73 | "debug.showBreakpointsInOverviewRuler": true, 74 | "diffEditor.ignoreTrimWhitespace": false, 75 | "diffEditor.wordWrap": "off", 76 | "editor.acceptSuggestionOnEnter": "off", 77 | "editor.accessibilitySupport": "off", 78 | "editor.autoClosingBrackets": "always", 79 | "editor.codeLensFontFamily": "JetBrains Mono", 80 | "editor.cursorBlinking": "expand", 81 | "editor.cursorSmoothCaretAnimation": "on", 82 | "editor.detectIndentation": true, 83 | "editor.emptySelectionClipboard": false, 84 | "editor.fontFamily": "JetBrains Mono", 85 | "editor.fontLigatures": true, 86 | "editor.formatOnPaste": false, 87 | "editor.formatOnType": true, 88 | "editor.gotoLocation.multipleDeclarations": "goto", 89 | "editor.gotoLocation.multipleDefinitions": "goto", 90 | "editor.gotoLocation.multipleImplementations": "goto", 91 | "editor.gotoLocation.multipleReferences": "goto", 92 | "editor.gotoLocation.multipleTypeDefinitions": "goto", 93 | "editor.inlayHints.fontFamily": "JetBrains Mono", 94 | "editor.inlineSuggest.enabled": true, 95 | "editor.insertSpaces": true, 96 | "editor.lightbulb.enabled": "off", 97 | "editor.linkedEditing": true, 98 | "editor.minimap.maxColumn": 100, 99 | "editor.parameterHints.enabled": false, 100 | "editor.quickSuggestions": { 101 | "strings": true 102 | }, 103 | "editor.renderFinalNewline": "on", 104 | "editor.renderWhitespace": "none", 105 | "editor.roundedSelection": true, 106 | "editor.rulers": [ 107 | 100, 108 | ], 109 | "editor.stickyScroll.enabled": true, 110 | "editor.suggest.localityBonus": true, 111 | "editor.suggest.showValues": false, 112 | "editor.trimAutoWhitespace": true, 113 | "editor.wordBasedSuggestions": "off", 114 | "editor.wordSeparators": "`~!@#%^&*()-=+[{]}\\|;:'\",.<>/?", 115 | "editor.wordWrapColumn": 100, 116 | "editor.wrappingIndent": "none", 117 | "errorLens.enableOnDiffView": true, 118 | "errorLens.fontFamily": "JetBrains Mono", 119 | "errorLens.messageTemplate": "$message $source $code", 120 | "explorer.confirmDelete": false, 121 | "explorer.confirmDragAndDrop": false, 122 | "extensions.ignoreRecommendations": false, 123 | "files.autoGuessEncoding": true, 124 | "files.insertFinalNewline": true, 125 | "files.trimFinalNewlines": true, 126 | "files.trimTrailingWhitespace": true, 127 | "git.allowForcePush": true, 128 | "git.autofetch": true, 129 | "git.confirmSync": false, 130 | "git.enableCommitSigning": true, 131 | "git.enableSmartCommit": true, 132 | "git.fetchOnPull": true, 133 | "git.ignoreRebaseWarning": true, 134 | "git.mergeEditor": false, 135 | "git.showPushSuccessNotification": true, 136 | "github.copilot.enable": { 137 | "*": true, 138 | "yaml": false, 139 | "plaintext": false, 140 | "markdown": false 141 | }, 142 | "github.copilot.editor.enableAutoCompletions": true, 143 | "githubIssues.issueBranchTitle": "${user}/${sanitizedIssueTitle}", 144 | "githubIssues.queries": [ 145 | { 146 | "label": "My Issues", 147 | "query": "default" 148 | }, 149 | { 150 | "label": "Created Issues", 151 | "query": "author:${user} state:open repo:${owner}\/${repository} sort:created-desc" 152 | }, 153 | { 154 | "label": "Recent Issues", 155 | "query": "state:open repo:${owner}\/${repository} sort:updated-desc" 156 | } 157 | ], 158 | "githubIssues.workingIssueFormatScm": "", 159 | "githubPullRequests.assignCreated": "${user}", 160 | "githubPullRequests.defaultCreateOption": "createDraft", 161 | "githubPullRequests.defaultMergeMethod": "squash", 162 | "githubPullRequests.fileListLayout": "tree", 163 | "githubPullRequests.ignoredPullRequestBranches": [ 164 | "develop", 165 | "master" 166 | ], 167 | "githubPullRequests.pullBranch": "never", 168 | "githubPullRequests.showPullRequestNumberInTree": true, 169 | "githubPullRequests.terminalLinksHandler": "vscode", 170 | "gitlens.showWelcomeOnInstall": false, 171 | "gitlens.showWhatsNewAfterUpgrades": false, 172 | "gitlens.outputLevel": "off", 173 | "gitlens.plusFeatures.enabled": false, 174 | "gitlens.virtualRepositories.enabled": false, 175 | "gitlens.codeLens.enabled": false, 176 | "gitlens.codeLens.recentChange.enabled": false, 177 | "gitlens.codeLens.authors.enabled": false, 178 | "gitlens.statusBar.enabled": false, 179 | "gitlens.statusBar.pullRequests.enabled": false, 180 | "gitlens.hovers.enabled": false, 181 | "gitlens.hovers.avatars": false, 182 | "gitlens.hovers.pullRequests.enabled": false, 183 | "gitlens.hovers.autolinks.enabled": false, 184 | "gitlens.hovers.currentLine.enabled": false, 185 | "gitlens.hovers.autolinks.enhanced": false, 186 | "gitlens.hovers.currentLine.details": false, 187 | "gitlens.hovers.currentLine.changes": false, 188 | "gitlens.hovers.annotations.enabled": false, 189 | "gitlens.hovers.annotations.changes": false, 190 | "gitlens.hovers.annotations.details": false, 191 | "headwind.runOnSave": true, 192 | "html.format.indentHandlebars": true, 193 | "html.format.indentInnerHtml": true, 194 | "html.format.preserveNewLines": true, 195 | "html.format.wrapAttributes": "force", 196 | "html.format.wrapLineLength": 100, 197 | "indentRainbow.colorOnWhiteSpaceOnly": true, 198 | "intelephense.environment.documentRoot": "public/index.php", 199 | "intelephense.files.exclude": [ 200 | "**/.git/**", 201 | "**/.svn/**", 202 | "**/.hg/**", 203 | "**/CVS/**", 204 | "**/.DS_Store/**", 205 | "**/node_modules/**", 206 | "**/bower_components/**", 207 | "**/vendor/**/{Tests,tests}/**", 208 | "**/.history/**" 209 | ], 210 | "intelephense.phpdoc.returnVoid": false, 211 | "javascript.format.placeOpenBraceOnNewLineForControlBlocks": true, 212 | "LaravelExtraIntellisense.modelAccessorCase": "camel", 213 | "LaravelExtraIntellisense.modelAttributeCase": "camel", 214 | "LaravelExtraIntellisense.modelsPaths": [ 215 | "app", 216 | "app/Models" 217 | ], 218 | "LaravelExtraIntellisense.modelVariables": { 219 | "user": "App\\Models\\User" 220 | }, 221 | "LaravelIntellisense.model": "App\\Models", 222 | "markdown.preview.fontFamily": "JetBrains Mono", 223 | "markdownlint.config": { 224 | "default": true, 225 | "MD024": false, 226 | "MD022": false, 227 | "MD032": false, 228 | }, 229 | "namespaceResolver.leadingSeparator": false, 230 | "namespaceResolver.showMessageOnStatusBar": true, 231 | "namespaceResolver.sortAlphabetically": true, 232 | "namespaceResolver.sortOnSave": true, 233 | "php.suggest.basic": false, 234 | "php.validate.enable": false, 235 | "php.validate.run": "onType", 236 | "phpcs.executablePath": "vendor/bin/phpcs", 237 | "phpcs.showSources": true, 238 | "phpmd.rules": "phpmd.xml", 239 | "phpmd.command": "vendor/bin/phpmd", 240 | "phpmd.verbose": true, 241 | "phpunit.args": [ 242 | "--coverage-clover=coverage.xml", 243 | ], 244 | "redhat.telemetry.enabled": false, 245 | "search.exclude": { 246 | // Hide everything in /vendor, except "laravel" and "livewire" folders. 247 | "**/vendor/{[^l],?[^ai]}*": true, 248 | // Hide everything in /public, except "index.php" 249 | "**/public/{[^i],?[^n]}*": true, 250 | "**/node_modules": true, 251 | "**/dist": true, 252 | "**/_ide_helper.php": true, 253 | "**/composer.lock": true, 254 | "**/package-lock.json": true, 255 | "storage": true, 256 | ".phpunit.result.cache": true 257 | }, 258 | "tailwindCSS.validate": true, 259 | "terminal.explorerKind": "external", 260 | "terminal.integrated.drawBoldTextInBrightColors": false, 261 | "terminal.integrated.enableMultiLinePasteWarning": false, 262 | "terminal.integrated.fontFamily": "MesloLGS NF", 263 | "terminal.integrated.gpuAcceleration": "off", 264 | "terminal.integrated.scrollback": 5000, 265 | "typescript.suggest.enabled": false, 266 | "window.commandCenter": true, 267 | "window.title": "laravel-pivot-events", 268 | "workbench.editor.enablePreview": false, 269 | "workbench.editor.showIcons": false, 270 | "workbench.editor.showTabs": "single", 271 | "workbench.editor.tabCloseButton": "left", 272 | "workbench.fontAliasing": "auto", 273 | "workbench.iconTheme": "file-icons", 274 | "workbench.productIconTheme": "macos-modern", 275 | "workbench.startupEditor": "none", 276 | "zenMode.fullScreen": false, 277 | "zenMode.hideLineNumbers": false, 278 | // formaters 279 | "[blade]": { 280 | "editor.defaultFormatter": "shufo.vscode-blade-formatter" 281 | }, 282 | "[php]": { 283 | "editor.defaultFormatter": "bmewburn.vscode-intelephense-client" 284 | }, 285 | "workbench.editor.tabActionLocation": "left", 286 | } -------------------------------------------------------------------------------- /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 Events 2 | This package introduces new eloquent events for sync(), attach(), detach(), or 3 | updateExistingPivot() methods on BelongsToMany and MorphToMany relationships. 4 | 5 | This package is a fork of [fico7489/laravel-pivot](https://github.com/fico7489/laravel-pivot) 6 | created mainly to address compatibility issues with 7 | [Laravel Telescope](https://github.com/laravel/telescope) and 8 | [Model Caching for Laravel](https://github.com/GeneaLabs/laravel-model-caching). 9 | 10 | ## Sponsors 11 | We thank the following sponsors for their generosity. Please take a moment to check them out: 12 | 13 | - [LIX](https://lix-it.com) 14 | 15 | ## Requirements 16 | - Laravel 8.0+ 17 | - PHP 7.3+ 18 | 19 | ## Installation 20 | 1.Install package with composer: 21 | ``` 22 | composer require "mikebronner/laravel-pivot-events:*" 23 | ``` 24 | 25 | 2. Use `GeneaLabs\LaravelPivotEvents\Traits\PivotEventTrait` trait in your base 26 | model or only in particular models. 27 | ```php 28 | // ... 29 | use GeneaLabs\LaravelPivotEvents\Traits\PivotEventTrait; 30 | use Illuminate\Database\Eloquent\Model; 31 | 32 | abstract class BaseModel extends Model 33 | { 34 | use PivotEventTrait; 35 | // ... 36 | } 37 | ``` 38 | 39 | ## New Eloquent Events 40 | 41 | You can check all eloquent events here: https://laravel.com/docs/5.8/eloquent#events) 42 | 43 | New events are : 44 | - `pivotSyncing`, `pivotSynced` 45 | - `pivotAttaching`, `pivotAttached` 46 | - `pivotDetaching`, `pivotDetached` 47 | - `pivotUpdating`, `pivotUpdated` 48 | 49 | The easiest way to catch events is using methods in your model's `boot()` method: 50 | ```php 51 | public static function boot() 52 | { 53 | parent::boot(); 54 | 55 | static::pivotSyncing(function ($model, $relationName) { 56 | // 57 | }); 58 | 59 | static::pivotSynced(function ($model, $relationName, $changes) { 60 | // 61 | }); 62 | 63 | static::pivotAttaching(function ($model, $relationName, $pivotIds, $pivotIdsAttributes) { 64 | // 65 | }); 66 | 67 | static::pivotAttached(function ($model, $relationName, $pivotIds, $pivotIdsAttributes) { 68 | // 69 | }); 70 | 71 | static::pivotDetaching(function ($model, $relationName, $pivotIds) { 72 | // 73 | }); 74 | 75 | static::pivotDetached(function ($model, $relationName, $pivotIds) { 76 | // 77 | }); 78 | 79 | static::pivotUpdating(function ($model, $relationName, $pivotIds, $pivotIdsAttributes) { 80 | // 81 | }); 82 | 83 | static::pivotUpdated(function ($model, $relationName, $pivotIds, $pivotIdsAttributes) { 84 | // 85 | }); 86 | 87 | static::updating(function ($model) { 88 | //this is how we catch standard eloquent events 89 | }); 90 | } 91 | ``` 92 | 93 | You can also catch them using dedicated Event Listeners: 94 | ```php 95 | \Event::listen('eloquent.*', function ($eventName, array $data) { 96 | echo $eventName; //e.g. 'eloquent.pivotAttached' 97 | }); 98 | ``` 99 | 100 | ## Supported Relationships 101 | **BelongsToMany** and **MorphToMany** 102 | 103 | ## Which events are dispatched and when they are dispatched 104 | Four BelongsToMany methods dispatches events from this package : 105 | 106 | **attach()** 107 | Dispatches **one** **pivotAttaching** and **one** **pivotAttached** event. 108 | 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. 109 | 110 | **detach()** 111 | Dispatches **one** **pivotDetaching** and **one** **pivotDetached** event. 112 | 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. 113 | 114 | **updateExistingPivot()** 115 | Dispatches **one** **pivotUpdating** and **one** **pivotUpdated** event. 116 | You can change only one row in the pivot table with updateExistingPivot. 117 | 118 | **sync()** 119 | Dispatches **one** **pivotSyncing** and **one** **pivotSynced** event. 120 | Whether a row was attached/detached/updated during sync only **one** event is dispatched for all rows but in that case, you can see all the attached/detached/updated rows in the $changes variables. 121 | E.g. *How does sync work:* The sync first detaches all associations and then attaches or updates new entries one by one. 122 | 123 | ## Usage 124 | 125 | We have three tables in database users(id, name), roles(id, name), role_user(user_id, role_id). 126 | We have two models : 127 | 128 | ``` 129 | class User extends Model 130 | { 131 | use PivotEventTrait; 132 | // ... 133 | 134 | public function roles() 135 | { 136 | return $this->belongsToMany(Role::class); 137 | } 138 | 139 | static::pivotSynced(function ($model, $relationName, $changes) { 140 | echo 'pivotSynced'; 141 | echo get_class($model); 142 | echo $relationName; 143 | print_r($changes); 144 | }); 145 | 146 | static::pivotAttached(function ($model, $relationName, $pivotIds, $pivotIdsAttributes) { 147 | echo 'pivotAttached'; 148 | echo get_class($model); 149 | echo $relationName; 150 | print_r($pivotIds); 151 | print_r($pivotIdsAttributes); 152 | }); 153 | 154 | static::pivotUpdated(function ($model, $relationName, $pivotIds, $pivotIdsAttributes) { 155 | echo 'pivotUpdated'; 156 | echo get_class($model); 157 | echo $relationName; 158 | print_r($pivotIds); 159 | print_r($pivotIdsAttributes); 160 | }); 161 | 162 | static::pivotDetached(function ($model, $relationName, $pivotIds) { 163 | echo 'pivotDetached'; 164 | echo get_class($model); 165 | echo $relationName; 166 | print_r($pivotIds); 167 | }); 168 | 169 | // ... 170 | } 171 | ``` 172 | 173 | ``` 174 | class Role extends Model 175 | { 176 | // ... 177 | } 178 | ``` 179 | 180 | ### Attaching 181 | 182 | For attach() or detach() one event is dispatched for both pivot ids. 183 | 184 | #### Attaching With Primary Key 185 | Running this code 186 | ```php 187 | $user = User::first(); 188 | $user->roles()->attach(1); 189 | ``` 190 | 191 | You will see this output 192 | ``` 193 | pivotAttached 194 | App\Models\User 195 | roles 196 | [1] 197 | [1 => []] 198 | ``` 199 | 200 | #### Attaching with array 201 | Running this code 202 | ``` 203 | $user = User::first(); 204 | $user->roles()->attach([1]); 205 | ``` 206 | You will see this output 207 | ``` 208 | pivotAttached 209 | App\Models\User 210 | roles 211 | [1] 212 | [1 => []] 213 | ``` 214 | 215 | #### Attaching with model 216 | Running this code 217 | ```php 218 | $user = User::first(); 219 | $user->roles()->attach(Role::first()); 220 | ``` 221 | 222 | You will see this output 223 | ``` 224 | pivotAttached 225 | App\Models\User 226 | roles 227 | [1] 228 | [1 => []] 229 | ``` 230 | 231 | #### Attaching with collection 232 | Running this code 233 | ```php 234 | $user = User::first(); 235 | $user->roles()->attach(Role::get()); 236 | ``` 237 | 238 | You will see this output 239 | ``` 240 | pivotAttached 241 | App\Models\User 242 | roles 243 | [1, 2] 244 | [1 => [], 2 => []] 245 | ``` 246 | 247 | #### Attaching with array (id => attributes) 248 | Running this code 249 | ``` 250 | $user = User::first(); 251 | $user->roles()->attach([1, 2 => ['attribute' => 'test']], ['attribute2' => 'test2']); 252 | ``` 253 | You will see this output 254 | ``` 255 | pivotAttached 256 | App\Models\User 257 | roles 258 | [1, 2] 259 | [1 => [], 2 => ['attribute' => 'test', 'attribute2' => 'test2']] 260 | ``` 261 | 262 | ### Syncing 263 | Running this code 264 | ```php 265 | $user = User::first(); 266 | $user->roles()->attach([ 267 | 1 => ['pivot_attribut' => 1], 268 | 2 => ['pivot_attribut' => 0] 269 | ]); 270 | $user->roles()->sync([ 271 | 1 => ['pivot_attribut' => 0] 272 | 3 => ['pivot_attribut' => 1] 273 | ]); 274 | ``` 275 | 276 | You will see this output 277 | ``` 278 | pivotSynced 279 | App\Models\User 280 | roles 281 | [ 282 | "attached" => [ 283 | 0 => 3 284 | ] 285 | "detached" => [ 286 | 1 => 2 287 | ] 288 | "updated" => [ 289 | 0 => 1 290 | ] 291 | ] 292 | ``` 293 | 294 | ### Detaching 295 | Running this code 296 | ``` 297 | $user = User::first(); 298 | $user->roles()->detach([1, 2]); 299 | ``` 300 | You will see this output 301 | ``` 302 | pivotAttached 303 | App\Models\User 304 | roles 305 | [1, 2] 306 | ``` 307 | 308 | ### Updating 309 | 310 | Running this code 311 | ``` 312 | $user = User::first(); 313 | $user->roles()->updateExistingPivot(1, ['attribute' => 'test']); 314 | ``` 315 | You will see this output 316 | ``` 317 | pivotUpdated 318 | App\Models\User 319 | roles 320 | [1] 321 | [1 => ['attribute' => 'test']] 322 | ``` 323 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mikebronner/laravel-pivot-events", 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/mikebronner/laravel-pivot-events", 12 | "support": { 13 | "issues": "https://github.com/mikebronner/laravel-pivot-events/issues", 14 | "source": "https://github.com/mikebronner/laravel-pivot-events" 15 | }, 16 | "license": "MIT", 17 | "authors": [ 18 | { 19 | "name": "Mike Bronner", 20 | "email": "hello@genealabs.com", 21 | "homepage": "https://genealabs.com", 22 | "role": "Developer" 23 | } 24 | ], 25 | "require": { 26 | "illuminate/database": "^8.0|^9.0|^10.0|^11.0|^12.0", 27 | "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0" 28 | }, 29 | "require-dev": { 30 | "orchestra/testbench": "^9.0|^10.0", 31 | "phpunit/phpunit": "^10.5|^11.0", 32 | "symfony/thanks": "^1.0" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "GeneaLabs\\LaravelPivotEvents\\": "src" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "GeneaLabs\\LaravelPivotEvents\\Tests\\": "tests/" 42 | } 43 | }, 44 | "minimum-stability": "dev", 45 | "prefer-stable": true, 46 | "config": { 47 | "allow-plugins": { 48 | "symfony/thanks": true 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # For more information: https://laravel.com/docs/sail 2 | version: '3' 3 | services: 4 | laravel.test: 5 | image: ghcr.io/mikebronner/sail/php-8.2:latest 6 | extra_hosts: 7 | - 'host.docker.internal:host-gateway' 8 | ports: 9 | - '${APP_PORT:-80}:80' 10 | - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' 11 | environment: 12 | WWWUSER: '${WWWUSER}' 13 | LARAVEL_SAIL: 1 14 | XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}' 15 | XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}' 16 | volumes: 17 | - '.:/var/www/html' 18 | networks: 19 | - sail 20 | 21 | networks: 22 | sail: 23 | driver: bridge 24 | -------------------------------------------------------------------------------- /src/Relations/BelongsToManyCustom.php: -------------------------------------------------------------------------------- 1 | filterModelEventResults( 32 | $this->fireCustomModelEvent($event, $method) 33 | ); 34 | 35 | if (false === $result) { 36 | return false; 37 | } 38 | 39 | $payload = [ 40 | 'model' => $this, 41 | 'relation' => $relationName, 42 | 'pivotIds' => $ids, 43 | 'pivotIdsAttributes' => $idsAttributes, 44 | 0 => $this, 45 | ]; 46 | $result = $result 47 | ?: static::$dispatcher 48 | ->{$method}("eloquent.{$event}: " . static::class, $payload); 49 | $this->broadcastPivotEvent($event, $payload); 50 | 51 | return $result; 52 | } 53 | 54 | protected function broadcastPivotEvent(string $event, array $payload): void 55 | { 56 | $events = [ 57 | "pivotAttached", 58 | "pivotDetached", 59 | "pivotSynced", 60 | "pivotUpdated", 61 | ]; 62 | 63 | if (! in_array($event, $events)) { 64 | return; 65 | } 66 | 67 | $className = explode("\\", get_class($this)); 68 | $name = method_exists($this, "broadcastAs") 69 | ? $this->broadcastAs() 70 | : array_pop($className) . ucwords($event); 71 | $channels = method_exists($this, "broadcastOn") 72 | ? Arr::wrap($this->broadcastOn($event)) 73 | : []; 74 | 75 | if (empty($channels)) { 76 | return; 77 | } 78 | 79 | $connections = method_exists($this, "broadcastConnections") 80 | ? $this->broadcastConnections() 81 | : [null]; 82 | $manager = app(BroadcastingFactory::class); 83 | 84 | foreach ($connections as $connection) { 85 | $manager->connection($connection) 86 | ->broadcast($channels, $name, $payload); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Traits/ExtendRelationsTrait.php: -------------------------------------------------------------------------------- 1 | parent->fireModelEvent('pivotSyncing', true, $this->getRelationName())) { 19 | return false; 20 | } 21 | 22 | $parentResult = []; 23 | $this->parent->withoutEvents(function () use ($ids, $detaching, &$parentResult) { 24 | $parentResult = parent::sync($ids, $detaching); 25 | }); 26 | 27 | $this->parent->fireModelEvent('pivotSynced', false, $this->getRelationName(), $parentResult); 28 | 29 | return $parentResult; 30 | } 31 | 32 | /** 33 | * Attach a model to the parent. 34 | * 35 | * @param mixed $id 36 | * @param array $attributes 37 | * @param bool $touch 38 | */ 39 | public function attach($ids, array $attributes = [], $touch = true) 40 | { 41 | list($idsOnly, $idsAttributes) = $this->getIdsWithAttributes($ids, $attributes); 42 | 43 | $this->parent->fireModelEvent('pivotAttaching', true, $this->getRelationName(), $idsOnly, $idsAttributes); 44 | $parentResult = parent::attach($ids, $attributes, $touch); 45 | $this->parent->fireModelEvent('pivotAttached', false, $this->getRelationName(), $idsOnly, $idsAttributes); 46 | 47 | return $parentResult; 48 | } 49 | 50 | /** 51 | * Detach models from the relationship. 52 | * 53 | * @param mixed $ids 54 | * @param bool $touch 55 | * 56 | * @return int 57 | */ 58 | public function detach($ids = null, $touch = true) 59 | { 60 | if (is_null($ids)) { 61 | $ids = $this->query->pluck($this->query->qualifyColumn($this->relatedKey))->toArray(); 62 | } 63 | 64 | list($idsOnly) = $this->getIdsWithAttributes($ids); 65 | 66 | $this->parent->fireModelEvent('pivotDetaching', true, $this->getRelationName(), $idsOnly); 67 | $parentResult = parent::detach($ids, $touch); 68 | $this->parent->fireModelEvent('pivotDetached', false, $this->getRelationName(), $idsOnly); 69 | 70 | return $parentResult; 71 | } 72 | 73 | /** 74 | * Update an existing pivot record on the table. 75 | * 76 | * @param mixed $id 77 | * @param array $attributes 78 | * @param bool $touch 79 | * 80 | * @return int 81 | */ 82 | public function updateExistingPivot($id, array $attributes, $touch = true) 83 | { 84 | list($idsOnly, $idsAttributes) = $this->getIdsWithAttributes($id, $attributes); 85 | 86 | $this->parent->fireModelEvent('pivotUpdating', true, $this->getRelationName(), $idsOnly, $idsAttributes); 87 | $parentResult = parent::updateExistingPivot($id, $attributes, $touch); 88 | $this->parent->fireModelEvent('pivotUpdated', false, $this->getRelationName(), $idsOnly, $idsAttributes); 89 | 90 | return $parentResult; 91 | } 92 | 93 | /** 94 | * Cleans the ids and ids with attributes 95 | * Returns an array with and array of ids and array of id => attributes. 96 | * 97 | * @param mixed $id 98 | * @param array $attributes 99 | * 100 | * @return array 101 | */ 102 | private function getIdsWithAttributes($id, $attributes = []) 103 | { 104 | $ids = []; 105 | 106 | if ($id instanceof Model) { 107 | $ids[$id->getKey()] = $attributes; 108 | } elseif ($id instanceof Collection) { 109 | foreach ($id as $model) { 110 | $ids[$model->getKey()] = $attributes; 111 | } 112 | } elseif (is_array($id)) { 113 | foreach ($id as $key => $attributesArray) { 114 | if (is_array($attributesArray)) { 115 | $ids[$key] = array_merge($attributes, $attributesArray); 116 | } else { 117 | $ids[$attributesArray] = $attributes; 118 | } 119 | } 120 | } elseif (is_int($id) || is_string($id)) { 121 | $ids[$id] = $attributes; 122 | } 123 | 124 | $idsOnly = array_keys($ids); 125 | 126 | return [$idsOnly, $ids]; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Traits/PivotEventTrait.php: -------------------------------------------------------------------------------- 1 | observables 24 | ); 25 | } 26 | 27 | public static function pivotSyncing($callback, $priority = 0) 28 | { 29 | static::registerModelEvent('pivotSyncing', $callback, $priority); 30 | } 31 | 32 | public static function pivotSynced($callback, $priority = 0) 33 | { 34 | static::registerModelEvent('pivotSynced', $callback, $priority); 35 | } 36 | 37 | public static function pivotAttaching($callback, $priority = 0) 38 | { 39 | static::registerModelEvent('pivotAttaching', $callback, $priority); 40 | } 41 | 42 | public static function pivotAttached($callback, $priority = 0) 43 | { 44 | static::registerModelEvent('pivotAttached', $callback, $priority); 45 | } 46 | 47 | public static function pivotDetaching($callback, $priority = 0) 48 | { 49 | static::registerModelEvent('pivotDetaching', $callback, $priority); 50 | } 51 | 52 | public static function pivotDetached($callback, $priority = 0) 53 | { 54 | static::registerModelEvent('pivotDetached', $callback, $priority); 55 | } 56 | 57 | public static function pivotUpdating($callback, $priority = 0) 58 | { 59 | static::registerModelEvent('pivotUpdating', $callback, $priority); 60 | } 61 | 62 | public static function pivotUpdated($callback, $priority = 0) 63 | { 64 | static::registerModelEvent('pivotUpdated', $callback, $priority); 65 | } 66 | } 67 | --------------------------------------------------------------------------------