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

nette / php-generator / 12077616009

29 Nov 2024 01:41AM UTC coverage: 91.985%. First build
12077616009

push

github

dg
Factory & Extractor: added support for property hooks & asymmetric visibility

39 of 66 new or added lines in 2 files covered. (59.09%)

1733 of 1884 relevant lines covered (91.99%)

0.92 hits per line

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

80.39
/src/PhpGenerator/Factory.php
1
<?php
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
declare(strict_types=1);
9

10
namespace Nette\PhpGenerator;
11

12
use Nette;
13
use Nette\Utils\Reflection;
14

15

16
/**
17
 * Creates a representations based on reflection or source code.
18
 */
19
final class Factory
20
{
21
        /** @var string[][]  */
22
        private array $bodyCache = [];
23

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

27

28
        /** @param  \ReflectionClass<object>  $from */
29
        public function fromClassReflection(
1✔
30
                \ReflectionClass $from,
31
                bool $withBodies = false,
32
        ): ClassLike
33
        {
34
                if ($withBodies && ($from->isAnonymous() || $from->isInternal())) {
1✔
35
                        throw new Nette\NotSupportedException('The $withBodies parameter cannot be used for anonymous or internal classes.');
1✔
36
                }
37

38
                $enumIface = null;
1✔
39
                if (PHP_VERSION_ID >= 80100 && $from->isEnum()) {
1✔
40
                        $class = new EnumType($from->getShortName(), new PhpNamespace($from->getNamespaceName()));
×
41
                        $from = new \ReflectionEnum($from->getName());
×
42
                        $enumIface = $from->isBacked() ? \BackedEnum::class : \UnitEnum::class;
×
43
                } elseif ($from->isAnonymous()) {
1✔
44
                        $class = new ClassType;
1✔
45
                } elseif ($from->isInterface()) {
1✔
46
                        $class = new InterfaceType($from->getShortName(), new PhpNamespace($from->getNamespaceName()));
1✔
47
                } elseif ($from->isTrait()) {
1✔
48
                        $class = new TraitType($from->getShortName(), new PhpNamespace($from->getNamespaceName()));
1✔
49
                } else {
50
                        $class = new ClassType($from->getShortName(), new PhpNamespace($from->getNamespaceName()));
1✔
51
                        $class->setFinal($from->isFinal() && $class->isClass());
1✔
52
                        $class->setAbstract($from->isAbstract() && $class->isClass());
1✔
53
                        $class->setReadOnly(PHP_VERSION_ID >= 80200 && $from->isReadOnly());
1✔
54
                }
55

56
                $ifaces = $from->getInterfaceNames();
1✔
57
                foreach ($ifaces as $iface) {
1✔
58
                        $ifaces = array_filter($ifaces, fn(string $item): bool => !is_subclass_of($iface, $item));
1✔
59
                }
60

61
                if ($from->isInterface()) {
1✔
62
                        $class->setExtends($ifaces);
1✔
63
                } elseif ($ifaces) {
1✔
64
                        $ifaces = array_diff($ifaces, [$enumIface]);
1✔
65
                        $class->setImplements($ifaces);
1✔
66
                }
67

68
                $class->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
1✔
69
                $class->setAttributes($this->getAttributes($from));
1✔
70
                if ($from->getParentClass()) {
1✔
71
                        $class->setExtends($from->getParentClass()->name);
1✔
72
                        $class->setImplements(array_diff($class->getImplements(), $from->getParentClass()->getInterfaceNames()));
1✔
73
                }
74

75
                $props = [];
1✔
76
                foreach ($from->getProperties() as $prop) {
1✔
77
                        $declaringClass = Reflection::getPropertyDeclaringClass($prop);
1✔
78

79
                        if ($prop->isDefault()
1✔
80
                                && $declaringClass->name === $from->name
1✔
81
                                && !$prop->isPromoted()
1✔
82
                                && !$class->isEnum()
1✔
83
                        ) {
84
                                $props[] = $this->fromPropertyReflection($prop);
1✔
85
                        }
86
                }
87

88
                if ($props) {
1✔
89
                        $class->setProperties($props);
1✔
90
                }
91

92
                $methods = $resolutions = [];
1✔
93
                foreach ($from->getMethods() as $method) {
1✔
94
                        $declaringMethod = Reflection::getMethodDeclaringMethod($method);
1✔
95
                        $declaringClass = $declaringMethod->getDeclaringClass();
1✔
96

97
                        if (
98
                                $declaringClass->name === $from->name
1✔
99
                                && (!$enumIface || !method_exists($enumIface, $method->name))
1✔
100
                        ) {
101
                                $methods[] = $m = $this->fromMethodReflection($method);
1✔
102
                                if ($withBodies) {
1✔
103
                                        $bodies = &$this->bodyCache[$declaringClass->name];
1✔
104
                                        $bodies ??= $this->getExtractor($declaringClass->getFileName())->extractMethodBodies($declaringClass->name);
1✔
105
                                        if (isset($bodies[$declaringMethod->name])) {
1✔
106
                                                $m->setBody($bodies[$declaringMethod->name]);
1✔
107
                                        }
108
                                }
109
                        }
110

111
                        $modifier = $declaringMethod->getModifiers() !== $method->getModifiers()
1✔
112
                                ? ' ' . $this->getVisibility($method)
1✔
113
                                : null;
1✔
114
                        $alias = $declaringMethod->name !== $method->name ? ' ' . $method->name : '';
1✔
115
                        if ($modifier || $alias) {
1✔
116
                                $resolutions[] = $declaringMethod->name . ' as' . $modifier . $alias;
1✔
117
                        }
118
                }
119

120
                $class->setMethods($methods);
1✔
121

122
                foreach ($from->getTraitNames() as $trait) {
1✔
123
                        $class->addTrait($trait, $resolutions);
1✔
124
                        $resolutions = [];
1✔
125
                }
126

127
                $consts = $cases = [];
1✔
128
                foreach ($from->getReflectionConstants() as $const) {
1✔
129
                        if ($class->isEnum() && $from->hasCase($const->name)) {
1✔
130
                                $cases[] = $this->fromCaseReflection($const);
×
131
                        } elseif ($const->getDeclaringClass()->name === $from->name) {
1✔
132
                                $consts[] = $this->fromConstantReflection($const);
1✔
133
                        }
134
                }
135

136
                if ($consts) {
1✔
137
                        $class->setConstants($consts);
1✔
138
                }
139
                if ($cases) {
1✔
140
                        $class->setCases($cases);
×
141
                }
142

143
                return $class;
1✔
144
        }
145

146

147
        public function fromMethodReflection(\ReflectionMethod $from): Method
1✔
148
        {
149
                $method = new Method($from->name);
1✔
150
                $method->setParameters(array_map([$this, 'fromParameterReflection'], $from->getParameters()));
1✔
151
                $method->setStatic($from->isStatic());
1✔
152
                $isInterface = $from->getDeclaringClass()->isInterface();
1✔
153
                $method->setVisibility($isInterface ? null : $this->getVisibility($from));
1✔
154
                $method->setFinal($from->isFinal());
1✔
155
                $method->setAbstract($from->isAbstract() && !$isInterface);
1✔
156
                $method->setReturnReference($from->returnsReference());
1✔
157
                $method->setVariadic($from->isVariadic());
1✔
158
                $method->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
1✔
159
                $method->setAttributes($this->getAttributes($from));
1✔
160
                $method->setReturnType((string) $from->getReturnType());
1✔
161

162
                return $method;
1✔
163
        }
164

165

166
        public function fromFunctionReflection(\ReflectionFunction $from, bool $withBody = false): GlobalFunction|Closure
1✔
167
        {
168
                $function = $from->isClosure() ? new Closure : new GlobalFunction($from->name);
1✔
169
                $function->setParameters(array_map([$this, 'fromParameterReflection'], $from->getParameters()));
1✔
170
                $function->setReturnReference($from->returnsReference());
1✔
171
                $function->setVariadic($from->isVariadic());
1✔
172
                if (!$from->isClosure()) {
1✔
173
                        $function->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
1✔
174
                }
175

176
                $function->setAttributes($this->getAttributes($from));
1✔
177
                $function->setReturnType((string) $from->getReturnType());
1✔
178

179
                if ($withBody) {
1✔
180
                        if ($from->isClosure() || $from->isInternal()) {
1✔
181
                                throw new Nette\NotSupportedException('The $withBody parameter cannot be used for closures or internal functions.');
×
182
                        }
183

184
                        $function->setBody($this->getExtractor($from->getFileName())->extractFunctionBody($from->name));
1✔
185
                }
186

187
                return $function;
1✔
188
        }
189

190

191
        public function fromCallable(callable $from): Method|GlobalFunction|Closure
1✔
192
        {
193
                $ref = Nette\Utils\Callback::toReflection($from);
1✔
194
                return $ref instanceof \ReflectionMethod
1✔
195
                        ? $this->fromMethodReflection($ref)
1✔
196
                        : $this->fromFunctionReflection($ref);
1✔
197
        }
198

199

200
        public function fromParameterReflection(\ReflectionParameter $from): Parameter
1✔
201
        {
202
                if ($from->isPromoted()) {
1✔
203
                        $property = $from->getDeclaringClass()->getProperty($from->name);
1✔
204
                        $param = (new PromotedParameter($from->name))
1✔
205
                                ->setVisibility($this->getVisibility($property))
1✔
206
                                ->setReadOnly(PHP_VERSION_ID >= 80100 && $property->isReadonly());
1✔
207
                        $this->addHooks($property, $param);
1✔
208
                } else {
209
                        $param = new Parameter($from->name);
1✔
210
                }
211
                $param->setReference($from->isPassedByReference());
1✔
212
                $param->setType((string) $from->getType());
1✔
213

214
                if ($from->isDefaultValueAvailable()) {
1✔
215
                        if ($from->isDefaultValueConstant()) {
1✔
216
                                $parts = explode('::', $from->getDefaultValueConstantName());
1✔
217
                                if (count($parts) > 1) {
1✔
218
                                        $parts[0] = Helpers::tagName($parts[0]);
1✔
219
                                }
220

221
                                $param->setDefaultValue(new Literal(implode('::', $parts)));
1✔
222
                        } elseif (is_object($from->getDefaultValue())) {
1✔
223
                                $param->setDefaultValue($this->fromObject($from->getDefaultValue()));
×
224
                        } else {
225
                                $param->setDefaultValue($from->getDefaultValue());
1✔
226
                        }
227
                }
228

229
                $param->setAttributes($this->getAttributes($from));
1✔
230
                return $param;
1✔
231
        }
232

233

234
        public function fromConstantReflection(\ReflectionClassConstant $from): Constant
1✔
235
        {
236
                $const = new Constant($from->name);
1✔
237
                $const->setValue($from->getValue());
1✔
238
                $const->setVisibility($this->getVisibility($from));
1✔
239
                $const->setFinal(PHP_VERSION_ID >= 80100 && $from->isFinal());
1✔
240
                $const->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
1✔
241
                $const->setAttributes($this->getAttributes($from));
1✔
242
                return $const;
1✔
243
        }
244

245

246
        public function fromCaseReflection(\ReflectionClassConstant $from): EnumCase
247
        {
248
                $const = new EnumCase($from->name);
×
249
                $const->setValue($from->getValue()->value ?? null);
×
250
                $const->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
×
251
                $const->setAttributes($this->getAttributes($from));
×
252
                return $const;
×
253
        }
254

255

256
        public function fromPropertyReflection(\ReflectionProperty $from): Property
1✔
257
        {
258
                $defaults = $from->getDeclaringClass()->getDefaultProperties();
1✔
259
                $prop = new Property($from->name);
1✔
260
                $prop->setValue($defaults[$prop->getName()] ?? null);
1✔
261
                $prop->setStatic($from->isStatic());
1✔
262
                $prop->setVisibility($this->getVisibility($from));
1✔
263
                $prop->setType((string) $from->getType());
1✔
264
                $prop->setInitialized($from->hasType() && array_key_exists($prop->getName(), $defaults));
1✔
265
                $prop->setReadOnly(PHP_VERSION_ID >= 80100 && $from->isReadOnly());
1✔
266
                $prop->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
1✔
267
                $prop->setAttributes($this->getAttributes($from));
1✔
268

269
                if (PHP_VERSION_ID >= 80400) {
1✔
NEW
270
                        $this->addHooks($from, $prop);
×
NEW
271
                        $isInterface = $from->getDeclaringClass()->isInterface();
×
NEW
272
                        $prop->setFinal($from->isFinal() && !$prop->isPrivate(PropertyAccessMode::Set));
×
NEW
273
                        $prop->setAbstract($from->isAbstract() && !$isInterface);
×
274
                }
275
                return $prop;
1✔
276
        }
277

278

279
        private function addHooks(\ReflectionProperty $from, Property|PromotedParameter $prop): void
1✔
280
        {
281
                if (PHP_VERSION_ID < 80400) {
1✔
282
                        return;
1✔
283
                }
284

NEW
285
                $getV = $this->getVisibility($from);
×
NEW
286
                $setV = $from->isPrivateSet()
×
NEW
287
                        ? Visibility::Private
×
NEW
288
                        : ($from->isProtectedSet() ? Visibility::Protected : $getV);
×
NEW
289
                $defaultSetV = $from->isReadOnly() && $getV !== Visibility::Private
×
NEW
290
                        ? Visibility::Protected
×
NEW
291
                        : $getV;
×
NEW
292
                if ($setV !== $defaultSetV) {
×
NEW
293
                        $prop->setVisibility($getV === Visibility::Public ? null : $getV, $setV);
×
294
                }
295

NEW
296
                foreach ($from->getHooks() as $type => $hook) {
×
NEW
297
                        $params = $hook->getParameters();
×
298
                        if (
NEW
299
                                count($params) === 1
×
NEW
300
                                && $params[0]->getName() === 'value'
×
NEW
301
                                && $params[0]->getType() == $from->getType() // intentionally ==
×
302
                        ) {
NEW
303
                                $params = [];
×
304
                        }
NEW
305
                        $prop->addHook($type)
×
NEW
306
                                ->setParameters(array_map([$this, 'fromParameterReflection'], $params))
×
NEW
307
                                ->setAbstract($hook->isAbstract())
×
NEW
308
                                ->setFinal($hook->isFinal())
×
NEW
309
                                ->setReturnReference($hook->returnsReference())
×
NEW
310
                                ->setComment(Helpers::unformatDocComment((string) $hook->getDocComment()))
×
NEW
311
                                ->setAttributes($this->getAttributes($hook));
×
312
                }
313
        }
314

315

316
        public function fromObject(object $obj): Literal
317
        {
318
                return new Literal('new \\' . $obj::class . '(/* unknown */)');
×
319
        }
320

321

322
        public function fromClassCode(string $code): ClassLike
1✔
323
        {
324
                $classes = $this->fromCode($code)->getClasses();
1✔
325
                return reset($classes) ?: throw new Nette\InvalidStateException('The code does not contain any class.');
1✔
326
        }
327

328

329
        public function fromCode(string $code): PhpFile
1✔
330
        {
331
                $reader = new Extractor($code);
1✔
332
                return $reader->extractAll();
1✔
333
        }
334

335

336
        private function getAttributes($from): array
337
        {
338
                return array_map(function ($attr) {
1✔
339
                        $args = $attr->getArguments();
1✔
340
                        foreach ($args as &$arg) {
1✔
341
                                if (is_object($arg)) {
1✔
342
                                        $arg = $this->fromObject($arg);
×
343
                                }
344
                        }
345

346
                        return new Attribute($attr->getName(), $args);
1✔
347
                }, $from->getAttributes());
1✔
348
        }
349

350

351
        private function getVisibility($from): string
352
        {
353
                return $from->isPrivate()
1✔
354
                        ? Visibility::Private
1✔
355
                        : ($from->isProtected() ? Visibility::Protected : Visibility::Public);
1✔
356
        }
357

358

359
        private function getExtractor(string $file): Extractor
1✔
360
        {
361
                $cache = &$this->extractorCache[$file];
1✔
362
                $cache ??= new Extractor(file_get_contents($file));
1✔
363
                return $cache;
1✔
364
        }
365
}
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