1<?php
2
3/**
4 * @see       https://github.com/laminas/laminas-code for the canonical source repository
5 * @copyright https://github.com/laminas/laminas-code/blob/master/COPYRIGHT.md
6 * @license   https://github.com/laminas/laminas-code/blob/master/LICENSE.md New BSD License
7 */
8
9namespace Laminas\Code\Reflection;
10
11use ReflectionFunction;
12
13use function array_shift;
14use function array_slice;
15use function count;
16use function file;
17use function implode;
18use function preg_match;
19use function preg_quote;
20use function preg_replace;
21use function sprintf;
22use function strlen;
23use function strrpos;
24use function substr;
25use function var_export;
26
27class FunctionReflection extends ReflectionFunction implements ReflectionInterface
28{
29    /**
30     * Constant use in @MethodReflection to display prototype as an array
31     */
32    const PROTOTYPE_AS_ARRAY = 'prototype_as_array';
33
34    /**
35     * Constant use in @MethodReflection to display prototype as a string
36     */
37    const PROTOTYPE_AS_STRING = 'prototype_as_string';
38
39    /**
40     * Get function DocBlock
41     *
42     * @throws Exception\InvalidArgumentException
43     * @return DocBlockReflection
44     */
45    public function getDocBlock()
46    {
47        if ('' == ($comment = $this->getDocComment())) {
48            throw new Exception\InvalidArgumentException(sprintf(
49                '%s does not have a DocBlock',
50                $this->getName()
51            ));
52        }
53
54        $instance = new DocBlockReflection($comment);
55
56        return $instance;
57    }
58
59    /**
60     * Get start line (position) of function
61     *
62     * @param  bool $includeDocComment
63     * @return int
64     */
65    public function getStartLine($includeDocComment = false)
66    {
67        if ($includeDocComment) {
68            if ($this->getDocComment() != '') {
69                return $this->getDocBlock()->getStartLine();
70            }
71        }
72
73        return parent::getStartLine();
74    }
75
76    /**
77     * Get contents of function
78     *
79     * @param  bool   $includeDocBlock
80     * @return string
81     */
82    public function getContents($includeDocBlock = true)
83    {
84        $fileName = $this->getFileName();
85        if (false === $fileName) {
86            return '';
87        }
88
89        $startLine = $this->getStartLine();
90        $endLine = $this->getEndLine();
91
92        // eval'd protect
93        if (preg_match('#\((\d+)\) : eval\(\)\'d code$#', $fileName, $matches)) {
94            $fileName = preg_replace('#\(\d+\) : eval\(\)\'d code$#', '', $fileName);
95            $startLine = $endLine = $matches[1];
96        }
97
98        $lines = array_slice(
99            file($fileName, FILE_IGNORE_NEW_LINES),
100            $startLine - 1,
101            $endLine - ($startLine - 1),
102            true
103        );
104
105        $functionLine = implode("\n", $lines);
106
107        $content = '';
108        if ($this->isClosure()) {
109            preg_match('#function\s*\([^\)]*\)\s*(use\s*\([^\)]+\))?\s*\{(.*\;)?\s*\}#s', $functionLine, $matches);
110            if (isset($matches[0])) {
111                $content = $matches[0];
112            }
113        } else {
114            $name = substr($this->getName(), strrpos($this->getName(), '\\') + 1);
115            preg_match(
116                '#function\s+' . preg_quote($name) . '\s*\([^\)]*\)\s*{([^{}]+({[^}]+})*[^}]+)?}#',
117                $functionLine,
118                $matches
119            );
120            if (isset($matches[0])) {
121                $content = $matches[0];
122            }
123        }
124
125        $docComment = $this->getDocComment();
126
127        return $includeDocBlock && $docComment ? $docComment . "\n" . $content : $content;
128    }
129
130    /**
131     * Get method prototype
132     *
133     * @param string $format
134     * @return array|string
135     */
136    public function getPrototype($format = FunctionReflection::PROTOTYPE_AS_ARRAY)
137    {
138        $returnType = 'mixed';
139        $docBlock = $this->getDocBlock();
140        if ($docBlock) {
141            $return = $docBlock->getTag('return');
142            $returnTypes = $return->getTypes();
143            $returnType = count($returnTypes) > 1 ? implode('|', $returnTypes) : $returnTypes[0];
144        }
145
146        $prototype = [
147            'namespace' => $this->getNamespaceName(),
148            'name'      => substr($this->getName(), strlen($this->getNamespaceName()) + 1),
149            'return'    => $returnType,
150            'arguments' => [],
151        ];
152
153        $parameters = $this->getParameters();
154        foreach ($parameters as $parameter) {
155            $prototype['arguments'][$parameter->getName()] = [
156                'type'     => $parameter->detectType(),
157                'required' => ! $parameter->isOptional(),
158                'by_ref'   => $parameter->isPassedByReference(),
159                'default'  => $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null,
160            ];
161        }
162
163        if ($format == FunctionReflection::PROTOTYPE_AS_STRING) {
164            $line = $prototype['return'] . ' ' . $prototype['name'] . '(';
165            $args = [];
166            foreach ($prototype['arguments'] as $name => $argument) {
167                $argsLine = ($argument['type']
168                    ? $argument['type'] . ' '
169                    : '') . ($argument['by_ref'] ? '&' : '') . '$' . $name;
170                if (! $argument['required']) {
171                    $argsLine .= ' = ' . var_export($argument['default'], true);
172                }
173                $args[] = $argsLine;
174            }
175            $line .= implode(', ', $args);
176            $line .= ')';
177
178            return $line;
179        }
180
181        return $prototype;
182    }
183
184    /**
185     * Get function parameters
186     *
187     * @return ParameterReflection[]
188     */
189    public function getParameters()
190    {
191        $phpReflections  = parent::getParameters();
192        $laminasReflections = [];
193        while ($phpReflections && ($phpReflection = array_shift($phpReflections))) {
194            $instance          = new ParameterReflection($this->getName(), $phpReflection->getName());
195            $laminasReflections[] = $instance;
196            unset($phpReflection);
197        }
198        unset($phpReflections);
199
200        return $laminasReflections;
201    }
202
203    /**
204     * Get return type tag
205     *
206     * @throws Exception\InvalidArgumentException
207     * @return DocBlockReflection
208     */
209    public function getReturn()
210    {
211        $docBlock = $this->getDocBlock();
212        if (! $docBlock->hasTag('return')) {
213            throw new Exception\InvalidArgumentException(
214                'Function does not specify an @return annotation tag; cannot determine return type'
215            );
216        }
217
218        $tag    = $docBlock->getTag('return');
219
220        return new DocBlockReflection('@return ' . $tag->getDescription());
221    }
222
223    /**
224     * Get method body
225     *
226     * @return string|false
227     */
228    public function getBody()
229    {
230        $fileName = $this->getFileName();
231        if (false === $fileName) {
232            throw new Exception\InvalidArgumentException(
233                'Cannot determine internals functions body'
234            );
235        }
236
237        $startLine = $this->getStartLine();
238        $endLine = $this->getEndLine();
239
240        // eval'd protect
241        if (preg_match('#\((\d+)\) : eval\(\)\'d code$#', $fileName, $matches)) {
242            $fileName = preg_replace('#\(\d+\) : eval\(\)\'d code$#', '', $fileName);
243            $startLine = $endLine = $matches[1];
244        }
245
246        $lines = array_slice(
247            file($fileName, FILE_IGNORE_NEW_LINES),
248            $startLine - 1,
249            $endLine - ($startLine - 1),
250            true
251        );
252
253        $functionLine = implode("\n", $lines);
254
255        $body = false;
256        if ($this->isClosure()) {
257            preg_match('#function\s*\([^\)]*\)\s*(use\s*\([^\)]+\))?\s*\{(.*\;)\s*\}#s', $functionLine, $matches);
258            if (isset($matches[2])) {
259                $body = $matches[2];
260            }
261        } else {
262            $name = substr($this->getName(), strrpos($this->getName(), '\\') + 1);
263            preg_match('#function\s+' . $name . '\s*\([^\)]*\)\s*{([^{}]+({[^}]+})*[^}]+)}#', $functionLine, $matches);
264            if (isset($matches[1])) {
265                $body = $matches[1];
266            }
267        }
268
269        return $body;
270    }
271
272    /**
273     * @return string
274     */
275    public function toString()
276    {
277        return $this->__toString();
278    }
279
280    /**
281     * Required due to bug in php
282     *
283     * @return string
284     */
285    public function __toString()
286    {
287        return parent::__toString();
288    }
289}
290