// /** * Stores tal:repeat information during template execution. * * An instance of this class is created and stored into PHPTAL context on each * tal:repeat usage. * * repeat/item/index * repeat/item/number * ... * are provided by this instance. * * 'repeat' is an StdClass instance created to handle RepeatControllers, * 'item' is an instance of this class. * * @package phptal * @author Laurent Bedubourg */ class PHPTAL_RepeatController implements Iterator { private $key; private $current; private $valid; private $validOnNext; protected $iterator; protected $index; protected $end; protected $length; /** * Construct a new RepeatController. * * @param $source array, string, iterator, iterable. */ public function __construct($source) { if ( is_string($source) ) { $this->iterator = new ArrayIterator( str_split($source) ); // FIXME: invalid for UTF-8 encoding, use preg_match_all('/./u') trick } else if ( is_array($source) ) { $this->iterator = new ArrayIterator($source); } else if ( $source instanceof IteratorAggregate ) { $this->iterator = $source->getIterator(); } else if ( $source instanceof Iterator ) { $this->iterator = $source; } else if ( $source instanceof SimpleXMLElement) { // has non-unique keys! $array = array(); foreach ( $source as $v ) { $array[] = $v; } $this->iterator = new ArrayIterator($array); } else if ( $source instanceof Traversable || $source instanceof DOMNodeList ) { // PDO Statements for example implement the engine internal Traversable // interface. To make it fully iterable we traverse the set to populate // an array which will be actually used for iteration. $array = array(); foreach ( $source as $k=>$v ) { $array[$k] = $v; } $this->iterator = new ArrayIterator($array); } else { $this->iterator = new ArrayIterator( array() ); } // Try to find the set length $this->length = 0; if ( $this->iterator instanceof Countable ) { $this->length = count($this->iterator); } else if ( is_object($this->iterator) ) { // This should be removed since there is already the Countable interface in PHP5 if ( method_exists( $this->iterator, 'size' ) ) { $this->length = $this->iterator->size(); } else if ( method_exists( $this->iterator, 'length' ) ) { $this->length = $this->iterator->length(); } } $this->groups = new PHPTAL_RepeatController_Groups(); $this->rewind(); } /** * Returns the current element value in the iteration * * @return Mixed The current element value */ public function current() { return $this->current; } /** * Returns the current element key in the iteration * * @return String/Int The current element key */ public function key() { return $this->key; } /** * Tells if the iteration is over * * @return bool True if the iteration is not finished yet */ public function valid() { $valid = $this->valid || $this->validOnNext; $this->validOnNext = $this->valid; return $valid; } /** * Restarts the iteration process going back to the first element * */ public function rewind() { $this->index = 0; $this->end = false; $this->iterator->rewind(); // Prefetch the next element if ( $this->iterator->valid() ) { $this->validOnNext = true; $this->prefetch(); } else { $this->validOnNext = false; } // Notify the grouping helper of the change $this->groups->reset(); } /** * Fetches the next element in the iteration and advances the pointer * */ public function next() { $this->index++; // Prefetch the next element $this->prefetch(); // Notify the grouping helper of the change $this->groups->reset(); } /** * Gets an object property * * @return $var Mixed The variable value */ public function __get( $var ) { switch ( $var ) { case 'index': case 'end': case 'length': return $this->$var; case 'number': return $this->index + 1; case 'start': return $this->index === 0; case 'even': return ($this->index % 2) === 0; case 'odd': return ($this->index % 2) === 1; case 'key': return $this->key(); case 'letter': return strtolower( $this->int2letter($this->index+1) ); case 'Letter': return strtoupper( $this->int2letter($this->index+1) ); case 'roman': return strtolower( $this->int2roman($this->index+1) ); case 'Roman': return strtoupper( $this->int2roman($this->index+1) ); case 'first': // Compare the current one with the previous in the dictionary $res = $this->groups->first( $this->current ); return is_bool($res) ? $res : $this->groups; case 'last': // Compare the next one with the dictionary $res = $this->groups->last( $this->iterator->current() ); return is_bool($res) ? $res : $this->groups; default: throw new PHPTAL_Exception( "Unable to find part '$var' in repeater controller" ); } } /** * Fetches the next element from the source data store and * updates the end flag if needed. * * @access protected */ protected function prefetch() { $this->valid = true; $this->key = $this->iterator->key(); $this->current = $this->iterator->current(); $this->iterator->next(); if ( !$this->iterator->valid() ) { $this->valid = false; $this->end = true; } } /** * Converts an integer number (1 based) to a sequence of letters * * @param $int Int The number to convert * @return String The letters equivalent as a, b, c-z ... aa, ab, ac-zz ... * @access protected */ protected function int2letter( $int ) { $lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; $size = strlen($lookup); $letters = ''; while ( $int > 0 ) { $int--; $letters = $lookup[$int % $size] . $letters; $int = floor($int / $size); } return $letters; } /** * Converts an integer number (1 based) to a roman numeral * * @param $int Int The number to convert * @return String The roman numeral * @access protected */ protected function int2roman( $int ) { $lookup = array( '1000' => 'M', '900' => 'CM', '500' => 'D', '400' => 'CD', '100' => 'C', '90' => 'XC', '50' => 'L', '40' => 'XL', '10' => 'X', '9' => 'IX', '5' => 'V', '4' => 'IV', '1' => 'I', ); $roman = ''; foreach ( $lookup as $max => $letters ) { while ( $int >= $max ) { $roman .= $letters; $int -= $max; } } return $roman; } } /** * Keeps track of variable contents when using grouping in a path (first/ and last/) * * @package phptal * @author Iván Montes */ class PHPTAL_RepeatController_Groups { protected $dict = array(); protected $cache = array(); protected $data = null; protected $vars = array(); protected $branch; public function __construct() { $this->dict = array(); $this->reset(); } /** * Resets the result caches. Use it to signal an iteration in the loop * */ public function reset() { $this->cache = array(); } /** * Checks if the data passed is the first one in a group * * @param $data Mixed The data to evaluate * @return Mixed True if the first item in the group, false if not and * this same object if the path is not finished */ public function first( $data ) { if ( !is_array($data) && !is_object($data) && !is_null($data) ) { if ( !isset($this->cache['F']) ) { $hash = md5($data); if ( !isset($this->dict['F']) || $this->dict['F'] !== $hash ) { $this->dict['F'] = $hash; $res = true; } else { $res = false; } $this->cache['F'] = $res; } return $this->cache['F']; } $this->data = $data; $this->branch = 'F'; $this->vars = array(); return $this; } /** * Checks if the data passed is the last one in a group * * @param $data Mixed The data to evaluate * @return Mixed True if the last item in the group, false if not and * this same object if the path is not finished */ public function last( $data ) { if ( !is_array($data) && !is_object($data) && !is_null($data) ) { if ( !isset($this->cache['L']) ) { $hash = md5($data); if (empty($this->dict['L'])) { $this->dict['L'] = $hash; $res = false; } else if ( $this->dict['L'] !== $hash ) { $this->dict['L'] = $hash; $res = true; } else { $res = false; } $this->cache['L'] = $res; } return $this->cache['L']; } $this->data = $data; $this->branch = 'L'; $this->vars = array(); return $this; } /** * Handles variable accesses for the tal path resolver * * @param $var String The variable name to check * @return Mixed An object/array if the path is not over or a boolean * * @todo replace the phptal_path() with custom code */ public function __get( $var ) { // When the iterator item is empty we just let the tal // expression consume by continuously returning this // same object which should evaluate to true for 'last' if ( is_null($this->data) ) { return $this; } // Find the requested variable $value = @phptal_path( $this->data, $var, true ); // Check if it's an object or an array if ( is_array($value) || is_object($value) ) { // Move the context to the requested variable and return $this->data = $value; $this->addVarName( $var ); return $this; } // get a hash of the variable contents $hash = md5( $value ); // compute a path for the variable to use as dictionary key $path = $this->branch . $this->getVarPath() . $var; // If we don't know about this var store in the dictionary if ( !isset($this->cache[$path]) ) { if ( !isset($this->dict[$path]) ) { $this->dict[$path] = $hash; $res = $this->branch === 'F'; } else { // Check if the value has changed if ( $this->dict[$path] !== $hash ) { $this->dict[$path] = $hash; $res = true; } else { $res = false; } } $this->cache[$path] = $res; } return $this->cache[$path]; } /** * Adds a variable name to the current path of variables * * @param $varname String The variable name to store as a path part * @access protected */ protected function addVarName( $varname ) { $this->vars[] = $varname; } /** * Returns the current variable path separated by a slash * * @return String The current variable path * @access protected */ protected function getVarPath() { return implode('/', $this->vars) . '/'; } }