Subversion Repositories web.active

Rev

Rev 1 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download

<?php namespace ProcessWire;

/**
 * ProcessWire TemplateFile
 *
 * A template file that will be loaded and executed as PHP and its output returned.
 * 
 * ProcessWire 3.x, Copyright 2020 by Ryan Cramer
 * https://processwire.com
 * 
 * @property bool $halt Set to true to halt during render, or use method $this->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;
    }
    
}