<?php
Library::import('recess.cache.Cache');
Library::import('recess.lang.PathFinder');
Library::import('recess.framework.helpers.exceptions.MissingRequiredInputException');
Library::import('recess.framework.helpers.exceptions.InputTypeCheckException');
/**
* AssertiveTemplate is a helper class that provides support for
* 'Assertive Templates' or templates that assert their inputs. Typically
* you will use a subclass of AssertiveTemplate, rather than the base
* class itself.
*
* @author Kris Jordan
*/
abstract class AssertiveTemplate {
/**
* Used to locate AssertiveTemplates
* @var Paths
*/
private static $paths = false;
/**
* Initialize the AssertiveTemplate helper class by registering
* the application's views directory as a path.
*
* @param AbstractView
*/
public static function init(AbstractView $view = null) {
self::setPathFinder(Application::active()->viewPathFinder());
}
/**
* Add a directory to be checked for the existance of AssertiveTemplates.
* Paths are checked in the reverse order of their being added so
* that the most specific paths are checked first.
* @param string $path
*/
public static function addPath($path) {
if(!self::$paths instanceof PathFinder) {
// To-do: Cache Paths
self::$paths = new PathFinder();
}
self::$paths->addPath($path);
}
/**
* Set the PathFinder to use when looking for Assertive Templates
* @param PathFinder $pathFinder
*/
public static function setPathFinder(PathFinder $pathFinder) {
self::$paths = $pathFinder;
}
protected static $loaded = array();
/**
* Include a PHP file with a context and return the context that results
* after the template has been executed.
*
* @param string The PHP file to include relative to registered paths.
* @param array An associative array whose keys will become variables in the template.
* @return array The context after execution of the template as a key/value array.
*/
public static function includeTemplate($__assertive_template__, $context) {
$__assertive_template__ = self::$paths->find($__assertive_template__);
if($__assertive_template__ === false) {
throw new Exception('Could not locate AssertiveTemplate: ' . $templateFile);
}
// Unset 'context' if it isn't a key in $context
if(isset($context['context'])) {
extract($context);
} else {
extract($context);
unset($context);
}
include $__assertive_template__;
return get_defined_vars();
}
static $types = array('string','int','bool','float','array');
/**
* input is used by the assertive templates themselves to assert the
* variable name and type of an expected input, as well as a default
* value for optional inputs. If the input is required and is missing
* this will throw a MissingRequiredInputException. If the input type
* does not match the expected type this will throw an
* InputTypeCheckException.
*
* @param varies based on the type string passed at argument 2.
* @param string The type $input is expected to be.
* @param varies Default value for optional arguments.
*
* @return Returns the $input, if $input was null and optional returns $default.
*/
public static function input(&$input, $type, $default = null) {
if($input === NULL && $default !== null) {
$input = $default;
} else {
if($input === null) {
/** Hacky for better debuging experience. Analyze stack to find name of required input missing. */
$stack = debug_backtrace();
if(isset($stack[0])) {
$script = explode("\n", file_get_contents($stack[0]['file']));
$lineNumber = $stack[0]['line'] - 1;
$line = $script[$lineNumber];
preg_match('/\s*(\S*?)::input\(/', $line, $matches);
if(isset($matches[1])) {
preg_match(self::getInputRegex($matches[1]), $line, $inputMatches);
if(isset($inputMatches[1])) {
throw new MissingRequiredInputException('Missing input "'.$inputMatches[1].'" of type "'.$type.'" in: '.$stack[0]['file'], 1);
}
}
}
throw new MissingRequiredInputException('Missing required ' . $type . ' input in :' , 1);
}
}
if(!self::typeCheck($input, $type)) {
$passed = gettype($input);
if($passed === 'object') {
$passed = get_class($input);
}
throw new InputTypeCheckException("Input type mismatch, expected: '$type', actual:'$passed'.", 1);
}
return $input;
}
/**
* Determines whether a provided value is of the requested type. Not
* quite the same as PHP's internal type checking to allow for type
* 'array' to be satisfied by implementations of ArrayAccess. The type
* argument is a string that can be either a PHP type like 'string',
* 'int', 'float', or a class name.
*
* @param variable The value whose type is being checked.
* @param string The expected type.
* @return boolean True if $value is a $type. False if not.
*/
public static function typeCheck($value, $type) {
if(in_array($type, self::$types)) {
if($type === 'array') {
if(!(is_array($value) || $value instanceof ArrayAccess)) {
return false;
} else {
return true;
}
} else {
$fn = 'is_' . $type;
if(!$fn($value)) {
return false;
} else {
return true;
}
}
} else {
if(!$value instanceof $type) {
return false;
} else {
return true;
}
}
}
/**
* Returns a multi-dimensional array that describes the inputs
* of an assertive template. Data available:
* array[$inputName]['required'] = boolean
* array[$inputName]['type'] = string type representation
* array[$inputName]['default'] = string default value
*
* @param string Part name relative to AssertiveTemplate's paths.
* @param string The class name to look for, i.e. for Part::input 'Part', Layout::input 'Layout'
* @returns array Representation of required inputs.
*/
public static function getInputs($template, $class = 'AssertiveTemplate') {
if(!isset(self::$loaded[$template])) {
$cacheKey = 'AssertiveTemplate::inputs::' . $template;
if(($inputs = Cache::get($cacheKey)) !== false) {
self::$loaded[$template] = $inputs;
} else {
if(self::$paths === false) {
self::init();
}
$templateFile = self::$paths->find($template);
if($templateFile === false) {
throw new RecessFrameworkException("The file \"$template\" does not exist.", 1);
}
$file = file_get_contents($templateFile);
$pattern = self::getInputRegex($class);
preg_match_all($pattern, $file, $matches);
$inputs = array();
foreach($matches[0] as $key => $value) {
$input = array();
$name = $matches[1][$key];
$input['type'] = $matches[2][$key];
$input['required'] = !isset($matches[3][$key]) || $matches[3][$key] === '';
if(!$input['required']) {
$input['default'] = $matches[3][$key];
} else {
$input['default'] = null;
}
$inputs[$name] = $input;
}
self::$loaded[$template] = $inputs;
Cache::set($cacheKey, $inputs);
}
}
return self::$loaded[$template];
}
/**
* Returns the regex to extract all inputs from a file.
* @param string The class name to search for.
* @return string The regex.
*/
private static function getInputRegex($class) {
$ws = '(?:\s*)';
$openParen = '\(';
$closeParen = '\)';
$identifier = '[a-zA-Z_][a-zA-Z_0-9]*';
$quote = '["\']';
$classInput = "$class$ws::$ws" . "input$ws";
$dollar = '\$';
$pattern = "/$classInput$openParen$ws$dollar($identifier)$ws,$ws$quote($identifier)$quote$ws(?:,$ws(.*)|$ws)?$closeParen$ws;/";
return $pattern;
}
}
?>