Browse Source

Fixes #13679: Added `yii\behaviors\CacheableWidgetBehavior`

tags/2.0.14
Nikolay Oleynikov 7 years ago committed by Alexander Makarov
parent
commit
37bd97ad9a
  1. 1
      framework/CHANGELOG.md
  2. 166
      framework/behaviors/CacheableWidgetBehavior.php
  3. 183
      tests/framework/behaviors/CacheableWidgetBehaviorTest.php

1
framework/CHANGELOG.md

@ -68,6 +68,7 @@ Yii Framework 2 Change Log
- Enh #9137: Added `Access-Control-Allow-Method` header for the OPTIONS request (developeruz)
- Enh #9253: Allow `variations` to be a string for `yii\filters\PageCache` and `yii\widgets\FragmentCache` (schojniak, developeruz)
- Enh #12623: Added `yii\helpers\StringHelper::matchWildcard()` replacing usage of `fnmatch()`, which may be unreliable (klimov-paul)
- Enh #13679: Added `yii\behaviors\CacheableWidgetBehavior` (Kolyunya)
- Enh #13919: Added option to add comment for created table to migration console command (mixartemev, developeruz)
- Enh #14043: Added `yii\helpers\IpHelper` (silverfire, cebe)
- Enh #14355: Added ability to pass an empty array as a parameter in console command (developeruz)

166
framework/behaviors/CacheableWidgetBehavior.php

@ -0,0 +1,166 @@
<?php
namespace yii\behaviors;
use yii\base\Behavior;
use yii\base\Widget;
use yii\base\WidgetEvent;
use yii\caching\CacheInterface;
use yii\caching\Dependency;
use yii\di\Instance;
/**
* Cacheable widget behavior automatically caches widget contents according to duration and dependencies specified.
*
* The behavior may be used without any configuration if an application has `cache` component configured.
* By default the widget will be cached for one minute.
*
* The following example will cache the posts widget for an indefinite duration until any post is modified.
*
* ```php
* use yii\behaviors\CacheableWidgetBehavior;
*
* public function behaviors()
* {
* return [
* [
* 'class' => CacheableWidgetBehavior::className(),
* 'cacheDuration' => 0,
* 'cacheDependency' => [
* 'class' => 'yii\caching\DbDependency',
* 'sql' => 'SELECT MAX(updated_at) FROM posts',
* ],
* ],
* ];
* }
* ```
*
* @property Widget $owner
* @group behaviors
* @since 2.0.14
*/
class CacheableWidgetBehavior extends Behavior
{
/**
* @var CacheInterface|string|array a cache object or a cache component ID
* or a configuration array for creating a cache object.
* Defaults to the `cache` application component.
*/
public $cache = 'cache';
/**
* @var int cache duration in seconds.
* Set to `0` to indicate that the cached data will never expire.
* Defaults to 60 seconds or 1 minute.
*/
public $cacheDuration = 60;
/**
* @var Dependency|array|null a cache dependency or a configuration array
* for creating a cache dependency or `null` meaning no cache dependency.
*
* For example,
*
* ```php
* [
* 'class' => 'yii\caching\DbDependency',
* 'sql' => 'SELECT MAX(updated_at) FROM posts',
* ]
* ```
*
* would make the widget cache depend on the last modified time of all posts.
* If any post has its modification time changed, the cached content would be invalidated.
*/
public $cacheDependency;
/**
* @var string[]|string an array of strings or a single string which would cause
* the variation of the content being cached (e.g. an application language, a GET parameter).
*
* The following variation setting will cause the content to be cached in different versions
* according to the current application language:
*
* ```php
* [
* Yii::$app->language,
* ]
* ```
*/
public $cacheKeyVariations = [];
/**
* @var bool whether to enable caching or not. Allows to turn the widget caching
* on and off according to specific conditions.
* The following configuration will disable caching when a special GET parameter is passed:
*
* ```php
* empty(Yii::$app->request->get('disable-caching'))
* ```
*/
public $cacheEnabled = true;
/**
*{@inheritdoc}
*/
public function attach($owner)
{
parent::attach($owner);
$this->initializeEventHandlers();
}
/**
* Begins fragment caching. Prevents owner widget from execution
* if its contents can be retrieved from the cache.
*
* @param WidgetEvent $event `Widget::EVENT_BEFORE_RUN` event.
*/
public function beforeRun($event)
{
$cacheConfig = [
'cache' => Instance::ensure($this->cache, 'yii\caching\CacheInterface'),
'duration' => $this->cacheDuration,
'dependency' => $this->cacheDependency,
'enabled' => $this->cacheEnabled,
];
if (!$this->owner->view->beginCache($this->getCacheKey(), $cacheConfig)) {
$event->isValid = false;
}
}
/**
* Outputs widget contents and ends fragment caching.
*
* @param WidgetEvent $event `Widget::EVENT_AFTER_RUN` event.
*/
public function afterRun($event)
{
echo $event->result;
$event->result = null;
$this->owner->view->endCache();
}
/**
* Returns widget cache key.
*/
private function getCacheKey()
{
$cacheKey = array_merge(
(array)get_class($this->owner),
(array)$this->cacheKeyVariations
);
return $cacheKey;
}
/**
* Initializes widget event handlers.
*/
private function initializeEventHandlers()
{
$this->owner->on(Widget::EVENT_BEFORE_RUN, [$this, 'beforeRun']);
$this->owner->on(Widget::EVENT_AFTER_RUN, [$this, 'afterRun']);
}
}

