├── .styleci.yml
├── .gitattributes
├── .gitignore
├── src
├── HasOne.php
├── HasMany.php
├── MorphOne.php
├── MorphMany.php
├── MorphTo.php
├── BelongsTo.php
├── RelationshipEventsServiceProvider.php
├── Contracts
│ └── EventDispatcher.php
├── Helpers
│ └── AttributesMethods.php
├── Concerns
│ ├── HandlesMorphOneEvents.php
│ ├── HandlesHasOneEvents.php
│ ├── HandlesBelongsToEvents.php
│ ├── HasRelationshipEvents.php
│ ├── HandlesMorphManyEvents.php
│ ├── HandlesMorphToEvents.php
│ └── HandlesHasManyEvents.php
└── Traits
│ ├── HasBelongsToEvents.php
│ ├── HasOneOrManyEvents.php
│ └── HasEventDispatcher.php
├── tests
├── Stubs
│ ├── Address.php
│ ├── Comment.php
│ ├── UserObserver.php
│ ├── Profile.php
│ ├── Post.php
│ ├── ObserverProfile.php
│ └── User.php
├── TestCase.php
└── Feature
│ ├── RelationshipEventObserverTest.php
│ ├── MorphToEventsTest.php
│ ├── HasOneEventsTest.php
│ ├── MorphOneEventsTest.php
│ ├── BelongsToEventsTest.php
│ ├── HasManyEventsTest.php
│ └── MorphManyEventsTest.php
├── phpunit.xml.dist
├── .github
└── workflows
│ └── tests.yml
├── LICENSE.md
├── composer.json
├── doc
├── morph-one.md
├── morph-many.md
├── has-many.md
├── has-one.md
├── morph-to.md
└── belongs-to.md
└── README.md
/.styleci.yml:
--------------------------------------------------------------------------------
1 | php:
2 | preset: laravel
3 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 | *.css linguist-vendored
3 | *.scss linguist-vendored
4 | *.js linguist-vendored
5 | CHANGELOG.md export-ignore
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /public/hot
3 | /public/storage
4 | /storage/*.key
5 | /vendor
6 | /.idea
7 | /.vscode
8 | /.vagrant
9 | Homestead.json
10 | Homestead.yaml
11 | npm-debug.log
12 | yarn-error.log
13 | .env
14 | .DS_Store
15 | /composer.lock
16 | .phpunit.result.cache
17 |
--------------------------------------------------------------------------------
/src/HasOne.php:
--------------------------------------------------------------------------------
1 | increments('id');
17 | $table->string('addressable_id');
18 | $table->string('addressable_type');
19 | $table->string('name')->nullable();
20 | $table->timestamps();
21 | });
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ./src
6 |
7 |
8 |
9 |
10 | ./tests/
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/RelationshipEventsServiceProvider.php:
--------------------------------------------------------------------------------
1 | app['events']);
18 | HasMany::setEventDispatcher($this->app['events']);
19 | HasOne::setEventDispatcher($this->app['events']);
20 | MorphMany::setEventDispatcher($this->app['events']);
21 | MorphOne::setEventDispatcher($this->app['events']);
22 | MorphTo::setEventDispatcher($this->app['events']);
23 | }
24 |
25 | /**
26 | * Register the service provider.
27 | */
28 | public function register()
29 | {
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tests/Stubs/Comment.php:
--------------------------------------------------------------------------------
1 | increments('id');
20 | $table->string('commentable_id')->nullable();
21 | $table->string('commentable_type')->nullable();
22 | $table->string('name')->nullable();
23 | $table->timestamps();
24 | });
25 | }
26 |
27 | public function post()
28 | {
29 | return $this->morphTo(Post::class)->withEvents();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tests/Stubs/UserObserver.php:
--------------------------------------------------------------------------------
1 | name == 'badName') return false;
18 | }
19 |
20 | /**
21 | * Handle the User "observerProfileSaving" event.
22 | *
23 | * @param Artificertech\RelationshipEvents\Tests\Stubs\User $user
24 | * @param Artificertech\RelationshipEvents\Tests\Stubs\ObserverProfile $observerProfile
25 | *
26 | * @return void
27 | */
28 | public function observerProfileSaving($user, $observerProfile)
29 | {
30 | if ($observerProfile->name == 'badName') return false;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | set('database.default', 'testbench');
31 | $app['config']->set('database.connections.testbench', [
32 | 'driver' => 'sqlite',
33 | 'database' => ':memory:',
34 | 'prefix' => '',
35 | ]);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: tests
2 |
3 | on:
4 | push:
5 | pull_request:
6 |
7 | jobs:
8 | tests:
9 | runs-on: ubuntu-latest
10 | strategy:
11 | fail-fast: true
12 | matrix:
13 | php: [7.4, 8.0, 8.1]
14 | stability: [prefer-stable]
15 |
16 | name: P${{ matrix.php }} - S${{ matrix.stability }}
17 |
18 | steps:
19 | - name: Checkout code
20 | uses: actions/checkout@v2
21 |
22 | - name: Cache dependencies
23 | uses: actions/cache@v1
24 | with:
25 | path: ~/.composer/cache/files
26 | key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
27 |
28 | - name: Setup PHP
29 | uses: shivammathur/setup-php@v2
30 | with:
31 | php-version: ${{ matrix.php }}
32 | extensions: pdo, sqlite, pdo_sqlite
33 | coverage: none
34 |
35 | - name: Install dependencies
36 | run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress
37 |
38 | - name: Execute tests
39 | run: vendor/bin/phpunit --verbose
40 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Cole Shirley
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 |
--------------------------------------------------------------------------------
/src/Contracts/EventDispatcher.php:
--------------------------------------------------------------------------------
1 | name == 'badName') return false;
20 | });
21 |
22 | static::belongsToDissociating('user', function ($profile, $user) {
23 | if ($user->name == 'badDissociateName') return false;
24 | });
25 | }
26 |
27 | public static function setupTable()
28 | {
29 | Schema::create('profiles', function (Blueprint $table) {
30 | $table->increments('id');
31 | $table->unsignedInteger('user_id')->nullable();
32 | $table->string('name')->nullable();
33 | $table->timestamps();
34 | });
35 | }
36 |
37 | public function user()
38 | {
39 | return $this->belongsTo(User::class)->withEvents('user');
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tests/Stubs/Post.php:
--------------------------------------------------------------------------------
1 | name == 'badName') return false;
20 | });
21 |
22 | static::morphManySaving('comments', function ($post, $comment) {
23 | if ($comment->name == 'badName') return false;
24 | });
25 | }
26 |
27 | public static function setupTable()
28 | {
29 | Schema::create('posts', function (Blueprint $table) {
30 | $table->increments('id');
31 | $table->unsignedInteger('user_id')->nullable();
32 | $table->string('name')->nullable();
33 | $table->timestamps();
34 | });
35 | }
36 |
37 | public function comments()
38 | {
39 | return $this->morphMany(Comment::class, 'commentable')->withEvents();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tests/Stubs/ObserverProfile.php:
--------------------------------------------------------------------------------
1 | name == 'badName') return false;
20 | });
21 |
22 | static::belongsToDissociating('user', function ($profile, $user) {
23 | if ($user->name == 'badDissociateName') return false;
24 | });
25 | }
26 |
27 | public static function setupTable()
28 | {
29 | Schema::create('observer_profiles', function (Blueprint $table) {
30 | $table->increments('id');
31 | $table->unsignedInteger('user_id')->nullable();
32 | $table->string('name')->nullable();
33 | $table->timestamps();
34 | });
35 | }
36 |
37 | public function user()
38 | {
39 | return $this->belongsTo(User::class)->withEvents('user');
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tests/Feature/RelationshipEventObserverTest.php:
--------------------------------------------------------------------------------
1 | observerProfile()->create(['name' => 'badName']);
26 |
27 | $this->assertEquals(null, $user->observerProfile);
28 | }
29 |
30 | /** @test */
31 | public function if_false_is_returned_from_the_saving_event_observer_then_the_save_is_canceled()
32 | {
33 | $user = User::create();
34 | $user->observerProfile()->save(new ObserverProfile(['name' => 'badName']));
35 |
36 | $this->assertEquals(null, $user->observerProfile);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "artificertech/laravel-relationship-events",
3 | "description": "Missing relationship events for Laravel",
4 | "homepage": "https://github.com/artificertech/laravel-relationship-events",
5 | "license": "MIT",
6 | "keywords": [
7 | "laravel",
8 | "relationship",
9 | "relations",
10 | "events"
11 | ],
12 | "authors": [
13 | {
14 | "name": "Cole Shirley",
15 | "email": "cole.shirley@artificertech.com"
16 | }
17 | ],
18 | "require": {
19 | "php": "^7.4|^8.0",
20 | "illuminate/container": "^8.0|^9.0|^10.0",
21 | "illuminate/database": "^8.0|^9.0|^10.0",
22 | "illuminate/events": "^8.0|^9.0|^10.0",
23 | "illuminate/support": "^8.0|^9.0|^10.0"
24 | },
25 | "require-dev": {
26 | "phpunit/phpunit": "^9.0|^9.3|^9.5",
27 | "orchestra/testbench": "^6.0|^7.0|^8.0"
28 | },
29 | "autoload": {
30 | "psr-4": {
31 | "Artificertech\\RelationshipEvents\\": "src/"
32 | }
33 | },
34 | "autoload-dev": {
35 | "psr-4": {
36 | "Artificertech\\RelationshipEvents\\Tests\\": "tests/"
37 | }
38 | },
39 | "minimum-stability": "stable",
40 | "extra": {
41 | "laravel": {
42 | "providers": [
43 | "Artificertech\\RelationshipEvents\\RelationshipEventsServiceProvider"
44 | ]
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/src/Helpers/AttributesMethods.php:
--------------------------------------------------------------------------------
1 | getKey()];
22 | }
23 |
24 | if ($value instanceof Collection) {
25 | return $value->modelKeys();
26 | }
27 |
28 | if ($value instanceof BaseCollection) {
29 | return $value->toArray();
30 | }
31 |
32 | return (array) $value;
33 | }
34 |
35 | /**
36 | * Parse ids for event.
37 | *
38 | * @param array $ids
39 | *
40 | * @return array
41 | */
42 | public static function parseIdsForEvent(array $ids): array
43 | {
44 | return array_map(function ($key, $id) {
45 | return is_array($id) ? $key : $id;
46 | }, array_keys($ids), $ids);
47 | }
48 |
49 | /**
50 | * Parse attributes for event.
51 | *
52 | * @param mixed $rawIds
53 | * @param array $parsedIds
54 | * @param array $attributes
55 | *
56 | * @return array
57 | */
58 | public static function parseAttributesForEvent($rawIds, array $parsedIds, array $attributes = []): array
59 | {
60 | return is_array($rawIds) ? array_filter($parsedIds, function ($id) {
61 | return is_array($id) && ! empty($id);
62 | }) : $attributes;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Concerns/HandlesMorphOneEvents.php:
--------------------------------------------------------------------------------
1 | post()->associate($post);
28 |
29 | Event::assertDispatched(
30 | 'eloquent.postAssociating: ' . Comment::class,
31 | function ($event, $callback) use ($post, $comment) {
32 | return $callback[0]->is($comment) && $callback[1]->is($post);
33 | }
34 | );
35 | Event::assertDispatched(
36 | 'eloquent.postAssociated: ' . Comment::class,
37 | function ($event, $callback) use ($post, $comment) {
38 | return $callback[0]->is($comment) && $callback[1]->is($post);
39 | }
40 | );
41 | }
42 |
43 | /** @test */
44 | public function it_fires_morphToDissociating_and_morphToDissociated()
45 | {
46 | Event::fake();
47 |
48 | $post = Post::create();
49 | $comment = Comment::create();
50 | $comment->post()->associate($post);
51 | $comment->post()->dissociate($post);
52 |
53 | Event::assertDispatched(
54 | 'eloquent.postDissociating: ' . Comment::class,
55 | function ($event, $callback) use ($post, $comment) {
56 | return $callback[0]->is($comment) && $callback[1]->is($post);
57 | }
58 | );
59 | Event::assertDispatched(
60 | 'eloquent.postDissociated: ' . Comment::class,
61 | function ($event, $callback) use ($post, $comment) {
62 | return $callback[0]->is($comment) && $callback[1]->is($post);
63 | }
64 | );
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Concerns/HandlesBelongsToEvents.php:
--------------------------------------------------------------------------------
1 | willDispatchEvents() && $this->child->fireModelRelationshipEvent('associating', $this->eventRelationship, true, $this->child, $model) === false) {
24 | return false;
25 | }
26 |
27 | $result = parent::associate($model);
28 |
29 | if ($result && $this->willDispatchEvents()) {
30 | $this->child->fireModelRelationshipEvent('associated', $this->eventRelationship, false, $this->child, $model);
31 | }
32 |
33 | return $result;
34 | }
35 |
36 | /**
37 | * Dissociate previously associated model from the given parent.
38 | *
39 | * @return \Illuminate\Database\Eloquent\Model
40 | */
41 | public function dissociate()
42 | {
43 | $parent = $this->getResults();
44 |
45 | // If the "dissociating" event returns false we'll bail out of the dissociate and return
46 | // false, indicating that the dissociate failed. This provides a chance for any
47 | // listeners to cancel dissociate operations if validations fail or whatever.
48 | if ($this->willDispatchEvents() && $this->child->fireModelRelationshipEvent('dissociating', $this->eventRelationship, true, $this->child, $parent) === false) {
49 | return false;
50 | }
51 |
52 | $result = parent::dissociate();
53 |
54 | if (!is_null($parent) && $this->willDispatchEvents()) {
55 | $this->child->fireModelRelationshipEvent('dissociated', $this->eventRelationship, false, $this->child, $parent);
56 | }
57 |
58 | return $result;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/tests/Stubs/User.php:
--------------------------------------------------------------------------------
1 | name == 'badName') return false;
32 | });
33 |
34 | static::hasManySaving('posts', function ($user, $post) {
35 | if ($post->name == 'badName') return false;
36 | });
37 |
38 | static::hasOneCreating('profile', function ($user, $profile) {
39 | if ($profile->name == 'badName') return false;
40 | });
41 |
42 | static::hasOneSaving('profile', function ($user, $profile) {
43 | if ($profile->name == 'badName') return false;
44 | });
45 |
46 | static::morphOneCreating('address', function ($user, $address) {
47 | if ($address->name == 'badName') return false;
48 | });
49 |
50 | static::morphOneSaving('address', function ($user, $address) {
51 | if ($address->name == 'badName') return false;
52 | });
53 | }
54 |
55 | public static function setupTable()
56 | {
57 | Schema::create('users', function (Blueprint $table) {
58 | $table->increments('id');
59 | $table->string('name')->nullable();
60 | $table->timestamps();
61 | });
62 | }
63 |
64 | public function profile()
65 | {
66 | return $this->hasOne(Profile::class)->withEvents();
67 | }
68 |
69 | public function posts()
70 | {
71 | return $this->hasMany(Post::class)->withEvents();
72 | }
73 |
74 | public function address()
75 | {
76 | return $this->morphOne(Address::class, 'addressable')->withEvents();
77 | }
78 |
79 | public function observerProfile()
80 | {
81 | return $this->hasOne(ObserverProfile::class)->withEvents();
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/Concerns/HasRelationshipEvents.php:
--------------------------------------------------------------------------------
1 | getEventName($event, $relation);
38 |
39 | // First, we will get the proper method to call on the event dispatcher, and then we
40 | // will attempt to fire a custom, object based event for the given event. If that
41 | // returns a result we can return that result, or we'll call the string events.
42 | $method = $halt ? 'until' : 'dispatch';
43 |
44 | $result = $this->filterModelEventResults(
45 | $this->fireCustomModelRelationshipEvent($event, $method, $params)
46 | );
47 |
48 | if (false === $result) {
49 | return false;
50 | }
51 |
52 | return !empty($result) ? $result : static::$dispatcher->{$method}(
53 | "eloquent.{$event}: " . static::class,
54 | $params
55 | );
56 | }
57 |
58 | protected function getEventName($event, $relation)
59 | {
60 | return $relation . ucfirst($event);
61 | }
62 |
63 | /**
64 | * Fire a custom model event for the given event.
65 | *
66 | * @param string $event
67 | * @param string $method
68 | * @param array $params
69 | *
70 | * @return mixed|null
71 | */
72 | protected function fireCustomModelRelationshipEvent($event, $method, ...$params)
73 | {
74 | if (!isset($this->dispatchesEvents[$event])) {
75 | return;
76 | }
77 |
78 | $result = static::$dispatcher->$method(new $this->dispatchesEvents[$event]($this, $params));
79 |
80 | if (!is_null($result)) {
81 | return $result;
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Traits/HasOneOrManyEvents.php:
--------------------------------------------------------------------------------
1 | willDispatchEvents() && $this->parent->fireModelRelationshipEvent('saving', $this->eventRelationship, true, $this->parent, $model) === false) {
29 | return false;
30 | }
31 |
32 | $result = parent::save($model);
33 |
34 | if (false !== $result && $this->willDispatchEvents()) {
35 | $this->parent->fireModelRelationshipEvent('saved', $this->eventRelationship, false, $this->parent, $result);
36 | }
37 |
38 | return $result;
39 | }
40 |
41 | /**
42 | * Create a new instance of the related model.
43 | *
44 | * @param array $attributes
45 | *
46 | * @return \Illuminate\Database\Eloquent\Model
47 | */
48 | public function create(array $attributes = [])
49 | {
50 |
51 | return tap($this->related->newInstance($attributes), function ($instance) {
52 | // If the "creating" event returns false we'll bail out of the create and return
53 | // false, indicating that the create failed. This provides a chance for any
54 | // listeners to cancel create operations if validations fail or whatever.
55 | $eventResult = $this->parent->fireModelRelationshipEvent('creating', $this->eventRelationship, true, $this->parent, $instance);
56 |
57 | if ($this->willDispatchEvents() && $eventResult === false) {
58 | return false;
59 | }
60 |
61 | $this->setForeignAttributesForCreate($instance);
62 |
63 | if ($instance->save() && $this->willDispatchEvents()) {
64 | $this->parent->fireModelRelationshipEvent('created', $this->eventRelationship, false, $this->parent, $instance);
65 | }
66 | });
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Concerns/HandlesMorphManyEvents.php:
--------------------------------------------------------------------------------
1 | fireModelRelationshipEvent($event, $relation, true, $this, $parent);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Concerns/HandlesMorphToEvents.php:
--------------------------------------------------------------------------------
1 | fireModelRelationshipEvent($event, $relation, true, $this, $parent);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/tests/Feature/HasOneEventsTest.php:
--------------------------------------------------------------------------------
1 | profile()->create([]);
27 |
28 | Event::assertDispatched(
29 | 'eloquent.profileCreating: ' . User::class,
30 | function ($event, $callback) use ($user, $profile) {
31 | return $callback[0]->is($user) && $callback[1]->is($profile);
32 | }
33 | );
34 | Event::assertDispatched(
35 | 'eloquent.profileCreated: ' . User::class,
36 | function ($event, $callback) use ($user, $profile) {
37 | return $callback[0]->is($user) && $callback[1]->is($profile);
38 | }
39 | );
40 | }
41 |
42 | /** @test */
43 | public function if_false_is_returned_from_the_creating_event_then_the_create_is_canceled()
44 | {
45 | $user = User::create();
46 | $user->profile()->create(['name' => 'badName']);
47 |
48 | $this->assertEquals(null, $user->profile);
49 | }
50 |
51 | /** @test */
52 | public function it_fires_hasOneSaving_and_hasOneSaved_when_a_belonged_model_saved()
53 | {
54 | Event::fake();
55 |
56 | $user = User::create();
57 | $profile = $user->profile()->save(new Profile);
58 |
59 | Event::assertDispatched(
60 | 'eloquent.profileSaving: ' . User::class,
61 | function ($event, $callback) use ($user, $profile) {
62 | return $callback[0]->is($user) && $callback[1]->is($profile);
63 | }
64 | );
65 | Event::assertDispatched(
66 | 'eloquent.profileSaved: ' . User::class,
67 | function ($event, $callback) use ($user, $profile) {
68 | return $callback[0]->is($user) && $callback[1]->is($profile);
69 | }
70 | );
71 | }
72 |
73 | /** @test */
74 | public function if_false_is_returned_from_the_saving_event_then_the_save_is_canceled()
75 | {
76 | $user = User::create();
77 | $user->profile()->save(new Profile(['name' => 'badName']));
78 |
79 | $this->assertEquals(null, $user->profile);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/tests/Feature/MorphOneEventsTest.php:
--------------------------------------------------------------------------------
1 | address()->create([]);
27 |
28 | Event::assertDispatched(
29 | 'eloquent.addressCreating: ' . User::class,
30 | function ($event, $callback) use ($user, $address) {
31 | return $callback[0]->is($user) && $callback[1]->is($address);
32 | }
33 | );
34 | Event::assertDispatched(
35 | 'eloquent.addressCreated: ' . User::class,
36 | function ($event, $callback) use ($user, $address) {
37 | return $callback[0]->is($user) && $callback[1]->is($address);
38 | }
39 | );
40 | }
41 |
42 | /** @test */
43 | public function if_false_is_returned_from_the_creating_event_then_the_create_is_canceled()
44 | {
45 | $user = User::create();
46 | $user->address()->create(['name' => 'badName']);
47 |
48 | $this->assertEquals(null, $user->address);
49 | }
50 |
51 | /** @test */
52 | public function it_fires_morphOneSaving_and_morphOneSaved_when_belonged_model_with_morph_one_saved()
53 | {
54 | Event::fake();
55 |
56 | $user = User::create();
57 | $address = $user->address()->save(new Address);
58 |
59 | Event::assertDispatched(
60 | 'eloquent.addressSaving: ' . User::class,
61 | function ($event, $callback) use ($user, $address) {
62 | return $callback[0]->is($user) && $callback[1]->is($address);
63 | }
64 | );
65 | Event::assertDispatched(
66 | 'eloquent.addressSaved: ' . User::class,
67 | function ($event, $callback) use ($user, $address) {
68 | return $callback[0]->is($user) && $callback[1]->is($address);
69 | }
70 | );
71 | }
72 |
73 | /** @test */
74 | public function if_false_is_returned_from_the_saving_event_then_the_save_is_canceled()
75 | {
76 | $user = User::create();
77 | $user->address()->save(new Address(['name' => 'badName']));
78 |
79 | $this->assertEquals(null, $user->address);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Concerns/HandlesHasManyEvents.php:
--------------------------------------------------------------------------------
1 | fireModelRelationshipEvent($event, $relation, true, $this, $parent);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/Traits/HasEventDispatcher.php:
--------------------------------------------------------------------------------
1 | guessEventRelationship();
66 | }
67 |
68 | $this->eventRelationship = $eventRelationship;
69 |
70 | return $this;
71 | }
72 |
73 | /**
74 | * Turn off events for this relationship
75 | */
76 | public function withoutEvents()
77 | {
78 | $this->eventRelationship = null;
79 |
80 | return $this;
81 | }
82 |
83 | /**
84 | * Guess the "belongs to" relationship name.
85 | *
86 | * @return string
87 | */
88 | protected function guessEventRelationship()
89 | {
90 | [$one, $two, $caller] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
91 |
92 | return $caller['function'];
93 | }
94 |
95 | public function willDispatchEvents(): bool
96 | {
97 | return !is_null($this->eventRelationship);
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/tests/Feature/BelongsToEventsTest.php:
--------------------------------------------------------------------------------
1 | user()->associate($user = User::create());
27 |
28 | Event::assertDispatched(
29 | 'eloquent.userAssociating: ' . Profile::class,
30 | function ($event, $callback) use ($user, $profile) {
31 | return $callback[0]->is($profile) && $callback[1]->is($user);
32 | }
33 | );
34 | Event::assertDispatched(
35 | 'eloquent.userAssociated: ' . Profile::class,
36 | function ($event, $callback) use ($user, $profile) {
37 | return $callback[0]->is($profile) && $callback[1]->is($user);
38 | }
39 | );
40 | }
41 |
42 | /** @test */
43 | public function if_false_is_returned_from_the_associating_event_then_the_associate_is_canceled()
44 | {
45 | $profile = Profile::create();
46 | $profile->user()->associate(new User(['name' => 'badName']));
47 |
48 | $this->assertEquals(null, $profile->user);
49 | }
50 |
51 | /** @test */
52 | public function it_fires_belongsToDissociating_and_belongsToDissociated_when_a_model_dissociated()
53 | {
54 | Event::fake();
55 |
56 | $profile = Profile::create();
57 | $profile->user()->associate($user = User::create());
58 | $profile->user()->dissociate();
59 |
60 | Event::assertDispatched(
61 | 'eloquent.userDissociating: ' . Profile::class,
62 | function ($event, $callback) use ($user, $profile) {
63 | return $callback[0]->is($profile) && $callback[1]->is($user);
64 | }
65 | );
66 | Event::assertDispatched(
67 | 'eloquent.userDissociated: ' . Profile::class,
68 | function ($event, $callback) use ($user, $profile) {
69 | return $callback[0]->is($profile) && $callback[1]->is($user);
70 | }
71 | );
72 | }
73 |
74 | /** @test */
75 | public function if_false_is_returned_from_the_dissociating_event_then_the_dissociate_is_canceled()
76 | {
77 | $profile = Profile::create();
78 | $profile->user()->associate($user = User::create(['name' => 'badDissociateName']));
79 | $profile->user()->dissociate();
80 |
81 | $this->assertTrue($profile->user->is($user));
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/doc/morph-one.md:
--------------------------------------------------------------------------------
1 | # Has One Relations:
2 |
3 | ```User``` model might be associated with one ```Address```
4 |
5 | ```php
6 | namespace App\Models;
7 |
8 | use Artificertech\RelationshipEvents\Concerns\HasRelationshipEvents;
9 | use Illuminate\Database\Eloquent\Model;
10 |
11 | class User extends Model
12 | {
13 | use HasRelationshipEvents;
14 |
15 | /**
16 | * Get the address associated with the user.
17 | */
18 | public function address()
19 | {
20 | return $this->morphOne(Address::class)->withEvents();
21 | }
22 | }
23 | ```
24 |
25 | Now we can use methods to assosiate ```User``` with ```Address``` and also update assosiated model.
26 |
27 | ```php
28 | // ...
29 | $user = factory(User::class)->create([
30 | 'name' => 'John Smith',
31 | ]);
32 |
33 | $user->address()->create([
34 | 'name' => 'home'
35 | ]);
36 | // ...
37 | ```
38 |
39 | Now we should listen our events, for example we can register event listners in model's boot method:
40 | ```php
41 | // ...
42 | protected static function boot()
43 | {
44 | parent::boot();
45 |
46 | static::morphOneCreating('address', function ($user, $address) {
47 | Log::info("Creating address: {$address->name} for user {$user->name}.");
48 | });
49 |
50 | static::morphOneCreated('address', function ($user, $address) {
51 | Log::info("Address: {$address->name} for user: {$user->name} has been created.");
52 | });
53 |
54 | static::morphOneSaving('address', function ($user, $address) {
55 | Log::info("Saving address: {$address->name} for user {$user->name}.");
56 | });
57 |
58 | static::morphOneSaved('address', function ($user, $address) {
59 | Log::info("Address: {$address->name} for user: {$user->name} has been saved.");
60 | });
61 | }
62 | // ...
63 | ```
64 |
65 | Dispatch Events with the event dispatcher
66 | ```php
67 | // ...
68 | protected $dispatchesEvents = [
69 | 'addressCreating' => UserAddressCreating::class,
70 | 'addressCreated' => UserAddressCreated::class,
71 | 'addressSaving' => UserAddressSaveing::class,
72 | 'addressSaving' => UserAddressSaving::class,
73 | ];
74 | // ...
75 | ```
76 |
77 | Or you may use an Observer. Be sure to define the observable events in your model class - See [Detecting Observable Events](../README.md#detecting-observable-events)
78 | ```php
79 | namespace App\Observer;
80 |
81 | class UserObserver
82 | {
83 | /**
84 | * Handle the User "addressCreating" event.
85 | *
86 | * @param \App\Models\User $user
87 | * @param \App\Models\Address $address
88 | *
89 | * @return void
90 | */
91 | public function addressCreating(User $user, Address $address)
92 | {
93 | Log::info("Creating address: {$address->name} for user {$user->name}.");
94 | }
95 |
96 | /**
97 | * Handle the User "addressCreated" event.
98 | *
99 | * @param \App\Models\User $user
100 | * @param \App\Models\Address $address
101 | *
102 | * @return void
103 | */
104 | public function addressCreated(User $user, Address $address)
105 | {
106 | Log::info("Address: {$address->name} for user: {$user->name} has been created.");
107 | }
108 |
109 | /**
110 | * Handle the User "addressSaving" event.
111 | *
112 | * @param \App\Models\User $user
113 | * @param \App\Models\Address $address
114 | *
115 | * @return void
116 | */
117 | public function addressSaving(User $user, Address $address)
118 | {
119 | Log::info("Saving address: {$address->name} for user {$user->name}.");
120 | }
121 |
122 | /**
123 | * Handle the User "addressSaved" event.
124 | *
125 | * @param \App\Models\User $user
126 | * @param \App\Models\Address $address
127 | *
128 | * @return void
129 | */
130 | public function addressSaved(User $user, Address $address)
131 | {
132 | Log::info("Address: {$address->name} for user: {$user->name} has been saved.");
133 | }
134 | }
135 | ```
136 |
137 | ### Available methods and events
138 |
139 | #### MorphOne::create
140 | - fires {relationship}Creating, {relationship}Created
141 | - events have $parent and $related models
142 |
143 | #### MorphOne::save
144 | - fires {relationship}Saving, {relationship}Saved
145 | - events have $parent and $related models
--------------------------------------------------------------------------------
/doc/morph-many.md:
--------------------------------------------------------------------------------
1 | # Has One Relations:
2 |
3 | ```Post``` model might be associated with one ```Comments```
4 |
5 | ```php
6 | namespace App\Models;
7 |
8 | use Artificertech\RelationshipEvents\Concerns\HasRelationshipEvents;
9 | use Illuminate\Database\Eloquent\Model;
10 |
11 | class Post extends Model
12 | {
13 | use HasRelationshipEvents;
14 |
15 | /**
16 | * Get the comments associated with the post.
17 | */
18 | public function comments()
19 | {
20 | return $this->morphMany(Comment::class, 'commentable')->withEvents();
21 | }
22 | }
23 | ```
24 |
25 | Now we can use methods to assosiate ```Post``` with ```Comment``` and also update associated model.
26 |
27 | ```php
28 | // ...
29 | $post = factory(Post::class)->create([
30 | 'name' => 'John Smith',
31 | ]);
32 |
33 | $post->comments()->create([
34 | 'name' => 'my comment text',
35 | ]);
36 | // ...
37 | ```
38 |
39 | Now we should listen our events, for example we can register event listners in model's boot method:
40 | ```php
41 | // ...
42 | protected static function boot()
43 | {
44 | parent::boot();
45 |
46 | static::hasManyCreating('comments', function ($post, $comment) {
47 | Log::info("Creating comment: {$comment->name} for post {$post->name}.");
48 | });
49 |
50 | static::hasManyCreated('comments', function ($post, $comment) {
51 | Log::info("Comment: {$comment->name} for post: {$post->name} has been created.");
52 | });
53 |
54 | static::hasManySaving('comments', function ($post, $comment) {
55 | Log::info("Saving comment: {$comment->name} for post {$post->name}.");
56 | });
57 |
58 | static::hasManySaved('comments', function ($post, $comment) {
59 | Log::info("Comment: {$comment->name} for post: {$post->name} has been saved.");
60 | });
61 | }
62 | // ...
63 | ```
64 |
65 | Dispatch Events with the event dispatcher
66 | ```php
67 | // ...
68 | protected $dispatchesEvents = [
69 | 'commentsCreating' => PostCommentsCreating::class,
70 | 'commentsCreated' => PostCommentsCreated::class,
71 | 'commentsSaving' => PostCommentsSaveing::class,
72 | 'commentsSaving' => PostCommentsSaving::class,
73 | ];
74 | // ...
75 | ```
76 |
77 | Or you may use an Observer. Be sure to define the observable events in your model class - See [Detecting Observable Events](../README.md#detecting-observable-events)
78 | ```php
79 | namespace App\Observer;
80 |
81 | class PostObserver
82 | {
83 | /**
84 | * Handle the Post "commentsCreating" event.
85 | *
86 | * @param \App\Models\Post $post
87 | * @param \App\Models\Comments $comment
88 | *
89 | * @return void
90 | */
91 | public function commentsCreating(Post $post, Comments $comment)
92 | {
93 | Log::info("Creating comment: {$comment->name} for post {$post->name}.");
94 | }
95 |
96 | /**
97 | * Handle the Post "commentsCreated" event.
98 | *
99 | * @param \App\Models\Post $post
100 | * @param \App\Models\Comments $comment
101 | *
102 | * @return void
103 | */
104 | public function commentsCreated(Post $post, Comments $comment)
105 | {
106 | Log::info("Comment: {$comment->name} for post: {$post->name} has been created.");
107 | }
108 |
109 | /**
110 | * Handle the Post "commentsSaving" event.
111 | *
112 | * @param \App\Models\Post $post
113 | * @param \App\Models\Comments $comment
114 | *
115 | * @return void
116 | */
117 | public function commentsSaving(Post $post, Comments $comment)
118 | {
119 | Log::info("Saving comment: {$comment->name} for post {$post->name}.");
120 | }
121 |
122 | /**
123 | * Handle the Post "commentsSaved" event.
124 | *
125 | * @param \App\Models\Post $post
126 | * @param \App\Models\Comments $comment
127 | *
128 | * @return void
129 | */
130 | public function commentsSaved(Post $post, Comments $comment)
131 | {
132 | Log::info("Comment: {$comment->name} for post: {$post->name} has been saved.");
133 | }
134 | }
135 | ```
136 |
137 | ### Available methods and events
138 |
139 | #### MorphMany::create
140 | - fires {relationship}Creating, {relationship}Created
141 | - events have $parent and $related models
142 |
143 | #### MorphMany::save
144 | - fires {relationship}Saving, {relationship}Saved
145 | - events have $parent and $related models
--------------------------------------------------------------------------------
/doc/has-many.md:
--------------------------------------------------------------------------------
1 | # Has Many Relations:
2 |
3 | ```User``` model might be associated with many ```Posts```
4 |
5 | ```php
6 | namespace App\Models;
7 |
8 | use Artificertech\RelationshipEvents\Concerns\HasRelationshipEvents;
9 | use Illuminate\Database\Eloquent\Model;
10 |
11 | class User extends Model
12 | {
13 | use HasRelationshipEvents;
14 |
15 | /**
16 | * Get the posts associated with the user.
17 | */
18 | public function posts()
19 | {
20 | return $this->hasMany(Post::class)->withEvents();
21 | }
22 | }
23 | ```
24 |
25 | Now we can use methods to assosiate ```User``` with ```Post``` and also update associated model.
26 |
27 | ```php
28 | // ...
29 | $user = factory(User::class)->create([
30 | 'name' => 'John Smith',
31 | ]);
32 |
33 | // Create posts and assosiate it with user
34 | // This will fire two events hasManyCreating, hasManyCreated
35 | $user->posts()->create([
36 | 'phone' => '8-800-123-45-67',
37 | 'email' => 'user@example.com',
38 | 'address' => 'One Infinite Loop Cupertino, CA 95014',
39 | ]);
40 | // ...
41 | ```
42 |
43 | Now we should listen our events, for example we can register event listners in model's boot method:
44 | ```php
45 | // ...
46 | protected static function boot()
47 | {
48 | parent::boot();
49 |
50 | static::hasManyCreating('posts', function ($user, $post) {
51 | Log::info("Creating post: {$post->name} for user {$user->name}.");
52 | });
53 |
54 | static::hasManyCreated('posts', function ($user, $post) {
55 | Log::info("Post: {$post->name} for user: {$user->name} has been created.");
56 | });
57 |
58 | static::hasManySaving('posts', function ($user, $post) {
59 | Log::info("Saving post: {$post->name} for user {$user->name}.");
60 | });
61 |
62 | static::hasManySaved('posts', function ($user, $post) {
63 | Log::info("Post: {$post->name} for user: {$user->name} has been saved.");
64 | });
65 | }
66 | // ...
67 | ```
68 |
69 | Dispatch Events with the event dispatcher
70 | ```php
71 | // ...
72 | protected $dispatchesEvents = [
73 | 'postsCreating' => UserPostsCreating::class,
74 | 'postsCreated' => UserPostsCreated::class,
75 | 'postsSaving' => UserPostsSaveing::class,
76 | 'postsSaving' => UserPostsSaving::class,
77 | ];
78 | // ...
79 | ```
80 |
81 | Or you may use an Observer. Be sure to define the observable events in your model class - See [Detecting Observable Events](../README.md#detecting-observable-events)
82 | ```php
83 | namespace App\Observer;
84 |
85 | class UserObserver
86 | {
87 | /**
88 | * Handle the User "postsCreating" event.
89 | *
90 | * @param \App\Models\User $user
91 | * @param \App\Models\Posts $post
92 | *
93 | * @return void
94 | */
95 | public function postsCreating(User $user, Posts $post)
96 | {
97 | Log::info("Creating post: {$post->name} for user {$user->name}.");
98 | }
99 |
100 | /**
101 | * Handle the User "postsCreated" event.
102 | *
103 | * @param \App\Models\User $user
104 | * @param \App\Models\Posts $post
105 | *
106 | * @return void
107 | */
108 | public function postsCreated(User $user, Posts $post)
109 | {
110 | Log::info("Post: {$post->name} for user: {$user->name} has been created.");
111 | }
112 |
113 | /**
114 | * Handle the User "postsSaving" event.
115 | *
116 | * @param \App\Models\User $user
117 | * @param \App\Models\Posts $post
118 | *
119 | * @return void
120 | */
121 | public function postsSaving(User $user, Posts $post)
122 | {
123 | Log::info("Saving post: {$post->name} for user {$user->name}.");
124 | }
125 |
126 | /**
127 | * Handle the User "postsSaved" event.
128 | *
129 | * @param \App\Models\User $user
130 | * @param \App\Models\Posts $post
131 | *
132 | * @return void
133 | */
134 | public function postsSaved(User $user, Posts $post)
135 | {
136 | Log::info("Post: {$post->name} for user: {$user->name} has been saved.");
137 | }
138 | }
139 | ```
140 |
141 | ### Available methods and events
142 |
143 | #### HasMany::create
144 | - fires {relationship}Creating, {relationship}Created
145 | - events have $parent and $related models
146 |
147 | #### HasMany::save
148 | - fires {relationship}Saving, {relationship}Saved
149 | - events have $parent and $related models
--------------------------------------------------------------------------------
/doc/has-one.md:
--------------------------------------------------------------------------------
1 | # Has One Relations:
2 |
3 | ```User``` model might be associated with one ```Profile```
4 |
5 | ```php
6 | namespace App\Models;
7 |
8 | use Artificertech\RelationshipEvents\Concerns\HasRelationshipEvents;
9 | use Illuminate\Database\Eloquent\Model;
10 |
11 | class User extends Model
12 | {
13 | use HasRelationshipEvents;
14 |
15 | /**
16 | * Get the profile associated with the user.
17 | */
18 | public function profile()
19 | {
20 | return $this->hasOne(Profile::class)->withEvents();
21 | }
22 | }
23 | ```
24 |
25 | Now we can use methods to assosiate ```User``` with ```Profile``` and also update associated model.
26 |
27 | ```php
28 | // ...
29 | $user = factory(User::class)->create([
30 | 'name' => 'John Smith',
31 | ]);
32 |
33 | $user->profile()->create([
34 | 'phone' => '8-800-123-45-67',
35 | 'email' => 'user@example.com',
36 | 'address' => 'One Infinite Loop Cupertino, CA 95014',
37 | ]);
38 | // ...
39 | ```
40 |
41 | Now we should listen our events, for example we can register event listners in model's boot method:
42 | ```php
43 | // ...
44 | protected static function boot()
45 | {
46 | parent::boot();
47 |
48 | static::hasOneCreating('profile', function ($user, $profile) {
49 | Log::info("Creating profile: {$profile->name} for user {$user->name}.");
50 | });
51 |
52 | static::hasOneCreated('profile', function ($user, $profile) {
53 | Log::info("Profile: {$profile->name} for user: {$user->name} has been created.");
54 | });
55 |
56 | static::hasOneSaving('profile', function ($user, $profile) {
57 | Log::info("Saving profile: {$profile->name} for user {$user->name}.");
58 | });
59 |
60 | static::hasOneSaved('profile', function ($user, $profile) {
61 | Log::info("Profile: {$profile->name} for user: {$user->name} has been saved.");
62 | });
63 | }
64 | // ...
65 | ```
66 |
67 | Dispatch Events with the event dispatcher
68 | ```php
69 | // ...
70 | protected $dispatchesEvents = [
71 | 'profileCreating' => UserProfileCreating::class,
72 | 'profileCreated' => UserProfileCreated::class,
73 | 'profileSaving' => UserProfileSaveing::class,
74 | 'profileSaving' => UserProfileSaving::class,
75 | ];
76 | // ...
77 | ```
78 |
79 | Or you may use an Observer. Be sure to define the observable events in your model class - See [Detecting Observable Events](../README.md#detecting-observable-events)
80 | ```php
81 | namespace App\Observer;
82 |
83 | class UserObserver
84 | {
85 | /**
86 | * Handle the User "profileCreating" event.
87 | *
88 | * @param \App\Models\User $user
89 | * @param \App\Models\Profile $profile
90 | *
91 | * @return void
92 | */
93 | public function profileCreating(User $user, Profile $profile)
94 | {
95 | Log::info("Creating profile: {$profile->name} for user {$user->name}.");
96 | }
97 |
98 | /**
99 | * Handle the User "profileCreated" event.
100 | *
101 | * @param \App\Models\User $user
102 | * @param \App\Models\Profile $profile
103 | *
104 | * @return void
105 | */
106 | public function profileCreated(User $user, Profile $profile)
107 | {
108 | Log::info("Profile: {$profile->name} for user: {$user->name} has been created.");
109 | }
110 |
111 | /**
112 | * Handle the User "profileSaving" event.
113 | *
114 | * @param \App\Models\User $user
115 | * @param \App\Models\Profile $profile
116 | *
117 | * @return void
118 | */
119 | public function profileSaving(User $user, Profile $profile)
120 | {
121 | Log::info("Saving profile: {$profile->name} for user {$user->name}.");
122 | }
123 |
124 | /**
125 | * Handle the User "profileSaved" event.
126 | *
127 | * @param \App\Models\User $user
128 | * @param \App\Models\Profile $profile
129 | *
130 | * @return void
131 | */
132 | public function profileSaved(User $user, Profile $profile)
133 | {
134 | Log::info("Profile: {$profile->name} for user: {$user->name} has been saved.");
135 | }
136 | }
137 | ```
138 |
139 | ### Available methods and events
140 |
141 | #### HasOne::create
142 | - fires {relationship}Creating, {relationship}Created
143 | - events have $parent and $related models
144 |
145 | #### HasOne::save
146 | - fires {relationship}Saving, {relationship}Saved
147 | - events have $parent and $related models
--------------------------------------------------------------------------------
/doc/morph-to.md:
--------------------------------------------------------------------------------
1 | # Has One Relations:
2 |
3 | ```Post``` model might be associated with one ```Comment```
4 |
5 | ```php
6 | namespace App\Models;
7 |
8 | use Artificertech\RelationshipEvents\Concerns\HasRelationshipEvents;
9 | use Illuminate\Database\Eloquent\Model;
10 |
11 | class Comment extends Model
12 | {
13 | use HasRelationshipEvents;
14 |
15 | /**
16 | * Get the post associated with the comment.
17 | */
18 | public function post()
19 | {
20 | return $this->morphTo(Post::class)->withEvents();
21 | }
22 | }
23 | ```
24 |
25 | Now we can use methods to associate ```Comment``` with ```Post``` and also update assosiated model.
26 |
27 | ```php
28 | // ...
29 | $comment = Comment::create([
30 | 'name' => 'my comment'
31 | ]);
32 |
33 | $post = factory(Post::class)->create([
34 | 'name' => 'John Smith',
35 | ]);
36 |
37 | $comment->post()->associate($post);
38 | // ...
39 | ```
40 |
41 | Now we should listen our events, for example we can register event listners in model's boot method:
42 | ```php
43 | // ...
44 | protected static function boot()
45 | {
46 | parent::boot();
47 |
48 | static::morphToAssociating('post', function ($comment, $post) {
49 | Log::info("Associating comment: {$comment->name} with post {$post->name}.");
50 | });
51 |
52 | static::morphToAssociated('post', function ($parent, $post) {
53 | Log::info("Comment: {$comment->name} for post: {$post->name} has been associated.");
54 | });
55 |
56 | static::morphToDissociating('post', function ($comment, $post) {
57 | Log::info("Dissociating comment: {$comment->name} with post {$post->name}.");
58 | });
59 |
60 | static::morphToDissociated('post', function ($comment, $post) {
61 | Log::info("Comment: {$comment->name} for post: {$post->name} has been dissociated.");
62 | });
63 | }
64 | // ...
65 | ```
66 |
67 | Dispatch Events with the event dispatcher
68 | ```php
69 | // ...
70 | protected $dispatchesEvents = [
71 | 'postAssociating' => CommentPostAssociating::class,
72 | 'postAssociated' => CommentPostAssociated::class,
73 | 'postDissociating' => CommentPostDissociating::class,
74 | 'postDissociated' => CommentPostDissociated::class,
75 | ];
76 | // ...
77 | ```
78 |
79 | Or you may use an Observer. Be sure to define the observable events in your model class - See [Detecting Observable Events](../README.md#detecting-observable-events)
80 | ```php
81 | namespace App\Observer;
82 |
83 | class CommentObserver
84 | {
85 | /**
86 | * Handle the Post "postAssociating" event.
87 | *
88 | * @param \App\Models\Post $post
89 | * @param \App\Models\Comment $comment
90 | *
91 | * @return void
92 | */
93 | public function postAssociating(Comment $comment, Post $post)
94 | {
95 | Log::info("Associating comment: {$comment->name} with post {$post->name}.");
96 | }
97 |
98 | /**
99 | * Handle the Post "postAssociated" event.
100 | *
101 | * @param \App\Models\Post $post
102 | * @param \App\Models\Comment $comment
103 | *
104 | * @return void
105 | */
106 | public function postAssociated(Comment $comment, Post $post)
107 | {
108 | Log::info("Comment: {$comment->name} for post: {$post->name} has been associated.");
109 | }
110 |
111 | /**
112 | * Handle the Post "postDissociating" event.
113 | *
114 | * @param \App\Models\Post $post
115 | * @param \App\Models\Comment $comment
116 | *
117 | * @return void
118 | */
119 | public function postDissociating(Comment $comment, Post $post)
120 | {
121 | Log::info("Dissociating comment: {$comment->name} with post {$post->name}.");
122 | }
123 |
124 | /**
125 | * Handle the Post "postDissociated" event.
126 | *
127 | * @param \App\Models\Post $post
128 | * @param \App\Models\Comment $comment
129 | *
130 | * @return void
131 | */
132 | public function postDissociated(Comment $comment, Post $post)
133 | {
134 | Log::info("Comment: {$comment->name} for post: {$post->name} has been dissociated.");
135 | }
136 | }
137 | ```
138 |
139 | ### Available methods and events
140 |
141 | #### MorphTo::associate
142 | - fires {relationship}Associating, {relationship}Associated
143 | - events have $parent and $related models
144 |
145 | #### MorphTo::dissociate
146 | - fires {relationship}Dissociating, {relationship}Dissociated
147 | - events have $parent and $related models
--------------------------------------------------------------------------------
/tests/Feature/HasManyEventsTest.php:
--------------------------------------------------------------------------------
1 | posts()->create([]);
27 |
28 | Event::assertDispatched(
29 | 'eloquent.postsCreating: ' . User::class,
30 | function ($event, $callback) use ($user, $post) {
31 | return $callback[0]->is($user) && $callback[1]->is($post);
32 | }
33 | );
34 | Event::assertDispatched(
35 | 'eloquent.postsCreated: ' . User::class,
36 | function ($event, $callback) use ($user, $post) {
37 | return $callback[0]->is($user) && $callback[1]->is($post);
38 | }
39 | );
40 | }
41 |
42 | /** @test */
43 | public function it_fires_hasManyCreating_and_hasManyCreated_when_createMany_called_on_hasMany_relation()
44 | {
45 | Event::fake();
46 |
47 | $user = User::create();
48 | $posts = $user->posts()->createMany([[], []]);
49 |
50 | Event::assertDispatched(
51 | 'eloquent.postsCreating: ' . User::class,
52 | 2
53 | );
54 | Event::assertDispatched(
55 | 'eloquent.postsCreated: ' . User::class,
56 | 2
57 | );
58 | }
59 |
60 | /** @test */
61 | public function if_false_is_returned_from_the_creating_event_then_the_create_is_canceled()
62 | {
63 | $user = User::create();
64 | $user->posts()->create(['name' => 'badName']);
65 |
66 | $this->assertEquals(0, $user->posts->count());
67 | }
68 |
69 | /** @test */
70 | public function if_false_is_returned_from_the_creating_event_then_the_createMany_is_canceled_on_that_model_only()
71 | {
72 | $user = User::create();
73 | $user->posts()->createMany([[], ['name' => 'badName']]);
74 |
75 | $this->assertEquals(1, $user->posts->count());
76 | }
77 |
78 | /** @test */
79 | public function it_fires_hasManySaving_and_hasManySaved_when_belonged_model_with_many_saved()
80 | {
81 | Event::fake();
82 |
83 | $user = User::create();
84 | $post = $user->posts()->save(new Post);
85 |
86 | Event::assertDispatched(
87 | 'eloquent.postsSaving: ' . User::class,
88 | function ($event, $callback) use ($user, $post) {
89 | return $callback[0]->is($user) && $callback[1]->is($post);
90 | }
91 | );
92 | Event::assertDispatched(
93 | 'eloquent.postsSaved: ' . User::class,
94 | function ($event, $callback) use ($user, $post) {
95 | return $callback[0]->is($user) && $callback[1]->is($post);
96 | }
97 | );
98 | }
99 |
100 | /** @test */
101 | public function it_fires_hasManySaving_and_hasManySaved_when_saveMany_called_on_hasMany_relation()
102 | {
103 | Event::fake();
104 |
105 | $user = User::create();
106 | $user->posts()->saveMany([new Post, new Post()]);
107 |
108 | Event::assertDispatched(
109 | 'eloquent.postsSaving: ' . User::class,
110 | 2
111 | );
112 | Event::assertDispatched(
113 | 'eloquent.postsSaved: ' . User::class,
114 | 2
115 | );
116 | }
117 |
118 | /** @test */
119 | public function if_false_is_returned_from_the_saving_event_then_the_save_is_canceled()
120 | {
121 | $user = User::create();
122 | $user->posts()->save(new Post(['name' => 'badName']));
123 |
124 | $this->assertEquals(0, $user->posts->count());
125 | }
126 |
127 | /** @test */
128 | public function if_false_is_returned_from_the_saving_event_then_the_saveMany_is_canceled_on_that_model_only()
129 | {
130 | $user = User::create();
131 | $post = $user->posts()->saveMany([new Post, new Post(['name' => 'badName'])]);
132 |
133 | $this->assertEquals(1, $user->posts->count());
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/doc/belongs-to.md:
--------------------------------------------------------------------------------
1 | # Belongs To Relations:
2 |
3 | ```Profile``` model might belong to one ```User```
4 |
5 | ```php
6 | namespace App\Models;
7 |
8 | use Artificertech\RelationshipEvents\Concerns\HasRelationshipEvents;
9 | use Illuminate\Database\Eloquent\Model;
10 |
11 | class Profile extends Model
12 | {
13 | use HasRelationshipEvents;
14 |
15 | /**
16 | * Get the user associated with the profile.
17 | */
18 | public function user()
19 | {
20 | return $this->belongsTo(User::class)->withEvents();
21 | }
22 | }
23 | ```
24 |
25 | Now we can use methods to associate ```Profile``` with ```User``` and also update assosiated model.
26 |
27 | ```php
28 | // ...
29 | $profile = Profile::create([
30 | 'phone' => '8-800-123-45-67',
31 | 'email' => 'user@example.com',
32 | 'address' => 'One Infinite Loop Cupertino, CA 95014',
33 | ]);
34 |
35 | $user = factory(User::class)->create([
36 | 'name' => 'John Smith',
37 | ]);
38 |
39 | $profile->user()->associate($user);
40 | // ...
41 | ```
42 |
43 | Now we should listen our events, for example we can register event listners in model's boot method:
44 | ```php
45 | // ...
46 | protected static function boot()
47 | {
48 | parent::boot();
49 |
50 | static::belongsToAssociating('user', function ($profile, $user) {
51 | Log::info("Associating profile: {$profile->name} with user {$user->name}.");
52 | });
53 |
54 | static::belongsToAssociated('user', function ($parent, $user) {
55 | Log::info("Profile: {$profile->name} for user: {$user->name} has been associated.");
56 | });
57 |
58 | static::belongsToDissociating('user', function ($profile, $user) {
59 | Log::info("Dissociating profile: {$profile->name} with user {$user->name}.");
60 | });
61 |
62 | static::belongsToDissociated('user', function ($profile, $user) {
63 | Log::info("Profile: {$profile->name} for user: {$user->name} has been dissociated.");
64 | });
65 | }
66 | // ...
67 | ```
68 |
69 | Dispatch Events with the event dispatcher
70 | ```php
71 | // ...
72 | protected $dispatchesEvents = [
73 | 'userAssociating' => ProfileUserAssociating::class,
74 | 'userAssociated' => ProfileUserAssociated::class,
75 | 'userDissociating' => ProfileUserDissociating::class,
76 | 'userDissociated' => ProfileUserDissociated::class,
77 | ];
78 | // ...
79 | ```
80 |
81 | Or you may use an Observer. Be sure to define the observable events in your model class - See [Detecting Observable Events](../README.md#detecting-observable-events)
82 | ```php
83 | namespace App\Observer;
84 |
85 | class ProfileObserver
86 | {
87 | /**
88 | * Handle the User "userAssociating" event.
89 | *
90 | * @param \App\Models\User $user
91 | * @param \App\Models\Profile $profile
92 | *
93 | * @return void
94 | */
95 | public function userAssociating(Profile $profile, User $user)
96 | {
97 | Log::info("Associating profile: {$profile->name} with user {$user->name}.");
98 | }
99 |
100 | /**
101 | * Handle the User "userAssociated" event.
102 | *
103 | * @param \App\Models\User $user
104 | * @param \App\Models\Profile $profile
105 | *
106 | * @return void
107 | */
108 | public function userAssociated(Profile $profile, User $user)
109 | {
110 | Log::info("Profile: {$profile->name} for user: {$user->name} has been associated.");
111 | }
112 |
113 | /**
114 | * Handle the User "userDissociating" event.
115 | *
116 | * @param \App\Models\User $user
117 | * @param \App\Models\Profile $profile
118 | *
119 | * @return void
120 | */
121 | public function userDissociating(Profile $profile, User $user)
122 | {
123 | Log::info("Dissociating profile: {$profile->name} with user {$user->name}.");
124 | }
125 |
126 | /**
127 | * Handle the User "userDissociated" event.
128 | *
129 | * @param \App\Models\User $user
130 | * @param \App\Models\Profile $profile
131 | *
132 | * @return void
133 | */
134 | public function userDissociated(Profile $profile, User $user)
135 | {
136 | Log::info("Profile: {$profile->name} for user: {$user->name} has been dissociated.");
137 | }
138 | }
139 | ```
140 |
141 | ### Available methods and events
142 |
143 | #### BelongsTo::associate
144 | - fires {relationship}Associating, {relationship}Associated
145 | - events have $parent and $related models
146 |
147 | #### BelongsTo::dissociate
148 | - fires {relationship}Dissociating, {relationship}Dissociated
149 | - events have $parent and $related models
--------------------------------------------------------------------------------
/tests/Feature/MorphManyEventsTest.php:
--------------------------------------------------------------------------------
1 | 1]);
26 | $comment = $post->comments()->create([]);
27 |
28 | Event::assertDispatched(
29 | 'eloquent.commentsCreating: ' . Post::class,
30 | function ($event, $callback) use ($post, $comment) {
31 | return $callback[0]->is($post) && $callback[1]->is($comment);
32 | }
33 | );
34 | Event::assertDispatched(
35 | 'eloquent.commentsCreated: ' . Post::class,
36 | function ($event, $callback) use ($post, $comment) {
37 | return $callback[0]->is($post) && $callback[1]->is($comment);
38 | }
39 | );
40 | }
41 |
42 | /** @test */
43 | public function it_fires_morphManyCreating_and_morphManyCreated_when_createMany_called_on_morphMany_relation()
44 | {
45 | Event::fake();
46 |
47 | $post = Post::create();
48 | $post->comments()->createMany([[], []]);
49 |
50 | Event::assertDispatched(
51 | 'eloquent.commentsCreating: ' . Post::class,
52 | 2
53 | );
54 | Event::assertDispatched(
55 | 'eloquent.commentsCreated: ' . Post::class,
56 | 2
57 | );
58 | }
59 |
60 | /** @test */
61 | public function if_false_is_returned_from_the_creating_event_then_the_create_is_canceled()
62 | {
63 | $post = Post::create();
64 | $post->comments()->create(['name' => 'badName']);
65 |
66 | $this->assertEquals(0, $post->comments->count());
67 | }
68 |
69 | /** @test */
70 | public function if_false_is_returned_from_the_creating_event_then_the_createMany_is_canceled_on_that_model_only()
71 | {
72 | $post = Post::create();
73 | $post->comments()->createMany([[], ['name' => 'badName']]);
74 |
75 | $this->assertEquals(1, $post->comments->count());
76 | }
77 |
78 | /** @test */
79 | public function it_fires_morphManySaving_and_morphManySaved_when_belonged_model_with_morph_many_saved()
80 | {
81 | Event::fake();
82 |
83 | $post = Post::create(['user_id' => 1]);
84 | $comment = $post->comments()->save(new Comment);
85 |
86 | Event::assertDispatched(
87 | 'eloquent.commentsSaving: ' . Post::class,
88 | function ($event, $callback) use ($post, $comment) {
89 | return $callback[0]->is($post) && $callback[1]->is($comment);
90 | }
91 | );
92 | Event::assertDispatched(
93 | 'eloquent.commentsSaved: ' . Post::class,
94 | function ($event, $callback) use ($post, $comment) {
95 | return $callback[0]->is($post) && $callback[1]->is($comment);
96 | }
97 | );
98 | }
99 |
100 | /** @test */
101 | public function it_fires_morphManySaving_and_morphManySaved_when_saveMany_called_on_morphMany_relation()
102 | {
103 | Event::fake();
104 |
105 | $post = Post::create();
106 | $post->comments()->saveMany([new Comment, new Comment]);
107 |
108 | Event::assertDispatched(
109 | 'eloquent.commentsSaving: ' . Post::class,
110 | 2
111 | );
112 | Event::assertDispatched(
113 | 'eloquent.commentsSaved: ' . Post::class,
114 | 2
115 | );
116 | }
117 |
118 | /** @test */
119 | public function if_false_is_returned_from_the_saving_event_then_the_save_is_canceled()
120 | {
121 | $post = Post::create();
122 | $post->comments()->save(new Comment(['name' => 'badName']));
123 |
124 | $this->assertEquals(0, $post->comments->count());
125 | }
126 |
127 | /** @test */
128 | public function if_false_is_returned_from_the_saving_event_then_the_saveMany_is_canceled_on_that_model_only()
129 | {
130 | $post = Post::create();
131 | $post->comments()->saveMany([new Comment, new Comment(['name' => 'badName'])]);
132 |
133 | $this->assertEquals(1, $post->comments->count());
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Laravel Relationship Events
3 |
4 | Missing relationship events for Laravel
5 |
6 | This package was intitally forked from https://github.com/chelout/laravel-relationship-events which is not being actively developed. This package is a different take on the original idea that allows relationship event listeners to be created on a per-relationship basis
7 |
8 | This package is still in development. Feel free to contribute by submitting a pull request
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ## Install
18 |
19 | 1. Install package with composer
20 |
21 | #### Latest Release:
22 | Currently there are no releases for this project as it is still in development.
23 |
24 | ```
25 | composer require artificertech/laravel-relationship-events
26 | ```
27 |
28 | #### Development branch:
29 | ```
30 | composer require artificertech/laravel-relationship-events:dev-master
31 | ```
32 |
33 | 2. Add the HasRelationshipEvents trait to your model
34 |
35 |
36 | ```php
37 |
38 | use Artificertech\RelationshipEvents\Concerns\HasRelationshipEvents;
39 | use Illuminate\Database\Eloquent\Model;
40 |
41 | class User extends Model
42 | {
43 | use HasRelationshipEvents;
44 |
45 | public static function boot()
46 | {
47 | parent::boot();
48 |
49 | /**
50 | * hasOne
51 | */
52 | static::hasOneSaved('profile', function ($user, $profile) {
53 | dump('hasOneSaved', $user, $profile);
54 | });
55 | }
56 |
57 | public function profile()
58 | {
59 | return $this->hasOne(Profile::class)->withEvents();
60 | }
61 |
62 | }
63 | ```
64 |
65 | For all saving, attaching, creating, etc events that are fired before the operation takes place you may return false from the event listener to cancel the operation
66 |
67 | ```php
68 |
69 | use Artificertech\RelationshipEvents\Concerns\HasRelationshipEvents;
70 | use Illuminate\Database\Eloquent\Model;
71 |
72 | class User extends Model
73 | {
74 | use HasRelationshipEvents;
75 |
76 | public static function boot()
77 | {
78 | parent::boot();
79 |
80 | /**
81 | * hasMany
82 | */
83 | static::hasManyCreating('posts', function ($user, $post) {
84 | if ($post->name == 'badName') return false;
85 | });
86 | }
87 |
88 | public function posts()
89 | {
90 | return $this->hasMany(Post::class)->withEvents();
91 | }
92 |
93 | }
94 | ```
95 |
96 | 3. Dispatchable relationship events.
97 | It is possible to fire event classes via $dispatchesEvents properties
98 |
99 | ```php
100 |
101 | use Artificertech\RelationshipEvents\Concerns\HasRelationshipEvents;
102 | use Illuminate\Database\Eloquent\Model;
103 |
104 | class User extends Model
105 | {
106 | use HasRelationshipEvents;
107 |
108 | protected $dispatchesEvents = [
109 | 'postsCreating' => UserPostsCreating::class,
110 | 'postsCreated' => UserPostsCreated::class,
111 | 'postsSaving' => UserPostsSaving::class,
112 | 'postsSaved' => UserPostsSaved::class,
113 | ];
114 |
115 | public function posts()
116 | {
117 | return $this->hasMany(Post::class)->withEvents();
118 | }
119 |
120 | }
121 | ```
122 |
123 |
124 | ## Observers
125 |
126 | It is possible to use relationship events in [Laravel observers classes](https://laravel.com/docs/eloquent#observers) Usage is very simple. Define observer class:
127 |
128 | ```php
129 | namespace App\Observer;
130 |
131 | class UserObserver
132 | {
133 | /**
134 | * Handle the User "postsCreating" event.
135 | *
136 | * @param \App\Models\User $user
137 | * @param \App\Models\Post $post
138 | *
139 | * @return void
140 | */
141 | public function postsCreating(User $user, Post $post)
142 | {
143 | Log::info("Creating post: {$post->name} for user {$user->name}.");
144 | }
145 |
146 | /**
147 | * Handle the User "postsCreated" event.
148 | *
149 | * @param \App\Models\User $user
150 | * @param \App\Models\Post $post
151 | *
152 | * @return void
153 | */
154 | public function postsCreated(User $user, Post $post)
155 | {
156 | Log::info("Post: {$post->name} for user: {$user->name} has been created.");
157 | }
158 |
159 | /**
160 | * Handle the User "postsCreating" event.
161 | *
162 | * @param \App\Models\User $user
163 | * @param \App\Models\Post $post
164 | *
165 | * @return void
166 | */
167 | public function postsSaving(User $user, Post $post)
168 | {
169 | Log::info("Saving post: {$post->name} for user {$user->name}.");
170 | }
171 |
172 | /**
173 | * Handle the User "postsCreated" event.
174 | *
175 | * @param \App\Models\User $user
176 | * @param \App\Models\Post $post
177 | *
178 | * @return void
179 | */
180 | public function postsSaved(User $user, Post $post)
181 | {
182 | Log::info("Post: {$post->name} for user: {$user->name} has been saved.");
183 | }
184 | }
185 | ```
186 |
187 | ### Detecting observable events
188 | the laravel-relationship-events package cannot automatically detect relationship events that you want to observe. Please define them in your model class like so:
189 |
190 | ```php
191 | class User extends Model
192 | {
193 | use HasRelationshipEvents;
194 |
195 | /**
196 | * User exposed observable events.
197 | *
198 | * These are extra user-defined events observers may subscribe to.
199 | *
200 | * @var array
201 | */
202 | protected $observables = [
203 | 'postsCreating',
204 | 'postsCreated',
205 | 'postsSaving',
206 | 'postsSaved',
207 | ];
208 |
209 | public function posts()
210 | {
211 | return $this->hasMany(Post::class)->withEvents();
212 | }
213 |
214 | }
215 | ```
216 |
217 | Don't forget to register an observer in the ```boot``` method of your ```AppServiceProvider```:
218 | ```php
219 | namespace App\Providers;
220 |
221 | use App\Models\User;
222 | use App\Observers\UserObserver;
223 | use Illuminate\Support\ServiceProvider;
224 |
225 | class AppServiceProvider extends ServiceProvider
226 | {
227 | // ...
228 | public function boot()
229 | {
230 | User::observe(UserObserver::class);
231 | }
232 | // ...
233 | }
234 | ```
235 |
236 | And now just create profile for user:
237 | ```php
238 | // ...
239 | $user = factory(User::class)->create([
240 | 'name' => 'John Smith',
241 | ]);
242 |
243 | // Create profile and assosiate it with user
244 | // This will fire two events hasOneCreating, hasOneCreated
245 | $user->post()->create([
246 | 'name' => 'My first post!',
247 | ]);
248 | // ...
249 | ```
250 |
251 | ## Customizing the event name
252 | By default the relationship event name is equal to the relationship function name with the action taking place in camel case. For example if you have a HasOne relationship "profile" then the event names would be "profileCreating", "profileCreated", "profileSaving", "profileSaved".
253 |
254 | You may customize the event name by passing the relationship name into the withEvents() function as a string. For example:
255 | ```php
256 | class User extends Model
257 | {
258 | use HasRelationshipEvents;
259 |
260 | public function posts()
261 | {
262 | return $this->hasMany(Post::class)->withEvents('userPost');
263 | }
264 |
265 | }
266 | ```
267 |
268 | will fire "userPostCreating", "userPostCreated", "userPostSaving", "userPostSaved" events
269 |
270 | ## Relationship Specific info
271 |
272 | Each relationship as slightly different events. For example the belongsTo relationship fires {relationship}Associating, {relationship}Associated, {relationship}Dissociating, and {relationship}Dissociated events
273 |
274 | - [Belongs To](doc/belongs-to.md)
275 | - [Has Many](doc/has-many.md)
276 | - [Has One](doc/has-one.md)
277 | - [Morph Many](doc/morph-many.md)
278 | - [Morph One](doc/morph-one.md)
279 | - [Morph To](doc/morph-to.md)
280 |
281 | ## Todo
282 | - Fix Automated Tests
283 | - Add documentation for ManyToMany type events (these events can be handled by the built in pivot models and do not need this package)
284 | - Non-Default event name tests
285 | - Event Dispatcher Tests
286 | - Event Listener Exception Tests
287 | - HasOneThrough & HasManyThrough
288 | - New HasOneOfMany relationship?
289 |
--------------------------------------------------------------------------------