数据透视表的 Laravel 观察者

2022-01-22 00:00:00 pivot php laravel observers

我有一个有更新方法的观察者:

I've got a observer that has a update method:

ObserverServiceProvider.php

public function boot()
{
    Relation::observe(RelationObserver::class);
}

RelationObserver.php

public function updated(Relation $relation)
{
    $this->cache->tags(Relation::class)->flush();
}

所以当我更新控制器中的关系时:

So when I update a relation in my controller:

public function update(Request $request, Relation $relation)
{
     $relation->update($request->all()));
     return back();
}

一切都按预期工作.但现在我有一个数据透视表.relation belongsToMany 产品.

Everything is working as expected. But now I've got a pivot table. A relation belongsToMany products.

所以现在我的控制器方法如下所示:

So now my controller method looks like this:

public function update(Request $request, Relation $relation)
{
    if(empty($request->products)) {
        $relation->products()->detach();
    } else {
        $relation->products()->sync(collect($request->products)->pluck('id'));
    }

    $relation->update($request->all());

    return back();
}

问题是,如果我仅添加或删除产品,则不再触发观察者.

The problem is that the observer is not triggered anymore if I only add or remove products.

pivot table 更新时如何触发观察者?

How can I trigger the observer when the pivot table updates aswel?

谢谢

推荐答案

如你所知,Laravel 在调用 sync() 时并不会真正检索模型,也不会在任何模型上调用 save/update 因此默认情况下不会创建任何事件.但我想出了一些替代解决方案来解决您的问题.

As you already know, Laravel doesn't actually retrieve the models nor call save/update on any of the models when calling sync() thus no event's are created by default. But I came up with some alternative solutions for your problem.

1 - 为 sync() 方法添加一些额外的功能:

1 - To add some extra functionality to the sync() method:

如果您深入了解 belongsToMany 功能,您会发现它会尝试猜测一些变量名称并返回一个 BelongsToMany 对象.最简单的方法是让您的关系函数自己简单地返回一个自定义 BelongsToMany 对象:

If you dive deeper into the belongsToMany functionality you will see that it tries to guess some of the variable names and returns a BelongsToMany object. Easiest way would be to make your relationship function to simply return a custom BelongsToMany object yourself:

public function products() {

    // Product::class is by default the 1. argument in ->belongsToMany calll
    $instance = $this->newRelatedInstance(Product::class);

    return new BelongsToManySpecial(
        $instance->newQuery(),
        $this,
        $this->joiningTable(Product::class), // By default the 2. argument
        $this->getForeignKey(), // By default the 3. argument
        $instance->getForeignKey(), // By default the 4. argument
        null // By default the 5. argument
    );
}

或者复制整个函数,重命名它并使其返回 BelongsToManySpecial 类.或者省略所有变量,也许只是返回 new BelongsToManyProducts 类并解析 __construct 中的所有 BelongsToMany 变量......我想你明白了.

Or alternatively copy the whole function, rename it and make it return the BelongsToManySpecial class. Or omit all the variables and perhaps simply return new BelongsToManyProducts class and resolve all the BelongsToMany varialbes in the __construct... I think you got the idea.

使 BelongsToManySpecial 类扩展原来的 BelongsToMany 类,并将同步函数写入 BelongsToManySpecial 类.

Make the BelongsToManySpecial class extend the original BelongsToMany class and write a sync function to the BelongsToManySpecial class.

public function sync($ids, $detaching = true) {

    // Call the parent class for default functionality
    $changes = parent::sync($ids, $detaching);

    // $changes = [ 'attached' => [...], 'detached' => [...], 'updated' => [...] ]
    // Add your functionality
    // Here you have access to everything the BelongsToMany function has access and also know what changes the sync function made.

    // Return the original response
    return $changes
}

或者覆盖 detachattachNew 函数以获得类似的结果.

Alternatively override the detach and attachNew functions for similar results.

protected function attachNew(array $records, array $current, $touch = true) {
    $result = parent::attachNew($records, $current, $touch);

    // Your functionality

    return $result;
}

public function detach($ids = null, $touch = true)
    $result = parent::detach($ids, $touch);

    // Your functionality

    return $result;
}

如果您想深入挖掘并想了解幕后发生的事情,请分析 IlluminateDatabaseEloquentConcernsHasRelationship 特征 - 特别是 belongsToMany关系函数和 BelongsToMany 类本身.

If you want to dig deeper and want to understand what's going on under the hood then analyze the IlluminateDatabaseEloquentConcernsHasRelationship trait - specifically the belongsToMany relationship function and the BelongsToMany class itself.

2 - 创建一个名为 BelongsToManySyncEvents 的特征,它只返回您的特殊 BelongsToMany 类

2 - Create a trait called BelongsToManySyncEvents which doesn't do much more than returns your special BelongsToMany class

trait BelongsToManySyncEvents {

    public function belongsToMany($related, $table = null, $foreignKey = null, $relatedKey = null, $relation = null) {

        if (is_null($relation)) {
            $relation = $this->guessBelongsToManyRelation();
        }

        $instance = $this->newRelatedInstance($related);
        $foreignKey = $foreignKey ?: $this->getForeignKey();
        $relatedKey = $relatedKey ?: $instance->getForeignKey();

        if (is_null($table)) {
            $table = $this->joiningTable($related);
        }

        return new BelongsToManyWithSyncEvents(
            $instance->newQuery(), $this, $table, $foreignKey, $relatedKey, $relation
        );
    }

}

创建 BelongsToManyWithSyncEvents 类:

class BelongsToManyWithSyncEvents extends BelongsToMany {

    public function sync($ids, $detaching = true) {

        $changes = parent::sync($ids, $detaching);

        // Do your own magic. For example using these variables if needed:
        // $this->get() - returns an array of objects given with the sync method
        // $this->parent - Object they got attached to
        // Maybe call some function on the parent if it exists?

        return $changes;
    }

}

现在将 trait 添加到你的类中.

Now add the trait to your class.

3 - 结合之前的解决方案并将此功能添加到您在 BaseModel 类等中拥有的每个模型中.例如,让他们检查并调用某些方法以防它被定义...

3 - Combine the previous solutions and add this functionality to every Model that you have in a BaseModel class etc. For examples make them check and call some method in case it is defined...

$functionName = 'on' . $this->foreignKey . 'Sync';

if(method_exists($this->parent), $functionName) {
    $this->parent->$functionName($changes);
}

<小时>

4 - 创建服务


4 - Create a service

在该服务中创建一个您必须始终调用的函数,而不是默认的 sync().也许称它为 attachAndDetachProducts(...) 并添加您的事件或功能

Inside that service create a function that you must always call instead of the default sync(). Perhaps call it something attachAndDetachProducts(...) and add your events or functionality

由于我没有太多关于你的类和关系的信息,你可能可以选择比我提供的更好的类名.但是,如果您现在的用例只是清除缓存,那么我认为您可以使用提供的一些解决方案.

As I didn't have that much information about your classes and relationships you can probably choose better class names than I provided. But if your use case for now is simply to clear cache then I think you can make use of some of the provided solutions.

相关文章