* @copyright Copyright (c) 2013 - 2018, TechFuze. (http://techfuze.net) */ class Layout { /** * @var Factory */ protected $factory; /** * The file to be loaded by the layout manager. * * @var null|string */ public $file = null; /** * The directory of the file to be loaded by the layout manager. * * @var string */ public $directory; /** * All assigned currently assigned to the template. * * @var array Associative Assigned Variable Array */ protected $assigned_variables = array(); /** * All engines that can be used for templates. * * @var array of engines */ protected $engines = array(); /** * All file extensions that can be used and are bound to a template engine. * * @var array of names of engines */ protected $file_extensions = array(); /** * whether the template engines are already called. * * @var bool True if loaded */ protected $engines_loaded = false; /** * The currently selected template engine. * * @var string name of engine */ protected $current_engine; /** * Standard Component method for initializing components after adding extensions */ public function init() { $this->factory = Factory::getInstance(); $this->directory = Core::$appDirs[0] . DS .'Layout'; } /** * Retrieve a template file using a string and a directory and immediatly parse it to the output class. * * What template file gets loaded depends on the template engine that is being used. * PHP for example uses .php files. Providing this function with 'home/dashboard' will load the home/layout.dashboard.php file. * You can also provide no particular engine, and the manager will decide what template to load. * Remember that doing so will result in a LayoutException when multiple compatible files are found. * * @param string $file File to load * @param string $directory Directory to load it from * * @return mixed * @throws LayoutException On error * @throws EventException */ public function display($file, $directory = null) { $directory = (is_null($directory) ? $this->directory : $directory); $contents = $this->get($file, $directory); $event = Events::fireEvent('layoutDisplayEvent', $contents, $file, $directory); if (!$event->isCancelled()) echo $event->contents; return true; } /** * Retrieve a template file using a string and a directory. * * What template file gets loaded depends on the template engine that is being used. * PHP for example uses .php files. Providing this function with 'home/dashboard' will load the home/layout.dashboard.php file. * You can also provide no particular engine, and the manager will decide what template to load. * Remember that doing so will result in a LayoutException when multiple compatible files are found. * * @param string $file File to load * @param string $directory Directory to load it from * * @return string The output of the template * @throws LayoutException On error */ public function get($file, $directory = null): string { $directory = (is_null($directory) ? $this->directory : $directory); Logger::newLevel("Loading template file '".$file."' in '".$directory."'"); // First load the template engines $this->loadTemplateEngines(); // First retrieve the filePath if (is_null($this->current_engine)) { $this->setFileFromString($file, $directory, array_keys($this->file_extensions)); } else { $this->setFileFromString($file, $directory, $this->current_engine->getFileExtensions()); } // Then assign some basic variables for the template $main_config = $this->factory->config->get('main'); $this->assigned_variables['wwwDir'] = $main_config->base_url; $this->assigned_variables['siteURL'] = $main_config->base_url; $this->assigned_variables['serverName'] = $main_config->server_name; $this->assigned_variables['adminMail'] = $main_config->administrator_mail; // @TODO: Implement csrfTokenName and csrfHash from security under layoutLoadEvent // Select an engine if one is not already selected if (is_null($this->current_engine)) { $this->current_engine = $this->getEngineFromExtension($this->getExtensionFromFile($this->file)); } $this->current_engine->setDirectory($this->directory); // And run an Event to see what other parts have to say about it try { /** @var LayoutLoadEvent $event */ $event = Events::fireEvent('layoutLoadEvent', $this->file, $this->directory, $this->current_engine, $this->assigned_variables); // @codeCoverageIgnoreStart } catch (EventException $e) { throw new LayoutException("layoutEvent threw exception: '".$e->getMessage()."''", 1); // @codeCoverageIgnoreEnd } // The event has been cancelled if ($event->isCancelled()) { return 'cancelled'; } // And refetch the data from the event $this->current_engine = $event->engine; $this->assigned_variables = $event->assigned_variables; Logger::stopLevel(); // And finally run it if (file_exists($event->file)) { return $this->current_engine->get($event->file, $this->assigned_variables); } throw new LayoutException('The requested file was not found', 1); } /** * Retrieve a Template Engine from a File Extension. * * @param string $extension File extention to look for * * @return TemplateEngine * @throws LayoutException */ public function getEngineFromExtension($extension): TemplateEngine { if (isset($this->file_extensions[strtolower($extension)])) { return $this->engines[ $this->file_extensions[strtolower($extension)]]; } throw new LayoutException('Could not get Template Engine. No engine has corresponding file extension', 1); } /** * Retrieve the extension from a file string. * * @param string $fileString The path to the file * * @return string Extension of the file */ public function getExtensionFromFile($fileString): string { return substr($fileString, strrpos($fileString, '.') + 1); } /** * Converts a layout string to a file using the directory and the used extensions. * * It will detect whether the file exists and choose a file according to the provided extensions * * @param string $string The string used by a controller. eg: 'dashboard/home' * @param string $directory The directory to search in for the template * @param array $extensions Extensions to use for this template. Eg array('php', 'tpl') etc. * * @return string Filepath of the template * @throws LayoutException On error */ public function getFileFromString($string, $directory, $extensions = array()): string { $directory = preg_replace('#/+#', '/', (!is_null($directory) ? $directory : $this->directory).DS); // @TODO Malformed strings pass. Write better function if (strpbrk($directory, "\\/?%*:|\"<>") === TRUE || strpbrk($string, "\\/?%*:|\"<>") === TRUE) { // @codeCoverageIgnoreStart throw new LayoutException('Could not get file. Invalid file string', 1); // @codeCoverageIgnoreEnd } if (!file_exists($directory)) { throw new LayoutException('Could not get file. Directory does not exist', 1); } // Set the file name and location $layoutSelector = explode('/', $string); if (count($layoutSelector) == 1) { $layoutSelector = 'layout.'.$layoutSelector[0]; } else { // Get last file $file = end($layoutSelector); // Reset to start reset($layoutSelector); // Remove last value array_pop($layoutSelector); $layoutSelector[] = 'layout.'.$file; // And create the final value $layoutSelector = implode(DS, $layoutSelector); } // Then try and select a file $fileSelected = false; $selectedFile = null; foreach ($extensions as $extension) { $file = $directory.$layoutSelector.'.'.strtolower($extension); $file = preg_replace('#/+#', '/', $file); if (file_exists($file) && !$fileSelected) { $selectedFile = $file; $fileSelected = true; Logger::log("Found matching file: '".$file."'"); } elseif (file_exists($file) && $fileSelected) { throw new LayoutException('Could not select template. Multiple valid extensions detected. Can not choose.', 1); } } // And choose what to output if (!$fileSelected) { throw new LayoutException('Could not select template. No matching file found.'); } return $selectedFile; } /** * Converts a layout string to a file using the directory and the used extensions. * It also sets the file variable of this class. * * It will detect whether the file exists and choose a file according to the provided extensions * * @param string $string The string used by a controller. eg: 'dashboard/home' * @param string $directory The directory to search in for the template * @param array $extensions Extensions to use for this template. Eg array('php', 'tpl') etc. * * @throws LayoutException On error */ public function setFileFromString($string, $directory, $extensions = array()) { $this->file = $this->getFileFromString($string, $directory, $extensions); $this->directory = preg_replace('#/+#', '/', (!is_null($directory) ? $directory : $this->directory).DS); } /** * Get the current file to be loaded. * * @return null|string Path to the file */ public function getFile() { return $this->file; } /** * Set the file to be loaded. * * @param string $file Path to the file */ public function setFile($file) { $this->file = $file; } /** * Get the directory of the file to be loaded. * * @return null|string Path to the directory */ public function getDirectory() { return $this->directory; } /** * Set the directory of the file to be loaded. * * @param string $directory Path to the directory */ public function setDirectory($directory) { $this->directory = $directory; } /** * Assign a variable for the template. * * @param string $key Key of the variable * @param mixed $value Value of the variable */ public function assign($key, $value) { $this->assigned_variables[$key] = $value; } /** * Set the title of the template. * * @param string $title title of the template */ public function setTitle($title) { $this->assigned_variables['title'] = $title; } /** * Get the title of the template. * * @return string|bool title of the template */ public function getTitle() { if (!isset($this->assigned_variables['title'])) { return false; } return $this->assigned_variables['title']; } /** * Set the engine for the next layout. * * @param string $name Name of the template engine * * @return bool true on success * @throws LayoutException on error */ public function setEngine($name): bool { $this->loadTemplateEngines(); if (isset($this->engines[$name])) { $this->current_engine = $this->engines[$name]; Logger::log('Set the Template Engine to '.$name); return true; } throw new LayoutException('Could not set engine. Engine does not exist', 1); } /** * Get a loaded template engine. * * @param string $name Name of the template engine * * @return TemplateEngine * @throws LayoutException */ public function getEngine(string $name): TemplateEngine { $this->loadTemplateEngines(); if (isset($this->engines[$name])) { return $this->engines[$name]; } throw new LayoutException('Could not return engine. Engine does not exist', 1); } /** * Register a new template engine. * * @param TemplateEngine $engineClass Object that implements the \FuzeWorks\TemplateEngine * @param string $engineName Name of the template engine * @param array $engineFileExtensions File extensions this template engine should be used for * * @return bool true on success * @throws LayoutException */ public function registerEngine(TemplateEngine $engineClass, string $engineName, array $engineFileExtensions = array()): bool { // First check if the engine already exists if (isset($this->engines[$engineName])) throw new LayoutException("Could not register engine. Engine '".$engineName."' already registered", 1); // Install it $this->engines[$engineName] = $engineClass; // Then install them foreach ($engineFileExtensions as $extension) { if (isset($this->file_extensions[strtolower($extension)])) { throw new LayoutException('Could not register engine. File extension already bound to engine', 1); } // And add it $this->file_extensions[strtolower($extension)] = $engineName; } // And log it Logger::log('Registered Template Engine: '.$engineName); return true; } /** * Load the template engines by sending a layoutLoadEngineEvent. * @throws LayoutException * @returns bool True on loading. False when already loaded */ public function loadTemplateEngines(): bool { if (!$this->engines_loaded) { // Fire Engine Event try { Events::fireEvent('layoutLoadEngineEvent'); } catch (Exception\EventException $e) { throw new LayoutException("Could not loadTemplateEngines. layoutLoadEngineEvent threw exception: '".$e->getMessage()."''", 1); } // Load the engines provided in this file // PHP Engine $this->registerEngine(new PHPEngine(), 'PHP', array('php')); // JSON Engine if (extension_loaded('json')) $this->registerEngine(new JsonEngine(), 'JSON', array('json')); // Latte Engine if (class_exists('\Latte\Engine', true)) $this->registerEngine(new LatteEngine(), 'Latte', array('latte')); // Smarty Engine if (class_exists('\Smarty', true)) $this->registerEngine(new SmartyEngine(), 'Smarty', array('tpl')); $this->engines_loaded = true; return true; } return false; } /** * Resets the layout manager to its default state. */ public function reset() { if (!is_null($this->current_engine)) { $this->current_engine->reset(); } // Unload the engines $this->engines = array(); $this->engines_loaded = false; $this->file_extensions = array(); $this->current_engine = null; $this->assigned_variables = array(); $this->directory = Core::$appDirs[0]. DS . 'Layout'; Logger::log('Reset the layout manager to its default state'); } }