Issues (14)

src/Parser/Context.php (1 issue)

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 Context.php
36
 *
37
 *  The template Context class
38
 *
39
 *  @package    Platine\Template\Parser
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\Parser;
51
52
use Countable;
53
use Platine\Template\Exception\TemplateException;
54
use Platine\Template\Filter\ArrayFilter;
55
use TypeError;
56
57
/**
58
 * @class Context
59
 * @package Platine\Template\Parser
60
 */
61
class Context
62
{
63
    /**
64
     * The local scopes
65
     * @var array<int, array<string, mixed>>
66
     */
67
    protected array $assigns = [];
68
69
    /**
70
     * Registers for non-variable state data
71
     * @var array<string, mixed>
72
     */
73
    protected array $registers = [];
74
75
    /**
76
     * Global scopes
77
     * @var array<string, mixed>
78
     */
79
    protected array $environments = [];
80
81
    /**
82
     * The filter collection instance
83
     * @var FilterCollection
84
     */
85
    protected FilterCollection $filters;
86
87
    /**
88
     * Called "sometimes" while rendering.
89
     * For example to abort the execution of a rendering.
90
     * @var callable|null
91
     */
92
    protected $tickCallback = null;
93
94
    /**
95
     * The parser instance
96
     * @var Parser
97
     */
98
    protected Parser $parser;
99
100
101
    /**
102
     * Create new instance
103
     * @param array<string, mixed> $assigns
104
     * @param array<string, mixed> $registers
105
     */
106
    public function __construct(array $assigns = [], array $registers = [])
107
    {
108
        $this->assigns = [$assigns];
109
        $this->registers = $registers;
110
        $this->filters = new FilterCollection();
111
    }
112
113
    /**
114
     * Set the parser instance
115
     * @param Parser $parser
116
     * @return $this
117
     */
118
    public function setParser(Parser $parser): self
119
    {
120
        $this->parser = $parser;
121
122
        return $this;
123
    }
124
125
126
    /**
127
     * Set tick callback
128
     * @param callable|null $tickCallback
129
     * @return $this
130
     */
131
    public function setTickCallback(?callable $tickCallback): self
132
    {
133
        $this->tickCallback = $tickCallback;
134
135
        return $this;
136
    }
137
138
    /**
139
     * Add filter
140
     * @param class-string $filter
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
141
     * @return $this
142
     */
143
    public function addFilter(string $filter): self
144
    {
145
        $this->filters->addFilter($filter);
146
147
        return $this;
148
    }
149
150
    /**
151
     * Invokes the filter with the given name
152
     * @param string $name
153
     * @param mixed $value
154
     * @param array<int, mixed> $args
155
     * @return mixed
156
     */
157
    public function invokeFilter(string $name, mixed $value, array $args = []): mixed
158
    {
159
        try {
160
            return $this->filters->invoke($name, $value, $args);
161
        } catch (TypeError $ex) {
162
            throw new TemplateException($ex->getMessage(), 0, $ex);
163
        }
164
    }
165
166
    /**
167
     * Merges the given assigns into the current assigns
168
     * @param array<string, mixed> $assigns
169
     * @return void
170
     */
171
    public function merge(array $assigns): void
172
    {
173
        $this->assigns[0] = array_merge($this->assigns[0], $assigns);
174
    }
175
176
    /**
177
     * Push new local scope on the stack.
178
     * @return bool
179
     */
180
    public function push(): bool
181
    {
182
        array_unshift($this->assigns, []);
183
184
        return true;
185
    }
186
187
    public function pop(): bool
188
    {
189
        if (count($this->assigns) === 1) {
190
            throw new TemplateException('No elements to retrieve (pop) from context');
191
        }
192
        array_shift($this->assigns);
193
194
        return true;
195
    }
196
197
    /**
198
     * Return the context value for the given key
199
     * @param string $key
200
     * @return mixed
201
     */
202
    public function get(string $key): mixed
203
    {
204
        return $this->resolve($key);
205
    }
206
207
    /**
208
     * Set the value for the given key
209
     * @param string $key
210
     * @param mixed $value
211
     * @param bool $global
212
     * @return void
213
     */
214
    public function set(string $key, mixed $value, bool $global = false): void
215
    {
216
        if ($global) {
217
            $count = count($this->assigns);
218
            for ($i = 0; $i < $count; $i++) {
219
                $this->assigns[$i][$key] = $value;
220
            }
221
        } else {
222
            $this->assigns[0][$key] = $value;
223
        }
224
    }
225
226
    /**
227
     * Returns true if the given key will properly resolve
228
     * @param string $key
229
     * @return bool
230
     */
231
    public function hasKey(string $key): bool
232
    {
233
        return $this->resolve($key) !== null;
234
    }
235
236
    /**
237
     * Check whether the given register exists
238
     * @param string $name
239
     * @return bool
240
     */
241
    public function hasRegister(string $name): bool
242
    {
243
        return isset($this->registers[$name]);
244
    }
245
246
    /**
247
     * Clear the given register
248
     * @param string $name
249
     * @return $this
250
     */
251
    public function clearRegister(string $name): self
252
    {
253
        unset($this->registers[$name]);
254
255
        return $this;
256
    }
257
258
    /**
259
     * Return the value for the given register
260
     * @param string $name
261
     * @return mixed
262
     */
263
    public function getRegister(string $name): mixed
264
    {
265
        if ($this->hasRegister($name)) {
266
            return $this->registers[$name];
267
        }
268
269
        return null;
270
    }
271
272
    /**
273
     * Set the register value
274
     * @param string $name
275
     * @param mixed $value
276
     * @return $this
277
     */
278
    public function setRegister(string $name, mixed $value): self
279
    {
280
        $this->registers[$name] = $value;
281
282
        return $this;
283
    }
284
285
    /**
286
     * Check whether the given environment exists
287
     * @param string $name
288
     * @return bool
289
     */
290
    public function hasEnvironment(string $name): bool
291
    {
292
        return isset($this->environments[$name]);
293
    }
294
295
    /**
296
     * Return the value for the given environment
297
     * @param string $name
298
     * @return mixed
299
     */
300
    public function getEnvironment(string $name): mixed
301
    {
302
        if ($this->hasEnvironment($name)) {
303
            return $this->environments[$name];
304
        }
305
306
        return null;
307
    }
308
309
    /**
310
     * Set the environment value
311
     * @param string $name
312
     * @param mixed $value
313
     * @return $this
314
     */
315
    public function setEnvironment(string $name, mixed $value): self
316
    {
317
        $this->environments[$name] = $value;
318
319
        return $this;
320
    }
321
322
    /**
323
     * Call the tick callback
324
     * @return void
325
     */
326
    public function tick(): void
327
    {
328
        if ($this->tickCallback === null) {
329
            return;
330
        }
331
332
        ($this->tickCallback)($this);
333
    }
334
335
    /**
336
     * Resolve a key by either returning the appropriate literal
337
     * or by looking up the appropriate variable
338
     * @param string $key
339
     * @return mixed
340
     */
341
    protected function resolve(string $key): mixed
342
    {
343
        if ($key === 'null') {
344
            return null;
345
        }
346
347
        if ($key === 'true') {
348
            return true;
349
        }
350
351
        if ($key === 'false') {
352
            return false;
353
        }
354
355
        $matches = [];
356
        if (preg_match('/^\'(.*)\'$/', $key, $matches)) {
357
            return $matches[1];
358
        }
359
360
        if (preg_match('/^"(.*)"$/', $key, $matches)) {
361
            return $matches[1];
362
        }
363
364
        if (preg_match('/^(-?\d+)$/', $key, $matches)) {
365
            return $matches[1];
366
        }
367
368
        if (preg_match('/^(-?\d[\d\.]+)$/', $key, $matches)) {
369
            return $matches[1];
370
        }
371
372
        return $this->variable($key);
373
    }
374
375
    /**
376
     * Fetches the current key in all the scopes
377
     * @param string $key
378
     * @return mixed
379
     */
380
    protected function fetch(string $key): mixed
381
    {
382
        if (array_key_exists($key, $this->environments)) {
383
            return $this->environments[$key];
384
        }
385
386
        foreach ($this->assigns as $scope) {
387
            if (array_key_exists($key, $scope)) {
388
                $obj = $scope[$key];
389
390
                if ($obj instanceof Drop) {
391
                    $obj->setContext($this);
392
                }
393
394
                return $obj;
395
            }
396
        }
397
398
        return null;
399
    }
400
401
    /**
402
     * Resolved the name spaced queries gracefully.
403
     * @param string $key
404
     * @return mixed
405
     */
406
    protected function variable(string $key): mixed
407
    {
408
        // Support numeric and variable array indicies
409
        $matches = [];
410
        if (preg_match('|\[[0-9]+\]|', $key)) {
411
            $key = (string) preg_replace('|\[([0-9]+)\]|', ".$1", $key);
412
        } elseif (preg_match('|\[[0-9a-z._]+\]|', $key, $matches)) {
413
            $index = $this->get(str_replace(['[', ']'], '', $matches[0]));
414
            if (strlen((string) $index) > 0) {
415
                $key = (string) preg_replace('|\[([0-9a-z._]+)\]|', ".$index", $key);
416
            }
417
        }
418
419
        $parts = explode(Token::VARIABLE_ATTR_SEPARATOR, $key);
420
        if ($parts !== false) {
421
            $object = $this->fetch((string) array_shift($parts));
422
            while (count($parts) > 0) {
423
                // since we still have a part to consider
424
                // and since we can't dig deeper into plain values
425
                // it can be thought as if it has a property with a null value
426
                if (
427
                    is_object($object) === false
428
                    && is_array($object) === false
429
                    && is_string($object) === false
430
                ) {
431
                    return null;
432
                }
433
434
                // first try to cast an object to an array or value
435
                if (is_object($object)) {
436
                    if (method_exists($object, 'toObject')) {
437
                        $object = $object->toObject();
438
                    } elseif (method_exists($object, 'toArray')) {
439
                        $object = $object->toArray();
440
                    }
441
                }
442
443
                if (is_null($object)) {
444
                    return null;
445
                }
446
447
                if ($object instanceof Drop) {
448
                    $object->setContext($this);
449
                }
450
451
                $nextPartName = (string) array_shift($parts);
452
                if (is_string($object)) {
453
                    if ($nextPartName === 'size') {
454
                        return strlen($object);
455
                    }
456
457
                    return null;
458
                }
459
460
                if (is_array($object)) {
461
                    if (
462
                        $nextPartName === 'first'
463
                        && count($parts) === 0
464
                        && !array_key_exists('first', $object)
465
                    ) {
466
                        return ArrayFilter::first($object);
467
                    }
468
469
                    if (
470
                        $nextPartName === 'last'
471
                        && count($parts) === 0
472
                        && !array_key_exists('last', $object)
473
                    ) {
474
                        return ArrayFilter::last($object);
475
                    }
476
477
                    if (
478
                        $nextPartName === 'size'
479
                        && count($parts) === 0
480
                        && !array_key_exists('size', $object)
481
                    ) {
482
                        return count($object);
483
                    }
484
485
                    // no key - no value
486
                    if (!array_key_exists($nextPartName, $object)) {
487
                        return null;
488
                    }
489
490
                    $object = $object[$nextPartName];
491
                    continue;
492
                }
493
494
                if (is_object($object) === false) {
495
                    // we got plain value, yet asked to resolve a part
496
                    // think plain values have a null part with any name
497
                    return null;
498
                }
499
500
                if ($object instanceof Countable) {
501
                    if (
502
                        $nextPartName === 'size'
503
                        && count($parts) === 0
504
                    ) {
505
                        return count($object);
506
                    }
507
                }
508
509
                if ($object instanceof Drop) {
510
                    if ($object->hasKey($nextPartName) === false) {
511
                        return null;
512
                    }
513
514
                    $object = $object->invokeDrop($nextPartName);
515
                    continue;
516
                }
517
518
                // if it's just a regular object, attempt to access a public method
519
                $objectCallable = [$object, $nextPartName];
520
                if (is_callable($objectCallable)) {
521
                    $object = call_user_func($objectCallable);
522
                    continue;
523
                }
524
525
                // if a magic accessor method present...
526
                if (method_exists($object, '__get')) {
527
                    $object = $object->{$nextPartName};
528
                    continue;
529
                }
530
531
                // Inexistent property is a null, PHP-speak
532
                if (property_exists($object, $nextPartName) === false) {
533
                    return null;
534
                }
535
536
                // then try a property (independent of accessibility)
537
                $object = $object->{$nextPartName};
538
                continue;
539
                // we'll try casting this object in the next iteration
540
            }
541
542
            // lastly, try to get an embedded value of an object
543
            // value could be of any type, not just string, so we have to do this
544
            // conversion here, not later in AbstractBlock::renderAll
545
            if (is_object($object) && method_exists($object, 'toObject')) {
546
                $object = $object->toObject();
547
            }
548
549
            /*
550
            * Before here were checks for object types and object to string conversion.
551
            *
552
            * Now we just return what we have:
553
            * - Traversable objects are taken care of inside filters
554
            * - Object-to-string conversion is handled at the last moment
555
             * in AbstractCondition::stringValue, and in AbstractBlock::renderAll
556
            *
557
            * This way complex objects could be passed between templates and to filters
558
            */
559
            return $object;
560
        }
561
562
        return null;
563
    }
564
}
565