Laravel multiPatternRoute
Tags: laravel, php
Published: 2021-05-05 22:36:45

when we develop a multilingual web application, sometimes we face a case where the pattern of the route is different between two languages.

###how we can solve such a problem?


* First, we need to create our standard for route names, in our case, we opt for this scheme: "{name}.{locale}" when {name} is the route name and {locale} is a language or a group of languages.
```
Route::get('/', App\Http\Controllers\Front\HomeController::class)->name("home.fr");
Route::get('/{locale}', App\Http\Controllers\Front\HomeController::class)->where(['locale' => '[a-z]{2,3}'])
    ->name("home.locale");
```
* Second, we need our own Helper to generate URLs. 
we will need a class that will parse the list of routes and get the routes linked to the name we provide, After that the class will take the list of parameters that we pass and return just the parameters needed for the current pattern, also it will inject constant parameters if they exist.
```
class MultiPatternRoute{

    protected string $locale;
    protected $routes;

    public function __construct()
    {
        $this->routes =  Route::getRoutes()->getRoutesByName();
        $this->locale = app()->getLocale();
    }

    public function getParameters (string $routeName,array $parameters) : array
    {
        if(!isset($this->routes[$routeName.".fr"]) || !isset($this->routes[$routeName.".locale"]))
            throw new \Exception("unknown route : ". $routeName.".fr");
        
        $route_fr = $this->getCompiledRoute($this->routes[$routeName.".fr"]);
        $route_locale = $this->getCompiledRoute($this->routes[$routeName.".locale"]);
        if($this->locale === "fr"){
            $route = $routeName.".fr";
            return [$route,$this->getStaticParameters($route ,$this->cleanParameters($parameters, $route_fr->getVariables(), $route_locale->getVariables()))];
        }
        $route = $routeName.".locale";
        $parameters = $this->getStaticParameters($route, $this->cleanParameters($parameters, $route_locale->getVariables(), $route_fr->getVariables()));
        $parameters["locale"] = app()->getLocale();
        return [$route,$parameters];

    }

    private function getCompiledRoute(RoutingRoute $route): CompiledRoute
    {
        return $route->getCompiled()!==null?$route->getCompiled():$route->toSymfonyRoute()->compile();
    }

    private  function cleanParameters(array $parameters, array $neededParametersA, array $neededParametersB) : array
    {
        $neededParameters = array_intersect(array_keys($parameters), $neededParametersA);
        $queryStringParameters = array_diff(array_keys($parameters), $neededParametersB);
        $parameters_keys = array_unique (array_merge ($neededParameters,$queryStringParameters));
        return array_filter($parameters, fn($k)=>in_array($k,$parameters_keys),ARRAY_FILTER_USE_KEY);
    }

    private function getStaticParameters(string $routeName, array $parameters) : array
    {
        
        $controller = $this->routes[$routeName]->getController();
        if(!method_exists($controller,'getConstantRouteParameters'))
            return $parameters;
        return array_merge($this->routes[$routeName]->getController()::getConstantRouteParameters($this->locale),$parameters);
    }
}
```
* Third, we provide this class as Singleton via the container in our AppServiceProvider.
```
$this->app->singleton(
            'MultiPatternRoute',
           function(){
              return new  MultiPatternRoute();
           }
        );
```
* Fourth, we create a helper function that will consume our class and generate our URL.
```
if ( !function_exists('multiPatternRoute') )
{
	function multiPatternRoute(string $routeName, array $parameters =[], bool $isAbsolute = true){
        list($route, $parameters) = app("MultiPatternRoute")->getParameters($routeName, $parameters);
        return route($route, $parameters, $isAbsolute);
	}
}
```
* Fifth, we prepare our controllers to communicate with our class by providing constant parameters if they exist. we do that thanks to a trait as a result.
```
trait MultiPatternRoutable
{

    public static function getConstantRouteParameters($locale): array
    {
        return  isset(static::$constantRouteParameters)?static::$constantRouteParameters[$locale]:[];
    }

    public function loadConstantRouteParameters($locale): array
    {
        return array_keys(static::getConstantRouteParameters($locale));
    }

}
```
then in the controller
```
class DestinationController extends Controller
{
    use MultiPatternRoutable;

    protected static array $constantRouteParameters =[
        "fr" => [],
        "en" => ["slugSeo"=>"destinations"],
        "de" => ["slugSeo"=>"unsere-reiseziele"],
        "it" => ["slugSeo"=>"destinazioni"],
    ];

    public function __invoke(Request $request, string $slug){
        return view("front.pages.destination", compact("slug"));
    }

}
```
finally, we add a middleware to clean the request object from the constant or unnecessary parameters for the current route.
```
class Locale
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle(Request $request, Closure $next)
    {
        $locale = isset($request->route()->parameters['locale'])?$request->route()->parameters['locale']:null;
        if($locale){
            app()->setLocale($locale);
            $request->route()->forgetParameter('locale');
        }
        $otherParameterToForgot =$request->route()->getController()->loadConstantRouteParameters(app()->getLocale());
        foreach($otherParameterToForgot as $parameter){
            $request->route()->forgetParameter($parameter);
        }
        return $next($request);
    }
}
```
then in our route file
```

Route::group(["middleware" => ["locale"]], function () {

    Route::get('/', App\Http\Controllers\Front\HomeController::class)->name("home.fr");
    Route::get('/{locale}', App\Http\Controllers\Front\HomeController::class)->where(['locale' => '[a-z]{2,3}'])
    ->name("home.locale");

    Route::get("/{locale}/{slugSeo}", App\Http\Controllers\Front\DestinationsController::class)
        ->where(['locale' => '[a-z]{2,3}', 'slugSeo' => 'tutte-destinazioni|unsere-reiseziele|destinations-all'])
        ->name("destinations.locale");
    Route::get("/destinations/notre-list-destination", App\Http\Controllers\Front\DestinationsController::class)
        ->name("destinations.fr");

    Route::get("/{locale}/{slugSeo}/{slug}", App\Http\Controllers\Front\DestinationController::class)
        ->where(['locale' => '[a-z]{2,3}', 'slugSeo' => 'destinazioni|reiseziele|destinations'])
        ->name("destination.locale");
    Route::get("/destinations-{slug}-produit-{slugAlt}.html", App\Http\Controllers\Front\DestinationController::class)
        ->name("destination.fr");
});
```
then in blade file
```
 <a href="{{ multiPatternRoute('destination',["slug"=>"france","slugAlt"=>"france"]) }}">France</a>
```