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 Laminas\Code\Reflection\DocBlock\Tag\TagInterface as DocBlockTagInterface;
12use Laminas\Code\Reflection\DocBlock\TagManager as DocBlockTagManager;
13use Laminas\Code\Scanner\DocBlockScanner;
14use Reflector;
15
16use function count;
17use function get_class;
18use function is_string;
19use function ltrim;
20use function method_exists;
21use function preg_replace;
22use function sprintf;
23use function substr_count;
24
25class DocBlockReflection implements ReflectionInterface
26{
27    /**
28     * @var Reflector
29     */
30    protected $reflector;
31
32    /**
33     * @var string
34     */
35    protected $docComment;
36
37    /**
38     * @var DocBlockTagManager
39     */
40    protected $tagManager;
41
42    /**
43     * @var int
44     */
45    protected $startLine;
46
47    /**
48     * @var int
49     */
50    protected $endLine;
51
52    /**
53     * @var string
54     */
55    protected $cleanDocComment;
56
57    /**
58     * @var string
59     */
60    protected $longDescription;
61
62    /**
63     * @var string
64     */
65    protected $shortDescription;
66
67    /**
68     * @var array
69     */
70    protected $tags = [];
71
72    /**
73     * @var bool
74     */
75    protected $isReflected = false;
76
77    /**
78     * Export reflection
79     *
80     * Required by the Reflector interface.
81     *
82     * @todo   What should this do?
83     * @return void
84     */
85    public static function export()
86    {
87    }
88
89    /**
90     * @param  Reflector|string $commentOrReflector
91     * @param  null|DocBlockTagManager $tagManager
92     * @throws Exception\InvalidArgumentException
93     */
94    public function __construct($commentOrReflector, DocBlockTagManager $tagManager = null)
95    {
96        if (! $tagManager) {
97            $tagManager = new DocBlockTagManager();
98            $tagManager->initializeDefaultTags();
99        }
100        $this->tagManager = $tagManager;
101
102        if ($commentOrReflector instanceof Reflector) {
103            $this->reflector = $commentOrReflector;
104            if (! method_exists($commentOrReflector, 'getDocComment')) {
105                throw new Exception\InvalidArgumentException('Reflector must contain method "getDocComment"');
106            }
107            /* @var MethodReflection $commentOrReflector */
108            $this->docComment = $commentOrReflector->getDocComment();
109
110            // determine line numbers
111            $lineCount       = substr_count($this->docComment, "\n");
112            $this->startLine = $this->reflector->getStartLine() - $lineCount - 1;
113            $this->endLine   = $this->reflector->getStartLine() - 1;
114        } elseif (is_string($commentOrReflector)) {
115            $this->docComment = $commentOrReflector;
116        } else {
117            throw new Exception\InvalidArgumentException(sprintf(
118                '%s must have a (string) DocComment or a Reflector in the constructor',
119                get_class($this)
120            ));
121        }
122
123        if ($this->docComment == '') {
124            throw new Exception\InvalidArgumentException('DocComment cannot be empty');
125        }
126
127        $this->reflect();
128    }
129
130    /**
131     * Retrieve contents of DocBlock
132     *
133     * @return string
134     */
135    public function getContents()
136    {
137        $this->reflect();
138
139        return $this->cleanDocComment;
140    }
141
142    /**
143     * Get start line (position) of DocBlock
144     *
145     * @return int
146     */
147    public function getStartLine()
148    {
149        $this->reflect();
150
151        return $this->startLine;
152    }
153
154    /**
155     * Get last line (position) of DocBlock
156     *
157     * @return int
158     */
159    public function getEndLine()
160    {
161        $this->reflect();
162
163        return $this->endLine;
164    }
165
166    /**
167     * Get DocBlock short description
168     *
169     * @return string
170     */
171    public function getShortDescription()
172    {
173        $this->reflect();
174
175        return $this->shortDescription;
176    }
177
178    /**
179     * Get DocBlock long description
180     *
181     * @return string
182     */
183    public function getLongDescription()
184    {
185        $this->reflect();
186
187        return $this->longDescription;
188    }
189
190    /**
191     * Does the DocBlock contain the given annotation tag?
192     *
193     * @param  string $name
194     * @return bool
195     */
196    public function hasTag($name)
197    {
198        $this->reflect();
199        foreach ($this->tags as $tag) {
200            if ($tag->getName() == $name) {
201                return true;
202            }
203        }
204
205        return false;
206    }
207
208    /**
209     * Retrieve the given DocBlock tag
210     *
211     * @param  string $name
212     * @return DocBlockTagInterface|false
213     */
214    public function getTag($name)
215    {
216        $this->reflect();
217        foreach ($this->tags as $tag) {
218            if ($tag->getName() == $name) {
219                return $tag;
220            }
221        }
222
223        return false;
224    }
225
226    /**
227     * Get all DocBlock annotation tags
228     *
229     * @param  string $filter
230     * @return DocBlockTagInterface[]
231     */
232    public function getTags($filter = null)
233    {
234        $this->reflect();
235        if ($filter === null || ! is_string($filter)) {
236            return $this->tags;
237        }
238
239        $returnTags = [];
240        foreach ($this->tags as $tag) {
241            if ($tag->getName() == $filter) {
242                $returnTags[] = $tag;
243            }
244        }
245
246        return $returnTags;
247    }
248
249    /**
250     * Parse the DocBlock
251     *
252     * @return void
253     */
254    protected function reflect()
255    {
256        if ($this->isReflected) {
257            return;
258        }
259
260        $docComment = preg_replace('#[ ]{0,1}\*/$#', '', $this->docComment);
261
262        // create a clean docComment
263        $this->cleanDocComment = preg_replace("#[ \t]*(?:/\*\*|\*/|\*)[ ]{0,1}(.*)?#", '$1', $docComment);
264
265        // @todo should be changed to remove first and last empty line
266        $this->cleanDocComment = ltrim($this->cleanDocComment, "\r\n");
267
268        $scanner                = new DocBlockScanner($docComment);
269        $this->shortDescription = ltrim($scanner->getShortDescription());
270        $this->longDescription  = ltrim($scanner->getLongDescription());
271
272        foreach ($scanner->getTags() as $tag) {
273            $this->tags[] = $this->tagManager->createTag(ltrim($tag['name'], '@'), ltrim($tag['value']));
274        }
275
276        $this->isReflected = true;
277    }
278
279    /**
280     * @return string
281     */
282    public function toString()
283    {
284        $str = 'DocBlock [ /* DocBlock */ ] {' . "\n\n";
285        $str .= '  - Tags [' . count($this->tags) . '] {' . "\n";
286
287        foreach ($this->tags as $tag) {
288            $str .= '    ' . $tag;
289        }
290
291        $str .= '  }' . "\n";
292        $str .= '}' . "\n";
293
294        return $str;
295    }
296
297    /**
298     * Serialize to string
299     *
300     * Required by the Reflector interface
301     *
302     * @return string
303     */
304    public function __toString()
305    {
306        return $this->toString();
307    }
308}
309