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

nette / application / 27919019709

21 Jun 2026 10:08PM UTC coverage: 84.111% (+0.05%) from 84.059%
27919019709

push

github

dg
phpstan fix

2038 of 2423 relevant lines covered (84.11%)

0.84 hits per line

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

80.56
/src/Bridges/ApplicationDI/ApplicationExtension.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\Bridges\ApplicationDI;
9

10
use Composer\Autoload\ClassLoader;
11
use Nette;
12
use Nette\Application\Attributes;
13
use Nette\Application\UI;
14
use Nette\DI\Definitions;
15
use Nette\Schema\Expect;
16
use Nette\Utils\Reflection;
17
use Tracy;
18
use function is_array, is_string, sprintf;
19

20

21
/**
22
 * Application extension for Nette DI.
23
 *
24
 * @property object{
25
 *     debugger: bool|null,
26
 *     errorPresenter: string|array{'4xx': string, '5xx': string},
27
 *     catchExceptions: bool,
28
 *     mapping: string|array<string, string|array<mixed>>|null,
29
 *     aliases: array<string, string>,
30
 *     scanDirs: list<string>|false,
31
 *     scanComposer: bool,
32
 *     scanFilter: string,
33
 *     silentLinks: bool|null,
34
 * } $config
35
 */
36
final class ApplicationExtension extends Nette\DI\CompilerExtension
37
{
38
        /** @var string[] */
39
        private readonly array $scanDirs;
40
        private int $invalidLinkMode;
41

42
        /** @var array<class-string, true> */
43
        private array $checked = [];
44

45

46
        /** @param  ?list<string>  $scanDirs */
47
        public function __construct(
1✔
48
                private readonly bool $debugMode = false,
49
                ?array $scanDirs = null,
50
                private readonly ?string $tempDir = null,
51
                private readonly ?Nette\Loaders\RobotLoader $robotLoader = null,
52
        ) {
53
                $this->scanDirs = (array) $scanDirs;
1✔
54
        }
1✔
55

56

57
        public function getConfigSchema(): Nette\Schema\Schema
58
        {
59
                return Expect::structure([
1✔
60
                        'debugger' => Expect::bool(),
1✔
61
                        'errorPresenter' => Expect::anyOf(
1✔
62
                                Expect::structure([
1✔
63
                                        '4xx' => Expect::string('Nette:Error')->dynamic(),
1✔
64
                                        '5xx' => Expect::string('Nette:Error')->dynamic(),
1✔
65
                                ])->castTo('array'),
1✔
66
                                Expect::string()->dynamic(),
1✔
67
                        )->firstIsDefault(),
1✔
68
                        'catchExceptions' => Expect::bool(false)->dynamic(),
1✔
69
                        'mapping' => Expect::anyOf(
1✔
70
                                Expect::string(),
1✔
71
                                Expect::arrayOf('string|array'),
1✔
72
                        ),
73
                        'aliases' => Expect::arrayOf('string'),
1✔
74
                        'scanDirs' => Expect::anyOf(
1✔
75
                                Expect::arrayOf('string')->default($this->scanDirs)->mergeDefaults(),
1✔
76
                                false,
1✔
77
                        )->firstIsDefault(),
1✔
78
                        'scanComposer' => Expect::bool(class_exists(ClassLoader::class)),
1✔
79
                        'scanFilter' => Expect::string('*Presenter'),
1✔
80
                        'silentLinks' => Expect::bool(),
1✔
81
                ]);
82
        }
83

84

85
        public function loadConfiguration(): void
86
        {
87
                $config = $this->config;
1✔
88
                $builder = $this->getContainerBuilder();
1✔
89
                $builder->addExcludedClasses([UI\Presenter::class]);
1✔
90

91
                $this->invalidLinkMode = $this->debugMode
1✔
92
                        ? UI\Presenter::InvalidLinkTextual | ($config->silentLinks ? 0 : UI\Presenter::InvalidLinkWarning)
1✔
93
                        : UI\Presenter::InvalidLinkWarning;
1✔
94

95
                $application = $builder->addDefinition($this->prefix('application'))
1✔
96
                        ->setFactory(Nette\Application\Application::class);
1✔
97
                if ($config->catchExceptions || !$this->debugMode) {
1✔
98
                        $application->addSetup('$error4xxPresenter', [is_array($config->errorPresenter) ? $config->errorPresenter['4xx'] : $config->errorPresenter]);
1✔
99
                        $application->addSetup('$errorPresenter', [is_array($config->errorPresenter) ? $config->errorPresenter['5xx'] : $config->errorPresenter]);
1✔
100
                }
101

102
                $this->compiler->addExportedType(Nette\Application\Application::class);
1✔
103

104
                if ($this->debugMode && ($config->scanDirs || $this->robotLoader) && $this->tempDir) {
1✔
105
                        $touch = $this->tempDir . '/touch';
×
106
                        Nette\Utils\FileSystem::createDir($this->tempDir);
×
107
                        $this->getContainerBuilder()->addDependency($touch);
×
108
                }
109

110
                $presenterFactory = $builder->addDefinition($this->prefix('presenterFactory'))
1✔
111
                        ->setType(Nette\Application\IPresenterFactory::class)
1✔
112
                        ->setFactory(Nette\Application\PresenterFactory::class, [new Definitions\Statement(
1✔
113
                                Nette\Bridges\ApplicationDI\PresenterFactoryCallback::class,
114
                                [1 => $this->invalidLinkMode, $touch ?? null],
1✔
115
                        )]);
116

117
                if ($config->mapping) {
1✔
118
                        $presenterFactory->addSetup('setMapping', [
×
119
                                is_string($config->mapping) ? ['*' => $config->mapping] : $config->mapping,
×
120
                        ]);
121
                }
122

123
                if ($config->aliases) {
1✔
124
                        $presenterFactory->addSetup('setAliases', [$config->aliases]);
×
125
                }
126

127
                $builder->addDefinition($this->prefix('linkGenerator'))
1✔
128
                        ->setFactory(Nette\Application\LinkGenerator::class, [
1✔
129
                                1 => new Definitions\Statement([new Definitions\Statement('@Nette\Http\IRequest::getUrl'), 'withoutUserInfo']),
1✔
130
                        ]);
131

132
                if ($this->name === 'application') {
1✔
133
                        $builder->addAlias('application', $this->prefix('application'));
1✔
134
                        $builder->addAlias('nette.presenterFactory', $this->prefix('presenterFactory'));
1✔
135
                }
136
        }
1✔
137

138

139
        public function beforeCompile(): void
140
        {
141
                $builder = $this->getContainerBuilder();
1✔
142

143
                if ($this->config->debugger ?? $builder->getByType(Tracy\BlueScreen::class)) {
1✔
144
                        $builder->getDefinition($this->prefix('application'))
×
145
                                ->addSetup([self::class, 'initializeBlueScreenPanel']);
×
146
                }
147

148
                $all = [];
1✔
149

150
                foreach ($builder->findByType(Nette\Application\IPresenter::class) as $def) {
1✔
151
                        $all[(string) $def->getType()] = $def;
1✔
152
                }
153

154
                $counter = 0;
1✔
155
                foreach ($this->findPresenters() as $class) {
1✔
156
                        $this->checkPresenter($class);
1✔
157
                        if (empty($all[$class])) {
1✔
158
                                $all[$class] = $builder->addDefinition($this->prefix((string) ++$counter))
1✔
159
                                        ->setType($class);
1✔
160
                        }
161
                }
162

163
                foreach ($all as $def) {
1✔
164
                        $def->addTag(Nette\DI\Extensions\InjectExtension::TagInject)
1✔
165
                                ->setAutowired(false);
1✔
166

167
                        $type = $def->getType();
1✔
168
                        assert($type !== null);
169
                        if (is_subclass_of($type, UI\Presenter::class) && $def instanceof Definitions\ServiceDefinition) {
1✔
170
                                $def->addSetup('$invalidLinkMode', [$this->invalidLinkMode]);
1✔
171
                        }
172

173
                        $this->compiler->addExportedType($type);
1✔
174
                }
175
        }
1✔
176

177

178
        /** @return list<class-string<Nette\Application\IPresenter>> */
179
        private function findPresenters(): array
180
        {
181
                $config = $this->getConfig();
1✔
182

183
                if ($config->scanDirs) {
1✔
184
                        if (!class_exists(Nette\Loaders\RobotLoader::class)) {
1✔
185
                                throw new Nette\NotSupportedException("RobotLoader is required to find presenters, install package `nette/robot-loader` or disable option {$this->prefix('scanDirs')}: false");
×
186
                        }
187

188
                        $robot = new Nette\Loaders\RobotLoader;
1✔
189
                        $robot->addDirectory(...$config->scanDirs);
1✔
190
                        $robot->acceptFiles = [$config->scanFilter . '.php'];
1✔
191
                        if ($this->tempDir) {
1✔
192
                                $robot->setTempDirectory($this->tempDir);
×
193
                                $robot->refresh();
×
194
                        } else {
195
                                $robot->rebuild();
1✔
196
                        }
197
                } elseif ($this->robotLoader && $config->scanDirs !== false) {
1✔
198
                        $robot = $this->robotLoader;
1✔
199
                        $robot->refresh();
1✔
200
                }
201

202
                $classes = [];
1✔
203
                if (isset($robot)) {
1✔
204
                        $classes = array_keys($robot->getIndexedClasses());
1✔
205
                }
206

207
                if ($config->scanComposer) {
1✔
208
                        $rc = new \ReflectionClass(ClassLoader::class);
1✔
209
                        $classFile = dirname((string) $rc->getFileName()) . '/autoload_classmap.php';
1✔
210
                        if (is_file($classFile)) {
1✔
211
                                $this->getContainerBuilder()->addDependency($classFile);
1✔
212
                                $classes = array_merge($classes, array_keys((fn($path) => require $path)($classFile)));
1✔
213
                        }
214
                }
215

216
                $presenters = [];
1✔
217
                foreach (array_unique($classes) as $class) {
1✔
218
                        if (
219
                                is_string($class)
1✔
220
                                && fnmatch($config->scanFilter, $class)
1✔
221
                                && class_exists($class)
1✔
222
                                && is_subclass_of($class, Nette\Application\IPresenter::class)
1✔
223
                                && !(new \ReflectionClass($class))->isAbstract()
1✔
224
                        ) {
225
                                $presenters[] = $class;
1✔
226
                        }
227
                }
228

229
                return $presenters;
1✔
230
        }
231

232

233
        /** @internal */
234
        public static function initializeBlueScreenPanel(
235
                Tracy\BlueScreen $blueScreen,
236
                Nette\Application\Application $application,
237
        ): void
238
        {
239
                $blueScreen->addPanel(function (?\Throwable $e) use ($application, $blueScreen): ?array {
×
240
                        $dumper = $blueScreen->getDumper();
241
                        return $e ? null : [
242
                                'tab' => 'Nette Application',
243
                                'panel' => '<h3>Requests</h3>' . $dumper($application->getRequests())
244
                                        . '<h3>Presenter</h3>' . $dumper($application->getPresenter()),
245
                        ];
246
                });
×
247
                if (
248
                        version_compare(Tracy\Debugger::Version, '2.9.0', '>=')
×
249
                        && version_compare(Tracy\Debugger::Version, '3.0', '<')
×
250
                ) {
251
                        $blueScreen->addFileGenerator(self::generateNewPresenterFileContents(...));
×
252
                }
253
        }
254

255

256
        public static function generateNewPresenterFileContents(string $file, ?string $class = null): ?string
257
        {
258
                if (!$class || !str_ends_with($file, 'Presenter.php')) {
×
259
                        return null;
×
260
                }
261

262
                $res = "<?php\n\ndeclare(strict_types=1);\n\n";
×
263

264
                if ($pos = strrpos($class, '\\')) {
×
265
                        $res .= 'namespace ' . substr($class, 0, $pos) . ";\n\n";
×
266
                        $class = substr($class, $pos + 1);
×
267
                }
268

269
                return $res . "use Nette;\n\n\nclass $class extends Nette\\Application\\UI\\Presenter\n{\n\$END\$\n}\n";
×
270
        }
271

272

273
        /** @param class-string $class */
274
        private function checkPresenter(string $class): void
1✔
275
        {
276
                if (!is_subclass_of($class, UI\Presenter::class) || isset($this->checked[$class])) {
1✔
277
                        return;
1✔
278
                }
279
                $this->checked[$class] = true;
1✔
280

281
                $rc = new \ReflectionClass($class);
1✔
282
                if ($rc->getParentClass()) {
1✔
283
                        $this->checkPresenter($rc->getParentClass()->getName());
1✔
284
                }
285

286
                foreach ($rc->getProperties() as $rp) {
1✔
287
                        if (($rp->getAttributes($attr = Attributes\Parameter::class) || $rp->getAttributes($attr = Attributes\Persistent::class))
1✔
288
                                && (!$rp->isPublic() || $rp->isStatic() || $rp->isReadOnly())
1✔
289
                        ) {
290
                                throw new Nette\InvalidStateException(sprintf('Property %s: attribute %s can be used only with public non-static property.', Reflection::toString($rp), $attr));
×
291
                        }
292
                }
293

294
                $re = $class::formatActionMethod('') . '.|' . $class::formatRenderMethod('') . '.|' . $class::formatSignalMethod('') . '.';
1✔
295
                foreach ($rc->getMethods() as $rm) {
1✔
296
                        if (preg_match("#^(?!handleInvalidLink)($re)#", $rm->getName()) && (!$rm->isPublic() || $rm->isStatic())) {
1✔
297
                                throw new Nette\InvalidStateException(sprintf('Method %s: this method must be public non-static.', Reflection::toString($rm)));
×
298
                        } elseif (preg_match('#^createComponent.#', $rm->getName()) && ($rm->isPrivate() || $rm->isStatic())) {
1✔
299
                                throw new Nette\InvalidStateException(sprintf('Method %s: this method must be non-private non-static.', Reflection::toString($rm)));
×
300
                        } elseif ($rm->getAttributes(Attributes\Requires::class, \ReflectionAttribute::IS_INSTANCEOF)
1✔
301
                                && !preg_match("#^$re|createComponent.#", $rm->getName())
1✔
302
                        ) {
303
                                throw new Nette\InvalidStateException(sprintf('Method %s: attribute %s can be used only with action, render, handle or createComponent methods.', Reflection::toString($rm), Attributes\Requires::class));
×
304
                        } elseif ($rm->getAttributes(Attributes\Deprecated::class) && !preg_match("#^$re#", $rm->getName())) {
1✔
305
                                throw new Nette\InvalidStateException(sprintf('Method %s: attribute %s can be used only with action, render or handle methods.', Reflection::toString($rm), Attributes\Deprecated::class));
×
306
                        }
307
                }
308
        }
1✔
309
}
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