<?php
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\View;
use BadMethodCallException;
use Cake\Cache\Cache;
use Cake\Datasource\ModelAwareTrait;
use Cake\Event\EventDispatcherTrait;
use Cake\Event\EventManager;
use Cake\Http\Response;
use Cake\Http\ServerRequest;
use Cake\ORM\Locator\LocatorAwareTrait;
use Cake\Utility\Inflector;
use Cake\View\Exception\MissingCellViewException;
use Cake\View\Exception\MissingTemplateException;
use Error;
use Exception;
use ReflectionException;
use ReflectionMethod;
/**
* Cell base.
*/
abstract class Cell
{
use EventDispatcherTrait;
use LocatorAwareTrait;
use ModelAwareTrait;
use ViewVarsTrait;
/**
* Instance of the View created during rendering. Won't be set until after
* Cell::__toString()/render() is called.
*
* @var \Cake\View\View
*/
protected $View;
/**
* An instance of a Cake\Http\ServerRequest object that contains information about the current request.
* This object contains all the information about a request and several methods for reading
* additional information about the request.
*
* @var \Cake\Http\ServerRequest
*/
protected $request;
/**
* An instance of a Response object that contains information about the impending response
*
* @var \Cake\Http\Response
*/
protected $response;
/**
* The cell's action to invoke.
*
* @var string
*/
protected $action;
/**
* Arguments to pass to cell's action.
*
* @var array
*/
protected $args = [];
/**
* These properties can be set directly on Cell and passed to the View as options.
*
* @var array
* @see \Cake\View\View
* @deprecated 3.7.0 Use ViewBuilder::setOptions() or any one of it's setter methods instead.
*/
protected $_validViewOptions = [
'viewPath',
];
/**
* List of valid options (constructor's fourth arguments)
* Override this property in subclasses to whitelist
* which options you want set as properties in your Cell.
*
* @var array
*/
protected $_validCellOptions = [];
/**
* Caching setup.
*
* @var array|bool
*/
protected $_cache = false;
/**
* Constructor.
*
* @param \Cake\Http\ServerRequest|null $request The request to use in the cell.
* @param \Cake\Http\Response|null $response The response to use in the cell.
* @param \Cake\Event\EventManager|null $eventManager The eventManager to bind events to.
* @param array $cellOptions Cell options to apply.
*/
public function __construct(
ServerRequest $request = null,
Response $response = null,
EventManager $eventManager = null,
array $cellOptions = []
) {
if ($eventManager !== null) {
$this->setEventManager($eventManager);
}
$this->request = $request;
$this->response = $response;
$this->modelFactory('Table', [$this->getTableLocator(), 'get']);
$this->_validCellOptions = array_merge(['action', 'args'], $this->_validCellOptions);
foreach ($this->_validCellOptions as $var) {
if (isset($cellOptions[$var])) {
$this->{$var} = $cellOptions[$var];
}
}
if (!empty($cellOptions['cache'])) {
$this->_cache = $cellOptions['cache'];
}
$this->initialize();
}
/**
* Initialization hook method.
*
* Implement this method to avoid having to overwrite
* the constructor and calling parent::__construct().
*
* @return void
*/
public function initialize()
{
}
/**
* Render the cell.
*
* @param string|null $template Custom template name to render. If not provided (null), the last
* value will be used. This value is automatically set by `CellTrait::cell()`.
* @return string The rendered cell.
* @throws \Cake\View\Exception\MissingCellViewException When a MissingTemplateException is raised during rendering.
*/
public function render($template = null)
{
$cache = [];
if ($this->_cache) {
$cache = $this->_cacheConfig($this->action, $template);
}
$render = function () use ($template) {
try {
$reflect = new ReflectionMethod($this, $this->action);
$reflect->invokeArgs($this, $this->args);
} catch (ReflectionException $e) {
throw new BadMethodCallException(sprintf(
'Class %s does not have a "%s" method.',
get_class($this),
$this->action
));
}
$builder = $this->viewBuilder()->setLayout(false);
if ($template !== null) {
$builder->setTemplate($template);
}
$className = get_class($this);
$namePrefix = '\View\Cell\\';
$name = substr($className, strpos($className, $namePrefix) + strlen($namePrefix));
$name = substr($name, 0, -4);
if (!$builder->getTemplatePath()) {
$builder->setTemplatePath('Cell' . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $name));
}
$template = $builder->getTemplate();
$this->View = $this->createView();
try {
return $this->View->render($template);
} catch (MissingTemplateException $e) {
$attributes = $e->getAttributes();
$attributes = ['file' => basename($attributes['file']), 'name' => $name];
throw new MissingCellViewException($attributes, null, $e);
}
};
if ($cache) {
return Cache::remember($cache['key'], $render, $cache['config']);
}
return $render();
}
/**
* Generate the cache key to use for this cell.
*
* If the key is undefined, the cell class and action name will be used.
*
* @param string $action The action invoked.
* @param string|null $template The name of the template to be rendered.
* @return array The cache configuration.
*/
protected function _cacheConfig($action, $template = null)
{
if (empty($this->_cache)) {
return [];
}
$template = $template ?: 'default';
$key = 'cell_' . Inflector::underscore(get_class($this)) . '_' . $action . '_' . $template;
$key = str_replace('\\', '_', $key);
$default = [
'config' => 'default',
'key' => $key,
];
if ($this->_cache === true) {
return $default;
}
return $this->_cache + $default;
}
/**
* Magic method.
*
* Starts the rendering process when Cell is echoed.
*
* *Note* This method will trigger an error when view rendering has a problem.
* This is because PHP will not allow a __toString() method to throw an exception.
*
* @return string Rendered cell
* @throws \Error Include error details for PHP 7 fatal errors.
*/
public function __toString()
{
try {
return $this->render();
} catch (Exception $e) {
trigger_error(sprintf('Could not render cell - %s [%s, line %d]', $e->getMessage(), $e->getFile(), $e->getLine()), E_USER_WARNING);
return '';
} catch (Error $e) {
throw new Error(sprintf('Could not render cell - %s [%s, line %d]', $e->getMessage(), $e->getFile(), $e->getLine()));
}
}
/**
* Magic accessor for removed properties.
*
* @param string $name Property name
* @return mixed
*/
public function __get($name)
{
$deprecated = [
'template' => 'getTemplate',
'plugin' => 'getPlugin',
'helpers' => 'getHelpers',
];
if (isset($deprecated[$name])) {
$method = $deprecated[$name];
deprecationWarning(sprintf(
'Cell::$%s is deprecated. Use $cell->viewBuilder()->%s() instead.',
$name,
$method
));
return $this->viewBuilder()->{$method}();
}
$protected = [
'action',
'args',
'request',
'response',
'View',
];
if (in_array($name, $protected, true)) {
deprecationWarning(sprintf(
'Cell::$%s is now protected and shouldn\'t be accessed from outside a child class.',
$name
));
}
return $this->{$name};
}
/**
* Magic setter for removed properties.
*
* @param string $name Property name.
* @param mixed $value Value to set.
* @return void
*/
public function __set($name, $value)
{
$deprecated = [
'template' => 'setTemplate',
'plugin' => 'setPlugin',
'helpers' => 'setHelpers',
];
if (isset($deprecated[$name])) {
$method = $deprecated[$name];
deprecationWarning(sprintf(
'Cell::$%s is deprecated. Use $cell->viewBuilder()->%s() instead.',
$name,
$method
));
$this->viewBuilder()->{$method}($value);
return;
}
$protected = [
'action',
'args',
'request',
'response',
'View',
];
if (in_array($name, $protected, true)) {
deprecationWarning(sprintf(
'Cell::$%s is now protected and shouldn\'t be accessed from outside a child class.',
$name
));
}
$this->{$name} = $value;
}
/**
* Debug info.
*
* @return array
*/
public function __debugInfo()
{
return [
'action' => $this->action,
'args' => $this->args,
'request' => $this->request,
'response' => $this->response,
'viewBuilder' => $this->viewBuilder(),
];
}
}