Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.28% covered (success)
98.28%
57 / 58
75.00% covered (success)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
DocblockReflection
98.28% covered (success)
98.28%
57 / 58
75.00% covered (success)
75.00%
3 / 4
22
0.00% covered (danger)
0.00%
0 / 1
 parse
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
11
 parseParamTag
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
5.01
 parseReturnTag
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 parseGenericTag
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <dev@noladev.com>
8 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Code\Reflection;
16
17use Pop\Code\Generator\DocblockGenerator;
18
19/**
20 * Docblock reflection code class
21 *
22 * @category   Pop
23 * @package    Pop\Code
24 * @author     Nick Sagona, III <dev@noladev.com>
25 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    6.0.0
28 */
29class DocblockReflection extends AbstractReflection
30{
31
32    /**
33     * Method to parse a docblock
34     *
35     * @param  mixed $code
36     * @param  ?int  $forceIndent
37     * @throws Exception
38     * @return DocblockGenerator
39     */
40    public static function parse(mixed $code, ?int $forceIndent = null): DocblockGenerator
41    {
42        if ((!str_contains($code, '/*')) || (!str_contains($code, '*/'))) {
43            throw new Exception('The docblock is not in the correct format.');
44        }
45
46        $atPos = strpos($code, '@');
47
48        // Parse the description, if any. A docblock with no @-tags at all (just a summary line,
49        // as is common on enum cases) still has a description to extract, so this must not be
50        // gated on the presence of '@' — only the *extent* of the description text depends on it.
51        $desc    = ($atPos !== false) ? substr($code, 0, $atPos) : $code;
52        $desc    = str_replace('/*', '', $desc);
53        $desc    = str_replace('*/', '', $desc);
54        $desc    = str_replace(PHP_EOL . ' * ', ' ', $desc);
55        $desc    = trim(str_replace('*', '', $desc));
56        $descAry = explode("\n", $desc);
57
58        $formattedDesc = null;
59        foreach ($descAry as $line) {
60            $formattedDesc .= ' ' . trim($line);
61        }
62
63        $formattedDesc = trim($formattedDesc);
64        if ($formattedDesc === '') {
65            $formattedDesc = null;
66        }
67
68        // Get the indentation, if any, and create docblock object
69        $indent   = (empty($forceIndent)) ? strlen(substr($code, 0, strpos($code, '/'))) : $forceIndent;
70        $docblock = new DocblockGenerator($formattedDesc, $indent);
71
72        // Get the tags, if any
73        if ($atPos !== false) {
74            $tags    = substr($code, $atPos);
75            $tags    = substr($tags, 0, strpos($tags, '*/'));
76            $tags    = str_replace('*', '', $tags);
77            $tagsAry = explode("\n", $tags);
78
79            foreach ($tagsAry as $value) {
80                $value = trim(str_replace('@', '', $value));
81                if (stripos($value, 'param') !== false) {
82                    self::parseParamTag($docblock, $value);
83                } else if (stripos($value, 'return') !== false) {
84                    self::parseReturnTag($docblock, $value);
85                } else {
86                    self::parseGenericTag($docblock, $value);
87                }
88            }
89        }
90
91        return $docblock;
92    }
93
94    /**
95     * Parse a single "param" tag line and add it to the docblock
96     *
97     * @param  DocblockGenerator $docblock
98     * @param  string            $value
99     * @return void
100     */
101    private static function parseParamTag(DocblockGenerator $docblock, string $value): void
102    {
103        $paramTag      = trim(str_replace('param', '', $value));
104        $paramSpacePos = strpos($paramTag, ' ');
105        $paramType     = trim($paramSpacePos !== false ? substr($paramTag, 0, $paramSpacePos) : $paramTag);
106        $varName       = null;
107        $paramDesc     = null;
108
109        if ($paramSpacePos !== false) {
110            $varName = trim(substr($paramTag, $paramSpacePos));
111            if (str_contains($varName, ' ')) {
112                // $varName previously kept the trailing description text attached (only
113                // $paramDesc was extracted, never trimmed back off of $varName itself) -- e.g.
114                // "@param string $name The name to use" stored 'var' as "$name The name to use"
115                // instead of just "$name", duplicating the description once concatenated at
116                // render time, and also breaking the stale-@param-on-re-add dedup, which matches
117                // on the variable name exactly.
118                $spacePos  = strpos($varName, ' ');
119                $paramDesc = trim(substr($varName, $spacePos));
120                $varName   = trim(substr($varName, 0, $spacePos));
121            }
122        } else if (str_starts_with($paramTag, '$')) {
123            // A bare "@param $var" with no type at all -- $paramTag is the variable name, not a
124            // type. Without this check it fell into the branch below and was stored as the type
125            // instead, leaving 'var' unset -- which meant a later addArgument() call for the same
126            // parameter (which correctly computes the real variable name) couldn't recognize this
127            // as the same param to replace, and appended a second @param line instead.
128            $paramType = null;
129            $varName   = $paramTag;
130        } else {
131            $paramType = $paramTag;
132        }
133
134        $docblock->addParam($paramType, $varName, $paramDesc);
135    }
136
137    /**
138     * Parse a single "return" tag line and set it on the docblock
139     *
140     * @param  DocblockGenerator $docblock
141     * @param  string            $value
142     * @return void
143     */
144    private static function parseReturnTag(DocblockGenerator $docblock, string $value): void
145    {
146        $returnTag = trim(str_replace('return', '', $value));
147        if (str_contains($returnTag, ' ')) {
148            $returnType = substr($returnTag, 0, strpos($returnTag, ' '));
149            $returnDesc = trim(str_replace($returnType, '', $returnTag));
150        } else {
151            $returnType = $returnTag;
152            $returnDesc = null;
153        }
154
155        $docblock->setReturn($returnType, $returnDesc);
156    }
157
158    /**
159     * Parse any other tag line and add it to the docblock
160     *
161     * @param  DocblockGenerator $docblock
162     * @param  string            $value
163     * @return void
164     */
165    private static function parseGenericTag(DocblockGenerator $docblock, string $value): void
166    {
167        $tagSpacePos = strpos($value, ' ');
168        $tagName     = trim($tagSpacePos !== false ? substr($value, 0, $tagSpacePos) : $value);
169        $tagDesc     = trim(str_replace($tagName, '', $value));
170
171        if (!empty($tagName) && !empty($tagDesc)) {
172            $docblock->addTag($tagName, $tagDesc);
173        }
174    }
175
176}