ForTag   A
last analyzed

Complexity

Total Complexity 31

Size/Duplication

Total Lines 240
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 121
dl 0
loc 240
rs 9.92
c 0
b 0
f 0
wmc 31

4 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 29 3
A render() 0 11 3
B renderDigit() 0 52 6
F renderCollection() 0 80 19
1
<?php
2
3
/**
4
 * Platine Template
5
 *
6
 * Platine Template is a template engine that has taken a lot of inspiration from Django.
7
 *
8
 * This content is released under the MIT License (MIT)
9
 *
10
 * Copyright (c) 2020 Platine Template
11
 * Copyright (c) 2014 Guz Alexander, http://guzalexander.com
12
 * Copyright (c) 2011, 2012 Harald Hanek, http://www.delacap.com
13
 * Copyright (c) 2006 Mateo Murphy
14
 *
15
 * Permission is hereby granted, free of charge, to any person obtaining a copy
16
 * of this software and associated documentation files (the "Software"), to deal
17
 * in the Software without restriction, including without limitation the rights
18
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19
 * copies of the Software, and to permit persons to whom the Software is
20
 * furnished to do so, subject to the following conditions:
21
 *
22
 * The above copyright notice and this permission notice shall be included in all
23
 * copies or substantial portions of the Software.
24
 *
25
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
 * SOFTWARE.
32
 */
33
34
/**
35
 *  @file ForTag.php
36
 *
37
 *  The "for" Template tag class
38
 *
39
 *  @package    Platine\Template\Tag
40
 *  @author Platine Developers Team
41
 *  @copyright  Copyright (c) 2020
42
 *  @license    http://opensource.org/licenses/MIT  MIT License
43
 *  @link   https://www.platine-php.com
44
 *  @version 1.0.0
45
 *  @filesource
46
 */
47
48
declare(strict_types=1);
49
50
namespace Platine\Template\Tag;
51
52
use Generator;
53
use Platine\Template\Exception\ParseException;
54
use Platine\Template\Parser\AbstractBlock;
55
use Platine\Template\Parser\Context;
56
use Platine\Template\Parser\Lexer;
57
use Platine\Template\Parser\Parser;
58
use Platine\Template\Parser\Token;
59
use Traversable;
60
61
/**
62
 * @class ForTag
63
 * @package Platine\Template\Tag
64
 */
