0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ツリー構造の循環参照をチェックするバリデーションを考えてみる

Posted at

はじめに

E-kanの前中です。
前回の記事ではツリー構造を持つデータを扱うモデルを考えてみました。
今回はその続きで、循環参照を回避するためのバリデーションチェックについて考えてみます。

まず考えること

ツリー構造で循環参照が発生するのはどの場合でしょうか。
前回考えたのは次のようなデータでした。

社長
├部長1
│ ├課長A
│ ├課長B
│ └課長C
└部長2
  ├課長D
  └課長E
    ├係長a
    └係長b

仮に、データを追加登録する場合を考えてみます。
課長Eの下に係長cを増やしてみることを考えましょう。
さて、増やした係長cが循環参照を発生させることはあるでしょうか?
結論から言うと、データを追加登録する場合は循環参照にはなりません。
係長cの上司は指定できますが、この時点で係長cの下につく部下を指定することはできないからです。

では、データを編集する場合はどうでしょうか?
部長2の上司を社長から係長aに変えたら?
部長2の部下が課長Eでそのまた部下が係長aで、そのまた部下が部長2……という具合に上司部下の関係が循環する状態になりますね。
現実ではこんなことは起こらないはずですが、データ上は可能です。

ということで、編集時にチェックして回避すれば十分そうです。

どんなロジックにしようか

循環参照になればアウトですが、プログラムで判定するにはどうすれば良いでしょう。
上の例で考えてみると、部長2の上司を社長から係長aに変えようとしています。
この係長aの上司は課長Eです。
課長Eの上司は部長2、つまり今編集しようとしているデータ自身ということになります。
ということは、設定した上司のIDを順に辿っていって、自分のIDにたどり着いたらアウトと考えればいけそうですね。

Laravelで実装するために

さて、Laravelで実装するにあたってちょっと考えないといけません。
Laravelであらかじめ用意されているバリデーションルールにはこんなものは存在しませんので、オリジナルのバリデーションとして実装しなくてはいけません。
ではどこに実装すればよいか?というのが問題になります。

php artisan make:rule OriginalRule

みたいなコマンドを使ってRuleオブジェクトを作る方法もありますが、この場合は対象となる値しか使えません。
今回の例では上司のIDしか判定の処理に使えません。
考えたいバリデーションのロジックは上司のIDをたどって自分のIDと比較する必要があります。
つまり、自分のIDも判定処理に使わないといけません。
この場合はRequestオブジェクトに実装する方法が使えます。
ということで具体例を示します。
前回実装例として作成したソースの続きで作ってみます。
Requestオブジェクトはartisanコマンドで作成した時にまとめて作成されているものを使います。

UpdateAreaRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdateAreaRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        $parentAreaRule = function($attribute, $value, $fail) {
            // 入力を取得
            $input_data = $this->all();
            $parentArea = new Area::find($value);
            if(!self::isSafeParentArea($parentArea, $input_data['id']))
            {
                $fail('循環参照が発生します。親エリアを変更してください。')
            }
        };
        return [
            'name' => ['required', 'max:255'],
            'memo' => ['max:255'],
            'parent_area_id' => ['integer', $parentAreaRule],
        ];
    }

    /**
     * 親エリアのIDを順に辿って、比較対象のIDと一致するものがあるかをチェックする。
     * 一致するものがあれば循環参照になるのでfalseを返す
     */
    private function isSafeParentArea($area, $checkId)
    {
        if($area->parentArea)
        {
            if($area->parentArea->id == $checkId)
            {
                return false;
            }
            return self::isSafeParentArea($area->parentArea, $checkId);
        }
        return true;
    }
}

rules関数の中にオリジナルの判定処理を書くことができますが、今回は親IDを不定回数たどる必要があります。
つまり、親IDを何回たどったらいいかがわかりません。
こういった場合には再帰処理として実装するのが便利です。
今回のソースで実際に再帰処理を行っているのは、一番下に書いてあるisSafeParentAreaという関数になります。
場合分けをして、条件によって自分自身を呼び出しているのが特徴です。
あとはrules関数の戻り値の配列にオリジナル処理を入れてやればOKです。

テストや実際にフォームと組み合わせて使ってみるのはまた次回に。

最後に

プログラムをやっていると、再帰処理でコーディングした方が楽な状況に出くわします。
何も考えずに実装するとメモリ不足になることもあって注意が必要ですが、上手く使うと一見ややこしそうな処理もすっきりと実装できます。
狙い通り動くと気持ちいい、というのはさておき、後々メンテナンスが楽になったりしますので、これまで使ったことがないという人は機会があれば挑戦してみると良いですよ。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?