1<?php
2
3/*
4 * This file is part of Twig.
5 *
6 * (c) Fabien Potencier
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Twig\Node\Expression;
13
14use Twig\Compiler;
15use Twig\Error\SyntaxError;
16use Twig\Node\CoercesChildrenToStringInterface;
17use Twig\Node\Expression\Unary\SpreadUnary;
18use Twig\Node\Expression\Unary\StringCastUnary;
19
20class ArrayExpression extends AbstractExpression implements SupportDefinedTestInterface, ReturnArrayInterface, CoercesChildrenToStringInterface
21{
22    use SupportDefinedTestTrait;
23
24    private $index;
25
26    public function __construct(array $elements, int $lineno)
27    {
28        parent::__construct($elements, [], $lineno);
29
30        $this->index = -1;
31        foreach ($this->getKeyValuePairs() as $pair) {
32            if ($pair['key'] instanceof ConstantExpression && ctype_digit((string) $pair['key']->getAttribute('value')) && $pair['key']->getAttribute('value') > $this->index) {
33                $this->index = $pair['key']->getAttribute('value');
34            }
35        }
36    }
37
38    public function getKeyValuePairs(): array
39    {
40        $pairs = [];
41        foreach (array_chunk($this->nodes, 2) as $pair) {
42            $pairs[] = [
43                'key' => $pair[0],
44                'value' => $pair[1],
45            ];
46        }
47
48        return $pairs;
49    }
50
51    public function hasElement(AbstractExpression $key): bool
52    {
53        foreach ($this->getKeyValuePairs() as $pair) {
54            // we compare the string representation of the keys
55            // to avoid comparing the line numbers which are not relevant here.
56            if ((string) $key === (string) $pair['key']) {
57                return true;
58            }
59        }
60
61        return false;
62    }
63
64    /**
65     * Checks if the array is a sequence (keys are sequential integers starting from 0).
66     *
67     * @internal
68     */
69    public function isSequence(): bool
70    {
71        foreach ($this->getKeyValuePairs() as $i => $pair) {
72            $key = $pair['key'];
73            if ($key instanceof TempNameExpression) {
74                $keyValue = $key->getAttribute('name');
75            } elseif ($key instanceof ConstantExpression) {
76                $keyValue = $key->getAttribute('value');
77            } else {
78                return false;
79            }
80
81            if ($keyValue !== $i) {
82                return false;
83            }
84        }
85
86        return true;
87    }
88
89    public function addElement(AbstractExpression $value, ?AbstractExpression $key = null): void
90    {
91        if (null === $key) {
92            $key = new ConstantExpression(++$this->index, $value->getTemplateLine());
93        }
94
95        array_push($this->nodes, $key, $value);
96    }
97
98    public function getStringCoercedChildNames(): array
99    {
100        // dynamic mapping keys (computed at runtime) are coerced to string;
101        // static keys (constants or sequence indexes) are emitted as PHP
102        // literals by compile() and never trigger a __toString() call
103        $names = [];
104        foreach (array_chunk($this->nodes, 2) as $i => $pair) {
105            $key = $pair[0];
106            if ($key instanceof ConstantExpression || $key instanceof TempNameExpression) {
107                continue;
108            }
109
110            $names[] = (string) ($i * 2);
111        }
112
113        return $names;
114    }
115
116    public function compile(Compiler $compiler): void
117    {
118        if ($this->definedTest) {
119            $compiler->repr(true);
120
121            return;
122        }
123
124        // Check for empty expressions which are only allowed in destructuring
125        foreach ($this->getKeyValuePairs() as $pair) {
126            if ($pair['value'] instanceof EmptyExpression) {
127                throw new SyntaxError('Empty array elements are only allowed in destructuring assignments.', $pair['value']->getTemplateLine(), $this->getSourceContext());
128            }
129        }
130
131        $compiler->raw('[');
132        $isSequence = true;
133        foreach ($this->getKeyValuePairs() as $i => $pair) {
134            if (0 !== $i) {
135                $compiler->raw(', ');
136            }
137
138            $key = null;
139            if ($pair['key'] instanceof TempNameExpression) {
140                $key = $pair['key']->getAttribute('name');
141                $pair['key'] = new ConstantExpression($key, $pair['key']->getTemplateLine());
142            } elseif ($pair['key'] instanceof ConstantExpression) {
143                $key = $pair['key']->getAttribute('value');
144            } else {
145                // dynamic key: cast to string so PHP accepts it as an array offset
146                // (the sandbox visitor has already wrapped it with a __toString policy check)
147                $pair['key'] = new StringCastUnary($pair['key'], $pair['key']->getTemplateLine());
148            }
149
150            if ($key !== $i) {
151                $isSequence = false;
152            }
153
154            if (!$isSequence && !$pair['value'] instanceof SpreadUnary) {
155                $compiler
156                    ->subcompile($pair['key'])
157                    ->raw(' => ')
158                ;
159            }
160
161            $compiler->subcompile($pair['value']);
162        }
163        $compiler->raw(']');
164    }
165}
166