1<?php
2
3declare(strict_types=1);
4
5/**
6 * This file is part of phpDocumentor.
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 *
11 * @link      http://phpdoc.org
12 */
13
14namespace phpDocumentor\Reflection\DocBlock\Tags;
15
16use phpDocumentor\Reflection\DocBlock\Description;
17use phpDocumentor\Reflection\DocBlock\DescriptionFactory;
18use phpDocumentor\Reflection\Type;
19use phpDocumentor\Reflection\TypeResolver;
20use phpDocumentor\Reflection\Types\Context as TypeContext;
21use phpDocumentor\Reflection\Utils;
22use Webmozart\Assert\Assert;
23use function array_shift;
24use function array_unshift;
25use function implode;
26use function strpos;
27use function substr;
28use const PREG_SPLIT_DELIM_CAPTURE;
29
30/**
31 * Reflection class for the {@}param tag in a Docblock.
32 */
33final class Param extends TagWithType implements Factory\StaticMethod
34{
35    /** @var string|null */
36    private $variableName;
37
38    /** @var bool determines whether this is a variadic argument */
39    private $isVariadic;
40
41    /** @var bool determines whether this is passed by reference */
42    private $isReference;
43
44    public function __construct(
45        ?string $variableName,
46        ?Type $type = null,
47        bool $isVariadic = false,
48        ?Description $description = null,
49        bool $isReference = false
50    ) {
51        $this->name         = 'param';
52        $this->variableName = $variableName;
53        $this->type         = $type;
54        $this->isVariadic   = $isVariadic;
55        $this->description  = $description;
56        $this->isReference  = $isReference;
57    }
58
59    public static function create(
60        string $body,
61        ?TypeResolver $typeResolver = null,
62        ?DescriptionFactory $descriptionFactory = null,
63        ?TypeContext $context = null
64    ) : self {
65        Assert::stringNotEmpty($body);
66        Assert::notNull($typeResolver);
67        Assert::notNull($descriptionFactory);
68
69        [$firstPart, $body] = self::extractTypeFromBody($body);
70
71        $type         = null;
72        $parts        = Utils::pregSplit('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE);
73        $variableName = '';
74        $isVariadic   = false;
75        $isReference   = false;
76
77        // if the first item that is encountered is not a variable; it is a type
78        if ($firstPart && !self::strStartsWithVariable($firstPart)) {
79            $type = $typeResolver->resolve($firstPart, $context);
80        } else {
81            // first part is not a type; we should prepend it to the parts array for further processing
82            array_unshift($parts, $firstPart);
83        }
84
85        // if the next item starts with a $ or ...$ or &$ or &...$ it must be the variable name
86        if (isset($parts[0]) && self::strStartsWithVariable($parts[0])) {
87            $variableName = array_shift($parts);
88            if ($type) {
89                array_shift($parts);
90            }
91
92            Assert::notNull($variableName);
93
94            if (strpos($variableName, '$') === 0) {
95                $variableName = substr($variableName, 1);
96            } elseif (strpos($variableName, '&$') === 0) {
97                $isReference = true;
98                $variableName = substr($variableName, 2);
99            } elseif (strpos($variableName, '...$') === 0) {
100                $isVariadic = true;
101                $variableName = substr($variableName, 4);
102            } elseif (strpos($variableName, '&...$') === 0) {
103                $isVariadic   = true;
104                $isReference  = true;
105                $variableName = substr($variableName, 5);
106            }
107        }
108
109        $description = $descriptionFactory->create(implode('', $parts), $context);
110
111        return new static($variableName, $type, $isVariadic, $description, $isReference);
112    }
113
114    /**
115     * Returns the variable's name.
116     */
117    public function getVariableName() : ?string
118    {
119        return $this->variableName;
120    }
121
122    /**
123     * Returns whether this tag is variadic.
124     */
125    public function isVariadic() : bool
126    {
127        return $this->isVariadic;
128    }
129
130    /**
131     * Returns whether this tag is passed by reference.
132     */
133    public function isReference() : bool
134    {
135        return $this->isReference;
136    }
137
138    /**
139     * Returns a string representation for this tag.
140     */
141    public function __toString() : string
142    {
143        if ($this->description) {
144            $description = $this->description->render();
145        } else {
146            $description = '';
147        }
148
149        $variableName = '';
150        if ($this->variableName) {
151            $variableName .= ($this->isReference ? '&' : '') . ($this->isVariadic ? '...' : '');
152            $variableName .= '$' . $this->variableName;
153        }
154
155        $type = (string) $this->type;
156
157        return $type
158            . ($variableName !== '' ? ($type !== '' ? ' ' : '') . $variableName : '')
159            . ($description !== '' ? ($type !== '' || $variableName !== '' ? ' ' : '') . $description : '');
160    }
161
162    private static function strStartsWithVariable(string $str) : bool
163    {
164        return strpos($str, '$') === 0
165               ||
166               strpos($str, '...$') === 0
167               ||
168               strpos($str, '&$') === 0
169               ||
170               strpos($str, '&...$') === 0;
171    }
172}
173