├── bmc_qr.png
├── .phplint.yml
├── .gitignore
├── tests
├── Unit
│ ├── Enums.php
│ ├── RoutingRedirectorTest.php
│ └── RoutingUrlGeneratorTest.php
└── bootstrap.php
├── phpunit.xml
├── public
└── .htaccess
├── LICENSE.md
├── .github
└── workflows
│ └── php.yml
├── composer.json
├── CHANGELOG.md
├── src
├── RoutingServiceProvider.php
└── UrlGenerator.php
└── README.md
/bmc_qr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fsasvari/laravel-trailing-slash/HEAD/bmc_qr.png
--------------------------------------------------------------------------------
/.phplint.yml:
--------------------------------------------------------------------------------
1 | path: ./
2 | jobs: 10
3 | cache: phplint.cache
4 | extensions:
5 | - php
6 | exclude:
7 | - vendor
8 | warning: true
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | /.project
3 | /nbproject
4 | /.idea
5 | /.phpunit.cache
6 | .phpunit.result.cache
7 | .editorconfig
8 | composer.phar
9 | composer.lock
10 | phpstan.neon
11 | phplint.cache
12 | Thumbs.db
13 |
--------------------------------------------------------------------------------
/tests/Unit/Enums.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ./src
6 |
7 |
8 |
9 |
10 | ./tests
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/public/.htaccess:
--------------------------------------------------------------------------------
1 |
2 |
3 | Options -MultiViews
4 |
5 |
6 | RewriteEngine On
7 |
8 | # Redirect To Trailing Slashes If Not A Folder Or A File...
9 | RewriteCond %{REQUEST_FILENAME} !-d
10 | RewriteCond %{REQUEST_FILENAME} !-f
11 | RewriteCond %{REQUEST_URI} !(/$|\.)
12 | RewriteRule (.*) %{REQUEST_URI}/ [R=301,L]
13 |
14 | # Handle Front Controller...
15 | RewriteCond %{REQUEST_FILENAME} !-d
16 | RewriteCond %{REQUEST_FILENAME} !-f
17 | RewriteRule ^ index.php [L]
18 |
19 | # Handle Authorization Header
20 | RewriteCond %{HTTP:Authorization} .
21 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
22 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Frano Sasvari
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | registerUrlGenerator();
14 | }
15 |
16 | protected function registerUrlGenerator(): void
17 | {
18 | $this->app->singleton('url', function ($app) {
19 | $routes = $app['router']->getRoutes();
20 |
21 | // The URL generator needs the route collection that exists on the router.
22 | // Keep in mind this is an object, so we're passing by references here
23 | // and all the registered routes will be available to the generator.
24 | $app->instance('routes', $routes);
25 |
26 | $url = new UrlGenerator(
27 | $routes,
28 | $app->rebinding(
29 | 'request',
30 | $this->requestRebinder()
31 | ),
32 | $app['config']['app.asset_url']
33 | );
34 |
35 | // Next we will set a few service resolvers on the URL generator, so it can
36 | // get the information it needs to function. This just provides some of
37 | // the convenience features to this URL generator like "signed" URLs.
38 | $url->setSessionResolver(function () use ($app) {
39 | return $app['session'] ?? null;
40 | });
41 |
42 | $url->setKeyResolver(function () use ($app) {
43 | return $app->make('config')->get('app.key');
44 | });
45 |
46 | // If the route collection is "rebound", for example, when the routes stay
47 | // cached for the application, we will need to rebind the routes on the
48 | // URL generator instance so it has the latest version of the routes.
49 | $app->rebinding('routes', function ($app, $routes) {
50 | $app['url']->setRoutes($routes);
51 | });
52 |
53 | return $url;
54 | });
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/UrlGenerator.php:
--------------------------------------------------------------------------------
1 | getTrailingSlash($path);
25 | }
26 |
27 | /**
28 | * Determine if the signature from the given request matches the URL.
29 | *
30 | * @param \Illuminate\Http\Request $request
31 | * @param bool $absolute
32 | * @param array $ignoreQuery
33 | *
34 | * @return bool
35 | */
36 | public function hasCorrectSignature(Request $request, $absolute = true, Closure|array $ignoreQuery = [])
37 | {
38 | $ignoreQuery[] = 'signature';
39 |
40 | $url = ($absolute ? $request->url() : '/'.$request->path());
41 | $url = $url.$this->getTrailingSlash($url);
42 |
43 | $queryString = (new Collection(explode('&', (string) $request->server->get('QUERY_STRING'))))
44 | ->reject(fn ($parameter) => in_array(Str::before($parameter, '='), $ignoreQuery))
45 | ->join('&');
46 |
47 | $original = rtrim($url.'?'.$queryString, '?');
48 |
49 | $keys = call_user_func($this->keyResolver);
50 |
51 | $keys = is_array($keys) ? $keys : [$keys];
52 |
53 | foreach ($keys as $key) {
54 | if (hash_equals(
55 | hash_hmac('sha256', $original, $key),
56 | (string) $request->query('signature', '')
57 | )) {
58 | return true;
59 | }
60 | }
61 |
62 | return false;
63 | }
64 |
65 | /**
66 | * Get the previous path info for the request.
67 | *
68 | * @param mixed $fallback
69 | *
70 | * @return string
71 | */
72 | public function previousPath($fallback = false)
73 | {
74 | $rootToPath = rtrim($this->to('/'), '/');
75 | $previousPath = rtrim(preg_replace('/\?.*/', '', $this->previous($fallback)), '/');
76 |
77 | $previousPath = str_replace($rootToPath, '', $previousPath.$this->getTrailingSlash($previousPath));
78 |
79 | return $previousPath === '' ? '/' : $previousPath;
80 | }
81 |
82 | /**
83 | * Get trailing slash suffix for path or url, if no dash (#) is present.
84 | *
85 | * @param string|null $url
86 | *
87 | * @return string
88 | */
89 | private function getTrailingSlash(?string $url = null): string
90 | {
91 | if ($url === null) {
92 | return '/';
93 | }
94 |
95 | return Str::contains($url, '#') ? '' : '/';
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Laravel Trailing Slash
2 |
3 | Adds url formatting and redirection with trailing slash to Laravel framework versions 12.x, 11.x, 10.x, 9.x, 8.x, 7.x, 6.x and 5.x.
4 |
5 | [](https://styleci.io/repos/79834672)
6 | [](https://packagist.org/packages/fsasvari/laravel-trailing-slash)
7 | [](https://packagist.org/packages/fsasvari/laravel-trailing-slash)
8 | [](https://packagist.org/packages/fsasvari/laravel-trailing-slash)
9 | [](https://packagist.org/packages/fsasvari/laravel-trailing-slash)
10 |
11 | ## Compatibility Chart
12 |
13 | | Laravel Trailing Slash | Laravel | PHP |
14 | |-----------------------------------------------------------------------|---------|-----------|
15 | | [7.x](https://github.com/fsasvari/laravel-trailing-slash/tree/v7.0.0) | 12.x | 8.2+ |
16 | | [6.x](https://github.com/fsasvari/laravel-trailing-slash/tree/v6.1.1) | 11.x | 8.2+ |
17 | | [5.x](https://github.com/fsasvari/laravel-trailing-slash/tree/v5.0.0) | 10.x | 8.1+ |
18 | | [4.x](https://github.com/fsasvari/laravel-trailing-slash/tree/v4.0.0) | 9.x | 8.0.2+ |
19 | | [3.x](https://github.com/fsasvari/laravel-trailing-slash/tree/v3.0.0) | 8.x | 7.3+/8.0+ |
20 | | [2.x](https://github.com/fsasvari/laravel-trailing-slash/tree/v2.0.2) | 7.x | 7.3+ |
21 | | [1.x](https://github.com/fsasvari/laravel-trailing-slash/tree/v1.1.0) | 6.x | 7.2+ |
22 | | [0.3.x](https://github.com/fsasvari/laravel-trailing-slash/tree/0.3) | 5.7-5.8 | 7.1.3+ |
23 | | [0.2.x](https://github.com/fsasvari/laravel-trailing-slash/tree/0.2) | 5.6 | 7.1.3+ |
24 | | [0.1.x](https://github.com/fsasvari/laravel-trailing-slash/tree/0.1) | 5.5 | 7.0.0+ |
25 |
26 | ## Installation
27 |
28 | ### Step 1: Install package
29 |
30 | To get started with Laravel Trailing Slash, use Composer command to add the package to your composer.json project's dependencies:
31 |
32 | ```
33 | composer require fsasvari/laravel-trailing-slash
34 | ```
35 |
36 | Or add it directly by copying next line into composer.json:
37 |
38 | ```
39 | "fsasvari/laravel-trailing-slash": "7.*"
40 | ```
41 |
42 | ### Step 2: Service Provider
43 |
44 | If you are using `Laravel 11.x` and above, register the `LaravelTrailingSlash\RoutingServiceProvider` in your `bootstrap/providers.php` configuration file:
45 |
46 | ```php
47 | return [
48 | // Package Service Providers...
49 | // ...
50 | LaravelTrailingSlash\RoutingServiceProvider::class,
51 | // ...
52 | ],
53 | ```
54 |
55 | If you are using `Laravel 10.x` and below, register the `LaravelTrailingSlash\RoutingServiceProvider` in your `config/app.php` configuration file:
56 |
57 | ```php
58 | 'providers' => [
59 | // Application Service Providers...
60 | // ...
61 |
62 | // Package Service Providers...
63 | // ...
64 | LaravelTrailingSlash\RoutingServiceProvider::class,
65 | // ...
66 | ],
67 | ```
68 |
69 | ### Step 3: .htaccess
70 |
71 | If you are using apache, copy following redirection code from `public/.htaccess` to your own project:
72 |
73 | ```
74 |
75 | # Redirect To Trailing Slashes If Not A Folder Or A File...
76 | RewriteCond %{REQUEST_FILENAME} !-d
77 | RewriteCond %{REQUEST_FILENAME} !-f
78 | RewriteCond %{REQUEST_URI} !(/$|\.)
79 | RewriteRule (.*) %{REQUEST_URI}/ [R=301,L]
80 |
81 | ```
82 |
83 | ### Step 4: Routes
84 |
85 | In routes/web.php, you must use routes with trailing slashes now:
86 |
87 | ```php
88 | Route::get('/', function () {
89 | return view('welcome');
90 | });
91 |
92 | Route::get('about/', function () {
93 | return view('about');
94 | });
95 |
96 | Route::get('contact/', function () {
97 | return view('contact');
98 | });
99 | ```
100 |
101 | ## Usage
102 |
103 | Every time you use some Laravel redirect function, trailing slash ("/") will be applied at the end of url.
104 |
105 | ```php
106 | return redirect('about/');
107 |
108 | return back()->withInput();
109 |
110 | return redirect()->route('text', ['id' => 1]);
111 |
112 | return redirect()->action('IndexController@about');
113 | ```
114 |
115 | ## Change log
116 |
117 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
118 |
119 | ## Notice
120 |
121 | There is a problem with overriding Laravel `Paginator` and `LengthAwarePaginator` classes. So, every time you use `paginate()` method on your models, query builders etc., you must set current path for pagination links. Example:
122 |
123 | ```php
124 | $texts = Text::where('is_active', 1)->paginate();
125 | $texts->setPath(URL::current());
126 |
127 | $texts->links();
128 | ```
129 |
130 | ## Licence
131 |
132 | MIT Licence. Refer to the [LICENSE](https://github.com/fsasvari/laravel-trailing-slash/blob/main/LICENSE.md) file to get more info.
133 |
134 | ## Author
135 |
136 | Frano Šašvari
137 |
138 | Email: sasvari.frano@gmail.com
139 |
140 | ## Buy me a Beer
141 |
142 | [](https://buymeacoffee.com/sasvarifras)
143 |
144 |
--------------------------------------------------------------------------------
/tests/Unit/RoutingRedirectorTest.php:
--------------------------------------------------------------------------------
1 | headers = m::mock(HeaderBag::class);
27 |
28 | $this->request = m::mock(Request::class);
29 | $this->request->shouldReceive('isMethod')->andReturn(true)->byDefault();
30 | $this->request->shouldReceive('method')->andReturn('GET')->byDefault();
31 | $this->request->shouldReceive('route')->andReturn(true)->byDefault();
32 | $this->request->shouldReceive('ajax')->andReturn(false)->byDefault();
33 | $this->request->shouldReceive('expectsJson')->andReturn(false)->byDefault();
34 | $this->request->headers = $this->headers;
35 |
36 | $this->url = m::mock(UrlGenerator::class);
37 | $this->url->shouldReceive('getRequest')->andReturn($this->request);
38 | $this->url->shouldReceive('to')->with('bar', [], null)->andReturn('http://foo.com/bar/');
39 | $this->url->shouldReceive('to')->with('bar', [], true)->andReturn('https://foo.com/bar/');
40 | $this->url->shouldReceive('to')->with('login', [], null)->andReturn('http://foo.com/login/');
41 | $this->url->shouldReceive('to')->with('http://foo.com/bar/', [], null)->andReturn('http://foo.com/bar/');
42 | $this->url->shouldReceive('to')->with('/', [], null)->andReturn('http://foo.com/');
43 | $this->url->shouldReceive('to')->with('http://foo.com/bar/#foo', [], null)->andReturn('http://foo.com/bar/#foo');
44 | $this->url->shouldReceive('to')->with('http://foo.com/bar/?signature=secret', [], null)->andReturn('http://foo.com/bar/?signature=secret');
45 |
46 | $this->session = m::mock(Store::class);
47 |
48 | $this->redirect = new Redirector($this->url);
49 | $this->redirect->setSession($this->session);
50 | }
51 |
52 | protected function tearDown(): void
53 | {
54 | m::close();
55 | }
56 |
57 | public function testBasicRedirectTo()
58 | {
59 | $response = $this->redirect->to('bar');
60 |
61 | $this->assertInstanceOf(RedirectResponse::class, $response);
62 | $this->assertSame('http://foo.com/bar/', $response->getTargetUrl());
63 | $this->assertEquals(302, $response->getStatusCode());
64 | $this->assertEquals($this->session, $response->getSession());
65 | }
66 |
67 | public function testComplexRedirectTo()
68 | {
69 | $response = $this->redirect->to('bar', 303, ['X-RateLimit-Limit' => 60, 'X-RateLimit-Remaining' => 59], true);
70 |
71 | $this->assertSame('https://foo.com/bar/', $response->getTargetUrl());
72 | $this->assertEquals(303, $response->getStatusCode());
73 | $this->assertEquals(60, $response->headers->get('X-RateLimit-Limit'));
74 | $this->assertEquals(59, $response->headers->get('X-RateLimit-Remaining'));
75 | }
76 |
77 | public function testGuestPutCurrentUrlInSession()
78 | {
79 | $this->url->shouldReceive('full')->andReturn('http://foo.com/bar/');
80 | $this->session->shouldReceive('put')->once()->with('url.intended', 'http://foo.com/bar/');
81 |
82 | $response = $this->redirect->guest('login');
83 |
84 | $this->assertSame('http://foo.com/login/', $response->getTargetUrl());
85 | }
86 |
87 | public function testGuestPutPreviousUrlInSession()
88 | {
89 | $this->request->shouldReceive('isMethod')->once()->with('GET')->andReturn(false);
90 | $this->session->shouldReceive('put')->once()->with('url.intended', 'http://foo.com/bar/');
91 | $this->url->shouldReceive('previous')->once()->andReturn('http://foo.com/bar/');
92 |
93 | $response = $this->redirect->guest('login');
94 |
95 | $this->assertSame('http://foo.com/login/', $response->getTargetUrl());
96 | }
97 |
98 | public function testIntendedRedirectToIntendedUrlInSession()
99 | {
100 | $this->session->shouldReceive('pull')->with('url.intended', '/')->andReturn('http://foo.com/bar/');
101 |
102 | $response = $this->redirect->intended();
103 |
104 | $this->assertSame('http://foo.com/bar/', $response->getTargetUrl());
105 | }
106 |
107 | public function testIntendedWithoutIntendedUrlInSession()
108 | {
109 | $this->session->shouldReceive('forget')->with('url.intended');
110 |
111 | // without fallback url
112 | $this->session->shouldReceive('pull')->with('url.intended', '/')->andReturn('/');
113 | $response = $this->redirect->intended();
114 | $this->assertSame('http://foo.com/', $response->getTargetUrl());
115 |
116 | // with a fallback url
117 | $this->session->shouldReceive('pull')->with('url.intended', 'bar')->andReturn('bar');
118 | $response = $this->redirect->intended('bar');
119 | $this->assertSame('http://foo.com/bar/', $response->getTargetUrl());
120 | }
121 |
122 | public function testRefreshRedirectToCurrentUrl()
123 | {
124 | $this->request->shouldReceive('path')->andReturn('http://foo.com/bar/');
125 | $response = $this->redirect->refresh();
126 | $this->assertSame('http://foo.com/bar/', $response->getTargetUrl());
127 | }
128 |
129 | public function testBackRedirectToHttpReferer()
130 | {
131 | $this->headers->shouldReceive('has')->with('referer')->andReturn(true);
132 | $this->url->shouldReceive('previous')->andReturn('http://foo.com/bar/');
133 | $response = $this->redirect->back();
134 | $this->assertSame('http://foo.com/bar/', $response->getTargetUrl());
135 | }
136 |
137 | public function testAwayDoesntValidateTheUrl()
138 | {
139 | $response = $this->redirect->away('bar');
140 | $this->assertSame('bar', $response->getTargetUrl());
141 | }
142 |
143 | public function testSecureRedirectToHttpsUrl()
144 | {
145 | $response = $this->redirect->secure('bar');
146 | $this->assertSame('https://foo.com/bar/', $response->getTargetUrl());
147 | }
148 |
149 | public function testAction()
150 | {
151 | $this->url->shouldReceive('action')->with('bar@index', [])->andReturn('http://foo.com/bar/');
152 | $response = $this->redirect->action('bar@index');
153 | $this->assertSame('http://foo.com/bar/', $response->getTargetUrl());
154 | }
155 |
156 | public function testRoute()
157 | {
158 | $this->url->shouldReceive('route')->with('home')->andReturn('http://foo.com/bar/');
159 | $this->url->shouldReceive('route')->with('home', [])->andReturn('http://foo.com/bar/');
160 |
161 | $response = $this->redirect->route('home');
162 | $this->assertSame('http://foo.com/bar/', $response->getTargetUrl());
163 | }
164 |
165 | public function testSignedRoute()
166 | {
167 | $this->url->shouldReceive('signedRoute')->with('home', [], null)->andReturn('http://foo.com/bar/?signature=secret');
168 |
169 | $response = $this->redirect->signedRoute('home');
170 | $this->assertSame('http://foo.com/bar/?signature=secret', $response->getTargetUrl());
171 | }
172 |
173 | public function testTemporarySignedRoute()
174 | {
175 | $this->url->shouldReceive('temporarySignedRoute')->with('home', 10, [])->andReturn('http://foo.com/bar/?signature=secret');
176 |
177 | $response = $this->redirect->temporarySignedRoute('home', 10);
178 | $this->assertSame('http://foo.com/bar/?signature=secret', $response->getTargetUrl());
179 | }
180 |
181 | public function testItSetsAndGetsValidIntendedUrl()
182 | {
183 | $this->session->shouldReceive('put')->once()->with('url.intended', 'http://foo.com/bar/');
184 | $this->session->shouldReceive('get')->andReturn('http://foo.com/bar/');
185 |
186 | $result = $this->redirect->setIntendedUrl('http://foo.com/bar/');
187 | $this->assertInstanceOf(Redirector::class, $result);
188 |
189 | $this->assertSame('http://foo.com/bar/', $this->redirect->getIntendedUrl());
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/tests/Unit/RoutingUrlGeneratorTest.php:
--------------------------------------------------------------------------------
1 | assertSame('http://www.foo.com/foo/bar/', $url->to('foo/bar'));
32 | $this->assertSame('https://www.foo.com/foo/bar/', $url->to('foo/bar', [], true));
33 | $this->assertSame('https://www.foo.com/foo/bar/baz/boom/', $url->to('foo/bar', ['baz', 'boom'], true));
34 | $this->assertSame('https://www.foo.com/foo/bar/baz/?foo=bar', $url->to('foo/bar?foo=bar', ['baz'], true));
35 |
36 | /*
37 | * Test HTTPS request URL generation...
38 | */
39 | $url = new UrlGenerator(
40 | new RouteCollection(),
41 | Request::create('https://www.foo.com/')
42 | );
43 |
44 | $this->assertSame('https://www.foo.com/foo/bar/', $url->to('foo/bar'));
45 | }
46 |
47 | public function testQueryGeneration()
48 | {
49 | $url = new UrlGenerator(
50 | new RouteCollection(),
51 | Request::create('http://www.foo.com/')
52 | );
53 |
54 | $this->assertSame('http://www.foo.com/foo/bar/', $url->query('foo/bar'));
55 | $this->assertSame('http://www.foo.com/foo/bar/?0=foo', $url->query('foo/bar', ['foo']));
56 | $this->assertSame('http://www.foo.com/foo/bar/?baz=boom', $url->query('foo/bar', ['baz' => 'boom']));
57 | $this->assertSame('http://www.foo.com/foo/bar/?baz=zee&zal=bee', $url->query('foo/bar?baz=boom&zal=bee', ['baz' => 'zee']));
58 | $this->assertSame('http://www.foo.com/foo/bar/?zal=bee', $url->query('foo/bar?baz=boom&zal=bee', ['baz' => null]));
59 | $this->assertSame('http://www.foo.com/foo/bar/?baz=boom', $url->query('foo/bar?baz=boom', ['nonexist' => null]));
60 | $this->assertSame('http://www.foo.com/foo/bar/', $url->query('foo/bar?baz=boom', ['baz' => null]));
61 | $this->assertSame('https://www.foo.com/foo/bar/baz/?foo=bar&zal=bee', $url->query('foo/bar?foo=bar', ['zal' => 'bee'], ['baz'], true));
62 | $this->assertSame('http://www.foo.com/foo/bar/?baz[0]=boom&baz[1]=bam&baz[2]=bim', urldecode($url->query('foo/bar', ['baz' => ['boom', 'bam', 'bim']])));
63 | }
64 |
65 | public function testAssetGeneration()
66 | {
67 | $url = new UrlGenerator(
68 | new RouteCollection(),
69 | Request::create('http://www.foo.com/index.php/')
70 | );
71 |
72 | $this->assertSame('http://www.foo.com/foo/bar', $url->asset('foo/bar'));
73 | $this->assertSame('https://www.foo.com/foo/bar', $url->asset('foo/bar', true));
74 |
75 | $url = new UrlGenerator(
76 | new RouteCollection(),
77 | Request::create('http://www.foo.com/index.php/'),
78 | '/'
79 | );
80 |
81 | $this->assertSame('/foo/bar', $url->asset('foo/bar'));
82 | $this->assertSame('/foo/bar', $url->asset('foo/bar', true));
83 | }
84 |
85 | public function testBasicGenerationWithHostFormatting()
86 | {
87 | $url = new UrlGenerator(
88 | $routes = new RouteCollection(),
89 | Request::create('http://www.foo.com/')
90 | );
91 |
92 | $route = new Route(['GET'], '/named-route/', ['as' => 'plain']);
93 | $routes->add($route);
94 |
95 | $url->formatHostUsing(function ($host) {
96 | return str_replace('foo.com', 'foo.org', $host);
97 | });
98 |
99 | $this->assertSame('http://www.foo.org/foo/bar/', $url->to('foo/bar'));
100 | $this->assertSame('/named-route/', $url->route('plain', [], false));
101 | }
102 |
103 | public function testBasicGenerationWithRequestBaseUrlWithSubfolder()
104 | {
105 | $request = Request::create('http://www.foo.com/subfolder/foo/bar/subfolder/');
106 |
107 | $request->server->set('SCRIPT_FILENAME', '/var/www/laravel-project/public/subfolder/index.php');
108 | $request->server->set('PHP_SELF', '/subfolder/index.php');
109 |
110 | $url = new UrlGenerator(
111 | $routes = new RouteCollection(),
112 | $request
113 | );
114 |
115 | $route = new Route(['GET'], 'foo/bar/subfolder/', ['as' => 'foobar']);
116 | $routes->add($route);
117 |
118 | $this->assertSame('/subfolder', $request->getBaseUrl());
119 | $this->assertSame('/foo/bar/subfolder/', $url->route('foobar', [], false));
120 | }
121 |
122 | public function testBasicGenerationWithRequestBaseUrlWithSubfolderAndFileSuffix()
123 | {
124 | $request = Request::create('http://www.foo.com/subfolder/index.php');
125 |
126 | $request->server->set('SCRIPT_FILENAME', '/var/www/laravel-project/public/subfolder/index.php');
127 | $request->server->set('PHP_SELF', '/subfolder/index.php');
128 |
129 | $url = new UrlGenerator(
130 | $routes = new RouteCollection(),
131 | $request
132 | );
133 |
134 | $route = new Route(['GET'], 'foo/bar/subfolder/', ['as' => 'foobar']);
135 | $routes->add($route);
136 |
137 | $this->assertSame('/subfolder', $request->getBasePath());
138 | $this->assertSame('/subfolder/index.php', $request->getBaseUrl());
139 | $this->assertSame('/foo/bar/subfolder/', $url->route('foobar', [], false));
140 | }
141 |
142 | public function testBasicGenerationWithRequestBaseUrlWithFileSuffix()
143 | {
144 | $request = Request::create('http://www.foo.com/other.php');
145 |
146 | $request->server->set('SCRIPT_FILENAME', '/var/www/laravel-project/public/other.php');
147 | $request->server->set('PHP_SELF', '/other.php');
148 |
149 | $url = new UrlGenerator(
150 | $routes = new RouteCollection(),
151 | $request
152 | );
153 |
154 | $route = new Route(['GET'], 'foo/bar/subfolder/', ['as' => 'foobar']);
155 | $routes->add($route);
156 |
157 | $this->assertSame('', $request->getBasePath());
158 | $this->assertSame('/other.php', $request->getBaseUrl());
159 | $this->assertSame('/foo/bar/subfolder/', $url->route('foobar', [], false));
160 | }
161 |
162 | public function testBasicGenerationWithPathFormatting()
163 | {
164 | $url = new UrlGenerator(
165 | $routes = new RouteCollection(),
166 | Request::create('http://www.foo.com/')
167 | );
168 |
169 | $route = new Route(['GET'], '/named-route/', ['as' => 'plain']);
170 | $routes->add($route);
171 |
172 | $url->formatPathUsing(function ($path) {
173 | return '/something'.$path;
174 | });
175 |
176 | $this->assertSame('http://www.foo.com/something/foo/bar/', $url->to('foo/bar'));
177 | $this->assertSame('/something/named-route/', $url->route('plain', [], false));
178 | }
179 |
180 | public function testUrlFormattersShouldReceiveTargetRoute()
181 | {
182 | $url = new UrlGenerator(
183 | $routes = new RouteCollection(),
184 | Request::create('http://abc.com/')
185 | );
186 |
187 | $namedRoute = new Route(['GET'], '/bar/', ['as' => 'plain', 'root' => 'bar.com', 'path' => 'foo']);
188 | $routes->add($namedRoute);
189 |
190 | $url->formatHostUsing(function ($root, $route) {
191 | return $route ? 'http://'.$route->getAction('root') : $root;
192 | });
193 |
194 | $url->formatPathUsing(function ($path, $route) {
195 | return $route ? '/'.$route->getAction('path') : $path;
196 | });
197 |
198 | $this->assertSame('http://abc.com/foo/bar/', $url->to('foo/bar'));
199 | $this->assertSame('http://bar.com/foo/', $url->route('plain'));
200 | }
201 |
202 | public function testBasicRouteGeneration()
203 | {
204 | $url = new UrlGenerator(
205 | $routes = new RouteCollection(),
206 | Request::create('http://www.foo.com/')
207 | );
208 |
209 | /*
210 | * Empty Named Route
211 | */
212 | $route = new Route(['GET'], '/', ['as' => 'plain']);
213 | $routes->add($route);
214 |
215 | /*
216 | * Named Routes
217 | */
218 | $route = new Route(['GET'], 'foo/bar/', ['as' => 'foo']);
219 | $routes->add($route);
220 |
221 | /*
222 | * Parameters...
223 | */
224 | $route = new Route(['GET'], 'foo/bar/{baz}/breeze/{boom}/', ['as' => 'bar']);
225 | $routes->add($route);
226 |
227 | /*
228 | * Single Parameter...
229 | */
230 | $route = new Route(['GET'], 'foo/bar/{baz}/', ['as' => 'foobar']);
231 | $routes->add($route);
232 |
233 | /*
234 | * Optional parameter
235 | */
236 | $route = new Route(['GET'], 'foo/bar/{baz?}', ['as' => 'optional']);
237 | $routes->add($route);
238 |
239 | /*
240 | * HTTPS...
241 | */
242 | $route = new Route(['GET'], 'foo/baz/', ['as' => 'baz', 'https']);
243 | $routes->add($route);
244 |
245 | /*
246 | * Controller Route Route
247 | */
248 | $route = new Route(['GET'], 'foo/bam/', ['controller' => 'foo@bar']);
249 | $routes->add($route);
250 |
251 | /*
252 | * Non ASCII routes
253 | */
254 | $route = new Route(['GET'], 'foo/bar/åαф/{baz}/', ['as' => 'foobarbaz']);
255 | $routes->add($route);
256 |
257 | /*
258 | * Fragments
259 | */
260 | $route = new Route(['GET'], 'foo/bar/#derp', ['as' => 'fragment']);
261 | $routes->add($route);
262 |
263 | /*
264 | * Invoke action
265 | */
266 | $route = new Route(['GET'], 'foo/invoke/', ['controller' => 'InvokableActionStub']);
267 | $routes->add($route);
268 |
269 | /*
270 | * With Default Parameter
271 | */
272 | $url->defaults(['locale' => 'en']);
273 | $route = new Route(['GET'], 'foo/', ['as' => 'defaults', 'domain' => '{locale}.example.com', function () {
274 | //
275 | }]);
276 | $routes->add($route);
277 |
278 | /*
279 | * With backed enum name and domain
280 | */
281 | $route = (new Route(['GET'], 'backed-enum', ['as' => 'prefixed.']))->name(RouteNameEnum::UserIndex)->domain(RouteDomainEnum::DashboardDomain);
282 | $routes->add($route);
283 |
284 | $this->assertSame('/', $url->route('plain', [], false));
285 | $this->assertSame('/?foo=bar', $url->route('plain', ['foo' => 'bar'], false));
286 | $this->assertSame('http://www.foo.com/foo/bar/', $url->route('foo'));
287 | $this->assertSame('/foo/bar/', $url->route('foo', [], false));
288 | $this->assertSame('/foo/bar/?foo=bar', $url->route('foo', ['foo' => 'bar'], false));
289 | $this->assertSame('http://www.foo.com/foo/bar/taylor/breeze/otwell/?fly=wall', $url->route('bar', ['taylor', 'otwell', 'fly' => 'wall']));
290 | $this->assertSame('http://www.foo.com/foo/bar/otwell/breeze/taylor/?fly=wall', $url->route('bar', ['boom' => 'taylor', 'baz' => 'otwell', 'fly' => 'wall']));
291 | $this->assertSame('http://www.foo.com/foo/bar/0/', $url->route('foobar', 0));
292 | $this->assertSame('http://www.foo.com/foo/bar/2/', $url->route('foobar', 2));
293 | $this->assertSame('http://www.foo.com/foo/bar/taylor/', $url->route('foobar', 'taylor'));
294 | $this->assertSame('/foo/bar/taylor/breeze/otwell/?fly=wall', $url->route('bar', ['taylor', 'otwell', 'fly' => 'wall'], false));
295 | $this->assertSame('https://www.foo.com/foo/baz/', $url->route('baz'));
296 | $this->assertSame('http://www.foo.com/foo/bam/', $url->action('foo@bar'));
297 | $this->assertSame('http://www.foo.com/foo/bam/', $url->action(['foo', 'bar']));
298 | $this->assertSame('http://www.foo.com/foo/invoke/', $url->action('InvokableActionStub'));
299 | $this->assertSame('http://www.foo.com/foo/bar/taylor/breeze/otwell/?wall&woz', $url->route('bar', ['wall', 'woz', 'boom' => 'otwell', 'baz' => 'taylor']));
300 | $this->assertSame('http://www.foo.com/foo/bar/taylor/breeze/otwell/?wall&woz', $url->route('bar', ['taylor', 'otwell', 'wall', 'woz']));
301 | $this->assertSame('http://www.foo.com/foo/bar/', $url->route('optional'));
302 | $this->assertSame('http://www.foo.com/foo/bar/', $url->route('optional', ['baz' => null]));
303 | $this->assertSame('http://www.foo.com/foo/bar/', $url->route('optional', ['baz' => '']));
304 | $this->assertSame('http://www.foo.com/foo/bar/0/', $url->route('optional', ['baz' => 0]));
305 | $this->assertSame('http://www.foo.com/foo/bar/taylor/', $url->route('optional', 'taylor'));
306 | $this->assertSame('http://www.foo.com/foo/bar/taylor/', $url->route('optional', ['taylor']));
307 | $this->assertSame('http://www.foo.com/foo/bar/taylor/?breeze', $url->route('optional', ['taylor', 'breeze']));
308 | $this->assertSame('http://www.foo.com/foo/bar/taylor/?wall=woz', $url->route('optional', ['wall' => 'woz', 'taylor']));
309 | $this->assertSame('http://www.foo.com/foo/bar/taylor/?wall=woz&breeze', $url->route('optional', ['wall' => 'woz', 'breeze', 'baz' => 'taylor']));
310 | $this->assertSame('http://www.foo.com/foo/bar/?wall=woz', $url->route('optional', ['wall' => 'woz']));
311 | $this->assertSame('http://www.foo.com/foo/bar/%C3%A5%CE%B1%D1%84/%C3%A5%CE%B1%D1%84/', $url->route('foobarbaz', ['baz' => 'åαф']));
312 | $this->assertSame('/foo/bar/#derp', $url->route('fragment', [], false));
313 | $this->assertSame('/foo/bar/?foo=bar#derp', $url->route('fragment', ['foo' => 'bar'], false));
314 | $this->assertSame('/foo/bar/?baz=%C3%A5%CE%B1%D1%84#derp', $url->route('fragment', ['baz' => 'åαф'], false));
315 | $this->assertSame('http://en.example.com/foo/', $url->route('defaults'));
316 | $this->assertSame('http://dashboard.myapp.com/backed-enum/', $url->route('prefixed.users.index'));
317 | }
318 |
319 | public function testFluentRouteNameDefinitions()
320 | {
321 | $url = new UrlGenerator(
322 | $routes = new RouteCollection(),
323 | Request::create('http://www.foo.com/')
324 | );
325 |
326 | /*
327 | * Named Routes
328 | */
329 | $route = new Route(['GET'], 'foo/bar/', []);
330 | $route->name('foo');
331 | $routes->add($route);
332 | $routes->refreshNameLookups();
333 |
334 | $this->assertSame('http://www.foo.com/foo/bar/', $url->route('foo'));
335 | }
336 |
337 | public function testControllerRoutesWithADefaultNamespace()
338 | {
339 | $url = new UrlGenerator(
340 | $routes = new RouteCollection(),
341 | Request::create('http://www.foo.com/')
342 | );
343 |
344 | $url->setRootControllerNamespace('namespace');
345 |
346 | /*
347 | * Controller Route Route
348 | */
349 | $route = new Route(['GET'], 'foo/bar/', ['controller' => 'namespace\foo@bar']);
350 | $routes->add($route);
351 |
352 | $route = new Route(['GET'], 'something/else/', ['controller' => 'something\foo@bar']);
353 | $routes->add($route);
354 |
355 | $route = new Route(['GET'], 'foo/invoke/', ['controller' => 'namespace\InvokableActionStub']);
356 | $routes->add($route);
357 |
358 | $this->assertSame('http://www.foo.com/foo/bar/', $url->action('foo@bar'));
359 | $this->assertSame('http://www.foo.com/something/else/', $url->action('\something\foo@bar'));
360 | $this->assertSame('http://www.foo.com/foo/invoke/', $url->action('InvokableActionStub'));
361 | }
362 |
363 | public function testControllerRoutesOutsideOfDefaultNamespace()
364 | {
365 | $url = new UrlGenerator(
366 | $routes = new RouteCollection(),
367 | Request::create('http://www.foo.com/')
368 | );
369 |
370 | $url->setRootControllerNamespace('namespace');
371 |
372 | $route = new Route(['GET'], 'root/namespace/', ['controller' => '\root\namespace@foo']);
373 | $routes->add($route);
374 |
375 | $route = new Route(['GET'], 'invokable/namespace/', ['controller' => '\root\namespace\InvokableActionStub']);
376 | $routes->add($route);
377 |
378 | $this->assertSame('http://www.foo.com/root/namespace/', $url->action('\root\namespace@foo'));
379 | $this->assertSame('http://www.foo.com/invokable/namespace/', $url->action('\root\namespace\InvokableActionStub'));
380 | }
381 |
382 | public function testRoutableInterfaceRouting()
383 | {
384 | $url = new UrlGenerator(
385 | $routes = new RouteCollection(),
386 | Request::create('http://www.foo.com/')
387 | );
388 |
389 | $route = new Route(['GET'], 'foo/{bar}/', ['as' => 'routable']);
390 | $routes->add($route);
391 |
392 | $model = new RoutableInterfaceStub();
393 | $model->key = 'routable';
394 |
395 | $this->assertSame('/foo/routable/', $url->route('routable', [$model], false));
396 | }
397 |
398 | public function testRoutableInterfaceRoutingWithCustomBindingField()
399 | {
400 | $url = new UrlGenerator(
401 | $routes = new RouteCollection(),
402 | Request::create('http://www.foo.com/')
403 | );
404 |
405 | $route = new Route(['GET'], 'foo/{bar:slug}/', ['as' => 'routable']);
406 | $routes->add($route);
407 |
408 | $model = new RoutableInterfaceStub();
409 | $model->key = 'routable';
410 |
411 | $this->assertSame('/foo/test-slug/', $url->route('routable', ['bar' => $model], false));
412 | $this->assertSame('/foo/test-slug/', $url->route('routable', [$model], false));
413 | }
414 |
415 | public function testRoutableInterfaceRoutingAsQueryString()
416 | {
417 | $url = new UrlGenerator(
418 | $routes = new RouteCollection(),
419 | Request::create('http://www.foo.com/')
420 | );
421 |
422 | $route = new Route(['GET'], 'foo/', ['as' => 'query-string']);
423 | $routes->add($route);
424 |
425 | $model = new RoutableInterfaceStub();
426 | $model->key = 'routable';
427 |
428 | $this->assertSame('/foo/?routable', $url->route('query-string', $model, false));
429 | $this->assertSame('/foo/?routable', $url->route('query-string', [$model], false));
430 | $this->assertSame('/foo/?foo=routable', $url->route('query-string', ['foo' => $model], false));
431 | }
432 |
433 | /**
434 | * @todo Fix bug related to route keys
435 | *
436 | * @link https://github.com/laravel/framework/pull/42425
437 | */
438 | public function testRoutableInterfaceRoutingWithSeparateBindingFieldOnlyForSecondParameter()
439 | {
440 | $this->markTestSkipped('See https://github.com/laravel/framework/pull/43255');
441 |
442 | $url = new UrlGenerator(
443 | $routes = new RouteCollection(),
444 | Request::create('http://www.foo.com/')
445 | );
446 |
447 | $route = new Route(['GET'], 'foo/{bar}/{baz:slug}/', ['as' => 'routable']);
448 | $routes->add($route);
449 |
450 | $model1 = new RoutableInterfaceStub();
451 | $model1->key = 'routable-1';
452 |
453 | $model2 = new RoutableInterfaceStub();
454 | $model2->key = 'routable-2';
455 |
456 | $this->assertSame('/foo/routable-1/test-slug/', $url->route('routable', ['bar' => $model1, 'baz' => $model2], false));
457 | $this->assertSame('/foo/routable-1/test-slug/', $url->route('routable', [$model1, $model2], false));
458 | }
459 |
460 | public function testRoutableInterfaceRoutingWithSingleParameter()
461 | {
462 | $url = new UrlGenerator(
463 | $routes = new RouteCollection(),
464 | Request::create('http://www.foo.com/')
465 | );
466 |
467 | $route = new Route(['GET'], 'foo/{bar}/', ['as' => 'routable']);
468 | $routes->add($route);
469 |
470 | $model = new RoutableInterfaceStub();
471 | $model->key = 'routable';
472 |
473 | $this->assertSame('/foo/routable/', $url->route('routable', $model, false));
474 | }
475 |
476 | public function testRoutesMaintainRequestScheme()
477 | {
478 | $url = new UrlGenerator(
479 | $routes = new RouteCollection(),
480 | Request::create('https://www.foo.com/')
481 | );
482 |
483 | /*
484 | * Named Routes
485 | */
486 | $route = new Route(['GET'], 'foo/bar/', ['as' => 'foo']);
487 | $routes->add($route);
488 |
489 | $this->assertSame('https://www.foo.com/foo/bar/', $url->route('foo'));
490 | }
491 |
492 | public function testHttpOnlyRoutes()
493 | {
494 | $url = new UrlGenerator(
495 | $routes = new RouteCollection(),
496 | Request::create('https://www.foo.com/')
497 | );
498 |
499 | /*
500 | * Named Routes
501 | */
502 | $route = new Route(['GET'], 'foo/bar/', ['as' => 'foo', 'http']);
503 | $routes->add($route);
504 |
505 | $this->assertSame('http://www.foo.com/foo/bar/', $url->route('foo'));
506 | }
507 |
508 | public function testRoutesWithDomains()
509 | {
510 | $url = new UrlGenerator(
511 | $routes = new RouteCollection(),
512 | Request::create('http://www.foo.com/')
513 | );
514 |
515 | $route = new Route(['GET'], 'foo/bar/', ['as' => 'foo', 'domain' => 'sub.foo.com']);
516 | $routes->add($route);
517 |
518 | /*
519 | * Wildcards & Domains...
520 | */
521 | $route = new Route(['GET'], 'foo/bar/{baz}/', ['as' => 'bar', 'domain' => 'sub.{foo}.com']);
522 | $routes->add($route);
523 |
524 | $this->assertSame('http://sub.foo.com/foo/bar/', $url->route('foo'));
525 | $this->assertSame('http://sub.taylor.com/foo/bar/otwell/', $url->route('bar', ['taylor', 'otwell']));
526 | $this->assertSame('/foo/bar/otwell/', $url->route('bar', ['taylor', 'otwell'], false));
527 | }
528 |
529 | public function testRoutesWithDomainsAndPorts()
530 | {
531 | $url = new UrlGenerator(
532 | $routes = new RouteCollection(),
533 | Request::create('http://www.foo.com:8080/')
534 | );
535 |
536 | $route = new Route(['GET'], 'foo/bar/', ['as' => 'foo', 'domain' => 'sub.foo.com']);
537 | $routes->add($route);
538 |
539 | /*
540 | * Wildcards & Domains...
541 | */
542 | $route = new Route(['GET'], 'foo/bar/{baz}/', ['as' => 'bar', 'domain' => 'sub.{foo}.com']);
543 | $routes->add($route);
544 |
545 | $this->assertSame('http://sub.foo.com:8080/foo/bar/', $url->route('foo'));
546 | $this->assertSame('http://sub.taylor.com:8080/foo/bar/otwell/', $url->route('bar', ['taylor', 'otwell']));
547 | }
548 |
549 | public function testRoutesWithDomainsStripsProtocols()
550 | {
551 | /*
552 | * http:// Route
553 | */
554 | $url = new UrlGenerator(
555 | $routes = new RouteCollection(),
556 | Request::create('http://www.foo.com/')
557 | );
558 |
559 | $route = new Route(['GET'], 'foo/bar/', ['as' => 'foo', 'domain' => 'http://sub.foo.com']);
560 | $routes->add($route);
561 |
562 | $this->assertSame('http://sub.foo.com/foo/bar/', $url->route('foo'));
563 |
564 | /*
565 | * https:// Route
566 | */
567 | $url = new UrlGenerator(
568 | $routes = new RouteCollection(),
569 | Request::create('https://www.foo.com/')
570 | );
571 |
572 | $route = new Route(['GET'], 'foo/bar/', ['as' => 'foo', 'domain' => 'https://sub.foo.com']);
573 | $routes->add($route);
574 |
575 | $this->assertSame('https://sub.foo.com/foo/bar/', $url->route('foo'));
576 | }
577 |
578 | public function testHttpsRoutesWithDomains()
579 | {
580 | $url = new UrlGenerator(
581 | $routes = new RouteCollection(),
582 | Request::create('https://foo.com/')
583 | );
584 |
585 | /*
586 | * When on HTTPS, no need to specify 443
587 | */
588 | $route = new Route(['GET'], 'foo/bar/', ['as' => 'baz', 'domain' => 'sub.foo.com']);
589 | $routes->add($route);
590 |
591 | $this->assertSame('https://sub.foo.com/foo/bar/', $url->route('baz'));
592 | }
593 |
594 | public function testRoutesWithDomainsThroughProxy()
595 | {
596 | Request::setTrustedProxies(['10.0.0.1'], SymfonyRequest::HEADER_X_FORWARDED_FOR | SymfonyRequest::HEADER_X_FORWARDED_HOST | SymfonyRequest::HEADER_X_FORWARDED_PORT | SymfonyRequest::HEADER_X_FORWARDED_PROTO);
597 |
598 | $url = new UrlGenerator(
599 | $routes = new RouteCollection(),
600 | Request::create('http://www.foo.com/', 'GET', [], [], [], ['REMOTE_ADDR' => '10.0.0.1', 'HTTP_X_FORWARDED_PORT' => '80'])
601 | );
602 |
603 | $route = new Route(['GET'], 'foo/bar/', ['as' => 'foo', 'domain' => 'sub.foo.com']);
604 | $routes->add($route);
605 |
606 | $this->assertSame('http://sub.foo.com/foo/bar/', $url->route('foo'));
607 | }
608 |
609 | public static function providerRouteParameters()
610 | {
611 | return [
612 | [['test' => 123]],
613 | [['one' => null, 'test' => 123]],
614 | [['one' => '', 'test' => 123]],
615 | ];
616 | }
617 |
618 | #[DataProvider('providerRouteParameters')]
619 | public function testUrlGenerationForControllersRequiresPassingOfRequiredParameters($parameters)
620 | {
621 | $this->expectException(UrlGenerationException::class);
622 |
623 | $url = new UrlGenerator(
624 | $routes = new RouteCollection(),
625 | Request::create('http://www.foo.com:8080/')
626 | );
627 |
628 | $route = new Route(['GET'], 'foo/{one}/{two?}/{three?}', ['as' => 'foo', function () {
629 | //
630 | }]);
631 | $routes->add($route);
632 |
633 | $this->assertSame('http://www.foo.com:8080/foo/?test=123', $url->route('foo', $parameters));
634 | }
635 |
636 | public static function provideParametersAndExpectedMeaningfulExceptionMessages()
637 | {
638 | return [
639 | 'Missing parameters "one", "two" and "three"' => [
640 | [],
641 | 'Missing required parameters for [Route: foo] [URI: foo/{one}/{two}/{three}/{four?}] [Missing parameters: one, two, three].',
642 | ],
643 | 'Missing parameters "two" and "three"' => [
644 | ['one' => '123'],
645 | 'Missing required parameters for [Route: foo] [URI: foo/{one}/{two}/{three}/{four?}] [Missing parameters: two, three].',
646 | ],
647 | 'Missing parameters "one" and "three"' => [
648 | ['two' => '123'],
649 | 'Missing required parameters for [Route: foo] [URI: foo/{one}/{two}/{three}/{four?}] [Missing parameters: one, three].',
650 | ],
651 | 'Missing parameters "one" and "two"' => [
652 | ['three' => '123'],
653 | 'Missing required parameters for [Route: foo] [URI: foo/{one}/{two}/{three}/{four?}] [Missing parameters: one, two].',
654 | ],
655 | 'Missing parameter "three"' => [
656 | ['one' => '123', 'two' => '123'],
657 | 'Missing required parameter for [Route: foo] [URI: foo/{one}/{two}/{three}/{four?}] [Missing parameter: three].',
658 | ],
659 | 'Missing parameter "two"' => [
660 | ['one' => '123', 'three' => '123'],
661 | 'Missing required parameter for [Route: foo] [URI: foo/{one}/{two}/{three}/{four?}] [Missing parameter: two].',
662 | ],
663 | 'Missing parameter "one"' => [
664 | ['two' => '123', 'three' => '123'],
665 | 'Missing required parameter for [Route: foo] [URI: foo/{one}/{two}/{three}/{four?}] [Missing parameter: one].',
666 | ],
667 | ];
668 | }
669 |
670 | #[DataProvider('provideParametersAndExpectedMeaningfulExceptionMessages')]
671 | public function testUrlGenerationThrowsExceptionForMissingParametersWithMeaningfulMessage($parameters, $expectedMeaningfulExceptionMessage)
672 | {
673 | $this->expectException(UrlGenerationException::class);
674 | $this->expectExceptionMessage($expectedMeaningfulExceptionMessage);
675 |
676 | $url = new UrlGenerator(
677 | $routes = new RouteCollection(),
678 | Request::create('http://www.foo.com:8080/')
679 | );
680 |
681 | $route = new Route(['GET'], 'foo/{one}/{two}/{three}/{four?}', ['as' => 'foo', function () {
682 | //
683 | }]);
684 | $routes->add($route);
685 |
686 | $url->route('foo', $parameters);
687 | }
688 |
689 | public function testForceRootUrl()
690 | {
691 | $url = new UrlGenerator(
692 | $routes = new RouteCollection(),
693 | Request::create('http://www.foo.com/')
694 | );
695 |
696 | $url->forceRootUrl('https://www.bar.com');
697 | $this->assertSame('http://www.bar.com/foo/bar/', $url->to('foo/bar/'));
698 |
699 | // Ensure trailing slash is trimmed from root URL as UrlGenerator already handles this
700 | $url->forceRootUrl('http://www.foo.com/');
701 | $this->assertSame('http://www.foo.com/bar/', $url->to('/bar/'));
702 |
703 | /*
704 | * Route Based...
705 | */
706 | $url = new UrlGenerator(
707 | $routes = new RouteCollection(),
708 | Request::create('http://www.foo.com/')
709 | );
710 |
711 | $url->forceScheme('https');
712 | $route = new Route(['GET'], '/foo/', ['as' => 'plain']);
713 | $routes->add($route);
714 |
715 | $this->assertSame('https://www.foo.com/foo/', $url->route('plain'));
716 |
717 | $url->forceRootUrl('https://www.bar.com');
718 | $this->assertSame('https://www.bar.com/foo/', $url->route('plain'));
719 | }
720 |
721 | public function testForceHttps()
722 | {
723 | $url = new UrlGenerator(
724 | $routes = new RouteCollection(),
725 | Request::create('http://www.foo.com/')
726 | );
727 |
728 | $url->forceHttps();
729 | $route = new Route(['GET'], '/foo/', ['as' => 'plain']);
730 | $routes->add($route);
731 |
732 | $this->assertSame('https://www.foo.com/foo/', $url->route('plain'));
733 | }
734 |
735 | public function testPrevious()
736 | {
737 | $url = new UrlGenerator(
738 | new RouteCollection(),
739 | Request::create('http://www.foo.com/')
740 | );
741 |
742 | $url->getRequest()->headers->set('referer', 'http://www.bar.com/');
743 | $this->assertSame('http://www.bar.com/', $url->previous());
744 |
745 | $url->getRequest()->headers->remove('referer');
746 | $this->assertEquals($url->to('/'), $url->previous());
747 |
748 | $this->assertEquals($url->to('/foo/'), $url->previous('/foo/'));
749 | }
750 |
751 | public function testPreviousPath()
752 | {
753 | $url = new UrlGenerator(
754 | new RouteCollection(),
755 | Request::create('http://www.foo.com/')
756 | );
757 |
758 | $url->getRequest()->headers->set('referer', 'http://www.foo.com?baz=bah');
759 | $this->assertSame('/', $url->previousPath());
760 |
761 | $url->getRequest()->headers->set('referer', 'http://www.foo.com/?baz=bah');
762 | $this->assertSame('/', $url->previousPath());
763 |
764 | $url->getRequest()->headers->set('referer', 'http://www.foo.com/bar/?baz=bah');
765 | $this->assertSame('/bar/', $url->previousPath());
766 |
767 | $url->getRequest()->headers->remove('referer');
768 | $this->assertSame('/', $url->previousPath());
769 |
770 | $this->assertSame('/bar/', $url->previousPath('/bar/'));
771 | }
772 |
773 | public function testRouteNotDefinedException()
774 | {
775 | $this->expectException(RouteNotFoundException::class);
776 | $this->expectExceptionMessage('Route [not_exists_route] not defined.');
777 |
778 | $url = new UrlGenerator(
779 | new RouteCollection(),
780 | Request::create('http://www.foo.com/')
781 | );
782 |
783 | $url->route('not_exists_route');
784 | }
785 |
786 | public function testSignedUrl()
787 | {
788 | $url = new UrlGenerator(
789 | $routes = new RouteCollection(),
790 | $request = Request::create('http://www.foo.com/')
791 | );
792 | $url->setKeyResolver(function () {
793 | return 'secret';
794 | });
795 |
796 | $route = new Route(['GET'], 'foo/', ['as' => 'foo', function () {
797 | //
798 | }]);
799 | $routes->add($route);
800 |
801 | $request = Request::create($url->signedRoute('foo'));
802 |
803 | $this->assertTrue($url->hasValidSignature($request));
804 |
805 | $request = Request::create($url->signedRoute('foo').'?tampered=true');
806 |
807 | $this->assertFalse($url->hasValidSignature($request));
808 | }
809 |
810 | public function testSignedUrlImplicitModelBinding()
811 | {
812 | $url = new UrlGenerator(
813 | $routes = new RouteCollection(),
814 | $request = Request::create('http://www.foo.com/')
815 | );
816 | $url->setKeyResolver(function () {
817 | return 'secret';
818 | });
819 |
820 | $route = new Route(['GET'], 'foo/{user:uuid}', ['as' => 'foo', function () {
821 | //
822 | }]);
823 | $routes->add($route);
824 |
825 | $user = new RoutingUrlGeneratorTestUser(['uuid' => '0231d4ac-e9e3-4452-a89a-4427cfb23c3e']);
826 |
827 | $request = Request::create($url->signedRoute('foo', $user));
828 |
829 | $this->assertTrue($url->hasValidSignature($request));
830 | }
831 |
832 | public function testSignedRelativeUrl()
833 | {
834 | $url = new UrlGenerator(
835 | $routes = new RouteCollection(),
836 | $request = Request::create('http://www.foo.com/')
837 | );
838 | $url->setKeyResolver(function () {
839 | return 'secret';
840 | });
841 |
842 | $route = new Route(['GET'], 'foo/', ['as' => 'foo', function () {
843 | //
844 | }]);
845 | $routes->add($route);
846 |
847 | $result = $url->signedRoute('foo', [], null, false);
848 |
849 | $request = Request::create($result);
850 |
851 | $this->assertTrue($url->hasValidSignature($request, false));
852 |
853 | $request = Request::create($url->signedRoute('foo', [], null, false).'?tampered=true');
854 |
855 | $this->assertFalse($url->hasValidSignature($request, false));
856 | }
857 |
858 | public function testSignedUrlParameterCannotBeNamedSignature()
859 | {
860 | $url = new UrlGenerator(
861 | $routes = new RouteCollection(),
862 | $request = Request::create('http://www.foo.com/')
863 | );
864 | $url->setKeyResolver(function () {
865 | return 'secret';
866 | });
867 |
868 | $route = new Route(['GET'], 'foo/{signature}', ['as' => 'foo', function () {
869 | //
870 | }]);
871 | $routes->add($route);
872 |
873 | $this->expectException(InvalidArgumentException::class);
874 | $this->expectExceptionMessage('reserved');
875 |
876 | Request::create($url->signedRoute('foo', ['signature' => 'bar']));
877 | }
878 |
879 | public function testSignedUrlParameterCannotBeNamedExpires()
880 | {
881 | $url = new UrlGenerator(
882 | $routes = new RouteCollection(),
883 | $request = Request::create('http://www.foo.com/')
884 | );
885 | $url->setKeyResolver(function () {
886 | return 'secret';
887 | });
888 |
889 | $route = new Route(['GET'], 'foo/{expires}', ['as' => 'foo', function () {
890 | //
891 | }]);
892 | $routes->add($route);
893 |
894 | $this->expectException(InvalidArgumentException::class);
895 | $this->expectExceptionMessage('reserved');
896 |
897 | Request::create($url->signedRoute('foo', ['expires' => 253402300799]));
898 | }
899 |
900 | public function testRouteGenerationWithBackedEnums()
901 | {
902 | $url = new UrlGenerator(
903 | $routes = new RouteCollection(),
904 | Request::create('http://www.foo.com/')
905 | );
906 |
907 | $namedRoute = new Route(['GET'], '/foo/{bar}/', ['as' => 'foo.bar']);
908 | $routes->add($namedRoute);
909 |
910 | $this->assertSame('http://www.foo.com/foo/fruits/', $url->route('foo.bar', CategoryBackedEnum::Fruits));
911 | }
912 |
913 | public function testRouteGenerationWithNestedBackedEnums()
914 | {
915 | $url = new UrlGenerator(
916 | $routes = new RouteCollection(),
917 | Request::create('http://www.foo.com/')
918 | );
919 |
920 | $namedRoute = new Route(['GET'], '/foo/', ['as' => 'foo']);
921 | $routes->add($namedRoute);
922 |
923 | $this->assertSame(
924 | 'http://www.foo.com/foo/?filter%5B0%5D=people&filter%5B1%5D=fruits',
925 | $url->route('foo', ['filter' => [CategoryBackedEnum::People, CategoryBackedEnum::Fruits]]),
926 | );
927 | }
928 |
929 | public function testSignedUrlWithKeyResolver()
930 | {
931 | $url = new UrlGenerator(
932 | $routes = new RouteCollection(),
933 | $request = Request::create('http://www.foo.com/')
934 | );
935 | $url->setKeyResolver(function () {
936 | return 'first-secret';
937 | });
938 |
939 | $route = new Route(['GET'], 'foo', ['as' => 'foo', function () {
940 | //
941 | }]);
942 | $routes->add($route);
943 |
944 | $firstRequest = Request::create($url->signedRoute('foo'));
945 |
946 | $this->assertTrue($url->hasValidSignature($firstRequest));
947 |
948 | $request = Request::create($url->signedRoute('foo').'?tampered=true');
949 |
950 | $this->assertFalse($url->hasValidSignature($request));
951 |
952 | $url2 = $url->withKeyResolver(function () {
953 | return 'second-secret';
954 | });
955 |
956 | $this->assertFalse($url2->hasValidSignature($firstRequest));
957 |
958 | $secondRequest = Request::create($url2->signedRoute('foo'));
959 |
960 | $this->assertTrue($url2->hasValidSignature($secondRequest));
961 | $this->assertFalse($url->hasValidSignature($secondRequest));
962 |
963 | // Key resolver also supports multiple keys, for app key rotation via the config "app.previous_keys"
964 | $url3 = $url->withKeyResolver(function () {
965 | return ['first-secret', 'second-secret'];
966 | });
967 |
968 | $this->assertTrue($url3->hasValidSignature($firstRequest));
969 | $this->assertTrue($url3->hasValidSignature($secondRequest));
970 | }
971 |
972 | public function testMissingNamedRouteResolution()
973 | {
974 | $url = new UrlGenerator(
975 | new RouteCollection(),
976 | Request::create('http://www.foo.com/')
977 | );
978 |
979 | $url->resolveMissingNamedRoutesUsing(fn ($name, $parameters, $absolute) => 'test-url');
980 |
981 | $this->assertSame('test-url', $url->route('foo'));
982 | }
983 | }
984 |
985 | class RoutableInterfaceStub implements UrlRoutable
986 | {
987 | public $key;
988 | public $slug = 'test-slug';
989 |
990 | public function getRouteKey()
991 | {
992 | return $this->{$this->getRouteKeyName()};
993 | }
994 |
995 | public function getRouteKeyName()
996 | {
997 | return 'key';
998 | }
999 |
1000 | public function resolveRouteBinding($routeKey, $field = null)
1001 | {
1002 | //
1003 | }
1004 |
1005 | public function resolveChildRouteBinding($childType, $routeKey, $field = null)
1006 | {
1007 | //
1008 | }
1009 | }
1010 |
1011 | class InvokableActionStub
1012 | {
1013 | public function __invoke()
1014 | {
1015 | return 'hello';
1016 | }
1017 | }
1018 |
1019 | class RoutingUrlGeneratorTestUser extends Model
1020 | {
1021 | protected $fillable = ['uuid'];
1022 | }
1023 |
--------------------------------------------------------------------------------