halt(); * @property-read string $filename Primary file to render. * @property-read array $prependFilename Optional file name(s) used for prepend. * @property-read array $appendFilename Optional file name(s) used for append. * @property-read string $currentFilename Current file being rendered (whether primary, prepend, append). * @property-read bool $trim Whether or not leading/trailing whitespace is trimmed from output (3.0.154+). * @method string render() * @method bool fileFailed($filename, \Exception $e) * */ class TemplateFile extends WireData { /** * The full path and filename to the PHP template file * * @var string * */ protected $filename; /** * The current filename being rendered (whether prepend, main, append, etc.) * * @var string * */ protected $currentFilename; /** * Optional filenames that are prepended to the render * * @var array * */ protected $prependFilename = array(); /** * Optional filenames that are appended to the render * * @var array * */ protected $appendFilename = array(); /** * The saved directory location before render() was called * * @var string * */ protected $savedDir; /** * Directory to change to before rendering * * If not set, it will change to the directory that the $filename is in. * If false, no directories will be changed. * * @var null|string|bool * */ protected $chdir = null; /** * Saved ProcessWire instance * * @var ProcessWire * */ protected $savedInstance; /** * Throw exception when main template file doesn’t exist? * * @var bool * */ protected $throwExceptions = true; /** * Whether or not the template file called $this->halt() * * @var bool * */ protected $halt = false; /** * Last tracked profile event * * @var mixed * */ protected $profilerEvent = null; /** * @var WireProfilerInterface|null * */ protected $profiler = null; /** * Return value from rendered file * * @var null|mixed * */ protected $returnValue = null; /** * Trim leading/trailing whitespace from rendered output? * * @var bool * */ protected $trim = true; /** * Stack of files that are currently being rendered * * @var array * */ static protected $renderStack = array(); /** * DEPRECATED: Variables that will be applied globally to this and all other TemplateFile instances * */ static protected $globals = array(); /** * Output buffer starting level, set by first TemplateFile instance that gets created * * @var null|int * */ static protected $obStartLevel = null; /** * Construct the template file * * @param string $filename Full path and filename to the PHP template file * */ public function __construct($filename = '') { if(self::$obStartLevel === null) self::$obStartLevel = ob_get_level(); if($filename) $this->setFilename($filename); } /** * Sets the template file name, replacing whatever was set in the constructor * * @param string $filename Full path and filename to the PHP template file * @return bool true on success, false if file doesn't exist * @throws WireException if file doesn't exist (unless throwExceptions is disabled) * */ public function setFilename($filename) { if(empty($filename)) return false; if(is_file($filename)) { $this->filename = $filename; return true; } else { $error = "Filename doesn't exist: $filename"; if($this->throwExceptions) throw new WireException($error); $this->error($error); $this->filename = $filename; // in case it will exist when render() is called return false; } } /** * Set a file to prepend to the template file at render time * * @param string $filename * @return bool Returns true on success, false if file doesn't exist. * @throws WireException if file doesn't exist (unless throwExceptions is disabled) * */ public function setPrependFilename($filename) { if(empty($filename)) return false; if(is_file($filename)) { $this->prependFilename[] = $filename; return true; } else { $error = "Append filename doesn't exist: $filename"; if($this->throwExceptions) throw new WireException($error); $this->error($error); return false; } } /** * Set a file to append to the template file at render time * * @param string $filename * @return bool Returns true on success false if file doesn't exist. * @throws WireException if file doesn't exist (unless throwExceptions is disabled) * */ public function setAppendFilename($filename) { if(empty($filename)) return false; if(is_file($filename)) { $this->appendFilename[] = $filename; return true; } else { $error = "Prepend filename doesn't exist: $filename"; if($this->throwExceptions) throw new WireException($error); $this->error($error); return false; } } /** * Call this with boolean false to disable exceptions when file doesn’t exist * * @param bool $throwExceptions * */ public function setThrowExceptions($throwExceptions) { $this->throwExceptions = $throwExceptions ? true : false; } /** * Set whether rendered output should have leading/trailing whitespace trimmed * * By default whitespace is trimmed so you would call `$templateFile->setTrim(false);` to disable. * * @param bool $trim * @since 3.0.154 * */ public function setTrim($trim) { $this->trim = (bool) $trim; } /** * Set the directory to temporarily change to during rendering * * If not set, it changes to the directory that $filename is in. * To disable TemplateFile from changing any directories, set to false (3.0.154+). * * @param string|bool $chdir * */ public function setChdir($chdir) { $this->chdir = $chdir; } /** * Sets a variable to be globally accessable to all other TemplateFile instances (deprecated) * * Note, to set a variable for just this instance, use the set() as inherted from WireData. * * #pw-internal * * @param string $name * @param mixed $value * @param bool $overwrite Should the value be overwritten if it already exists? (default true) * @deprecated * */ public function setGlobal($name, $value, $overwrite = true) { // set template variable that will apply across all instances of Template if(!$overwrite && isset(self::$globals[$name])) return; self::$globals[$name] = $value; } /** * Render the template: execute it and return its output * * @return string The output of the Template File * @throws WireException|\Exception Throws WireException if file not exist + any exceptions thrown by included file(s) * */ public function ___render() { /** @noinspection PhpIncludeInspection */ if(!$this->filename) return ''; if(!file_exists($this->filename)) { $error = "Template file does not exist: $this->filename"; if($this->throwExceptions) throw new WireException($error); $this->error($error); return ''; } $this->renderReady(); // make API variables available to PHP file $fuel = array_merge($this->getArray(), self::$globals); // so that script can foreach all vars to see what's there extract($fuel); ob_start(); try { // include prepend files foreach($this->prependFilename as $_filename) { if($this->halt) break; $this->fileReady($_filename); require($_filename); $this->fileFinished(); } } catch(\Exception $e) { if($this->fileFailed($this->currentFilename, $e)) throw $this->renderFailed($e); } if($this->halt) { // if prepend file indicates we should halt, then do not render next file $this->returnValue = 0; } else { // include main file to render try { $this->fileReady($this->filename); $this->returnValue = require($this->filename); $this->fileFinished(); } catch(\Exception $e) { if($this->fileFailed($this->filename, $e)) throw $this->renderFailed($e); } } try { // include append files foreach($this->appendFilename as $_filename) { if($this->halt) break; $this->fileReady($_filename); require($_filename); $this->fileFinished(); } } catch(\Exception $e) { if($this->fileFailed($this->currentFilename, $e)) throw $this->renderFailed($e); } $out = ob_get_contents(); ob_end_clean(); $this->renderFinished(); if($this->trim) $out = trim($out); if(!strlen($out) && !$this->halt && $this->returnValue && $this->returnValue !== 1) { return $this->returnValue; } return $out; } /** * Prepare to nclude specific file (whether prepend, main or append) * * @param string $filename * @since 3.0.154 * */ protected function fileReady($filename) { $this->currentFilename = $filename; if($this->profiler) { $f = str_replace($this->wire('config')->paths->root, '/', $filename); $this->profilerEvent = $this->profiler->start($f, $this); } self::pushRenderStack($filename); } /** * Clean up after include specific file * * @since 3.0.154 * */ protected function fileFinished() { $this->currentFilename = ''; if($this->profiler && $this->profilerEvent) { $this->profiler->stop($this->profilerEvent); } self::popRenderStack(); } /** * Called when render of specific file failed with Exception * * #pw-hooker * * @param string $filename * @param \Exception $e * @return bool True if Exception $e should be thrown, false if it should be ignored * @since 3.0.154 * */ protected function ___fileFailed($filename, \Exception $e) { $this->fileFinished(); if($e || $filename) {} // ignore return true; } /** * Prepare to render * * Called right before render about to start * * @since 3.0.154 * */ protected function renderReady() { // ensure that wire() functions in template file map to correct ProcessWire instance $this->savedInstance = ProcessWire::getCurrentInstance(); ProcessWire::setCurrentInstance($this->wire()); $this->profiler = $this->wire('profiler'); if($this->chdir !== false) { $cwd = getcwd(); if($this->chdir) { $chdir = $this->chdir; } else { $chdir = dirname($this->filename); } if($chdir === $cwd) { // already in required directory $this->savedDir = ''; } else { // change to new directory $this->savedDir = $cwd; chdir($chdir); } } } /** * Cleanup after render * * @since 3.0.154 * */ protected function renderFinished() { if($this->currentFilename) { $this->fileFinished(); } if($this->savedDir && $this->chdir !== false) { chdir($this->savedDir); } ProcessWire::setCurrentInstance($this->savedInstance); } /** * Called when overall render failed * * @param \Exception $e * @return \Exception * @since 3.0.154 * */ protected function renderFailed(\Exception $e) { $this->renderFinished(); return $e; } /** * Set the current filename being rendered * * @param string $filename * @deprecated Moved to fileReady() and fileFinished() * */ protected function setCurrentFilename($filename) { if(strlen($filename)) { $this->fileReady($filename); } else { $this->fileFinished(); } } /** * Get an array of all variables accessible (locally scoped) to the PHP template file * * @return array * */ public function getArray() { return array_merge($this->wire('fuel')->getArray(), parent::getArray()); } /** * Get a set property from the template file, typically to check if a template has access to a given variable * * @param string $property * @return mixed Returns the value of the requested property, or NULL if it doesn't exist * */ public function get($property) { if($property === 'filename') return $this->filename; if($property === 'appendFilename' || $property === 'appendFilenames') return $this->appendFilename; if($property === 'prependFilename' || $property === 'prependFilenames') return $this->prependFilename; if($property === 'currentFilename') return $this->currentFilename; if($property === 'halt') return $this->halt; if($property === 'trim') return $this->trim; if($value = parent::get($property)) return $value; if(isset(self::$globals[$property])) return self::$globals[$property]; return null; } /** * Set a property * * @param string $property * @param mixed $value * @return $this|WireData * */ public function set($property, $value) { if($property === 'halt') { $this->halt($value); return $this; } return parent::set($property, $value); } /** * Push a filename onto the render stack * * #pw-internal * * @param string $filename * */ public static function pushRenderStack($filename) { self::$renderStack[] = $filename; } /** * Pop last file off of render stack * * #pw-internal * * @return string|null item that was removed, or null if none found * */ public static function popRenderStack() { $result = array_pop(self::$renderStack); return $result; } /** * Get the current render stack * * This contains the files currently being rendered from first to last * * @return array * */ public static function getRenderStack() { return self::$renderStack; } /** * Clear out all pending output buffers * * @since 3.0.175 * @return int Number of output buffers cleaned * */ public static function clearAll() { $n = 0; if(self::$obStartLevel !== null) { while(ob_get_level() > self::$obStartLevel) { ob_end_clean(); $n++; } } return $n; } /** * The string value of a TemplateFile is its PHP template filename OR its class name if no filename is set * * @return string * */ public function __toString() { if(!$this->filename) return $this->className(); return $this->filename; } /** * This method can be called by any template file to stop further render inclusions * * This is preferable to doing an exit; or die() from your template file(s), as it only halts the rendering * of output and doesn't halt the rest of ProcessWire. * * Can be called from prepend/append files as well. * * USAGE from template file is: return $this->halt(); * * @param bool $halt * @return $this * */ protected function halt($halt = true) { $this->halt = $halt ? true : false; return $this; } }