Passed
Push — main ( d71b68...a743f7 )
by Dimitri
08:12 queued 04:09
created

UrlGenerator::formatRoot()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 6
nc 6
nop 2
dl 0
loc 13
ccs 4
cts 4
cp 1
crap 5
rs 9.6111
c 1
b 0
f 0
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 !== null && $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
        return $this->getSession()?->previousUrl();
141
    }
142
143
    /**
144
     * Generate an absolute URL to the given path.
145
     */
146
    public function to(string $path, mixed $extra = [], ?bool $secure = null): string
147
    {
148
        // First we will check if the URL is already a valid URL. If it is we will not
149
        // try to generate a new one but will simply return the URL as is, which is
150
        // convenient since developers do not always have to check if it's valid.
151
        if ($this->isValidUrl($path)) {
152 8
            return $path;
153
        }
154
155
        $tail = implode(
156
            '/',
157
            array_map(
158
                'rawurlencode',
159
                $this->formatParameters($extra)
160
            )
161 4
        );
162
163
        // Once we have the scheme we will compile the "tail" by collapsing the values
164
        // into a single string delimited by slashes. This just makes it convenient
165
        // for passing the array of parameters to this URL as a list of segments.
166 4
        $root = $this->formatRoot($this->formatScheme($secure));
167
168 4
        [$path, $query] = $this->extractQueryString($path);
169
170
        return $this->format(
171
            $root,
172
            '/' . trim($path . '/' . $tail, '/')
173 4
        ) . $query;
174
    }
175
176
    /**
177
     * Generate a secure, absolute URL to the given path.
178
     */
179
    public function secure(string $path, array $parameters = []): string
180
    {
181
        return $this->to($path, $parameters, true);
182
    }
183
184
    /**
185
     * Generate the URL to an application asset.
186
     */
187
    public function asset(string $path, ?bool $secure = null): string
188
    {
189
        if ($this->isValidUrl($path)) {
190
            return $path;
191
        }
192
193
        // Once we get the root URL, we will check to see if it contains an index.php
194
        // file in the paths. If it does, we will remove it since it is not needed
195
        // for asset paths, but only for routes to endpoints in the application.
196
        $root = $this->assetRoot ?: $this->formatRoot($this->formatScheme($secure));
197
198
        return $this->removeIndex($root) . '/' . trim($path, '/');
199
    }
200
201
    /**
202
     * Generate the URL to a secure asset.
203
     */
204
    public function secureAsset(string $path): string
205
    {
206
        return $this->asset($path, true);
207
    }
208
209
    /**
210
     * Generate the URL to an asset from a custom root domain such as CDN, etc.
211
     */
212
    public function assetFrom(string $root, string $path, ?bool $secure = null): string
213
    {
214
        // Once we get the root URL, we will check to see if it contains an index.php
215
        // file in the paths. If it does, we will remove it since it is not needed
216
        // for asset paths, but only for routes to endpoints in the application.
217
        $root = $this->formatRoot($this->formatScheme($secure), $root);
218
219
        return $this->removeIndex($root) . '/' . trim($path, '/');
220
    }
221
222
    /**
223
     * Remove the index.php file from a path.
224
     */
225
    protected function removeIndex(string $root): string
226
    {
227
        $i = 'index.php';
228
229
        return Text::contains($root, /** @scrutinizer ignore-type */ $i) ? str_replace('/' . $i, '', $root) : $root;
230
    }
231
232
    /**
233
     * Get the default scheme for a raw URL.
234
     */
235
    public function formatScheme(?bool $secure = null): string
236
    {
237
        if (null !== $secure) {
238 2
            return $secure ? 'https://' : 'http://';
239
        }
240
241
        if (null === $this->cachedScheme) {
242 4
            $this->cachedScheme = $this->forceScheme ?: $this->request->getScheme() . '://';
243
        }
244
245 4
        return $this->cachedScheme;
246
    }
247
248
    /**
249
     * Get the URL to a named route.
250
     */
251
    public function route(string $name, array $parameters = [], bool $absolute = true): string
252
    {
253
        if (false === $route = $this->routes->reverseRoute($name, ...$parameters)) {
254 4
            throw HttpException::invalidRedirectRoute($name);
255
        }
256
257 4
        return $absolute ? site_url($route) : $route;
258
    }
259
260
    /**
261
     * Get the URL to a controller action.
262
     *
263
     * @return false|string
264
     */
265
    public function action(array|string $action, array $parameters = [], bool $absolute = true)
266
    {
267
        if (is_array($action)) {
0 ignored issues
show
introduced by
The condition is_array($action) is always true.
Loading history...
268 2
            $action = implode('::', $action);
269
        }
270
271 2
        $route = $this->routes->reverseRoute($action, ...$parameters);
272
273
        if (! $route) {
274 2
            throw RouterException::actionNotDefined($action);
275
        }
276
277 2
        return $absolute ? site_url($route) : $route;
278
    }
279
280
    /**
281
     * Format the array of URL parameters.
282
     */
283
    public function formatParameters(mixed $parameters): array
284
    {
285 4
        return Arr::wrap($parameters);
286
    }
