Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
80 / 80
100.00% covered (success)
100.00%
18 / 18
CRAP
100.00% covered (success)
100.00%
1 / 1
FunctionTrait
100.00% covered (success)
100.00%
80 / 80
100.00% covered (success)
100.00%
18 / 18
50
100.00% covered (success)
100.00%
1 / 1
 addArgument
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
11
 addArguments
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 hasArgument
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasArguments
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getArgument
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getArguments
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addParameter
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addParameters
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 hasParameter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasParameters
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getParameter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getParameters
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addReturnType
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 addReturnTypes
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 hasReturnType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasReturnTypes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getReturnTypes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 formatArguments
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
18
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <dev@noladev.com>
8 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Code\Generator\Traits;
16
17use Pop\Code\Generator\DocblockGenerator;
18use Pop\Code\Generator\Exception;
19use Pop\Code\Generator\NoValue;
20use Pop\Code\Generator\Support\ValueFormatter;
21
22/**
23 * Function trait
24 *
25 * @category   Pop
26 * @package    Pop\Code
27 * @author     Nick Sagona, III <dev@noladev.com>
28 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
29 * @license    https://www.popphp.org/license     New BSD License
30 * @version    6.0.0
31 */
32trait FunctionTrait
33{
34
35    /**
36     * Arguments
37     * @var array
38     */
39    protected array $arguments = [];
40
41    /**
42     * Return Types
43     * @var array
44     */
45    protected array $returnTypes = [];
46
47    /**
48     * Add an argument
49     *
50     * @param  string  $name
51     * @param  mixed   $value
52     * @param  ?string $type
53     * @param  bool    $variadic
54     * @param  bool    $byRef
55     * @param  array   $attributes
56     * @throws Exception
57     * @return static
58     */
59    public function addArgument(
60        string $name, mixed $value = new NoValue(), ?string $type = null, bool $variadic = false, bool $byRef = false,
61        array $attributes = []
62    ): static
63    {
64        if ($variadic && !($value instanceof NoValue)) {
65            throw new Exception('Error: A variadic argument cannot have a default value.');
66        }
67
68        $this->arguments[$name] = [
69            'value' => $value, 'type' => $type, 'variadic' => $variadic, 'byRef' => $byRef, 'attributes' => $attributes
70        ];
71
72        if ($this->docblock === null) {
73            $this->docblock = new DocblockGenerator(null, $this->indent);
74        }
75
76        $docName = $name;
77        if (!str_starts_with($docName, '$')) {
78            $docName = '$' . $docName;
79        }
80        $docType = $type;
81        if (!empty($docType) && !str_starts_with($docType, '?') && ($docType !== 'mixed') && ($value === null)
82            && !in_array('null', explode('|', $docType), true)
83        ) {
84            $docType = str_contains($docType, '&') ? '(' . $docType . ')|null' : $docType . '|null';
85        }
86        // A caller re-adding an argument for a name that already exists (e.g. to change its
87        // type) leaves a stale @param entry behind otherwise -- $this->arguments is name-keyed
88        // and correctly overwrites, but the docblock's params are append-only. This also covers
89        // MethodReflection/FunctionReflection's own flow: they set a fully-parsed docblock
90        // (which may carry a hand-written per-param description from the real source) *before*
91        // calling addArgument() for each parameter -- preserve that description across the
92        // remove+re-add rather than silently dropping it, since addArgument() itself has no way
93        // to be told a description directly.
94        $existingParam = $this->docblock->findParam($docName);
95        $docDesc       = $existingParam['desc'] ?? null;
96        $this->docblock->removeParam($docName);
97        $this->docblock->addParam($docType, $docName, $docDesc);
98
99        return $this;
100    }
101
102    /**
103     * Add arguments
104     *
105     * @param  array $args  each element shaped ['name' => string, 'value' => mixed, 'type' => ?string,
106     *                       'variadic' => bool, 'byRef' => bool, 'attributes' => array]
107     * @throws Exception
108     * @return static
109     */
110    public function addArguments(array $args): static
111    {
112        foreach ($args as $arg) {
113            if (!isset($arg['name'])) {
114                throw new Exception("Error: The 'name' key was not set.");
115            }
116            $value      = array_key_exists('value', $arg) ? $arg['value'] : new NoValue();
117            $type       = $arg['type'] ?? null;
118            $variadic   = $arg['variadic'] ?? false;
119            $byRef      = $arg['byRef'] ?? false;
120            $attributes = $arg['attributes'] ?? [];
121            $this->addArgument($arg['name'], $value, $type, $variadic, $byRef, $attributes);
122        }
123        return $this;
124    }
125
126    /**
127     * Has an argument
128     *
129     * @param  string $name
130     * @return bool
131     */
132    public function hasArgument(string $name): bool
133    {
134        return isset($this->arguments[$name]);
135    }
136
137    /**
138     * Has arguments
139     *
140     * @return bool
141     */
142    public function hasArguments(): bool
143    {
144        return !empty($this->arguments);
145    }
146
147    /**
148     * Get an argument
149     *
150     * @param  string $name
151     * @return array|null
152     */
153    public function getArgument(string $name): array|null
154    {
155        return $this->arguments[$name] ?? null;
156    }
157
158    /**
159     * Get the arguments
160     *
161     * @return array
162     */
163    public function getArguments(): array
164    {
165        return $this->arguments;
166    }
167
168    /**
169     * Add an argument (alias method for convenience)
170     *
171     * @param  string  $name
172     * @param  mixed   $value
173     * @param  ?string $type
174     * @param  bool    $variadic
175     * @param  bool    $byRef
176     * @param  array   $attributes
177     * @throws Exception
178     * @return static
179     */
180    public function addParameter(
181        string $name, mixed $value = new NoValue(), ?string $type = null, bool $variadic = false, bool $byRef = false,
182        array $attributes = []
183    ): static
184    {
185        $this->addArgument($name, $value, $type, $variadic, $byRef, $attributes);
186        return $this;
187    }
188
189    /**
190     * Add arguments (alias method for convenience)
191     *
192     * @param  array $args
193     * @return static
194     */
195    public function addParameters(array $args): static
196    {
197        $this->addArguments($args);
198        return $this;
199    }
200
201    /**
202     * Has an argument (alias method for convenience)
203     *
204     * @param  string $name
205     * @return bool
206     */
207    public function hasParameter(string $name): bool
208    {
209        return $this->hasArgument($name);
210    }
211
212    /**
213     * Has arguments (alias method for convenience)
214     *
215     * @return bool
216     */
217    public function hasParameters(): bool
218    {
219        return $this->hasArguments();
220    }
221
222    /**
223     * Get an argument (alias method for convenience)
224     *
225     * @param  string $name
226     * @return array|null
227     */
228    public function getParameter(string $name): array|null
229    {
230        return $this->getArgument($name);
231    }
232
233    /**
234     * Get the arguments (alias method for convenience)
235     *
236     * @return array
237     */
238    public function getParameters(): array
239    {
240        return $this->getArguments();
241    }
242
243    /**
244     * Add a return type
245     *
246     * @param  string $type
247     * @return static
248     */
249    public function addReturnType(string $type): static
250    {
251        $this->returnTypes[] = $type;
252
253        if ($this->docblock === null) {
254            $this->docblock = new DocblockGenerator(null, $this->indent);
255        }
256
257        // Preserve an existing @return description (e.g. one already parsed from a real source
258        // docblock by MethodReflection/FunctionReflection before this is called) -- setReturn()
259        // always resets the description to null when not given one explicitly, which silently
260        // discarded it otherwise.
261        $existingReturn = $this->docblock->getReturn();
262        $returnDesc     = $existingReturn['desc'] ?? null;
263        $this->docblock->setReturn(implode('|', $this->returnTypes), $returnDesc);
264
265        return $this;
266    }
267
268    /**
269     * Add return types
270     *
271     * @param  array $types
272     * @return static
273     */
274    public function addReturnTypes(array $types): static
275    {
276        foreach ($types as $type) {
277            $this->addReturnType($type);
278        }
279        return $this;
280    }
281
282    /**
283     * Has return type
284     *
285     * @param  string $type
286     * @return bool
287     */
288    public function hasReturnType(string $type): bool
289    {
290        return in_array($type, $this->returnTypes);
291    }
292
293    /**
294     * Has return types
295     *
296     * @return bool
297     */
298    public function hasReturnTypes(): bool
299    {
300        return !empty($this->returnTypes);
301    }
302
303    /**
304     * Get the return types
305     *
306     * @return array
307     */
308    public function getReturnTypes(): array
309    {
310        return $this->returnTypes;
311    }
312
313    /**
314     * Format the arguments
315     *
316     * @return string|null
317     */
318    protected function formatArguments(): string|null
319    {
320        $args = null;
321
322        $i = 0;
323        foreach ($this->arguments as $name => $arg) {
324            $i++;
325
326            if (!empty($arg['attributes'])) {
327                $attrs = [];
328                foreach ($arg['attributes'] as $attribute) {
329                    $attrs[] = $attribute->render();
330                }
331                $args .= implode(' ', $attrs) . ' ';
332            }
333
334            $promoted = null;
335            if (!empty($arg['promotedVisibility'])) {
336                $promoted = $arg['promotedVisibility'] . ' ' . (!empty($arg['promotedReadonly']) ? 'readonly ' : '');
337            }
338
339            if ($arg['type'] !== null) {
340                $type = $arg['type'];
341                if (!empty($type) && !str_starts_with($type, '?') && ($type !== 'mixed') && ($arg['value'] === null)
342                    && !in_array('null', explode('|', $type), true)
343                ) {
344                    // An intersection type (`Countable&Traversable`) needs parens before
345                    // combining with `|null` -- PHP requires DNF syntax `(A&B)|null`.
346                    $type = str_contains($type, '&') ? '(' . $type . ')|null' : $type . '|null';
347                }
348                $args .= $promoted . $type . ' ';
349            } else {
350                $args .= $promoted;
351            }
352
353            $args .= (!empty($arg['byRef']) ? '&' : '') . (!empty($arg['variadic']) ? '...' : '');
354            $args .= (substr($name, 0, 1) != '$') ? "\$" . $name : $name;
355
356            if (!($arg['value'] instanceof NoValue)) {
357                $args .= ' = ' . ValueFormatter::format($arg['value'], $arg['type']);
358            }
359
360            if ($i < count($this->arguments)) {
361                $args .= ', ';
362            }
363        }
364
365        return $args;
366    }
367
368}