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\Generator;
10
11use Laminas\Code\Reflection\ParameterReflection;
12use ReflectionParameter;
13
14use function is_string;
15use function method_exists;
16use function str_replace;
17use function strtolower;
18
19class ParameterGenerator extends AbstractGenerator
20{
21    /**
22     * @var string
23     */
24    protected $name;
25
26    /**
27     * @var TypeGenerator|null
28     */
29    protected $type;
30
31    /**
32     * @var ValueGenerator
33     */
34    protected $defaultValue;
35
36    /**
37     * @var int
38     */
39    protected $position;
40
41    /**
42     * @var bool
43     */
44    protected $passedByReference = false;
45
46    /**
47     * @var bool
48     */
49    private $variadic = false;
50
51    /**
52     * @var bool
53     */
54    private $omitDefaultValue = false;
55
56    /**
57     * @param  ParameterReflection $reflectionParameter
58     * @return ParameterGenerator
59     */
60    public static function fromReflection(ParameterReflection $reflectionParameter)
61    {
62        $param = new ParameterGenerator();
63
64        $param->setName($reflectionParameter->getName());
65
66        if ($type = self::extractFQCNTypeFromReflectionType($reflectionParameter)) {
67            $param->setType($type);
68        }
69
70        $param->setPosition($reflectionParameter->getPosition());
71
72        $variadic = method_exists($reflectionParameter, 'isVariadic') && $reflectionParameter->isVariadic();
73
74        $param->setVariadic($variadic);
75
76        if (! $variadic && ($reflectionParameter->isOptional() || $reflectionParameter->isDefaultValueAvailable())) {
77            try {
78                $param->setDefaultValue($reflectionParameter->getDefaultValue());
79            } catch (\ReflectionException $e) {
80                $param->setDefaultValue(null);
81            }
82        }
83
84        $param->setPassedByReference($reflectionParameter->isPassedByReference());
85
86        return $param;
87    }
88
89    /**
90     * Generate from array
91     *
92     * @configkey name                  string                                          [required] Class Name
93     * @configkey type                  string
94     * @configkey defaultvalue          null|bool|string|int|float|array|ValueGenerator
95     * @configkey passedbyreference     bool
96     * @configkey position              int
97     * @configkey sourcedirty           bool
98     * @configkey indentation           string
99     * @configkey sourcecontent         string
100     * @configkey omitdefaultvalue      bool
101     *
102     * @throws Exception\InvalidArgumentException
103     * @param  array $array
104     * @return ParameterGenerator
105     */
106    public static function fromArray(array $array)
107    {
108        if (! isset($array['name'])) {
109            throw new Exception\InvalidArgumentException(
110                'Parameter generator requires that a name is provided for this object'
111            );
112        }
113
114        $param = new static($array['name']);
115        foreach ($array as $name => $value) {
116            // normalize key
117            switch (strtolower(str_replace(['.', '-', '_'], '', $name))) {
118                case 'type':
119                    $param->setType($value);
120                    break;
121                case 'defaultvalue':
122                    $param->setDefaultValue($value);
123                    break;
124                case 'passedbyreference':
125                    $param->setPassedByReference($value);
126                    break;
127                case 'position':
128                    $param->setPosition($value);
129                    break;
130                case 'sourcedirty':
131                    $param->setSourceDirty($value);
132                    break;
133                case 'indentation':
134                    $param->setIndentation($value);
135                    break;
136                case 'sourcecontent':
137                    $param->setSourceContent($value);
138                    break;
139                case 'omitdefaultvalue':
140                    $param->omitDefaultValue($value);
141                    break;
142            }
143        }
144
145        return $param;
146    }
147
148    /**
149     * @param  string $name
150     * @param  string $type
151     * @param  mixed $defaultValue
152     * @param  int $position
153     * @param  bool $passByReference
154     */
155    public function __construct(
156        $name = null,
157        $type = null,
158        $defaultValue = null,
159        $position = null,
160        $passByReference = false
161    ) {
162        if (null !== $name) {
163            $this->setName($name);
164        }
165        if (null !== $type) {
166            $this->setType($type);
167        }
168        if (null !== $defaultValue) {
169            $this->setDefaultValue($defaultValue);
170        }
171        if (null !== $position) {
172            $this->setPosition($position);
173        }
174        if (false !== $passByReference) {
175            $this->setPassedByReference(true);
176        }
177    }
178
179    /**
180     * @param  string $type
181     * @return ParameterGenerator
182     */
183    public function setType($type)
184    {
185        $this->type = TypeGenerator::fromTypeString($type);
186
187        return $this;
188    }
189
190    /**
191     * @return string
192     */
193    public function getType()
194    {
195        return $this->type
196            ? (string) $this->type
197            : null;
198    }
199
200    /**
201     * @param  string $name
202     * @return ParameterGenerator
203     */
204    public function setName($name)
205    {
206        $this->name = (string) $name;
207        return $this;
208    }
209
210    /**
211     * @return string
212     */
213    public function getName()
214    {
215        return $this->name;
216    }
217
218    /**
219     * Set the default value of the parameter.
220     *
221     * Certain variables are difficult to express
222     *
223     * @param  null|bool|string|int|float|array|ValueGenerator $defaultValue
224     * @return ParameterGenerator
225     */
226    public function setDefaultValue($defaultValue)
227    {
228        if (! $defaultValue instanceof ValueGenerator) {
229            $defaultValue = new ValueGenerator($defaultValue);
230        }
231        $this->defaultValue = $defaultValue;
232
233        return $this;
234    }
235
236    /**
237     * @return ValueGenerator
238     */
239    public function getDefaultValue()
240    {
241        return $this->defaultValue;
242    }
243
244    /**
245     * @param  int $position
246     * @return ParameterGenerator
247     */
248    public function setPosition($position)
249    {
250        $this->position = (int) $position;
251        return $this;
252    }
253
254    /**
255     * @return int
256     */
257    public function getPosition()
258    {
259        return $this->position;
260    }
261
262    /**
263     * @return bool
264     */
265    public function getPassedByReference()
266    {
267        return $this->passedByReference;
268    }
269
270    /**
271     * @param  bool $passedByReference
272     * @return ParameterGenerator
273     */
274    public function setPassedByReference($passedByReference)
275    {
276        $this->passedByReference = (bool) $passedByReference;
277        return $this;
278    }
279
280    /**
281     * @param bool $variadic
282     *
283     * @return ParameterGenerator
284     */
285    public function setVariadic($variadic)
286    {
287        $this->variadic = (bool) $variadic;
288
289        return $this;
290    }
291
292    /**
293     * @return bool
294     */
295    public function getVariadic()
296    {
297        return $this->variadic;
298    }
299
300    /**
301     * @return string
302     */
303    public function generate()
304    {
305        $output = $this->generateTypeHint();
306
307        if (true === $this->passedByReference) {
308            $output .= '&';
309        }
310
311        if ($this->variadic) {
312            $output .= '... ';
313        }
314
315        $output .= '$' . $this->name;
316
317        if ($this->omitDefaultValue) {
318            return $output;
319        }
320
321        if ($this->defaultValue instanceof ValueGenerator) {
322            $output .= ' = ';
323            $this->defaultValue->setOutputMode(ValueGenerator::OUTPUT_SINGLE_LINE);
324            $output .= $this->defaultValue;
325        }
326
327        return $output;
328    }
329
330    /**
331     * @param ParameterReflection $reflectionParameter
332     *
333     * @return null|string
334     */
335    private static function extractFQCNTypeFromReflectionType(ParameterReflection $reflectionParameter)
336    {
337        if (! method_exists($reflectionParameter, 'getType')) {
338            return self::prePhp7ExtractFQCNTypeFromReflectionType($reflectionParameter);
339        }
340
341        $type = method_exists($reflectionParameter, 'getType')
342            ? $reflectionParameter->getType()
343            : null;
344
345        if (! $type) {
346            return null;
347        }
348
349        if (! method_exists($type, 'getName')) {
350            return self::expandLiteralParameterType((string) $type, $reflectionParameter);
351        }
352
353        return ($type->allowsNull() ? '?' : '')
354            . self::expandLiteralParameterType($type->getName(), $reflectionParameter);
355    }
356
357    /**
358     * For ancient PHP versions (yes, you should upgrade to 7.0):
359     *
360     * @param ParameterReflection $reflectionParameter
361     *
362     * @return string|null
363     */
364    private static function prePhp7ExtractFQCNTypeFromReflectionType(ParameterReflection $reflectionParameter)
365    {
366        if ($reflectionParameter->isCallable()) {
367            return 'callable';
368        }
369
370        if ($reflectionParameter->isArray()) {
371            return 'array';
372        }
373
374        if ($class = $reflectionParameter->getClass()) {
375            return $class->getName();
376        }
377
378        return null;
379    }
380
381    /**
382     * @param string              $literalParameterType
383     * @param ReflectionParameter $reflectionParameter
384     *
385     * @return string
386     */
387    private static function expandLiteralParameterType($literalParameterType, ReflectionParameter $reflectionParameter)
388    {
389        if ('self' === strtolower($literalParameterType)) {
390            return $reflectionParameter->getDeclaringClass()->getName();
391        }
392
393        if ('parent' === strtolower($literalParameterType)) {
394            return $reflectionParameter->getDeclaringClass()->getParentClass()->getName();
395        }
396
397        return $literalParameterType;
398    }
399
400    /**
401     * @return string
402     */
403    private function generateTypeHint()
404    {
405        if (null === $this->type) {
406            return '';
407        }
408
409        return $this->type->generate() . ' ';
410    }
411
412    /**
413     * @param bool $omit
414     * @return ParameterGenerator
415     */
416    public function omitDefaultValue(bool $omit = true)
417    {
418        $this->omitDefaultValue = $omit;
419
420        return $this;
421    }
422}
423