1<?php 2/** 3 * PHPTAL templating engine 4 * 5 * PHP Version 5 6 * 7 * @category HTML 8 * @package PHPTAL 9 * @author Laurent Bedubourg <lbedubourg@motion-twin.com> 10 * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk> 11 * @author Iván Montes <drslump@pollinimini.net> 12 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License 13 * @version SVN: $Id$ 14 * @link http://phptal.org/ 15 */ 16 17/** 18 * Stores tal:repeat information during template execution. 19 * 20 * An instance of this class is created and stored into PHPTAL context on each 21 * tal:repeat usage. 22 * 23 * repeat/item/index 24 * repeat/item/number 25 * ... 26 * are provided by this instance. 27 * 28 * 'repeat' is an stdClass instance created to handle RepeatControllers, 29 * 'item' is an instance of this class. 30 * 31 * @package PHPTAL 32 * @subpackage Php 33 * @author Laurent Bedubourg <lbedubourg@motion-twin.com> 34 */ 35class PHPTAL_RepeatController implements Iterator 36{ 37 public $key; 38 private $current; 39 private $valid; 40 private $validOnNext; 41 42 private $uses_groups = false; 43 44 protected $iterator; 45 public $index; 46 public $end; 47 48 /** 49 * computed lazily 50 */ 51 private $length = null; 52 53 /** 54 * Construct a new RepeatController. 55 * 56 * @param $source array, string, iterator, iterable. 57 */ 58 public function __construct($source) 59 { 60 if ( is_string($source) ) { 61 $this->iterator = new ArrayIterator( str_split($source) ); // FIXME: invalid for UTF-8 encoding, use preg_match_all('/./u') trick 62 } elseif ( is_array($source) ) { 63 $this->iterator = new ArrayIterator($source); 64 } elseif ($source instanceof IteratorAggregate) { 65 $this->iterator = $source->getIterator(); 66 } elseif ($source instanceof DOMNodeList) { 67 $array = array(); 68 foreach ($source as $k=>$v) { 69 $array[$k] = $v; 70 } 71 $this->iterator = new ArrayIterator($array); 72 } elseif ($source instanceof Iterator) { 73 $this->iterator = $source; 74 } elseif ($source instanceof Traversable) { 75 $this->iterator = new IteratorIterator($source); 76 } elseif ($source instanceof Closure) { 77 $this->iterator = new ArrayIterator( (array) $source() ); 78 } elseif ($source instanceof stdClass) { 79 $this->iterator = new ArrayIterator( (array) $source ); 80 } else { 81 $this->iterator = new ArrayIterator( array() ); 82 } 83 } 84 85 /** 86 * Returns the current element value in the iteration 87 * 88 * @return Mixed The current element value 89 */ 90 public function current() 91 { 92 return $this->current; 93 } 94 95 /** 96 * Returns the current element key in the iteration 97 * 98 * @return String/Int The current element key 99 */ 100 public function key() 101 { 102 return $this->key; 103 } 104 105 /** 106 * Tells if the iteration is over 107 * 108 * @return bool True if the iteration is not finished yet 109 */ 110 public function valid() 111 { 112 $valid = $this->valid || $this->validOnNext; 113 $this->validOnNext = $this->valid; 114 115 return $valid; 116 } 117 118 public function length() 119 { 120 if ($this->length === null) { 121 if ($this->iterator instanceof Countable) { 122 return $this->length = count($this->iterator); 123 } elseif ( is_object($this->iterator) ) { 124 // for backwards compatibility with existing PHPTAL templates 125 if ( method_exists($this->iterator, 'size') ) { 126 return $this->length = $this->iterator->size(); 127 } elseif ( method_exists($this->iterator, 'length') ) { 128 return $this->length = $this->iterator->length(); 129 } 130 } 131 $this->length = '_PHPTAL_LENGTH_UNKNOWN_'; 132 } 133 134 if ($this->length === '_PHPTAL_LENGTH_UNKNOWN_') // return length if end is discovered 135 { 136 return $this->end ? $this->index + 1 : null; 137 } 138 return $this->length; 139 } 140 141 /** 142 * Restarts the iteration process going back to the first element 143 * 144 */ 145 public function rewind() 146 { 147 $this->index = 0; 148 $this->length = null; 149 $this->end = false; 150 151 $this->iterator->rewind(); 152 153 // Prefetch the next element 154 if ($this->iterator->valid()) { 155 $this->validOnNext = true; 156 $this->prefetch(); 157 } else { 158 $this->validOnNext = false; 159 } 160 161 if ($this->uses_groups) { 162 // Notify the grouping helper of the change 163 $this->groups->reset(); 164 } 165 } 166 167 /** 168 * Fetches the next element in the iteration and advances the pointer 169 * 170 */ 171 public function next() 172 { 173 $this->index++; 174 175 // Prefetch the next element 176 if ($this->validOnNext) $this->prefetch(); 177 178 if ($this->uses_groups) { 179 // Notify the grouping helper of the change 180 $this->groups->reset(); 181 } 182 } 183 184 /** 185 * Ensures that $this->groups works. 186 * 187 * Groups are rarely-used feature, which is why they're lazily loaded. 188 */ 189 private function initializeGroups() 190 { 191 if (!$this->uses_groups) { 192 $this->groups = new PHPTAL_RepeatControllerGroups(); 193 $this->uses_groups = true; 194 } 195 } 196 197 /** 198 * Gets an object property 199 * 200 * @return $var Mixed The variable value 201 */ 202 public function __get($var) 203 { 204 switch ($var) { 205 case 'number': 206 return $this->index + 1; 207 case 'start': 208 return $this->index === 0; 209 case 'even': 210 return ($this->index % 2) === 0; 211 case 'odd': 212 return ($this->index % 2) === 1; 213 case 'length': 214 return $this->length(); 215 case 'letter': 216 return strtolower( $this->int2letter($this->index+1) ); 217 case 'Letter': 218 return strtoupper( $this->int2letter($this->index+1) ); 219 case 'roman': 220 return strtolower( $this->int2roman($this->index+1) ); 221 case 'Roman': 222 return strtoupper( $this->int2roman($this->index+1) ); 223 224 case 'groups': 225 $this->initializeGroups(); 226 return $this->groups; 227 228 case 'first': 229 $this->initializeGroups(); 230 // Compare the current one with the previous in the dictionary 231 $res = $this->groups->first($this->current); 232 return is_bool($res) ? $res : $this->groups; 233 234 case 'last': 235 $this->initializeGroups(); 236 // Compare the next one with the dictionary 237 $res = $this->groups->last( $this->iterator->current() ); 238 return is_bool($res) ? $res : $this->groups; 239 240 default: 241 throw new PHPTAL_VariableNotFoundException("Unable to find part '$var' in repeat variable"); 242 } 243 } 244 245 /** 246 * Fetches the next element from the source data store and 247 * updates the end flag if needed. 248 * 249 * @access protected 250 */ 251 protected function prefetch() 252 { 253 $this->valid = true; 254 $this->current = $this->iterator->current(); 255 $this->key = $this->iterator->key(); 256 257 $this->iterator->next(); 258 if ( !$this->iterator->valid() ) { 259 $this->valid = false; 260 $this->end = true; 261 } 262 } 263 264 /** 265 * Converts an integer number (1 based) to a sequence of letters 266 * 267 * @param int $int The number to convert 268 * 269 * @return String The letters equivalent as a, b, c-z ... aa, ab, ac-zz ... 270 * @access protected 271 */ 272 protected function int2letter($int) 273 { 274 $lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 275 $size = strlen($lookup); 276 277 $letters = ''; 278 while ($int > 0) { 279 $int--; 280 $letters = $lookup[$int % $size] . $letters; 281 $int = floor($int / $size); 282 } 283 return $letters; 284 } 285 286 /** 287 * Converts an integer number (1 based) to a roman numeral 288 * 289 * @param int $int The number to convert 290 * 291 * @return String The roman numeral 292 * @access protected 293 */ 294 protected function int2roman($int) 295 { 296 $lookup = array( 297 '1000' => 'M', 298 '900' => 'CM', 299 '500' => 'D', 300 '400' => 'CD', 301 '100' => 'C', 302 '90' => 'XC', 303 '50' => 'L', 304 '40' => 'XL', 305 '10' => 'X', 306 '9' => 'IX', 307 '5' => 'V', 308 '4' => 'IV', 309 '1' => 'I', 310 ); 311 312 $roman = ''; 313 foreach ($lookup as $max => $letters) { 314 while ($int >= $max) { 315 $roman .= $letters; 316 $int -= $max; 317 } 318 } 319 320 return $roman; 321 } 322} 323 324