View.php 14.6 KB
Newer Older
Qiang Xue committed
1 2 3
<?php
/**
 * @link http://www.yiiframework.com/
Qiang Xue committed
4
 * @copyright Copyright (c) 2008 Yii Software LLC
Qiang Xue committed
5 6 7 8 9
 * @license http://www.yiiframework.com/license/
 */

namespace yii\base;

Qiang Xue committed
10
use Yii;
Qiang Xue committed
11
use yii\helpers\FileHelper;
12 13 14
use yii\widgets\Block;
use yii\widgets\ContentDecorator;
use yii\widgets\FragmentCache;
Qiang Xue committed
15

Qiang Xue committed
16
/**
Qiang Xue committed
17
 * View represents a view object in the MVC pattern.
Qiang Xue committed
18
 *
Qiang Xue committed
19
 * View provides a set of methods (e.g. [[render()]]) for rendering purpose.
Qiang Xue committed
20
 *
Qiang Xue committed
21 22 23 24 25
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
class View extends Component
{
26
	/**
Qiang Xue committed
27
	 * @event Event an event that is triggered by [[beginPage()]].
28 29 30
	 */
	const EVENT_BEGIN_PAGE = 'beginPage';
	/**
Qiang Xue committed
31
	 * @event Event an event that is triggered by [[endPage()]].
32 33
	 */
	const EVENT_END_PAGE = 'endPage';
34
	/**
35
	 * @event ViewEvent an event that is triggered by [[renderFile()]] right before it renders a view file.
36 37 38
	 */
	const EVENT_BEFORE_RENDER = 'beforeRender';
	/**
39
	 * @event ViewEvent an event that is triggered by [[renderFile()]] right after it renders a view file.
40 41 42
	 */
	const EVENT_AFTER_RENDER = 'afterRender';

43
	/**
44
	 * @var ViewContextInterface the context under which the [[renderFile()]] method is being invoked.
45 46
	 */
	public $context;
Qiang Xue committed
47
	/**
Qiang Xue committed
48
	 * @var mixed custom parameters that are shared among view templates.
Qiang Xue committed
49
	 */
Alexander Makarov committed
50
	public $params = [];
Qiang Xue committed
51
	/**
Qiang Xue committed
52 53
	 * @var array a list of available renderers indexed by their corresponding supported file extensions.
	 * Each renderer may be a view renderer object or the configuration for creating the renderer object.
54 55 56
	 * For example, the following configuration enables both Smarty and Twig view renderers:
	 *
	 * ~~~
Alexander Makarov committed
57 58 59 60
	 * [
	 *     'tpl' => ['class' => 'yii\smarty\ViewRenderer'],
	 *     'twig' => ['class' => 'yii\twig\ViewRenderer'],
	 * ]
61
	 * ~~~
Qiang Xue committed
62 63 64
	 *
	 * If no renderer is available for the given view file, the view file will be treated as a normal PHP
	 * and rendered via [[renderPhpFile()]].
Qiang Xue committed
65
	 */
66
	public $renderers;
67
	/**
68
	 * @var Theme|array the theme object or the configuration array for creating the theme object.
Qiang Xue committed
69
	 * If not set, it means theming is not enabled.
70
	 */
Qiang Xue committed
71
	public $theme;
Qiang Xue committed
72
	/**
Qiang Xue committed
73 74
	 * @var array a list of named output blocks. The keys are the block names and the values
	 * are the corresponding block content. You can call [[beginBlock()]] and [[endBlock()]]
75
	 * to capture small fragments of a view. They can be later accessed somewhere else
Qiang Xue committed
76 77
	 * through this property.
	 */
Qiang Xue committed
78
	public $blocks;
Qiang Xue committed
79 80
	/**
	 * @var array a list of currently active fragment cache widgets. This property
81 82
	 * is used internally to implement the content caching feature. Do not modify it directly.
	 * @internal
Qiang Xue committed
83
	 */
Alexander Makarov committed
84
	public $cacheStack = [];
