• 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

92.57
/src/Application/LinkGenerator.php
1
<?php declare(strict_types=1);
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\Application;
9

10
use Nette\Http\UrlScript;
11
use Nette\Routing\Router;
12
use Nette\Utils\Reflection;
13
use function array_intersect_key, array_key_exists, http_build_query, is_string, is_subclass_of, parse_str, preg_match, rtrim, str_contains, str_ends_with, strcasecmp, strlen, strtr, substr, trigger_error, urldecode;
14

15

16
/**
17
 * Link generator.
18
 */
19
final class LinkGenerator
20
{
21
        /** @internal */
22
        public ?Request $lastRequest = null;
23

24

25
        public function __construct(
1✔
26
                private readonly Router $router,
27
                private readonly UrlScript $refUrl,
28
                private readonly ?IPresenterFactory $presenterFactory = null,
29
        ) {
30
        }
1✔
31

32

33
        /**
34
         * Generates URL to presenter.
35
         * @param  string  $destination  in format "[//] [[[module:]presenter:]action | signal! | this | @alias] [#fragment]"
36
         * @param  mixed[]  $args
37
         * @return ($mode is 'forward'|'test' ? null : string)
38
         * @throws UI\InvalidLinkException
39
         */
40
        public function link(
1✔
41
                string $destination,
42
                array $args = [],
43
                ?UI\Component $component = null,
44
                ?string $mode = null,
45
        ): ?string
46
        {
47
                $parts = self::parseDestination($destination);
1✔
48
                $args = $parts['args'] ?? $args;
1✔
49
                $request = $this->createRequest($component, $parts['path'] . ($parts['signal'] ? '!' : ''), $args, $mode ?? 'link');
1✔
50
                $relative = $mode === 'link' && !$parts['absolute'] && !$component?->getPresenter()->absoluteUrls;
1✔
51
                return $mode === 'forward' || $mode === 'test'
1✔
52
                        ? null
×
53
                        : $this->requestToUrl($request, $relative) . $parts['fragment'];
1✔
54
        }
55

56

57
        /**
58
         * @param  string  $destination  in format "[[[module:]presenter:]action | signal! | this | @alias]"
59
         * @param  mixed[]  $args
60
         * @param  string  $mode  forward|redirect|link
61
         * @throws UI\InvalidLinkException
62
         * @internal
63
         */
64
        public function createRequest(
1✔
65
                ?UI\Component $component,
66
                string $destination,
67
                array $args,
68
                string $mode,
69
        ): Request
70
        {
71
                // note: createRequest supposes that saveState(), run() & tryCall() behaviour is final
72

73
                $this->lastRequest = null;
1✔
74
                $refPresenter = $component?->getPresenter();
1✔
75
                $path = $destination;
1✔
76

77
                if (($component && !$component instanceof UI\Presenter) || str_ends_with($destination, '!')) {
1✔
78
                        if ($component === null) {
1✔
79
                                throw new UI\InvalidLinkException("Signal '$destination' requires component context.");
×
80
                        }
81
                        [$cname, $signal] = Helpers::splitName(rtrim($destination, '!'));
1✔
82
                        if ($cname !== '') {
1✔
83
                                $subcomponent = $component->getComponent(strtr($cname, ':', '-'));
1✔
84
                                if (!$subcomponent instanceof UI\Component) {
1✔
85
                                        throw new UI\InvalidLinkException("Component '$cname' in '{$component->getUniqueId()}' is not " . UI\Component::class . '.');
×
86
                                }
87
                                $component = $subcomponent;
1✔
88
                        }
89

90
                        if ($signal === '') {
1✔
91
                                throw new UI\InvalidLinkException('Signal must be non-empty string.');
1✔
92
                        }
93

94
                        $path = 'this';
1✔
95
                }
96

97
                if ($path[0] === '@') {
1✔
98
                        if (!$this->presenterFactory instanceof PresenterFactory) {
1✔
99
                                throw new \LogicException('Link aliasing requires PresenterFactory service.');
×
100
                        }
101
                        $path = ':' . $this->presenterFactory->getAlias(substr($path, 1));
1✔
102
                }
103

104
                $current = false;
1✔
105
                [$presenter, $action] = Helpers::splitName($path);
1✔
106
                if ($presenter === '') {
1✔
107
                        if (!$refPresenter) {
1✔
108
                                throw new \LogicException("Presenter must be specified in '$destination'.");
1✔
109
                        }
110
                        $action = $path === 'this' ? $refPresenter->getAction() : $action;
1✔
111
                        $presenter = (string) $refPresenter->getName();
1✔
112
                        $presenterClass = $refPresenter::class;
1✔
113

114
                } else {
115
                        if ($presenter[0] === ':') { // absolute
1✔
116
                                $presenter = substr($presenter, 1);
1✔
117
                                if (!$presenter) {
1✔
118
                                        throw new UI\InvalidLinkException("Missing presenter name in '$destination'.");
×
119
                                }
120
                        } elseif ($refPresenter) { // relative
1✔
121
                                [$module, , $sep] = Helpers::splitName((string) $refPresenter->getName());
1✔
122
                                $presenter = $module . $sep . $presenter;
1✔
123
                        }
124

125
                        try {
126
                                $presenterClass = $this->presenterFactory?->getPresenterClass($presenter);
1✔
127
                        } catch (InvalidPresenterException $e) {
×
128
                                throw new UI\InvalidLinkException($e->getMessage(), 0, $e);
×
129
                        }
130
                }
131

132
                // PROCESS SIGNAL ARGUMENTS
133
                if (isset($signal)) { // $component must be StatePersistent
1✔
134
                        assert($component !== null);
135
                        $reflection = new UI\ComponentReflection($component::class);
1✔
136
                        if ($signal === 'this') { // means "no signal"
1✔
137
                                $signal = '';
1✔
138
                                if (array_key_exists(0, $args)) {
1✔
139
                                        throw new UI\InvalidLinkException("Unable to pass parameters to 'this!' signal.");
×
140
                                }
141
                        } elseif (!str_contains($signal, UI\Component::NameSeparator)) {
1✔
142
                                // counterpart of signalReceived() & tryCall()
143

144
                                $method = $reflection->getSignalMethod($signal);
1✔
145
                                if (!$method) {
1✔
146
                                        throw new UI\InvalidLinkException("Unknown signal '$signal', missing handler {$reflection->getName()}::{$component::formatSignalMethod($signal)}()");
×
147
                                }
148

149
                                $this->validateLinkTarget($refPresenter, $method, "signal '$signal'" . ($component === $refPresenter ? '' : ' in ' . $component::class), $mode);
1✔
150

151
                                // convert indexed parameters to named
152
                                UI\ParameterConverter::toParameters($method, $args, [], $missing);
1✔
153
                        }
154

155
                        // counterpart of StatePersistent
156
                        if ($args && array_intersect_key($args, $reflection->getPersistentParams())) {
1✔
157
                                $component->saveState($args);
1✔
158
                        }
159

160
                        if ($args && $component !== $refPresenter) {
1✔
161
                                $prefix = $component->getUniqueId() . UI\Component::NameSeparator;
1✔
162
                                foreach ($args as $key => $val) {
1✔
163
                                        unset($args[$key]);
1✔
164
                                        $args[$prefix . $key] = $val;
1✔
165
                                }
166
                        }
167
                }
168

169
                // PROCESS ARGUMENTS
170
                if ($presenterClass !== null && is_subclass_of($presenterClass, UI\Presenter::class)) {
1✔
171
                        if ($action === '') {
1✔
172
                                $action = UI\Presenter::DefaultAction;
1✔
173
                        }
174

175
                        $current = $refPresenter && ($action === '*' || strcasecmp($action, $refPresenter->getAction()) === 0) && $presenterClass === $refPresenter::class;
1✔
176

177
                        $reflection = new UI\ComponentReflection($presenterClass);
1✔
178
                        $this->validateLinkTarget($refPresenter, $reflection, "presenter '$presenter'", $mode);
1✔
179

180
                        foreach (array_intersect_key($reflection->getParameters(), $args) as $name => $param) {
1✔
181
                                if ($args[$name] === $param['def']) {
1✔
182
                                        $args[$name] = null; // value transmit is unnecessary
1✔
183
                                }
184
                        }
185

186
                        // counterpart of run() & tryCall()
187
                        if ($method = $reflection->getActionRenderMethod($action)) {
1✔
188
                                $this->validateLinkTarget($refPresenter, $method, "action '$presenter:$action'", $mode);
1✔
189

190
                                UI\ParameterConverter::toParameters($method, $args, $path === 'this' ? ($refPresenter?->getParameters() ?? []) : [], $missing);
1✔
191

192
                        } elseif (array_key_exists(0, $args)) {
1✔
193
                                throw new UI\InvalidLinkException("Unable to pass parameters to action '$presenter:$action', missing corresponding method $presenterClass::{$presenterClass::formatRenderMethod($action)}().");
1✔
194
                        }
195

196
                        // counterpart of StatePersistent
197
                        if ($refPresenter) {
1✔
198
                                if (empty($signal) && $args && array_intersect_key($args, $reflection->getPersistentParams())) {
1✔
199
                                        $refPresenter->saveStatePartial($args, $reflection);
1✔
200
                                }
201

202
                                $globalState = $refPresenter->getGlobalState($path === 'this' ? null : $presenterClass);
1✔
203
                                if ($current && $args) {
1✔
204
                                        $tmp = $globalState + $refPresenter->getParameters();
1✔
205
                                        foreach ($args as $key => $val) {
1✔
206
                                                if (http_build_query([$val]) !== (isset($tmp[$key]) ? http_build_query([$tmp[$key]]) : '')) {
1✔
207
                                                        $current = false;
1✔
208
                                                        break;
1✔
209
                                                }
210
                                        }
211
                                }
212

213
                                $args += $globalState;
1✔
214
                        }
215
                }
216

217
                if ($mode !== 'test' && !empty($missing)) {
1✔
218
                        foreach ($missing as $rp) {
1✔
219
                                if (!array_key_exists($rp->getName(), $args)) {
1✔
220
                                        throw new UI\InvalidLinkException("Missing parameter \${$rp->getName()} required by " . Reflection::toString($rp->getDeclaringFunction()));
1✔
221
                                }
222
                        }
223
                }
224

225
                // ADD ACTION & SIGNAL & FLASH
226
                if ($action) {
1✔
227
                        $args[UI\Presenter::ActionKey] = $action;
1✔
228
                }
229

230
                if (!empty($signal)) {
1✔
231
                        assert($component !== null);
232
                        $args[UI\Presenter::SignalKey] = $component->getParameterId($signal);
1✔
233
                        $current = $current && $args[UI\Presenter::SignalKey] === $refPresenter?->getParameter(UI\Presenter::SignalKey);
1✔
234
                }
235

236
                if (($mode === 'redirect' || $mode === 'forward') && $refPresenter?->hasFlashSession()) {
1✔
237
                        $flashKey = $refPresenter->getParameter(UI\Presenter::FlashKey);
×
238
                        $args[UI\Presenter::FlashKey] = is_string($flashKey) && $flashKey !== '' ? $flashKey : null;
×
239
                }
240

241
                return $this->lastRequest = new Request($presenter, Request::FORWARD, $args, flags: ['current' => $current]);
1✔
242
        }
243

244

245
        /**
246
         * Parse destination in format "[//] [[[module:]presenter:]action | signal! | this | @alias] [?query] [#fragment]"
247
         * @throws UI\InvalidLinkException
248
         * @return array{absolute: bool, path: string, signal: bool, args: ?array<string, mixed>, fragment: string}
249
         * @internal
250
         */
251
        public static function parseDestination(string $destination): array
1✔
252
        {
253
                if (!preg_match('~^ (?<absolute>//)?+ (?<path>[^!?#]++) (?<signal>!)?+ (?<query>\?[^#]*)?+ (?<fragment>\#.*)?+ $~x', $destination, $matches)) {
1✔
254
                        throw new UI\InvalidLinkException("Invalid destination '$destination'.");
1✔
255
                }
256

257
                if (!empty($matches['query'])) {
1✔
258
                        parse_str(substr($matches['query'], 1), $parsed);
1✔
259
                        /** @var array<string, mixed> $args */
260
                        $args = $parsed;
1✔
261
                }
262

263
                return [
264
                        'absolute' => (bool) $matches['absolute'],
1✔
265
                        'path' => $matches['path'],
1✔
266
                        'signal' => !empty($matches['signal']),
1✔
267
                        'args' => $args ?? null,
1✔
268
                        'fragment' => $matches['fragment'] ?? '',
1✔
269
                ];
270
        }
271

272

273
        /**
274
         * Converts Request to URL.
275
         */
276
        public function requestToUrl(Request $request, ?bool $relative = false): string
1✔
277
        {
278
                $url = $this->router->constructUrl($request->toArray(), $this->refUrl);
1✔
279
                if ($url === null) {
1✔
280
                        $params = $request->getParameters();
1✔
281
                        unset($params[UI\Presenter::ActionKey], $params[UI\Presenter::PresenterKey]);
1✔
282
                        $params = urldecode(http_build_query($params, '', ', '));
1✔
283
                        throw new UI\InvalidLinkException("No route for {$request->getPresenterName()}:{$request->getParameter('action')}($params)");
1✔
284
                }
285

286
                if ($relative) {
1✔
287
                        $hostUrl = $this->refUrl->getHostUrl() . '/';
1✔
288
                        if (str_starts_with($url, $hostUrl)) {
1✔
289
                                $url = substr($url, strlen($hostUrl) - 1);
1✔
290
                        }
291
                }
292

293
                return $url;
1✔
294
        }
295

296

297
        public function withReferenceUrl(string $url): static
1✔
298
        {
299
                return new self(
1✔
300
                        $this->router,
1✔
301
                        new UrlScript($url),
1✔
302
                        $this->presenterFactory,
1✔
303
                );
304
        }
305

306

307
        /** @param  \ReflectionClass<object>|\ReflectionMethod  $element */
308
        private function validateLinkTarget(
1✔
309
                ?UI\Presenter $presenter,
310
                \ReflectionClass|\ReflectionMethod $element,
311
                string $message,
312
                string $mode,
313
        ): void
314
        {
315
                $message .= $presenter === null
1✔
316
                        ? ''
1✔
317
                        : " from '{$presenter->getName()}:{$presenter->getAction()}'";
1✔
318
                if ($mode !== 'forward' && !(new UI\AccessPolicy($element))->isLinkable()) {
1✔
319
                        throw new UI\InvalidLinkException("Link to forbidden $message.");
1✔
320
                } elseif ($presenter?->invalidLinkMode
1✔
321
                        && (UI\ComponentReflection::parseAnnotation($element, 'deprecated') || $element->getAttributes(Attributes\Deprecated::class))
1✔
322
                ) {
323
                        trigger_error("Link to deprecated $message.", E_USER_DEPRECATED);
1✔
324
                }
325
        }
1✔
326

327

328
        /** @internal */
329
        public static function applyBase(string $link, string $base): string
1✔
330
        {
331
                return str_contains($link, ':') && $link[0] !== ':'
1✔
332
                        ? ":$base:$link"
1✔
333
                        : $link;
1✔
334
        }
335
}
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