├── CHANGELOG.md ├── src ├── Contracts │ ├── Redirector.php │ └── RedirectionModelContract.php ├── Commands │ └── RedirectionListCommand.php ├── Exceptions │ └── RedirectionException.php ├── Redirect.php ├── Drivers │ ├── DatabaseRedirector.php │ └── FileRedirector.php ├── RedirectionServiceProvider.php ├── RedirectRequests.php └── Models │ └── Redirection.php ├── database └── migrations │ └── create_redirections_table.php.stub ├── LICENSE.md ├── .php_cs.dist.php ├── composer.json ├── config └── redirection.php └── README.md /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-redirection` will be documented in this file. 4 | 5 | ## 1.0.0 - 202X-XX-XX 6 | 7 | - initial release 8 | -------------------------------------------------------------------------------- /src/Contracts/Redirector.php: -------------------------------------------------------------------------------- 1 | comment('All done'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Exceptions/RedirectionException.php: -------------------------------------------------------------------------------- 1 | oldUrl = $oldUrl; 19 | $this->newUrl = $newUrl; 20 | $this->statusCode = $statusCode; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /database/migrations/create_redirections_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->string('old_url')->unique(); 14 | $table->string('new_url'); 15 | $table->unsignedSmallInteger('status_code')->default(301); 16 | $table->timestamps(); 17 | }); 18 | } 19 | 20 | public function down() 21 | { 22 | Schema::dropIfExists('redirections'); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/Drivers/DatabaseRedirector.php: -------------------------------------------------------------------------------- 1 | driver = $driver; 16 | } 17 | 18 | public function getRedirectFor(string $path): ?Redirect 19 | { 20 | $model = app()->make( 21 | config("redirection.drivers.{$this->driver}.source"), 22 | [$this->driver], 23 | ); 24 | 25 | $redirect = $model->findValidOrNull($path); 26 | 27 | return $redirect !== null 28 | ? new Redirect($redirect->old_url, $redirect->new_url, $redirect->status_code) 29 | : null; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/RedirectionServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-redirection') 26 | ->hasConfigFile() 27 | ->hasMigration('create_redirections_table') 28 | ->hasCommand(RedirectionListCommand::class); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) laravel-redirection 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/RedirectRequests.php: -------------------------------------------------------------------------------- 1 | make( 24 | config("redirection.drivers.{$driverName}.driver"), 25 | [$driverName], 26 | ); 27 | $redirection = $redirectorDriverInstance->getRedirectFor($request->path()); 28 | 29 | if (! $redirection && $request->getQueryString()) { 30 | $path = sprintf('%s?%s', $request->path(), $request->getQueryString()); 31 | $redirection = $redirectorDriverInstance->getRedirectFor($path); 32 | } 33 | 34 | if ($redirection) { 35 | return redirect($redirection->newUrl, $redirection->statusCode); 36 | } 37 | 38 | return $next($request); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Contracts/RedirectionModelContract.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR12' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'class_attributes_separation' => [ 30 | 'elements' => [ 31 | 'method' => 'one', 32 | ], 33 | ], 34 | 'method_argument_space' => [ 35 | 'on_multiline' => 'ensure_fully_multiline', 36 | 'keep_multiple_spaces_after_comma' => true, 37 | ], 38 | 'single_trait_insert_per_statement' => true, 39 | ]) 40 | ->setFinder($finder); 41 | -------------------------------------------------------------------------------- /src/Drivers/FileRedirector.php: -------------------------------------------------------------------------------- 1 | driver = $driver; 16 | } 17 | 18 | public function getRedirectFor(string $path): ?Redirect 19 | { 20 | $redirect = config(config("redirection.drivers.{$this->driver}.source")); 21 | 22 | if (false === config('redirection.case-sensitive')) { 23 | $redirect = array_change_key_case($redirect, CASE_LOWER); 24 | $path = strtolower($path); 25 | } 26 | 27 | if (! array_key_exists($path, $redirect)) { 28 | return null; 29 | } 30 | 31 | $redirect = $redirect[$path]; 32 | // in case of associative array with redirection status code 33 | if (is_array($redirect)) { 34 | return new Redirect( 35 | $path, 36 | $redirect['new_url'], 37 | $redirect['status_code'] ?? config('redirection.default_status_code') 38 | ); 39 | } 40 | 41 | return new Redirect( 42 | $path, 43 | $redirect, 44 | config('redirection.default_status_code'), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sirodiaz/laravel-redirection", 3 | "description": "Laravel package that allows storing in database (or other sources) urls to redirect for SEO purposes", 4 | "keywords": [ 5 | "sirodiaz", 6 | "laravel", 7 | "laravel-redirection", 8 | "seo", 9 | "url", 10 | "slug", 11 | "redirects", 12 | "redirect", 13 | "laravel-redirect", 14 | "laravel-redirects" 15 | ], 16 | "homepage": "https://github.com/SiroDiaz/laravel-redirection", 17 | "license": "MIT", 18 | "authors": [ 19 | { 20 | "name": "Siro Díaz Palazón", 21 | "email": "sirodiaz93@gmail.com", 22 | "role": "Developer" 23 | } 24 | ], 25 | "require": { 26 | "php": "^8.0", 27 | "illuminate/contracts": "^9.0|^10.0|^11.0", 28 | "illuminate/database": "^9.0|^10.0|^11.0", 29 | "illuminate/support": "^9.0|^10.0|^11.0", 30 | "spatie/laravel-package-tools": "^1.16" 31 | }, 32 | "require-dev": { 33 | "friendsofphp/php-cs-fixer": "^3.2", 34 | "nunomaduro/collision": "^5.3|^6.0|^7.0|^8.0", 35 | "orchestra/testbench": "^7.0|^8.0|^9.0", 36 | "vimeo/psalm": "^4.8|^5.0" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "SiroDiaz\\Redirection\\": "src" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "SiroDiaz\\Redirection\\Tests\\": "tests" 46 | } 47 | }, 48 | "scripts": { 49 | "test": "./vendor/bin/phpunit --no-coverage", 50 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage", 51 | "cs-fix": "vendor/bin/php-cs-fixer fix --config .php_cs.dist.php --allow-risky=yes" 52 | }, 53 | "config": { 54 | "sort-packages": true 55 | }, 56 | "extra": { 57 | "laravel": { 58 | "providers": [ 59 | "SiroDiaz\\Redirection\\RedirectionServiceProvider" 60 | ] 61 | } 62 | }, 63 | "minimum-stability": "dev", 64 | "prefer-stable": true 65 | } 66 | -------------------------------------------------------------------------------- /src/Models/Redirection.php: -------------------------------------------------------------------------------- 1 | old_url, '/')) === strtolower(trim($model->new_url, '/'))) { 33 | throw RedirectionException::sameUrls(); 34 | } 35 | 36 | // to prevent a redirection loop, we delete all that conflicting url 37 | static::whereOldUrl($model->new_url)->whereNewUrl($model->old_url)->delete(); 38 | 39 | 40 | $model->syncOldRedirects($model, $model->new_url); 41 | }); 42 | } 43 | 44 | /** 45 | * Get all redirect statuses defined inside the "config/redirects.php" file. 46 | * 47 | * @return array 48 | */ 49 | public static function getStatuses(): array 50 | { 51 | return (array) config('redirection.statuses', []); 52 | } 53 | 54 | /** 55 | * Filter the query by an old url. 56 | * 57 | * @param Builder $query 58 | * @param string $url 59 | * 60 | * @return Builder 61 | */ 62 | public function scopeWhereOldUrl(Builder $query, string $url): Builder 63 | { 64 | return $query->where('old_url', config('redirection.case-sensitive') ? $url : strtolower($url)); 65 | } 66 | 67 | /** 68 | * Filter the query by a new url. 69 | * 70 | * @param Builder $query 71 | * @param string $url 72 | * 73 | * @return Builder 74 | */ 75 | public function scopeWhereNewUrl(Builder $query, string $url): Builder 76 | { 77 | return $query->where('new_url', config('redirection.case-sensitive') ? $url : strtolower($url)); 78 | } 79 | 80 | /** 81 | * The mutator to set the "old_url" attribute. 82 | * 83 | * @param string $value 84 | * @returns void 85 | */ 86 | public function setOldUrlAttribute(string $value): void 87 | { 88 | $value = trim(parse_url($value)['path'], '/'); 89 | $this->attributes['old_url'] = config('redirection.case-sensitive') ? $value : strtolower($value); 90 | } 91 | 92 | /** 93 | * The mutator to set the "new_url" attribute. 94 | * 95 | * @param string $value 96 | * @returns void 97 | */ 98 | public function setNewUrlAttribute(string $value): void 99 | { 100 | $value = trim(parse_url($value)['path'], '/'); 101 | $this->attributes['new_url'] = config('redirection.case-sensitive') ? $value : strtolower($value); 102 | } 103 | 104 | /** 105 | * Sync old redirects to point to the new (final) url. 106 | * 107 | * @param RedirectionModelContract $model 108 | * @param string $finalUrl 109 | * @return void 110 | */ 111 | public function syncOldRedirects(RedirectionModelContract $model, string $finalUrl): void 112 | { 113 | $items = static::whereNewUrl($model->old_url)->get(); 114 | 115 | foreach ($items as $item) { 116 | $item->update(['new_url' => $finalUrl]); 117 | $item->syncOldRedirects($model, $finalUrl); 118 | } 119 | } 120 | 121 | /** 122 | * Return a valid redirect entity for a given path (old url). 123 | * A redirect is valid if: 124 | * - it has an url to redirect to (new url) 125 | * - it's status code is one of the statuses defined on this model. 126 | * 127 | * @param string $path 128 | * @return Redirection|null 129 | */ 130 | public static function findValidOrNull(string $path): ?Redirection 131 | { 132 | $path = ($path === '/' ? $path : trim($path, '/')); 133 | 134 | return static::where('old_url', config('redirection.case-sensitive') ? $path : strtolower($path)) 135 | ->whereNotNull('new_url') 136 | ->whereIn('status_code', array_keys(self::getStatuses())) 137 | ->latest() 138 | ->first(); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /config/redirection.php: -------------------------------------------------------------------------------- 1 | [ 14 | 301 => 'Permanent (301)', 15 | 302 => 'Normal (302)', 16 | 307 => 'Temporary (307)', 17 | ], 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Default Redirect status code (in case of not defined) 22 | |-------------------------------------------------------------------------- 23 | | 24 | | Status code used by default to redirect from an old URL to a new mapped 25 | | URL. 26 | | 27 | */ 28 | 'default_status_code' => (int)env('REDIRECT_DEFAULT_STATUS', 301), 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Case sensitivity 33 | |-------------------------------------------------------------------------- 34 | | 35 | | Whether to match URLs case sensitively or not. 36 | | Default to false because most URLs are not case sensitive. 37 | | 38 | */ 39 | 'case-sensitive' => (bool) env('REDIRECT_CASE_SENSITIVE', false), 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | Redirect Driver 44 | |-------------------------------------------------------------------------- 45 | | 46 | | Here you may specify the default redirect driver that you want to use. 47 | | The "config" driver is used by default when you want to code faster. 48 | | Consider database driver better for admin panel configuration backed by 49 | | a relational DB. 50 | | 51 | */ 52 | 'driver' => env('REDIRECT_DRIVER', 'config'), 53 | 54 | /* 55 | |-------------------------------------------------------------------------- 56 | | Array containing all available drivers and its implementations and source 57 | |-------------------------------------------------------------------------- 58 | | 59 | | Concrete implementation for the "redirection model". 60 | | To extend or replace this functionality, change the value below with 61 | | your full "redirection model" FQN. 62 | | 63 | | Your class will have to (first option is recommended): 64 | | - extend the "SiroDiaz\Redirection\Models\Redirection" class 65 | | - or at least implement the "SiroDiaz\Redirection\Contracts\RedirectionModelContract" interface. 66 | | 67 | | Regardless of the concrete implementation below, you can still use it like: 68 | | - app('redirection.') OR app('\SiroDiaz\Redirection\Contracts\RedirectionModelContract') 69 | | - or you could even use your own class as a direct implementation. For this 70 | | case you must extend from "SiroDiaz\Redirection\Models\Redirection" model class and 71 | | replace in the published config file 'drivers.database.source'. 72 | | 73 | | 74 | */ 75 | 'drivers' => [ 76 | 'config' => [ 77 | 'driver' => SiroDiaz\Redirection\Drivers\FileRedirector::class, 78 | 'source' => 'redirection.urls', 79 | ], 80 | 'database' => [ 81 | 'driver' => SiroDiaz\Redirection\Drivers\DatabaseRedirector::class, 82 | 'source' => SiroDiaz\Redirection\Models\Redirection::class, 83 | ], 84 | ], 85 | 86 | /* 87 | |-------------------------------------------------------------------------- 88 | | Url list with redirections used for config driver 89 | |-------------------------------------------------------------------------- 90 | | 91 | | You can use urls array of two different ways. The simple one uses the 92 | | default redirect status code ('redirection.default_status_code'). 93 | | Example: 94 | | 'urls' => [ 95 | | '/old/url' => '/new/url', 96 | | '/another/old/url' => '/another/new/url', 97 | | '/url/with?id=123' => '/url/with/123', 98 | | ], 99 | | 100 | | The second way to write redirect urls in your config/redirection.php 101 | | is using associative arrays. You can combine this method with the previous one. 102 | | Look at this example: 103 | | 'urls' => [ 104 | | '/old/url' => ['new_url' => '/new/url', 'status_code' => 302], 105 | | '/another/old/url' => '/another/new/url', 106 | | '/url/with?id=123' => ['new_url' => '/url/with/123'], 107 | | ], 108 | | 109 | */ 110 | 'urls' => [], 111 | 112 | ]; 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel package for manage your URL redirects in database or other sources to get better SEO results 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/sirodiaz/laravel-redirection.svg?style=flat-square)](https://packagist.org/packages/SiroDiaz/laravel-redirection) 4 | [![run-tests](https://github.com/SiroDiaz/laravel-redirection/actions/workflows/run-tests.yml/badge.svg)](https://github.com/SiroDiaz/laravel-redirection/actions/workflows/run-tests.yml) 5 | [![Check & fix styling](https://github.com/SiroDiaz/laravel-redirection/actions/workflows/php-cs-fixer.yml/badge.svg?branch=main)](https://github.com/SiroDiaz/laravel-redirection/actions/workflows/php-cs-fixer.yml) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/SiroDiaz/laravel-redirection.svg?style=flat-square)](https://packagist.org/packages/SiroDiaz/laravel-redirection) 7 | 8 | ## Requirements 9 | 10 | You need PHP 8.0 or higher. It is tested and designed for Laravel 9 and 10. 11 | This package will receive updates for future Laravel versions. Previous Laravel versions 12 | are not contemplated so use [Neurony/laravel-redirects](https://github.com/Neurony/laravel-redirects) package for 13 | older Laravel versions. 14 | 15 | ## Installation 16 | 17 | You can install the package via composer: 18 | 19 | ```bash 20 | composer require SiroDiaz/laravel-redirection 21 | ``` 22 | 23 | You can publish and run the migrations with: 24 | 25 | ```bash 26 | php artisan vendor:publish --provider="SiroDiaz\Redirection\RedirectionServiceProvider" --tag="redirection-migrations" 27 | php artisan migrate 28 | ``` 29 | 30 | You can publish the config file with: 31 | ```bash 32 | php artisan vendor:publish --provider="SiroDiaz\Redirection\RedirectionServiceProvider" --tag="redirection-config" 33 | ``` 34 | 35 | This is the contents of the published config file: 36 | 37 | ```php 38 | [ 51 | 301 => 'Permanent (301)', 52 | 302 => 'Normal (302)', 53 | 307 => 'Temporary (307)', 54 | ], 55 | 56 | /* 57 | |-------------------------------------------------------------------------- 58 | | Default Redirect status code (in case of not defined) 59 | |-------------------------------------------------------------------------- 60 | | 61 | | Status code used by default to redirect from an old URL to a new mapped 62 | | URL. 63 | | 64 | */ 65 | 'default_status_code' => (int)env('REDIRECT_DEFAULT_STATUS', 301), 66 | 67 | /* 68 | |-------------------------------------------------------------------------- 69 | | Case sensitivity 70 | |-------------------------------------------------------------------------- 71 | | 72 | | Whether to match URLs case sensitively or not. 73 | | Default to false because most URLs are not case sensitive. 74 | | 75 | */ 76 | 'case-sensitive' => (bool) env('REDIRECT_CASE_SENSITIVE', false), 77 | 78 | /* 79 | |-------------------------------------------------------------------------- 80 | | Redirect Driver 81 | |-------------------------------------------------------------------------- 82 | | 83 | | Here you may specify the default redirect driver that you want to use. 84 | | The "config" driver is used by default when you want to code faster. 85 | | Consider database driver better for admin panel configuration backed by 86 | | a relational DB. 87 | | 88 | */ 89 | 'driver' => env('REDIRECT_DRIVER', 'config'), 90 | 91 | /* 92 | |-------------------------------------------------------------------------- 93 | | Array containing all available drivers and its implementations and source 94 | |-------------------------------------------------------------------------- 95 | | 96 | | Concrete implementation for the "redirection model". 97 | | To extend or replace this functionality, change the value below with 98 | | your full "redirection model" FQN. 99 | | 100 | | Your class will have to (first option is recommended): 101 | | - extend the "SiroDiaz\Redirection\Models\Redirection" class 102 | | - or at least implement the "SiroDiaz\Redirection\Contracts\RedirectionModelContract" interface. 103 | | 104 | | Regardless of the concrete implementation below, you can still use it like: 105 | | - app('redirection.') OR app('\SiroDiaz\Redirection\Contracts\RedirectionModelContract') 106 | | - or you could even use your own class as a direct implementation. For this 107 | | case you must extend from "SiroDiaz\Redirection\Models\Redirection" model class and 108 | | replace in the published config file 'drivers.database.source'. 109 | | 110 | | 111 | */ 112 | 'drivers' => [ 113 | 'config' => [ 114 | 'driver' => SiroDiaz\Redirection\Drivers\FileRedirector::class, 115 | 'source' => 'redirection.urls', 116 | ], 117 | 'database' => [ 118 | 'driver' => SiroDiaz\Redirection\Drivers\DatabaseRedirector::class, 119 | 'source' => SiroDiaz\Redirection\Models\Redirection::class, 120 | ], 121 | ], 122 | 123 | /* 124 | |-------------------------------------------------------------------------- 125 | | Url list with redirections used for config driver 126 | |-------------------------------------------------------------------------- 127 | | 128 | | You can use urls array of two different ways. The simple one uses the 129 | | default redirect status code ('redirection.default_status_code'). 130 | | Example: 131 | | 'urls' => [ 132 | | '/old/url' => '/new/url', 133 | | '/another/old/url' => '/another/new/url', 134 | | '/url/with?id=123' => '/url/with/123', 135 | | ], 136 | | 137 | | The second way to write redirect urls in your config/redirection.php 138 | | is using associative arrays. You can combine this method with the previous one. 139 | | Look at this example: 140 | | 'urls' => [ 141 | | '/old/url' => ['new_url' => '/new/url', 'status_code' => 302], 142 | | '/another/old/url' => '/another/new/url', 143 | | '/url/with?id=123' => ['new_url' => '/url/with/123'], 144 | | ], 145 | | 146 | */ 147 | 'urls' => [], 148 | 149 | ]; 150 | 151 | 152 | ``` 153 | 154 | You can change and extend the default `SiroDiaz\Redirection\Models\Redirection` model class. 155 | Image that you want to add some methods or fields. You would need to create a new model class, for example `App\Models\Redirect`. 156 | 157 | Here is a basic example of how to extend the functionality of the default Redirection model. 158 | We want to include support for Laravel BackPack admin panel would be: 159 | ```php 160 |