1434 lines
34 KiB
PHP
1434 lines
34 KiB
PHP
<?php
|
||
/**
|
||
* @package FOF
|
||
* @copyright Copyright (c)2010-2021 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||
* @license GNU General Public License version 2, or later
|
||
*/
|
||
|
||
namespace FOF30\View;
|
||
|
||
defined('_JEXEC') || die;
|
||
|
||
use ErrorException;
|
||
use Exception;
|
||
use FOF30\Container\Container;
|
||
use FOF30\Input\Input;
|
||
use FOF30\Model\Model;
|
||
use FOF30\View\Engine\EngineInterface;
|
||
use FOF30\View\Engine\PhpEngine;
|
||
use FOF30\View\Exception\CannotGetName;
|
||
use FOF30\View\Exception\EmptyStack;
|
||
use FOF30\View\Exception\ModelNotFound;
|
||
use FOF30\View\Exception\UnrecognisedExtension;
|
||
use Joomla\CMS\Language\Text;
|
||
|
||
/**
|
||
* Class View
|
||
*
|
||
* A generic MVC view implementation
|
||
*
|
||
* @property-read Input $input The input object (magic __get returns the Input from the Container)
|
||
*/
|
||
class View
|
||
{
|
||
public $baseurl = null;
|
||
/**
|
||
* Current or most recently performed task.
|
||
* Currently public, it should be reduced to protected in the future
|
||
*
|
||
* @var string
|
||
*/
|
||
public $task;
|
||
/**
|
||
* The mapped task that was performed.
|
||
* Currently public, it should be reduced to protected in the future
|
||
*
|
||
* @var string
|
||
*/
|
||
public $doTask;
|
||
/**
|
||
* The name of the view
|
||
*
|
||
* @var array
|
||
*/
|
||
protected $name = null;
|
||
/**
|
||
* Registered models
|
||
*
|
||
* @var array
|
||
*/
|
||
protected $modelInstances = [];
|
||
/**
|
||
* The default model
|
||
*
|
||
* @var string
|
||
*/
|
||
protected $defaultModel = null;
|
||
/**
|
||
* Layout name
|
||
*
|
||
* @var string
|
||
*/
|
||
protected $layout = 'default';
|
||
/**
|
||
* Layout template
|
||
*
|
||
* @var string
|
||
*/
|
||
protected $layoutTemplate = '_';
|
||
/**
|
||
* The set of search directories for view templates
|
||
*
|
||
* @var array
|
||
*/
|
||
protected $templatePaths = [];
|
||
/**
|
||
* The name of the default template source file.
|
||
*
|
||
* @var string
|
||
*/
|
||
protected $template = null;
|
||
/**
|
||
* The output of the template script.
|
||
*
|
||
* @var string
|
||
*/
|
||
protected $output = null;
|
||
/**
|
||
* A cached copy of the configuration
|
||
*
|
||
* @var array
|
||
*/
|
||
protected $config = [];
|
||
/**
|
||
* The container attached to this view
|
||
*
|
||
* @var Container
|
||
*/
|
||
protected $container;
|
||
/**
|
||
* The object used to locate view templates in the filesystem
|
||
*
|
||
* @var ViewTemplateFinder
|
||
*/
|
||
protected $viewFinder = null;
|
||
/**
|
||
* Used when loading template files to avoid variable scope issues
|
||
*
|
||
* @var null
|
||
*/
|
||
protected $_tempFilePath = null;
|
||
/**
|
||
* Should I run the pre-render step?
|
||
*
|
||
* @var boolean
|
||
*/
|
||
protected $doPreRender = true;
|
||
|
||
/**
|
||
* Should I run the post-render step?
|
||
*
|
||
* @var boolean
|
||
*/
|
||
protected $doPostRender = true;
|
||
|
||
/**
|
||
* Maps view template extensions to view engine classes
|
||
*
|
||
* @var array
|
||
*/
|
||
protected $viewEngineMap = [
|
||
'.blade.php' => 'FOF30\\View\\Engine\\BladeEngine',
|
||
'.php' => 'FOF30\\View\\Engine\\PhpEngine',
|
||
];
|
||
|
||
/**
|
||
* All of the finished, captured sections.
|
||
*
|
||
* @var array
|
||
*/
|
||
protected $sections = [];
|
||
|
||
/**
|
||
* The stack of in-progress sections.
|
||
*
|
||
* @var array
|
||
*/
|
||
protected $sectionStack = [];
|
||
|
||
/**
|
||
* The number of active rendering operations.
|
||
*
|
||
* @var int
|
||
*/
|
||
protected $renderCount = 0;
|
||
|
||
/**
|
||
* Aliases of view templates. For example:
|
||
*
|
||
* array('userProfile' => 'site://com_foobar/users/profile')
|
||
*
|
||
* allows you to do something like $this->loadAnyTemplate('userProfile') to display the frontend view template
|
||
* site://com_foobar/users/profile. You can also alias one view template with another, e.g.
|
||
* 'site://com_something/users/profile' => 'admin://com_foobar/clients/record'
|
||
*
|
||
* @var array
|
||
*/
|
||
protected $viewTemplateAliases = [];
|
||
|
||
/**
|
||
* Constructor.
|
||
*
|
||
* The $config array can contain the following overrides:
|
||
* name string The name of the view (defaults to the view class name)
|
||
* template_path string The path of the layout directory
|
||
* layout string The layout for displaying the view
|
||
* viewFinder ViewTemplateFinder The object used to locate view templates in the filesystem
|
||
* viewEngineMap array Maps view template extensions to view engine classes
|
||
*
|
||
* @param Container $container The container we belong to
|
||
* @param array $config The configuration overrides for the view
|
||
*
|
||
* @return View
|
||
*/
|
||
public function __construct(Container $container, array $config = [])
|
||
{
|
||
$this->container = $container;
|
||
|
||
$this->config = $config;
|
||
|
||
// Get the view name
|
||
if (isset($this->config['name']))
|
||
{
|
||
$this->name = $this->config['name'];
|
||
}
|
||
|
||
$this->name = $this->getName();
|
||
|
||
// Set the default template search path
|
||
if (array_key_exists('template_path', $this->config))
|
||
{
|
||
// User-defined dirs
|
||
$this->setTemplatePath($this->config['template_path']);
|
||
}
|
||
else
|
||
{
|
||
$this->setTemplatePath($this->container->thisPath . '/View/' . ucfirst($this->name) . '/tmpl');
|
||
}
|
||
|
||
// Set the layout
|
||
if (array_key_exists('layout', $this->config))
|
||
{
|
||
$this->setLayout($this->config['layout']);
|
||
}
|
||
|
||
// Apply the viewEngineMap
|
||
if (isset($config['viewEngineMap']))
|
||
{
|
||
// If the overrides are a string convert it to an array first.
|
||
if (!is_array($config['viewEngineMap']))
|
||
{
|
||
$temp = explode(',', $config['viewEngineMap']);
|
||
$config['viewEngineMap'] = [];
|
||
|
||
foreach ($temp as $assignment)
|
||
{
|
||
$parts = explode('=>', $assignment, 2);
|
||
|
||
if (count($parts) != 2)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
$parts = array_map(function ($x) {
|
||
return trim($x);
|
||
}, $parts);
|
||
|
||
$config['viewEngineMap'][$parts[0]] = $parts[1];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* We want to always have a sane fallback to plain .php template files at the end of the view engine stack.
|
||
* For this to happen to we need to remove it from the current stack, disallow overriding it in the
|
||
* viewEngineMap overrides, merge the overrides and then add the fallback at the very end of the stack.
|
||
*
|
||
* In previous versions we didn't do that which had two side effects:
|
||
* 1. It was no longer a fallback, it was in the middle of the stack
|
||
* 2. Any new template engines using a .something.php extension wouldn't work, see
|
||
* https://github.com/akeeba/fof/issues/694
|
||
*/
|
||
// Do not allow overriding the fallback .php handler
|
||
if (isset($config['viewEngineMap']['.php']))
|
||
{
|
||
unset ($config['viewEngineMap']['.php']);
|
||
}
|
||
|
||
// Temporarily remove the fallback .php handler
|
||
$phpHandler = $this->viewEngineMap['.php'] ?? PhpEngine::class;
|
||
unset ($this->viewEngineMap['.php']);
|
||
|
||
// Add the overrides
|
||
$this->viewEngineMap = array_merge($this->viewEngineMap, $config['viewEngineMap']);
|
||
|
||
// Push the fallback .php handler to the end of the view engine map stack
|
||
$this->viewEngineMap = array_merge($this->viewEngineMap, [
|
||
'.php' => $phpHandler
|
||
]);
|
||
}
|
||
|
||
// Set the ViewFinder
|
||
$this->viewFinder = $this->container->factory->viewFinder($this);
|
||
|
||
if (isset($config['viewFinder']) && !empty($config['viewFinder']) && is_object($config['viewFinder']) && ($config['viewFinder'] instanceof ViewTemplateFinder))
|
||
{
|
||
$this->viewFinder = $config['viewFinder'];
|
||
}
|
||
|
||
// Apply the registered view template extensions to the view finder
|
||
$this->viewFinder->setExtensions(array_keys($this->viewEngineMap));
|
||
|
||
// Apply the base URL
|
||
$this->baseurl = $this->container->platform->URIbase();
|
||
}
|
||
|
||
/**
|
||
* Magic get method. Handles magic properties:
|
||
* $this->input mapped to $this->container->input
|
||
*
|
||
* @param string $name The property to fetch
|
||
*
|
||
* @return mixed|null
|
||
*/
|
||
public function __get($name)
|
||
{
|
||
// Handle $this->input
|
||
if ($name == 'input')
|
||
{
|
||
return $this->container->input;
|
||
}
|
||
|
||
// Property not found; raise error
|
||
$trace = debug_backtrace();
|
||
trigger_error(
|
||
'Undefined property via __get(): ' . $name .
|
||
' in ' . $trace[0]['file'] .
|
||
' on line ' . $trace[0]['line'],
|
||
E_USER_NOTICE);
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Method to get the view name
|
||
*
|
||
* The model name by default parsed using the classname, or it can be set
|
||
* by passing a $config['name'] in the class constructor
|
||
*
|
||
* @return string The name of the model
|
||
*
|
||
* @throws Exception
|
||
*/
|
||
public function getName()
|
||
{
|
||
if (empty($this->name))
|
||
{
|
||
$r = null;
|
||
|
||
if (!preg_match('/(.*)\\\\View\\\\(.*)\\\\(.*)/i', get_class($this), $r))
|
||
{
|
||
throw new CannotGetName;
|
||
}
|
||
|
||
$this->name = $r[2];
|
||
}
|
||
|
||
return $this->name;
|
||
}
|
||
|
||
/**
|
||
* Escapes a value for output in a view script.
|
||
*
|
||
* @param mixed $var The output to escape.
|
||
*
|
||
* @return mixed The escaped value.
|
||
*/
|
||
public function escape($var)
|
||
{
|
||
return htmlspecialchars($var, ENT_COMPAT, 'UTF-8');
|
||
}
|
||
|
||
/**
|
||
* Method to get data from a registered model or a property of the view
|
||
*
|
||
* @param string $property The name of the method to call on the Model or the property to get
|
||
* @param string $default The default value [optional]
|
||
* @param string $modelName The name of the Model to reference [optional]
|
||
*
|
||
* @return mixed The return value of the method
|
||
*/
|
||
public function get($property, $default = null, $modelName = null)
|
||
{
|
||
// If $model is null we use the default model
|
||
if (is_null($modelName))
|
||
{
|
||
$model = $this->defaultModel;
|
||
}
|
||
else
|
||
{
|
||
$model = $modelName;
|
||
}
|
||
|
||
// First check to make sure the model requested exists
|
||
if (isset($this->modelInstances[$model]))
|
||
{
|
||
// Model exists, let's build the method name
|
||
$method = 'get' . ucfirst($property);
|
||
|
||
// Does the method exist?
|
||
if (method_exists($this->modelInstances[$model], $method))
|
||
{
|
||
// The method exists, let's call it and return what we get
|
||
$result = $this->modelInstances[$model]->$method();
|
||
|
||
return $result;
|
||
}
|
||
else
|
||
{
|
||
$result = $this->modelInstances[$model]->$property();
|
||
|
||
if (is_null($result))
|
||
{
|
||
return $default;
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
}
|
||
// If the model doesn't exist, try to fetch a View property
|
||
else
|
||
{
|
||
if (@isset($this->$property))
|
||
{
|
||
return $this->$property;
|
||
}
|
||
else
|
||
{
|
||
return $default;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Returns a named Model object
|
||
*
|
||
* @param string $name The Model name. If null we'll use the modelName
|
||
* variable or, if it's empty, the same name as
|
||
* the Controller
|
||
*
|
||
* @return Model The instance of the Model known to this Controller
|
||
*/
|
||
public function getModel($name = null)
|
||
{
|
||
if (!empty($name))
|
||
{
|
||
$modelName = $name;
|
||
}
|
||
elseif (!empty($this->defaultModel))
|
||
{
|
||
$modelName = $this->defaultModel;
|
||
}
|
||
else
|
||
{
|
||
$modelName = $this->name;
|
||
}
|
||
|
||
if (!array_key_exists($modelName, $this->modelInstances))
|
||
{
|
||
throw new ModelNotFound($modelName, $this->name);
|
||
}
|
||
|
||
return $this->modelInstances[$modelName];
|
||
}
|
||
|
||
/**
|
||
* Pushes the default Model to the View
|
||
*
|
||
* @param Model $model The model to push
|
||
*/
|
||
public function setDefaultModel(Model &$model)
|
||
{
|
||
$name = $model->getName();
|
||
|
||
$this->setDefaultModelName($name);
|
||
$this->setModel($this->defaultModel, $model);
|
||
}
|
||
|
||
/**
|
||
* Set the name of the Model to be used by this View
|
||
*
|
||
* @param string $modelName The name of the Model
|
||
*
|
||
* @return void
|
||
*/
|
||
public function setDefaultModelName($modelName)
|
||
{
|
||
$this->defaultModel = $modelName;
|
||
}
|
||
|
||
/**
|
||
* Pushes a named model to the View
|
||
*
|
||
* @param string $modelName The name of the Model
|
||
* @param Model $model The actual Model object to push
|
||
*
|
||
* @return void
|
||
*/
|
||
public function setModel($modelName, Model &$model)
|
||
{
|
||
$this->modelInstances[$modelName] = $model;
|
||
}
|
||
|
||
/**
|
||
* Overrides the default method to execute and display a template script.
|
||
* Instead of loadTemplate is uses loadAnyTemplate.
|
||
*
|
||
* @param string $tpl The name of the template file to parse
|
||
*
|
||
* @return boolean True on success
|
||
*
|
||
* @throws Exception When the layout file is not found
|
||
*/
|
||
public function display($tpl = null)
|
||
{
|
||
$eventName = 'onBefore' . ucfirst($this->doTask);
|
||
$this->triggerEvent($eventName, [$tpl]);
|
||
|
||
$preRenderResult = '';
|
||
|
||
if ($this->doPreRender)
|
||
{
|
||
@ob_start();
|
||
$this->preRender();
|
||
$preRenderResult = @ob_get_contents();
|
||
@ob_end_clean();
|
||
}
|
||
|
||
$templateResult = $this->loadTemplate($tpl);
|
||
|
||
$eventName = 'onAfter' . ucfirst($this->doTask);
|
||
$this->triggerEvent($eventName, [$tpl]);
|
||
|
||
if (is_object($templateResult) && ($templateResult instanceof Exception))
|
||
{
|
||
throw $templateResult;
|
||
}
|
||
|
||
echo $preRenderResult . $templateResult;
|
||
|
||
if ($this->doPostRender)
|
||
{
|
||
$this->postRender();
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Get the layout.
|
||
*
|
||
* @return string The layout name
|
||
*/
|
||
public function getLayout()
|
||
{
|
||
return $this->layout;
|
||
}
|
||
|
||
/**
|
||
* Sets the layout name to use
|
||
*
|
||
* @param string $layout The layout name or a string in format <template>:<layout file>
|
||
*
|
||
* @return string Previous value.
|
||
*/
|
||
public function setLayout($layout)
|
||
{
|
||
$previous = $this->layout;
|
||
|
||
if (is_null($layout))
|
||
{
|
||
$layout = 'default';
|
||
}
|
||
|
||
if (strpos($layout, ':') === false)
|
||
{
|
||
$this->layout = $layout;
|
||
}
|
||
else
|
||
{
|
||
// Convert parameter to array based on :
|
||
$temp = explode(':', $layout);
|
||
$this->layout = $temp[1];
|
||
|
||
// Set layout template
|
||
$this->layoutTemplate = $temp[0];
|
||
}
|
||
|
||
return $previous;
|
||
}
|
||
|
||
/**
|
||
* Our function uses loadAnyTemplate to provide smarter view template loading.
|
||
*
|
||
* @param string $tpl The name of the template file to parse
|
||
* @param boolean $strict Should we use strict naming, i.e. force a non-empty $tpl?
|
||
*
|
||
* @return mixed A string if successful, otherwise an Exception
|
||
*/
|
||
public function loadTemplate($tpl = null, $strict = false)
|
||
{
|
||
$result = '';
|
||
|
||
$uris = $this->viewFinder->getViewTemplateUris([
|
||
'component' => $this->container->componentName,
|
||
'view' => $this->getName(),
|
||
'layout' => $this->getLayout(),
|
||
'tpl' => $tpl,
|
||
'strictTpl' => $strict,
|
||
]);
|
||
|
||
foreach ($uris as $uri)
|
||
{
|
||
try
|
||
{
|
||
$result = $this->loadAnyTemplate($uri);
|
||
|
||
break;
|
||
}
|
||
catch (Exception $e)
|
||
{
|
||
$result = $e;
|
||
}
|
||
}
|
||
|
||
if ($result instanceof Exception)
|
||
{
|
||
$this->container->platform->raiseError($result->getCode(), $result->getMessage());
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Loads a template given any path. The path is in the format componentPart://componentName/viewName/layoutName,
|
||
* for example
|
||
* site:com_example/items/default
|
||
* admin:com_example/items/default_subtemplate
|
||
* auto:com_example/things/chair
|
||
* any:com_example/invoices/printpreview
|
||
*
|
||
* @param string $uri The template path
|
||
* @param array $forceParams A hash array of variables to be extracted in the local scope of the template
|
||
* file
|
||
* @param callable $callback A method to post-process the 3ναluα+3d view template (I use leetspeak here
|
||
* because of bad quality hosts with broken scanners)
|
||
* @param bool $noOverride If true we will not load Joomla! template overrides
|
||
*
|
||
* @return string The output of the template
|
||
*
|
||
* @throws Exception When the layout file is not found
|
||
*/
|
||
public function loadAnyTemplate($uri = '', $forceParams = [], $callback = null, $noOverride = false)
|
||
{
|
||
if (isset($this->viewTemplateAliases[$uri]))
|
||
{
|
||
$uri = $this->viewTemplateAliases[$uri];
|
||
}
|
||
|
||
$layoutTemplate = $this->getLayoutTemplate();
|
||
|
||
$extraPaths = [];
|
||
|
||
if (!empty($this->templatePaths))
|
||
{
|
||
$extraPaths = $this->templatePaths;
|
||
}
|
||
|
||
// First get the raw view template path
|
||
$path = $this->viewFinder->resolveUriToPath($uri, $layoutTemplate, $extraPaths, $noOverride);
|
||
|
||
// Now get the parsed view template path
|
||
$this->_tempFilePath = $this->getEngine($path)->get($path, $forceParams);
|
||
|
||
// We will keep track of the amount of views being rendered so we can flush
|
||
// the section after the complete rendering operation is done. This will
|
||
// clear out the sections for any separate views that may be rendered.
|
||
$this->incrementRender();
|
||
|
||
// Get the processed template
|
||
$contents = $this->processTemplate($forceParams);
|
||
|
||
// Once we've finished rendering the view, we'll decrement the render count
|
||
// so that each sections get flushed out next time a view is created and
|
||
// no old sections are staying around in the memory of an environment.
|
||
$this->decrementRender();
|
||
|
||
$response = isset($callback) ? $callback($this, $contents) : null;
|
||
|
||
if (!is_null($response))
|
||
{
|
||
$contents = $response;
|
||
}
|
||
|
||
// Once we have the contents of the view, we will flush the sections if we are
|
||
// done rendering all views so that there is nothing left hanging over when
|
||
// another view gets rendered in the future by the application developer.
|
||
$this->flushSectionsIfDoneRendering();
|
||
|
||
return $contents;
|
||
}
|
||
|
||
/**
|
||
* Increment the rendering counter.
|
||
*
|
||
* @return void
|
||
*/
|
||
public function incrementRender()
|
||
{
|
||
$this->renderCount++;
|
||
}
|
||
|
||
/**
|
||
* Decrement the rendering counter.
|
||
*
|
||
* @return void
|
||
*/
|
||
public function decrementRender()
|
||
{
|
||
$this->renderCount--;
|
||
}
|
||
|
||
/**
|
||
* Check if there are no active render operations.
|
||
*
|
||
* @return bool
|
||
*/
|
||
public function doneRendering()
|
||
{
|
||
return $this->renderCount == 0;
|
||
}
|
||
|
||
/**
|
||
* Go through a data array and render a subtemplate against each record (think master-detail views). This is
|
||
* accessible through Blade templates as @each
|
||
*
|
||
* @param string $viewTemplate The view template to use for each subitem, format
|
||
* componentPart://componentName/viewName/layoutName
|
||
* @param array $data The array of data you want to render. It can be a DataModel\Collection, array,
|
||
* ...
|
||
* @param string $eachItemName How to call each item in the loaded subtemplate (passed through $forceParams)
|
||
* @param string $empty What to display if the array is empty
|
||
*
|
||
* @return string
|
||
*/
|
||
public function renderEach($viewTemplate, $data, $eachItemName, $empty = 'raw|')
|
||
{
|
||
$result = '';
|
||
|
||
// If is actually data in the array, we will loop through the data and append
|
||
// an instance of the partial view to the final result HTML passing in the
|
||
// iterated value of this data array, allowing the views to access them.
|
||
if (count($data) > 0)
|
||
{
|
||
foreach ($data as $key => $value)
|
||
{
|
||
$data = ['key' => $key, $eachItemName => $value];
|
||
|
||
$result .= $this->loadAnyTemplate($viewTemplate, $data);
|
||
}
|
||
}
|
||
// If there is no data in the array, we will render the contents of the empty
|
||
// view. Alternatively, the "empty view" could be a raw string that begins
|
||
// with "raw|" for convenience and to let this know that it is a string. Or
|
||
// a language string starting with text|.
|
||
else
|
||
{
|
||
if (starts_with($empty, 'raw|'))
|
||
{
|
||
$result = substr($empty, 4);
|
||
}
|
||
elseif (starts_with($empty, 'text|'))
|
||
{
|
||
$result = Text::_(substr($empty, 5));
|
||
}
|
||
else
|
||
{
|
||
$result = $this->loadAnyTemplate($empty);
|
||
}
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Start injecting content into a section.
|
||
*
|
||
* @param string $section
|
||
* @param string $content
|
||
*
|
||
* @return void
|
||
*/
|
||
public function startSection($section, $content = '')
|
||
{
|
||
if ($content === '')
|
||
{
|
||
if (ob_start())
|
||
{
|
||
$this->sectionStack[] = $section;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
$this->extendSection($section, $content);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Stop injecting content into a section and return its contents.
|
||
*
|
||
* @return string
|
||
*/
|
||
public function yieldSection()
|
||
{
|
||
return $this->yieldContent($this->stopSection());
|
||
}
|
||
|
||
/**
|
||
* Stop injecting content into a section.
|
||
*
|
||
* @param bool $overwrite
|
||
*
|
||
* @return string
|
||
*/
|
||
public function stopSection($overwrite = false)
|
||
{
|
||
if (empty($this->sectionStack))
|
||
{
|
||
// Let's close the output buffering
|
||
ob_get_clean();
|
||
|
||
throw new EmptyStack();
|
||
}
|
||
|
||
$last = array_pop($this->sectionStack);
|
||
|
||
if ($overwrite)
|
||
{
|
||
$this->sections[$last] = ob_get_clean();
|
||
}
|
||
else
|
||
{
|
||
$this->extendSection($last, ob_get_clean());
|
||
}
|
||
|
||
return $last;
|
||
}
|
||
|
||
/**
|
||
* Stop injecting content into a section and append it.
|
||
*
|
||
* @return string
|
||
*/
|
||
public function appendSection()
|
||
{
|
||
if (empty($this->sectionStack))
|
||
{
|
||
// Let's close the output buffering
|
||
ob_get_clean();
|
||
|
||
throw new EmptyStack();
|
||
}
|
||
|
||
$last = array_pop($this->sectionStack);
|
||
|
||
if (isset($this->sections[$last]))
|
||
{
|
||
$this->sections[$last] .= ob_get_clean();
|
||
}
|
||
else
|
||
{
|
||
$this->sections[$last] = ob_get_clean();
|
||
}
|
||
|
||
return $last;
|
||
}
|
||
|
||
/**
|
||
* Get the string contents of a section.
|
||
*
|
||
* @param string $section
|
||
* @param string $default
|
||
*
|
||
* @return string
|
||
*/
|
||
public function yieldContent($section, $default = '')
|
||
{
|
||
$sectionContent = $default;
|
||
|
||
if (isset($this->sections[$section]))
|
||
{
|
||
$sectionContent = $this->sections[$section];
|
||
}
|
||
|
||
return str_replace('@parent', '', $sectionContent);
|
||
}
|
||
|
||
/**
|
||
* Flush all of the section contents.
|
||
*
|
||
* @return void
|
||
*/
|
||
public function flushSections()
|
||
{
|
||
$this->sections = [];
|
||
|
||
$this->sectionStack = [];
|
||
}
|
||
|
||
/**
|
||
* Flush all of the section contents if done rendering.
|
||
*
|
||
* @return void
|
||
*/
|
||
public function flushSectionsIfDoneRendering()
|
||
{
|
||
if ($this->doneRendering())
|
||
{
|
||
$this->flushSections();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get the layout template.
|
||
*
|
||
* @return string The layout template name
|
||
*/
|
||
public function getLayoutTemplate()
|
||
{
|
||
return $this->layoutTemplate;
|
||
}
|
||
|
||
/**
|
||
* Load a helper file
|
||
*
|
||
* @param string $helperClass The last part of the name of the helper
|
||
* class.
|
||
*
|
||
* @return void
|
||
*
|
||
* @deprecated 3.0 Just use the class in your code. That's what the autoloader is for.
|
||
*/
|
||
public function loadHelper($helperClass = null)
|
||
{
|
||
// Get the helper class name
|
||
$className = '\\' . $this->container->getNamespacePrefix() . 'Helper\\' . ucfirst($helperClass);
|
||
|
||
// This trick autoloads the helper class. We can't instantiate it as
|
||
// helpers are (supposed to be) abstract classes with static method
|
||
// interfaces.
|
||
class_exists($className);
|
||
}
|
||
|
||
/**
|
||
* Returns a reference to the container attached to this View
|
||
*
|
||
* @return Container
|
||
*/
|
||
public function &getContainer()
|
||
{
|
||
return $this->container;
|
||
}
|
||
|
||
public function getTask()
|
||
{
|
||
return $this->task;
|
||
}
|
||
|
||
/**
|
||
* @param string $task
|
||
*
|
||
* @return $this This for chaining
|
||
*/
|
||
public function setTask($task)
|
||
{
|
||
$this->task = $task;
|
||
|
||
return $this;
|
||
}
|
||
|
||
public function getDoTask()
|
||
{
|
||
return $this->doTask;
|
||
}
|
||
|
||
/**
|
||
* @param string $task
|
||
*
|
||
* @return $this This for chaining
|
||
*/
|
||
public function setDoTask($task)
|
||
{
|
||
$this->doTask = $task;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets the pre-render flag
|
||
*
|
||
* @param boolean $value True to enable the pre-render step
|
||
*
|
||
* @return void
|
||
*/
|
||
public function setPreRender($value)
|
||
{
|
||
$this->doPreRender = $value;
|
||
}
|
||
|
||
/**
|
||
* Sets the post-render flag
|
||
*
|
||
* @param boolean $value True to enable the post-render step
|
||
*
|
||
* @return void
|
||
*/
|
||
public function setPostRender($value)
|
||
{
|
||
$this->doPostRender = $value;
|
||
}
|
||
|
||
/**
|
||
* Add an alias for a view template.
|
||
*
|
||
* @param string $viewTemplate Existing view template, in the format
|
||
* componentPart://componentName/viewName/layoutName
|
||
* @param string $alias The alias of the view template (any string will do)
|
||
*
|
||
* @return void
|
||
*/
|
||
public function alias($viewTemplate, $alias)
|
||
{
|
||
$this->viewTemplateAliases[$alias] = $viewTemplate;
|
||
}
|
||
|
||
/**
|
||
* Add a JS script file to the page generated by the CMS.
|
||
*
|
||
* There are three combinations of defer and async (see http://www.w3schools.com/tags/att_script_defer.asp):
|
||
* * $defer false, $async true: The script is executed asynchronously with the rest of the page
|
||
* (the script will be executed while the page continues the parsing)
|
||
* * $defer true, $async false: The script is executed when the page has finished parsing.
|
||
* * $defer false, $async false. (default) The script is loaded and executed immediately. When it finishes
|
||
* loading the browser continues parsing the rest of the page.
|
||
*
|
||
* When you are using $defer = true there is no guarantee about the load order of the scripts. Whichever
|
||
* script loads first will be executed first. The order they appear on the page is completely irrelevant.
|
||
*
|
||
* @param string $uri A path definition understood by parsePath, e.g. media://com_example/js/foo.js
|
||
* @param string $version (optional) Version string to be added to the URL
|
||
* @param string $type MIME type of the script
|
||
* @param boolean $defer Adds the defer attribute, see above
|
||
* @param boolean $async Adds the async attribute, see above
|
||
*
|
||
* @return $this Self, for chaining
|
||
*/
|
||
public function addJavascriptFile($uri, $version = null, $type = 'text/javascript', $defer = false, $async = false)
|
||
{
|
||
// Add an automatic version if $version is null. For no version parameter pass an empty string to $version.
|
||
if (is_null($version))
|
||
{
|
||
$version = $this->container->mediaVersion;
|
||
}
|
||
|
||
$this->container->template->addJS($uri, $defer, $async, $version, $type);
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Adds an inline JavaScript script to the page header
|
||
*
|
||
* @param string $script The script content to add
|
||
* @param string $type The MIME type of the script
|
||
*
|
||
* @return $this Self, for chaining
|
||
*/
|
||
public function addJavascriptInline($script, $type = 'text/javascript')
|
||
{
|
||
$this->container->template->addJSInline($script, $type);
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Add a CSS file to the page generated by the CMS
|
||
*
|
||
* @param string $uri A path definition understood by parsePath, e.g. media://com_example/css/foo.css
|
||
* @param string $version (optional) Version string to be added to the URL
|
||
* @param string $type MIME type of the stylesheeet
|
||
* @param string $media Media target definition of the style sheet, e.g. "screen"
|
||
* @param array $attribs Array of attributes
|
||
*
|
||
* @return $this Self, for chaining
|
||
*/
|
||
public function addCssFile($uri, $version = null, $type = 'text/css', $media = null, $attribs = [])
|
||
{
|
||
// Add an automatic version if $version is null. For no version parameter pass an empty string to $version.
|
||
if (is_null($version))
|
||
{
|
||
$version = $this->container->mediaVersion;
|
||
}
|
||
|
||
$this->container->template->addCSS($uri, $version, $type, $media, $attribs);
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Adds an inline stylesheet (inline CSS) to the page header
|
||
*
|
||
* @param string $css The stylesheet content to add
|
||
* @param string $type The MIME type of the script
|
||
*
|
||
* @return $this Self, for chaining
|
||
*/
|
||
public function addCssInline($css, $type = 'text/css')
|
||
{
|
||
$this->container->template->addCSSInline($css, $type);
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets an entire array of search paths for templates or resources.
|
||
*
|
||
* @param mixed $path The new search path, or an array of search paths. If null or false, resets to the current
|
||
* directory only.
|
||
*
|
||
* @return void
|
||
*/
|
||
protected function setTemplatePath($path)
|
||
{
|
||
// Clear out the prior search dirs
|
||
$this->templatePaths = [];
|
||
|
||
// Actually add the user-specified directories
|
||
$this->addTemplatePath($path);
|
||
|
||
// Set the alternative template search dir
|
||
$templatePath = JPATH_THEMES;
|
||
$fallback = $templatePath . '/' . $this->container->platform->getTemplate() . '/html/' . $this->container->componentName . '/' . $this->name;
|
||
$this->addTemplatePath($fallback);
|
||
|
||
// Get extra directories through event dispatchers
|
||
$extraPathsResults = $this->container->platform->runPlugins('onGetViewTemplatePaths', [
|
||
$this->container->componentName,
|
||
$this->getName(),
|
||
]);
|
||
|
||
if (is_array($extraPathsResults) && !empty($extraPathsResults))
|
||
{
|
||
foreach ($extraPathsResults as $somePaths)
|
||
{
|
||
if (!empty($somePaths))
|
||
{
|
||
foreach ($somePaths as $aPath)
|
||
{
|
||
$this->addTemplatePath($aPath);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Adds to the search path for templates and resources.
|
||
*
|
||
* @param mixed $path The directory or stream, or an array of either, to search.
|
||
*
|
||
* @return void
|
||
*/
|
||
protected function addTemplatePath($path)
|
||
{
|
||
// Just force to array
|
||
$path = (array) $path;
|
||
|
||
// Loop through the path directories
|
||
foreach ($path as $dir)
|
||
{
|
||
// No surrounding spaces allowed!
|
||
$dir = trim($dir);
|
||
|
||
// Add trailing separators as needed
|
||
if (substr($dir, -1) != DIRECTORY_SEPARATOR)
|
||
{
|
||
// Directory
|
||
$dir .= DIRECTORY_SEPARATOR;
|
||
}
|
||
|
||
// Add to the top of the search dirs
|
||
array_unshift($this->templatePaths, $dir);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Append content to a given section.
|
||
*
|
||
* @param string $section
|
||
* @param string $content
|
||
*
|
||
* @return void
|
||
*/
|
||
protected function extendSection($section, $content)
|
||
{
|
||
if (isset($this->sections[$section]))
|
||
{
|
||
$content = str_replace('@parent', $content, $this->sections[$section]);
|
||
}
|
||
|
||
$this->sections[$section] = $content;
|
||
}
|
||
|
||
/**
|
||
* Evaluates the template described in the _tempFilePath property
|
||
*
|
||
* @param array $forceParams Forced parameters
|
||
*
|
||
* @return string
|
||
* @throws Exception
|
||
*/
|
||
protected function processTemplate(array &$forceParams)
|
||
{
|
||
// If the engine returned raw content, return the raw content immediately
|
||
if ($this->_tempFilePath['type'] == 'raw')
|
||
{
|
||
return $this->_tempFilePath['content'];
|
||
}
|
||
|
||
if (substr($this->_tempFilePath['content'], 0, 4) == 'raw|')
|
||
{
|
||
return substr($this->_tempFilePath['content'], 4);
|
||
}
|
||
|
||
$obLevel = ob_get_level();
|
||
|
||
ob_start();
|
||
|
||
// We'll process the contents of the view inside a try/catch block so we can
|
||
// flush out any stray output that might get out before an error occurs or
|
||
// an exception is thrown. This prevents any partial views from leaking.
|
||
try
|
||
{
|
||
$this->includeTemplateFile($forceParams);
|
||
}
|
||
catch (Exception $e)
|
||
{
|
||
$this->handleViewException($e, $obLevel);
|
||
}
|
||
|
||
return ob_get_clean();
|
||
}
|
||
|
||
/**
|
||
* Handle a view exception.
|
||
*
|
||
* @param Exception $e The exception to handle
|
||
* @param int $obLevel The target output buffering level
|
||
*
|
||
* @return void
|
||
*
|
||
* @throws $e
|
||
*/
|
||
protected function handleViewException(Exception $e, $obLevel)
|
||
{
|
||
while (ob_get_level() > $obLevel)
|
||
{
|
||
ob_end_clean();
|
||
}
|
||
|
||
$message = $e->getMessage() . ' (View template: ' . realpath($this->_tempFilePath['content']) . ')';
|
||
|
||
$newException = new ErrorException($message, 0, 1, $e->getFile(), $e->getLine(), $e);
|
||
|
||
throw $newException;
|
||
}
|
||
|
||
/**
|
||
* Get the appropriate view engine for the given view template path.
|
||
*
|
||
* @param string $path The path of the view template
|
||
*
|
||
* @return EngineInterface
|
||
*
|
||
* @throws UnrecognisedExtension
|
||
*/
|
||
protected function getEngine($path)
|
||
{
|
||
foreach ($this->viewEngineMap as $extension => $engine)
|
||
{
|
||
if (substr($path, -strlen($extension)) == $extension)
|
||
{
|
||
return new $engine($this);
|
||
}
|
||
}
|
||
|
||
throw new UnrecognisedExtension($path);
|
||
}
|
||
|
||
/**
|
||
* Get the extension used by the view file.
|
||
*
|
||
* @param string $path
|
||
*
|
||
* @return string
|
||
*/
|
||
protected function getExtension($path)
|
||
{
|
||
$extensions = array_keys($this->viewEngineMap);
|
||
|
||
return array_first($extensions, function ($key, $value) use ($path) {
|
||
return ends_with($path, $value);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Triggers an object-specific event. The event runs both locally –if a suitable method exists– and through the
|
||
* Joomla! plugin system. A true/false return value is expected. The first false return cancels the event.
|
||
*
|
||
* EXAMPLE
|
||
* Component: com_foobar, Object name: item, Event: onBeforeSomething, Arguments: array(123, 456)
|
||
* The event calls:
|
||
* 1. $this->onBeforeSomething(123, 456)
|
||
* 2. Joomla! plugin event onComFoobarViewItemBeforeSomething($this, 123, 456)
|
||
*
|
||
* @param string $event The name of the event, typically named onPredicateVerb e.g. onBeforeKick
|
||
* @param array $arguments The arguments to pass to the event handlers
|
||
*
|
||
* @return bool
|
||
*/
|
||
protected function triggerEvent($event, array $arguments = [])
|
||
{
|
||
$result = true;
|
||
|
||
// If there is an object method for this event, call it
|
||
if (method_exists($this, $event))
|
||
{
|
||
switch (count($arguments))
|
||
{
|
||
case 0:
|
||
$result = $this->{$event}();
|
||
break;
|
||
case 1:
|
||
$result = $this->{$event}($arguments[0]);
|
||
break;
|
||
case 2:
|
||
$result = $this->{$event}($arguments[0], $arguments[1]);
|
||
break;
|
||
case 3:
|
||
$result = $this->{$event}($arguments[0], $arguments[1], $arguments[2]);
|
||
break;
|
||
case 4:
|
||
$result = $this->{$event}($arguments[0], $arguments[1], $arguments[2], $arguments[3]);
|
||
break;
|
||
case 5:
|
||
$result = $this->{$event}($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4]);
|
||
break;
|
||
default:
|
||
$result = call_user_func_array([$this, $event], $arguments);
|
||
break;
|
||
}
|
||
}
|
||
|
||
if ($result === false)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// All other event handlers live outside this object, therefore they need to be passed a reference to this
|
||
// objects as the first argument.
|
||
array_unshift($arguments, $this);
|
||
|
||
// If we have an "on" prefix for the event (e.g. onFooBar) remove it and stash it for later.
|
||
$prefix = '';
|
||
|
||
if (substr($event, 0, 2) == 'on')
|
||
{
|
||
$prefix = 'on';
|
||
$event = substr($event, 2);
|
||
}
|
||
|
||
// Get the component/model prefix for the event
|
||
$prefix .= 'Com' . ucfirst($this->container->bareComponentName) . 'View';
|
||
$prefix .= ucfirst($this->getName());
|
||
|
||
// The event name will be something like onComFoobarItemsBeforeSomething
|
||
$event = $prefix . $event;
|
||
|
||
// Call the Joomla! plugins
|
||
$results = $this->container->platform->runPlugins($event, $arguments);
|
||
|
||
if (!empty($results))
|
||
{
|
||
foreach ($results as $result)
|
||
{
|
||
if ($result === false)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Runs before rendering the view template, echoing HTML to put before the
|
||
* view template's generated HTML
|
||
*
|
||
* @return void
|
||
*/
|
||
protected function preRender()
|
||
{
|
||
// You need to implement this in children classes
|
||
}
|
||
|
||
/**
|
||
* Runs after rendering the view template, echoing HTML to put after the
|
||
* view template's generated HTML
|
||
*
|
||
* @return void
|
||
*/
|
||
protected function postRender()
|
||
{
|
||
// You need to implement this in children classes
|
||
}
|
||
|
||
/**
|
||
* This method makes sure the current scope isn't polluted with variables when including a view template
|
||
*
|
||
* @param array $forceParams Forced parameters
|
||
*
|
||
* @return void
|
||
*/
|
||
private function includeTemplateFile(array &$forceParams)
|
||
{
|
||
// Extract forced parameters
|
||
if (!empty($forceParams))
|
||
{
|
||
extract($forceParams);
|
||
}
|
||
|
||
include $this->_tempFilePath['content'];
|
||
}
|
||
}
|