65
class ForTag extends AbstractBlock
66
{
67
    /**
68
     * Type digit
69
     */
70
    protected const TYPE_DIGIT = 1;
71
72
    /**
73
     * Type collection
74
     */
75
    protected const TYPE_COLLECTION = 2;
76
77
    /**
78
     * The collection name to loop over
79
     * @var string
80
     */
81
    protected string $collectionName;
82
83
    /**
84
     * The variable name to assign collection elements to
85
     * @var string
86
     */
87
    protected string $variableName;
88
89
    /**
90
     * The name of the loop, which is a
91
     * compound of the collection and variable names
92
     * @var string
93
     */
94
    protected string $name;
95
96
    /**
97
     * The type of the loop (collection or digit)
98
     * @var int
99
     */
100
    protected int $type = self::TYPE_COLLECTION;
101
102
    /**
103
     * The loop start value
104
     * @var int|string
105
     */
106
    protected int|string $start;
107
108
    /**
109
    * {@inheritdoc}
110
    */
111
    public function __construct(string $markup, array &$tokens, Parser $parser)
112
    {
113
        parent::__construct($markup, $tokens, $parser);
114
115
        $lexerCollection = new Lexer('/(\w+)\s+in\s+(' . Token::VARIABLE_NAME . ')/');
116
        if ($lexerCollection->match($markup)) {
117
            $this->variableName = $lexerCollection->getStringMatch(1);
118
            $this->collectionName = $lexerCollection->getStringMatch(2);
119
            $this->name = $this->variableName . '-' . $this->collectionName;
120
            $this->extractAttributes($markup);
121
        } else {
122
            $lexerDigit = new Lexer(
123
                '/(\w+)\s+in\s+\((\d+|'
124
                . Token::VARIABLE_NAME
125
                . ')\s*\.\.\s*(\d+|'
126
                . Token::VARIABLE_NAME
127
                . ')\)/'
128
            );
129
            if ($lexerDigit->match($markup)) {
130
                $this->type = self::TYPE_DIGIT;
131
                $this->variableName = $lexerDigit->getStringMatch(1);
132
                $this->start = $lexerDigit->getStringMatch(2);
133
                $this->collectionName = $lexerDigit->getStringMatch(3);
134
                $this->name = $this->variableName . '-digit';
135
                $this->extractAttributes($markup);
136
            } else {
137
                throw new ParseException(sprintf(
138
                    'Syntax Error in "%s" loop - Valid syntax: for [item] in [collection]',
139
                    'for'
140
                ));
141
            }
142
        }
143
    }
144
145
    /**
146
    * {@inheritdoc}
147
    */
148
    public function render(Context $context): string
149
    {
150
        if (!$context->hasRegister('for')) {
151
            $context->setRegister('for', []);
152
        }
153
154
        if ($this->type === self::TYPE_DIGIT) {
155
            return $this->renderDigit($context);
156
        }
157
158
        return $this->renderCollection($context);
159
    }
160
161
    /**
162
     * Render for type "digit"
163
     * @param Context $context
164
     * @return string
165
     */
166
    protected function renderDigit(Context $context): string
167
    {
168
        $start = $this->start;
169
170
        if (!is_int($start)) {
171
            $start = (int) $context->get($start);
172
        }
173
174
        $end = $this->collectionName;
175
        if (!is_numeric($end)) {
176
            $end = (int) $context->get($end);
177
        } else {
178
            $end = (int) $this->collectionName;
179
        }
180
181
        $range = [$start, $end];
182
183
        $context->push();
184
        $result = '';
185
        /** @var int $index */
186
        $index = 0;
187
        $length = $range[1] - $range[0] + 1;
188
        for ($i = $range[0]; $i <= $range[1]; $i++) {
189
            $context->set($this->variableName, $i);
190
            $context->set('forloop', [
191
                'name' => $this->name,
192
                'length' => $length,
193
                'index' => $index + 1,
194
                'index0' => $index,
195
                'rindex' => $length - $index,
196
                'rindex0' => $length - $index - 1,
197
                'first' => ((int)$index === 0),
198
                'last' => ((int)$index === ($length - 1)),
199
            ]);
200
201
            $result .= $this->renderAll($this->nodeList, $context);
202
203
            $index++;
204
205
            if ($context->hasRegister('break')) {
206
                $context->clearRegister('break');
207
                break;
208
            }
209
210
            if ($context->hasRegister('continue')) {
211
                $context->clearRegister('continue');
212
            }
213
        }
214
215
        $context->pop();
216
217
        return $result;
218
    }
219
220
    /**
221
     * Render for type "collection"
222
     * @param Context $context
223
     * @return string
224
     */
225
    protected function renderCollection(Context $context): string
226
    {
227
        $collection = $context->get($this->collectionName);
228
229
        if ($collection instanceof Generator && !$collection->valid()) {
230
            return '';
231
        }
232
233
        if ($collection instanceof Traversable) {
234
            $collection = iterator_to_array($collection);
235
        }
236
237
        if ($collection === null || !is_array($collection) || count($collection) === 0) {
238
            return '';
239
        }
240
241
        $range = [0, count($collection)];
242
        if (isset($this->attributes['limit']) || isset($this->attributes['offset'])) {
243
            /** @var int $offset */
244
            $offset = 0;
245
            if (isset($this->attributes['offset'])) {
246
                $forRegister = $context->getRegister('for');
247
                $offset =  ($this->attributes['offset'] === 'continue')
248
                          ? (isset($forRegister[$this->name])
249
                                ? (int) $forRegister[$this->name]
250
                                : 0)
251
                          : (int) $context->get($this->attributes['offset']);
252
            }
253
254
            /** @var int|null $limit */
255
            $limit = (isset($this->attributes['limit']))
256
                          ? (int) $context->get($this->attributes['limit'])
257
                          : null;
258
259
            $rangeEnd = ($limit !== null) ? $limit : count($collection) - $offset;
260
            $range = [$offset, $rangeEnd];
261
262
            $context->setRegister('for', [$this->name => $rangeEnd + $offset]);
263
        }
264
265
        $result = '';
266
        $segment = array_slice($collection, $range[0], $range[1]);
267
        if (count($segment) <= 0) {
268
            return '';
269
        }
270
271
        $context->push();
272
        $length = count($segment);
273
        $index = 0;
274
        foreach ($segment as $key => $item) {
275
            $value = is_numeric($key) ? $item : [$key, $item];
276
            $context->set($this->variableName, $value);
277
            $context->set('forloop', [
278
                'name' => $this->name,
279
                'length' => $length,
280
                'index' => $index + 1,
281
                'index0' => $index,
282
                'rindex' => $length - $index,
283
                'rindex0' => $length - $index - 1,
284
                'first' => ((int)$index === 0),
285
                'last' => ((int)$index === ($length - 1)),
286
            ]);
287
288
            $result .= $this->renderAll($this->nodeList, $context);
289
290
            $index++;
291
292
            if ($context->hasRegister('break')) {
293
                $context->clearRegister('break');
294
                break;
295
            }
296
297
            if ($context->hasRegister('continue')) {
298
                $context->clearRegister('continue');
299
            }
300
        }
301
302
        $context->pop();
303
304
        return $result;
305
    }
306
}
307