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

nette / application / 20834855301

08 Jan 2026 10:54PM UTC coverage: 84.605% (+1.7%) from 82.856%
20834855301

push

github

dg
added CLAUDE.md

1940 of 2293 relevant lines covered (84.61%)

0.85 hits per line

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

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

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

22

23
/**
24
 * Application extension for Nette DI.
25
 */
26
final class ApplicationExtension extends Nette\DI\CompilerExtension
27
{
28
        /** @var string[] */
29
        private readonly array $scanDirs;
30
        private int $invalidLinkMode;
31

32
        /** @var array<class-string, true> */
33
        private array $checked = [];
34

35

36
        public function __construct(
1✔
37
                private readonly bool $debugMode = false,
38
                ?array $scanDirs = null,
39
                private readonly ?string $tempDir = null,
40
                private readonly ?Nette\Loaders\RobotLoader $robotLoader = null,
41
        ) {
42
                $this->scanDirs = (array) $scanDirs;
1✔
43
        }
1✔
44

45

46
        public function getConfigSchema(): Nette\Schema\Schema
47
        {
48
                return Expect::structure([
1✔
49
                        'debugger' => Expect::bool(),
1✔
50
                        'errorPresenter' => Expect::anyOf(
1✔
51
                                Expect::structure([
1✔
52
                                        '4xx' => Expect::string('Nette:Error')->dynamic(),
1✔
53
                                        '5xx' => Expect::string('Nette:Error')->dynamic(),
1✔
54
                                ])->castTo('array'),
1✔
55
                                Expect::string()->dynamic(),
1✔
56
                        )->firstIsDefault(),
1✔
57
                        'catchExceptions' => Expect::bool(false)->dynamic(),
1✔
58
                        'mapping' => Expect::anyOf(
1✔
59
                                Expect::string(),
1✔
60
                                Expect::arrayOf('string|array'),
1✔
61
                        ),
62
                        'aliases' => Expect::arrayOf('string'),
1✔
63
                        'scanDirs' => Expect::anyOf(
1✔
64
                                Expect::arrayOf('string')->default($this->scanDirs)->mergeDefaults(),
1✔
65
                                false,
1✔
66
                        )->firstIsDefault(),
1✔
67
                        'scanComposer' => Expect::bool(class_exists(ClassLoader::class)),
1✔
68
                        'scanFilter' => Expect::string('*Presenter'),
1✔
69
                        'silentLinks' => Expect::bool(),
1✔
70
                ]);
71
        }
72

73

74
        public function loadConfiguration(): void
75
        {
76
                $config = $this->config;
1✔
77
                $builder = $this->getContainerBuilder();
1✔
78
                $builder->addExcludedClasses([UI\Presenter::class]);
1✔
79

80
                $this->invalidLinkMode = $this->debugMode
1✔
81
                        ? UI\Presenter::InvalidLinkTextual | ($config->silentLinks ? 0 : UI\Presenter::InvalidLinkWarning)
1✔
82
                        : UI\Presenter::InvalidLinkWarning;
1✔
83

84
                $application = $builder->addDefinition($this->prefix('application'))
1✔
85
                        ->setFactory(Nette\Application\Application::class);
1✔
86
                if ($config->catchExceptions || !$this->debugMode) {
1✔
87
                        $application->addSetup('$error4xxPresenter', [is_array($config->errorPresenter) ? $config->errorPresenter['4xx'] : $config->errorPresenter]);
1✔
88
                        $application->addSetup('$errorPresenter', [is_array($config->errorPresenter) ? $config->errorPresenter['5xx'] : $config->errorPresenter]);
1✔
89
                }
90

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

93
                if ($this->debugMode && ($config->scanDirs || $this->robotLoader) && $this->tempDir) {
1✔
94
                        $touch = $this->tempDir . '/touch';
×
95
                        Nette\Utils\FileSystem::createDir($this->tempDir);
×
96
                        $this->getContainerBuilder()->addDependency($touch);
×
97
                }
98

99
                $presenterFactory = $builder->addDefinition($this->prefix('presenterFactory'))
1✔
100
                        ->setType(Nette\Application\IPresenterFactory::class)
1✔
101
                        ->setFactory(Nette\Application\PresenterFactory::class, [new Definitions\Statement(
1✔
102
                                Nette\Bridges\ApplicationDI\PresenterFactoryCallback::class,
103
                                [1 => $touch ?? null],
1✔
104
                        )]);
105

106
                if ($config->mapping) {
1✔
107
                        $presenterFactory->addSetup('setMapping', [
×
108
                                is_string($config->mapping) ? ['*' => $config->mapping] : $config->mapping,
×
109
                        ]);
110
                }
111

112
                if ($config->aliases) {
1✔
113
                        $presenterFactory->addSetup('setAliases', [$config->aliases]);
×
114
                }
115

116
                $builder->addDefinition($this->prefix('linkGenerator'))
1✔
117
                        ->setFactory(Nette\Application\LinkGenerator::class, [
1✔
118
                                1 => new Definitions\Statement([new Definitions\Statement('@Nette\Http\IRequest::getUrl'), 'withoutUserInfo']),
1✔
119
                        ]);
120

121
                if ($this->name === 'application') {
1✔
122
                        $builder->addAlias('application', $this->prefix('application'));
1✔
123
                        $builder->addAlias('nette.presenterFactory', $this->prefix('presenterFactory'));
1✔
124
                }
125
        }
1✔
126

127

128
        public function beforeCompile(): void
129
        {
130
                $builder = $this->getContainerBuilder();
1✔
131

132
                if ($this->config->debugger ?? $builder->getByType(Tracy\BlueScreen::class)) {
1✔
133
                        $builder->getDefinition($this->prefix('application'))
×
134
                                ->addSetup([self::class, 'initializeBlueScreenPanel']);
×
135
                }
136

137
                $all = [];
1✔
138

139
                foreach ($builder->findByType(Nette\Application\IPresenter::class) as $def) {
1✔
140
                        $all[$def->getType()] = $def;
1✔
141
                }
142

143
                $counter = 0;
1✔
144
                foreach ($this->findPresenters() as $class) {
1✔
145
                        $this->checkPresenter($class);
1✔
146
                        if (empty($all[$class])) {
1✔
147
                                $all[$class] = $builder->addDefinition($this->prefix((string) ++$counter))
1✔
148
                                        ->setType($class);
1✔
149
                        }
150
                }
151

152
                foreach ($all as $def) {
1✔
153
                        $def->addTag(Nette\DI\Extensions\InjectExtension::TagInject)
1✔
154
                                ->setAutowired(false);
1✔
155

156
                        if (is_subclass_of($def->getType(), UI\Presenter::class) && $def instanceof Definitions\ServiceDefinition) {
1✔
157
                                $def->addSetup('$invalidLinkMode', [$this->invalidLinkMode]);
1✔
158
                        }
159

160
                        $this->compiler->addExportedType($def->getType());
1✔
161
                }
162
        }
1✔
163

164

165
        /** @return string[] */
166
        private function findPresenters(): array
167
        {
168
                $config = $this->getConfig();
1✔
169

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

175
                        $robot = new Nette\Loaders\RobotLoader;
1✔
176
                        $robot->addDirectory(...$config->scanDirs);
1✔
177
                        $robot->acceptFiles = [$config->scanFilter . '.php'];
1✔
178
                        if ($this->tempDir) {
1✔
179
                                $robot->setTempDirectory($this->tempDir);
×
180
                                $robot->refresh();
×
181
                        } else {
182
                                $robot->rebuild();
1✔
183
                        }
184
                } elseif ($this->robotLoader && $config->scanDirs !== false) {
1✔
185
                        $robot = $this->robotLoader;
1✔
186
                        $robot->refresh();
1✔
187
                }
188

189
                $classes = [];
1✔
190
                if (isset($robot)) {
1✔
191
                        $classes = array_keys($robot->getIndexedClasses());
1✔
192
                }
193

194
                if ($config->scanComposer) {
1✔
195
                        $rc = new \ReflectionClass(ClassLoader::class);
1✔
196
                        $classFile = dirname($rc->getFileName()) . '/autoload_classmap.php';
1✔
197
                        if (is_file($classFile)) {
1✔
198
                                $this->getContainerBuilder()->addDependency($classFile);
1✔
199
                                $classes = array_merge($classes, array_keys((fn($path) => require $path)($classFile)));
1✔
200
                        }
201
                }
202

203
                $presenters = [];
1✔
204
                foreach (array_unique($classes) as $class) {
1✔
205
                        if (
206
                                fnmatch($config->scanFilter, $class)
1✔
207
                                && class_exists($class)
1✔
208
                                && ($rc = new \ReflectionClass($class))
1✔
209
                                && $rc->implementsInterface(Nette\Application\IPresenter::class)
1✔
210
                                && !$rc->isAbstract()
1✔
211
                        ) {
212
                                $presenters[] = $rc->getName();
1✔
213
                        }
214
                }
215

216
                return $presenters;
1✔
217
        }
218

219

220
        /** @internal */
221
        public static function initializeBlueScreenPanel(
222
                Tracy\BlueScreen $blueScreen,
223
                Nette\Application\Application $application,
224
        ): void
225
        {
226
                $blueScreen->addPanel(function (?\Throwable $e) use ($application, $blueScreen): ?array {
×
227
                        $dumper = $blueScreen->getDumper();
228
                        return $e ? null : [
229
                                'tab' => 'Nette Application',
230
                                'panel' => '<h3>Requests</h3>' . $dumper($application->getRequests())
231
                                        . '<h3>Presenter</h3>' . $dumper($application->getPresenter()),
232
                        ];
233
                });
×
234
                if (
235
                        version_compare(Tracy\Debugger::Version, '2.9.0', '>=')
×
236
                        && version_compare(Tracy\Debugger::Version, '3.0', '<')
×
237
                ) {
238
                        $blueScreen->addFileGenerator(self::generateNewPresenterFileContents(...));
×
239
                }
240
        }
241

242

243
        public static function generateNewPresenterFileContents(string $file, ?string $class = null): ?string
244
        {
245
                if (!$class || !str_ends_with($file, 'Presenter.php')) {
×
246
                        return null;
×
247
                }
248

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

251
                if ($pos = strrpos($class, '\\')) {
×
252
                        $res .= 'namespace ' . substr($class, 0, $pos) . ";\n\n";
×
253
                        $class = substr($class, $pos + 1);
×
254
                }
255

256
                return $res . "use Nette;\n\n\nclass $class extends Nette\\Application\\UI\\Presenter\n{\n\$END\$\n}\n";
×
257
        }