287
288
    /**
289
     * Extract the query string from the given path.
290
     */
291
    protected function extractQueryString(string $path): array
292
    {
293
        if (($queryPosition = strpos($path, '?')) !== false) {
294
            return [
295
                substr($path, 0, $queryPosition),
296
                substr($path, $queryPosition),
297 4
            ];
298
        }
299
300 4
        return [$path, ''];
301
    }
302
303
    /**
304
     * Get the base URL for the request.
305
     */
306
    public function formatRoot(string $scheme, ?string $root = null): string
307
    {
308
        if (null === $root) {
309
            if (null === $this->cachedRoot) {
310 4
                $this->cachedRoot = $this->forcedRoot ?: $this->request->root();
311
            }
312
313 4
            $root = $this->cachedRoot;
314
        }
315
316 4
        $start = Text::startsWith($root, /** @scrutinizer ignore-type */ 'http://') ? 'http://' : 'https://';
317
318 4
        return preg_replace('~' . $start . '~', $scheme, $root, 1);
319
    }
320
321
    /**
322
     * Format the given URL segments into a single URL.
323
     */
324
    public function format(string $root, string $path, mixed $route = null): string
325
    {
326 4
        $path = '/' . trim($path, '/');
327
328
        if ($this->formatHostUsing) {
329 4
            $root = ($this->formatHostUsing)($root, $route);
330
        }
331
332
        if ($this->formatPathUsing) {
333 4
            $path = ($this->formatPathUsing)($path, $route);
334
        }
335
336 4
        return trim($root . $path, '/');
337
    }
338
339
    /**
340
     * Determine if the given path is a valid URL.
341
     */
342
    public function isValidUrl(string $path): bool
343
    {
344
        if (! preg_match('~^(#|//|https?://|(mailto|tel|sms):)~', $path)) {
345 4
            return filter_var($path, FILTER_VALIDATE_URL) !== false;
346
        }
347
348 8
        return true;
349
    }
350
351
    /**
352
     * Force the scheme for URLs.
353
     */
354
    public function forceScheme(?string $scheme): void
355
    {
356
        $this->cachedScheme = null;
357
358
        $this->forceScheme = $scheme ? $scheme . '://' : null;
359
    }
360
361
    /**
362
     * Set the forced root URL.
363
     */
364
    public function forceRootUrl(?string $root): void
365
    {
366
        $this->forcedRoot = $root ? rtrim($root, '/') : null;
367
368
        $this->cachedRoot = null;
369
    }
370
371
    /**
372
     * Set a callback to be used to format the host of generated URLs.
373
     *
374
     * @return $this
375
     */
376
    public function formatHostUsing(Closure $callback)
377
    {
378
        $this->formatHostUsing = $callback;
379
380
        return $this;
381
    }
382
383
    /**
384
     * Set a callback to be used to format the path of generated URLs.
385
     *
386
     * @return $this
387
     */
388
    public function formatPathUsing(Closure $callback)
389
    {
390
        $this->formatPathUsing = $callback;
391
392
        return $this;
393
    }
394
395
    /**
396
     * Get the path formatter being used by the URL generator.
397
     *
398
     * @return Closure
399
     */
400
    public function pathFormatter()
401
    {
402
        return $this->formatPathUsing ?: static fn ($path) => $path;
403
    }
404
405
    /**
406
     * Get the request instance.
407
     */
408
    public function getRequest(): Request
409
    {
410 10
        return $this->request;
411
    }
412
413
    /**
414
     * Set the current request instance.
415
     */
416
    public function setRequest(Request $request): self
417
    {
418 10
        $this->request = $request;
419
420 10
        $this->cachedRoot   = null;
421 10
        $this->cachedScheme = null;
422
423 10
        return $this;
424
    }
425
426
    /**
427
     * Set the route collection.
428
     */
429
    public function setRoutes(RouteCollectionInterface $routes): self
430
    {
431
        $this->routes = $routes;
432
433
        return $this;
434
    }
435
436
    /**
437
     * Get the session implementation from the resolver.
438
     */
439
    protected function getSession(): ?Store
440
    {
441
        if ($this->sessionResolver) {
442
            return ($this->sessionResolver)();
443
        }
444
445
        return Services::session();
446
    }
447
448
    /**
449
     * Set the session resolver for the generator.
450
     *
451
     * @return $this
452
     */
453
    public function setSessionResolver(callable $sessionResolver)
454
    {
455
        $this->sessionResolver = $sessionResolver;
456
457
        return $this;
458
    }
459
460
    /**
461
     * Set the encryption key resolver.
462
     *
463
     * @return $this
464
     */
465
    public function setKeyResolver(callable $keyResolver)
466
    {
467
        $this->keyResolver = $keyResolver;
468
469
        return $this;
470
    }
471
472
    /**
473
     * Set the root controller namespace.
474
     *
475
     * @param string $rootNamespace
476
     *
477
     * @return $this
478
     */
479
    public function setRootControllerNamespace($rootNamespace)
480
    {
481
        $this->rootNamespace = $rootNamespace;
482
483
        return $this;
484
    }
485
}
486