Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Call method on model when syncing #50

Closed
wants to merge 2 commits into from
Closed

Call method on model when syncing #50

wants to merge 2 commits into from

Conversation

mdavis1982
Copy link

Because of the issues I was having in #48 and the issue @ericdiviney was having in #16, this PR adds a call to a method on the underlying model class, if it exists.

The Laravel ->sync() method returns an array of changes as can be seen here: https://github.com/laravel/framework/blob/6.x/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php#L90.

This PR takes the attribute name, converts to camelCase and adds Synced to the end, and attempts to call that method if it exists on the model.

This is useful in the following circumstance (as outlined in #48).

If I have an Author model:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Author extends Model
{
    public function relatedAuthors(): BelongsToMany
    {
        return $this->belongsToMany(self::class, 'related_authors', 'author_id', 'related_author_id');
    }
}

I can now add a method relatedAuthorsSynced() to the model which can accept the list of changes that have happened:

public function relatedAuthorsSynced(array $changes): void
{
    info('The following IDs have been attached:');
    info($changes['attached']);

    info('The following IDs have been detached:');
    info($changes['detached']);

    info('The following IDs have been updated:');
    info($changes['updated']);
}

Using this information I could now sync the inverse side really easily. In the case of #16 the model could either fire a new event to be observed, or the code from the observer could be brought into the model.

Users are free to ignore the method if they want the existing behaviour and the line referenced in #48 (https://github.com/dillingham/nova-attach-many/blob/master/src/AttachMany.php#L46) can continue working as it is, ensuring that the model has been saved successfully before attempting the sync.

I've also updated the README to document the change.

@dillingham
Copy link
Owner

Just starting to sync my teeth into this, sorry for the delay.
First thought is maybe $this->fireModelEvent('attached', false);
Thanks for being so thorough on the description. Will circle back soon

@mdavis1982
Copy link
Author

Thanks for getting back to me.

I actually didn't do it this way in the end...

I ended up doing the following:

$this->fillUsing(function ($request, $model, $attribute, $requestAttribute) use ($resource) {
    if (is_subclass_of($model, Model::class)) {
        $model::saved(function ($model) use ($attribute, $request) {
            $changes = $model->{$attribute}()->sync(
                json_decode($request->{$attribute}, true)
            );

            event(new AttachManySynced($model, $attribute, $changes));
        });

        unset($request->{$attribute});
    }
});

The AttachManySynced class looks like this:

class AttachManySynced
{
    use Dispatchable, SerializesModels;

    /** @var Model */
    public $model;

    /** @var string */
    public $attribute;

    /** @var array */
    public $changes;

    /**
     * Create a new event instance.
     */
    public function __construct(Model $model, string $attribute, array $changes)
    {
        $this->model = $model;
        $this->attribute = $attribute;
        $this->changes = $changes;
    }
}

Then I had a listener set up in my EventServiceProvier.php:

protected $listen = [
    Registered::class => [
        SendEmailVerificationNotification::class,
    ],
    AttachManySynced::class => [
        SyncRelatedAuthors::class,
    ],
];

And my SyncRelatedAuthors class did the following:

class SyncRelatedAuthors
{
    /**
     * Handle the event.
     */
    public function handle(AttachManySynced $event)
    {
        if (! $event->model instanceof Author || 'relatedAuthors' !== $event->attribute) {
            return;
        }

        $attachedIds = $event->changes['attached'];
        $detachedIds = $event->changes['detached'];

        // Add the author as a related author to all the newly attached authors
        Author::whereIn('id', $attachedIds)
            ->get()
            ->each(function (Author $author) use ($event) {
                $author->relatedAuthors()->attach($event->model->id);
            });

        // Remove the author from the related authors of all the newly detached authors
        Author::whereIn('id', $detachedIds)
            ->get()
            ->each(function (Author $author) use ($event) {
                $author->relatedAuthors()->detach($event->model->id);
            });
    }
}

This might not be ideal, but it felt like the work needed to happen somewhere other than in the actual model class.

I'd appreciate your thoughts! 😄

@mdavis1982
Copy link
Author

As this is now over a year old, I'm going to close this pull request 😄

@mdavis1982 mdavis1982 closed this Oct 22, 2020
@dillingham
Copy link
Owner

Thank you for this PR. I added it as 3fc3624

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants