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\Extension\ExtensionInterface;
17use Twig\Node\Node;
18
19abstract class CallExpression extends AbstractExpression
20{
21    private $reflector;
22
23    protected function compileCallable(Compiler $compiler)
24    {
25        $callable = $this->getAttribute('callable');
26
27        $closingParenthesis = false;
28        $isArray = false;
29        if (\is_string($callable) && false === strpos($callable, '::')) {
30            $compiler->raw($callable);
31        } else {
32            list($r, $callable) = $this->reflectCallable($callable);
33            if ($r instanceof \ReflectionMethod && \is_string($callable[0])) {
34                if ($r->isStatic()) {
35                    $compiler->raw(sprintf('%s::%s', $callable[0], $callable[1]));
36                } else {
37                    $compiler->raw(sprintf('$this->env->getRuntime(\'%s\')->%s', $callable[0], $callable[1]));
38                }
39            } elseif ($r instanceof \ReflectionMethod && $callable[0] instanceof ExtensionInterface) {
40                // For BC/FC with namespaced aliases
41                $class = (new \ReflectionClass(\get_class($callable[0])))->name;
42                if (!$compiler->getEnvironment()->hasExtension($class)) {
43                    // Compile a non-optimized call to trigger a \Twig\Error\RuntimeError, which cannot be a compile-time error
44                    $compiler->raw(sprintf('$this->env->getExtension(\'%s\')', $class));
45                } else {
46                    $compiler->raw(sprintf('$this->extensions[\'%s\']', ltrim($class, '\\')));
47                }
48
49                $compiler->raw(sprintf('->%s', $callable[1]));
50            } else {
51                $closingParenthesis = true;
52                $isArray = true;
53                $compiler->raw(sprintf('call_user_func_array($this->env->get%s(\'%s\')->getCallable(), ', ucfirst($this->getAttribute('type')), $this->getAttribute('name')));
54            }
55        }
56
57        $this->compileArguments($compiler, $isArray);
58
59        if ($closingParenthesis) {
60            $compiler->raw(')');
61        }
62    }
63
64    protected function compileArguments(Compiler $compiler, $isArray = false)
65    {
66        $compiler->raw($isArray ? '[' : '(');
67
68        $first = true;
69
70        if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) {
71            $compiler->raw('$this->env');
72            $first = false;
73        }
74
75        if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) {
76            if (!$first) {
77                $compiler->raw(', ');
78            }
79            $compiler->raw('$context');
80            $first = false;
81        }
82
83        if ($this->hasAttribute('arguments')) {
84            foreach ($this->getAttribute('arguments') as $argument) {
85                if (!$first) {
86                    $compiler->raw(', ');
87                }
88                $compiler->string($argument);
89                $first = false;
90            }
91        }
92
93        if ($this->hasNode('node')) {
94            if (!$first) {
95                $compiler->raw(', ');
96            }
97            $compiler->subcompile($this->getNode('node'));
98            $first = false;
99        }
100
101        if ($this->hasNode('arguments')) {
102            $callable = $this->getAttribute('callable');
103            $arguments = $this->getArguments($callable, $this->getNode('arguments'));
104            foreach ($arguments as $node) {
105                if (!$first) {
106                    $compiler->raw(', ');
107                }
108                $compiler->subcompile($node);
109                $first = false;
110            }
111        }
112
113        $compiler->raw($isArray ? ']' : ')');
114    }
115
116    protected function getArguments($callable, $arguments)
117    {
118        $callType = $this->getAttribute('type');
119        $callName = $this->getAttribute('name');
120
121        $parameters = [];
122        $named = false;
123        foreach ($arguments as $name => $node) {
124            if (!\is_int($name)) {
125                $named = true;
126                $name = $this->normalizeName($name);
127            } elseif ($named) {
128                throw new SyntaxError(sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
129            }
130
131            $parameters[$name] = $node;
132        }
133
134        $isVariadic = $this->hasAttribute('is_variadic') && $this->getAttribute('is_variadic');
135        if (!$named && !$isVariadic) {
136            return $parameters;
137        }
138
139        if (!$callable) {
140            if ($named) {
141                $message = sprintf('Named arguments are not supported for %s "%s".', $callType, $callName);
142            } else {
143                $message = sprintf('Arbitrary positional arguments are not supported for %s "%s".', $callType, $callName);
144            }
145
146            throw new \LogicException($message);
147        }
148
149        list($callableParameters, $isPhpVariadic) = $this->getCallableParameters($callable, $isVariadic);
150        $arguments = [];
151        $names = [];
152        $missingArguments = [];
153        $optionalArguments = [];
154        $pos = 0;
155        foreach ($callableParameters as $callableParameter) {
156            $name = $this->normalizeName($callableParameter->name);
157            if (\PHP_VERSION_ID >= 80000 && 'range' === $callable) {
158                if ('start' === $name) {
159                    $name = 'low';
160                } elseif ('end' === $name) {
161                    $name = 'high';
162                }
163            }
164
165            $names[] = $name;
166
167            if (\array_key_exists($name, $parameters)) {
168                if (\array_key_exists($pos, $parameters)) {
169                    throw new SyntaxError(sprintf('Argument "%s" is defined twice for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
170                }
171
172                if (\count($missingArguments)) {
173                    throw new SyntaxError(sprintf(
174                        'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".',
175                        $name, $callType, $callName, implode(', ', $names), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments)
176                    ), $this->getTemplateLine(), $this->getSourceContext());
177                }
178
179                $arguments = array_merge($arguments, $optionalArguments);
180                $arguments[] = $parameters[$name];
181                unset($parameters[$name]);
182                $optionalArguments = [];
183            } elseif (\array_key_exists($pos, $parameters)) {
184                $arguments = array_merge($arguments, $optionalArguments);
185                $arguments[] = $parameters[$pos];
186                unset($parameters[$pos]);
187                $optionalArguments = [];
188                ++$pos;
189            } elseif ($callableParameter->isDefaultValueAvailable()) {
190                $optionalArguments[] = new ConstantExpression($callableParameter->getDefaultValue(), -1);
191            } elseif ($callableParameter->isOptional()) {
192                if (empty($parameters)) {
193                    break;
194                } else {
195                    $missingArguments[] = $name;
196                }
197            } else {
198                throw new SyntaxError(sprintf('Value for argument "%s" is required for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
199            }
200        }
201
202        if ($isVariadic) {
203            $arbitraryArguments = $isPhpVariadic ? new VariadicExpression([], -1) : new ArrayExpression([], -1);
204            foreach ($parameters as $key => $value) {
205                if (\is_int($key)) {
206                    $arbitraryArguments->addElement($value);
207                } else {
208                    $arbitraryArguments->addElement($value, new ConstantExpression($key, -1));
209                }
210                unset($parameters[$key]);
211            }
212
213            if ($arbitraryArguments->count()) {
214                $arguments = array_merge($arguments, $optionalArguments);
215                $arguments[] = $arbitraryArguments;
216            }
217        }
218
219        if (!empty($parameters)) {
220            $unknownParameter = null;
221            foreach ($parameters as $parameter) {
222                if ($parameter instanceof Node) {
223                    $unknownParameter = $parameter;
224                    break;
225                }
226            }
227
228            throw new SyntaxError(
229                sprintf(
230                    'Unknown argument%s "%s" for %s "%s(%s)".',
231                    \count($parameters) > 1 ? 's' : '', implode('", "', array_keys($parameters)), $callType, $callName, implode(', ', $names)
232                ),
233                $unknownParameter ? $unknownParameter->getTemplateLine() : $this->getTemplateLine(),
234                $unknownParameter ? $unknownParameter->getSourceContext() : $this->getSourceContext()
235            );
236        }
237
238        return $arguments;
239    }
240
241    protected function normalizeName($name)
242    {
243        return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name));
244    }
245
246    private function getCallableParameters($callable, bool $isVariadic): array
247    {
248        list($r) = $this->reflectCallable($callable);
249        if (null === $r) {
250            return [[], false];
251        }
252
253        $parameters = $r->getParameters();
254        if ($this->hasNode('node')) {
255            array_shift($parameters);
256        }
257        if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) {
258            array_shift($parameters);
259        }
260        if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) {
261            array_shift($parameters);
262        }
263        if ($this->hasAttribute('arguments') && null !== $this->getAttribute('arguments')) {
264            foreach ($this->getAttribute('arguments') as $argument) {
265                array_shift($parameters);
266            }
267        }
268        $isPhpVariadic = false;
269        if ($isVariadic) {
270            $argument = end($parameters);
271            $isArray = $argument && $argument->hasType() && 'array' === $argument->getType()->getName();
272            if ($isArray && $argument->isDefaultValueAvailable() && [] === $argument->getDefaultValue()) {
273                array_pop($parameters);
274            } elseif ($argument && $argument->isVariadic()) {
275                array_pop($parameters);
276                $isPhpVariadic = true;
277            } else {
278                $callableName = $r->name;
279                if ($r instanceof \ReflectionMethod) {
280                    $callableName = $r->getDeclaringClass()->name.'::'.$callableName;
281                }
282
283                throw new \LogicException(sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $callableName, $this->getAttribute('type'), $this->getAttribute('name')));
284            }
285        }
286
287        return [$parameters, $isPhpVariadic];
288    }
289
290    private function reflectCallable($callable)
291    {
292        if (null !== $this->reflector) {
293            return $this->reflector;
294        }
295
296        if (\is_array($callable)) {
297            if (!method_exists($callable[0], $callable[1])) {
298                // __call()
299                return [null, []];
300            }
301            $r = new \ReflectionMethod($callable[0], $callable[1]);
302        } elseif (\is_object($callable) && !$callable instanceof \Closure) {
303            $r = new \ReflectionObject($callable);
304            $r = $r->getMethod('__invoke');
305            $callable = [$callable, '__invoke'];
306        } elseif (\is_string($callable) && false !== $pos = strpos($callable, '::')) {
307            $class = substr($callable, 0, $pos);
308            $method = substr($callable, $pos + 2);
309            if (!method_exists($class, $method)) {
310                // __staticCall()
311                return [null, []];
312            }
313            $r = new \ReflectionMethod($callable);
314            $callable = [$class, $method];
315        } else {
316            $r = new \ReflectionFunction($callable);
317        }
318
319        return $this->reflector = [$r, $callable];
320    }
321}
322
323class_alias('Twig\Node\Expression\CallExpression', 'Twig_Node_Expression_Call');
324