Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.19% covered (success)
89.19%
66 / 74
0.00% covered (danger)
0.00%
0 / 1
CRAP
0.00% covered (danger)
0.00%
0 / 1
ClassReflection
89.19% covered (success)
89.19%
66 / 74
0.00% covered (danger)
0.00%
0 / 1
38.73
0.00% covered (danger)
0.00%
0 / 1
 parse
89.19% covered (success)
89.19%
66 / 74
0.00% covered (danger)
0.00%
0 / 1
38.73
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;
18use Pop\Code\Reflection\Support\UseStatementParser;
19use Pop\Code\Reflection\Support\AttributeCollector;
20use Pop\Code\Reflection\Support\InterfaceHierarchyResolver;
21use Pop\Code\Reflection\Support\NamespaceImportResolver;
22use ReflectionException;
23
24/**
25 * Class reflection code class
26 *
27 * @category   Pop
28 * @package    Pop\Code
29 * @author     Nick Sagona, III <dev@noladev.com>
30 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
31 * @license    https://www.popphp.org/license     New BSD License
32 * @version    6.0.0
33 */
34class ClassReflection extends AbstractReflection
35{
36
37    /**
38     * Method to parse a class
39     *
40     * @param  mixed   $code
41     * @param  ?string $name
42     * @throws Exception|ReflectionException
43     * @return Generator\ClassGenerator
44     */
45    public static function parse(mixed $code, ?string $name = null): Generator\ClassGenerator
46    {
47        $reflection     = new \ReflectionClass($code);
48
49        if ($reflection->isEnum()) {
50            throw new Exception('Error: The code is an enum; use Reflection::createEnum() instead.');
51        }
52
53        $reflectionName = $reflection->getShortName();
54        $reflectionFile = $reflection->getFileName();
55        $fileContents   = null;
56
57        if (!empty($reflectionFile) && file_exists($reflectionFile)) {
58            $fileContents = file_get_contents($reflectionFile);
59        }
60
61        if (($name === null) && !empty($reflectionName)) {
62            $name = $reflectionName;
63        }
64
65        if (($reflection->isInterface()) || ($reflection->isTrait())) {
66            throw new Exception('Error: The code must be a class, not an interface or trait.');
67        }
68
69        $class = new Generator\ClassGenerator($name);
70
71        // Detect and set namespace
72        if (($reflection->inNamespace()) && ($fileContents !== null)) {
73            $class->setNamespace(NamespaceReflection::parse($fileContents, $reflection->getNamespaceName()));
74        }
75
76        // Shared across attributes, the parent class, and interfaces below, so a same-short-name
77        // collision between e.g. the parent class and an attribute is caught too, not just among
78        // attributes alone. See NamespaceImportResolver for why: two `use` statements for
79        // different classes sharing one short name is a PHP fatal error, so the second one must
80        // fall back to a fully-qualified reference instead of colliding.
81        $importResolver = new NamespaceImportResolver();
82
83        // Detect attributes
84        foreach ($reflection->getAttributes() as $reflectionAttribute) {
85            [$attributeReference, $needsImport] = $importResolver->resolve($reflectionAttribute->getName(), $reflection->getNamespaceName());
86            if ($needsImport) {
87                if (!$class->hasNamespace()) {
88                    $class->setNamespace(new Generator\NamespaceGenerator());
89                }
90                $class->getNamespace()->addUse($reflectionAttribute->getName());
91            }
92            $class->addAttribute(AttributeCollector::build($reflectionAttribute, $attributeReference));
93        }
94
95        // Detect and set the class doc block
96        $classDocBlock = $reflection->getDocComment();
97        if (!empty($classDocBlock) && (str_contains($classDocBlock, '/*'))) {
98            $class->setDocblock(DocblockReflection::parse($classDocBlock));
99        }
100
101        if ($reflection->isAbstract()) {
102            $class->setAsAbstract(true);
103        } else if ($reflection->isFinal()) {
104            $class->setAsFinal(true);
105        }
106
107        if ($reflection->isReadOnly()) {
108            $class->setAsReadonly(true);
109        }
110
111        // Detect parent class
112        $parent = $reflection->getParentClass();
113        if ($parent !== false) {
114            [$parentReference, $needsImport] = $importResolver->resolve($parent->getName(), $reflection->getNamespaceName());
115            if ($needsImport) {
116                if (!$class->hasNamespace()) {
117                    $class->setNamespace(new Generator\NamespaceGenerator());
118                }
119                $class->getNamespace()->addUse($parent->getName());
120            }
121            $class->setParent($parentReference);
122        }
123
124        // Detect implemented interfaces -- getInterfaces() returns the full transitive closure
125        // (every interface reachable via this class, its parent chain, and any interface's own
126        // extends), not just what this class itself directly declares in `implements`. A
127        // candidate is kept only if it isn't already provided by the parent class (inherited, not
128        // re-declared) and isn't reachable via another candidate already in this class's own set
129        // (implied by that candidate's own extends, not itself a distinct direct implements) --
130        // see InterfaceHierarchyResolver.
131        $interfaces           = $reflection->getInterfaces();
132        $parentInterfaceNames = ($parent !== false) ? $parent->getInterfaceNames() : [];
133        $interfacesAry        = [];
134        foreach (InterfaceHierarchyResolver::direct($interfaces, $parentInterfaceNames) as $candidateName => $interface) {
135            [$interfaceReference, $needsImport] = $importResolver->resolve($candidateName, $reflection->getNamespaceName());
136            if ($needsImport) {
137                if (!$class->hasNamespace()) {
138                    $class->setNamespace(new Generator\NamespaceGenerator());
139                }
140                $class->getNamespace()->addUse($candidateName);
141            }
142            $interfacesAry[] = $interfaceReference;
143        }
144        $class->addInterfaces($interfacesAry);
145
146        // Detect used traits
147        if ($fileContents !== null) {
148            foreach (UseStatementParser::parse($fileContents) as $use => $as) {
149                $class->addUse($use, $as);
150            }
151        }
152
153        // Detect constants -- getReflectionConstants() includes inherited constants; keep only
154        // ones actually declared on this class (a trait-provided constant still reports its
155        // declaring class as this one, since PHP flattens trait members into the using class, so
156        // this filter only excludes constants inherited from a parent class, not trait ones).
157        foreach ($reflection->getReflectionConstants() as $constant) {
158            if ($constant->getDeclaringClass()->getName() !== $reflection->getName()) {
159                continue;
160            }
161            $class->addConstant(ConstantReflection::parse($constant));
162        }
163
164        // Detect properties -- getProperties() includes inherited properties; same declaring-class
165        // filter as constants above, with the same trait-member caveat.
166        $classIsReadonly = $reflection->isReadOnly();
167        foreach ($reflection->getProperties() as $property) {
168            if ($property->isPromoted() || ($property->getDeclaringClass()->getName() !== $reflection->getName())) {
169                continue;
170            }
171            $value             = $property->hasDefaultValue() ? $property->getDefaultValue() : null;
172            $propertyGenerator = PropertyReflection::parse($property, $property->getName(), $value);
173            if ($classIsReadonly) {
174                // Every property in a readonly class reports isReadOnly()=true regardless of whether it
175                // says so explicitly; rely on the class-level keyword instead of stuttering it per-property.
176                // NOTE: deviates from the task brief's literal `setAsReadonly(false)` — that call also
177                // re-enables PropertyGenerator's nullable-widening/default-value logic (gated on the same
178                // flag), which produced invalid PHP (a default value on a readonly property). Verified
179                // empirically; see task-3-report.md. suppressReadonlyKeyword() only hides the redundant
180                // keyword while keeping the property's true readonly semantics for rendering.
181                $propertyGenerator->suppressReadonlyKeyword();
182            }
183            $class->addProperty($propertyGenerator);
184        }
185
186        // Detect methods -- getMethods() includes inherited methods; same declaring-class filter,
187        // same trait-member caveat. An overridden method (e.g. implementing an abstract parent
188        // method) still reports its declaring class as this one, since the override itself is a
189        // real declaration here.
190        $methods = $reflection->getMethods();
191        if (count($methods) > 0) {
192            foreach ($methods as $method) {
193                if ($method->getDeclaringClass()->getName() !== $reflection->getName()) {
194                    continue;
195                }
196                $class->addMethod(MethodReflection::parse($method, $method->name));
197            }
198        }
199
200        return $class;
201    }
202
203}