【问题标题】:Infinite loop while listening model event监听模型事件时无限循环
【发布时间】:2016-10-29 22:00:16
【问题描述】:

我有一个用于属性的 Laravel 应用程序,假设在我的代码中某处:

$property = new Property();
$property->city = "New York";
 ...
$property->save();

然后我有监听特定事件的事件监听器:

$events->listen(
    'eloquent.saved: Properties\\Models\\Property',
    'Google\Listeners\SetGeoLocationInfo@fire'
);

最后在SetGeoLocationInfo.php我有

public function fire($event)
 {
    $property = $event;
    ...
    //get GPS data from Google Maps

    $property->latitude = $googleMapsObject->latitude;
    $property->longitude = $googleMapsObject->longitude;
    $property->save();
 }

当我保存模型时进入无限递归,因为在处理程序中引发了save()

如何更改我的代码以使其在保存后仅填充一次位置数据并避免递归?

我不能使用flushEventListeners(),因为在这种情况下,其他监听器会停止工作(例如附加属性照片)。

【问题讨论】:

  • 触发时,使用保存,而不是保存
  • 只需覆盖属性中的保存方法 - 传递保存模式的参数。在这样的 fire() 中: $property->save(Property::SAVE_FROM_GOOGLE_FIRE);其他更多的东西。

标签: php laravel events eloquent


【解决方案1】:

我也打过。在 Laravel 5.6 中,您可以简单地覆盖模型中的 finishSave() 函数:

protected function finishSave(array $options)
{
    // this condition allow to control whenever fire the vent or not
    if (!isset($options['nosavedevent']) or empty($options['nosavedevent'])) {
        $this->fireModelEvent('saved', false);
    }

    if ($this->isDirty() && ($options['touch'] ?? true)) {
        $this->touchOwners();
    }

    $this->syncOriginal();
}

那么你可以这样使用它:

public function fire($event)
{
    $property = $event;
    ...
    //get GPS data from Google Maps

   $property->latitude = $googleMapsObject->latitude;
   $property->longitude = $googleMapsObject->longitude;
   $property->save(['nosavedevent' => true]);
}

【讨论】:

    【解决方案2】:

    在这种情况下,使用saving 方法可能会更好。但请注意,在saving 期间,您不应再使用save 方法,因此您的fire 方法应如下所示:

    public function fire($event)
    {
        $property = $event;
        ...
        //get GPS data from Google Maps
    
        $property->latitude = $googleMapsObject->latitude;
        $property->longitude = $googleMapsObject->longitude;
    }
    

    其他解决方案是添加条件以设置和保存 GPS 位置(仅在尚未设置时):

    if (empty($property->latitude) || empty($property->longitude)) {
        $property->latitude = $googleMapsObject->latitude;
        $property->longitude = $googleMapsObject->longitude;
        $property->save();
    }
    

    【讨论】:

    • 这无法避免递归
    • @Deep 你为什么这么认为?如果只保存两次,则不会有任何递归
    • 因为属性(值,存在)不能作为保存模式的主/主
    • 你的意思是我必须听保存eloquent.saving: Exclusives\\Models\\Exclusive事件?其他解决方案不适用,因为如果用户更改街道地址,我必须找到新坐标。
    • @ademin 是的,我的意思是,但每次更新坐标也可能不合理,您可能会对 getDirty 方法感兴趣,仅当特定字段已更改时才更新坐标跨度>
    【解决方案3】:

    在我的例子中,使用saveQuietly() 而不是save() 解决了这个问题,因为saveQuietly 不会触发任何事件,因此您不会陷入事件的无限循环。

    编辑:我认为 saveQuietly() 方法目前仅在 laravel 8 中可用。

    【讨论】:

      【解决方案4】:

      您的属性保存方法(您必须为其定义属性常量):

      public function save($mode = Property::SAVE_DEFAULT)
      {
          switch ($mode) {
              case Property::SAVE_FOO:
                  // something for foo
              break;
              case Property::SAVE_BAR:
                  // something for bar
              break;
              default:
                  parent::save();
              break;
          }
      }
      

      叫它:

      public function fire($event)
       {
          $property = $event;
          ...
          //get GPS data from Google Maps
      
          $property->latitude = $googleMapsObject->latitude;
          $property->longitude = $googleMapsObject->longitude;
          $property->save(Property::SAVE_FOO);
       }
      

      $property->save(); // as default
      

      有什么好处? 所有条件都在一个地方(在保存方法中)。

      【讨论】:

      • 对不起,你的意思是我应该直接写到数据库而不是// something for foo
      • 是的,为什么不呢?你自己已经来递归了。你有两种方法:1)不递归重写 2)覆盖保存方法,并在其中写入所有需要的内容。
      • 我不知道,它看起来不像解决方案,它更像是一个 hack。我预计可能是我可以创建自定义事件或通过事件传递一些参数...
      • @ademin 我同意。老实说,我在 laravel 方面没什么技能。我们一起等。我也对这些机会 laravel 感兴趣。但您必须明白,这种情况是导致应用程序架构设计不佳的原因。
      • @ademin 你好吗?你是怎么解决的?
      【解决方案5】:

      您可以使用forget() 取消设置事件监听器。

      Event::listen('a', function(){
          Event::forget('a');
      
          echo 'update a ';
          event("b");
      });
      
      Event::listen('b', function(){
          Event::forget('b');
      
          echo 'update b ';
          event("a");
      });
      
      event("a"); // update a update b
      

      模型事件键被命名为“eloquent.{$event}: {$name}”例如“eloquent.updated: Foo

      【讨论】:

        【解决方案6】:

        在你的 fire 方法中,你可以让它在应用新属性和保存之前调用 $property->syncOriginal()

        模型类有一个“脏”属性与“原始”属性的概念,作为了解哪些值已被推送到数据库以及哪些值仍计划用于即将到来的保存的一种方式。通常,Laravel 直到 Observers 被触发后才会将它们同步在一起。严格来说,让你的 Listener 表现得好像它知道触发它的上下文是一种蚂蚁模式。因为您是从saved 操作触发它,因此可以确信数据已经到达数据库。但既然你是,问题只是模型还没有意识到这一点。任何后续更新都会让 Observer 误以为让你到达那里的原始值是全新的。因此,通过在应用任何其他更改之前自己显式调用 syncOriginal() 方法,您应该能够避免递归。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-08-16
          • 1970-01-01
          • 2017-12-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多