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