Qiang Xue committed
85 86
	/**
	 * @var array a list of placeholders for embedding dynamic contents. This property
87 88
	 * is used internally to implement the content caching feature. Do not modify it directly.
	 * @internal
Qiang Xue committed
89
	 */
Alexander Makarov committed
90
	public $dynamicPlaceholders = [];
Qiang Xue committed
91 92


Qiang Xue committed
93
	/**
Qiang Xue committed
94
	 * Initializes the view component.
Qiang Xue committed
95
	 */
Qiang Xue committed
96
	public function init()
Qiang Xue committed
97
	{
Qiang Xue committed
98 99
		parent::init();
		if (is_array($this->theme)) {
100 101 102
			if (!isset($this->theme['class'])) {
				$this->theme['class'] = 'yii\base\Theme';
			}
Qiang Xue committed
103
			$this->theme = Yii::createObject($this->theme);
Qiang Xue committed
104 105 106
		}
	}

Qiang Xue committed
107
	/**
Qiang Xue committed
108
	 * Renders a view.
Qiang Xue committed
109
	 *
110
	 * The view to be rendered can be specified in one of the following formats:
Qiang Xue committed
111
	 *
112 113 114 115 116 117
	 * - path alias (e.g. "@app/views/site/index");
	 * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes.
	 *   The actual view file will be looked for under the [[Application::viewPath|view path]] of the application.
	 * - absolute path within current module (e.g. "/site/index"): the view name starts with a single slash.
	 *   The actual view file will be looked for under the [[Module::viewPath|view path]] of [[module]].
	 * - resolving any other format will be performed via [[ViewContext::findViewFile()]].
Qiang Xue committed
118 119 120
	 *
	 * @param string $view the view name. Please refer to [[Controller::findViewFile()]]
	 * and [[Widget::findViewFile()]] on how to specify this parameter.
Qiang Xue committed
121
	 * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file.
122 123
	 * @param object $context the context that the view should use for rendering the view. If null,
	 * existing [[context]] will be used.
124
	 * @return string the rendering result
Qiang Xue committed
125 126
	 * @throws InvalidParamException if the view cannot be resolved or the view file does not exist.
	 * @see renderFile
Qiang Xue committed
127
	 */
128
	public function render($view, $params = [], $context = null)
Qiang Xue committed
129
	{
130 131 132 133 134 135 136 137 138 139 140
		$viewFile = $this->findViewFile($view, $context);
		return $this->renderFile($viewFile, $params, $context);
	}

	/**
	 * Finds the view file based on the given view name.
	 * @param string $view the view name or the path alias of the view file. Please refer to [[render()]]
	 * on how to specify this parameter.
	 * @param object $context the context that the view should be used to search the view file. If null,
	 * existing [[context]] will be used.
	 * @return string the view file path. Note that the file may not exist.
141
	 * @throws InvalidCallException if [[context]] is required and invalid.
142 143 144 145 146 147 148 149 150
	 */
	protected function findViewFile($view, $context = null)
	{
		if (strncmp($view, '@', 1) === 0) {
			// e.g. "@app/views/main"
			$file = Yii::getAlias($view);
		} elseif (strncmp($view, '//', 2) === 0) {
			// e.g. "//layouts/main"
			$file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/');
151
		} elseif (strncmp($view, '/', 1) === 0) {
152
			// e.g. "/site/index"
153 154 155 156 157
			if (Yii::$app->controller !== null) {
				$file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/');
			} else {
				throw new InvalidCallException("Unable to locate view file for view '$view': no active controller.");
			}
Qiang Xue committed
158
		} else {
159 160 161 162
			// context required
			if ($context === null) {
				$context = $this->context;
			}
163
			if ($context instanceof ViewContextInterface) {
164 165
				$file = $context->findViewFile($view);
			} else {
166
				throw new InvalidCallException("Unable to locate view file for view '$view': no active view context.");
167
			}
Qiang Xue committed
168
		}
169 170

		return pathinfo($file, PATHINFO_EXTENSION) === '' ? $file . '.php' : $file;
Qiang Xue committed
171 172
	}

Qiang Xue committed
173 174
	/**
	 * Renders a view file.
Qiang Xue committed
175
	 *
Qiang Xue committed
176 177
	 * If [[theme]] is enabled (not null), it will try to render the themed version of the view file as long
	 * as it is available.
Qiang Xue committed
178
	 *
Qiang Xue committed
179 180 181 182 183 184
	 * The method will call [[FileHelper::localize()]] to localize the view file.
	 *
	 * If [[renderer]] is enabled (not null), the method will use it to render the view file.
	 * Otherwise, it will simply include the view file as a normal PHP file, capture its output and
	 * return it as a string.
	 *
Qiang Xue committed
185
	 * @param string $viewFile the view file. This can be either a file path or a path alias.
Qiang Xue committed
186
	 * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file.
Qiang Xue committed
187 188
	 * @param object $context the context that the view should use for rendering the view. If null,
	 * existing [[context]] will be used.
Qiang Xue committed
189
	 * @return string the rendering result
Qiang Xue committed
190
	 * @throws InvalidParamException if the view file does not exist
Qiang Xue committed
191
	 */
Alexander Makarov committed
192
	public function renderFile($viewFile, $params = [], $context = null)
Qiang Xue committed
193
	{
Qiang Xue committed
194
		$viewFile = Yii::getAlias($viewFile);
195 196 197
		if ($this->theme !== null) {
			$viewFile = $this->theme->applyTo($viewFile);
		}
Qiang Xue committed
198 199 200 201 202 203
		if (is_file($viewFile)) {
			$viewFile = FileHelper::localize($viewFile);
		} else {
			throw new InvalidParamException("The view file does not exist: $viewFile");
		}

Qiang Xue committed
204
		$oldContext = $this->context;
Qiang Xue committed
205 206 207
		if ($context !== null) {
			$this->context = $context;
		}
Qiang Xue committed
208

209 210
		$output = '';
		if ($this->beforeRender($viewFile)) {
Qiang Xue committed
211
			Yii::trace("Rendering view file: $viewFile", __METHOD__);
Qiang Xue committed
212 213
			$ext = pathinfo($viewFile, PATHINFO_EXTENSION);
			if (isset($this->renderers[$ext])) {
Qiang Xue committed
214
				if (is_array($this->renderers[$ext]) || is_string($this->renderers[$ext])) {
Qiang Xue committed
215 216 217 218 219
					$this->renderers[$ext] = Yii::createObject($this->renderers[$ext]);
				}
				/** @var ViewRenderer $renderer */
				$renderer = $this->renderers[$ext];
				$output = $renderer->render($this, $viewFile, $params);
220 221 222 223
			} else {
				$output = $this->renderPhpFile($viewFile, $params);
			}
			$this->afterRender($viewFile, $output);
Qiang Xue committed
224
		}
Qiang Xue committed
225 226 227 228

		$this->context = $oldContext;

		return $output;
Qiang Xue committed
229 230
	}

231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
	/**
	 * This method is invoked right before [[renderFile()]] renders a view file.
	 * The default implementation will trigger the [[EVENT_BEFORE_RENDER]] event.
	 * If you override this method, make sure you call the parent implementation first.
	 * @param string $viewFile the view file to be rendered
	 * @return boolean whether to continue rendering the view file.
	 */
	public function beforeRender($viewFile)
	{
		$event = new ViewEvent($viewFile);
		$this->trigger(self::EVENT_BEFORE_RENDER, $event);
		return $event->isValid;
	}

	/**
	 * This method is invoked right after [[renderFile()]] renders a view file.
	 * The default implementation will trigger the [[EVENT_AFTER_RENDER]] event.
	 * If you override this method, make sure you call the parent implementation first.
	 * @param string $viewFile the view file to be rendered
	 * @param string $output the rendering result of the view file. Updates to this parameter
	 * will be passed back and returned by [[renderFile()]].
	 */
	public function afterRender($viewFile, &$output)
	{
		if ($this->hasEventHandlers(self::EVENT_AFTER_RENDER)) {
			$event = new ViewEvent($viewFile);
			$event->output = $output;
			$this->trigger(self::EVENT_AFTER_RENDER, $event);
			$output = $event->output;
		}
	}

