Passed
Push — main ( cd5116...99c066 )
by Dimitri
12:52
created

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

230
        return Text::contains($root, /** @scrutinizer ignore-type */ $i) ? str_replace('/' . $i, '', $root) : $root;
Loading history...
231
    }
232
233
    /**
234
     * Get the default scheme for a raw URL.
235
     */
236
    public function formatScheme(?bool $secure = null): string
237
    {
238
        if (null !== $secure) {
239 2
            return $secure ? 'https://' : 'http://';
240
        }
241
242
        if (null === $this->cachedScheme) {
243 4
            $this->cachedScheme = $this->forceScheme ?: $this->request->getScheme() . '://';
244
        }
245
246 4
        return $this->cachedScheme;
247
    }
248
249
    /**
250
     * Get the URL to a named route.
251
     *
252
     * @return false|string
253
     */
254
    public function route(string $name, array $parameters = [], bool $absolute = true)
255
    {
256 4
        $route = $this->routes->reverseRoute($name, ...$parameters);
257
258
        if (! $route) {
259 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

259
            throw HttpException::invalidRedirectRoute(/** @scrutinizer ignore-type */ $route);
Loading history...
260
        }
261
262 4
        return $absolute ? site_url($route) : $route;
263
    }
264
265
    /**
266
     * Format the array of URL parameters.
267
     */
268
    public function formatParameters(mixed $parameters): array
269
    {
270 4
        return Arr::wrap($parameters);
271
    }
272
273
    /**
274
     * Extract the query string from the given path.
275
     */
276
    protected function extractQueryString(string $path): array
277
    {
278
        if (($queryPosition = strpos($path, '?')) !== false) {
279
            return [
280
                substr($path, 0, $queryPosition),
281
                substr($path, $queryPosition),
282 4
            ];
283
        }
284
285 4
        return [$path, ''];
286
    }
287
288
    /**
289
     * Get the base URL for the request.
290
     */
291
    public function formatRoot(string $scheme, ?string $root = null): string
292
    {
293
        if (null === $root) {
294
            if (null === $this->cachedRoot) {
295 4
                $this->cachedRoot = $this->forcedRoot ?: $this->request->root();
296
            }
297
298 4
            $root = $this->cachedRoot;
299
        }
300
301 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

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