├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── UPGRADE.md
├── composer.json
└── src
├── CompiledRouteCollection.php
├── Middleware
└── RedirectTrailingSlash.php
├── ResourceRegistrar.php
├── Route.php
├── RouteCollection.php
├── Router.php
├── RoutingServiceProvider.php
├── Testing
└── AllowsUrlTrailingSlash.php
└── UrlGenerator.php
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Laravel URL Route Trailing Slash
2 | ================================
3 |
4 | 1.1.14, March 6, 2025
5 | ---------------------
6 |
7 | - Enh #18: Added support for "illuminate/routing" 12.0 (klimov-paul)
8 |
9 |
10 | 1.1.13, January 17, 2025
11 | ------------------------
12 |
13 | - Bug #17: Fixed missing explicit nullable type hint at `Router` for PHP 8.4 (daikazu)
14 |
15 |
16 | 1.1.12, May 17, 2024
17 | --------------------
18 |
19 | - Bug #15: Fixed `RedirectTrailingSlash` middleware no longer allows multiple trailing slashes (klimov-paul)
20 |
21 |
22 | 1.1.11, March 25, 2024
23 | ----------------------
24 |
25 | - Enh #14: Added support for "illuminate/routing" 11.0 (klimov-paul)
26 |
27 |
28 | 1.1.10, June 23, 2023
29 | ---------------------
30 |
31 | - Bug #12: Fixed incorrect default route name generation for `Route::resource()` with trailing slash (klimov-paul)
32 |
33 |
34 | 1.1.9, February 24, 2023
35 | ------------------------
36 |
37 | - Enh #11: Added support for "illuminate/routing" 10.0 (klimov-paul)
38 |
39 |
40 | 1.1.8, February 9, 2022
41 | -----------------------
42 |
43 | - Enh: Added support for "illuminate/routing" 9.0 (klimov-paul)
44 |
45 |
46 | 1.1.7, November 13, 2020
47 | ------------------------
48 |
49 | - Bug #8: Fixed `keyResolver` and `sessionResolver` setup for `UrlGenerator` (oheck, klimov-paul)
50 |
51 |
52 | 1.1.6, September 9, 2020
53 | ------------------------
54 |
55 | - Enh: Added support for "illuminate/routing" 8.0 (klimov-paul)
56 |
57 |
58 | 1.1.5, July 24, 2020
59 | --------------------
60 |
61 | - Bug #6: Fixed `Route::$hasTrailingSlash` value loss during route caching with "illuminate/routing" >= 7.0 (klimov-paul)
62 |
63 |
64 | 1.1.4, April 23, 2020
65 | ---------------------
66 |
67 | - Bug #4: Fixed incompatibility with new route caching of "illuminate/routing" >= 7.0 (klimov-paul)
68 |
69 |
70 | 1.1.3, March 4, 2020
71 | --------------------
72 |
73 | - Enh: Added support for "illuminate/routing" 7.0 (klimov-paul)
74 |
75 |
76 | 1.1.2, February 17, 2020
77 | ------------------------
78 |
79 | - Bug #3: Fix `UrlGenerator::full()` does not respects trailing slash in current request URI (klimov-paul)
80 |
81 |
82 | 1.1.1, October 2, 2019
83 | ----------------------
84 |
85 | - Enh #1: Added trailing slash options for resource routes definition (klimov-paul)
86 |
87 |
88 | 1.1.0, September 6, 2019
89 | ------------------------
90 |
91 | - Enh: Added support for "illuminate/routing" 6.0 (klimov-paul)
92 |
93 |
94 | 1.0.1, July 10, 2019
95 | --------------------
96 |
97 | - Enh: Added `AllowsUrlTrailingSlash` test case trait for the better unit and feature tests support (klimov-paul)
98 |
99 |
100 | 1.0.0, July 8, 2019
101 | -------------------
102 |
103 | - Initial release.
104 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | This is free software. It is released under the terms of the
2 | following BSD License.
3 |
4 | Copyright © 2019 by Illuminatech (https://github.com/illuminatech)
5 | All rights reserved.
6 |
7 | Redistribution and use in source and binary forms, with or without
8 | modification, are permitted provided that the following conditions
9 | are met:
10 |
11 | * Redistributions of source code must retain the above copyright
12 | notice, this list of conditions and the following disclaimer.
13 | * Redistributions in binary form must reproduce the above copyright
14 | notice, this list of conditions and the following disclaimer in
15 | the documentation and/or other materials provided with the
16 | distribution.
17 | * Neither the name of Illuminatech nor the names of its
18 | contributors may be used to endorse or promote products derived
19 | from this software without specific prior written permission.
20 |
21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 | POSSIBILITY OF SUCH DAMAGE.
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Laravel URL Route Trailing Slash
6 |
7 |
8 |
9 | This extension allows enforcing URL routes with or without trailing slash.
10 |
11 | For license information check the [LICENSE](LICENSE.md)-file.
12 |
13 | [](https://packagist.org/packages/illuminatech/url-trailing-slash)
14 | [](https://packagist.org/packages/illuminatech/url-trailing-slash)
15 | [](https://github.com/illuminatech/url-trailing-slash/actions)
16 |
17 |
18 | Installation
19 | ------------
20 |
21 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
22 |
23 | Either run
24 |
25 | ```
26 | php composer.phar require --prefer-dist illuminatech/url-trailing-slash
27 | ```
28 |
29 | or add
30 |
31 | ```json
32 | "illuminatech/url-trailing-slash": "*"
33 | ```
34 |
35 | to the require section of your composer.json.
36 |
37 | Once package is installed you should manually register `\Illuminatech\UrlTrailingSlash\RoutingServiceProvider` instance at your
38 | application in the way it comes before kernel instantiation, e.g. at the application bootstrap stage. This can be done
39 | in 'bootstrap/app.php' file of regular Laravel application. For example:
40 |
41 | ```php
42 | withRouting(
50 | // ...
51 | )
52 | ->withMiddleware(function (Middleware $middleware) {
53 | // ...
54 | })
55 | // ...
56 | ->create();
57 |
58 | $app->register(new Illuminatech\UrlTrailingSlash\RoutingServiceProvider($app)); // register trailing slashes routing
59 |
60 | return $app;
61 | ```
62 |
63 | > Note: `\Illuminatech\UrlTrailingSlash\RoutingServiceProvider` can not be registered in normal way or be automatically
64 | discovered by Laravel, since it alters the router, which is bound to the HTTP kernel instance at constructor level.
65 |
66 | In order to setup automatic redirection for the routes with trailing slash add `\Illuminatech\UrlTrailingSlash\Middleware\RedirectTrailingSlash`
67 | middleware to your HTTP kernel. For example:
68 |
69 | ```php
70 | withRouting(
78 | // ...
79 | )
80 | ->withMiddleware(function (Middleware $middleware) {
81 | $middleware->prependToGroup('web', Illuminatech\UrlTrailingSlash\Middleware\RedirectTrailingSlash::class); // enable automatic redirection on incorrect URL trailing slashes
82 | // probably you do not need trailing slash redirection anywhere besides public web routes,
83 | // thus there is no reason for addition its middleware to other groups, like API
84 | // ...
85 | })
86 | // ...
87 | ->create();
88 |
89 | $app->register(new Illuminatech\UrlTrailingSlash\RoutingServiceProvider($app)); // register trailing slashes routing
90 |
91 | return $app;
92 | ```
93 |
94 | **Heads up!** Make sure you do not have any trailing slash redirection mechanism at the server configuration level, which
95 | may conflict with `\Illuminatech\UrlTrailingSlash\Middleware\RedirectTrailingSlash`. Remember, that by default Laravel
96 | application is shipped with `.htaccess` file, which contains redirection rule enforcing trailing slash absence in project URLs.
97 | Make sure you adjust or disable it, otherwise your application may end in infinite redirection loop.
98 |
99 |
100 | Usage
101 | -----
102 |
103 | This extension allows enforcing URL routes with or without trailing slash. You can decide per each route, whether its URL
104 | should have a trailing slash or not, simply adding or removing slash symbol ('/') in particular route definition.
105 |
106 | In case URL for particular route is specified with the trailing slash - it will be enforced for this route, and request
107 | without slash in the URL ending will cause 301 redirection.
108 | In case URL for particular route is specified without the trailing slash - its absence will be enforced for this route,
109 | and request containing slash in the URL end will cause 301 redirection.
110 |
111 | For example:
112 |
113 | ```php
114 | name('items.index'); // enforce trailing slash
121 | Route::get('items/{item}', ItemController::class.'@show')->name('items.show'); // enforce no trailing slash
122 |
123 | // ...
124 |
125 | echo route('items.index'); // outputs: 'http://example.com/items/'
126 | echo route('items.show', [1]); // outputs: 'http://example.com/items/1'
127 | ```
128 |
129 | > Tip: the best SEO practice is having trailing slash at the URLs, which have nested pages, e.g. "defines a folder", and
130 | have no trailing slashes at the URLs without nested pages, e.g. "pathname of the file".
131 |
132 | In case you have setup `\Illuminatech\UrlTrailingSlash\Middleware\RedirectTrailingSlash` middleware, application will automatically
133 | redirect request with incorrect URL according to the routes definition. For the example above: request of `http://example.com/items`
134 | causes redirect to `http://example.com/items/` while request to `http://example.com/items/1/` causes redirect to `http://example.com/items/1`.
135 |
136 | **Heads up!** Remember, that with this extension installed, you are controlling requirements of URL trailing slashes presence
137 | or absence in **each** route you define. While normally Laravel strips any trailing slashes from route URL automatically,
138 | this extension gives them meaning. You should carefully examine your routes definitions, ensuring you do not set trailing
139 | slash for the wrong ones.
140 |
141 |
142 | ### Slash in Root URL
143 |
144 | Unfortunally this extension is unable to handle trailing slashes for the project root URL, e.g. for a 'home' page.
145 | In other words `\Illuminatech\UrlTrailingSlash\Middleware\RedirectTrailingSlash` middleware is unable to distinguish URL
146 | like `http://examle.com` from `http://examle.com/`. This restriction caused by PHP itself, as `$_SERVER['REQUEST_URI']`
147 | value equals to '/' in both cases.
148 |
149 | You'll have to deal with trailing slash for root URL separately at the server settings level.
150 |
151 |
152 | ### Resource Routes
153 |
154 | You can define trailing slash presence for resource URLs using the same notation as for regular routes. In case resource
155 | name is specified with trailing slash, all its URLs will have it. For example:
156 |
157 | ```php
158 | 'index']); // trailing slash will be present only for 'index'
188 | Route::resource('categories', CategoryController::class, ['trailingSlashExcept' => 'show']); // trailing slash will be present for all but 'show'
189 |
190 | // ...
191 |
192 | echo route('items.index'); // outputs: 'http://example.com/items/'
193 | echo route('items.show', [1]); // outputs: 'http://example.com/items/1'
194 |
195 | echo route('categories.index'); // outputs: 'http://example.com/categories/'
196 | echo route('categories.show', [1]); // outputs: 'http://example.com/categories/1'
197 | ```
198 |
199 | > Note: 'trailingSlashExcept' option takes precedence over 'trailingSlashOnly'.
200 |
201 |
202 | ### Trailing Slash in Pagination
203 |
204 | Unfortunately, the trailing slash will not automatically appear at pagination URLs.
205 | The problem is that Laravel paginators trim the trailing slashes from the URL path at the constructor level.
206 | Thus even adjustment of `\Illuminate\Pagination\Paginator::currentPathResolver()` can not fix the problem.
207 |
208 | In case you need a pagination at the URL endpoint with a trailing slash, you should manually set the path for it, using
209 | `\Illuminate\Pagination\AbstractPaginator::withPath()`. For example:
210 |
211 | ```php
212 | paginate()
219 | ->withPath(URL::current());
220 | ```
221 |
222 |
223 | ### Trailing Slash in Unit Tests
224 |
225 | Since `Illuminatech\UrlTrailingSlash\RoutingServiceProvider` can not be registered as regular data provider, while writing
226 | unit and feature tests you will have to manually register it within test application before test kernel instantiation.
227 | This can be done within your `\Tests\CreatesApplication` trait:
228 |
229 | ```php
230 | register(new RoutingServiceProvider($app)); // register trailing slashes routing
249 |
250 | $app->make(Kernel::class)->bootstrap();
251 |
252 | return $app;
253 | }
254 | }
255 | ```
256 |
257 | However, this in not enough to make tests running correctly, because Laravel automatically strips trailing slashes from requests
258 | URL before staring test HTTP request. Thus you will need to override `\Illuminate\Foundation\Testing\Concerns\MakesHttpRequests::prepareUrlForRequest()`
259 | in the way it respects trailing slashes. This can be achieved using `Illuminatech\UrlTrailingSlash\Testing\AllowsUrlTrailingSlash` trait.
260 | For example:
261 |
262 | ```php
263 |
20 | * @since 1.1.5
21 | */
22 | class CompiledRouteCollection extends BaseCompiledRouteCollection
23 | {
24 | /**
25 | * {@inheritdoc}
26 | */
27 | protected function newRoute(array $attributes)
28 | {
29 | $route = parent::newRoute($attributes);
30 |
31 | if (array_key_exists('hasTrailingSlash', $attributes)) {
32 | $route->hasTrailingSlash = $attributes['hasTrailingSlash'];
33 | }
34 |
35 | return $route;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Middleware/RedirectTrailingSlash.php:
--------------------------------------------------------------------------------
1 | withRouting(
29 | * $middleware->prependToGroup('web', Illuminatech\UrlTrailingSlash\Middleware\RedirectTrailingSlash::class); // enable automatic redirection on incorrect URL trailing slashes
30 | * // ...
31 | * )
32 | * ->withMiddleware(function (Middleware $middleware) {
33 | * // ...
34 | * })
35 | * // ...
36 | * ->create();
37 | *
38 | * $app->register(new Illuminatech\UrlTrailingSlash\RoutingServiceProvider($app)); // register trailing slashes routing
39 | *
40 | * return $app;
41 | * ```
42 | *
43 | * > Tip: there is no point to assign this middleware to the routes, which are not indexed by search engines, like API.
44 | *
45 | * @see \Illuminatech\UrlTrailingSlash\Route
46 | *
47 | * @author Paul Klimov
48 | * @since 1.0
49 | */
50 | class RedirectTrailingSlash
51 | {
52 | /**
53 | * @var \Illuminate\Contracts\Container\Container DI container instance.
54 | */
55 | protected $container;
56 |
57 | /**
58 | * Constructor.
59 | *
60 | * @param Container $container DI container instance.
61 | */
62 | public function __construct(Container $container)
63 | {
64 | $this->container = $container;
65 | }
66 |
67 | /**
68 | * Handle an incoming request, performing redirection in case URI trailing slash does not match the route.
69 | *
70 | * @param \Illuminate\Http\Request $request HTTP request.
71 | * @param \Closure $next
72 | * @return mixed response.
73 | */
74 | public function handle($request, Closure $next)
75 | {
76 | if (!in_array($request->getMethod(), ['GET', 'HEAD', 'OPTIONS'])) {
77 | return $next($request);
78 | }
79 |
80 | $currentRoute = $request->route();
81 | if (!$currentRoute instanceof Route) {
82 | return $next($request);
83 | }
84 |
85 | $pathInfo = $request->getPathInfo();
86 |
87 | if ($pathInfo === '/') {
88 | // there is no way to determine whether path info empty or equals to single slash from PHP side:
89 | // `$_SERVER['REQUEST_URI']` equals to '/' in both cases
90 | return $next($request);
91 | }
92 |
93 | if (Str::endsWith($pathInfo, '/')) {
94 | if ($currentRoute->hasTrailingSlash) {
95 | $expectedPathInfo = rtrim($pathInfo, '/') . '/';
96 | if ($expectedPathInfo !== $pathInfo) { // multiple trailing slashes, e.g. '/path/info///'
97 | $url = $this->createRedirectUrl($request, $expectedPathInfo);
98 |
99 | return $this->redirect($url);
100 | }
101 | } else {
102 | $url = $this->createRedirectUrl($request, rtrim($pathInfo, '/'));
103 |
104 | return $this->redirect($url);
105 | }
106 |
107 | return $next($request);
108 | }
109 |
110 | if ($currentRoute->hasTrailingSlash) {
111 | $url = $this->createRedirectUrl($request, $pathInfo . '/');
112 |
113 | return $this->redirect($url);
114 | }
115 |
116 | return $next($request);
117 | }
118 |
119 | /**
120 | * Creates URL for redirection from given request replacing its path with new value.
121 | *
122 | * @param \Illuminate\Http\Request $request HTTP request.
123 | * @param string $newPath new request path.
124 | * @return string generated URL.
125 | */
126 | protected function createRedirectUrl($request, $newPath): string
127 | {
128 | $url = $request->getSchemeAndHttpHost();
129 | if (($baseUrl = $request->getBaseUrl()) !== null) {
130 | $url .= $baseUrl;
131 | }
132 |
133 | $url .= $newPath;
134 |
135 | if (($queryString = $request->getQueryString()) !== null) {
136 | $url .= '?' . $queryString;
137 | }
138 |
139 | return $url;
140 | }
141 |
142 | /**
143 | * Permanently redirects browser to the new URL.
144 | *
145 | * @param string $url URL to be redirected to.
146 | * @return \Illuminate\Http\RedirectResponse response.
147 | */
148 | protected function redirect(string $url)
149 | {
150 | /* @var $redirector \Illuminate\Routing\Redirector */
151 | $redirector = $this->container->make('redirect');
152 |
153 | return $redirector->to($url, 301);
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/ResourceRegistrar.php:
--------------------------------------------------------------------------------
1 | 'index']);
28 | * Route::resource('index-plus-show', ItemController::class, ['trailingSlashOnly' => ['index', 'show']]);
29 | * Route::resource('all-except-show', ItemController::class, ['trailingSlashExcept' => 'show']);
30 | * Route::resource('all-except-update-and-delete', ItemController::class, ['trailingSlashExcept' => ['update', 'delete']]);
31 | * ```
32 | *
33 | * @see \Illuminatech\UrlTrailingSlash\Router
34 | *
35 | * @author Paul Klimov
36 | * @since 1.0
37 | */
38 | class ResourceRegistrar extends BaseResourceRegistrar
39 | {
40 | /**
41 | * {@inheritdoc}
42 | */
43 | protected function addResourceIndex($name, $base, $controller, $options)
44 | {
45 | $route = parent::addResourceIndex($name, $base, $controller, $options);
46 |
47 | $route->hasTrailingSlash = $this->hasTrailingSlash('index', $options);
48 |
49 | return $route;
50 | }
51 |
52 | /**
53 | * {@inheritdoc}
54 | */
55 | protected function addResourceCreate($name, $base, $controller, $options)
56 | {
57 | $route = parent::addResourceCreate($name, $base, $controller, $options);
58 |
59 | $route->hasTrailingSlash = $this->hasTrailingSlash('create', $options);
60 |
61 | return $route;
62 | }
63 |
64 | /**
65 | * {@inheritdoc}
66 | */
67 | protected function addResourceStore($name, $base, $controller, $options)
68 | {
69 | $route = parent::addResourceStore($name, $base, $controller, $options);
70 |
71 | $route->hasTrailingSlash = $this->hasTrailingSlash('store', $options);
72 |
73 | return $route;
74 | }
75 |
76 | /**
77 | * {@inheritdoc}
78 | */
79 | protected function addResourceShow($name, $base, $controller, $options)
80 | {
81 | $route = parent::addResourceShow($name, $base, $controller, $options);
82 |
83 | $route->hasTrailingSlash = $this->hasTrailingSlash('show', $options);
84 |
85 | return $route;
86 | }
87 |
88 | /**
89 | * {@inheritdoc}
90 | */
91 | protected function addResourceEdit($name, $base, $controller, $options)
92 | {
93 | $route = parent::addResourceEdit($name, $base, $controller, $options);
94 |
95 | $route->hasTrailingSlash = $this->hasTrailingSlash('edit', $options);
96 |
97 | return $route;
98 | }
99 |
100 | /**
101 | * {@inheritdoc}
102 | */
103 | protected function addResourceUpdate($name, $base, $controller, $options)
104 | {
105 | $route = parent::addResourceUpdate($name, $base, $controller, $options);
106 |
107 | $route->hasTrailingSlash = $this->hasTrailingSlash('update', $options);
108 |
109 | return $route;
110 | }
111 |
112 | /**
113 | * {@inheritdoc}
114 | */
115 | protected function addResourceDestroy($name, $base, $controller, $options)
116 | {
117 | $route = parent::addResourceDestroy($name, $base, $controller, $options);
118 |
119 | $route->hasTrailingSlash = $this->hasTrailingSlash('destroy', $options);
120 |
121 | return $route;
122 | }
123 |
124 | /**
125 | * Checks whether specified resource controller method URL should have a trailing slash.
126 | *
127 | * @param string $method resource controller method name.
128 | * @param array $options route options.
129 | * @return bool
130 | */
131 | protected function hasTrailingSlash($method, $options): bool
132 | {
133 | if (! empty($options['trailingSlashExcept'])) {
134 | if (in_array($method, (array) $options['trailingSlashExcept'], true)) {
135 | return false;
136 | }
137 |
138 | if (empty($options['trailingSlashOnly'])) {
139 | return true;
140 | }
141 | }
142 |
143 | if (! empty($options['trailingSlashOnly']) && in_array($method, (array) $options['trailingSlashOnly'], true)) {
144 | return true;
145 | }
146 |
147 | return false;
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/Route.php:
--------------------------------------------------------------------------------
1 | name('items.index'); // enforce trailing slash
29 | * Route::get('items/{item}', ItemController::class.'@show')->name('items.show'); // enforce no trailing slash
30 | * ```
31 | *
32 | * @see \Illuminatech\UrlTrailingSlash\Router
33 | * @see \Illuminatech\UrlTrailingSlash\Middleware\RedirectTrailingSlash
34 | *
35 | * @author Paul Klimov
36 | * @since 1.0
37 | */
38 | class Route extends BaseRoute
39 | {
40 | /**
41 | * @var bool whether route URI ends with slash or not.
42 | */
43 | public $hasTrailingSlash = false;
44 |
45 | /**
46 | * {@inheritdoc}
47 | */
48 | public function __construct($methods, $uri, $action)
49 | {
50 | $this->hasTrailingSlash = Str::endsWith($uri, '/');
51 | $uri = trim($uri, '/');
52 |
53 | parent::__construct($methods, $uri, $action);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/RouteCollection.php:
--------------------------------------------------------------------------------
1 |
20 | * @since 1.1.5
21 | */
22 | class RouteCollection extends BaseRouteCollection
23 | {
24 | /**
25 | * {@inheritdoc}
26 | */
27 | public function compile()
28 | {
29 | $compiled = parent::compile();
30 |
31 | foreach ($this->getRoutes() as $route) {
32 | if (! $route instanceof Route) {
33 | continue;
34 | }
35 |
36 | $compiled['attributes'][$route->getName()]['hasTrailingSlash'] = $route->hasTrailingSlash;
37 | }
38 |
39 | return $compiled;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Router.php:
--------------------------------------------------------------------------------
1 |
23 | * @since 1.0
24 | */
25 | class Router extends BaseRouter
26 | {
27 | /**
28 | * {@inheritdoc}
29 | */
30 | public function __construct(Dispatcher $events, ?Container $container = null)
31 | {
32 | parent::__construct($events, $container);
33 |
34 | $this->routes = new RouteCollection;
35 | }
36 |
37 | /**
38 | * {@inheritdoc}
39 | */
40 | protected function prefix($uri): string
41 | {
42 | $result = parent::prefix($uri);
43 |
44 | if (Str::endsWith($uri, '/')) {
45 | return $result.'/';
46 | }
47 |
48 | return $result;
49 | }
50 |
51 | /**
52 | * {@inheritdoc}
53 | */
54 | public function newRoute($methods, $uri, $action)
55 | {
56 | return (new Route($methods, $uri, $action))
57 | ->setRouter($this)
58 | ->setContainer($this->container);
59 | }
60 |
61 | /**
62 | * {@inheritdoc}
63 | */
64 | public function resource($name, $controller, array $options = [])
65 | {
66 | if (Str::endsWith($name, '/')) {
67 | $name = rtrim($name, "/ \t\n\r\0\x0B");
68 | if (! isset($options['trailingSlashOnly'])) {
69 | $options['trailingSlashOnly'] = ['index', 'create', 'store', 'show', 'edit', 'update', 'destroy'];
70 | }
71 | }
72 |
73 | if (empty($options['trailingSlashOnly']) && empty($options['trailingSlashExcept'])) {
74 | return parent::resource($name, $controller, $options);
75 | }
76 |
77 | if ($this->container && $this->container->bound(BaseResourceRegistrar::class)) {
78 | $registrar = $this->container->make(BaseResourceRegistrar::class);
79 | } else {
80 | $registrar = new ResourceRegistrar($this);
81 | }
82 |
83 | return new PendingResourceRegistration(
84 | $registrar, $name, $controller, $options
85 | );
86 | }
87 |
88 | /**
89 | * {@inheritdoc}
90 | */
91 | public function setCompiledRoutes(array $routes)
92 | {
93 | $this->routes = (new CompiledRouteCollection($routes['compiled'], $routes['attributes']))
94 | ->setRouter($this)
95 | ->setContainer($this->container);
96 |
97 | $this->container->instance('routes', $this->routes);
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/RoutingServiceProvider.php:
--------------------------------------------------------------------------------
1 | withRouting(
28 | * // ...
29 | * )
30 | * ->withMiddleware(function (Middleware $middleware) {
31 | * // ...
32 | * })
33 | * // ...
34 | * ->create();
35 | *
36 | * $app->register(new Illuminatech\UrlTrailingSlash\RoutingServiceProvider($app)); // register trailing slashes routing
37 | *
38 | * return $app;
39 | * ```
40 | *
41 | * Registering this provided in normal way will have no effect, since it alters the router, which is bound to the HTTP kernel
42 | * instance at constructor level.
43 | *
44 | * @see \Illuminate\Routing\RoutingServiceProvider
45 | *
46 | * @author Paul Klimov
47 | * @since 1.0
48 | */
49 | class RoutingServiceProvider extends ServiceProvider
50 | {
51 | /**
52 | * {@inheritdoc}
53 | */
54 | public function register(): void
55 | {
56 | $this->app->singleton('router', function (Container $app) {
57 | return new Router($app->make('events'), $app);
58 | });
59 |
60 | $this->app->extend('url', function (\Illuminate\Routing\UrlGenerator $urlGenerator) {
61 | $newUrlGenerator = new UrlGenerator(
62 | $this->app->make('router')->getRoutes(),
63 | $urlGenerator->getRequest(),
64 | $this->app->make('config')->get('app.asset_url')
65 | );
66 |
67 | $newUrlGenerator->setSessionResolver(function () {
68 | return $this->app->has('session') ? $this->app->make('session') : null;
69 | });
70 |
71 | $newUrlGenerator->setKeyResolver(function () {
72 | return $this->app->make('config')->get('app.key');
73 | });
74 |
75 | return $newUrlGenerator;
76 | });
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Testing/AllowsUrlTrailingSlash.php:
--------------------------------------------------------------------------------
1 |
34 | * @since 1.0
35 | */
36 | trait AllowsUrlTrailingSlash
37 | {
38 | /**
39 | * {@inheritdoc}
40 | */
41 | protected function prepareUrlForRequest($uri)
42 | {
43 | $result = parent::prepareUrlForRequest($uri);
44 |
45 | if (Str::endsWith($uri, '/')) {
46 | $result .= '/';
47 | }
48 |
49 | return $result;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/UrlGenerator.php:
--------------------------------------------------------------------------------
1 |
19 | * @since 1.0
20 | */
21 | class UrlGenerator extends BaseUrlGenerator
22 | {
23 | /**
24 | * {@inheritdoc}
25 | */
26 | public function format($root, $path, $route = null): string
27 | {
28 | $url = parent::format($root, $path, $route);
29 |
30 | if ($route === null) {
31 | if (Str::endsWith($path, '/')) {
32 | return $url.'/';
33 | }
34 |
35 | return $url;
36 | }
37 |
38 | if ($route instanceof Route && $route->hasTrailingSlash) {
39 | return $url.'/';
40 | }
41 |
42 | return $url;
43 | }
44 |
45 | /**
46 | * {@inheritdoc}
47 | */
48 | public function to($path, $extra = [], $secure = null)
49 | {
50 | if ($this->isValidUrl($path)) {
51 | return $path;
52 | }
53 |
54 | $url = parent::to($path, $extra, $secure);
55 |
56 | if (Str::endsWith($path, '/')) {
57 | return $url.'/';
58 | }
59 |
60 | return $url;
61 | }
62 |
63 | /**
64 | * {@inheritdoc}
65 | */
66 | public function full()
67 | {
68 | $url = parent::full();
69 |
70 | $pathInfo = $this->request->getPathInfo();
71 | if (Str::endsWith($pathInfo, '/')) {
72 | if (strpos($url, '?') === false) {
73 | if (! Str::endsWith($url, '/')) {
74 | return $url.'/';
75 | }
76 | } elseif (strpos($url, '/?') === false) {
77 | return str_replace('?', '/?', $url);
78 | }
79 | }
80 |
81 | return $url;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------