Skip to content
Snippets Groups Projects
Select Git revision
  • 30360f111dc8e724487184d9d8980f3fb8d51c95
  • master default
  • prereq-parse
  • catalog-rewrite
4 results

Savvy.php

Blame
  • user avatar
    Kevin Abel authored
    30360f11
    History
    Savvy.php 28.07 KiB
    <?php
    /**
     * Savvy
     *
     * PHP version 5
     *
     * @category  Templates
     * @package   Savvy
     * @author    Brett Bieber <saltybeagle@php.net>
     * @copyright 2010 Brett Bieber
     * @license   http://www.opensource.org/licenses/bsd-license.php New BSD License
     * @version   SVN: $Id$
     * @link      https://github.com/saltybeagle/savvy
     */
    
    /**
     * Main class for Savvy
     *
     * @category  Templates
     * @package   Savvy
     * @author    Brett Bieber <saltybeagle@php.net>
     * @copyright 2010 Brett Bieber
     * @license   http://www.opensource.org/licenses/bsd-license.php New BSD License
     * @link      https://github.com/saltybeagle/savvy
     */
    class Savvy
    {
        /**
        *
        * Array of configuration parameters.
        *
        * @access protected
        *
        * @var array
        *
        */
    
        protected $__config = array(
            'compiler'            => null,
            'filters'             => array(),
            'escape'              => null,
            'iterate_traversable' => false,
        );
    
        /**
         * Parameters for escaping.
         *
         * @var array
         */
        protected $_escape = array(
            'quotes'  => ENT_COMPAT,
            'charset' => 'UTF-8',
            );
    
        /**
         * The output template to render using
         *
         * @var string
         */
        protected $template;
    
        /**
         * stack of templates, so we can access the parent template
         *
         * @var array
         */
        protected $templateStack = array();
    
        /**
         * To avoid stats on locating templates, populate this array with
         * full path => 1 for any existing templates
         *
         * @var array
         */
        protected $templateMap = array();
    
        /**
         * An array of paths to look for template files in.
         *
         * @var array
         */
        protected $template_path = array('./');
    
        /**
         * The current controller to use
         *
         * @var string
         */
        protected $selected_controller;
    
        /**
         * How class names are translated to templates
         *
         * @var MapperInterface
         */
        protected $class_to_template;
    
        /**
         * Array of globals available within every template
         *
         * @var array
         */
        protected $globals = array();
        // -----------------------------------------------------------------
        //
        // Constructor and magic methods
        //
        // -----------------------------------------------------------------
    
        /**
        *
        * Constructor.
        *
        * @access public
        *
        * @param array $config An associative array of configuration keys for
        * the Main object.  Any, or none, of the keys may be set.
        *
        * @return Savvy A Savvy instance.
        *
        */
    
        public function __construct($config = null)
        {
            $savvy = $this;
    
            $this->selected_controller = 'basic';
    
            // set the default template search path
            if (isset($config['template_path'])) {
                // user-defined dirs
                $this->setTemplatePath($config['template_path']);
            }
    
            // set the output escaping callbacks
            if (isset($config['escape'])) {
                $this->setEscape($config['escape']);
            }
    
            // set the default filter callbacks
            if (isset($config['filters'])) {
                $this->addFilters($config['filters']);
            }
    
            // set whether to iterate over Traversable objects
            if (isset($config['iterate_traversable'])) {
                $this->setIterateTraversable($config['iterate_traversable']);
            }
        }
    
        /**
         * Basic output controller
         *
         * @param mixed $context The context passed to the template
         * @param mixed $parent  Parent template with context and parents $parent->context
         * @param mixed $file    The filename to include
         * @param Savvy $savvy   The Savvy templating system
         *
         * @return string
         */
        protected static function basicOutputController($context, $parent, $file, $savvy)
        {
            foreach ($savvy->getGlobals() as $__name => $__value) {
                $$__name = $__value;
            }
            unset($__name, $__value);
            ob_start();
            include $file;
    
            return ob_get_clean();
        }
    
        /**
         * Basic output controller
         *
         * @param mixed $context The context passed to the template
         * @param mixed $parent  Parent template with context and parents $parent->context
         * @param mixed $file    The filename to include
         * @param Savvy $savvy   The Savvy templating system
         *
         * @return string
         */
        protected static function filterOutputController($context, $parent, $file, $savvy)
        {
            foreach ($savvy->getGlobals() as $__name => $__value) {
                $$__name = $__value;
            }
            unset($__name, $__value);
            ob_start();
            include $file;
    
            return $savvy->applyFilters(ob_get_clean());
        }
    
        /**
         * Basic output controller
         *
         * @param mixed $context The context passed to the template
         * @param mixed $parent  Parent template with context and parents $parent->context
         * @param mixed $file    The filename to include
         * @param Savvy $savvy   The Savvy templating system
         *
         * @return string
         */
        protected static function basicCompiledOutputController($context, $parent, $file, $savvy)
        {
            foreach ($savvy->getGlobals() as $__name => $__value) {
                $$__name = $__value;
            }
            unset($__name, $__value);
            ob_start();
            include $savvy->template($file);
    
            return ob_get_clean();
        }
    
        /**
         * Basic output controller
         *
         * @param mixed $context The context passed to the template
         * @param mixed $parent  Parent template with context and parents $parent->context
         * @param mixed $file    The filename to include
         * @param Savvy $savvy   The Savvy templating system
         *
         * @return string
         */
        protected static function filterCompiledOutputController($context, $parent, $file, $savvy)
        {
            foreach ($savvy->getGlobals() as $__name => $__value) {
                $$__name = $__value;
            }
            unset($__name, $__value);
            ob_start();
            include $savvy->template($file);
    
            return $savvy->applyFilters(ob_get_clean());
        }
    
        /**
         * Basic output controller
         *
         * @param mixed $context The context passed to the template
         * @param mixed $parent  Parent template with context and parents $parent->context
         * @param mixed $file    The filename to include
         * @param Savvy $savvy   The Savvy templating system
         *
         * @return string
         */
        protected static function basicFastCompiledOutputController($context, $parent, $file, $savvy)
        {
            return include $savvy->template($file);
        }
    
        /**
         * Basic output controller
         *
         * @param mixed $context The context passed to the template
         * @param mixed $parent  Parent template with context and parents $parent->context
         * @param mixed $file    The filename to include
         * @param Savvy $savvy   The Savvy templating system
         *
         * @return string
         */
        protected static function filterFastCompiledOutputController($context, $parent, $file, $savvy)
        {
            return $savvy->applyFilters(include $savvy->template($file));
        }
    
        /**
         * Add a global variable which will be available inside every template
         * 
         * Inside templates, reference the global using the name passed
         * <code>
         * $savvy->addGlobal('formHelper', new FormHelper());
         * </code>
         * 
         * Sample template, Form.tpl.php
         * <code>
         * echo $formHelper->renderInput('name');
         * </code>
         *
         * @param string $var   The global variable name
         * @param mixed  $value The value or variable to expose globally
         *
         * @return void
         */
        public function addGlobal($name, $value)
        {
            // disallow specific variable names, these are reserved variables
            switch ($name) {
                case 'context':
                case 'parent':
                case 'template':
                case 'savvy':
                case 'this':
                    throw new Savvy_BadMethodCallException('Invalid global variable name');
            }
    
            // if output is currently escaped, make sure the global is escaped
            if ($this->__config['escape']) {
                $value = $this->filterVar($value);
            }
    
            $this->globals[$name] = $value;
        }
    
        /**
         * Filter a variable of unknown type
         *
         * @param mixed $var The variable to filter
         *
         * @return string|Savvy_ObjectProxy
         */
        public function filterVar($var)
        {
            switch (gettype($var)) {
            case 'object':
                if ($var instanceof ArrayIterator) {
                    return new Savvy_ObjectProxy_ArrayIterator($var, $this);
                }
                if ($var instanceof ArrayAccess) {
                    return new Savvy_ObjectProxy_ArrayAccess($var, $this);
                }
    
                return Savvy_ObjectProxy::factory($var, $this);
            case 'string':
            case 'integer':
            case 'double':
                return $this->escape($var);
            case 'array':
                return new Savvy_ObjectProxy_ArrayObject(
                    new \ArrayObject($var),
                    $this
                );
            }
    
            return $var;
        }
    
        /**
         * Get the array of assigned globals
         *
         * @return array
         */
        public function getGlobals()
        {
            return $this->globals;
        }
    
        /**
         * Return the current template set (if any)
         *
         * @return string
         */
        public function getTemplate()
        {
            return $this->template;
        }
    
        // -----------------------------------------------------------------
        //
        // Public configuration management (getters and setters).
        //
        // -----------------------------------------------------------------
    
        /**
        *
        * Returns a copy of the Savvy configuration parameters.
        *
        * @access public
        *
        * @param string $key The specific configuration key to return.  If null,
        * returns the entire configuration array.
        *
        * @return mixed A copy of the $this->__config array.
        *
        */
    
        public function getConfig($key = null)
        {
            if (is_null($key)) {
                // no key requested, return the entire config array
                return $this->__config;
            } elseif (empty($this->__config[$key])) {
                // no such key
                return null;
            } else {
                // return the requested key
                return $this->__config[$key];
            }
        }
    
        /**
        *
        * Sets a custom compiler/pre-processor callback for template sources.
        *
        * By default, Savvy does not use a compiler; use this to set your
        * own custom compiler (pre-processor) for template sources.
        *
        * @access public
        *
        * @param mixed $compiler A compiler callback value suitable for the
        * first parameter of call_user_func().  Set to null/false/empty to
        * use PHP itself as the template markup (i.e., no compiling).
        *
        * @return void
        *
        */
    
        public function setCompiler(Savvy_CompilerInterface $compiler)
        {
            $this->__config['compiler'] = $compiler;
            if ($compiler instanceof Savvy_FastCompilerInterface) {
                switch ($this->selected_controller) {
                    case 'basic' :
                    case 'basiccompiled';
                        $this->selected_controller = 'basicfastcompiled';
                        break;
                    case 'filter' :
                    case 'filtercompiled' :
                        $this->selected_controller = 'filterfastcompiled';
                        break;
                }
    
                return;
            }
            if (!strpos($this->selected_controller, 'compiled')) {
                $this->selected_controller .= 'compiled';
            }
        }
    
        /**
         * Set the class to template mapper.
         *
         * @see MapperInterface
         *
         * @param MapperInterface $mapper The mapper interface to use
         *
         * @return Main
         */
        public function setClassToTemplateMapper(Savvy_MapperInterface $mapper)
        {
            $this->class_to_template = $mapper;
    
            return $this;
        }
    
        /**
         * Get the class to template mapper.
         *
         * @return MapperInterface
         */
        public function getClassToTemplateMapper()
        {
            if (!isset($this->class_to_template)) {
                $this->setClassToTemplateMapper(new Savvy_ClassToTemplateMapper());
            }
    
            return $this->class_to_template;
        }
    
        public function setIterateTraversable($iterate)
        {
            $this->__config['iterate_traversable'] = (bool)$iterate;
    
            return $this;
        }
    
        public function getIterateTraversable()
        {
            return $this->__config['iterate_traversable'];
        }
    
        // -----------------------------------------------------------------
        //
        // Output escaping and management.
        //
        // -----------------------------------------------------------------
    
        /**
        *
        * Clears then sets the callbacks to use when calling $this->escape().
        *
        * Each parameter passed to this function is treated as a separate
        * callback.  For example:
        *
        * <code>
        * $savvy->setEscape(
        *     'stripslashes',
        *     'htmlspecialchars',
        *     array('StaticClass', 'method'),
        *     array($object, $method)
        * );
        * </code>
        *
        * @access public
        *
        * @return Main
        *
        */
    
        public function setEscape()
        {
            $this->__config['escape'] = @func_get_args();
    
            return $this;
        }
    
        /**
        *
        * Gets the array of output-escaping callbacks.
        *
        * @access public
        *
        * @return array The array of output-escaping callbacks.
        *
        */
    
        public function getEscape()
        {
            return $this->__config['escape'];
        }
    
        /**
         * Escapes a value for output in a view script.
         *
         * If escaping mechanism is one of htmlspecialchars or htmlentities, uses
         * {@link $_encoding} setting.
         *
         * @param mixed $var The output to escape.
         *
         * @return mixed The escaped value.
         */
        public function escape($var)
        {
            foreach ($this->__config['escape'] as $escape) {
                if (in_array($escape,
                        array('htmlspecialchars', 'htmlentities'), true)) {
                    $var = call_user_func($escape,
                                          $var,
                                          $this->_escape['quotes'],
                                          $this->_escape['charset']);
                } else {
                    $var = call_user_func($escape, $var);
                }
            }
    
            return $var;
        }
    
        // -----------------------------------------------------------------
        //
        // File management
        //
        // -----------------------------------------------------------------
    
        /**
         * Get the template path.
         *
         * @return array
         */
        public function getTemplatePath()
        {
            return $this->template_path;
        }
    
        /**
        *
        * Sets an entire array of search paths for templates or resources.
        *
        * @access public
        *
        * @param string|array $path The new set of search paths.  If null or
        * false, resets to the current directory only.
        *
        * @return Main
        *
        */
    
        public function setTemplatePath($path = null)
        {
            // clear out the prior search dirs, add default
            $this->template_path = array('./');
    
            // actually add the user-specified directories
            $this->addTemplatePath($path);
    
            return $this;
        }
    
        /**
        *
        * Adds to the search path for templates and resources.
        *
        * @access public
        *
        * @param string|array $path The directory or stream to search.
        *
        * @return Main
        *
        */
    
        public function addTemplatePath($path)
        {
            // convert from path string to array of directories
            if (is_string($path) && !strpos($path, '://')) {
    
                // the path config is a string, and it's not a stream
                // identifier (the "://" piece). add it as a path string.
                $path = explode(PATH_SEPARATOR, $path);
    
                // typically in path strings, the first one is expected
                // to be searched first. however, Savvy uses a stack,
                // so the first would be last.  reverse the path string
                // so that it behaves as expected with path strings.
                $path = array_reverse($path);
    
            } else {
    
                // just force to array
                settype($path, 'array');
    
            }
    
            // loop through the path directories
            foreach ($path as $dir) {
    
                // no surrounding spaces allowed!
                $dir = trim($dir);
    
                // add trailing separators as needed
                if (strpos($dir, '://')) {
                    if (substr($dir, -1) != '/') {
                        // stream
                        $dir .= '/';
                    }
                } elseif (substr($dir, -1) != DIRECTORY_SEPARATOR) {
                    if (false !== strpos($dir, '..')) {
                        // checking for weird paths here removes directory traversal threat
                        throw new Savvy_UnexpectedValueException('upper directory reference .. cannot be used in template path');
                    }
                    // directory
                    $dir .= DIRECTORY_SEPARATOR;
                }
    
                // add to the top of the search dirs
                array_unshift(
                    $this->template_path,
                    $dir
                );
            }
        }
    
    
        /**
        *
        * Searches the directory paths for a given file.
        *
        * @param string $file The file name to look for.
        *
        * @return string|bool The full path and file name for the target file,
        * or boolean false if the file is not found in any of the paths.
        *
        */
    
        public function findTemplateFile($file)
        {
            if (false !== strpos($file, '..')) {
                // checking for weird path here removes directory traversal threat
                throw new Savvy_UnexpectedValueException('upper directory reference .. cannot be used in template filename');
            }
    
            // start looping through the path set
            foreach ($this->template_path as $path) {
                // get the path to the file
                $fullname = $path . $file;
    
                if (isset($this->templateMap[$fullname])) {
                    return $fullname;
                }
    
                if (!@is_readable($fullname)) {
                    continue;
                }
    
                return $fullname;
            }
    
            // could not find the file in the set of paths
            throw new Savvy_TemplateException('Could not find the template ' . $file);
        }
    
    
        // -----------------------------------------------------------------
        //
        // Template processing
        //
        // -----------------------------------------------------------------
    
        /**
         * Render context data through a template.
         *
         * This method allows you to render data through a template. Typically one
         * will pass the model they wish to display through an optional template.
         * If no template is specified, the ClassToTemplateMapper::map() method
         * will be called which should return the name of a template to render.
         *
         * Arrays will be looped over and rendered through the template specified.
         *
         * Strings, ints, and doubles will returned if no template parameter is
         * present.
         *
         * Within templates, two variables will be available, $context and $savvy.
         * The $context variable will contain the data passed to the render method,
         * the $savvy object will be an instance of the Main class with which you
         * can render nested data through partial templates.
         *
         * @param mixed  $mixed    Data to display through the template.
         * @param string $template A template to display data in.
         *
         * @return string The template output
         */
        public function render($mixed = null, $template = null)
        {
            $method = 'render'.gettype($mixed);
    
            return $this->$method($mixed, $template);
        }
    
        /**
         * Called when a resource is rendered
         *
         * @param resource $resouce  The resources
         * @param string   $template Template
         *
         * @return void
         *
         * @throws UnexpectedValueException
         */
        protected function renderResource($resouce, $template = null)
        {
            throw new Savvy_UnexpectedValueException('No way to render a resource!');
        }
    
        protected function renderBoolean($bool, $template = null)
        {
            return $this->renderString((string) $bool, $template);
        }
    
        protected function renderDouble($double, $template = null)
        {
            return $this->renderString($double, $template);
        }
    
        protected function renderInteger($int, $template = null)
        {
            return $this->renderString($int, $template);
        }
    
        /**
         * Render string of data
         *
         * @param string $string   String of data
         * @param string $template A template to display the string in
         *
         * @return string
         */
        protected function renderString($string, $template = null)
        {
            if ($this->__config['escape']) {
                $string = $this->escape($string);
            }
    
            if ($template) {
                return $this->fetch($string, $template);
            }
    
            if (!$this->__config['filters']) {
                return $string;
            }
    
            return $this->applyFilters($string);
        }
    
        /**
         * Used to render context array
         *
         * @param array  $array    Data to render
         * @param string $template Template to render
         *
         * @return string Rendered output
         */
        protected function renderArray(array $array, $template = null)
        {
            $output = '';
            foreach ($array as $mixed) {
                $output .= $this->render($mixed, $template);
            }
    
            return $output;
        }
    
        /**
         * Render an associative array of data through a template.
         *
         * Three parameters will be passed to the closure, the array key, value,
         * and selective third parameter.
         *
         * @param array   $array    Associative array of data
         * @param mixed   $selected Optional parameter to pass
         * @param Closure $template A closure that will be called
         *
         * @return string
         */
        public function renderAssocArray(array $array, $selected = false, Closure $template)
        {
            $ret = '';
            foreach ($array as $key => $element) {
                $ret .= $template($key, $element, $selected);
            }
    
            return $ret;
        }
    
        protected function renderTraversable(Traversable $array, $template = null)
        {
            $ret = '';
            foreach ($array as $key => $element) {
                $ret .= $this->render($element, $template);
            }
    
            return $ret;
        }
    
        /**
         * Render an if else conditional template output.
         *
         * @param mixed  $condition      The conditional to evaluate
         * @param mixed  $render         Context data to render if condition is true
         * @param mixed  $else           Context data to render if condition is false
         * @param string $rendertemplate If true, render using this template
         * @param string $elsetemplate   If false, render using this template
         *
         * @return string
         */
        public function renderElse($condition, $render, $else, $rendertemplate = null, $elsetemplate = null)
        {
            if ($condition) {
                $this->render($render, $rendertemplate);
            } else {
                $this->render($else, $elsetemplate);
            }
        }
    
        /**
         * Used to render an object through a template.
         *
         * @param object $object   Model containing data
         * @param string $template Template to render data through
         *
         * @return string Rendered output
         */
        protected function renderObject($object, $template = null)
        {
            if ($this->__config['escape']) {
    
                if (!$object instanceof Savvy_ObjectProxy) {
                    $object = Savvy_ObjectProxy::factory($object, $this);
                }
    
                if ($object instanceof Traversable
                    && $this->__config['iterate_traversable']
                    ) {
                    return $this->renderTraversable($object->getRawObject(), $template);
                }
            }
    
            return $this->fetch($object, $template);
        }
    
        /**
         * Used to render null through an optional template
         *
         * @param null   $null     The null var
         * @param string $template Template to render null through
         *
         * @return string Rendered output
         */
        protected function renderNULL($null, $template = null)
        {
            if ($template) {
                return $this->fetch(null, $template);
            }
        }
    
        protected function fetch($mixed, $template = null)
        {
            if ($template) {
                $this->template = $template;
            } else {
                if ($mixed instanceof Savvy_ObjectProxy) {
                    $class = $mixed->__getClass();
                } else {
                    $class = get_class($mixed);
                }
                $this->template = $this->getClassToTemplateMapper()->map($class);
            }
            $current          = new stdClass;
            $current->file    = $this->findTemplateFile($this->template);
            $current->context = $mixed;
            $current->parent  = null;
            if (count($this->templateStack)) {
                $current->parent = $this->templateStack[count($this->templateStack)-1];
            }
            $this->templateStack[] = $current;
            $ret = call_user_func(array($this, $this->selected_controller.'OutputController'), $current->context, $current->parent, $current->file, $this);
            array_pop($this->templateStack);
    
            return $ret;
        }
    
        /**
        *
        * Compiles a template and returns path to compiled script.
        *
        * By default, Savvy does not compile templates, it uses PHP as the
        * markup language, so the "compiled" template is the same as the source
        * template.
        *
        * If a compiler is specific, this method is used to look up the compiled
        * template script name
        *
        * @param string $tpl The template source name to look for.
        *
        * @return string The full path to the compiled template script.
        *
        * @throws Savvy_UnexpectedValueException
        * @throws Savvy_Exception
        *
        */
    
        public function template($tpl = null)
        {
            // find the template source.
            $file = $this->findTemplateFile($tpl);
    
            // are we compiling source into a script?
            if ($this->__config['compiler']) {
                // compile the template source and get the path to the
                // compiled script (will be returned instead of the
                // source path)
                $result = $this->__config['compiler']->compile($file, $this);
            } else {
                // no compiling requested, use the source path
                $result = $file;
            }
    
            // is there a script from the compiler?
            if (!$result) {
                // return an error, along with any error info
                // generated by the compiler.
                throw new Savvy_TemplateException('Compiler error for template '.$tpl.'. '.$result );
    
            } else {
                // no errors, the result is a path to a script
                return $result;
            }
        }
    
    
        // -----------------------------------------------------------------
        //
        // Filter management and processing
        //
        // -----------------------------------------------------------------
    
    
        /**
        *
        * Resets the filter stack to the provided list of callbacks.
        *
        * @access protected
        *
        * @param array An array of filter callbacks.
        *
        * @return void
        *
        */
    
        public function setFilters()
        {
            $this->__config['filters'] = (array) @func_get_args();
            if (!$this->__config['filters']) {
                $this->selected_controller = 'basic';
            } else {
                $this->selected_controller = 'filter';
            }
        }
    
    
        /**
        *
        * Adds filter callbacks to the stack of filters.
        *
        * @access protected
        *
        * @param array An array of filter callbacks.
        *
        * @return void
        *
        */
    
        public function addFilters()
        {
            // add the new filters to the static config variable
            // via the reference
            foreach ((array) @func_get_args() as $callback) {
                $this->__config['filters'][] = $callback;
                $this->selected_controller = 'filter';
            }
        }
    
    
        /**
        *
        * Runs all filter callbacks on buffered output.
        *
        * @access protected
        *
        * @param string The template output.
        *
        * @return void
        *
        */
    
        public function applyFilters($buffer)
        {
            foreach ($this->__config['filters'] as $callback) {
                $buffer = call_user_func($callback, $buffer);
            }
    
            return $buffer;
        }
    
    }