1<?php
2/**
3 * This file is part of phpDocumentor.
4 *
5 * For the full copyright and license information, please view the LICENSE
6 * file that was distributed with this source code.
7 *
8 * @copyright 2010-2015 Mike van Riel<mike@phpdoc.org>
9 * @license   http://www.opensource.org/licenses/mit-license.php MIT
10 * @link      http://phpdoc.org
11 */
12
13namespace phpDocumentor\Reflection\DocBlock\Tags;
14
15use phpDocumentor\Reflection\DocBlock\Description;
16use phpDocumentor\Reflection\DocBlock\DescriptionFactory;
17use phpDocumentor\Reflection\Type;
18use phpDocumentor\Reflection\TypeResolver;
19use phpDocumentor\Reflection\Types\Context as TypeContext;
20use phpDocumentor\Reflection\Types\Void_;
21use Webmozart\Assert\Assert;
22
23/**
24 * Reflection class for an {@}method in a Docblock.
25 */
26final class Method extends BaseTag implements Factory\StaticMethod
27{
28    protected $name = 'method';
29
30    /** @var string */
31    private $methodName = '';
32
33    /** @var string[] */
34    private $arguments = [];
35
36    /** @var bool */
37    private $isStatic = false;
38
39    /** @var Type */
40    private $returnType;
41
42    public function __construct(
43        $methodName,
44        array $arguments = [],
45        Type $returnType = null,
46        $static = false,
47        Description $description = null
48    ) {
49        Assert::stringNotEmpty($methodName);
50        Assert::boolean($static);
51
52        if ($returnType === null) {
53            $returnType = new Void_();
54        }
55
56        $this->methodName  = $methodName;
57        $this->arguments   = $this->filterArguments($arguments);
58        $this->returnType  = $returnType;
59        $this->isStatic    = $static;
60        $this->description = $description;
61    }
62
63    /**
64     * {@inheritdoc}
65     */
66    public static function create(
67        $body,
68        TypeResolver $typeResolver = null,
69        DescriptionFactory $descriptionFactory = null,
70        TypeContext $context = null
71    ) {
72        Assert::stringNotEmpty($body);
73        Assert::allNotNull([ $typeResolver, $descriptionFactory ]);
74
75        // 1. none or more whitespace
76        // 2. optionally the keyword "static" followed by whitespace
77        // 3. optionally a word with underscores followed by whitespace : as
78        //    type for the return value
79        // 4. then optionally a word with underscores followed by () and
80        //    whitespace : as method name as used by phpDocumentor
81        // 5. then a word with underscores, followed by ( and any character
82        //    until a ) and whitespace : as method name with signature
83        // 6. any remaining text : as description
84        if (!preg_match(
85            '/^
86                # Static keyword
87                # Declares a static method ONLY if type is also present
88                (?:
89                    (static)
90                    \s+
91                )?
92                # Return type
93                (?:
94                    (
95                        (?:[\w\|_\\\\]*\$this[\w\|_\\\\]*)
96                        |
97                        (?:
98                            (?:[\w\|_\\\\]+)
99                            # array notation
100                            (?:\[\])*
101                        )*
102                    )
103                    \s+
104                )?
105                # Legacy method name (not captured)
106                (?:
107                    [\w_]+\(\)\s+
108                )?
109                # Method name
110                ([\w\|_\\\\]+)
111                # Arguments
112                (?:
113                    \(([^\)]*)\)
114                )?
115                \s*
116                # Description
117                (.*)
118            $/sux',
119            $body,
120            $matches
121        )) {
122            return null;
123        }
124
125        list(, $static, $returnType, $methodName, $arguments, $description) = $matches;
126
127        $static      = $static === 'static';
128
129        if ($returnType === '') {
130            $returnType = 'void';
131        }
132
133        $returnType  = $typeResolver->resolve($returnType, $context);
134        $description = $descriptionFactory->create($description, $context);
135
136        if (is_string($arguments) && strlen($arguments) > 0) {
137            $arguments = explode(',', $arguments);
138            foreach ($arguments as &$argument) {
139                $argument = explode(' ', self::stripRestArg(trim($argument)), 2);
140                if ($argument[0][0] === '$') {
141                    $argumentName = substr($argument[0], 1);
142                    $argumentType = new Void_();
143                } else {
144                    $argumentType = $typeResolver->resolve($argument[0], $context);
145                    $argumentName = '';
146                    if (isset($argument[1])) {
147                        $argument[1] = self::stripRestArg($argument[1]);
148                        $argumentName = substr($argument[1], 1);
149                    }
150                }
151
152                $argument = [ 'name' => $argumentName, 'type' => $argumentType];
153            }
154        } else {
155            $arguments = [];
156        }
157
158        return new static($methodName, $arguments, $returnType, $static, $description);
159    }
160
161    /**
162     * Retrieves the method name.
163     *
164     * @return string
165     */
166    public function getMethodName()
167    {
168        return $this->methodName;
169    }
170
171    /**
172     * @return string[]
173     */
174    public function getArguments()
175    {
176        return $this->arguments;
177    }
178
179    /**
180     * Checks whether the method tag describes a static method or not.
181     *
182     * @return bool TRUE if the method declaration is for a static method, FALSE otherwise.
183     */
184    public function isStatic()
185    {
186        return $this->isStatic;
187    }
188
189    /**
190     * @return Type
191     */
192    public function getReturnType()
193    {
194        return $this->returnType;
195    }
196
197    public function __toString()
198    {
199        $arguments = [];
200        foreach ($this->arguments as $argument) {
201            $arguments[] = $argument['type'] . ' $' . $argument['name'];
202        }
203
204        return trim(($this->isStatic() ? 'static ' : '')
205            . (string)$this->returnType . ' '
206            . $this->methodName
207            . '(' . implode(', ', $arguments) . ')'
208            . ($this->description ? ' ' . $this->description->render() : ''));
209    }
210
211    private function filterArguments($arguments)
212    {
213        foreach ($arguments as &$argument) {
214            if (is_string($argument)) {
215                $argument = [ 'name' => $argument ];
216            }
217
218            if (! isset($argument['type'])) {
219                $argument['type'] = new Void_();
220            }
221
222            $keys = array_keys($argument);
223            sort($keys);
224            if ($keys !== [ 'name', 'type' ]) {
225                throw new \InvalidArgumentException(
226                    'Arguments can only have the "name" and "type" fields, found: ' . var_export($keys, true)
227                );
228            }
229        }
230
231        return $arguments;
232    }
233
234    private static function stripRestArg($argument)
235    {
236        if (strpos($argument, '...') === 0) {
237            $argument = trim(substr($argument, 3));
238        }
239
240        return $argument;
241    }
242}
243