Yiiではフラグメントキャッシュ中のウィジェット利用に注意

Yiiはウィジェットが便利で、なぜってアセットバンドルがあって便利だからで、いっぽうでビューのフラグメントキャッシュも便利、なんですが、それらを全て組み合わせて使う場合には注意が必要です。

### うまくいく例

```php
<?php
use yii\widgets\ListView;
use yii\jui\DatePicker;
?>

<?php ListView::widget([
    'dataProvider' => $provider,
]); ?>

<?php DatePicker::widget([
    'model' => $model,
    'attribute' => 'some_date',
]); ?>
```

これだけで、ざっくりこういう感じになります。

```html
<div id="#w0">
  ... n 件中 1 - x を表示しています。
  <ul>
    <li>...</li>
    ...
  </ul>
  ... pagination など
</div>

<input id="#w1" name="SomeModel[some_date]" value="..."> 

<script src=".../jquery-ui.js"></script>
<script>
jQuery(function() {
   $('#w1').datepicker(...);
});
</script>
```

とくに JS を意識していませんが、 必要なアセットが勝手に読み込まれ、しかも、自動的にセレクタを指して、jQuery プラグインが勝手に適用されています。

### うまくいかない例

ここで、ListViewが重いのでフラグメントキャッシュを使おうとしてこうしたとします。

```php
<?php if ($this->beginCache(...)): ?>
    <?php ListView::widget([
        'dataProvider' => $provider,
    ]); ?>
    <?php $this->endCahce(); ?>
<?php endif; ?>
```

初回は上記HTML出力と同じになりますが、キャッシュが効いたときはなんとこうなります。

```html
<div id="#w0">...</div>

<input id="#w0" name="SomeModel[some_date]" value="..."> 

<script src=".../jquery-ui.js"></script>
<script>
jQuery(function() {
   $('#w0').datepicker(...);
});
</script>
```

あれあれ?? `w0` が重複しますね。このままだと `<div>` のほうに `datepicker` が適用されるかもしれません。

キャッシュが聞いていると、 ListView のレンダリング処理がスキップされて高速化されます。そうすると、ウィジェットの自動ID採番が狂ってきます。

### どうするか

ウィジェットを自動採番に任せない、とくに、フラグメントキャッシュ内では絶対に避けるようにしましょう。

```php
<?php if ($this->beginCache(...)): ?>
    <?php ListView::widget([
        'id' => 'heavy-list-view',  // 明示的にidをつける
        'dataProvider' => $provider,
    ]); ?>
    <?php $this->endCahce(); ?>
<?php endif; ?>
```

```html
<div id="#heavy-list-view">...</div>

<input id="#w0" name="SomeModel[some_date]" value="..."> 

<script src=".../jquery-ui.js"></script>
<script>
jQuery(function() {
   $('#w0').datepicker(...);
});
</script>
```

少なくともキャッシュの外にあるものの整合性は合いました。めでたしめでたし。

### 蛇足
... ところでこれ ListView じゃなくて GridView だったら? GridView は `yii.gridView.js` をリンクしてウィジェットをJSでリッチにしてくれます。

```php
<?php
use yii\grid\GridView;
?>

<?php GridView::widget([
    'id' => 'heavy-list-view',
    'dataProvider' => $provider,
]); ?>
```

```php
<div id="heavy-grid-view">...</div>

<script src=".../yii.gridView.js"></script>
<script>
jQuery(function() {
   jQuery('#heavy-grid-view').yiiGridView(...);
});
</script>
```

GridView はかなり動的でキャッシュとは相性が悪いので普通はやりませんが、まあ、もしこういう性質のものを何か使ったとしたら、と考えてください。

まれにですが、JavaScript ですごいことをしたいけど、重いからキャッシュもしたい、それをウィジェットにしておきたい、という状況になることがあります。

```php
<?php if ($this->beginCache(...)): ?>
    <?php GridView::widget([
        'id' => 'heavy-grid-view',
        'dataProvider' => $provider,
    ]); ?>
    <?php $this->endCahce(); ?>
<?php endif; ?>
```

```html
<div id="heavy-grid-view">...</div>
```

キャッシュが効くと JavaScript 関連のコードがまったく出力されません。当然ですね。`if-endif` でスキップされてしまうんだから。

GridView だったらこれどうするか。

```php
<?php if ($this->beginCache(...)): ?>
    <?php GridView::widget([
        'id' => 'heavy-grid-view',
        'dataProvider' => $provider,
    ]); ?>
    <?php $this->endCahce(); ?>
<?php else: ?>
    <?php
    // キャッシュが効いたとき
    GridView::widget([
        'id' => 'heavy-grid-view',
        'dataProvider' => new ArrayDataProvider([
             'allModels' => [], // ダミーの空データ
         ]),
    ]); // echo してないのでHTML出力されない。キャッシュからのものだけが出力される
    ?>
<?php endif; ?>
```

似た代替処理を走らせてうまいこと騙します。これで、キャッシュが効いた場合にアセット関連の処理だけが生きてくるはずです。

既存のウィジェットでどうにかしようとした結果なのでハック臭が強いですが、自分で作ったウィジェットをキャッシュ内で使うのなら、アセット関連の処理だけを実行できるメソッドを呼べるようにしておいてもいいですね。

ようするに、フラグメントキャッシュはアセットまわりの処理結果まではキャッシュしない、普段は気にしなくてもセットになってて楽ちんだけど、本当はどうなっているのかを分けて考えることが大事ってことです。