Passed
Push — main ( 99c066...305d80 )
by Dimitri
04:43
created

UrlGenerator::setRoutes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 5
ccs 0
cts 2
cp 0
crap 2
rs 10
1
<?php
2
3
/**
4
 * This file is part of Blitz PHP framework.
5
 *
6
 * (c) 2022 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace BlitzPHP\Http;
13
14
use BlitzPHP\Container\Services;
15
use BlitzPHP\Contracts\Router\RouteCollectionInterface;
16
use BlitzPHP\Exceptions\HttpException;
17
use BlitzPHP\Exceptions\RouterException;
18
use BlitzPHP\Session\Store;
19
use BlitzPHP\Traits\Macroable;
20
use BlitzPHP\Utilities\Iterable\Arr;
21
use BlitzPHP\Utilities\String\Text;
22
use Closure;
23
24
/**
25
 * @credit <a href="http://laravel.com">Laravel - \Illuminate\Routing\UrlGenerator</a>
26
 */
27
class UrlGenerator
28
{
29
    use Macroable;
0 ignored issues
show
Bug introduced by
The trait BlitzPHP\Traits\Macroable requires the property $name which is not provided by BlitzPHP\Http\UrlGenerator.
Loading history...
30
31
    /**
32
     * The forced URL root.
33
     */
34
    protected string $forcedRoot = '';
35
36
    /**
37
     * The forced scheme for URLs.
38
     */
39
    protected string $forceScheme = '';
40
41
    /**
42
     * A cached copy of the URL root for the current request.
43
     */
44
    protected ?string $cachedRoot = null;
45
46
    /**
47
     * A cached copy of the URL scheme for the current request.
48
     */
49
    protected ?string $cachedScheme = null;
50
51
    /**
52
     * The root namespace being applied to controller actions.
53
     */
54
    protected string $rootNamespace = '';
55
56
    /**
57
     * The session resolver callable.
58
     *
59
     * @var callable
60
     */
61
    protected $sessionResolver;
62
63
    /**
64
     * The encryption key resolver callable.
65
     *
66
     * @var callable
67
     */
68
    protected $keyResolver;
69
70
    /**
71
     * The callback to use to format hosts.
72
     *
73
     * @var Closure
74
     */
75
    protected $formatHostUsing;
76
77
    /**
78
     * The callback to use to format paths.
79
     *
80
     * @var Closure
81
     */
82
    protected $formatPathUsing;
83
84
    /**
85
     * Create a new URL Generator instance.
86
     *
87
     * @param RouteCollectionInterface $routes    The route collection.
88
     * @param Request                  $request   The request instance.
89
     * @param string|null              $assetRoot The asset root URL.
90
     *
91
     * @return void
92
     */
93
    public function __construct(protected RouteCollectionInterface $routes, protected Request $request, protected ?string $assetRoot = null)
94
    {
95 10
        $this->setRequest($request);
96
    }
97
98
    /**
99
     * Get the full URL for the current request.
100
     */
101
    public function full(): string
102
    {
103 2
        return $this->request->fullUrl();
104
    }
105
106
    /**
107
     * Get the current URL for the request.
108
     */
109
    public function current(): string
110
    {
111
        return $this->to($this->request->getUri()->getPath());
112
    }
113
114
    /**
115
     * Get the URL for the previous request.
116
     *
117
     * @param mixed $fallback
118
     */
119
    public function previous($fallback = false): string
120
    {
121 2
        $referrer = $this->request->getHeaderLine('Referer');
122
123 2
        $url = $referrer ? $this->to($referrer) : $this->getPreviousUrlFromSession();
124
125
        if ($url) {
126 2
            return $url;
127
        }
128
        if ($fallback) {
129
            return $this->to($fallback);
130
        }
131
132
        return $this->to('/');
133
    }
134
135
    /**
136
     * Get the previous URL from the session if possible.
137
     */
138
    protected function getPreviousUrlFromSession(): ?string
139
    {
140
        $session = $this->getSession();
141
142
        return $session ? $session->previousUrl() : null;
0 ignored issues
show
introduced by
$session is of type BlitzPHP\Session\Store, thus it always evaluated to true.
Loading history...
143
    }
144
145
    /**
146
     * Generate an absolute URL to the given path.
147
     */
148
    public function to(string $path, mixed $extra = [], ?bool $secure = null): string
149
    {
150
        // First we will check if the URL is already a valid URL. If it is we will not
151
        // try to generate a new one but will simply return the URL as is, which is
152
        // convenient since developers do not always have to check if it's valid.
153
        if ($this->isValidUrl($path)) {
154 8
            return $path;
155
        }
156
157
        $tail = implode(
158
            '/',
159
            array_map(
160
                'rawurlencode',
161
                (array) $this->formatParameters($extra)
162
            )
163 4
        );
164
165
        // Once we have the scheme we will compile the "tail" by collapsing the values
166
        // into a single string delimited by slashes. This just makes it convenient
167
        // for passing the array of parameters to this URL as a list of segments.
168 4
        $root = $this->formatRoot($this->formatScheme($secure));
169
170 4
        [$path, $query] = $this->extractQueryString($path);
171
172
        return $this->format(
173
            $root,
174
            '/' . trim($path . '/' . $tail, '/')
175 4
        ) . $query;
176
    }
177
178
    /**
179
     * Generate a secure, absolute URL to the given path.
180
     */
181
    public function secure(string $path, array $parameters = []): string
182
    {
183
        return $this->to($path, $parameters, true);
184
    }
185
186
    /**
187
     * Generate the URL to an application asset.
188
     */
189
    public function asset(string $path, ?bool $secure = null): string
190
    {
191
        if ($this->isValidUrl($path)) {
192
            return $path;
193
        }
194
195
        // Once we get the root URL, we will check to see if it contains an index.php
196
        // file in the paths. If it does, we will remove it since it is not needed
197
        // for asset paths, but only for routes to endpoints in the application.
198
        $root = $this->assetRoot ?: $this->formatRoot($this->formatScheme($secure));
199
200
        return $this->removeIndex($root) . '/' . trim($path, '/');
201
    }
202
203
    /**
204
     * Generate the URL to a secure asset.
205
     */
206
    public function secureAsset(string $path): string
207
    {
208
        return $this->asset($path, true);
209
    }
210
211
    /**
212
     * Generate the URL to an asset from a custom root domain such as CDN, etc.
213
     */
214
    public function assetFrom(string $root, string $path, ?bool $secure = null): string
215
    {
216
        // Once we get the root URL, we will check to see if it contains an index.php
217
        // file in the paths. If it does, we will remove it since it is not needed
218
        // for asset paths, but only for routes to endpoints in the application.
219
        $root = $this->formatRoot($this->formatScheme($secure), $root);
220
221
        return $this->removeIndex($root) . '/' . trim($path, '/');
222
    }
223
224
    /**
225
     * Remove the index.php file from a path.
226
     */
227
    protected function removeIndex(string $root): string
228
    {
229
        $i = 'index.php';
230
231
        return Text::contains($root, $i) ? str_replace('/' . $i, '', $root) : $root;
0 ignored issues
show
Bug introduced by
$i of type string is incompatible with the type iterable expected by parameter $needles of BlitzPHP\Utilities\String\Text::contains(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

231
        return Text::contains($root, /** @scrutinizer ignore-type */ $i) ? str_replace('/' . $i, '', $root) : $root;
Loading history...
232
    }
233
234
    /**
235
     * Get the default scheme for a raw URL.
236
     */
237
    public function formatScheme(?bool $secure = null): string
238
    {
239
        if (null !== $secure) {
240 2
            return $secure ? 'https://' : 'http://';
241
        }
242
243
        if (null === $this->cachedScheme) {
244 4
            $this->cachedScheme = $this->forceScheme ?: $this->request->getScheme() . '://';
245
        }
246
247 4
        return $this->cachedScheme;
248
    }
249
250
    /**
251
     * Get the URL to a named route.
252
     *
253
     * @return false|string
254
     */
255
    public function route(string $name, array $parameters = [], bool $absolute = true)
256
    {
257 4
        $route = $this->routes->reverseRoute($name, ...$parameters);
258
259
        if (! $route) {
260 4
            throw HttpException::invalidRedirectRoute($route);
0 ignored issues
show
Bug introduced by
It seems like $route can also be of type false; however, parameter $route of BlitzPHP\Exceptions\Http...:invalidRedirectRoute() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

260
            throw HttpException::invalidRedirectRoute(/** @scrutinizer ignore-type */ $route);
Loading history...
261
        }
262
263 4
        return $absolute ? site_url($route) : $route;
264
    }
265
266
    /**
267
     * Get the URL to a controller action.
268
     *
269
     * @return false|string
270
     */
271
    public function action(array|string $action, array $parameters = [], bool $absolute = true)
272
    {
273
        if (is_array($action)) {
0 ignored issues
show
introduced by
The condition is_array($action) is always true.
Loading history...
274 2
            $action = implode('::', $action);
275
        }
276
277 2
        $route = $this->routes->reverseRoute($action, ...$parameters);
278
279
        if (! $route) {
280 2
            throw RouterException::actionNotDefined($action);
281
        }
282
283 2
        return $absolute ? site_url($route) : $route;
284
    }
285
286
    /**
287
     * Format the array of URL parameters.
288
     */
289
    public function formatParameters(mixed $parameters): array
290
    {
291 4
        return Arr::wrap($parameters);
292
    }
293
294
    /**
295
     * Extract the query string from the given path.
296
     */
297
    protected function extractQueryString(string $path): array
298
    {
299
        if (($queryPosition = strpos($path, '?')) !== false) {
300
            return [
301
                substr($path, 0, $queryPosition),
302
                substr($path, $queryPosition),
303 4
            ];
304
        }
305
306 4
        return [$path, ''];
307
    }
308
309
    /**
310
     * Get the base URL for the request.
311
     */
312
    public function formatRoot(string $scheme, ?string $root = null): string
313
    {
314
        if (null === $root) {
315
            if (null === $this->cachedRoot) {
316 4
                $this->cachedRoot = $this->forcedRoot ?: $this->request->root();
317
            }
318
319 4
            $root = $this->cachedRoot;
320
        }
321
322 4
        $start = Text::startsWith($root, 'http://') ? 'http://' : 'https://';
0 ignored issues
show
Bug introduced by
'http://' of type string is incompatible with the type iterable expected by parameter $needles of BlitzPHP\Utilities\String\Text::startsWith(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

322
        $start = Text::startsWith($root, /** @scrutinizer ignore-type */ 'http://') ? 'http://' : 'https://';
Loading history...
323
324 4
        return preg_replace('~' . $start . '~', $scheme, $root, 1);
325
    }
326
327
    /**
328
     * Format the given URL segments into a single URL.
329
     */
330
    public function format(string $root, string $path, mixed $route = null): string
331
    {
332 4
        $path = '/' . trim($path, '/');
333
334
        if ($this->formatHostUsing) {
335 4
            $root = ($this->formatHostUsing)($root, $route);
336
        }
337
338
        if ($this->formatPathUsing) {
339 4
            $path = ($this->formatPathUsing)($path, $route);
340
        }
341
342 4
        return trim($root . $path, '/');
343
    }
344
345
    /**
346
     * Determine if the given path is a valid URL.
347
     */
348
    public function isValidUrl(string $path): bool
349
    {
350
        if (! preg_match('~^(#|//|https?://|(mailto|tel|sms):)~', $path)) {
351 4
            return filter_var($path, FILTER_VALIDATE_URL) !== false;
352
        }
353
354 8
        return true;
355
    }
356
357
    /**
358
     * Force the scheme for URLs.
359
     */
360
    public function forceScheme(?string $scheme): void
361
    {
362
        $this->cachedScheme = null;
363
364
        $this->forceScheme = $scheme ? $scheme . '://' : null;
365
    }
366
367
    /**
368
     * Set the forced root URL.
369
     */
370
    public function forceRootUrl(?string $root): void
371
    {
372
        $this->forcedRoot = $root ? rtrim($root, '/') : null;
373
374
        $this->cachedRoot = null;
375
    }
376
377
    /**
378
     * Set a callback to be used to format the host of generated URLs.
379
     *
380
     * @return $this
381
     */
382
    public function formatHostUsing(Closure $callback)
383
    {
384
        $this->formatHostUsing = $callback;
385
386
        return $this;
387
    }
388
389
    /**
390
     * Set a callback to be used to format the path of generated URLs.
391
     *
392
     * @return $this
393
     */
394
    public function formatPathUsing(Closure $callback)
395
    {
396
        $this->formatPathUsing = $callback;
397
398
        return $this;
399
    }
400
401
    /**
402
     * Get the path formatter being used by the URL generator.
403
     *
404
     * @return Closure
405
     */
406
    public function pathFormatter()
407
    {
408
        return $this->formatPathUsing ?: static fn ($path) => $path;
409
    }
410
411
    /**
412
     * Get the request instance.
413
     */
414
    public function getRequest(): Request
415
    {
416 10
        return $this->request;
417
    }
418
419
    /**
420
     * Set the current request instance.
421
     */
422
    public function setRequest(Request $request): self
423
    {
424 10
        $this->request = $request;
425
426 10
        $this->cachedRoot   = null;
427 10
        $this->cachedScheme = null;
428
429 10
        return $this;
430
    }
431
432
    /**
433
     * Set the route collection.
434
     */
435
    public function setRoutes(RouteCollectionInterface $routes): self
436
    {
437
        $this->routes = $routes;
438
439
        return $this;
440
    }
441
442
    /**
443
     * Get the session implementation from the resolver.
444
     */
445
    protected function getSession(): ?Store
446
    {
447
        if ($this->sessionResolver) {
448
            return ($this->sessionResolver)();
449
        }
450
451
        return Services::session();
452
    }
453
454
    /**
455
     * Set the session resolver for the generator.
456
     *
457
     * @return $this
458
     */
459
    public function setSessionResolver(callable $sessionResolver)
460
    {
461
        $this->sessionResolver = $sessionResolver;
462
463
        return $this;
464
    }
465
466
    /**
467
     * Set the encryption key resolver.
468
     *
469
     * @return $this
470
     */
471
    public function setKeyResolver(callable $keyResolver)
472
    {
473
        $this->keyResolver = $keyResolver;
474
475
        return $this;
476
    }
477
478
    /**
479
     * Set the root controller namespace.
480
     *
481
     * @param string $rootNamespace
482
     *
483
     * @return $this
484
     */
485
    public function setRootControllerNamespace($rootNamespace)
486
    {
487
        $this->rootNamespace = $rootNamespace;
488
489
        return $this;
490
    }
491
}
492