├── 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 | [![Latest Stable Version](https://img.shields.io/packagist/v/illuminatech/url-trailing-slash.svg)](https://packagist.org/packages/illuminatech/url-trailing-slash) 14 | [![Total Downloads](https://img.shields.io/packagist/dt/illuminatech/url-trailing-slash.svg)](https://packagist.org/packages/illuminatech/url-trailing-slash) 15 | [![Build Status](https://github.com/illuminatech/url-trailing-slash/workflows/build/badge.svg)](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 | --------------------------------------------------------------------------------