Eloquentの多対多関係でattach/detach/sync時にイベント発火

f:id:n06utk:20190308164355p:plain

GLUGの小林です。初めてなので軽めのネタで。 弊社の子会社が運営しているEATASというメディアで、記事にカテゴリがついており、元のカテゴリ部分は以下でした。

class Article extends Model
{
    public function category()
    {
        return $this->belongsTo('App\Models\Category');
    }

以下新しい要件が出てきました。

  • カテゴリをネストしたい
  • カテゴリを複数をつけたい。
    • パンくずは流入経路を優先
    • それ以外は優先順位に従って
  • カテゴリページが欲しい

そこで以下に変更

class Article extends Model
{
    public function categories()
    {
        return $this->belongsToMany('App\Models\Category')->withPivot('priority');
    }
class Category extends Model
{
    public function articles()
    {
        return $this->belongsToMany('App\Models\Article');
    }
    public function parentCategory()
    {
        return $this->belongsTo(self::class,'parent_category_id');
    }

    public function childCategories()
    {
        return $this->hasMany(self::class,'parent_category_id');
    }

ここまでは問題なかったのですが、以下の新しい要件でちょっと問題が

  • NEWSカテゴリについてはトップページに表示しない

今のままだとSQLで表現するとこうなるので、ちょっとパフォーマンス的に厳しい

SELECT * 
FROM articles
    INNER JOIN article_category ON articles.id = article_category.areticle_id
WHERE
    article_category.category_id = 1
    AND
    他の条件

なので、articlesテーブルにis_newsカラム作ってそれで絞り込もう、と考えましたが、

$article->categories()->attach(Category::NEWS_CATEGORY_ID);

のときにいちいち

$article->is_news = 1;
$article->save();

するわけにもいかないので、attach/detach/syncのときにイベント発生しないかあと考えました。 割とやりたい人いそうな気がしたのですが、ぐぐってもやり方出てこず。意外といないものなのですね。 ということで、

https://github.com/laravel/framework/blob/5.5/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php#L210

public function attach($id, array $attributes = [], $touch = true)
{
    // Here we will insert the attachment records into the pivot table. Once we have
    // inserted the records, we will touch the relationships if necessary and the
    // function will return. We can parse the IDs before inserting the records.
    $this->newPivotStatement()->insert($this->formatAttachRecords(
        $this->parseIds($id), $attributes
    ));
    if ($touch) {
        $this->touchIfTouching();
    }
}

https://github.com/laravel/framework/blob/5.5/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php#L677

public function touchIfTouching()
{
    if ($this->touchingParent()) {
        $this->getParent()->touch();
    }
    if ($this->getParent()->touches($this->relationName)) {
        $this->touch();
    }
}

touch()呼んでるみたいなので、以下対応でカテゴリ追加・削除時にis_newsが変わるようになりました。

class Article
{
    public function touch(){
        $article = $this->fresh();
        if ($article->categories != null && $article->categories->find(Category::NEWS_CATEGORY_ID)){
            $article->is_news = 1;
        }else{
            $article->is_news = 0;
        }
        if ($this->is_news == $article->is_news){
            parent::touch();
        }else{
            $article->save();
        }
    }
}
class Category
{
    protected $touches = ['articles'];
}