Passed
Push — develop ( 067c9c...bf1d25 )
by nguereza
04:45
created

Context::merge()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 1
b 0
f 0
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   http://www.iacademy.cf
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)
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, $value, array $args = [])
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)
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, $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|null
262
     */
263
    public function getRegister(string $name)
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, $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|null
299
     */
300
    public function getEnvironment(string $name)
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, $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|null
340
     */
341
    protected function resolve(string $key)
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|null
379
     */
380
    protected function fetch(string $key)
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|null
405
     */
406
    protected function variable(string $key)
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 (is_string($index) && strlen($index) > 0) {
415
                $key = (string) preg_replace('|\[([0-9a-z._]+)\]|', ".$index", $key);
416
            }
417
        }
418
419
        $parts = explode(Token::VARIABLE_ATTR_SEPARATOR, $key);
420
421
        if ($parts !== false) {
422
            $object = $this->fetch((string) array_shift($parts));
423
            while (count($parts) > 0) {
424
                // since we still have a part to consider
425
                // and since we can't dig deeper into plain values
426
                // it can be thought as if it has a property with a null value
427
                if (
428
                    !is_object($object)
429
                    && !is_array($object)
430
                    && !is_string($object)
431
                ) {
432
                    return null;
433
                }
434
435
                // first try to cast an object to an array or value
436
                if (is_object($object)) {
437
                    if (method_exists($object, 'toObject')) {
438
                        $object = $object->toObject();
439
                    } elseif (method_exists($object, 'toArray')) {
440
                        $object = $object->toArray();
441
                    }
442
                }
443
444
                if (is_null($object)) {
445
                    return null;
446
                }
447
448
                if ($object instanceof Drop) {
449
                    $object->setContext($this);
450
                }
451
452
                $nextPartName = (string) array_shift($parts);
453
                if (is_string($object)) {
454
                    if ($nextPartName === 'size') {
455
                        return strlen($object);
456
                    }
457
458
                    return null;
459
                }
460
461
462
                if (is_array($object)) {
463
                    if (
464
                        $nextPartName === 'first'
465
                        && count($parts) === 0
466
                        && !array_key_exists('first', $object)
467
                    ) {
468
                        return ArrayFilter::first($object);
469
                    }
470
471
                    if (
472
                        $nextPartName === 'last'
473
                        && count($parts) === 0
474
                        && !array_key_exists('last', $object)
475
                    ) {
476
                        return ArrayFilter::last($object);
477
                    }
478
479
                    if (
480
                        $nextPartName === 'size'
481
                        && count($parts) === 0
482
                        && !array_key_exists('size', $object)
483
                    ) {
484
                        return count($object);
485
                    }
486
487
                    // no key - no value
488
                    if (!array_key_exists($nextPartName, $object)) {
489
                        return null;
490
                    }
491
492
                    $object = $object[$nextPartName];
493
                    continue;
494
                }
495
496
                if (!is_object($object)) {
497
                    // we got plain value, yet asked to resolve a part
498
                    // think plain values have a null part with any name
499
                    return null;
500
                }
501
502
                if ($object instanceof Countable) {
503
                    if (
504
                        $nextPartName === 'size'
505
                        && count($parts) === 0
506
                    ) {
507
                        return count($object);
508
                    }
509
                }
510
511
                if ($object instanceof Drop) {
512
                    if (!$object->hasKey($nextPartName)) {
513
                        return null;
514
                    }
515
516
                    $object = $object->invokeDrop($nextPartName);
517
                    continue;
518
                }
519
520
                // if it's just a regular object, attempt to access a public method
521
                $objectCallable = [$object, $nextPartName];
522
                if (is_callable($objectCallable)) {
523
                    $object = call_user_func($objectCallable);
524
                    continue;
525
                }
526
527
                // if a magic accessor method present...
528
                if (is_object($object) && method_exists($object, '__get')) {
529
                    $object = $object->{$nextPartName};
530
                    continue;
531
                }
532
533
                // Inexistent property is a null, PHP-speak
534
                if (!property_exists($object, $nextPartName)) {
535
                    return null;
536
                }
537
538
                // then try a property (independent of accessibility)
539
                if (property_exists($object, $nextPartName)) {
540
                    $object = $object->{$nextPartName};
541
                    continue;
542
                }
543
                // we'll try casting this object in the next iteration
544
            }
545
546
            // lastly, try to get an embedded value of an object
547
            // value could be of any type, not just string, so we have to do this
548
            // conversion here, not later in AbstractBlock::renderAll
549
            if (is_object($object) && method_exists($object, 'toObject')) {
550
                $object = $object->toObject();
551
            }
552
553
            /*
554
            * Before here were checks for object types and object to string conversion.
555
            *
556
            * Now we just return what we have:
557
            * - Traversable objects are taken care of inside filters
558
            * - Object-to-string conversion is handled at the last moment
559
             * in AbstractCondition::stringValue, and in AbstractBlock::renderAll
560
            *
561
            * This way complex objects could be passed between templates and to filters
562
            */
563
            return $object;
564
        }
565
    }
566
}
567