laravel blade の @foreach ディレクティブ で利用可能な $loop 変数がどういう仕組みなのか調べてみた


blade で @foreach ディレクティブを使用した際に利用できる $loop 変数を初めて知った時、ちょっと感動したのはきっと僕だけではないはずです。何て便利なんだと。

https://readouble.com/laravel/10.x/ja/blade.html

そんな $loop 変数がどういう仕組みなのか気になって仕方がないので調べてみた結果をメモとして残します。


## サンプル foreach ディレクティブ

blade ファイルに下記のようなコードを書いた場合、

```php
@foreach ($array as $value)
    {{ $value }}
@endforeach
```

内部ではこんな感じのコードに変換されています


```php
// Illuminate\View\Compilers\Concerns\CompilesLoops

$__currentLoopData = $array;
$__env->addLoop($__currentLoopData);

foreach ($__currentLoopData as $value) :
    $__env->incrementLoopIndices();
    $loop = $__env->getLastLoop();

    echo $value;
endforeach;
$__env->popLoop(); 
$loop = $__env->getLastLoop();
```

順に説明を入れます。


## $__env->addLoop($__currentLoopData);

$loop 変数に格納するデータは、この関数で初期化されています。

初期化したデータは、下記の関数で $this->loopsStack に格納されます。

```php
// Illuminate\View\Concerns\ManagesLoops

/**
 * Add new loop to the stack.
 *
 * @param  \Countable|array  $data
 * @return void
 */
public function addLoop($data)
{
    $length = is_countable($data) && ! $data instanceof LazyCollection
                    ? count($data)
                    : null;

    $parent = Arr::last($this->loopsStack);

    $this->loopsStack[] = [
        'iteration' => 0,
        'index' => 0,
        'remaining' => $length ?? null,
        'count' => $length,
        'first' => true,
        'last' => isset($length) ? $length == 1 : null,
        'odd' => false,
        'even' => true,
        'depth' => count($this->loopsStack) + 1,
        'parent' => $parent ? (object) $parent : null,
    ];
}
```

この $this->loopsStack は、ループがネストした場合のための配列変数です
例えば、@foreach がネストした場合

```php
// 最初のループ
@foreach ($loopA as $loopB)
  // ネストされた2つ目のループ
  @foreach ($loopB as $loopC)
    // ネストされた3つ目のループ
    @foreach ($loopC as $value)
      // 何らかの処理
    @endforeach
  @endforeach
@endforeach
```

$this->loopsStack には下記のように格納されます

```
$this->loopsStack = [
  0 => $loopAのためのカウンタデータ, // 最初のループの対象変数
  1 => $loopBのためのカウンタデータ, // ネストされた2つ目のループの対象変数
  2 => $loopCのためのカウンタデータ, // ネストされた3つ目のループの対象変数
];

```




## $__env->incrementLoopIndices()

$this->loopsStack 内の最後の要素が、ループが回る度にこの関数でカウンタされます。

```php
// Illuminate\View\Concerns\ManagesLoops

/**
 * Increment the top loop's indices.
 *
 * @return void
 */
public function incrementLoopIndices()
{
    $loop = $this->loopsStack[$index = count($this->loopsStack) - 1];

    $this->loopsStack[$index] = array_merge($this->loopsStack[$index], [
        'iteration' => $loop['iteration'] + 1,
        'index' => $loop['iteration'],
        'first' => $loop['iteration'] == 0,
        'odd' => ! $loop['odd'],
        'even' => ! $loop['even'],
        'remaining' => isset($loop['count']) ? $loop['remaining'] - 1 : null,
        'last' => isset($loop['count']) ? $loop['iteration'] == $loop['count'] - 1 : null,
    ]);
}
```


## $loop = $__env->getLastLoop();

単純に $this->loopsStack の最後の要素を取り出しているだけです。
（現在のループの要素を取り出しています）

皆さんが利用している $loop 変数はこちらでセットされています。

```php
// Illuminate\View\Concerns\ManagesLoops

/**
 * Get an instance of the last loop in the stack.
 *
 * @return \stdClass|null
 */
public function getLastLoop()
{
    if ($last = Arr::last($this->loopsStack)) {
        return (object) $last;
    }
}
```

## $__env->popLoop(); 

endforeach の外側で行われているこの処理は、ループが終了して使わなくなった要素を $this->loopsStack から取り除くための処理です。

```php
// Illuminate\View\Concerns\ManagesLoops

/**
 * Pop a loop from the top of the loop stack.
 *
 * @return void
 */
public function popLoop()
{
    array_pop($this->loopsStack);
}
```


## 最後に行われている $loop = $__env->getLastLoop();

機能としては上の説明と一緒ですが、こちらはループがネストしている場合のための処理です
下記コードの該当部分のための処理になります。

```php
// 最初のループ
@foreach ($loopA as $loopB)
  // ネストされた2つ目のループ
  @foreach ($loopB as $loopC)
   　// 何らかの処理
  @endforeach

  // こちらで $loop 変数にアクセスするための処理

@endforeach
```

2つ目のループが終了したため、最初のループのデータを $this->loopsStack から取り出してセットします。



# 感想

ロジックとしてはそんなに難しいことやってないですが、便利な $loop 変数はこうやって作られるんだなと思いました（小並感


調べたついでに、blade ディレクティブを追加する拡張機能を作りました。よかったらどうぞ。

https://qiita.com/kazumacchi/items/c59d799d1f28d6ba9181