Completed
Push — master ( 2d063c...d7e2f2 )
by Thomas
04:57 queued 03:26
created

Docblock::getLongDescription()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
3
namespace gossi\docblock;
4
5
use gossi\docblock\tags\TagFactory;
6
use gossi\docblock\tags\AbstractTag;
7
use phootwork\collection\ArrayList;
8
use phootwork\collection\Map;
9
10
class Docblock {
11
	
12
	protected $shortDescription;
13
	protected $longDescription;
14
	protected $tags;
15
	protected $comparator = null;
16
	
17
	const REGEX_TAGNAME = '[\w\-\_\\\\]+';
18
	
19
	/**
20
	 * Static docblock factory
21
	 * 
22
	 * @param string \Reflector|$docblock
23
	 * @return $this
24
	 */
25 1
	public static function create($docblock = null) {
26 1
		return new static($docblock);
27
	}
28
	
29
	/**
30
	 * Creates a new docblock instance and parses the initial string or reflector object if given
31
	 * 
32
	 * @param \ReflectionFunctionAbstract|\ReflectionClass|\ReflectionProperty|string a docblock to parse
33
	 */
34 12
	public function __construct($docblock = null) {
35 12
		$this->tags = new ArrayList();
36 12
		$this->parse($docblock);
37 10
	}
38
	
39
	/**
40
	 * @see https://github.com/phpDocumentor/ReflectionDocBlock/blob/master/src/phpDocumentor/Reflection/DocBlock.php Original Method
41
	 * @param \ReflectionFunctionAbstract|\ReflectionClass|\ReflectionProperty|string $docblock
42
	 * @throws \InvalidArgumentException if there is no getDocCommect() method available
43
	 */
44 12
	protected function parse($docblock) {
45 12
		if (is_object($docblock)) {
46 6
			if (!method_exists($docblock, 'getDocComment')) {
47 1
				throw new \InvalidArgumentException('Invalid object passed; the given ' .
48 1
						'reflector must support the getDocComment method');
49
			}
50
		
51 1
			$docblock = $docblock->getDocComment();
52 1
		}
53
		
54 11
		$docblock = $this->cleanInput($docblock);
55
		
56 11
		list($short, $long, $tags) = $this->splitDocBlock($docblock);
57 11
		$this->shortDescription = $short;
58 11
		$this->longDescription = $long;
59 11
		$this->parseTags($tags);
60 10
	}
61
	
62
	/**
63
	 * Strips the asterisks from the DocBlock comment.
64
	 * 
65
	 * @see https://github.com/phpDocumentor/ReflectionDocBlock/blob/master/src/phpDocumentor/Reflection/DocBlock.php Original Method
66
	 * @param string $comment String containing the comment text.
67
	 * @return string
68
	 */
69 11
	protected function cleanInput($comment) {
70 11
		$comment = trim(preg_replace('#[ \t]*(?:\/\*\*|\*\/|\*)?[ \t]{0,1}(.*)?#u',	'$1', $comment));
71
	
72
		// reg ex above is not able to remove */ from a single line docblock
73 11
		if (substr($comment, -2) == '*/') {
74 1
			$comment = trim(substr($comment, 0, -2));
75 1
		}
76
	
77
		// normalize strings
78 11
		$comment = str_replace(array("\r\n", "\r"), "\n", $comment);
79
	
80 11
		return $comment;
81
	}
82
	
83
	/**
84
	 * Splits the Docblock into a short description, long description and
85
	 * block of tags.
86
	 * 
87
	 * @see https://github.com/phpDocumentor/ReflectionDocBlock/blob/master/src/phpDocumentor/Reflection/DocBlock.php Original Method
88
	 *
89
	 * @param string $comment Comment to split into the sub-parts.
90
	 *
91
	 * @author RichardJ Special thanks to RichardJ for the regex responsible
92
	 *     for the split.
93
	 *
94
	 * @return string[] containing the short-, long description and an element
95
	 *     containing the tags.
96
	 */
97 10
	protected function splitDocBlock($comment) {
98 10
		$matches = [];
99
		
100 10
		if (strpos($comment, '@') === 0) {
101 1
			$matches = array('', '', $comment);
102 1
		} else {
103
			// clears all extra horizontal whitespace from the line endings
104
			// to prevent parsing issues
105 9
			$comment = preg_replace('/\h*$/Sum', '', $comment);
106
			
107
			/*
108
			 * Splits the docblock into a short description, long description and
109
			 * tags section
110
			 * - The short description is started from the first character until
111
			 *   a dot is encountered followed by a newline OR
112
			 *   two consecutive newlines (horizontal whitespace is taken into
113
			 *   account to consider spacing errors)
114
			 * - The long description, any character until a new line is
115
			 *   encountered followed by an @ and word characters (a tag).
116
			 *   This is optional.
117
			 * - Tags; the remaining characters
118
			 *
119
			 * Big thanks to RichardJ for contributing this Regular Expression
120
			 */
121 9
			preg_match(
122
				'/
123
		        \A (
124
		          [^\n.]+
125
		          (?:
126
		            (?! \. \n | \n{2} ) # disallow the first seperator here
127
		            [\n.] (?! [ \t]* @\pL ) # disallow second seperator
128
		            [^\n.]+
129
		          )*
130
		          \.?
131
		        )
132
		        (?:
133
		          \s* # first seperator (actually newlines but it\'s all whitespace)
134
		          (?! @\pL ) # disallow the rest, to make sure this one doesn\'t match,
135
		          #if it doesn\'t exist
136
		          (
137
		            [^\n]+
138
		            (?: \n+
139
		              (?! [ \t]* @\pL ) # disallow second seperator (@param)
140
		              [^\n]+
141
		            )*
142
		          )
143
		        )?
144
		        (\s+ [\s\S]*)? # everything that follows
145 9
		        /ux',
146 9
				$comment,
147
				$matches
148 9
			);
149 9
			array_shift($matches);
150
		}
151
	
152 10
		while (count($matches) < 3) {
153 7
			$matches[] = '';
154 7
		}
155 10
		return $matches;
156
	}
157
	
158
	/**
159
	 * Parses the tags
160
	 * 
161
	 * @see https://github.com/phpDocumentor/ReflectionDocBlock/blob/master/src/phpDocumentor/Reflection/DocBlock.php Original Method
162
	 * @param string $tags
163
	 * @throws \LogicException
164
	 * @throws \InvalidArgumentException
165
	 */
166 11
	protected function parseTags($tags) {
167 11
		$tags = trim($tags);
168 11
		if (!empty($tags)) {
169
			
170
			// sanitize lines
171 4
			$result = [];
172 4
			foreach (explode("\n", $tags) as $line) {
173 4
				if ($this->isTagLine($line) || count($result) == 0) {
174 4
					$result[] = $line;
175 4
				} else {
176 1
					$result[count($result) - 1] .= PHP_EOL . $line;
177
				}
178 4
			}
179
180
			// create proper Tag objects
181 4
			if (count($result)) {
182 4
				$this->tags->clear();
183 4
				foreach ($result as $line) {
184 4
					$this->tags->add($this->parseTag($line)); 
185 3
				}
186 3
			}
187 3
		}
188
		
189
		
190 10
	}
191
	
192
	/**
193
	 * Checks whether the given line is a tag line (= starts with @) or not
194
	 * 
195
	 * @param string $line
196
	 * @return boolean
197
	 */
198 4
	protected function isTagLine($line) {
199 4
		return isset($line[0]) && $line[0] == '@';
200
	}
201
	
202
	/**
203
	 * Parses an individual tag line
204
	 * 
205
	 * @param string $line
206
	 * @throws \InvalidArgumentException
207
	 * @return \gossi\docblock\tags\AbstractTag
208
	 */
209 4
	protected function parseTag($line) {
210 4
		$matches = [];
211 4
		if (!preg_match('/^@(' . self::REGEX_TAGNAME . ')(?:\s*([^\s].*)|$)?/us', $line, $matches)) {
212 1
			throw new \InvalidArgumentException('Invalid tag line detected: ' . $line);
213
		}
214
		
215 3
		$tagName = $matches[1];
216 3
		$content = isset($matches[2]) ? $matches[2] : '';
217
		
218 3
		return TagFactory::create($tagName, $content);
219
	}
220
	
221
	/**
222
	 * Returns the short description
223
	 * 
224
	 * @return string the short description
225
	 */
226 3
	public function getShortDescription() {
227 3
		return $this->shortDescription;
228
	}
229
	
230
	/**
231
	 * Sets the short description
232
	 * 
233
	 * @param string $description the new description     
234
	 * @return $this   	
235
	 */
236 2
	public function setShortDescription($description) {
237 2
		$this->shortDescription = $description;
238 2
		return $this;
239
	}
240
	
241
	/**
242
	 * Returns the long description
243
	 *
244
	 * @return string the long description
245
	 */
246 2
	public function getLongDescription() {
247 2
		return $this->longDescription;
248
	}
249
	
250
	/**
251
	 * Sets the long description
252
	 * 
253
	 * @param string $description the new description        	
254
	 */
255 2
	public function setLongDescription($description) {
256 2
		$this->longDescription = $description;
257 2
		return $this;
258
	}
259
260
	/**
261
	 * Adds a tag to this docblock
262
	 * 
263
	 * @param AbstractTag $tag
264
	 * @return $this
265
	 */
266 4
	public function appendTag(AbstractTag $tag) {
267 4
		$this->tags->add($tag);
268 4
		return $this;
269
	}
270
271
	/**
272
	 * removes tags (by tag name)
273
	 *
274
	 * @param string $tagName
275
	 */
276
	public function removeTags($tagName = null) {
277
		$this->tags = $this->tags->filter(function ($tag) use ($tagName) {
278
			return $tagName !== $tag->getTagName();
279
		});
280
	}
281
282
	/**
283
	 * Checks whether a tag is present
284
	 * 
285
	 * @param string $tagName
286
	 * @return boolean
287
	 */
288 1
	public function hasTag($tagName) {
289
		return $this->tags->search($tagName, function (AbstractTag $tag, $query) {
290 1
			return $tag->getTagName() == $query;
291 1
		});
292
	}
293
	
294
	/**
295
	 * Gets tags (by tag name)
296
	 * 
297
	 * @param string $tagName
298
	 * @return ArrayList the tags
299
	 */
300 1
	public function getTags($tagName = null) {
301
		return $this->tags->filter(function ($tag) use ($tagName) {
302 1
			return $tagName === null || $tag->getTagName() == $tagName;
303 1
		});
304
	}
305
306
	/**
307
	 * A list of tags sorted by tag-name
308
	 * 
309
	 * @return ArrayList
310
	 */
311 6
	public function getSortedTags() {
312 6
		if ($this->comparator === null) {
313 6
			$this->comparator = new TagNameComparator(); 
314 6
		}
315
316
		// 1) group by tag name
317 6
		$group = new Map();
318 6
		foreach ($this->tags as $tag) {
319 5
			if (!$group->has($tag->getTagName())) {
320 5
				$group->set($tag->getTagName(), new ArrayList());
321 5
			}
322
			
323 5
			$group->get($tag->getTagName())->add($tag);
324 6
		} 
325
		
326
		// 2) Sort the group by tag name
327 6
		$group->sortKeys(new TagNameComparator());
328
		
329
		// 3) flatten the group
330 6
		$sorted = new ArrayList();
331 6
		foreach ($group->values() as $tags) {
332 5
			$sorted->addAll($tags);
333 6
		}
334
		
335 6
		return $sorted;
336
	}
337
	
338
	/**
339
	 * Returns true when there is no content in the docblock
340
	 *  
341
	 * @return boolean
342
	 */
343 1
	public function isEmpty() {
344 1
		return empty($this->shortDescription) 
345 1
				&& empty($this->longDescription) 
346 1
				&& $this->tags->size() == 0;
347
	}
348
	
349
	/**
350
	 * Returns the string version of the docblock
351
	 * 
352
	 * @return string
353
	 */
354 5
	public function toString() {
355 5
		$docblock = "/**\n";
356
		
357
		// short description
358 5
		$short = trim($this->shortDescription);
359 5
		if (!empty($short)) {
360 3
			$docblock .= $this->writeLines(explode("\n", $short));
361 3
		}
362
		
363
		// short description
364 5
		$long = trim($this->longDescription);
365 5
		if (!empty($long)) {
366 2
			$docblock .= $this->writeLines(explode("\n", $long), !empty($short));
367 2
		}
368
		
369
		// tags
370 5
		$tags = $this->getSortedTags()->map(function($tag) {
371 4
			return $tag->toString();
372 5
		});
373
		
374 5
		if (!$tags->isEmpty()) {
375 4
			$docblock .= $this->writeLines($tags->toArray(), !empty($short) || !empty($long));
376 4
		}
377
378 5
		$docblock .= ' */';
379
		
380 5
		return $docblock;
381
	}
382
	
383
	/**
384
	 * Writes multiple lines with ' * ' prefixed for docblock
385
	 * 
386
	 * @param string[] $lines the lines to be written
387
	 * @param boolean $newline if a new line should be added before
388
	 * @return string the lines as string
389
	 */
390 5
	protected function writeLines($lines, $newline = false) {
391 5
		$docblock = '';
392 5
		if ($newline) {
393 3
			$docblock .= " *\n";
394 3
		}
395
396 5
		foreach ($lines as $line) {
397 5
			if (strpos($line, "\n")) {
398 1
				$sublines = explode("\n", $line);
399 1
				$line = array_shift($sublines);
400 1
				$docblock .= ' * ' . $line . "\n";
401 1
				$docblock .= $this->writeLines($sublines);
402 1
			} else {
403 5
				$docblock .= ' * ' . $line . "\n";
404
			}
405 5
		}
406
		
407 5
		return $docblock;
408
	}
409
	
410
	/**
411
	 * Magic toString() method
412
	 * 
413
	 * @return string
414
	 */
415 1
	public function __toString() {
416 1
		return $this->toString();
417
	}
418
	
419
}
420