• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

brick / reflection / 17533079061

07 Sep 2025 07:47PM UTC coverage: 79.856%. Remained the same
17533079061

push

github

BenMorel
Migrate to PHPUnit 10

111 of 139 relevant lines covered (79.86%)

10.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.89
/src/ReflectionTools.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Brick\Reflection;
6

7
use Brick\VarExporter\VarExporter;
8
use Closure;
9
use Exception;
10
use Reflection;
11
use ReflectionClass;
12
use ReflectionFunction;
13
use ReflectionFunctionAbstract;
14
use ReflectionIntersectionType;
15
use ReflectionMethod;
16
use ReflectionNamedType;
17
use ReflectionProperty;
18
use ReflectionType;
19
use ReflectionUnionType;
20

21
use function array_map;
22
use function array_reverse;
23
use function implode;
24
use function is_array;
25
use function is_object;
26

27
/**
28
 * Tools for the Reflection API.
29
 */
30
class ReflectionTools
31
{
32
    /**
33
     * Returns reflections of all the non-static methods that make up one object.
34
     *
35
     * Like ReflectionClass::getMethods(), this method:
36
     *
37
     * - does not return overridden protected or public class methods, and only returns the overriding one;
38
     * - returns methods inside a class in the order they are declared.
39
     *
40
     * Unlike ReflectionClass::getMethods(), this method:
41
     *
42
     * - returns the private methods of parent classes;
43
     * - returns methods in hierarchical order: methods from parent classes are returned first.
44
     *
45
     * @return ReflectionMethod[]
46
     */
47
    public function getClassMethods(ReflectionClass $class): array
48
    {
49
        $classes = $this->getClassHierarchy($class);
6✔
50

51
        $methods = [];
6✔
52

53
        foreach ($classes as $hClass) {
6✔
54
            $hClassName = $hClass->getName();
6✔
55

56
            foreach ($hClass->getMethods() as $method) {
6✔
57
                if ($method->isStatic()) {
6✔
58
                    // exclude static methods
59
                    continue;
1✔
60
                }
61

62
                if ($method->getDeclaringClass()->getName() !== $hClassName) {
5✔
63
                    // exclude inherited methods
64
                    continue;
1✔
65
                }
66

67
                $methods[] = $method;
5✔
68
            }
69
        }
70

71
        return $this->filterReflectors($methods);
6✔
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);
6✔
92

93
        /** @var ReflectionProperty[] $properties */
94
        $properties = [];
6✔
95

96
        foreach ($classes as $hClass) {
6✔
97
            $hClassName = $hClass->getName();
6✔
98

99
            foreach ($hClass->getProperties() as $property) {
6✔
100
                if ($property->isStatic()) {
6✔
101
                    // exclude static properties
102
                    continue;
1✔
103
                }
104

105
                if ($property->getDeclaringClass()->getName() !== $hClassName) {
5✔
106
                    // exclude inherited properties
107
                    continue;
1✔
108
                }
109

110
                $properties[] = $property;
5✔
111
            }
112
        }
113

114
        return $this->filterReflectors($properties);
6✔
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 = [];
12✔
125

126
        while ($class) {
12✔
127
            $classes[] = $class;
12✔
128
            $class = $class->getParentClass();
12✔
129
        }
130

131
        return array_reverse($classes);
12✔
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 = '';
34✔
179

180
        if ($function instanceof ReflectionMethod) {
34✔
181
            $modifiers = $function->getModifiers();
34✔
182
            $modifiers &= ~$excludeModifiers;
34✔
183

184
            foreach (Reflection::getModifierNames($modifiers) as $modifier) {
34✔
185
                $result .= $modifier . ' ';
34✔
186
            }
187
        }
188

189
        $result .= 'function ';
34✔
190

191
        if ($function->returnsReference()) {
34✔
192
            $result .= '& ';
2✔
193
        }
194

195
        $result .= $function->getShortName();
34✔
196
        $result .= '(' . $this->exportFunctionParameters($function) . ')';
34✔
197

198
        if (null !== $returnType = $function->getReturnType()) {
34✔
199
            $result .= ': ' . $this->exportType($returnType);
18✔
200
        }
201

202
        return $result;
34✔
203
    }
204

205
    private function exportFunctionParameters(ReflectionFunctionAbstract $function): string
206
    {
207
        $result = '';
34✔
208

209
        foreach ($function->getParameters() as $key => $parameter) {
34✔
210
            if ($key !== 0) {
23✔
211
                $result .= ', ';
10✔
212
            }
213

214
            if (null !== $type = $parameter->getType()) {
23✔
215
                $result .= $this->exportType($type) . ' ';
21✔
216
            }
217

218
            if ($parameter->isPassedByReference()) {
23✔
219
                $result .= '& ';
7✔
220
            }
221

222
            if ($parameter->isVariadic()) {
23✔
223
                $result .= '...';
4✔
224
            }
225

226
            $result .= '$' . $parameter->getName();
23✔
227

228
            if ($parameter->isDefaultValueAvailable()) {
23✔
229
                if ($parameter->isDefaultValueConstant()) {
9✔
230
                    $result .= ' = ' . '\\' . $parameter->getDefaultValueConstantName();
2✔
231
                } else {
232
                    $result .= ' = ' . VarExporter::export($parameter->getDefaultValue(), VarExporter::INLINE_ARRAY);
8✔
233
                }
234
            }
235
        }
236

237
        return $result;
34✔
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) {
31✔
246
            return implode('|', array_map(
6✔
247
                fn (ReflectionType $type) => $this->exportType($type, true),
6✔
248
                $type->getTypes(),
6✔
249
            ));
6✔
250
        }
251

252
        if ($type instanceof ReflectionIntersectionType) {
31✔
253
            $result = implode('&', array_map(
2✔
254
                fn (ReflectionType $type) => $this->exportType($type),
2✔
255
                $type->getTypes(),
2✔
256
            ));
2✔
257

258
            return $inUnion ? "($result)" : $result;
2✔
259
        }
260

261
        if (! $type instanceof ReflectionNamedType) {
31✔
262
            throw new Exception('Unsupported ReflectionType class: ' . $type::class);
×
263
        }
264

265
        $result = '';
31✔
266

267
        if ($type->allowsNull() && $type->getName() !== 'mixed' && $type->getName() !== 'null') {
31✔
268
            $result .= '?';
15✔
269
        }
270

271
        if (! $type->isBuiltin() && $type->getName() !== 'self' && $type->getName() !== 'static') {
31✔
272
            $result .= '\\';
9✔
273
        }
274

275
        $result .= $type->getName();
31✔
276

277
        return $result;
31✔
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 = [];
12✔
294

295
        foreach ($reflectors as $index => $reflector) {
12✔
296
            if ($reflector->isPrivate()) {
10✔
297
                $filteredReflectors[] = $reflector;
6✔
298

299
                continue;
6✔
300
            }
301

302
            foreach ($reflectors as $index2 => $reflector2) {
10✔
303
                if ($index2 <= $index) {
10✔
304
                    continue;
10✔
305
                }
306

307
                if ($reflector->getName() === $reflector2->getName()) {
10✔
308
                    // overridden
309
                    continue 2;
4✔
310
                }
311
            }
312

313
            $filteredReflectors[] = $reflector;
10✔
314
        }
315

316
        return $filteredReflectors;
12✔
317
    }
318
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc