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

nette / application / 20561245963

28 Dec 2025 11:47PM UTC coverage: 82.276% (-0.2%) from 82.434%
20561245963

push

github

dg
composer: calls Tester with the -C parameter

A pragmatic decision - I do not know how to set extension_dir in a cross-platform way

1959 of 2381 relevant lines covered (82.28%)

0.82 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
        private array $checked = [];
32

33

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

43

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

71

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

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

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

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

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

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

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

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

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

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

125

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

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

135
                $all = [];
1✔
136

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

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

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

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

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

162

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

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

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

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

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

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

214
                return $presenters;
1✔
215
        }
216

217

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

240

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

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

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

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

257

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

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

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

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