Render a very heavy widget (with very expensive DB queries, rendering and calculations) in many views/layouts and cache it entirely as a single entity.
We need a base cacheable widget.
$dependency = [
'class' => 'yii\caching\DbDependency',
'sql' => 'SELECT MAX(updated_at) FROM post',
];
if ($this->beginCache($id, ['dependency' => $dependency])) {
// ... our widget goes here ...
$this->endCache();
}
if ($this->beginCache($id, MyWidget::CACHE_CONFIG)) {
// ... our widget goes here ...
$this->endCache();
}
public function run()
{
$cache = Yii::$app->cache;
$cacheKey = sprintf(
'my-widget-%d-%d-%d-%d',
$this->foo,
$this->bar,
$this->baz,
$this->quux,
);
$widget = $cache->get($cacheKey);
if ($widget === false) {
$this->initialize();
$widget = $this->render($this->template, [
'foo' => $this->foo,
'bar' => $this->bar,
'baz' => $this->baz,
'quuz' => $this->quux,
]);
$cache->set($cacheKey, $widget, self::CACHE_DURATION);
}
return $widget;
}
Not applicable since can't cache entire widget with template rendering and generic calculations.
We need to implement a base cacheable widget. We already have a yii\widgets\FragmentCache but it's impossible to use it conveniently in described scenario. The protected interface of the widget should have the following methods:
getCacheComponent() - allow widget to define which cache component to use or leave empty to use default.getCacheDuration() - allow widget to define caching duration or leave empty for a default value.getCacheDependency() - allow widget to define cache dependency or leave empty.getCacheKeyVariations() - allow widget to define cache variations or leave empty.getCacheKey() - allow widget to define a custom cache key if really needed.I've made a draft implementation using yii\widgets\FragmentCache with renderDynamic() support.
class Widget extends CacheableWidget
{
/**
*
* @var Foo
*/
public $foo;
/**
*
* @var Bar
*/
public $bar;
/**
* @inheritdoc
*/
public function run()
{
// Do some heavy stuff.
$this->doSomeHeavyStuff();
$this->doSomeOtherHeavyStuff();
// Render some heavy templates.
return $this->render('widget',
'foo' => $this->foo,
'bar' => $this->bar,
]);
}
/**
* @inheritdoc
*/
protected function getCacheKeyVariations()
{
return [
$this->foo,
$this->bar,
];
}
}
<?= Widget::widget(); ?>
I can't see any yet.
I would be glad to hear community and developers opinion on this.
What about widgets using begin() and end()...
CacheableWidget::begin();
AnotherCachableWidget::widget();
CacheableWidget::end();
How would you know if a sub widget's cache is still valid?
It might useful to also add a class to the framework that keeps track of the widget stack and allows widgets to add arbitrary data to keep track of such as cache expired etc. But this might be too heavy since any lookup would require you to traverse an array checking values. However it may not be that bad since there shouldn't be that many nested widgets in most use cases.
How would you know if a sub widget's cache is still valid?
@derekisbusy the whole idea behind a cacheable widget is that we don't care about what happens inside of it is as long as it's cache is valid. As long as the cache value is valid we just get widget's contents from the cache. When cache invalidates child widget's cache will be revalidated and re-rendered if needed.
@kolyunya. That is the case. However. If the parent widget is valid and the childs aint. The parents cache wont get busted
@TerraSkye that is how fragment caching and cacheable widget work by design. The parent widget cache is not supposed to get invalidated when child widgets do. If you want to achieve this you should add extra cache dependencies to the parent widget.
With EVENT_BEFORE_RUN and EVENT_AFTER_RUN added in 2.0.11 it seems it's possible to implement it via behaviors.
@Kolyunya, because caching is mainly about wrapping static code, the new events added in 2.0.11 can be used for that. In this way we can keep our classes clean and move different logic in different classes.
This topic will be closed in favor or https://github.com/yiisoft/yii2/pull/9981#issuecomment-150582976
@samdark @dynasource I'm currently working on a cacheable behavior for widgets. Are there any chances you are going to be interested in having such behavior in core? Should I make a PR?
Depends on usage and how easier it is compared to subclassing.
@samdark this is still a WIP, but you can already get the general idea. Usage is as simple as:
class MyCacheableWidget extends \yii\base\Widget
{
/**
* @inheritdoc
*/
public function behaviors()
{
return [
'cacheable' => 'yii\behaviors\CacheableBehavior',
];
}
/**
* @inheritdoc
*/
public function run()
{
return 'contents';
}
}
If you're using inheritance already, why not to wrap call explicitly with caching? It's very simple.
@samdark I'm sorry I wasn't able to get your point. Current implementation doesn't include widget inheritance. Which call do you suggest to wrap in caching?
class MyCacheableWidget extends Widget
Isn't that inheritance? In order to make widget cacheable you have to extend from it and add behavior, right?
@samdark in order to make any widget you have to extend it from the yii\base\Widget, right? If you want to make it cacheable you just add a behavior to it. Current implementation does not introduce any new widget classes. It just implements a single widget behavior.
why not to wrap call explicitly with caching?
Could you elaborate please?
Regarding the internal usage of the FragmentCache instead of plain data caching. This approach was chosen in order to support dynamic contents.
You know precisely if the widget you're developing is using begin()/end() or just widget(). Let's assume we're developing UserAvatar and it uses widget() only. In that case adding caching is easy:
class UserAvatar extends Widget
{
public static function widget($config = [])
{
$key = 'user_avatar_' . $config['user']->id;
$out = Yii::$app->cache->get($key);
if (!$out) {
$out = parent::widget($config);
Yii::$app->cache->set($key, $out, 3600);
}
return $out;
}
}
Another thing is that both implementing cache like I did or with a behavior that you did won't save you from passing data to ::widget() call and probably some checks in init(). Explicit fragment caching you can wrap any widget into would do.
class UserAvatar extends Widget
{
public static function widget($config = [])
{
$key = 'user_avatar_' . $config['user']->id;
$out = Yii::$app->cache->get($key);
if (!$out) {
$out = parent::widget($config);
Yii::$app->cache->set($key, $out, 3600);
}
return $out;
}
}
I mentioned that in the OP. That approach obviously violates both DRY and KISS. Why would you want to implement same caching logic over and over again for a number of different widgets instead of just attaching a single behavior?
Downsides of the fragment caching in templates is also described in the OP.
OK. How about decorator which uses fragment caching instead of behavior?
@samdark I haven't considered that yet. Not sure if i would be convenient to use. We'll have to implement a separate decorator for each widget class or will have to configure a base decorator in templates which is undesired.
Which issues of the proposed behavior are we trying to solve using a decorator?
Caching sometimes varies per page and it's not a good idea to couple it to whole widget class.
i.e. it makes sense to cache stats longer on user-visible page but shorter on admin-only visible page. Stats widget used is the same.
@samdark that is not a problem. Just add the Yii::$app->request->pathInfo (or anything else what varies the widget apperiance) to your cacheKeyVariations.
@samdark this behavior doesn't force you to couple it with the widget. You can easily extend the behavior and reuse it for multiple widgets. Or just have it in a separate class for the sake of the separation of concepts.
@dynasource, @cebe, @SilverFire, @klimov-paul need your opinions.
Just to be clear: the latest implementation via a behavior is here.
to be able to wrap widgets for ie. caching purposes was one of the reasons to merge https://github.com/yiisoft/yii2/commit/f20c0177afc4dc5a4521adb57c7684eb67780335
IMO, such a behavior would be useful to have in core
Most helpful comment
With
EVENT_BEFORE_RUNandEVENT_AFTER_RUNadded in 2.0.11 it seems it's possible to implement it via behaviors.