Qiang Xue committed
263
	/**
Qiang Xue committed
264 265 266 267 268 269
	 * Renders a view file as a PHP script.
	 *
	 * This method treats the view file as a PHP script and includes the file.
	 * It extracts the given parameters and makes them available in the view file.
	 * The method captures the output of the included view file and returns it as a string.
	 *
Qiang Xue committed
270 271
	 * This method should mainly be called by view renderer or [[renderFile()]].
	 *
Qiang Xue committed
272 273 274
	 * @param string $_file_ the view file.
	 * @param array $_params_ the parameters (name-value pairs) that will be extracted and made available in the view file.
	 * @return string the rendering result
Qiang Xue committed
275
	 */
Alexander Makarov committed
276
	public function renderPhpFile($_file_, $_params_ = [])
Qiang Xue committed
277
	{
Qiang Xue committed
278 279 280 281 282
		ob_start();
		ob_implicit_flush(false);
		extract($_params_, EXTR_OVERWRITE);
		require($_file_);
		return ob_get_clean();
Qiang Xue committed
283 284
	}

Qiang Xue committed
285 286 287 288 289 290 291 292 293
	/**
	 * Renders dynamic content returned by the given PHP statements.
	 * This method is mainly used together with content caching (fragment caching and page caching)
	 * when some portions of the content (called *dynamic content*) should not be cached.
	 * The dynamic content must be returned by some PHP statements.
	 * @param string $statements the PHP statements for generating the dynamic content.
	 * @return string the placeholder of the dynamic content, or the dynamic content if there is no
	 * active content cache currently.
	 */
Qiang Xue committed
294 295
	public function renderDynamic($statements)
	{
Qiang Xue committed
296 297
		if (!empty($this->cacheStack)) {
			$n = count($this->dynamicPlaceholders);
Qiang Xue committed
298
			$placeholder = "<![CDATA[YII-DYNAMIC-$n]]>";
Qiang Xue committed
299
			$this->addDynamicPlaceholder($placeholder, $statements);
Qiang Xue committed
300 301 302 303 304 305
			return $placeholder;
		} else {
			return $this->evaluateDynamicContent($statements);
		}
	}

Qiang Xue committed
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
	/**
	 * 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
	 */
	public function addDynamicPlaceholder($placeholder, $statements)
	{
		foreach ($this->cacheStack as $cache) {
			$cache->dynamicPlaceholders[$placeholder] = $statements;
		}
		$this->dynamicPlaceholders[$placeholder] = $statements;
	}

	/**
	 * Evaluates the given PHP statements.
	 * This method is mainly used internally to implement dynamic content feature.
	 * @param string $statements the PHP statements to be evaluated.
	 * @return mixed the return value of the PHP statements.
	 */
Qiang Xue committed
326 327 328 329 330
	public function evaluateDynamicContent($statements)
	{
		return eval($statements);
	}

Qiang Xue committed
331
	/**
Qiang Xue committed
332
	 * Begins recording a block.
333
	 * This method is a shortcut to beginning [[Block]]
Qiang Xue committed
334 335 336
	 * @param string $id the block ID.
	 * @param boolean $renderInPlace whether to render the block content in place.
	 * Defaults to false, meaning the captured block will not be displayed.
337
	 * @return Block the Block widget instance
Qiang Xue committed
338
	 */
Qiang Xue committed
339
	public function beginBlock($id, $renderInPlace = false)