258

259

260
        private function checkPresenter(string $class): void
1✔
261
        {
262
                if (!is_subclass_of($class, UI\Presenter::class) || isset($this->checked[$class])) {
1✔
263
                        return;
1✔
264
                }
265
                $this->checked[$class] = true;
1✔
266

267
                $rc = new \ReflectionClass($class);
1✔
268
                if ($rc->getParentClass()) {
1✔
269
                        $this->checkPresenter($rc->getParentClass()->getName());
1✔
270
                }
271

272
                foreach ($rc->getProperties() as $rp) {
1✔
273
                        if (($rp->getAttributes($attr = Attributes\Parameter::class) || $rp->getAttributes($attr = Attributes\Persistent::class))
1✔
274
                                && (!$rp->isPublic() || $rp->isStatic() || $rp->isReadOnly())
1✔
275
                        ) {
276
                                throw new Nette\InvalidStateException(sprintf('Property %s: attribute %s can be used only with public non-static property.', Reflection::toString($rp), $attr));
×
277
                        }
278
                }
279

280
                $re = $class::formatActionMethod('') . '.|' . $class::formatRenderMethod('') . '.|' . $class::formatSignalMethod('') . '.';
1✔
281
                foreach ($rc->getMethods() as $rm) {
1✔
282
                        if (preg_match("#^(?!handleInvalidLink)($re)#", $rm->getName()) && (!$rm->isPublic() || $rm->isStatic())) {
1✔
283
                                throw new Nette\InvalidStateException(sprintf('Method %s: this method must be public non-static.', Reflection::toString($rm)));
×
284
                        } elseif (preg_match('#^createComponent.#', $rm->getName()) && ($rm->isPrivate() || $rm->isStatic())) {
1✔
285
                                throw new Nette\InvalidStateException(sprintf('Method %s: this method must be non-private non-static.', Reflection::toString($rm)));
×
286
                        } elseif ($rm->getAttributes(Attributes\Requires::class, \ReflectionAttribute::IS_INSTANCEOF)
1✔
287
                                && !preg_match("#^$re|createComponent.#", $rm->getName())
1✔
288
                        ) {
289
                                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));
×
290
                        } elseif ($rm->getAttributes(Attributes\Deprecated::class) && !preg_match("#^$re#", $rm->getName())) {
1✔
291
                                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));
×
292
                        }
293
                }
294
        }
1✔
295
}
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