diff --git a/docs/guide/caching-fragment.md b/docs/guide/caching-fragment.md index a9fef47ae1..5a5e9a1086 100644 --- a/docs/guide/caching-fragment.md +++ b/docs/guide/caching-fragment.md @@ -174,3 +174,6 @@ if ($this->beginCache($id1)) { The [[yii\base\View::renderDynamic()|renderDynamic()]] method takes a piece of PHP code as its parameter. The return value of the PHP code is treated as the dynamic content. The same PHP code will be executed for every request, no matter the enclosing fragment is being served from cached or not. + +> Note: since version 2.0.14 a dynamic content API is exposed via the [[yii\base\DynamicContentAwareInterface]] interface and its [[yii\base\DynamicContentAwareTrait]] trait. + As an example, you may refer to the [[yii\widgets\FragmentCache]] class. diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 7d48277070..88dd240ed5 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -4,6 +4,7 @@ Yii Framework 2 Change Log 2.0.14 under development ------------------------ +- Enh #15120: Refactored dynamic caching introducing `DynamicContentAwareInterface` and `DynamicContentAwareTrait` (sergeymakinen) - Bug #8983: Only truncate the original log file for rotation (matthewyang, developeruz) - Bug #11401: Fixed `yii\web\DbSession` concurrency issues when writing and regenerating IDs (samdark, andreasanta, cebe) - Bug #13034: Fixed `normalizePath` for windows network shares that start with two backslashes (developeruz) diff --git a/framework/base/DynamicContentAwareInterface.php b/framework/base/DynamicContentAwareInterface.php new file mode 100644 index 0000000000..2367f0837b --- /dev/null +++ b/framework/base/DynamicContentAwareInterface.php @@ -0,0 +1,40 @@ + + * @since 2.0.14 + */ +interface DynamicContentAwareInterface +{ + /** + * Returns a list of placeholders for dynamic content. This method + * is used internally to implement the content caching feature. + * @return array a list of placeholders. + */ + public function getDynamicPlaceholders(); + + /** + * Sets a list of placeholders for dynamic content. This method + * is used internally to implement the content caching feature. + * @param array $placeholders a list of placeholders. + */ + public function setDynamicPlaceholders($placeholders); + + /** + * Adds a placeholder for dynamic content. + * This method is used internally to implement the content caching feature. + * @param string $name the placeholder name. + * @param string $statements the PHP statements for generating the dynamic content. + */ + public function addDynamicPlaceholder($name, $statements); +} diff --git a/framework/base/DynamicContentAwareTrait.php b/framework/base/DynamicContentAwareTrait.php new file mode 100644 index 0000000000..a330717da7 --- /dev/null +++ b/framework/base/DynamicContentAwareTrait.php @@ -0,0 +1,77 @@ + + * @since 2.0.14 + */ +trait DynamicContentAwareTrait +{ + /** + * @var string[] a list of placeholders for dynamic content + */ + private $_dynamicPlaceholders; + + /** + * Returns the view object that can be used to render views or view files using dynamic contents. + * @return View the view object that can be used to render views or view files. + */ + abstract protected function getView(); + + /** + * {@inheritdoc} + */ + public function getDynamicPlaceholders() + { + return $this->_dynamicPlaceholders; + } + + /** + * {@inheritdoc} + */ + public function setDynamicPlaceholders($placeholders) + { + $this->_dynamicPlaceholders = $placeholders; + } + + /** + * {@inheritdoc} + */ + public function addDynamicPlaceholder($name, $statements) + { + $this->_dynamicPlaceholders[$name] = $statements; + } + + /** + * Replaces placeholders in $content with results of evaluated dynamic statements. + * @param string $content content to be parsed. + * @param string[] $placeholders placeholders and their values. + * @param bool $isRestoredFromCache whether content is going to be restored from cache. + * @return string final content. + */ + protected function updateDynamicContent($content, $placeholders, $isRestoredFromCache = false) + { + if (empty($placeholders) || !is_array($placeholders)) { + return $content; + } + + if (count($this->getView()->getDynamicContents()) === 0) { + // outermost cache: replace placeholder with dynamic content + foreach ($placeholders as $name => $statements) { + $placeholders[$name] = $this->getView()->evaluateDynamicContent($statements); + } + $content = strtr($content, $placeholders); + } + if ($isRestoredFromCache) { + foreach ($placeholders as $name => $statements) { + $this->getView()->addDynamicPlaceholder($name, $statements); + } + } + + return $content; + } +} diff --git a/framework/base/View.php b/framework/base/View.php index 093537d29c..65557189a9 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -26,7 +26,7 @@ use yii\widgets\FragmentCache; * @author Qiang Xue * @since 2.0 */ -class View extends Component +class View extends Component implements DynamicContentAwareInterface { /** * @event Event an event that is triggered by [[beginPage()]]. @@ -86,15 +86,19 @@ class View extends Component */ public $blocks; /** - * @var array a list of currently active fragment cache widgets. This property - * is used internally to implement the content caching feature. Do not modify it directly. + * @var array|DynamicContentAwareInterface[] a list of currently active dynamic content class instances. + * This property is used internally to implement the dynamic content caching feature. Do not modify it directly. * @internal + * @deprecated Sice 2.0.14. Do not use this property directly. Use methods [[getDynamicContents()]], + * [[pushDynamicContent()]], [[popDynamicContent()]] instead. */ public $cacheStack = []; /** * @var array a list of placeholders for embedding dynamic contents. This property * is used internally to implement the content caching feature. Do not modify it directly. * @internal + * @deprecated Since 2.0.14. Do not use this property directly. Use methods [[getDynamicPlaceholders()]], + * [[setDynamicPlaceholders()]], [[addDynamicPlaceholder()]] instead. */ public $dynamicPlaceholders = []; @@ -371,18 +375,36 @@ class View extends Component } /** - * Adds a placeholder for dynamic content. - * This method is internally used. - * @param string $placeholder the placeholder name - * @param string $statements the PHP statements for generating the dynamic content + * {@inheritdoc} + */ + public function getDynamicPlaceholders() + { + return $this->dynamicPlaceholders; + } + + /** + * {@inheritdoc} + */ + public function setDynamicPlaceholders($placeholders) + { + $this->dynamicPlaceholders = $placeholders; + } + + /** + * {@inheritdoc} */ public function addDynamicPlaceholder($placeholder, $statements) { foreach ($this->cacheStack as $cache) { - $cache->dynamicPlaceholders[$placeholder] = $statements; + if ($cache instanceof DynamicContentAwareInterface) { + $cache->addDynamicPlaceholder($placeholder, $statements); + } else { + // To be removed in 2.1 + $cache->dynamicPlaceholders[$placeholder] = $statements; + } } $this->dynamicPlaceholders[$placeholder] = $statements; - } +} /** * Evaluates the given PHP statements. @@ -395,6 +417,37 @@ class View extends Component return eval($statements); } + /** + * Returns a list of currently active dynamic content class instances. + * @return DynamicContentAwareInterface[] class instances supporting dynamic contents. + * @since 2.0.14 + */ + public function getDynamicContents() + { + return $this->cacheStack; + } + + /** + * Adds a class instance supporting dynamic contents to the end of a list of currently active + * dynamic content class instances. + * @param DynamicContentAwareInterface $instance class instance supporting dynamic contents. + * @since 2.0.14 + */ + public function pushDynamicContent(DynamicContentAwareInterface $instance) + { + $this->cacheStack[] = $instance; + } + + /** + * Removes a last class instance supporting dynamic contents from a list of currently active + * dynamic content class instances. + * @since 2.0.14 + */ + public function popDynamicContent() + { + array_pop($this->cacheStack); + } + /** * Begins recording a block. * diff --git a/framework/filters/PageCache.php b/framework/filters/PageCache.php index 63412ac6d2..7045efe24b 100644 --- a/framework/filters/PageCache.php +++ b/framework/filters/PageCache.php @@ -10,6 +10,8 @@ namespace yii\filters; use Yii; use yii\base\Action; use yii\base\ActionFilter; +use yii\base\DynamicContentAwareInterface; +use yii\base\DynamicContentAwareTrait; use yii\caching\CacheInterface; use yii\caching\Dependency; use yii\di\Instance; @@ -49,8 +51,16 @@ use yii\web\Response; * @author Sergey Makinen * @since 2.0 */ -class PageCache extends ActionFilter +class PageCache extends ActionFilter implements DynamicContentAwareInterface { + use DynamicContentAwareTrait; + + /** + * Page cache version, to detect incompatibilities in cached values when the + * data format of the cache changes. + */ + const PAGE_CACHE_VERSION = 1; + /** * @var bool whether the content being cached should be differentiated according to the route. * A route consists of the requested controller ID and action ID. Defaults to `true`. @@ -124,13 +134,6 @@ class PageCache extends ActionFilter * @since 2.0.4 */ public $cacheHeaders = true; - /** - * @var array a list of placeholders for embedding dynamic contents. This property - * is used internally to implement the content caching feature. Do not modify it. - * @internal - * @since 2.0.11 - */ - public $dynamicPlaceholders; /** @@ -164,8 +167,8 @@ class PageCache extends ActionFilter $response = Yii::$app->getResponse(); $data = $this->cache->get($this->calculateCacheKey()); - if (!is_array($data) || !isset($data['cacheVersion']) || $data['cacheVersion'] !== 1) { - $this->view->cacheStack[] = $this; + if (!is_array($data) || !isset($data['cacheVersion']) || $data['cacheVersion'] !== static::PAGE_CACHE_VERSION) { + $this->view->pushDynamicContent($this); ob_start(); ob_implicit_flush(false); $response->on(Response::EVENT_AFTER_SEND, [$this, 'cacheResponse']); @@ -217,13 +220,7 @@ class PageCache extends ActionFilter } } if (!empty($data['dynamicPlaceholders']) && is_array($data['dynamicPlaceholders'])) { - if (empty($this->view->cacheStack)) { - // outermost cache: replace placeholder with dynamic content - $response->content = $this->updateDynamicContent($response->content, $data['dynamicPlaceholders']); - } - foreach ($data['dynamicPlaceholders'] as $name => $statements) { - $this->view->addDynamicPlaceholder($name, $statements); - } + $response->content = $this->updateDynamicContent($response->content, $data['dynamicPlaceholders'], true); } $this->afterRestoreResponse(isset($data['cacheData']) ? $data['cacheData'] : null); } @@ -234,20 +231,16 @@ class PageCache extends ActionFilter */ public function cacheResponse() { - array_pop($this->view->cacheStack); + $this->view->popDynamicContent(); $beforeCacheResponseResult = $this->beforeCacheResponse(); if ($beforeCacheResponseResult === false) { - $content = ob_get_clean(); - if (empty($this->view->cacheStack) && !empty($this->dynamicPlaceholders)) { - $content = $this->updateDynamicContent($content, $this->dynamicPlaceholders); - } - echo $content; + echo $this->updateDynamicContent(ob_get_clean(), $this->getDynamicPlaceholders()); return; } $response = Yii::$app->getResponse(); $data = [ - 'cacheVersion' => 1, + 'cacheVersion' => static::PAGE_CACHE_VERSION, 'cacheData' => is_array($beforeCacheResponseResult) ? $beforeCacheResponseResult : null, 'content' => ob_get_clean(), ]; @@ -255,16 +248,14 @@ class PageCache extends ActionFilter return; } - $data['dynamicPlaceholders'] = $this->dynamicPlaceholders; + $data['dynamicPlaceholders'] = $this->getDynamicPlaceholders(); foreach (['format', 'version', 'statusCode', 'statusText'] as $name) { $data[$name] = $response->{$name}; } $this->insertResponseCollectionIntoData($response, 'headers', $data); $this->insertResponseCollectionIntoData($response, 'cookies', $data); $this->cache->set($this->calculateCacheKey(), $data, $this->duration, $this->dependency); - if (empty($this->view->cacheStack) && !empty($this->dynamicPlaceholders)) { - $data['content'] = $this->updateDynamicContent($data['content'], $this->dynamicPlaceholders); - } + $data['content'] = $this->updateDynamicContent($data['content'], $this->getDynamicPlaceholders()); echo $data['content']; } @@ -297,22 +288,6 @@ class PageCache extends ActionFilter $data[$collectionName] = $all; } - /** - * Replaces placeholders in content by results of evaluated dynamic statements. - * @param string $content content to be parsed. - * @param array $placeholders placeholders and their values. - * @return string final content. - * @since 2.0.11 - */ - protected function updateDynamicContent($content, $placeholders) - { - foreach ($placeholders as $name => $statements) { - $placeholders[$name] = $this->view->evaluateDynamicContent($statements); - } - - return strtr($content, $placeholders); - } - /** * @return array the key used to cache response properties. * @since 2.0.3 @@ -325,4 +300,12 @@ class PageCache extends ActionFilter } return array_merge($key, (array)$this->variations); } + + /** + * {@inheritdoc} + */ + protected function getView() + { + return $this->view; + } } diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php index 88848cce2c..d9bed1f1b9 100644 --- a/framework/widgets/FragmentCache.php +++ b/framework/widgets/FragmentCache.php @@ -8,6 +8,8 @@ namespace yii\widgets; use Yii; +use yii\base\DynamicContentAwareInterface; +use yii\base\DynamicContentAwareTrait; use yii\base\Widget; use yii\caching\CacheInterface; use yii\caching\Dependency; @@ -22,8 +24,10 @@ use yii\di\Instance; * @author Qiang Xue * @since 2.0 */ -class FragmentCache extends Widget +class FragmentCache extends Widget implements DynamicContentAwareInterface { + use DynamicContentAwareTrait; + /** * @var CacheInterface|array|string the cache object or the application component ID of the cache object. * After the FragmentCache object is created, if you want to change this property, @@ -70,11 +74,6 @@ class FragmentCache extends Widget * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). */ public $enabled = true; - /** - * @var array a list of placeholders for embedding dynamic contents. This property - * is used internally to implement the content caching feature. Do not modify it. - */ - public $dynamicPlaceholders; /** @@ -87,7 +86,7 @@ class FragmentCache extends Widget $this->cache = $this->enabled ? Instance::ensure($this->cache, 'yii\caching\CacheInterface') : null; if ($this->cache instanceof CacheInterface && $this->getCachedContent() === false) { - $this->getView()->cacheStack[] = $this; + $this->getView()->pushDynamicContent($this); ob_start(); ob_implicit_flush(false); } @@ -104,7 +103,7 @@ class FragmentCache extends Widget if (($content = $this->getCachedContent()) !== false) { echo $content; } elseif ($this->cache instanceof CacheInterface) { - array_pop($this->getView()->cacheStack); + $this->getView()->popDynamicContent(); $content = ob_get_clean(); if ($content === false || $content === '') { @@ -113,13 +112,9 @@ class FragmentCache extends Widget if (is_array($this->dependency)) { $this->dependency = Yii::createObject($this->dependency); } - $data = [$content, $this->dynamicPlaceholders]; + $data = [$content, $this->getDynamicPlaceholders()]; $this->cache->set($this->calculateKey(), $data, $this->duration, $this->dependency); - - if (empty($this->getView()->cacheStack) && !empty($this->dynamicPlaceholders)) { - $content = $this->updateDynamicContent($content, $this->dynamicPlaceholders); - } - echo $content; + echo $this->updateDynamicContent($content, $this->getDynamicPlaceholders()); } } @@ -155,33 +150,10 @@ class FragmentCache extends Widget return $this->_content; } - if (empty($this->getView()->cacheStack)) { - // outermost cache: replace placeholder with dynamic content - $this->_content = $this->updateDynamicContent($this->_content, $placeholders); - } - foreach ($placeholders as $name => $statements) { - $this->getView()->addDynamicPlaceholder($name, $statements); - } - + $this->_content = $this->updateDynamicContent($this->_content, $placeholders, true); return $this->_content; } - /** - * Replaces placeholders in content by results of evaluated dynamic statements. - * - * @param string $content - * @param array $placeholders - * @return string final content - */ - protected function updateDynamicContent($content, $placeholders) - { - foreach ($placeholders as $name => $statements) { - $placeholders[$name] = $this->getView()->evaluateDynamicContent($statements); - } - - return strtr($content, $placeholders); - } - /** * Generates a unique key used for storing the content in cache. * The key generated depends on both [[id]] and [[variations]].