183
tests/framework/behaviors/CacheableWidgetBehaviorTest.php

@ -0,0 +1,183 @@
<?php
namespace yiiunit\framework\behaviors;
use PHPUnit_Framework_MockObject_MockObject;
use yii\base\Widget;
use yii\behaviors\CacheableWidgetBehavior;
use yiiunit\TestCase;
/**
* Unit test for [[\yii\behaviors\CacheableWidgetBehavior]].
*
* @see CacheableWidgetBehavior
* @group behaviors
*/
class CacheableWidgetBehaviorTest extends TestCase
{
/**
* Default-initialized simple cacheable widget mock.
*
* @var PHPUnit_Framework_MockObject_MockObject
*/
private $simpleWidget;
/**
* Default-initialized dynamic cacheable widget mock.
*
* @var PHPUnit_Framework_MockObject_MockObject
*/
private $dynamicWidget;
/**
* @inheritdoc
*/
protected function setUp()
{
$this->initializeApplicationMock();
$this->initializeWidgetMocks();
}
public function testWidgetIsRunWhenCacheIsEmpty()
{
$this->simpleWidget
->expects($this->once())
->method('run');
$contents = $this->simpleWidget->test();
$this->assertEquals('contents', $contents);
}
public function testWidgetIsNotRunWhenCacheIsNotEmpty()
{
$this->simpleWidget->cacheDuration = 0;
$this->simpleWidget
->expects($this->once())
->method('run');
for ($counter = 0; $counter <= 42; $counter++) {
$this->assertEquals('contents', $this->simpleWidget->test());
}
}
public function testDynamicContent()
{
$this->dynamicWidget->cacheDuration = 0;
$this->dynamicWidget
->expects($this->once())
->method('run');
for ($counter = 0; $counter <= 42; $counter++) {
$expectedContents = sprintf('<div>dynamic contents: %d</div>', $counter);
$this->assertEquals($expectedContents, $this->dynamicWidget->test());
}
}
/**
* Initializes a mock application.
*
*/
private function initializeApplicationMock()
{
$this->mockApplication([
'components' => [
'cache' => [
'class' => '\yii\caching\ArrayCache',
],
],
'params' => [
// Counter for dynamic contents testing.
'counter' => 0,
],
]);
}
/**
* Initializes mock widgets.
*
*/
private function initializeWidgetMocks()
{
$this->simpleWidget = $this->getWidgetMock(SimpleCacheableWidget::className());
$this->dynamicWidget = $this->getWidgetMock(DynamicCacheableWidget::className());
}
/**
* Returns a widget mock.
*
*/
private function getWidgetMock($widgetClass)
{
$widgetMock = $this->getMockBuilder($widgetClass)
->setMethods(['run'])
->enableOriginalConstructor()
->enableProxyingToOriginalMethods()
->getMock();
return $widgetMock;
}
}
class BaseCacheableWidget extends Widget
{
/**
* @inheritdoc
*/
public function test()
{
ob_start();
ob_implicit_flush(false);
try {
$out = '';
if ($this->beforeRun()) {
$result = $this->run();
$out = $this->afterRun($result);
}
} catch (\Exception $exception) {
if (ob_get_level() > 0) {
ob_end_clean();
}
throw $exception;
}
return ob_get_clean() . $out;
}
/**
* @inheritdoc
*/
public function behaviors()
{
return [
'cacheable' => 'yii\behaviors\CacheableWidgetBehavior',
];
}
}
class SimpleCacheableWidget extends BaseCacheableWidget
{
/**
* @inheritdoc
*/
public function run()
{
$content = 'contents';
return $content;
}
}
class DynamicCacheableWidget extends BaseCacheableWidget
{
/**
* @inheritdoc
*/
public function run()
{
$dynamicContentsExpression = 'return "dynamic contents: " . \Yii::$app->params["counter"]++;';
$dynamicContents = $this->view->renderDynamic($dynamicContentsExpression);
$content = '<div>' . $dynamicContents . '</div>';
return $content;
}
}
Loading…
Cancel
Save