Коллекции, Builder и удаление вложенных моделей

В Ларавеле вложенные (каскадные) отношения моделей — обычное дело. У автора есть записи, у записей есть комментарии, и так далее. Не менее обычное дело — необходимость удалять дочерние модели при удалении родителя. Самое первое, что приходит на ум — каскадное удаление моделей, которое включается при определении foreign-ключей и будет выполняться автоматически базой данных. Но у такого способа есть достаточно весомый недостаток: события deleting и deleted для удаляемых моделей не произойдут и их обработчики, соответственно, не будут вызваны. Ну, потому что удаление в БД происходит.

Поэтому, если существование обработчиков событий предполагается, лучше навесить удаление дочерних моделей на событие deleting модели родительской. Вот есть допустим модель Company и у нее множество моделей Contract, определенное вот так:

public function contracts() {
    return $this->hasMany('App\Contract');
}

Реализуем обработчик события deleting:

protected static function boot() {
    parent::boot();

    static::deleting(function (Company $company) {
        // здесь мы можем удалять контракты
    });

}

Но тут есть еще одна маленькая хитрость. Если мы напишем $company->contracts()->delete(), то контракты удалятся, но их события deleting и deleted тоже не произойдут. Дело в том, что contracts() возвращает нам не что иное, как Query Builder, и когда мы вызовем delete() — это будет обычный delete-запрос к БД. На эту тему даже иссуй есть.

Вызвать $company->contracts->delete() мы тоже не можем, потому что $company->contracts — это просто коллекция, нет такого метода у коллекции. Чтобы события сработали (например, чтобы удалить дочерние модели контрактов), нужно загружать каждую модель и вызывать delete() на ней. Примерно так:

static::deleting(function (Company $company) {
   $company->contracts->each(function (Contract $contract) {
       $contract->delete();
   });
});

Также подойдет метод Contract::destroy(), в который нужно передать массив с айдишниками контрактов — он сделает то же самое, что и приведенный выше код.

Комментарии