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

nette / php-generator / 29280914714

13 Jul 2026 08:03PM UTC coverage: 94.223% (-0.04%) from 94.264%
29280914714

push

github

dg
Dumper: added support for preserving array references

32 of 32 new or added lines in 1 file covered. (100.0%)

13 existing lines in 2 files now uncovered.

1892 of 2008 relevant lines covered (94.22%)

0.94 hits per line

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

87.28
/src/PhpGenerator/Factory.php
1
<?php declare(strict_types=1);
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
namespace Nette\PhpGenerator;
9

10
use Nette;
11
use Nette\Utils\Reflection;
12
use function array_diff, array_filter, array_key_exists, array_map, count, explode, file_get_contents, implode, is_object, is_subclass_of, method_exists, reset;
13
use const PHP_VERSION_ID;
14

15

16
/**
17
 * Creates PhpGenerator representations from reflection objects or PHP source code.
18
 */
19
final class Factory
20
{
21
        /** @var array<string, array<string, string>> */
22
        private array $bodyCache = [];
23

24
        /** @var array<string, Extractor> */
25
        private array $extractorCache = [];
26

27

28
        /**
29
         * Creates a ClassLike instance from a reflection object.
30
         * @param  \ReflectionClass<object>  $from
31
         * @param  bool  $withBodies  load method bodies (requires nikic/php-parser, not available for anonymous/internal classes or interfaces)
32
         */
33
        public function fromClassReflection(
1✔
34
                \ReflectionClass $from,
35
                bool $withBodies = false,
36
        ): ClassLike
37
        {
38
                if ($withBodies && ($from->isAnonymous() || $from->isInternal() || $from->isInterface())) {
1✔
39
                        throw new Nette\NotSupportedException('The $withBodies parameter cannot be used for anonymous or internal classes or interfaces.');
1✔
40
                }
41

42
                $class = $this->createClassObject($from);
1✔
43
                $this->setupInheritance($class, $from);
1✔
44
                $this->populateMembers($class, $from, $withBodies);
1✔
45
                return $class;
1✔
46
        }
47

48

49
        /** @param \ReflectionClass<object> $from */
50
        private function createClassObject(\ReflectionClass &$from): ClassLike
1✔
51
        {
52
                if ($from->isAnonymous()) {
1✔
53
                        return new ClassType;
1✔
54
                } elseif ($from->isEnum()) {
1✔
55
                        $name = $from->getName();
1✔
56
                        /** @var class-string<\UnitEnum> $name */
57
                        $from = new \ReflectionEnum($name);
1✔
58
                        $class = new EnumType($from->getName());
1✔
59
                } elseif ($from->isInterface()) {
1✔
60
                        $class = new InterfaceType($from->getName());
1✔
61
                } elseif ($from->isTrait()) {
1✔
62
                        $class = new TraitType($from->getName());
1✔
63
                } else {
64
                        $class = new ClassType($from->getShortName());
1✔
65
                        $class->setFinal($from->isFinal() && $class->isClass());
1✔
66
                        $class->setAbstract($from->isAbstract() && $class->isClass());
1✔
67
                        $class->setReadOnly(PHP_VERSION_ID >= 80200 && $from->isReadOnly());
1✔
68
                }
69

70
                (new PhpNamespace($from->getNamespaceName()))->add($class);
1✔
71
                return $class;
1✔
72
        }
73

74

75
        /** @param \ReflectionClass<object> $from */
76
        private function setupInheritance(ClassLike $class, \ReflectionClass $from): void
1✔
77
        {
78
                $ifaces = $from->getInterfaceNames();
1✔
79
                foreach ($ifaces as $iface) {
1✔
80
                        $ifaces = array_filter($ifaces, fn(string $item): bool => !is_subclass_of($iface, $item));
1✔
81
                }
82

83
                if ($from->isInterface()) {
1✔
84
                        assert($class instanceof InterfaceType);
85
                        $class->setExtends(array_values($ifaces));
1✔
86
                } elseif ($ifaces) {
1✔
87
                        assert($class instanceof ClassType || $class instanceof EnumType);
88
                        $ifaces = array_diff($ifaces, [\BackedEnum::class, \UnitEnum::class]);
1✔
89
                        $class->setImplements(array_values($ifaces));
1✔
90
                }
91

92
                $class->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
1✔
93
                $class->setAttributes($this->formatAttributes($from->getAttributes()));
1✔
94
                if ($from->getParentClass()) {
1✔
95
                        assert($class instanceof ClassType);
96
                        $class->setExtends($from->getParentClass()->name);
1✔
97
                        $class->setImplements(array_values(array_diff($class->getImplements(), $from->getParentClass()->getInterfaceNames())));
1✔
98
                }
99
        }
1✔
100

101

102
        /** @param \ReflectionClass<object> $from */
103
        private function populateMembers(ClassLike $class, \ReflectionClass $from, bool $withBodies): void
1✔
104
        {
105
                // Properties
106
                $props = [];
1✔
107
                foreach ($from->getProperties() as $prop) {
1✔
108
                        $declaringClass = Reflection::getPropertyDeclaringClass($prop);
1✔
109

110
                        if ($prop->isDefault()
1✔
111
                                && $declaringClass->name === $from->name
1✔
112
                                && !$prop->isPromoted()
1✔
113
                                && !$class->isEnum()
1✔
114
                        ) {
115
                                $props[] = $p = $this->fromPropertyReflection($prop);
1✔
116
                                if ($withBodies && ($file = $declaringClass->getFileName())) {
1✔
117
                                        $hookBodies ??= $this->getExtractor($file)->extractPropertyHookBodies($declaringClass->name);
1✔
118
                                        /** @var array<'set'|'get', array{string, bool}> $propHookBodies */
119
                                        $propHookBodies = $hookBodies[$prop->getName()] ?? [];
1✔
120
                                        foreach ($propHookBodies as $hookType => [$body, $short]) {
1✔
121
                                                $p->getHook($hookType)?->setBody($body, short: $short);
×
122
                                        }
123
                                }
124
                        }
125
                }
126

127
                if ($props) {
1✔
128
                        assert($class instanceof ClassType || $class instanceof InterfaceType || $class instanceof TraitType);
129
                        $class->setProperties($props);
1✔
130
                }
131

132
                // Methods and trait resolutions
133
                $methods = $resolutions = [];
1✔
134
                foreach ($from->getMethods() as $method) {
1✔
135
                        $declaringMethod = Reflection::getMethodDeclaringMethod($method);
1✔
136
                        $declaringClass = $declaringMethod->getDeclaringClass();
1✔
137

138
                        if (
139
                                $declaringClass->name === $from->name
1✔
140
                                && (!$from instanceof \ReflectionEnum || !method_exists($from->isBacked() ? \BackedEnum::class : \UnitEnum::class, $method->name))
1✔
141
                        ) {
142
                                $methods[] = $m = $this->fromMethodReflection($method);
1✔
143
                                if ($withBodies && ($file = $declaringClass->getFileName())) {
1✔
144
                                        $bodies = &$this->bodyCache[$declaringClass->name];
1✔
145
                                        $bodies ??= $this->getExtractor($file)->extractMethodBodies($declaringClass->name);
1✔
146
                                        if (isset($bodies[$declaringMethod->name])) {
1✔
147
                                                $m->setBody($bodies[$declaringMethod->name]);
1✔
148
                                        }
149
                                }
150
                        }
151

152
                        $modifier = $declaringMethod->getModifiers() !== $method->getModifiers()
1✔
153
                                ? ' ' . $this->getVisibility($method)->value
1✔
154
                                : null;
1✔
155
                        $alias = $declaringMethod->name !== $method->name ? ' ' . $method->name : '';
1✔
156
                        if ($modifier || $alias) {
1✔
157
                                $resolutions[] = $declaringMethod->name . ' as' . $modifier . $alias;
1✔
158
                        }
159
                }
160

161
                assert($class instanceof ClassType || $class instanceof InterfaceType || $class instanceof TraitType || $class instanceof EnumType);
162
                $class->setMethods($methods);
1✔
163

164
                // Traits
165
                foreach ($from->getTraitNames() as $trait) {
1✔
166
                        assert($class instanceof ClassType || $class instanceof TraitType || $class instanceof EnumType);
167
                        $trait = $class->addTrait($trait);
1✔
168
                        foreach ($resolutions as $resolution) {
1✔
169
                                $trait->addResolution($resolution);
1✔
170
                        }
171
                        $resolutions = [];
1✔
172
                }
173

174
                // Constants and enum cases
175
                $consts = $cases = [];
1✔
176
                foreach ($from->getReflectionConstants() as $const) {
1✔
177
                        if ($from instanceof \ReflectionEnum && $from->hasCase($const->name)) {
1✔
178
                                $cases[] = $this->fromCaseReflection($const);
1✔
179
                        } elseif ($const->getDeclaringClass()->name === $from->name) {
1✔
180
                                $consts[] = $this->fromConstantReflection($const);
1✔
181
                        }
182
                }
183

184
                if ($consts) {
1✔
185
                        $class->setConstants($consts);
1✔
186
                }
187
                if ($cases) {
1✔
188
                        assert($class instanceof EnumType);
189
                        $class->setCases($cases);
1✔
190
                }
191
        }
1✔
192

193

194
        public function fromMethodReflection(\ReflectionMethod $from): Method
1✔
195
        {
196
                $method = new Method($from->name);
1✔
197
                $method->setParameters(array_map($this->fromParameterReflection(...), $from->getParameters()));
1✔
198
                $method->setStatic($from->isStatic());
1✔
199
                $isInterface = $from->getDeclaringClass()->isInterface();
1✔
200
                $method->setVisibility($isInterface ? null : $this->getVisibility($from));
1✔
201
                $method->setFinal($from->isFinal());
1✔
202
                $method->setAbstract($from->isAbstract() && !$isInterface);
1✔
203
                $method->setReturnReference($from->returnsReference());
1✔
204
                $method->setVariadic($from->isVariadic());
1✔
205
                $method->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
1✔
206
                $method->setAttributes($this->formatAttributes($from->getAttributes()));
1✔
207
                $method->setReturnType((string) $from->getReturnType());
1✔
208

209
                return $method;
1✔
210
        }
211

212

213
        /**
214
         * Creates a GlobalFunction or Closure instance from a reflection object.
215
         * @param  bool  $withBody  load function body (requires nikic/php-parser, not available for closures or internal functions)
216
         */
217
        public function fromFunctionReflection(\ReflectionFunction $from, bool $withBody = false): GlobalFunction|Closure
1✔
218
        {
219
                $function = $from->isClosure() ? new Closure : new GlobalFunction($from->name);
1✔
220
                $function->setParameters(array_map($this->fromParameterReflection(...), $from->getParameters()));
1✔
221
                $function->setReturnReference($from->returnsReference());
1✔
222
                $function->setVariadic($from->isVariadic());
1✔
223
                if (!$from->isClosure()) {
1✔
224
                        assert($function instanceof GlobalFunction);
225
                        $function->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
1✔
226
                }
227

228
                $function->setAttributes($this->formatAttributes($from->getAttributes()));
1✔
229
                $function->setReturnType((string) $from->getReturnType());
1✔
230

231
                if ($withBody) {
1✔
232
                        if ($from->isClosure() || $from->isInternal() || !($file = $from->getFileName())) {
1✔
233
                                throw new Nette\NotSupportedException('The $withBody parameter cannot be used for closures or internal functions.');
×
234
                        }
235

236
                        $function->setBody($this->getExtractor($file)->extractFunctionBody($from->name));
1✔
237
                }
238

239
                return $function;
1✔
240
        }
241

242

243
        /**
244
         * Creates a Method, GlobalFunction, or Closure instance from a callable.
245
         * @param  callable(): mixed  $from
246
         */
247
        public function fromCallable(callable $from): Method|GlobalFunction|Closure
1✔
248
        {
249
                $ref = Nette\Utils\Callback::toReflection($from);
1✔
250
                return $ref instanceof \ReflectionMethod
1✔
251
                        ? $this->fromMethodReflection($ref)
1✔
252
                        : $this->fromFunctionReflection($ref);
1✔
253
        }
254

255

256
        public function fromParameterReflection(\ReflectionParameter $from): Parameter
1✔
257
        {
258
                if ($from->isPromoted()) {
1✔
259
                        $property = $from->getDeclaringClass()?->getProperty($from->name);
1✔
260
                        \assert($property instanceof \ReflectionProperty);
261
                        $param = (new PromotedParameter($from->name))
1✔
262
                                ->setVisibility($this->getVisibility($property))
1✔
263
                                ->setReadOnly($property->isReadOnly())
1✔
264
                                ->setFinal(PHP_VERSION_ID >= 80500 && $property->isFinal() && !$property->isPrivateSet());
1✔
265
                        $this->addHooks($property, $param);
1✔
266
                } else {
267
                        $param = new Parameter($from->name);
1✔
268
                }
269
                $param->setReference($from->isPassedByReference());
1✔
270
                $param->setType((string) $from->getType());
1✔
271

272
                if ($from->isDefaultValueAvailable()) {
1✔
273
                        if ($from->isDefaultValueConstant()) {
1✔
274
                                $parts = explode('::', $from->getDefaultValueConstantName() ?? '');
1✔
275
                                if (count($parts) > 1) {
1✔
276
                                        $parts[0] = Helpers::tagName($parts[0]);
1✔
277
                                }
278

279
                                $param->setDefaultValue(new Literal(implode('::', $parts)));
1✔
280
                        } elseif (is_object($from->getDefaultValue())) {
1✔
281
                                $param->setDefaultValue($this->fromObject($from->getDefaultValue()));
1✔
282
                        } else {
283
                                $param->setDefaultValue($from->getDefaultValue());
1✔
284
                        }
285
                }
286

287
                $param->setAttributes($this->formatAttributes($from->getAttributes()));
1✔
288
                return $param;
1✔
289
        }
290

291

292
        public function fromConstantReflection(\ReflectionClassConstant $from): Constant
1✔
293
        {
294
                $const = new Constant($from->name);
1✔
295
                $const->setValue($from->getValue());
1✔
296
                $const->setVisibility($this->getVisibility($from));
1✔
297
                $const->setFinal($from->isFinal());
1✔
298
                if (PHP_VERSION_ID >= 80300 && ($type = $from->getType())) {
1✔
UNCOV
299
                        $const->setType((string) $type);
×
300
                }
301
                $const->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
1✔
302
                $const->setAttributes($this->formatAttributes($from->getAttributes()));
1✔
303
                return $const;
1✔
304
        }
305

306

307
        public function fromCaseReflection(\ReflectionClassConstant $from): EnumCase
1✔
308
        {
309
                $const = new EnumCase($from->name);
1✔
310
                $const->setValue($from->getValue()->value ?? null);
1✔
311
                $const->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
1✔
312
                $const->setAttributes($this->formatAttributes($from->getAttributes()));
1✔
313
                return $const;
1✔
314
        }
315

316

317
        public function fromPropertyReflection(\ReflectionProperty $from): Property
1✔
318
        {
319
                $defaults = $from->getDeclaringClass()->getDefaultProperties();
1✔
320
                $prop = new Property($from->name);
1✔
321
                $prop->setValue($defaults[$prop->getName()] ?? null);
1✔
322
                $prop->setStatic($from->isStatic());
1✔
323
                $prop->setVisibility($this->getVisibility($from));
1✔
324
                $prop->setType((string) $from->getType());
1✔
325
                $prop->setInitialized($from->hasType() && array_key_exists($prop->getName(), $defaults));
1✔
326
                $prop->setReadOnly($from->isReadOnly());
1✔
327
                $prop->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
1✔
328
                $prop->setAttributes($this->formatAttributes($from->getAttributes()));
1✔
329

330
                if (PHP_VERSION_ID >= 80400) {
1✔
331
                        $this->addHooks($from, $prop);
×
UNCOV
332
                        $isInterface = $from->getDeclaringClass()->isInterface();
×
UNCOV
333
                        $prop->setFinal($from->isFinal() && !$prop->isPrivate(PropertyAccessMode::Set));
×
UNCOV
334
                        $prop->setAbstract($from->isAbstract() && !$isInterface);
×
335
                }
336
                return $prop;
1✔
337
        }
338

339

340
        private function addHooks(\ReflectionProperty $from, Property|PromotedParameter $prop): void
1✔
341
        {
342
                if (PHP_VERSION_ID < 80400) {
1✔
343
                        return;
1✔
344
                }
345

346
                $getV = $this->getVisibility($from);
×
347
                $setV = $from->isPrivateSet()
×
348
                        ? Visibility::Private
×
349
                        : ($from->isProtectedSet() ? Visibility::Protected : $getV);
×
350
                $defaultSetV = $from->isReadOnly() && $getV !== Visibility::Private
×
351
                        ? Visibility::Protected
×
UNCOV
352
                        : $getV;
×
UNCOV
353
                if ($setV !== $defaultSetV) {
×
UNCOV
354
                        $prop->setVisibility($getV === Visibility::Public ? null : $getV, $setV);
×
355
                }
356

357
                /** @var 'set'|'get' $type */
358
                foreach ($from->getHooks() as $type => $hook) {
×
359
                        $params = $hook->getParameters();
×
360
                        if (
UNCOV
361
                                count($params) === 1
×
362
                                && $params[0]->getName() === 'value'
×
UNCOV
363
                                && $params[0]->getType() == $from->getType() // intentionally ==
×
364
                        ) {
365
                                $params = [];
×
366
                        }
367
                        $prop->addHook($type)
×
368
                                ->setParameters(array_map($this->fromParameterReflection(...), $params))
×
369
                                ->setAbstract($hook->isAbstract())
×
370
                                ->setFinal($hook->isFinal())
×
UNCOV
371
                                ->setReturnReference($hook->returnsReference())
×
UNCOV
372
                                ->setComment(Helpers::unformatDocComment((string) $hook->getDocComment()))
×
UNCOV
373
                                ->setAttributes($this->formatAttributes($hook->getAttributes()));
×
374
                }
375
        }
376

377

378
        public function fromObject(object $obj): Literal
1✔
379
        {
380
                return new Literal('new \\' . $obj::class . '(/* unknown */)');
1✔
381
        }
382

383

384
        /**
385
         * Parses PHP source code and returns the first class-like type found.
386
         * @throws Nette\InvalidStateException if the code contains no class
387
         */
388
        public function fromClassCode(string $code): ClassLike
1✔
389
        {
390
                $classes = $this->fromCode($code)->getClasses();
1✔
391
                return reset($classes) ?: throw new Nette\InvalidStateException('The code does not contain any class.');
1✔
392
        }
393

394

395
        /**
396
         * Parses PHP source code and returns the resulting PhpFile representation.
397
         */
398
        public function fromCode(string $code): PhpFile
1✔
399
        {
400
                $reader = new Extractor($code);
1✔
401
                return $reader->extractAll();
1✔
402
        }
403

404

405
        /**
406
         * @param  list<\ReflectionAttribute<object>>  $attrs
407
         * @return list<Attribute>
408
         */
409
        private function formatAttributes(array $attrs): array
1✔
410
        {
411
                $res = [];
1✔
412
                foreach ($attrs as $attr) {
1✔
413
                        $args = $attr->getArguments();
1✔
414
                        foreach ($args as &$arg) {
1✔
415
                                if (is_object($arg)) {
1✔
416
                                        $arg = $this->fromObject($arg);
1✔
417
                                }
418
                        }
419
                        $res[] = new Attribute($attr->getName(), $args);
1✔
420
                }
421
                return $res;
1✔
422
        }
423

424

425
        private function getVisibility(\ReflectionProperty|\ReflectionMethod|\ReflectionClassConstant $from): Visibility
1✔
426
        {
427
                return $from->isPrivate()
1✔
428
                        ? Visibility::Private
1✔
429
                        : ($from->isProtected() ? Visibility::Protected : Visibility::Public);
1✔
430
        }
431

432

433
        private function getExtractor(string $file): Extractor
1✔
434
        {
435
                $cache = &$this->extractorCache[$file];
1✔
436
                $cache ??= new Extractor(file_get_contents($file) ?: throw new Nette\InvalidStateException("Unable to read file '$file'."));
1✔
437
                return $cache;
1✔
438
        }
439
}
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