├── .docker └── app │ └── Dockerfile ├── .gitignore ├── .travis.yml ├── LICENCE ├── README.md ├── composer.json ├── docker-compose.yml ├── phpunit.xml ├── src ├── database │ └── migrations │ │ └── 0000_00_00_000000_create_redirect_rules_table.php └── package │ ├── DbRedirectorRouter.php │ ├── Http │ └── Middleware │ │ └── DbRedirectorMiddleware.php │ ├── Models │ └── RedirectRule.php │ └── Providers │ └── DbRedirectorServiceProvider.php └── tests ├── Integration ├── RedirectRuleModelTest.php └── RoutingViaDbRedirectorTest.php └── TestCase.php /.docker/app/Dockerfile: -------------------------------------------------------------------------------- 1 | # Native image 2 | FROM php:7.2-cli 3 | 4 | # Enable debuging 5 | RUN mv /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini 6 | 7 | # Set default workdir 8 | WORKDIR /var/www/html 9 | 10 | # Keep spinning 11 | CMD ["tail", "-f", "/dev/null"] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # OS generated files and junk 3 | # 4 | 5 | .DS_Store 6 | .DS_Store? 7 | ._* 8 | Thumbs.db 9 | Icon? 10 | .Trashes 11 | ehthumbs.db 12 | *.log 13 | 14 | # 15 | # PhpStorm 16 | # 17 | 18 | .idea 19 | 20 | # 21 | # Project 22 | # 23 | 24 | vendor 25 | composer.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | - 7.2 6 | 7 | before_script: 8 | - composer self-update 9 | - composer install --prefer-source --no-interaction 10 | - composer dump-autoload 11 | 12 | script: 13 | - vendor/bin/phpunit -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Vladimir Ković 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel DB redirector 2 | 3 | [![Build](https://travis-ci.org/vkovic/laravel-db-redirector.svg?branch=master)](https://travis-ci.org/vkovic/laravel-db-redirector) 4 | [![Downloads](https://poser.pugx.org/vkovic/laravel-db-redirector/downloads)](https://packagist.org/packages/vkovic/laravel-db-redirector) 5 | [![Stable](https://poser.pugx.org/vkovic/laravel-db-redirector/v/stable)](https://packagist.org/packages/vkovic/laravel-db-redirector) 6 | [![License](https://poser.pugx.org/vkovic/laravel-db-redirector/license)](https://packagist.org/packages/vkovic/laravel-db-redirector) 7 | 8 | ### Manage HTTP redirections in Laravel using database 9 | 10 | Manage redirects using database rules. Rules are intended to be very similar to Laravel default routes, so syntax is pretty easy 11 | to comprehend. 12 | 13 | --- 14 | 15 | ## Compatibility 16 | 17 | The package is compatible with Laravel versions `5.5`, `5.6`, `5.7` and `5.8` and PHP versions `7.1` and `7.2` 18 | 19 | ## Installation 20 | 21 | Install the package via composer: 22 | 23 | ```bash 24 | composer require vkovic/laravel-db-redirector 25 | ``` 26 | 27 | Database redirector middleware needs to be added to middleware array: 28 | 29 | ```php 30 | // File: app/Http/Kernel.php 31 | 32 | // ... 33 | 34 | protected $middleware = [ 35 | // ... 36 | \Vkovic\LaravelDbRedirector\Http\Middleware\DbRedirectorMiddleware::class 37 | ]; 38 | ``` 39 | 40 | Run migrations to create table which will store redirect rules: 41 | 42 | ```bash 43 | php artisan migrate 44 | ``` 45 | 46 | ## Usage: Simple Examples 47 | 48 | Creating a redirect is easy. You just have to add db record via provided RedirectRule model. 49 | Default status code for redirections will be 301 (Moved Permanently). 50 | 51 | ```php 52 | use Vkovic\LaravelDbRedirector\Models\RedirectRule; 53 | 54 | // ... 55 | 56 | RedirectRule::create([ 57 | 'origin' => '/one/two', 58 | 'destination' => '/three' 59 | ]); 60 | ``` 61 | 62 | You can also specify another redirection status code: 63 | 64 | ```php 65 | RedirectRule::create([ 66 | 'origin' => '/one/two', 67 | 'destination' => '/three', 68 | 'status_code' => 307 // Temporary Redirect 69 | ]); 70 | ``` 71 | 72 | You may use route parameters just like in native Laravel routes, 73 | they'll be passed down the road - from origin to destination: 74 | 75 | ```php 76 | RedirectRule::create([ 77 | 'origin' => '/one/{param}', 78 | 'destination' => '/two/{param}' 79 | ]); 80 | 81 | // If we visit: "/one/foo" we will end up at "two/foo" 82 | ``` 83 | 84 | Optional parameters can also be used: 85 | 86 | ```php 87 | RedirectRule::create([ 88 | 'origin' => '/one/{param1?}/{param2?}', 89 | 'destination' => '/four/{param1}/{param2}' 90 | ]); 91 | 92 | // If we visit: "/one" we'll end up at "/four 93 | // If we visit: "/one/two" we'll end up at "/four/two" 94 | // If we visit: "/one/two/three" we'll end up at "/four/two/three" 95 | ``` 96 | 97 | Chained redirects will also work: 98 | 99 | ```php 100 | RedirectRule::create([ 101 | 'origin' => '/one', 102 | 'destination' => '/two' 103 | ]); 104 | 105 | RedirectRule::create([ 106 | 'origin' => '/two', 107 | 'destination' => '/three' 108 | ]); 109 | 110 | RedirectRule::create([ 111 | 'origin' => '/three', 112 | 'destination' => '/four' 113 | ]); 114 | 115 | // If we visit: "/one" we'll end up at "/four" 116 | ``` 117 | 118 | We also can delete the whole chain at once 119 | (3 previous redirect records in this example): 120 | 121 | ```php 122 | RedirectRule::deleteChainedRecursively('/four'); 123 | ``` 124 | 125 | Sometimes it's possible that you'll have more than one redirection with 126 | the same destination. So it's smart to surround code with try catch block, because exception 127 | will be raised in this case: 128 | 129 | ```php 130 | RedirectRule::create(['origin' => '/one/two', 'destination' => '/three/four']); 131 | RedirectRule::create(['origin' => '/three/four', 'destination' => '/five/six']); 132 | 133 | // One more with same destination ("/five/six") as the previous one. 134 | RedirectRule::create(['origin' => '/ten/eleven', 'destination' => '/five/six']); 135 | 136 | try { 137 | RedirectRule::deleteChainedRecursively('five/six'); 138 | } catch (\Exception $e) { 139 | // ... handle exception 140 | } 141 | ``` 142 | 143 | ## Usage: Advanced 144 | 145 | What about order of rules execution when given url corresponds to multiple rules. 146 | Let's find out in this simple example: 147 | 148 | ```php 149 | RedirectRule::create(['origin' => '/one/{param}/three', 'destination' => '/four']); 150 | RedirectRule::create(['origin' => '/{param}/two/three', 'destination' => '/five']); 151 | 152 | // If we visit: "/one/two/three" it corresponds to both of rules above, 153 | // so, where should we end up: "/four" or "/five" ? 154 | // ... 155 | // It does not have anything to do with rule order in our rules table! 156 | ``` 157 | 158 | To solve this problem, we need to agree on simple (and logical) rule prioritizing: 159 | 160 | **Priority 1:** 161 | Rules without named parameters have top priority: 162 | 163 | **Priority 2:** 164 | If rule origin have named parameters, those with less named parameters will have higher priority 165 | 166 | **Priority 3:** 167 | If rule origin have same number of named parameters, those where named parameters are nearer the 168 | end of the rule string will have priority 169 | 170 | So lets examine our previous case, we have: 171 | - "/one/{param}/three" => "/four" 172 | - "/{param}/two/three" => "/five" 173 | 174 | In this case both rules have the same number of named params, but in the first rule "{param}" is 175 | nearer the end of the rule, so it will have priority and we'll end up at "/four". 176 | 177 | --- 178 | 179 | ## Contributing 180 | 181 | If you plan to modify this Laravel package you should run tests that comes with it. 182 | Easiest way to accomplish this would be with `Docker`, `docker-compose` and `phpunit`. 183 | 184 | First, we need to initialize Docker containers: 185 | 186 | ```bash 187 | docker-compose up -d 188 | ``` 189 | 190 | After that, we can run tests and watch the output: 191 | 192 | ```bash 193 | docker-compose exec app vendor/bin/phpunit 194 | ``` 195 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vkovic/laravel-db-redirector", 3 | "description": "Persist and manage HTTP redirections in Laravel", 4 | "keywords": [ 5 | "laravel", 6 | "redirect", 7 | "database" 8 | ], 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Vladimir Ković", 13 | "email": "vlada.kovic@gmail.com" 14 | } 15 | ], 16 | "autoload": { 17 | "psr-4": { 18 | "Vkovic\\LaravelDbRedirector\\": "src/package" 19 | } 20 | }, 21 | "autoload-dev": { 22 | "psr-4": { 23 | "Vkovic\\LaravelDbRedirector\\Test\\": "tests" 24 | } 25 | }, 26 | "require": { 27 | "php": "^7.1" 28 | }, 29 | "require-dev": { 30 | "laravel/framework": "5.5.*|5.6.*|5.7.*|5.8.*", 31 | "orchestra/testbench": "3.5.*|3.6.*|3.7.*|3.8.*", 32 | "orchestra/database": "3.5.*|3.6.*|3.7.*|3.8.*", 33 | "phpunit/phpunit": "^6.3|^7.0" 34 | }, 35 | "scripts": { 36 | "test": "vendor/bin/phpunit" 37 | }, 38 | "extra": { 39 | "laravel": { 40 | "providers": [ 41 | "Vkovic\\LaravelDbRedirector\\Providers\\DbRedirectorServiceProvider" 42 | ] 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | app: 5 | build: 6 | context: . 7 | dockerfile: .docker/app/Dockerfile 8 | volumes: 9 | - ./:/var/www/html/ 10 | 11 | composer: 12 | image: composer:1.8.3 13 | depends_on: 14 | - app 15 | volumes: 16 | - .:/app 17 | command: install --ignore-platform-reqs --no-scripts -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/database/migrations/0000_00_00_000000_create_redirect_rules_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('origin', 512)->unique(); 19 | $table->text('destination'); 20 | $table->unsignedSmallInteger('status_code')->default(301); 21 | 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('redirect_rules'); 34 | } 35 | } -------------------------------------------------------------------------------- /src/package/DbRedirectorRouter.php: -------------------------------------------------------------------------------- 1 | router = $router; 27 | } 28 | 29 | /** 30 | * @param Request $request 31 | * 32 | * @return Response|null 33 | */ 34 | public function getRedirectFor(Request $request) 35 | { 36 | $potentialRules = $this->getPotentialRules($request->path()); 37 | 38 | // Make route for each of potential rules and let Laravel handle the rest 39 | $potentialRules->each(function ($redirect) { 40 | $this->router->get($redirect->origin, function () use ($redirect) { 41 | $destination = $this->resolveDestination($redirect->destination); 42 | 43 | return redirect($destination, $redirect->status_code); 44 | }); 45 | }); 46 | 47 | // If one of the routes could be dispatched it means 48 | // we have a match in database and we can continue with redirection 49 | // (from callback above) 50 | try { 51 | return $this->router->dispatch($request); 52 | } catch (Exception $e) { 53 | return null; 54 | } 55 | } 56 | 57 | /** 58 | * Get potential rules based on requested URI 59 | * 60 | * @param string $uri 61 | * 62 | * @return Collection 63 | */ 64 | public function getPotentialRules($uri) 65 | { 66 | // 67 | // Try to match uri with rule without route params 68 | // 69 | 70 | $redirectRules = RedirectRule::where('origin', $uri)->get(); 71 | 72 | if ($redirectRules->isNotEmpty()) { 73 | return $redirectRules; 74 | } 75 | 76 | // 77 | // Try to match uri with rule with url params: 78 | // 79 | 80 | // Search only rules with params but without optional params 81 | $query = RedirectRule::where('origin', 'LIKE', '%{%') 82 | ->where('origin', 'NOT LIKE', '%?}%'); 83 | 84 | // Narrow potential matches by matching number of url segments 85 | // (by matching number of slashes) 86 | $slashesCount = substr_count($uri, '/'); 87 | $rawWhere = \DB::raw("LENGTH(origin) - LENGTH(REPLACE(origin, '/', ''))"); 88 | $query = $query->where($rawWhere, $slashesCount); 89 | 90 | // Ordering 91 | $query 92 | // Route with lesser number of parameters will have top priority 93 | ->orderByRaw("LENGTH(origin) - LENGTH(REPLACE(origin, '{', ''))") 94 | // Rules with params nearer end of route will have last priority 95 | ->orderByRaw("INSTR(origin, '{') DESC"); 96 | 97 | // Get collection of potential rules 98 | $potentialRules = $query->get(); 99 | 100 | if ($potentialRules->isNotEmpty()) { 101 | return $potentialRules; 102 | } 103 | 104 | // 105 | // Try to match uri with rule with optional url params: 106 | // 107 | 108 | // Search only rules with optional params 109 | $query = RedirectRule::where('origin', 'LIKE', '%?}%'); 110 | 111 | // Ordering 112 | $query 113 | // Route with less segments will have top priority 114 | ->orderByRaw("LENGTH(origin) - LENGTH(REPLACE(origin, '/', ''))") 115 | // Route with lesser number of parameters will have next priority 116 | ->orderByRaw("LENGTH(origin) - LENGTH(REPLACE(origin, '{', ''))") 117 | // Rules with params nearer end of route will have last priority 118 | ->orderByRaw("INSTR(origin, '{') DESC"); 119 | 120 | return $query->get(); 121 | } 122 | 123 | /** 124 | * Resolve destination by replacing parameters from 125 | * current route into destination rule 126 | * 127 | * @param string $destination 128 | * 129 | * @return mixed 130 | */ 131 | protected function resolveDestination($destination) 132 | { 133 | foreach ($this->router->getCurrentRoute()->parameters() as $key => $value) { 134 | $destination = str_replace("{{$key}}", $value, $destination); 135 | } 136 | 137 | // Remove non existent optional params from destination 138 | // but after existent params has been resolved 139 | $destination = preg_replace('/\/{[\w-_]+}/', '', $destination); 140 | 141 | return $destination; 142 | } 143 | } -------------------------------------------------------------------------------- /src/package/Http/Middleware/DbRedirectorMiddleware.php: -------------------------------------------------------------------------------- 1 | getRedirectFor($request); 21 | 22 | return $redirectResponse ?? $next($request); 23 | } 24 | } -------------------------------------------------------------------------------- /src/package/Models/RedirectRule.php: -------------------------------------------------------------------------------- 1 | 'datetime', 11 | 'status_code' => 'integer', 12 | 'hits' => 'integer' 13 | ]; 14 | 15 | protected $guarded = []; 16 | 17 | /** 18 | * Setter for the "status" attribute 19 | * 20 | * @param $value 21 | * 22 | * @throws \Exception 23 | */ 24 | public function setStatusCodeAttribute($value) 25 | { 26 | $this->attributes['status_code'] = $value ?? 301; 27 | } 28 | 29 | /** 30 | * Setter for the "origin" attribute 31 | * 32 | * @param $value 33 | */ 34 | public function setOriginAttribute($value) 35 | { 36 | $this->attributes['origin'] = mb_strtolower(trim($value, '/')); 37 | } 38 | 39 | /** 40 | * Setter for the "destination" attribute 41 | * 42 | * @param $value 43 | */ 44 | public function setDestinationAttribute($value) 45 | { 46 | $this->attributes['destination'] = mb_strtolower(trim($value, '/')); 47 | } 48 | 49 | /** 50 | * Delete chained redirect with recursive delete 51 | * 52 | * @param $destination 53 | * 54 | * @throws \Exception When there is multiple rules with same destination 55 | * exception will be raised 56 | * 57 | * @return void 58 | */ 59 | public static function deleteChainedRecursively($destination) 60 | { 61 | $destination = mb_strtolower(trim($destination, '/')); 62 | 63 | $redirectRules = RedirectRule::where('destination', $destination)->get(); 64 | 65 | if ($redirectRules->count() > 1) { 66 | $message = 'There is multiple redirections with the same destination! '; 67 | $message .= 'Recursive delete will not continue'; 68 | 69 | throw new \Exception($message); 70 | } 71 | 72 | $redirectRule = $redirectRules->first(); 73 | 74 | if ($redirectRule === null) { 75 | return; 76 | } 77 | 78 | $nextDestination = $redirectRule->origin; 79 | 80 | $redirectRule->delete(); 81 | 82 | self::deleteChainedRecursively($nextDestination); 83 | } 84 | } -------------------------------------------------------------------------------- /src/package/Providers/DbRedirectorServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom(__DIR__ . '/../../database/migrations'); 19 | 20 | $this->app->bind(DbRedirectorRouter::class, function () { 21 | $router = new Router($this->app['events']); 22 | 23 | return new DbRedirectorRouter($router); 24 | }); 25 | } 26 | 27 | /** 28 | * Register the application services. 29 | * 30 | * @return void 31 | */ 32 | public function register() 33 | { 34 | // 35 | } 36 | } -------------------------------------------------------------------------------- /tests/Integration/RedirectRuleModelTest.php: -------------------------------------------------------------------------------- 1 | origin = '/eleven'; 14 | $redirectRule->destination = '/twelve'; 15 | $redirectRule->status_code = null; 16 | $redirectRule->save(); 17 | 18 | $redirectRule = RedirectRule::find($redirectRule->id); 19 | 20 | $this->assertEquals(301, $redirectRule->status_code); 21 | 22 | $redirectRule->delete(); 23 | } 24 | 25 | public function test_recursive_rule_deletion() 26 | { 27 | RedirectRule::create([ 28 | 'origin' => '/one', 29 | 'destination' => '/two' 30 | ]); 31 | 32 | RedirectRule::create([ 33 | 'origin' => '/two', 34 | 'destination' => '/three' 35 | ]); 36 | 37 | RedirectRule::create([ 38 | 'origin' => '/three', 39 | 'destination' => '/four' 40 | ]); 41 | 42 | RedirectRule::deleteChainedRecursively('/four'); 43 | 44 | $this->assertEmpty(RedirectRule::all()); 45 | 46 | RedirectRule::truncate(); 47 | } 48 | 49 | public function test_recursive_rule_delete_will_raise_exception_when_multiple_record_for_same_destination_exists() 50 | { 51 | $this->expectExceptionMessage('There is multiple redirections with the same destination'); 52 | 53 | RedirectRule::create([ 54 | 'origin' => '/one', 55 | 'destination' => '/ten' 56 | ]); 57 | 58 | RedirectRule::create([ 59 | 'origin' => '/two', 60 | 'destination' => '/ten' 61 | ]); 62 | 63 | RedirectRule::deleteChainedRecursively('/ten'); 64 | 65 | RedirectRule::truncate(); 66 | } 67 | 68 | public function test_origin_and_destination_are_converted_to_lowercase() 69 | { 70 | $origin = 'ONE/TWO/THREE'; 71 | $destination = 'FOUR/FIVE/SEVEN'; 72 | 73 | $redirectRule = RedirectRule::create([ 74 | 'origin' => $origin, 75 | 'destination' => $destination 76 | ]); 77 | 78 | 79 | $redirectRule = RedirectRule::find($redirectRule->id); 80 | 81 | $this->assertEquals(mb_strtolower($origin), $redirectRule->origin); 82 | $this->assertEquals(mb_strtolower($destination), $redirectRule->destination); 83 | 84 | $redirectRule->delete(); 85 | } 86 | 87 | public function test_origin_and_destination_do_not_starts_or_ends_with_slash() 88 | { 89 | $origin = '/one/two/'; 90 | $destination = '/three/four/'; 91 | 92 | $redirectRule = RedirectRule::create([ 93 | 'origin' => $origin, 94 | 'destination' => $destination 95 | ]); 96 | 97 | $redirectRule = RedirectRule::find($redirectRule->id); 98 | 99 | $this->assertFalse(starts_with($redirectRule->origin, '/')); 100 | $this->assertFalse(ends_with($redirectRule->destination, '/')); 101 | 102 | $redirectRule->delete(); 103 | } 104 | } -------------------------------------------------------------------------------- /tests/Integration/RoutingViaDbRedirectorTest.php: -------------------------------------------------------------------------------- 1 | '/one', 14 | 'destination' => '/two' 15 | ]); 16 | 17 | $this->get('/one') 18 | ->assertRedirect('/two') 19 | ->assertStatus(301); 20 | 21 | $redirectRule->delete(); 22 | } 23 | 24 | public function test_router_misses_non_matching_similar_rule() 25 | { 26 | $rules = [ 27 | 'one/two/{c}' => 'a', 28 | '{a}/two/{c}' => 'b', 29 | '{a}/two/{c?}' => 'c', 30 | ]; 31 | 32 | foreach ($rules as $origin => $destination) { 33 | RedirectRule::create([ 34 | 'origin' => $origin, 35 | 'destination' => $destination 36 | ]); 37 | } 38 | 39 | $this->get('/one/two_/X')->assertStatus(404); 40 | $this->get('/X/tw_o/Y')->assertStatus(404); 41 | $this->get('/X/tw_o/Y/z')->assertStatus(404); 42 | 43 | RedirectRule::truncate(); 44 | } 45 | 46 | public function test_non_default_redirect_status_code() 47 | { 48 | $redirectRule = RedirectRule::create([ 49 | 'origin' => '/one', 50 | 'destination' => '/two', 51 | 'status_code' => 302 52 | ]); 53 | 54 | $this->get('/one') 55 | ->assertRedirect('/two') 56 | ->assertStatus(302); 57 | 58 | $redirectRule->delete(); 59 | } 60 | 61 | public function test_route_can_use_single_named_param() 62 | { 63 | $redirectRule = RedirectRule::create([ 64 | 'origin' => '/one/{a}/two', 65 | 'destination' => '/three/{a}' 66 | ]); 67 | 68 | $this->get('/one/X/two') 69 | ->assertRedirect('/three/X'); 70 | 71 | $redirectRule->delete(); 72 | } 73 | 74 | public function test_route_can_use_multiple_named_params() 75 | { 76 | $redirectRule = RedirectRule::create([ 77 | 'origin' => '/one/{a}/{b}/two/{c}', 78 | 'destination' => '/{c}/{b}/{a}/three' 79 | ]); 80 | 81 | $this->get('/one/X/Y/two/Z') 82 | ->assertRedirect('/Z/Y/X/three'); 83 | 84 | $redirectRule->delete(); 85 | } 86 | 87 | public function test_route_can_use_multiple_named_params_in_one_segment() 88 | { 89 | $redirectRule = RedirectRule::create([ 90 | 'origin' => '/one/two/{a}-{b}/{c}', 91 | 'destination' => '/three/{a}/four/{b}/{a}-{c}' 92 | ]); 93 | 94 | $this->get('/one/two/X-Y/Z') 95 | ->assertRedirect('/three/X/four/Y/X-Z'); 96 | 97 | $redirectRule->delete(); 98 | } 99 | 100 | public function test_route_can_use_optional_named_parameters() 101 | { 102 | $redirectRule = RedirectRule::create([ 103 | 'origin' => '/one/{a?}/{b?}', 104 | 'destination' => '/two/{a}/{b}' 105 | ]); 106 | 107 | $this->get('/one/X')->assertRedirect('/two/X'); 108 | $this->get('/one/X/Y')->assertRedirect('/two/X/Y'); 109 | $this->get('/one')->assertRedirect('/two'); 110 | 111 | $redirectRule->delete(); 112 | } 113 | 114 | public function test_router_can_perform_chained_redirects() 115 | { 116 | RedirectRule::create([ 117 | 'origin' => '/one', 118 | 'destination' => '/two' 119 | ]); 120 | 121 | RedirectRule::create([ 122 | 'origin' => '/two', 123 | 'destination' => '/three' 124 | ]); 125 | 126 | // TODO.IMPROVE 127 | // This is actually working but i'm not sure how to test 128 | // chained redirects. For now we'll test one by one. 129 | 130 | $this->get('/one')->assertRedirect('/two'); 131 | $this->get('/two')->assertRedirect('/three'); 132 | 133 | RedirectRule::truncate(); 134 | } 135 | 136 | public function test_router_matches_order_for_rules_with_named_params() 137 | { 138 | // Rules in this array are ordered like the logic 139 | // in router works - we'll shuffle them later, just in case 140 | $rules = [ 141 | // 3 segments 142 | 'one/two/{c}' => 'a', 143 | '{a}/two/{c}' => 'b', 144 | // 4 segments 145 | 'one/two/{d}/{e}' => 'c', 146 | 'one/{b}/three/{d}' => 'd', 147 | 'one/{b}/{c}/{d}' => 'e', 148 | // 6 segments 149 | 'one/{b}/three/{d}/five/{f}' => 'f', 150 | 'one/two/{c}/{d}/{e}/{f}' => 'g', 151 | 'one/{b}/three/{d}/{e}/{f}' => 'h', 152 | // 7 segments 153 | 'one/two/three/four/five/six/{g}' => 'i', 154 | '{a}/two/three/four/five/six/{g}' => 'j', 155 | ]; 156 | 157 | // Shuffle routes to avoid coincidentally 158 | // matching (by order in database) 159 | uksort($rules, function () { 160 | return rand() > rand(); 161 | }); 162 | 163 | foreach ($rules as $origin => $destination) { 164 | RedirectRule::create([ 165 | 'origin' => $origin, 166 | 'destination' => $destination 167 | ]); 168 | } 169 | 170 | // 3 segments 171 | $this->get('one/two/X')->assertRedirect('/a'); 172 | $this->get('X/two/Y')->assertRedirect('/b'); 173 | 174 | // 4 segments 175 | $this->get('one/two/X/Y')->assertRedirect('/c'); 176 | $this->get('one/X/three/Y')->assertRedirect('/d'); 177 | $this->get('one/X/Y/Z')->assertRedirect('/e'); 178 | 179 | // 6 segments 180 | $this->get('one/X/three/Y/five/Z')->assertRedirect('/f'); 181 | $this->get('one/two/X/Y/Z/K')->assertRedirect('/g'); 182 | $this->get('one/X/three/Y/Z/K')->assertRedirect('/h'); 183 | 184 | // 7 segments 185 | $this->get('one/two/three/four/five/six/X')->assertRedirect('/i'); 186 | $this->get('X/two/three/four/five/six/Y')->assertRedirect('/j'); 187 | 188 | RedirectRule::truncate(); 189 | } 190 | 191 | public function test_router_matches_order_for_rules_with_optional_named_params() 192 | { 193 | // Rules in this array are ordered like the logic 194 | // in router works - we'll shuffle them later, just in case 195 | $rules = [ 196 | // 3 segments 197 | 'one/two/{c?}' => 'a', 198 | '{a}/two/{c?}' => 'b', 199 | // 6 segments 200 | 'one/{b}/three/{d?}/five/{f?}' => 'c', 201 | 'one/two/{c}/{d?}/{e?}/{f?}' => 'd', 202 | 'one/{b}/three/{d}/{e?}/{f?}' => 'e', 203 | ]; 204 | 205 | // Shuffle routes to avoid accidentally 206 | // matching (by order in database) 207 | uksort($rules, function () { 208 | return rand() > rand(); 209 | }); 210 | 211 | foreach ($rules as $origin => $destination) { 212 | RedirectRule::create([ 213 | 'origin' => $origin, 214 | 'destination' => $destination 215 | ]); 216 | } 217 | 218 | // 3 segments 219 | $this->get('one/two')->assertRedirect('/a'); 220 | $this->get('one/two/X')->assertRedirect('/a'); 221 | 222 | $this->get('X/two')->assertRedirect('/b'); 223 | $this->get('X/two/Y')->assertRedirect('/b'); 224 | 225 | // 6 segments 226 | $this->get('one/X/three/Y/five')->assertRedirect('/c'); 227 | $this->get('one/X/three/Y/five/X')->assertRedirect('/c'); 228 | 229 | $this->get('one/two/X/Y')->assertRedirect('/d'); 230 | $this->get('one/two/X/Y/X/K')->assertRedirect('/d'); 231 | 232 | $this->get('one/X/three/Y/Z')->assertRedirect('/e'); 233 | $this->get('one/X/three/Y/Z/K')->assertRedirect('/e'); 234 | 235 | RedirectRule::truncate(); 236 | } 237 | } -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | packageMigrations(); 24 | } 25 | 26 | /** 27 | * Run default package migrations 28 | * 29 | * @return void 30 | */ 31 | protected function packageMigrations() 32 | { 33 | $this->artisan('migrate'); 34 | } 35 | 36 | /** 37 | * Get package providers 38 | * 39 | * @param \Illuminate\Foundation\Application $app 40 | * 41 | * @return array 42 | */ 43 | protected function getPackageProviders($app) 44 | { 45 | return [DbRedirectorServiceProvider::class]; 46 | } 47 | 48 | /** 49 | * Define environment setup 50 | * 51 | * @param Application $app 52 | * 53 | * @return void 54 | */ 55 | protected function getEnvironmentSetUp($app) 56 | { 57 | $app['config']->set('database.default', 'testbench'); 58 | $app['config']->set('database.connections.testbench', [ 59 | 'driver' => 'sqlite', 60 | 'database' => ':memory:', 61 | ]); 62 | 63 | $app->make(Kernel::class)->pushMiddleware(DbRedirectorMiddleware::class); 64 | } 65 | } --------------------------------------------------------------------------------