├── tools └── ecs │ ├── composer.json │ └── ecs.php ├── composer.json ├── LICENSE ├── src ├── ImportResolver.php ├── Internal │ └── TokenParser.php └── ReflectionTools.php └── CHANGELOG.md /tools/ecs/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "brick/coding-standard": "v2" 4 | }, 5 | "config": { 6 | "allow-plugins": { 7 | "dealerdirect/phpcodesniffer-composer-installer": false 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tools/ecs/ecs.php: -------------------------------------------------------------------------------- 1 | import(__DIR__ . '/vendor/brick/coding-standard/ecs.php'); 9 | 10 | $libRootPath = realpath(__DIR__ . '/../../'); 11 | 12 | $ecsConfig->paths( 13 | [ 14 | $libRootPath . '/src', 15 | $libRootPath . '/tests', 16 | __FILE__, 17 | ], 18 | ); 19 | 20 | $ecsConfig->skip([ 21 | 'tests/Classes/*.php', 22 | ]); 23 | }; 24 | -------------------------------------------------------------------------------- /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 | "brick/varexporter": "^0.3.7 || ^0.4.0 || ^0.5.0" 13 | }, 14 | "require-dev": { 15 | "phpunit/phpunit": "^10.5", 16 | "php-coveralls/php-coveralls": "^2.0", 17 | "vimeo/psalm": "6.13.1" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "Brick\\Reflection\\": "src/" 22 | } 23 | }, 24 | "autoload-dev": { 25 | "psr-4": { 26 | "Brick\\Reflection\\Tests\\": "tests/" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/ImportResolver.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | private array $aliases; 36 | 37 | /** 38 | * Class constructor. 39 | * 40 | * @param Reflector $context A reflection of the context in which the types will be resolved. 41 | * The context can be a class, property, method or parameter. 42 | * 43 | * @throws InvalidArgumentException If the class or file name cannot be inferred from the context. 44 | */ 45 | public function __construct(Reflector $context) 46 | { 47 | $class = $this->getDeclaringClass($context); 48 | 49 | if ($class === null) { 50 | throw $this->invalidArgumentException('declaring class', $context); 51 | } 52 | 53 | $fileName = $class->getFileName(); 54 | 55 | if ($fileName === false) { 56 | throw $this->invalidArgumentException('file name', $context); 57 | } 58 | 59 | $source = @file_get_contents($fileName); 60 | 61 | if ($source === false) { 62 | throw new RuntimeException('Could not read ' . $fileName); 63 | } 64 | 65 | $parser = new TokenParser($source); 66 | 67 | $this->namespace = $class->getNamespaceName(); 68 | $this->aliases = $parser->parseUseStatements($this->namespace); 69 | } 70 | 71 | /** 72 | * @param string $type A class or interface name. 73 | * 74 | * @return string The fully qualified class name. 75 | */ 76 | public function resolve(string $type): string 77 | { 78 | $pos = strpos($type, '\\'); 79 | 80 | if ($pos === 0) { 81 | return substr($type, 1); // Already fully qualified. 82 | } 83 | 84 | if ($pos === false) { 85 | $first = $type; 86 | $next = ''; 87 | } else { 88 | $first = substr($type, 0, $pos); 89 | $next = substr($type, $pos); 90 | } 91 | 92 | $first = strtolower($first); 93 | 94 | if (isset($this->aliases[$first])) { 95 | return $this->aliases[$first] . $next; 96 | } 97 | 98 | return $this->namespace . '\\' . $type; 99 | } 100 | 101 | /** 102 | * Returns the ReflectionClass of the given Reflector. 103 | */ 104 | private function getDeclaringClass(Reflector $reflector): ?ReflectionClass 105 | { 106 | if ($reflector instanceof ReflectionClass) { 107 | return $reflector; 108 | } 109 | 110 | if ($reflector instanceof ReflectionClassConstant) { 111 | return $reflector->getDeclaringClass(); 112 | } 113 | 114 | if ($reflector instanceof ReflectionProperty) { 115 | return $reflector->getDeclaringClass(); 116 | } 117 | 118 | if ($reflector instanceof ReflectionMethod) { 119 | return $reflector->getDeclaringClass(); 120 | } 121 | 122 | if ($reflector instanceof ReflectionParameter) { 123 | return $reflector->getDeclaringClass(); 124 | } 125 | 126 | return null; 127 | } 128 | 129 | private function invalidArgumentException(string $inferring, Reflector $reflector): InvalidArgumentException 130 | { 131 | return new InvalidArgumentException(sprintf( 132 | 'Cannot infer the %s from the given %s', 133 | $inferring, 134 | $reflector::class, 135 | )); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Internal/TokenParser.php: -------------------------------------------------------------------------------- 1 | 41 | */ 42 | private array $tokens; 43 | 44 | /** 45 | * The number of tokens. 46 | */ 47 | private int $numTokens; 48 | 49 | /** 50 | * The current array pointer. 51 | */ 52 | private int $pointer = 0; 53 | 54 | public function __construct(string $contents) 55 | { 56 | $this->tokens = token_get_all($contents); 57 | 58 | // The PHP parser sets internal compiler globals for certain things. Annoyingly, the last docblock comment it 59 | // saw gets stored in doc_comment. When it comes to compile the next thing to be include()d this stored 60 | // doc_comment becomes owned by the first thing the compiler sees in the file that it considers might have a 61 | // docblock. If the first thing in the file is a class without a doc block this would cause calls to 62 | // getDocBlock() on said class to return our long lost doc_comment. Argh. 63 | // To workaround, cause the parser to parse an empty docblock. Sure getDocBlock() will return this, but at least 64 | // it's harmless to us. 65 | token_get_all("numTokens = count($this->tokens); 68 | } 69 | 70 | /** 71 | * Gets the next non whitespace and non comment token. 72 | * 73 | * @param bool $docCommentIsComment If TRUE then a doc comment is considered a comment and skipped. 74 | * If FALSE then only whitespace and normal comments are skipped. 75 | * 76 | * @return Token|null The token if exists, null otherwise. 77 | */ 78 | public function next(bool $docCommentIsComment = true): null|array|string 79 | { 80 | for ($i = $this->pointer; $i < $this->numTokens; $i++) { 81 | $this->pointer++; 82 | if ( 83 | $this->tokens[$i][0] === T_WHITESPACE || 84 | $this->tokens[$i][0] === T_COMMENT || 85 | ($docCommentIsComment && $this->tokens[$i][0] === T_DOC_COMMENT) 86 | ) { 87 | continue; 88 | } 89 | 90 | return $this->tokens[$i]; 91 | } 92 | 93 | return null; 94 | } 95 | 96 | /** 97 | * Parses a single use statement. 98 | * 99 | * @return array A list with all found class names for a use statement. 100 | */ 101 | public function parseUseStatement(): array 102 | { 103 | $groupRoot = ''; 104 | $class = ''; 105 | $alias = ''; 106 | $statements = []; 107 | $explicitAlias = false; 108 | while (($token = $this->next())) { 109 | if (! $explicitAlias && $token[0] === T_STRING) { 110 | $class .= $token[1]; 111 | $alias = $token[1]; 112 | } elseif ($explicitAlias && $token[0] === T_STRING) { 113 | $alias = $token[1]; 114 | } elseif ( 115 | PHP_VERSION_ID >= 80000 && 116 | ($token[0] === T_NAME_QUALIFIED || $token[0] === T_NAME_FULLY_QUALIFIED) 117 | ) { 118 | $class .= $token[1]; 119 | 120 | $classSplit = explode('\\', $token[1]); 121 | $alias = $classSplit[count($classSplit) - 1]; 122 | } elseif ($token[0] === T_NS_SEPARATOR) { 123 | $class .= '\\'; 124 | $alias = ''; 125 | } elseif ($token[0] === T_AS) { 126 | $explicitAlias = true; 127 | $alias = ''; 128 | } elseif ($token === ',') { 129 | $statements[strtolower($alias)] = $groupRoot . $class; 130 | $class = ''; 131 | $alias = ''; 132 | $explicitAlias = false; 133 | } elseif ($token === ';') { 134 | $statements[strtolower($alias)] = $groupRoot . $class; 135 | 136 | break; 137 | } elseif ($token === '{') { 138 | $groupRoot = $class; 139 | $class = ''; 140 | } elseif ($token === '}') { 141 | continue; 142 | } else { 143 | break; 144 | } 145 | } 146 | 147 | return $statements; 148 | } 149 | 150 | /** 151 | * Gets all use statements. 152 | * 153 | * @param string $namespaceName The namespace name of the reflected class. 154 | * 155 | * @return array A list with all found use statements. 156 | */ 157 | public function parseUseStatements(string $namespaceName): array 158 | { 159 | $statements = []; 160 | while (($token = $this->next())) { 161 | if ($token[0] === T_USE) { 162 | $statements = array_merge($statements, $this->parseUseStatement()); 163 | 164 | continue; 165 | } 166 | 167 | if ($token[0] !== T_NAMESPACE || $this->parseNamespace() !== $namespaceName) { 168 | continue; 169 | } 170 | 171 | // Get fresh array for new namespace. This is to prevent the parser to collect the use statements 172 | // for a previous namespace with the same name. This is the case if a namespace is defined twice 173 | // or if a namespace with the same name is commented out. 174 | $statements = []; 175 | } 176 | 177 | return $statements; 178 | } 179 | 180 | /** 181 | * Gets the namespace. 182 | * 183 | * @return string The found namespace. 184 | */ 185 | public function parseNamespace(): string 186 | { 187 | $name = ''; 188 | while ( 189 | ($token = $this->next()) && ($token[0] === T_STRING || $token[0] === T_NS_SEPARATOR || ( 190 | PHP_VERSION_ID >= 80000 && 191 | ($token[0] === T_NAME_QUALIFIED || $token[0] === T_NAME_FULLY_QUALIFIED) 192 | )) 193 | ) { 194 | $name .= $token[1]; 195 | } 196 | 197 | return $name; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/ReflectionTools.php: -------------------------------------------------------------------------------- 1 | getClassHierarchy($class); 50 | 51 | $methods = []; 52 | 53 | foreach ($classes as $hClass) { 54 | $hClassName = $hClass->getName(); 55 | 56 | foreach ($hClass->getMethods() as $method) { 57 | if ($method->isStatic()) { 58 | // exclude static methods 59 | continue; 60 | } 61 | 62 | if ($method->getDeclaringClass()->getName() !== $hClassName) { 63 | // exclude inherited methods 64 | continue; 65 | } 66 | 67 | $methods[] = $method; 68 | } 69 | } 70 | 71 | return $this->filterReflectors($methods); 72 | } 73 | 74 | /** 75 | * Returns reflections of all the non-static properties that make up one object. 76 | * 77 | * Like ReflectionClass::getProperties(), this method: 78 | * 79 | * - does not return overridden protected or public class properties, and only returns the overriding one; 80 | * - returns properties inside a class in the order they are declared. 81 | * 82 | * Unlike ReflectionClass::getProperties(), this method: 83 | * 84 | * - returns the private properties of parent classes; 85 | * - returns properties in hierarchical order: properties from parent classes are returned first. 86 | * 87 | * @return ReflectionProperty[] 88 | */ 89 | public function getClassProperties(ReflectionClass $class): array 90 | { 91 | $classes = $this->getClassHierarchy($class); 92 | 93 | /** @var ReflectionProperty[] $properties */ 94 | $properties = []; 95 | 96 | foreach ($classes as $hClass) { 97 | $hClassName = $hClass->getName(); 98 | 99 | foreach ($hClass->getProperties() as $property) { 100 | if ($property->isStatic()) { 101 | // exclude static properties 102 | continue; 103 | } 104 | 105 | if ($property->getDeclaringClass()->getName() !== $hClassName) { 106 | // exclude inherited properties 107 | continue; 108 | } 109 | 110 | $properties[] = $property; 111 | } 112 | } 113 | 114 | return $this->filterReflectors($properties); 115 | } 116 | 117 | /** 118 | * Returns the hierarchy of classes, starting from the first ancestor and ending with the class itself. 119 | * 120 | * @return ReflectionClass[] 121 | */ 122 | public function getClassHierarchy(ReflectionClass $class): array 123 | { 124 | $classes = []; 125 | 126 | while ($class) { 127 | $classes[] = $class; 128 | $class = $class->getParentClass(); 129 | } 130 | 131 | return array_reverse($classes); 132 | } 133 | 134 | /** 135 | * Returns a reflection object for any callable. 136 | */ 137 | public function getReflectionFunction(callable $function): ReflectionFunctionAbstract 138 | { 139 | if (is_array($function)) { 140 | return new ReflectionMethod($function[0], $function[1]); 141 | } 142 | 143 | if ($function instanceof Closure) { 144 | return new ReflectionFunction($function); 145 | } 146 | 147 | if (is_object($function)) { 148 | return new ReflectionMethod($function, '__invoke'); 149 | } 150 | 151 | return new ReflectionFunction($function); 152 | } 153 | 154 | /** 155 | * Returns a meaningful name for the given function, including the class name if it is a method. 156 | * 157 | * Example for a method: Namespace\Class::method 158 | * Example for a function: strlen 159 | * Example for a closure: {closure} 160 | */ 161 | public function getFunctionName(ReflectionFunctionAbstract $function): string 162 | { 163 | if ($function instanceof ReflectionMethod) { 164 | return $function->getDeclaringClass()->getName() . '::' . $function->getName(); 165 | } 166 | 167 | return $function->getName(); 168 | } 169 | 170 | /** 171 | * Exports the function signature. 172 | * 173 | * @param ReflectionFunctionAbstract $function The function to export. 174 | * @param int $excludeModifiers An optional bitmask of modifiers to exclude. 175 | */ 176 | public function exportFunctionSignature(ReflectionFunctionAbstract $function, int $excludeModifiers = 0): string 177 | { 178 | $result = ''; 179 | 180 | if ($function instanceof ReflectionMethod) { 181 | $modifiers = $function->getModifiers(); 182 | $modifiers &= ~$excludeModifiers; 183 | 184 | foreach (Reflection::getModifierNames($modifiers) as $modifier) { 185 | $result .= $modifier . ' '; 186 | } 187 | } 188 | 189 | $result .= 'function '; 190 | 191 | if ($function->returnsReference()) { 192 | $result .= '& '; 193 | } 194 | 195 | $result .= $function->getShortName(); 196 | $result .= '(' . $this->exportFunctionParameters($function) . ')'; 197 | 198 | if (null !== $returnType = $function->getReturnType()) { 199 | $result .= ': ' . $this->exportType($returnType); 200 | } 201 | 202 | return $result; 203 | } 204 | 205 | private function exportFunctionParameters(ReflectionFunctionAbstract $function): string 206 | { 207 | $result = ''; 208 | 209 | foreach ($function->getParameters() as $key => $parameter) { 210 | if ($key !== 0) { 211 | $result .= ', '; 212 | } 213 | 214 | if (null !== $type = $parameter->getType()) { 215 | $result .= $this->exportType($type) . ' '; 216 | } 217 | 218 | if ($parameter->isPassedByReference()) { 219 | $result .= '& '; 220 | } 221 | 222 | if ($parameter->isVariadic()) { 223 | $result .= '...'; 224 | } 225 | 226 | $result .= '$' . $parameter->getName(); 227 | 228 | if ($parameter->isDefaultValueAvailable()) { 229 | if ($parameter->isDefaultValueConstant()) { 230 | $result .= ' = ' . '\\' . $parameter->getDefaultValueConstantName(); 231 | } else { 232 | $result .= ' = ' . VarExporter::export($parameter->getDefaultValue(), VarExporter::INLINE_ARRAY); 233 | } 234 | } 235 | } 236 | 237 | return $result; 238 | } 239 | 240 | /** 241 | * @psalm-suppress RedundantCondition https://github.com/vimeo/psalm/pull/8201 242 | */ 243 | private function exportType(ReflectionType $type, bool $inUnion = false): string 244 | { 245 | if ($type instanceof ReflectionUnionType) { 246 | return implode('|', array_map( 247 | fn (ReflectionType $type) => $this->exportType($type, true), 248 | $type->getTypes(), 249 | )); 250 | } 251 | 252 | if ($type instanceof ReflectionIntersectionType) { 253 | $result = implode('&', array_map( 254 | fn (ReflectionType $type) => $this->exportType($type), 255 | $type->getTypes(), 256 | )); 257 | 258 | return $inUnion ? "($result)" : $result; 259 | } 260 | 261 | if (! $type instanceof ReflectionNamedType) { 262 | throw new Exception('Unsupported ReflectionType class: ' . $type::class); 263 | } 264 | 265 | $result = ''; 266 | 267 | if ($type->allowsNull() && $type->getName() !== 'mixed' && $type->getName() !== 'null') { 268 | $result .= '?'; 269 | } 270 | 271 | if (! $type->isBuiltin() && $type->getName() !== 'self' && $type->getName() !== 'static') { 272 | $result .= '\\'; 273 | } 274 | 275 | $result .= $type->getName(); 276 | 277 | return $result; 278 | } 279 | 280 | /** 281 | * Filters a list of ReflectionProperty or ReflectionMethod objects. 282 | * 283 | * This method removes overridden properties, while keeping original order. 284 | * 285 | * @template T of ReflectionProperty|ReflectionMethod 286 | * 287 | * @param T[] $reflectors 288 | * 289 | * @return T[] 290 | */ 291 | private function filterReflectors(array $reflectors): array 292 | { 293 | $filteredReflectors = []; 294 | 295 | foreach ($reflectors as $index => $reflector) { 296 | if ($reflector->isPrivate()) { 297 | $filteredReflectors[] = $reflector; 298 | 299 | continue; 300 | } 301 | 302 | foreach ($reflectors as $index2 => $reflector2) { 303 | if ($index2 <= $index) { 304 | continue; 305 | } 306 | 307 | if ($reflector->getName() === $reflector2->getName()) { 308 | // overridden 309 | continue 2; 310 | } 311 | } 312 | 313 | $filteredReflectors[] = $reflector; 314 | } 315 | 316 | return $filteredReflectors; 317 | } 318 | } 319 | --------------------------------------------------------------------------------