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

nette / application / 19799648203

30 Nov 2025 01:31PM UTC coverage: 81.757% (+0.2%) from 81.549%
19799648203

push

github

dg
mockery dev [WIP]

1945 of 2379 relevant lines covered (81.76%)

0.82 hits per line

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

93.48
/src/Application/LinkGenerator.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\Application;
11

12
use Nette\Http\UrlScript;
13
use Nette\Routing\Router;
14
use Nette\Utils\Reflection;
15
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, strncmp, strtr, substr, trigger_error, urldecode;
16

17

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

26

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

34

35
        /**
36
         * Generates URL to presenter.
37
         * @param  string  $destination  in format "[//] [[[module:]presenter:]action | signal! | this | @alias] [#fragment]"
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  string  $mode  forward|redirect|link
60
         * @throws UI\InvalidLinkException
61
         * @internal
62
         */
63
        public function createRequest(
1✔
64
                ?UI\Component $component,
65
                string $destination,
66
                array $args,
67
                string $mode,
68
        ): Request
69
        {
70
                // note: createRequest supposes that saveState(), run() & tryCall() behaviour is final
71

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

76
                if (($component && !$component instanceof UI\Presenter) || str_ends_with($destination, '!')) {
1✔
77
                        [$cname, $signal] = Helpers::splitName(rtrim($destination, '!'));
1✔
78
                        if ($cname !== '') {
1✔
79
                                $component = $component->getComponent(strtr($cname, ':', '-'));
1✔
80
                        }
81

82
                        if ($signal === '') {
1✔
83
                                throw new UI\InvalidLinkException('Signal must be non-empty string.');
1✔
84
                        }
85

86
                        $path = 'this';
1✔
87
                }
88

89
                if ($path[0] === '@') {
1✔
90
                        if (!$this->presenterFactory instanceof PresenterFactory) {
1✔
91
                                throw new \LogicException('Link aliasing requires PresenterFactory service.');
×
92
                        }
93
                        $path = ':' . $this->presenterFactory->getAlias(substr($path, 1));
1✔
94
                }
95

96
                $current = false;
1✔
97
                [$presenter, $action] = Helpers::splitName($path);
1✔
98
                if ($presenter === '') {
1✔
99
                        if (!$refPresenter) {
1✔
100
                                throw new \LogicException("Presenter must be specified in '$destination'.");
1✔
101
                        }
102
                        $action = $path === 'this' ? $refPresenter->getAction() : $action;
1✔
103
                        $presenter = $refPresenter->getName();
1✔
104
                        $presenterClass = $refPresenter::class;
1✔
105

106
                } else {
107
                        if ($presenter[0] === ':') { // absolute
1✔
108
                                $presenter = substr($presenter, 1);
1✔
109
                                if (!$presenter) {
1✔
110
                                        throw new UI\InvalidLinkException("Missing presenter name in '$destination'.");
×
111
                                }
112
                        } elseif ($refPresenter) { // relative
1✔
113
                                [$module, , $sep] = Helpers::splitName($refPresenter->getName());
1✔
114
                                $presenter = $module . $sep . $presenter;
1✔
115
                        }
116

117
                        try {
118
                                $presenterClass = $this->presenterFactory?->getPresenterClass($presenter);
1✔
119
                        } catch (InvalidPresenterException $e) {
×
120
                                throw new UI\InvalidLinkException($e->getMessage(), 0, $e);
×
121
                        }
122
                }
123

124
                // PROCESS SIGNAL ARGUMENTS
125
                if (isset($signal)) { // $component must be StatePersistent
1✔
126
                        $reflection = new UI\ComponentReflection($component::class);
1✔
127
                        if ($signal === 'this') { // means "no signal"
1✔
128
                                $signal = '';
1✔
129
                                if (array_key_exists(0, $args)) {
1✔
130
                                        throw new UI\InvalidLinkException("Unable to pass parameters to 'this!' signal.");
×
131
                                }
132
                        } elseif (!str_contains($signal, UI\Component::NameSeparator)) {
1✔
133
                                // counterpart of signalReceived() & tryCall()
134

135
                                $method = $reflection->getSignalMethod($signal);
1✔
136
                                if (!$method) {
1✔
137
                                        throw new UI\InvalidLinkException("Unknown signal '$signal', missing handler {$reflection->getName()}::{$component::formatSignalMethod($signal)}()");
×
138
                                }
139

140
                                $this->checkAllowed($refPresenter, $method, "signal '$signal'" . ($component === $refPresenter ? '' : ' in ' . $component::class), $mode);
1✔
141

142
                                // convert indexed parameters to named
143
                                UI\ParameterConverter::toParameters($method, $args, [], $missing);
1✔
144
                        }
145

146
                        // counterpart of StatePersistent
147
                        if ($args && array_intersect_key($args, $reflection->getPersistentParams())) {
1✔
148
                                $component->saveState($args);
1✔
149
                        }
150

151
                        if ($args && $component !== $refPresenter) {
1✔
152
                                $prefix = $component->getUniqueId() . UI\Component::NameSeparator;
1✔
153
                                foreach ($args as $key => $val) {
1✔
154
                                        unset($args[$key]);
1✔
155
                                        $args[$prefix . $key] = $val;
1✔
156
                                }
157
                        }
158
                }
159

160
                // PROCESS ARGUMENTS
161
                if (is_subclass_of($presenterClass, UI\Presenter::class)) {
1✔
162
                        if ($action === '') {
1✔
163
                                $action = UI\Presenter::DefaultAction;
1✔
164
                        }
165

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

168
                        $reflection = new UI\ComponentReflection($presenterClass);
1✔
169
                        $this->checkAllowed($refPresenter, $reflection, "presenter '$presenter'", $mode);
1✔
170

171
                        foreach (array_intersect_key($reflection->getParameters(), $args) as $name => $param) {
1✔
172
                                if ($args[$name] === $param['def']) {
1✔
173
                                        $args[$name] = null; // value transmit is unnecessary
1✔
174
                                }
175
                        }
176

177
                        // counterpart of run() & tryCall()
178
                        if ($method = $reflection->getActionRenderMethod($action)) {
1✔
179
                                $this->checkAllowed($refPresenter, $method, "action '$presenter:$action'", $mode);
1✔
180

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

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

187
                        // counterpart of StatePersistent
188
                        if ($refPresenter) {
1✔
189
                                if (empty($signal) && $args && array_intersect_key($args, $reflection->getPersistentParams())) {
1✔
190
                                        $refPresenter->saveStatePartial($args, $reflection);
1✔
191
                                }
192

193
                                $globalState = $refPresenter->getGlobalState($path === 'this' ? null : $presenterClass);
1✔
194
                                if ($current && $args) {
1✔
195
                                        $tmp = $globalState + $refPresenter->getParameters();
1✔
196
                                        foreach ($args as $key => $val) {
1✔
197
                                                if (http_build_query([$val]) !== (isset($tmp[$key]) ? http_build_query([$tmp[$key]]) : '')) {
1✔
198
                                                        $current = false;
1✔
199
                                                        break;
1✔
200
                                                }
201
                                        }
202
                                }
203

204
                                $args += $globalState;
1✔
205
                        }
206
                }
207

208
                if ($mode !== 'test' && !empty($missing)) {
1✔
209
                        foreach ($missing as $rp) {
1✔
210
                                if (!array_key_exists($rp->getName(), $args)) {
1✔
211
                                        throw new UI\InvalidLinkException("Missing parameter \${$rp->getName()} required by " . Reflection::toString($rp->getDeclaringFunction()));
1✔
212
                                }
213
                        }
214
                }
215

216
                // ADD ACTION & SIGNAL & FLASH
217
                if ($action) {
1✔
218
                        $args[UI\Presenter::ActionKey] = $action;
1✔
219
                }
220

221
                if (!empty($signal)) {
1✔
222
                        $args[UI\Presenter::SignalKey] = $component->getParameterId($signal);
1✔
223
                        $current = $current && $args[UI\Presenter::SignalKey] === $refPresenter->getParameter(UI\Presenter::SignalKey);
1✔
224
                }
225

226
                if (($mode === 'redirect' || $mode === 'forward') && $refPresenter->hasFlashSession()) {
1✔
227
                        $flashKey = $refPresenter->getParameter(UI\Presenter::FlashKey);
×
228
                        $args[UI\Presenter::FlashKey] = is_string($flashKey) && $flashKey !== '' ? $flashKey : null;
×
229
                }
230

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

234

235
        /**
236
         * Parse destination in format "[//] [[[module:]presenter:]action | signal! | this | @alias] [?query] [#fragment]"
237
         * @throws UI\InvalidLinkException
238
         * @return array{absolute: bool, path: string, signal: bool, args: ?array, fragment: string}
239
         * @internal
240
         */
241
        public static function parseDestination(string $destination): array
1✔
242
        {
243
                if (!preg_match('~^ (?<absolute>//)?+ (?<path>[^!?#]++) (?<signal>!)?+ (?<query>\?[^#]*)?+ (?<fragment>\#.*)?+ $~x', $destination, $matches)) {
1✔
244
                        throw new UI\InvalidLinkException("Invalid destination '$destination'.");
1✔
245
                }
246

247
                if (!empty($matches['query'])) {
1✔
248
                        parse_str(substr($matches['query'], 1), $args);
1✔
249
                }
250

251
                return [
252
                        'absolute' => (bool) $matches['absolute'],
1✔
253
                        'path' => $matches['path'],
1✔
254
                        'signal' => !empty($matches['signal']),
1✔
255
                        'args' => $args ?? null,
1✔
256
                        'fragment' => $matches['fragment'] ?? '',
1✔
257
                ];
258
        }
259

260

261
        /**
262
         * Converts Request to URL.
263
         */
264
        public function requestToUrl(Request $request, ?bool $relative = false): string
1✔
265
        {
266
                $url = $this->router->constructUrl($request->toArray(), $this->refUrl);
1✔
267
                if ($url === null) {
1✔
268
                        $params = $request->getParameters();
1✔
269
                        unset($params[UI\Presenter::ActionKey], $params[UI\Presenter::PresenterKey]);
1✔
270
                        $params = urldecode(http_build_query($params, '', ', '));
1✔
271
                        throw new UI\InvalidLinkException("No route for {$request->getPresenterName()}:{$request->getParameter('action')}($params)");
1✔
272
                }
273

274
                if ($relative) {
1✔
275
                        $hostUrl = $this->refUrl->getHostUrl() . '/';
1✔
276
                        if (strncmp($url, $hostUrl, strlen($hostUrl)) === 0) {
1✔
277
                                $url = substr($url, strlen($hostUrl) - 1);
1✔
278
                        }
279
                }
280

281
                return $url;
1✔
282
        }
283

284

285
        public function withReferenceUrl(string $url): static
1✔
286
        {
287
                return new self(
1✔
288
                        $this->router,
1✔
289
                        new UrlScript($url),
1✔
290
                        $this->presenterFactory,
1✔
291
                );
292
        }
293

294

295
        private function checkAllowed(
1✔
296
                ?UI\Presenter $presenter,
297
                \ReflectionClass|\ReflectionMethod $element,
298
                string $message,
299
                string $mode,
300
        ): void
301
        {
302
                if ($mode !== 'forward' && !(new UI\AccessPolicy($element))->canGenerateLink()) {
1✔
303
                        throw new UI\InvalidLinkException("Link to forbidden $message from '{$presenter->getName()}:{$presenter->getAction()}'.");
1✔
304
                } elseif ($presenter?->invalidLinkMode
1✔
305
                        && (UI\ComponentReflection::parseAnnotation($element, 'deprecated') || $element->getAttributes(Attributes\Deprecated::class))
1✔
306
                ) {
307
                        trigger_error("Link to deprecated $message from '{$presenter->getName()}:{$presenter->getAction()}'.", E_USER_DEPRECATED);
1✔
308
                }
309
        }
1✔
310

311

312
        /** @internal */
313
        public static function applyBase(string $link, string $base): string
1✔
314
        {
315
                return str_contains($link, ':') && $link[0] !== ':'
1✔
316
                        ? ":$base:$link"
1✔
317
                        : $link;
1✔
318
        }
319
}
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