├── CHANGELOG.md ├── LICENSE ├── composer.json └── src ├── ImportResolver.php └── ReflectionTools.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.5.4](https://github.com/brick/reflection/releases/tag/0.5.4) - 2024-05-10 4 | 5 | ✨ **Compatibility improvements** 6 | 7 | - Compatibility with `brick/varexporter` version `0.5` 8 | 9 | ## [0.5.3](https://github.com/brick/reflection/releases/tag/0.5.3) - 2024-05-03 10 | 11 | ✨ **New features** 12 | 13 | - Support for `ReflectionClassConstant` as Reflector in `ImportResolver` 14 | 15 | ## [0.5.2](https://github.com/brick/reflection/releases/tag/0.5.2) - 2024-05-02 16 | 17 | ✨ **Compatibility improvements** 18 | 19 | - Compatibility with `brick/varexporter` version `0.4` 20 | - Compatibility with `doctrine/annotations` version `2.x` 21 | 22 | ## [0.5.1](https://github.com/brick/reflection/releases/tag/0.5.1) - 2023-01-16 23 | 24 | ✨ **New features** 25 | 26 | - Support for PHP 8.2 [DNF types](https://wiki.php.net/rfc/dnf_types) in `ReflectionTools::exportFunctionSignature()` 27 | 28 | ## [0.5.0](https://github.com/brick/reflection/releases/tag/0.5.0) - 2023-01-15 29 | 30 | 💥 **Breaking changes** 31 | 32 | - Minimum PHP version is now `8.0` 33 | - The following methods have been **removed**: 34 | - `ReflectionTools::getParameterTypes()` 35 | - `ReflectionTools::getPropertyTypes()` 36 | - `ReflectionTools::exportFunction()` has been renamed to `exportFunctionSignature()` 37 | - `ReflectionTools::exportFunctionParameters()` is no longer part of the public API 38 | 39 | ✨ **New features** 40 | 41 | - `ReflectionTools::exportFunctionSignature()`: 42 | - Support for `self`, `static` and `never` types 43 | - Support for union types and intersection types 44 | - Support for functions returning references 45 | 46 | 🐛 **Bug fixes** 47 | 48 | - `ReflectionTools::exportFunctionSignature()`: 49 | - constants are now properly exported with a leading `\` 50 | - nullable types are now always output with a leading `?` 51 | 52 | 💄 **Cosmetic changes** 53 | 54 | - `ReflectionTools::exportFunctionSignature()`: 55 | - Null values in parameter default values are now output as `null` instead of `NULL` 56 | - Arrays in parameter default values are now exported with short array syntax, on a single line 57 | - There is no more space between closing parenthesis and colon, i.e. `): int` instead of `) : int` 58 | 59 | ## [0.4.1](https://github.com/brick/reflection/releases/tag/0.4.1) - 2020-10-24 60 | 61 | 🐛 **Bug fix** 62 | 63 | - `ReflectionTools::exportFunction()` returned a `?`-nullable type for untyped parameters (#2) 64 | 65 | ## [0.4.0](https://github.com/brick/reflection/releases/tag/0.4.0) - 2020-09-28 66 | 67 | ✨ **New features** 68 | 69 | - **PHP 8 compatibility** 🚀 70 | - `ReflectionTools::getPropertyTypes()` now supports PHP 8 union types 71 | - `ReflectionTools::getParameterTypes()` now supports reflection & PHP 8 union types 72 | 73 | 💥 **Breaking changes** 74 | 75 | - `ReflectionTools::getParameterTypes()` now reads types from reflection first 76 | - `ReflectionTools::getPropertyTypes()` and `getParameterTypes()`: 77 | - always return class names as FQCN (including namespace) 78 | - always return built-in types as lowercase 79 | - `ReflectionTools::getFunctionParameterTypes()` has been removed 80 | - `ReflectionTools::getPropertyClass()` has been removed 81 | 82 | ⬆️ **Dependency upgrade** 83 | 84 | - For compatibility with PHP 8, this version requires `doctrine/annotations: ^1.10.4` 85 | 86 | ## [0.3.0](https://github.com/brick/reflection/releases/tag/0.3.0) - 2019-12-24 87 | 88 | Minimum PHP version is now `7.2`. No other changes. 89 | 90 | ## [0.2.4](https://github.com/brick/reflection/releases/tag/0.2.4) - 2019-11-05 91 | 92 | Fix support for typed properties in `ReflectionTools::getPropertyClass()`. 93 | 94 | ## [0.2.3](https://github.com/brick/reflection/releases/tag/0.2.3) - 2019-11-05 95 | 96 | Support for typed properties (PHP 7.4) in `ReflectionTools::getPropertyTypes()`. 97 | 98 | ## [0.2.2](https://github.com/brick/reflection/releases/tag/0.2.2) - 2019-03-27 99 | 100 | **Improvement** 101 | 102 | `ReflectionTools::getClassMethods()` and `getClassProperties()` now always respect the hierarchical order, returning methods and properties from parent classes first. 103 | 104 | ## [0.2.1](https://github.com/brick/reflection/releases/tag/0.2.1) - 2017-10-13 105 | 106 | **Internal refactoring.** Several methods now have a simpler implementation. 107 | 108 | ## [0.2.0](https://github.com/brick/reflection/releases/tag/0.2.0) - 2017-10-03 109 | 110 | **Minimum PHP version is now 7.1.** 111 | 112 | `ReflectionTools::exportFunction()` now supports scalar type hints, return types, nullable types and variadics. 113 | 114 | ## [0.1.0](https://github.com/brick/reflection/releases/tag/0.1.0) - 2017-10-03 115 | 116 | First beta release. 117 | 118 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-present Benjamin Morel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brick/reflection", 3 | "description": "Low-level tools to extend PHP reflection capabilities", 4 | "type": "library", 5 | "keywords": [ 6 | "Brick", 7 | "Reflection" 8 | ], 9 | "license": "MIT", 10 | "require": { 11 | "php": "^8.1", 12 | "doctrine/annotations": "^1.10.4 || ^2.0", 13 | "brick/varexporter": "^0.3.7 || ^0.4.0 || ^0.5.0" 14 | }, 15 | "require-dev": { 16 | "phpunit/phpunit": "^9.0", 17 | "php-coveralls/php-coveralls": "^2.0", 18 | "vimeo/psalm": "6.3.0" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Brick\\Reflection\\": "src/" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Brick\\Reflection\\Tests\\": "tests/" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/ImportResolver.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | private array $aliases; 30 | 31 | /** 32 | * Class constructor. 33 | * 34 | * @param Reflector $context A reflection of the context in which the types will be resolved. 35 | * The context can be a class, property, method or parameter. 36 | * 37 | * @throws InvalidArgumentException If the class or file name cannot be inferred from the context. 38 | */ 39 | public function __construct(Reflector $context) 40 | { 41 | $class = $this->getDeclaringClass($context); 42 | 43 | if ($class === null) { 44 | throw $this->invalidArgumentException('declaring class', $context); 45 | } 46 | 47 | $fileName = $class->getFileName(); 48 | 49 | if ($fileName === false) { 50 | throw $this->invalidArgumentException('file name', $context); 51 | } 52 | 53 | $source = @ file_get_contents($fileName); 54 | 55 | if ($source === false) { 56 | throw new RuntimeException('Could not read ' . $fileName); 57 | } 58 | 59 | $parser = new TokenParser($source); 60 | 61 | $this->namespace = $class->getNamespaceName(); 62 | $this->aliases = $parser->parseUseStatements($this->namespace); 63 | } 64 | 65 | /** 66 | * Returns the ReflectionClass of the given Reflector. 67 | */ 68 | private function getDeclaringClass(Reflector $reflector) : ?ReflectionClass 69 | { 70 | if ($reflector instanceof ReflectionClass) { 71 | return $reflector; 72 | } 73 | 74 | if ($reflector instanceof ReflectionClassConstant) { 75 | return $reflector->getDeclaringClass(); 76 | } 77 | 78 | if ($reflector instanceof ReflectionProperty) { 79 | return $reflector->getDeclaringClass(); 80 | } 81 | 82 | if ($reflector instanceof ReflectionMethod) { 83 | return $reflector->getDeclaringClass(); 84 | } 85 | 86 | if ($reflector instanceof ReflectionParameter) { 87 | return $reflector->getDeclaringClass(); 88 | } 89 | 90 | return null; 91 | } 92 | 93 | private function invalidArgumentException(string $inferring, Reflector $reflector) : InvalidArgumentException 94 | { 95 | return new InvalidArgumentException(sprintf( 96 | 'Cannot infer the %s from the given %s', 97 | $inferring, 98 | $reflector::class 99 | )); 100 | } 101 | 102 | /** 103 | * @param string $type A class or interface name. 104 | * 105 | * @return string The fully qualified class name. 106 | */ 107 | public function resolve(string $type) : string 108 | { 109 | $pos = strpos($type, '\\'); 110 | 111 | if ($pos === 0) { 112 | return substr($type, 1); // Already fully qualified. 113 | } 114 | 115 | if ($pos === false) { 116 | $first = $type; 117 | $next = ''; 118 | } else { 119 | $first = substr($type, 0, $pos); 120 | $next = substr($type, $pos); 121 | } 122 | 123 | $first = strtolower($first); 124 | 125 | if (isset($this->aliases[$first])) { 126 | return $this->aliases[$first] . $next; 127 | } 128 | 129 | return $this->namespace . '\\' . $type; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/ReflectionTools.php: -------------------------------------------------------------------------------- 1 | getClassHierarchy($class); 44 | 45 | $methods = []; 46 | 47 | foreach ($classes as $hClass) { 48 | $hClassName = $hClass->getName(); 49 | 50 | foreach ($hClass->getMethods() as $method) { 51 | if ($method->isStatic()) { 52 | // exclude static methods 53 | continue; 54 | } 55 | 56 | if ($method->getDeclaringClass()->getName() !== $hClassName) { 57 | // exclude inherited methods 58 | continue; 59 | } 60 | 61 | $methods[] = $method; 62 | } 63 | } 64 | 65 | return $this->filterReflectors($methods); 66 | } 67 | 68 | /** 69 | * Returns reflections of all the non-static properties that make up one object. 70 | * 71 | * Like ReflectionClass::getProperties(), this method: 72 | * 73 | * - does not return overridden protected or public class properties, and only returns the overriding one; 74 | * - returns properties inside a class in the order they are declared. 75 | * 76 | * Unlike ReflectionClass::getProperties(), this method: 77 | * 78 | * - returns the private properties of parent classes; 79 | * - returns properties in hierarchical order: properties from parent classes are returned first. 80 | * 81 | * @return ReflectionProperty[] 82 | */ 83 | public function getClassProperties(ReflectionClass $class) : array 84 | { 85 | $classes = $this->getClassHierarchy($class); 86 | 87 | /** @var ReflectionProperty[] $properties */ 88 | $properties = []; 89 | 90 | foreach ($classes as $hClass) { 91 | $hClassName = $hClass->getName(); 92 | 93 | foreach ($hClass->getProperties() as $property) { 94 | if ($property->isStatic()) { 95 | // exclude static properties 96 | continue; 97 | } 98 | 99 | if ($property->getDeclaringClass()->getName() !== $hClassName) { 100 | // exclude inherited properties 101 | continue; 102 | } 103 | 104 | $properties[] = $property; 105 | } 106 | } 107 | 108 | return $this->filterReflectors($properties); 109 | } 110 | 111 | /** 112 | * Returns the hierarchy of classes, starting from the first ancestor and ending with the class itself. 113 | * 114 | * @return ReflectionClass[] 115 | */ 116 | public function getClassHierarchy(ReflectionClass $class) : array 117 | { 118 | $classes = []; 119 | 120 | while ($class) { 121 | $classes[] = $class; 122 | $class = $class->getParentClass(); 123 | } 124 | 125 | return array_reverse($classes); 126 | } 127 | 128 | /** 129 | * Returns a reflection object for any callable. 130 | */ 131 | public function getReflectionFunction(callable $function) : ReflectionFunctionAbstract 132 | { 133 | if (is_array($function)) { 134 | return new ReflectionMethod($function[0], $function[1]); 135 | } 136 | 137 | if ($function instanceof Closure) { 138 | return new ReflectionFunction($function); 139 | } 140 | 141 | if (is_object($function)) { 142 | return new ReflectionMethod($function, '__invoke'); 143 | } 144 | 145 | return new ReflectionFunction($function); 146 | } 147 | 148 | /** 149 | * Returns a meaningful name for the given function, including the class name if it is a method. 150 | * 151 | * Example for a method: Namespace\Class::method 152 | * Example for a function: strlen 153 | * Example for a closure: {closure} 154 | */ 155 | public function getFunctionName(ReflectionFunctionAbstract $function) : string 156 | { 157 | if ($function instanceof ReflectionMethod) { 158 | return $function->getDeclaringClass()->getName() . '::' . $function->getName(); 159 | } 160 | 161 | return $function->getName(); 162 | } 163 | 164 | /** 165 | * Exports the function signature. 166 | * 167 | * @param ReflectionFunctionAbstract $function The function to export. 168 | * @param int $excludeModifiers An optional bitmask of modifiers to exclude. 169 | */ 170 | public function exportFunctionSignature(ReflectionFunctionAbstract $function, int $excludeModifiers = 0) : string 171 | { 172 | $result = ''; 173 | 174 | if ($function instanceof ReflectionMethod) { 175 | $modifiers = $function->getModifiers(); 176 | $modifiers &= ~ $excludeModifiers; 177 | 178 | foreach (Reflection::getModifierNames($modifiers) as $modifier) { 179 | $result .= $modifier . ' '; 180 | } 181 | } 182 | 183 | $result .= 'function '; 184 | 185 | if ($function->returnsReference()) { 186 | $result .= '& '; 187 | } 188 | 189 | $result .= $function->getShortName(); 190 | $result .= '(' . $this->exportFunctionParameters($function) . ')'; 191 | 192 | if (null !== $returnType = $function->getReturnType()) { 193 | $result .= ': ' . $this->exportType($returnType); 194 | } 195 | 196 | return $result; 197 | } 198 | 199 | private function exportFunctionParameters(ReflectionFunctionAbstract $function) : string 200 | { 201 | $result = ''; 202 | 203 | foreach ($function->getParameters() as $key => $parameter) { 204 | if ($key !== 0) { 205 | $result .= ', '; 206 | } 207 | 208 | if (null !== $type = $parameter->getType()) { 209 | $result .= $this->exportType($type) . ' '; 210 | } 211 | 212 | if ($parameter->isPassedByReference()) { 213 | $result .= '& '; 214 | } 215 | 216 | if ($parameter->isVariadic()) { 217 | $result .= '...'; 218 | } 219 | 220 | $result .= '$' . $parameter->getName(); 221 | 222 | if ($parameter->isDefaultValueAvailable()) { 223 | if ($parameter->isDefaultValueConstant()) { 224 | $result .= ' = ' . '\\' . $parameter->getDefaultValueConstantName(); 225 | } else { 226 | $result .= ' = ' . VarExporter::export($parameter->getDefaultValue(), VarExporter::INLINE_ARRAY); 227 | } 228 | } 229 | } 230 | 231 | return $result; 232 | } 233 | 234 | /** 235 | * @psalm-suppress RedundantCondition https://github.com/vimeo/psalm/pull/8201 236 | */ 237 | private function exportType(ReflectionType $type, bool $inUnion = false): string 238 | { 239 | if ($type instanceof ReflectionUnionType) { 240 | return implode('|', array_map( 241 | fn (ReflectionType $type) => $this->exportType($type, true), 242 | $type->getTypes(), 243 | )); 244 | } 245 | 246 | if ($type instanceof ReflectionIntersectionType) { 247 | $result = implode('&', array_map( 248 | fn (ReflectionType $type) => $this->exportType($type), 249 | $type->getTypes(), 250 | )); 251 | 252 | return $inUnion ? "($result)" : $result; 253 | } 254 | 255 | if (! $type instanceof ReflectionNamedType) { 256 | throw new Exception('Unsupported ReflectionType class: ' . $type::class); 257 | } 258 | 259 | $result = ''; 260 | 261 | if ($type->allowsNull() && $type->getName() !== 'mixed' && $type->getName() !== 'null') { 262 | $result .= '?'; 263 | } 264 | 265 | if (! $type->isBuiltin() && $type->getName() !== 'self' && $type->getName() !== 'static') { 266 | $result .= '\\'; 267 | } 268 | 269 | $result .= $type->getName(); 270 | 271 | return $result; 272 | } 273 | 274 | /** 275 | * Filters a list of ReflectionProperty or ReflectionMethod objects. 276 | * 277 | * This method removes overridden properties, while keeping original order. 278 | * 279 | * @template T of ReflectionProperty|ReflectionMethod 280 | * 281 | * @param T[] $reflectors 282 | * 283 | * @return T[] 284 | */ 285 | private function filterReflectors(array $reflectors) : array 286 | { 287 | $filteredReflectors = []; 288 | 289 | foreach ($reflectors as $index => $reflector) { 290 | if ($reflector->isPrivate()) { 291 | $filteredReflectors[] = $reflector; 292 | continue; 293 | } 294 | 295 | foreach ($reflectors as $index2 => $reflector2) { 296 | if ($index2 <= $index) { 297 | continue; 298 | } 299 | 300 | if ($reflector->getName() === $reflector2->getName()) { 301 | // overridden 302 | continue 2; 303 | } 304 | } 305 | 306 | $filteredReflectors[] = $reflector; 307 | } 308 | 309 | return $filteredReflectors; 310 | } 311 | } 312 | --------------------------------------------------------------------------------