You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
PlexShare/fuel/core/classes/theme.php

936 lines
23 KiB

<?php
/**
* Part of the Fuel framework.
*
* @package Fuel
* @version 1.8
* @author Fuel Development Team
* @license MIT License
* @copyright 2010 - 2016 Fuel Development Team
* @link http://fuelphp.com
*/
namespace Fuel\Core;
class ThemeException extends \FuelException {}
/**
* Handles loading theme views and assets.
*/
class Theme
{
/**
* All the Theme instances
*
* @var array
*/
protected static $instances = array();
/**
* Acts as a Multiton. Will return the requested instance, or will create
* a new named one if it does not exist.
*
* @param string $name The instance name
* @param array $config
* @return Theme
*/
public static function instance($name = '_default_', array $config = array())
{
if ( ! \array_key_exists($name, static::$instances))
{
static::$instances[$name] = static::forge($config);
}
return static::$instances[$name];
}
/**
* Gets a new instance of the Theme class.
*
* @param array $config Optional config override
* @return Theme
*/
public static function forge(array $config = array())
{
return new static($config);
}
/**
* @var Asset_Instance $asset Asset instance for this theme instance
*/
public $asset = null;
/**
* @var array $paths Possible locations for themes
*/
protected $paths = array();
/**
* @var View $template View instance for this theme instance template
*/
public $template = null;
/**
* @var array $active Currently active theme
*/
protected $active = array(
'name' => null,
'path' => null,
'asset_base' => false,
'asset_path' => false,
'info' => array(),
);
/**
* @var array $fallback Fallback theme
*/
protected $fallback = array(
'name' => null,
'path' => null,
'asset_base' => false,
'asset_path' => false,
'info' => array(),
);
/**
* @var array $config Theme config
*/
protected $config = array(
'active' => 'default',
'fallback' => 'default',
'paths' => array(),
'assets_folder' => 'themes',
'view_ext' => '.html',
'require_info_file' => false,
'info_file_name' => 'themeinfo.php',
'use_modules' => false,
);
/**
* @var array $partials Storage for defined template partials
*/
protected $partials = array();
/**
* @var array $chrome Storage for defined partial chrome
*/
protected $chrome = array();
/**
* @var array $order Order in which partial sections should be rendered
*/
protected $order = array();
/**
* Sets up the theme object. If a config is given, it will not use the config
* file.
*
* @param array $config Optional config override
*/
public function __construct(array $config = array())
{
if (empty($config))
{
\Config::load('theme', true, false, true);
$config = \Config::get('theme', array());
}
// Order of this addition is important, do not change this.
$this->config = $config + $this->config;
// define the default theme paths...
$this->add_paths($this->config['paths']);
// create a unique asset instance for this theme instance...
$this->asset = \Asset::forge('theme_'.spl_object_hash($this), array('paths' => array()));
// and set the active and the fallback theme
$this->active($this->config['active']);
$this->fallback($this->config['fallback']);
}
/**
* Magic method, returns the output of [static::render].
*
* @return string
* @uses Theme::render
*/
public function __toString()
{
try
{
return (string) $this->render();
}
catch (\Exception $e)
{
\Errorhandler::exception_handler($e);
return '';
}
}
/**
* Sets the currently active theme. Will return the currently active
* theme. It will throw a \ThemeException if it cannot locate the theme.
*
* @param string $theme Theme name to set active
* @return array The theme array
* @throws \ThemeException
*/
public function active($theme = null)
{
return $this->set_theme($theme, 'active');
}
/**
* Sets the fallback theme. This theme will be used if a view or asset
* cannot be found in the active theme. Will return the fallback
* theme. It will throw a \ThemeException if it cannot locate the theme.
*
* @param string $theme Theme name to set active
* @return array The theme array
* @throws \ThemeException
*/
public function fallback($theme = null)
{
return $this->set_theme($theme, 'fallback');
}
/**
* Loads a view from the currently active theme, the fallback theme, or
* via the standard FuelPHP cascading file system for views
*
* @param string $view View name
* @param array $data View data
* @param bool $auto_filter Auto filter the view data
* @return View New View object
* @throws \ThemeException
*/
public function view($view, $data = array(), $auto_filter = null)
{
if ($this->active['path'] === null)
{
throw new \ThemeException('You must set an active theme.');
}
return \View::forge($this->find_file($view), $data, $auto_filter);
}
/**
* Loads a viewmodel, and have it use the view from the currently active theme,
* the fallback theme, or the standard FuelPHP cascading file system
*
* @param string $view ViewModel classname without View_ prefix or full classname
* @param string $method Method to execute
* @param bool $auto_filter Auto filter the view data
* @return View New View object
*
* @deprecated 1.8
*/
public function viewmodel($view, $method = 'view', $auto_filter = null)
{
return \Viewmodel::forge($view, $method, $auto_filter, $this->find_file($view));
}
/**
* Loads a presenter, and have it use the view from the currently active theme,
* the fallback theme, or the standard FuelPHP cascading file system
*
* @param string $presenter Presenter classname without View_ prefix or full classname
* @param string $method Method to execute
* @param bool $auto_filter Auto filter the view data
* @param string $view Custom View to associate with this persenter
* @return Presenter
*/
public function presenter($presenter, $method = 'view', $auto_filter = null, $view = null)
{
// if no custom view is given, make it equal to the presenter name
if (is_null($view))
{
// loading from a specific namespace?
if (strpos($presenter, '::') !== false)
{
$split = explode('::', $presenter, 2);
if (isset($split[1]))
{
// remove the namespace from the view name
$view = $split[1];
}
}
else
{
$view = $presenter;
}
}
return \Presenter::forge($presenter, $method, $auto_filter, $this->find_file($view));
}
/**
* Loads an asset from the currently loaded theme.
*
* @param string $path Relative path to the asset
* @return string Full asset URL or path if outside docroot
* @throws \ThemeException
*/
public function asset_path($path)
{
if ($this->active['path'] === null)
{
throw new \ThemeException('You must set an active theme.');
}
if ($this->active['asset_base'])
{
return $this->active['asset_base'].$path;
}
else
{
return $this->active['path'].$path;
}
}
/**
* Sets a template for a theme
*
* @param string $template Name of the template view
* @return View
*/
public function set_template($template)
{
// make sure the template is a View
if (is_string($template))
{
$this->template = $this->view($template);
}
else
{
$this->template = $template;
}
// return the template view for chaining
return $this->template;
}
/**
* Get the template view so it can be manipulated
*
* @return string|View
* @throws \ThemeException
*/
public function get_template()
{
// make sure the partial entry exists
if (empty($this->template))
{
throw new \ThemeException('No valid template could be found. Use set_template() to define a page template.');
}
// return the template
return $this->template;
}
/**
* Define a custom order for a partial section
*
* @param string $section name of the partial section
* @param mixed $order
* @throws \ThemeException
*/
public function set_order($section, $order)
{
$this->order[$section] = $order;
}
/**
* Render the partials and the theme template
*
* @return string|View
* @throws \ThemeException
*/
public function render()
{
// make sure the template to be rendered is defined
if (empty($this->template))
{
throw new \ThemeException('No valid template could be found. Use set_template() to define a page template.');
}
// storage for rendered results
$rendered = array();
// make sure we have a render ordering for all defined partials
foreach ($this->partials as $key => $partials)
{
if ( ! isset($this->order[$key]))
{
$this->order[$key] = 0;
}
}
// determine the rendering sequence
asort($this->order, SORT_NUMERIC);
// pre-process all defined partials in defined order
foreach ($this->order as $key => $order)
{
$output = '';
if (isset($this->partials[$key]))
{
foreach ($this->partials[$key] as $index => $partial)
{
// render the partial
if (is_callable(array($partial, 'render')))
{
$output .= $partial->render();
}
else
{
$output .= $partial;
}
}
}
// store the rendered output
if ( ! empty($output) and array_key_exists($key, $this->chrome))
{
// encapsulate the partial in the chrome template
$rendered[$key] = $this->chrome[$key]['view']->set($this->chrome[$key]['var'], $output, false);
}
else
{
// store the partial output
$rendered[$key] = $output;
}
}
// assign the partials to the template
$this->template->set('partials', $rendered, false);
// return the template
return $this->template;
}
/**
* Sets a partial for the current template
*
* @param string $section Name of the partial section in the template
* @param string|View|ViewModel|Presenter $view View, or name of the view
* @param bool $overwrite If true overwrite any already defined partials for this section
* @return View
*/
public function set_partial($section, $view, $overwrite = false)
{
// make sure the partial entry exists
array_key_exists($section, $this->partials) or $this->partials[$section] = array();
// make sure the partial is a view
if (is_string($view))
{
$name = $view;
$view = $this->view($view);
}
else
{
$name = 'partial_'.count($this->partials[$section]);
}
// store the partial
if ($overwrite)
{
$this->partials[$section] = array($name => $view);
}
else
{
$this->partials[$section][$name] = $view;
}
// return the partial view object for chaining
return $this->partials[$section][$name];
}
/**
* Get a partial so it can be manipulated
*
* @param string $section Name of the partial section in the template
* @param string $view name of the view
* @return View
* @throws \ThemeException
*/
public function get_partial($section, $view)
{
// make sure the partial entry exists
if ( ! array_key_exists($section, $this->partials) or ! array_key_exists($view, $this->partials[$section]))
{
throw new \ThemeException(sprintf('No partial named "%s" can be found in the "%s" section.', $view, $section));
}
return $this->partials[$section][$view];
}
/**
* Returns whether or not a section has partials defined
*
* @param string $section Name of the partial section in the template
* @return bool
*/
public function has_partials($section)
{
return $this->partial_count($section) > 0;
}
/**
* Returns the number of partials defined for a section
*
* @param string $section Name of the partial section in the template
* @return int
*/
public function partial_count($section)
{
// return the defined partial count
return array_key_exists($section, $this->partials) ? count($this->partials[$section]) : 0;
}
/**
* Sets a chrome for a partial
*
* @param string $section Name of the partial section in the template
* @param string|View|ViewModel|Presenter $view chrome View, or name of the view
* @param string $var Name of the variable in the chrome that will output the partial
*
* @return View|ViewModel|Presenter, the view partial
*/
public function set_chrome($section, $view, $var = 'content')
{
// make sure the chrome is a view
if (is_string($view))
{
$view = $this->view($view);
}
$this->chrome[$section] = array('var' => $var, 'view' => $view);
return $view;
}
/**
* Get a set chrome view
*
* @param string $section Name of the partial section in the template
* @return mixed
* @throws \ThemeException
*/
public function get_chrome($section)
{
// make sure the partial entry exists
if ( ! array_key_exists($section, $this->chrome))
{
throw new \ThemeException(sprintf('No chrome for a partial named "%s" can be found.', $section));
}
return $this->chrome[$section]['view'];
}
/**
* Adds the given path to the theme search path.
*
* @param string $path Path to add
* @return void
*/
public function add_path($path)
{
$this->paths[] = rtrim($path, DS).DS;
}
/**
* Adds the given paths to the theme search path.
*
* @param array $paths Paths to add
* @return void
*/
public function add_paths(array $paths)
{
array_walk($paths, array($this, 'add_path'));
}
/**
* Finds the given theme by searching through all of the theme paths. If
* found it will return the path, else it will return `false`.
*
* @param string $theme Theme to find
* @return string|false Path or false if not found
*/
public function find($theme)
{
foreach ($this->paths as $path)
{
if (is_dir($path.$theme))
{
return $path.$theme.DS;
}
}
return false;
}
/**
* Gets an array of all themes in all theme paths, sorted alphabetically.
*
* @return array
*/
public function all()
{
$themes = array();
foreach ($this->paths as $path)
{
$iterator = new \GlobIterator($path.'*');
foreach($iterator as $theme)
{
$themes[] = $theme->getFilename();
}
}
sort($themes);
return $themes;
}
/**
* Get a value from the info array
*
* @param mixed $var
* @param mixed $default
* @param mixed $theme
* @return mixed
* @throws \ThemeException
*/
public function get_info($var = null, $default = null, $theme = null)
{
// if no theme is given
if ($theme === null)
{
// if no var to search is given return the entire active info array
if ($var === null)
{
return $this->active['info'];
}
// find the value in the active theme info
if (($value = \Arr::get($this->active['info'], $var, null)) !== null)
{
return $value;
}
// and if not found, check the fallback
elseif (($value = \Arr::get($this->fallback['info'], $var, null)) !== null)
{
return $value;
}
}
// or if we have a specific theme
else
{
// fetch the info from that theme
$info = $this->load_info($theme);
// and return the requested value
return $var === null ? $info : \Arr::get($info, $var, $default);
}
// not found, return the given default value
return $default;
}
/**
* Set a value in the info array
*
* @return Theme
*/
public function set_info($var, $value = null, $type = 'active')
{
if ($type == 'active')
{
\Arr::set($this->active['info'], $var, $value);
}
elseif ($type == 'fallback')
{
\Arr::set($this->fallback['info'], $var, $value);
}
// return for chaining
return $this;
}
/**
* Load in the theme.info file for the given (or active) theme.
*
* @param string $theme Name of the theme (null for active)
* @return array Theme info array
* @throws \ThemeException
*/
public function load_info($theme = null)
{
if ($theme === null)
{
$theme = $this->active;
}
if (is_array($theme))
{
$path = $theme['path'];
$name = $theme['name'];
}
else
{
$path = $this->find($theme);
$name = $theme;
$theme = array(
'name' => $name,
'path' => $path,
);
}
if ( ! $path)
{
throw new \ThemeException(sprintf('Could not find theme "%s".', $theme));
}
if (($file = $this->find_file($this->config['info_file_name'], array($theme))) == $this->config['info_file_name'])
{
if ($this->config['require_info_file'])
{
throw new \ThemeException(sprintf('Theme "%s" is missing "%s".', $name, $this->config['info_file_name']));
}
else
{
return array();
}
}
return \Config::load($file, false, true);
}
/**
* Save the theme.info file for the active (or fallback) theme.
*
* @param string $type Name of the theme (null for active)
* @return array Theme info array
* @throws \ThemeException
*/
public function save_info($type = 'active')
{
if ($type == 'active')
{
$theme = $this->active;
}
elseif ($type == 'fallback')
{
$theme = $this->fallback;
}
else
{
throw new \ThemeException('No location found to save the info file to.');
}
if ( ! $theme['path'])
{
throw new \ThemeException(sprintf('Could not find theme "%s".', $theme['name']));
}
if ( ! ($file = $this->find_file($this->config['info_file_name'], array($theme))))
{
throw new \ThemeException(sprintf('Theme "%s" is missing "%s".', $theme['name'], $this->config['info_file_name']));
}
return \Config::save($file, $theme['info']);
}
/**
* Enable or disable the use of modules. If enabled, every theme view loaded
* will be prefixed with the module name, so you don't have to hardcode the
* module name as a view file prefix
*
* @param bool|string $enable enable if true or string, disable if false
* @return Theme
*/
public function use_modules($enable = true)
{
$this->config['use_modules'] = $enable;
// return for chaining
return $this;
}
/**
* Find the absolute path to a file in a set of Themes. You can optionally
* send an array of themes to search. If you do not, it will search active
* then fallback (in that order).
*
* @param string $view name of the view to find
* @param array $themes optional array of themes to search
* @return string absolute path to the view
* @throws \ThemeException when not found
*/
protected function find_file($view, $themes = null)
{
if ($themes === null)
{
$themes = array($this->active, $this->fallback);
}
// determine the path prefix and optionally the module path
$path_prefix = '';
$module_path = null;
if ($this->config['use_modules'] and class_exists('Request', false) and $request = \Request::active() and $module = $request->module)
{
// we're using module name prefixing
$path_prefix = $module.DS;
// and modules are in a separate path
is_string($this->config['use_modules']) and $path_prefix = trim($this->config['use_modules'], '\\/').DS.$path_prefix;
// do we need to check the module too?
$this->config['use_modules'] === true and $module_path = \Module::exists($module).'themes'.DS;
}
foreach ($themes as $theme)
{
$ext = pathinfo($view, PATHINFO_EXTENSION)
? ('.'.pathinfo($view, PATHINFO_EXTENSION))
: $this->config['view_ext'];
$file = pathinfo($view, PATHINFO_DIRNAME)
? (str_replace(array('/', DS), DS, pathinfo($view, PATHINFO_DIRNAME)).DS)
: '';
$file .= pathinfo($view, PATHINFO_FILENAME);
if (empty($theme['find_file']))
{
if ($module_path and ! empty($theme['name']) and is_file($path = $module_path.$theme['name'].DS.$file.$ext))
{
return $path;
}
elseif (is_file($path = $theme['path'].$path_prefix.$file.$ext))
{
return $path;
}
elseif (is_file($path = $theme['path'].$file.$ext))
{
return $path;
}
}
else
{
if ($path = \Finder::search($theme['path'].$path_prefix, $file, $ext))
{
return $path;
}
}
}
// not found, return the viewname to fall back to the standard View processing
return $view;
}
/**
* Sets a theme.
*
* @param string $theme Theme name to set active
* @param string $type name of the internal theme array to set
* @return array The theme array
* @throws \ThemeException
*/
protected function set_theme($theme = null, $type = 'active')
{
// set the theme if given
if ($theme !== null)
{
// remove the defined theme asset paths from the asset instance
empty($this->active['asset_path']) or $this->asset->remove_path($this->active['asset_path']);
empty($this->fallback['asset_path']) or $this->asset->remove_path($this->fallback['asset_path']);
$this->{$type} = $this->create_theme_array($theme);
// add the asset paths to the asset instance
empty($this->fallback['asset_path']) or $this->asset->add_path($this->fallback['asset_path']);
empty($this->active['asset_path']) or $this->asset->add_path($this->active['asset_path']);
}
// and return the theme config
return $this->{$type};
}
/**
* Creates a theme array by locating the given theme and setting all of the
* option. It will throw a \ThemeException if it cannot locate the theme.
*
* @param string $theme Theme name to set active
* @return array The theme array
* @throws \ThemeException
*/
protected function create_theme_array($theme)
{
if ( ! is_array($theme))
{
if ( ! $path = $this->find($theme))
{
throw new \ThemeException(sprintf('Theme "%s" could not be found.', $theme));
}
$theme = array(
'name' => $theme,
'path' => $path,
);
}
else
{
if ( ! isset($theme['name']) or ! isset($theme['path']))
{
throw new \ThemeException('Theme name and path must be given in array config.');
}
}
// load the theme info file
if ( ! isset($theme['info']))
{
$theme['info'] = $this->load_info($theme);
}
if ( ! isset($theme['asset_base']))
{
// determine the asset location and base URL
$assets_folder = rtrim($this->config['assets_folder'], DS).'/';
// all theme files are inside the docroot
if (strpos($path, DOCROOT) === 0 and is_dir($path.$assets_folder))
{
$theme['asset_path'] = $path.$assets_folder;
$theme['asset_base'] = str_replace(DOCROOT, '', $theme['asset_path']);
}
// theme views and templates are outside the docroot
else
{
$theme['asset_base'] = $assets_folder.$theme['name'].'/';
}
}
if ( ! isset($theme['asset_path']) and strpos($theme['asset_base'], '://') === false)
{
$theme['asset_path'] = DOCROOT.$theme['asset_base'];
}
// always uses forward slashes for the asset base and path
$theme['asset_base'] = str_replace(DS, '/', $theme['asset_base']);
$theme['asset_path'] = str_replace(DS, '/', $theme['asset_path']);
// but if on windows, file paths require a backslash
if (strpos($theme['asset_base'], '://') === false and DS !== '/')
{
$theme['asset_path'] = str_replace('/', DS, $theme['asset_path']);
}
return $theme;
}
}