LoginSignup
0
0

LaravelのCollectionのuniqueメソッドの挙動で気になったのでコードを追ってみた

Posted at

ある日の出来事

「複数件のデータが取得できるはずなのに一件分しか取得できてない!」とバグがあがりました。

調べてみると取得クエリのwhereInの引数に渡す際、laravelのpluckで$collectionから必要な要素を取り出した後にuniqueで重複削除処理をしている部分があるのですがそれによって
想定だと複数の要素が入ってくる予定なのに要素が一つだけになっていました。

 // 想定だと [1,2,3,4]と複数が入ってくるはずなのに 
 //   結果は、[1]が返ってきた
 $targetIds = $collection->pluck('target_id')->unique('target_id') 
 
 Target::whereIn('target_id',$targetIds)->get();

結論

->pluck('target_id')の部分で [1,2,3,4]のようなデータ構造になっていて

その後にuniqueメソッドの引数で存在してないkeyを渡していたことが原因でした

単なるミスなのですが
でもなぜ1つだけは返ってくるのかが気になったのでlaravelのコードを追ってみました

解説

lararvelのuniqueメソッドの定義

    /**
     * Return only unique items from the collection array.
     *
     * @param  string|callable|null  $key
     * @param  bool  $strict
     * @return static
     */
    public function unique($key = null, $strict = false)
    {
        if (is_null($key) && $strict === false) {
            return new static(array_unique($this->items, SORT_REGULAR));
        }

        $callback = $this->valueRetriever($key);

        $exists = [];

        return $this->reject(function ($item, $key) use ($callback, $strict, &$exists) {
            if (in_array($id = $callback($item, $key), $exists, $strict)) {
                return true;
            }

            $exists[] = $id;
        });
    }
    

rejectの定義

    /**
     * Create a collection of all elements that do not pass a given truth test.
     *
     * @param  callable|mixed  $callback
     * @return static
     */
    public function reject($callback = true)
    {
        $useAsCallable = $this->useAsCallable($callback);

        return $this->filter(function ($value, $key) use ($callback, $useAsCallable) {
            return $useAsCallable
                ? ! $callback($value, $key)
                : $value != $callback;
        });
    }

valueRetrieverの定義

    /**
     * Get a value retrieving callback.
     *
     * @param  callable|string|null  $value
     * @return callable
     */
    protected function valueRetriever($value)
    {
        if ($this->useAsCallable($value)) {
            return $value;
        }

        return function ($item) use ($value) {
            return data_get($item, $value);
        };
    }

collect([1,2,3])->unique('a')のように実行すると以下のコードで
unique関数での戻り値の部分は実質このようになります

$exists = []
return $this->filter(function ($value, $key) use(&$exists) {
            if(in_array($id = data_get($value, 'a'), $exists, $strict)) {
                return false
            }

            $exists[] = $id;
            return true
       })

一番最初
1が評価されます、その時$idにnullが入ります。
そして$existsに1がpushされます
そしてreturn true

2番目
2が評価されます、その時$idにnullが入ります。
$existsにはnullが既に入っているのでifの条件式がtrueになります
そして return falseされます

その次以降
2番目と同じ全てfalseを返します

よって一番最初の要素のみが入ったコレクションが返却されるということになる

補足

unique関数での戻り値の部分の実質のコードへの変換する際に
以下の前提を考慮する必要がある

  • PHPは戻り値を明示的に書かない場合は暗黙的にnullが返ってくる
  • PHPは定義している関数の想定より引数が少ない状態で実行した場合はエラーになるが、引数を余計に渡している場合は無視してエラーは出ない
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