Qiang Xue committed
340
	{
Alexander Makarov committed
341
		return Block::begin([
Qiang Xue committed
342 343
			'id' => $id,
			'renderInPlace' => $renderInPlace,
344
			'view' => $this,
Alexander Makarov committed
345
		]);
Qiang Xue committed
346 347 348
	}

	/**
Qiang Xue committed
349
	 * Ends recording a block.
Qiang Xue committed
350
	 */
Qiang Xue committed
351
	public function endBlock()
Qiang Xue committed
352
	{
353
		Block::end();
Qiang Xue committed
354 355
	}

Qiang Xue committed
356 357
	/**
	 * Begins the rendering of content that is to be decorated by the specified view.
Qiang Xue committed
358
	 * This method can be used to implement nested layout. For example, a layout can be embedded
359
	 * in another layout file specified as '@app/view/layouts/base.php' like the following:
Qiang Xue committed
360 361
	 *
	 * ~~~
362
	 * <?php $this->beginContent('@app/view/layouts/base.php'); ?>
Qiang Xue committed
363 364 365 366 367 368
	 * ...layout content here...
	 * <?php $this->endContent(); ?>
	 * ~~~
	 *
	 * @param string $viewFile the view file that will be used to decorate the content enclosed by this widget.
	 * This can be specified as either the view file path or path alias.
resurtm committed
369
	 * @param array $params the variables (name => value) to be extracted and made available in the decorative view.
370 371
	 * @return ContentDecorator the ContentDecorator widget instance
	 * @see ContentDecorator
Qiang Xue committed
372
	 */
Alexander Makarov committed
373
	public function beginContent($viewFile, $params = [])
Qiang Xue committed
374
	{
Alexander Makarov committed
375
		return ContentDecorator::begin([
Qiang Xue committed
376
			'viewFile' => $viewFile,
Qiang Xue committed
377
			'params' => $params,
378
			'view' => $this,
Alexander Makarov committed
379
		]);
Qiang Xue committed
380 381 382 383 384 385 386
	}

	/**
	 * Ends the rendering of content.
	 */
	public function endContent()
	{
387
		ContentDecorator::end();
Qiang Xue committed
388 389
	}

390 391 392 393 394 395 396 397
	/**
	 * Begins fragment caching.
	 * This method will display cached content if it is available.
	 * If not, it will start caching and would expect an [[endCache()]]
	 * call to end the cache and save the content into cache.
	 * A typical usage of fragment caching is as follows,
	 *
	 * ~~~
resurtm committed
398
	 * if ($this->beginCache($id)) {
399 400 401 402 403 404
	 *     // ...generate content here
	 *     $this->endCache();
	 * }
	 * ~~~
	 *
	 * @param string $id a unique ID identifying the fragment to be cached.
405
	 * @param array $properties initial property values for [[FragmentCache]]
406 407 408
	 * @return boolean whether you should generate the content for caching.
	 * False if the cached version is available.
	 */
Alexander Makarov committed
409
	public function beginCache($id, $properties = [])
410 411
	{
		$properties['id'] = $id;
412
		$properties['view'] = $this;
413
		/** @var $cache FragmentCache */
414
		$cache = FragmentCache::begin($properties);
Qiang Xue committed
415
		if ($cache->getCachedContent() !== false) {
416 417 418 419 420 421 422 423 424 425 426 427
			$this->endCache();
			return false;
		} else {
			return true;
		}
	}

	/**
	 * Ends fragment caching.
	 */
	public function endCache()
	{
428
		FragmentCache::end();
429
	}
430 431

	/**
Alexander Makarov committed
432
	 * Marks the beginning of a page.
433 434 435 436 437
	 */
	public function beginPage()
	{
		ob_start();
		ob_implicit_flush(false);
438 439

		$this->trigger(self::EVENT_BEGIN_PAGE);
440 441 442
	}

	/**
Alexander Makarov committed
443
	 * Marks the ending of a page.
444 445 446
	 */
	public function endPage()
	{
447
		$this->trigger(self::EVENT_END_PAGE);
Alexander Makarov committed
448
		ob_end_flush();
449
	}
Zander Baldwin committed
450
}