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

nette / application / 28120116131

24 Jun 2026 05:59PM UTC coverage: 84.766% (+0.2%) from 84.605%
28120116131

push

github

dg
added CLAUDE.md

1992 of 2350 relevant lines covered (84.77%)

0.85 hits per line

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

92.95
/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
                $hash = $args['#'] ?? null;
1✔
50
                unset($args['#']);
1✔
51
                if ($hash !== null && !is_scalar($hash)) {
1✔
52
                        throw new UI\InvalidLinkException("Value of '#' must be scalar, " . get_debug_type($hash) . ' given.');
1✔
53
                }
54
                $fragment = (string) $hash !== ''
1✔
55
                        ? '#' . rawurlencode((string) $hash)
1✔
56
                        : $parts['fragment'];
1✔
57
                $request = $this->createRequest($component, $parts['path'] . ($parts['signal'] ? '!' : ''), $args, $mode ?? 'link');
1✔
58
                $relative = $mode === 'link' && !$parts['absolute'] && !$component?->getPresenter()->absoluteUrls;
1✔
59
                return $mode === 'forward' || $mode === 'test'
1✔
60
                        ? null
×
61
                        : $this->requestToUrl($request, $relative) . $fragment;
1✔
62
        }
63

64

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

81
                $this->lastRequest = null;
1✔
82
                $refPresenter = $component?->getPresenter();
1✔
83
                $path = $destination;
1✔
84

85
                if (($component && !$component instanceof UI\Presenter) || str_ends_with($destination, '!')) {
1✔
86
                        if ($component === null) {
1✔
87
                                throw new UI\InvalidLinkException("Signal '$destination' requires component context.");
×
88
                        }
89
                        [$cname, $signal] = Helpers::splitName(rtrim($destination, '!'));
1✔
90
                        if ($cname !== '') {
1✔
91
                                $subcomponent = $component->getComponent(strtr($cname, ':', '-'));
1✔
92
                                if (!$subcomponent instanceof UI\Component) {
1✔
93
                                        throw new UI\InvalidLinkException("Component '$cname' in '{$component->getUniqueId()}' is not " . UI\Component::class . '.');
×
94
                                }
95
                                $component = $subcomponent;
1✔
96
                        }
97

98
                        if ($signal === '') {
1✔
99
                                throw new UI\InvalidLinkException('Signal must be non-empty string.');
1✔
100
                        }
101

102
                        $path = 'this';
1✔
103
                }
104

105
                if ($path[0] === '@') {
1✔
106
                        if (!$this->presenterFactory instanceof PresenterFactory) {
1✔
107
                                throw new \LogicException('Link aliasing requires PresenterFactory service.');
×
108
                        }
109
                        $path = ':' . $this->presenterFactory->getAlias(substr($path, 1));
1✔
110
                }
111

112
                $current = false;
1✔
113
                [$presenter, $action] = Helpers::splitName($path);
1✔
114
                if ($presenter === '') {
1✔
115
                        if (!$refPresenter) {
1✔
116
                                throw new \LogicException("Presenter must be specified in '$destination'.");
1✔
117
                        }
118
                        $action = $path === 'this' ? $refPresenter->getAction() : $action;
1✔
119
                        $presenter = (string) $refPresenter->getName();
1✔
120
                        $presenterClass = $refPresenter::class;
1✔
121

122
                } else {
123
                        if ($presenter[0] === ':') { // absolute
1✔
124
                                $presenter = substr($presenter, 1);
1✔
125
                                if (!$presenter) {
1✔
126
                                        throw new UI\InvalidLinkException("Missing presenter name in '$destination'.");
×
127
                                }
128
                        } elseif ($refPresenter) { // relative
1✔
129
                                [$module, , $sep] = Helpers::splitName((string) $refPresenter->getName());
1✔
130
                                $presenter = $module . $sep . $presenter;
1✔
131
                        }
132

133
                        try {
134
                                $presenterClass = $this->presenterFactory?->getPresenterClass($presenter);
1✔
135
                        } catch (InvalidPresenterException $e) {
×
136
                                throw new UI\InvalidLinkException($e->getMessage(), 0, $e);
×
137
                        }
138
                }
139

140
                // PROCESS SIGNAL ARGUMENTS
141
                if (isset($signal)) { // $component must be StatePersistent
1✔
142
                        assert($component !== null);
143
                        $reflection = new UI\ComponentReflection($component::class);
1✔
144
                        if ($signal === 'this') { // means "no signal"
1✔
145
                                $signal = '';
1✔
146
                                if (array_key_exists(0, $args)) {
1✔
147
                                        throw new UI\InvalidLinkException("Unable to pass parameters to 'this!' signal.");
×
148
                                }
149
                        } elseif (!str_contains($signal, UI\Component::NameSeparator)) {
1✔
150
                                // counterpart of signalReceived() & tryCall()
151

152
                                $method = $reflection->getSignalMethod($signal);
1✔
153
                                if (!$method) {
1✔
154
                                        throw new UI\InvalidLinkException("Unknown signal '$signal', missing handler {$reflection->getName()}::{$component::formatSignalMethod($signal)}()");
×
155
                                }
156

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

159
                                // convert indexed parameters to named
160
                                UI\ParameterConverter::toParameters($method, $args, [], $missing);
1✔
161
                        }
162

163
                        // counterpart of StatePersistent
164
                        if ($args && array_intersect_key($args, $reflection->getPersistentParams())) {
1✔
165
                                $component->saveState($args);
1✔
166
                        }
167

168
                        if ($args && $component !== $refPresenter) {
1✔
169
                                $prefix = $component->getUniqueId() . UI\Component::NameSeparator;
1✔
170
                                foreach ($args as $key => $val) {
1✔
171
                                        unset($args[$key]);
1✔
172
                                        $args[$prefix . $key] = $val;
1✔
173
                                }
174
                        }
175
                }
176

177
                // PROCESS ARGUMENTS
178
                if ($presenterClass !== null && is_subclass_of($presenterClass, UI\Presenter::class)) {
1✔
179
                        if ($action === '') {
1✔
180
                                $action = UI\Presenter::DefaultAction;
1✔
181
                        }
182

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

185
                        $reflection = new UI\ComponentReflection($presenterClass);
1✔
186
                        $this->validateLinkTarget($refPresenter, $reflection, "presenter '$presenter'", $mode);
1✔
187

188
                        foreach (array_intersect_key($reflection->getParameters(), $args) as $name => $param) {
1✔
189
                                if ($args[$name] === $param['def']) {
1✔
190
                                        $args[$name] = null; // value transmit is unnecessary
1✔
191
                                }
192
                        }
193

194
                        // counterpart of run() & tryCall()
195
                        if ($method = $reflection->getActionRenderMethod($action)) {
1✔
196
                                $this->validateLinkTarget($refPresenter, $method, "action '$presenter:$action'", $mode);
1✔
197

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

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

204
                        // counterpart of StatePersistent
205
                        if ($refPresenter) {
1✔
206
                                if (empty($signal) && $args && array_intersect_key($args, $reflection->getPersistentParams())) {
1✔
207
                                        $refPresenter->saveStatePartial($args, $reflection);
1✔
208
                                }
209

210
                                $globalState = $refPresenter->getGlobalState($path === 'this' ? null : $presenterClass);
1✔
211
                                if ($current && $args) {
1✔
212
                                        $tmp = $globalState + $refPresenter->getParameters();
1✔
213
                                        foreach ($args as $key => $val) {
1✔
214
                                                if (http_build_query([$val]) !== (isset($tmp[$key]) ? http_build_query([$tmp[$key]]) : '')) {
1✔
215
                                                        $current = false;
1✔
216
                                                        break;
1✔
217
                                                }
218
                                        }
219
                                }
220

221
                                $args += $globalState;
1✔
222
                        }
223
                }
224

225
                if ($mode !== 'test' && !empty($missing)) {
1✔
226
                        foreach ($missing as $rp) {
1✔
227
                                if (!array_key_exists($rp->getName(), $args)) {
1✔
228
                                        throw new UI\InvalidLinkException("Missing parameter \${$rp->getName()} required by " . Reflection::toString($rp->getDeclaringFunction()));
1✔
229
                                }
230
                        }
231
                }
232

233
                // ADD ACTION & SIGNAL & FLASH
234
                if ($action) {
1✔
235
                        $args[UI\Presenter::ActionKey] = $action;
1✔
236
                }
237

238
                if (!empty($signal)) {
1✔
239
                        assert($component !== null);
240
                        $args[UI\Presenter::SignalKey] = $component->getParameterId($signal);
1✔
241
                        $current = $current && $args[UI\Presenter::SignalKey] === $refPresenter?->getParameter(UI\Presenter::SignalKey);
1✔
242
                }
243

244
                if (($mode === 'redirect' || $mode === 'forward') && $refPresenter?->hasFlashSession()) {
1✔
245
                        $flashKey = $refPresenter->getParameter(UI\Presenter::FlashKey);
×
246
                        $args[UI\Presenter::FlashKey] = is_string($flashKey) && $flashKey !== '' ? $flashKey : null;
×
247
                }
248

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

252

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

265
                if (!empty($matches['query'])) {
1✔
266
                        trigger_error("Link format is obsolete, use arguments instead of query string in '$destination'.", E_USER_DEPRECATED);
1✔
267
                        parse_str(substr($matches['query'], 1), $parsed);
1✔
268
                        /** @var array<string, mixed> $args */
269
                        $args = $parsed;
1✔
270
                }
271

272
                return [
273
                        'absolute' => (bool) $matches['absolute'],
1✔
274
                        'path' => $matches['path'],
1✔
275
                        'signal' => !empty($matches['signal']),
1✔
276
                        'args' => $args ?? null,
1✔
277
                        'fragment' => $matches['fragment'] ?? '',
1✔
278
                ];
279
        }
280

281

282
        /**
283
         * Converts Request to URL.
284
         */
285
        public function requestToUrl(Request $request, ?bool $relative = false): string
1✔
286
        {
287
                $url = $this->router->constructUrl($request->toArray(), $this->refUrl);
1✔
288
                if ($url === null) {
1✔
289
                        $params = $request->getParameters();
1✔
290
                        unset($params[UI\Presenter::ActionKey], $params[UI\Presenter::PresenterKey]);
1✔
291
                        $params = urldecode(http_build_query($params, '', ', '));
1✔
292
                        throw new UI\InvalidLinkException("No route for {$request->getPresenterName()}:{$request->getParameter('action')}($params)");
1✔
293
                }
294

295
                if ($relative) {
1✔
296
                        $hostUrl = $this->refUrl->getHostUrl() . '/';
1✔
297
                        if (str_starts_with($url, $hostUrl)) {
1✔
298
                                $url = substr($url, strlen($hostUrl) - 1);
1✔
299
                        }
300
                }
301

302
                return $url;
1✔
303
        }
304

305

306
        public function withReferenceUrl(string $url): static
1✔
307
        {
308
                return new self(
1✔
309
                        $this->router,
1✔
310
                        new UrlScript($url),
1✔
311
                        $this->presenterFactory,
1✔
312
                );
313
        }
314

315

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

336

337
        /** @internal */
338
        public static function applyBase(string $link, string $base): string
1✔
339
        {
340
                return str_contains($link, ':') && $link[0] !== ':'
1✔
341
                        ? ":$base:$link"
1✔
342
                        : $link;
1✔
343
        }
344
}
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