Skip to content

Commit d98d249

Browse files
committed
feat(helper): Add Reflector helper for class, property, type, and attribute metadata introspection.
1 parent 28d3a10 commit d98d249

11 files changed

Lines changed: 794 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## 0.2.1 Under development
44

5+
- Enh #0: Add `Reflector` helper for class, property, type, and attribute metadata introspection (@terabytesoftw)
6+
57
## 0.2.0 February 25, 2024
68

79
- Enh #6: Refactor codebase to improve performance (@terabytesoftw)

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "php-forge/helper",
33
"type": "library",
4-
"description": "PHP Helper.",
4+
"description": "PHPForge Helpers for Common PHP Tasks.",
55
"keywords": [
66
"php-forge",
77
"helper",

src/Exception/Message.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ enum Message: string
2323
*/
2424
case PASSWORD_LENGTH_TOO_SHORT = "Password length must be at least '%d' characters.";
2525

26+
/**
27+
* Error message for missing reflected property.
28+
*
29+
* Format: "Property '%s' does not exist in '%s'."
30+
*/
31+
case REFLECTOR_PROPERTY_NOT_FOUND = "Property '%s' does not exist in '%s'.";
32+
33+
/**
34+
* Error message for invalid reflection target.
35+
*
36+
* Format: "Invalid reflection target '%s'."
37+
*/
38+
case REFLECTOR_TARGET_INVALID = "Invalid reflection target '%s'.";
39+
2640
/**
2741
* Returns the formatted message string for the error case.
2842
*

src/Reflector.php

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPForge\Helper;
6+
7+
use InvalidArgumentException;
8+
use PHPForge\Helper\Exception\Message;
9+
use ReflectionAttribute;
10+
use ReflectionClass;
11+
use ReflectionIntersectionType;
12+
use ReflectionNamedType;
13+
use ReflectionProperty;
14+
use ReflectionUnionType;
15+
16+
use function array_map;
17+
use function class_exists;
18+
use function count;
19+
use function is_object;
20+
21+
/**
22+
* Provides lightweight reflection utilities for classes and properties.
23+
*
24+
* Usage examples:
25+
* ```php
26+
* // Get short class name
27+
* $shortName = \PHPForge\Helper\Reflector::shortName(SomeClass::class);
28+
*
29+
* // Check if a property exists
30+
* $hasProperty = \PHPForge\Helper\Reflector::hasProperty(SomeClass::class, 'someProperty');
31+
* ```
32+
*
33+
* @copyright Copyright (C) 2026 Terabytesoftw.
34+
* @license https://opensource.org/license/bsd-3-clause BSD 3-Clause License.
35+
*/
36+
final class Reflector
37+
{
38+
/**
39+
* Maximum number of cached reflection classes.
40+
*/
41+
private const MAX_REFLECTION_CLASS_CACHE_SIZE = 1024;
42+
43+
/**
44+
* Stores reflection instances keyed by class name.
45+
*
46+
* @var array<string, ReflectionClass<object>>
47+
*/
48+
private static array $reflectionClassCache = [];
49+
50+
/**
51+
* Returns class attributes, optionally filtered by attribute class.
52+
*
53+
* Usage example:
54+
* ```php
55+
* $attributes = \PHPForge\Helper\Reflector::classAttributes(
56+
* SomeClass::class,
57+
* SomeAttribute::class,
58+
* ReflectionAttribute::IS_INSTANCEOF,
59+
* );
60+
* ```
61+
*
62+
* @return array Class attributes.
63+
*
64+
* @phpstan-return list<ReflectionAttribute<object>>
65+
*/
66+
public static function classAttributes(object|string $class, string|null $attribute = null, int $flags = 0): array
67+
{
68+
$reflectionClass = self::reflectionClass($class);
69+
70+
if ($attribute === null) {
71+
return $reflectionClass->getAttributes();
72+
}
73+
74+
return $reflectionClass->getAttributes($attribute, $flags);
75+
}
76+
77+
/**
78+
* Returns the first matching instantiated property attribute.
79+
*
80+
* Usage example:
81+
* ```php
82+
* $attributeInstance = \PHPForge\Helper\Reflector::firstPropertyAttribute(
83+
* SomeClass::class,
84+
* 'someProperty',
85+
* SomeAttribute::class,
86+
* ReflectionAttribute::IS_INSTANCEOF,
87+
* );
88+
* ```
89+
*
90+
* @return object|null First matching instantiated attribute, or `null` if no matching attribute is found.
91+
*/
92+
public static function firstPropertyAttribute(
93+
object|string $class,
94+
string $property,
95+
string $attribute,
96+
int $flags = 0,
97+
): object|null {
98+
$attributes = self::propertyAttributes($class, $property, $attribute, $flags);
99+
100+
if ($attributes === []) {
101+
return null;
102+
}
103+
104+
return $attributes[0]->newInstance();
105+
}
106+
107+
/**
108+
* Checks whether a property exists on the class.
109+
*
110+
* Usage example:
111+
* ```php
112+
* $hasProperty = \PHPForge\Helper\Reflector::hasProperty(SomeClass::class, 'someProperty');
113+
* ```
114+
*
115+
* @return bool `true` if the property exists, `false` otherwise.
116+
*/
117+
public static function hasProperty(object|string $class, string $property): bool
118+
{
119+
return self::reflectionClass($class)->hasProperty($property);
120+
}
121+
122+
/**
123+
* Returns reflected properties, optionally filtered by visibility flags.
124+
*
125+
* Usage example:
126+
* ```php
127+
* $publicProperties = \PHPForge\Helper\Reflector::properties(SomeClass::class, ReflectionProperty::IS_PUBLIC);
128+
* ```
129+
*
130+
* @return array Reflected properties.
131+
*
132+
* @phpstan-return list<ReflectionProperty>
133+
*/
134+
public static function properties(object|string $class, int|null $filter = null): array
135+
{
136+
$reflectionClass = self::reflectionClass($class);
137+
138+
if ($filter === null) {
139+
return $reflectionClass->getProperties();
140+
}
141+
142+
return $reflectionClass->getProperties($filter);
143+
}
144+
145+
/**
146+
* Returns a reflected property.
147+
*
148+
* Usage example:
149+
* ```php
150+
* $property = \PHPForge\Helper\Reflector::property(SomeClass::class, 'someProperty');
151+
* ```
152+
*
153+
* @throws InvalidArgumentException if the property does not exist.
154+
*
155+
* @return ReflectionProperty Reflected property.
156+
*/
157+
public static function property(object|string $class, string $property): ReflectionProperty
158+
{
159+
$reflectionClass = self::reflectionClass($class);
160+
161+
if (!$reflectionClass->hasProperty($property)) {
162+
throw new InvalidArgumentException(
163+
Message::REFLECTOR_PROPERTY_NOT_FOUND->getMessage($property, $reflectionClass->getName()),
164+
);
165+
}
166+
167+
return $reflectionClass->getProperty($property);
168+
}
169+
170+
/**
171+
* Returns instantiated property attributes.
172+
*
173+
* Usage example:
174+
* ```php
175+
* $attributeInstances = \PHPForge\Helper\Reflector::propertyAttributeInstances(
176+
* SomeClass::class,
177+
* 'someProperty',
178+
* SomeAttribute::class,
179+
* ReflectionAttribute::IS_INSTANCEOF,
180+
* );
181+
*
182+
* @return array Instantiated property attributes.
183+
*
184+
* @phpstan-return list<object>
185+
*/
186+
public static function propertyAttributeInstances(
187+
object|string $class,
188+
string $property,
189+
string|null $attribute = null,
190+
int $flags = 0,
191+
): array {
192+
$attributes = self::propertyAttributes($class, $property, $attribute, $flags);
193+
194+
return array_map(
195+
static fn(ReflectionAttribute $reflectionAttribute): object => $reflectionAttribute->newInstance(),
196+
$attributes,
197+
);
198+
}
199+
200+
/**
201+
* Returns property attributes, optionally filtered by attribute class.
202+
*
203+
* Usage example:
204+
* ```php
205+
* $attributes = \PHPForge\Helper\Reflector::propertyAttributes(
206+
* SomeClass::class,
207+
* 'someProperty',
208+
* SomeAttribute::class,
209+
* ReflectionAttribute::IS_INSTANCEOF,
210+
* );
211+
* ```
212+
*
213+
* @return array Property attributes.
214+
*
215+
* @phpstan-return list<ReflectionAttribute<object>>
216+
*/
217+
public static function propertyAttributes(
218+
object|string $class,
219+
string $property,
220+
string|null $attribute = null,
221+
int $flags = 0,
222+
): array {
223+
$reflectionProperty = self::property($class, $property);
224+
225+
if ($attribute === null) {
226+
return $reflectionProperty->getAttributes();
227+
}
228+
229+
return $reflectionProperty->getAttributes($attribute, $flags);
230+
}
231+
232+
/**
233+
* Returns property type names.
234+
*
235+
* For nullable named types, the list includes `'null'` as an extra element.
236+
*
237+
* Usage example:
238+
* ```php
239+
* $typeNames = \PHPForge\Helper\Reflector::propertyTypeNames(SomeClass::class, 'someProperty');
240+
* ```
241+
*
242+
* @return list<string> Type names of the property, or an empty list if the property has no type declaration.
243+
*
244+
* @phpstan-return list<string>
245+
*/
246+
public static function propertyTypeNames(object|string $class, string $property): array
247+
{
248+
$type = self::property($class, $property)->getType();
249+
250+
if ($type instanceof ReflectionNamedType) {
251+
$typeName = $type->getName();
252+
253+
if ($type->allowsNull() && $typeName !== 'null') {
254+
return [$typeName, 'null'];
255+
}
256+
257+
return [$typeName];
258+
}
259+
260+
if ($type instanceof ReflectionUnionType || $type instanceof ReflectionIntersectionType) {
261+
/** @var list<string> $typeNames */
262+
$typeNames = [];
263+
264+
foreach ($type->getTypes() as $nestedType) {
265+
if ($nestedType instanceof ReflectionNamedType) {
266+
$typeNames[] = $nestedType->getName();
267+
}
268+
}
269+
270+
return $typeNames;
271+
}
272+
273+
return [];
274+
}
275+
276+
/**
277+
* Returns the short class name.
278+
*
279+
* Usage example:
280+
* ```php
281+
* $shortName = \PHPForge\Helper\Reflector::shortName(SomeClass::class);
282+
* ```
283+
*
284+
* @return string Short name of the class, or an empty string for anonymous classes.
285+
*/
286+
public static function shortName(object|string $class): string
287+
{
288+
$reflectionClass = self::reflectionClass($class);
289+
290+
return $reflectionClass->isAnonymous() ? '' : $reflectionClass->getShortName();
291+
}
292+
293+
/**
294+
* Creates a reflection class from an object or class name.
295+
*
296+
* @param object|string $class Object instance or class name to reflect.
297+
*
298+
* @throws InvalidArgumentException if the class target is invalid.
299+
*
300+
* @return ReflectionClass<object> Reflection class instance.
301+
*/
302+
private static function reflectionClass(object|string $class): ReflectionClass
303+
{
304+
$className = is_object($class) ? $class::class : $class;
305+
306+
if (!class_exists($className)) {
307+
throw new InvalidArgumentException(
308+
Message::REFLECTOR_TARGET_INVALID->getMessage($className),
309+
);
310+
}
311+
312+
if (isset(self::$reflectionClassCache[$className])) {
313+
return self::$reflectionClassCache[$className];
314+
}
315+
316+
$reflectionClass = new ReflectionClass($className);
317+
318+
if (!$reflectionClass->isAnonymous()) {
319+
if (count(self::$reflectionClassCache) >= self::MAX_REFLECTION_CLASS_CACHE_SIZE) {
320+
self::$reflectionClassCache = [];
321+
}
322+
323+
self::$reflectionClassCache[$className] = $reflectionClass;
324+
}
325+
326+
return $reflectionClass;
327+
}
328+
}

0 commit comments

Comments
 (0)