Summer Sale! All accounts are 50% off this week.

findstar's avatar

How to extend Router or Replace Customer Router class on Laravel5?

In my purpose. I want to find the way to extend Router class and use my custom Router class On Laravel5.

To extend laravel's router or use my custom router.

How can I find the way.

I try to change code app\Providers\RouteServiceProvider.php register method.

$this->app->rebinding('router', function($app) use($router)
{
    return new MyCustomRouter($app['events'], $app);
});

but I had fail.

Ultimately I want to append 'alias' method on my custom router.

like this.

MyCustomerRoute::get('/user/{slug}', array('UserController@show'));
// GET /user/taylor --> show user #taylor description page

MyCustomerRoute::alias('/user/taylor', '/taylor', $redirect = false);
// GET /user/taylor === GET /taylor --> shows user #taylor description page

// same thing for companies with redirection enabled - great for SEO
MyCustomerRoute::alias('/company/userscape', '/userscape', $redirect = true);
// GET /company/userscape makes a 301 redirect to GET /userscape --> shows company #userscape description page
0 likes
17 replies
bestmomo's avatar

Hello, L5 has Contracts in Illuminate\Contracts. For Routing it's in Illuminate\Contracts\Routing. So you can extend or create with a class that implements these interfaces. You just need to bind it to the interface.

findstar's avatar

@bestmomo Can you explain more detail plz? I don't understand your answer.

I have tried on bootstrap/app.php

$app->singleton(
    'Illuminate\Contracts\Routing\Registrar',
    'App\Routing\MyRouter'
);

but it didn't work.

bestmomo's avatar

Sorry, forget my suggestion, it is a wrong way to do that. Routing is very hard coding in application...

pmall's avatar

But you can bind your own UrlGenerator class. I've already done this successfully once. It is used to generate url from routes. I don't know if it will help you because I didn't fully understand your problem.

And from a SEO point of view I don't think /company/userscape or /userscape makes any difference.

bestmomo's avatar

Not so easy to extend the router because it's hard coding in application so I had a look in L5 core and this is what I made to have it work. It's ugly but I dont see another way.

First I need to extend the core application :

<?php namespace App\Services;
use Illuminate\Events\EventServiceProvider;
class Application extends \Illuminate\Foundation\Application {
    /**
     * Register all of the base service providers.
     *
     * @return void
     */
    protected function registerBaseServiceProviders()
    {
        $this->register(new EventServiceProvider($this));
        $this->register(new \App\Providers\RoutingServiceProvider($this));
    }
}

So in bootstrap :

$app = new App\Services\Application(realpath(__DIR__.'/../'));

I extend the RoutingServiceProvider :

<?php namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class RoutingServiceProvider extends \Illuminate\Routing\RoutingServiceProvider {
    /**
     * Register the router instance.
     *
     * @return void
     */
    protected function registerRouter()
    {
        $this->app['router'] = $this->app->share(function($app)
        {
            return new \App\Services\Router($app['events'], $app);
        });
    }
}

And a last my router :

<?php namespace App\Services;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Routing\Router as BaseRouter;
class Router extends BaseRouter {

    public function __construct(Dispatcher $events)
    {
        parent::__construct($events);
    }
    ...
}
MikeHopley's avatar

I did something similar, but in a different way. The end result is that I can define my redirects and aliases in configuration files. For example:

<?php
// app/config/testing/links/redirects.php

return [

'/about-the-badminton-bible' => '/about',

'/articles/grips-guide' => '/articles/grips',
    '/articles/grips/the-grips' => '/articles/grips/holds',
        '/articles/grips/holds/basic' => '/articles/grips/holds/forehand',

];

The indentation is "cosmetic". My convention is that, when a whole folder is redirected, I define that first. If any pages have also changed, these redirects are defined afterwards, using the new folder URL.

Instead of replacing the router, I created a links.correct filter that runs for all requests:

// At the top of routes.php

Route::when( '*', 'links.correct' );

This runs my LinkResolver filter:

<?php namespace BBapp\filters;

use App;
use Request;
use Session;
use Redirect;

class LinkResolver {

    private $link;

    function filter()
    {
        // Fix broken links and enforce canonical URLs
        $this->link = App::make('LinkResolver');
        $this->link->resolve( Request::rawUrl() );

        // Store the results in the container for later
        // Maybe this should be changed to something more
        // maintainable / explicit!
        App::getFacadeRoot()->BBappUrl = $this->link->url;
        App::getFacadeRoot()->BBappView = $this->link->view;

        // Enforce SSL
        if ( !Request::secure() )
        {
            $link->status = 301;
        }

        // For redirection after login, etc.
        if ( $this->allowedForRedirectIntended() )
        {
            Session::put( 'url.intended', $this->link->url );
        }

        if ( $this->link->status !== 200 )
        {
            return Redirect::secure( $this->link->url, $this->link->status );
        }
    }

    private function allowedForRedirectIntended()
    {
        // We don't want login page to wipe out the intended page
        $disallowedPages = [
            '/login',
            '/logout',
        ];

        // We don't want validation errors to wipe out the intended page
        $disallowedStatuses = [ 302 ];

        return !in_array($this->link->url, $disallowedPages)
            && !in_array($this->link->status, $disallowedStatuses);
    }

}

(Note that I did extend the Request class, purely for dealing with multiple slashes.)

The "heavy lifting" is done by a separate LinkResolver class:

<?php namespace BBapp\providers;

use BBapp\repositories\ConfigInterface;
use Config;

class LinkResolver {

    public $url;             // The final URL of the page, after any redirects
    public $view;            // The location of the file (within public/pages)
    public $status = 200;    // The HTTP status code

    private $config;
    private $languages;
    private $redirects;
    private $aliases;

    public function __construct()
    {
        $this->assembleConfigData();
    }

    public function resolve($path)
    {
        $this->url  = $path;
        $this->view = $path;

        // Remove query string, unless we're on a page that needs it
        if ( !$this->pageRequiresQueryString() )
        {
            $this->removeQuery();
        }

        // Special case for English homepage, i.e. '/' route
        if ( $this->url === '/' )
        {
            return;
        }

        // Is this an old link with a .php extension?
        $this->removeExtension();

        // Are there any extra slashes?
        $this->removeSlashes();

        // Has this page or folder been moved / renamed?
        $this->resolveRedirects();

        // Is this folder an alias? E.g. '/de' maps to '/languages/de'
        $this->resolveAliases();
    }

    private function removeQuery()
    {
        // Redirect if there was a query string
        if ( $this->getQuery() )
        {
            $this->url    = $this->getQuery()['base'];
            $this->view   = $this->getQuery()['base'];
            $this->status = 301;
        }
    }

    private function getQuery()
    {
        $parts = explode('?', $this->url, 2);

        if ( isset($parts[1]) )
        {
            return [ 'base' => $parts[0], 'query' => '?' . $parts[1] ];
        }
        return false;
    }

    private function removeExtension()
    {
        if ( ends_with($this->url, '.php') )
        {
            $this->url    = substr($this->url, 0, -4);
            $this->status = 301;
        }
    }

    private function removeSlashes()
    {
        // Replace multiple slashes with a single slash, i.e. '//' --> '/'
        while ( strpos($this->url, '//') !== false )
        {
            $this->url    = str_replace('//', '/', $this->url);
            $this->status = 301;
        }

        // Remove trailing slash, even if there's still a query string (Paypal!)
        $query = '';
        if ( $this->getQuery() )
        {
            $query = $this->getQuery()['query'];
            $this->url = $this->getQuery()['base'];
        }

        if ( ends_with($this->url, '/') )
        {
            $this->url    = substr( $this->url, 0, -1 );
            $this->status = 301;
        }

        $this->url .= $query;
    }

    private function resolveRedirects()
    {
        foreach ($this->redirects as $old => $new)
        {
            if ( starts_with($this->url, $old) )
            {
                $this->url    = str_replace_once( $old, $new, $this->url );
                $this->status = 301;

                // Check for any later fragments of the URL that match
                $this->resolveRedirects( $this->url );
            }
        }
    }

    private function resolveAliases()
    {
        foreach ($this->aliases as $alias => $real)
        {
            if ( starts_with($this->url, $alias) )
            {
                $this->view = str_replace_once($alias, $real, $this->url);
            }
        }
    }

    private function assembleConfigData()
    {
        $this->languages = Config::get('links/languages');
        $this->redirects = Config::get('links/redirects');
        $this->aliases   = Config::get('links/aliases');

        // Copy redirects & aliases for languages: all languages share English
        // URL patterns. Also add the base URL of '/languages/' as an alias
        foreach($this->languages as $lang)
        {
            // Alias '/french' to '/languages/french'
            $this->aliases['/' . $lang] = '/languages/' . $lang;

            // Reuse redirect patterns from English
            foreach($this->redirects as $old => $new)
            {
                $this->redirects['/' . $lang . $old] = '/' . $lang . $new;
            }

            // Reuse alias patterns from English
            foreach($this->aliases as $alias => $real)
            {
                $this->aliases['/' . $lang . $alias] = '/' . $lang . $real;
            }
        }

        // Force aliases to be used, i.e. redirect from real path to alias
        foreach($this->aliases as $alias => $real)
        {
            $this->redirects[$real] = $alias;
        }
    }

    private function pageRequiresQueryString()
    {
        $queryPages = Config::get('links/queryStringPages');

        if ( in_array( $this->getQuery()['base'], $queryPages) )
        {
            return true;
        }
        return false;
    }

}

What this essentially does is take my URL logic out of .htaccess, where it should never belong. .htaccess is a server configuration override file. Cramming tonnes of redirects in there is horrendously messy.

Instead, I want my redirection logic contained in my PHP code, where it is easily testable, and where I can use separate configuration files for each concern.

pmall's avatar

Don't do all this mess for two links :)

Simplicity > all above.

MikeHopley's avatar

Don't do all this mess for two links :)

Simplicity > all above.

Absolutely, but it becomes a bit different when you publish a lot of content that you keep updating and rearranging.

URLs are the public interface to your content. Vast, complicated .htaccess guides have been written on how to wrangle your URLs for SEO and for fixing broken links.

If you don't care about this stuff, no problem. But if you do care, then I maintain it's better to put it in your PHP code, where you can separate concerns and test it.

pmall's avatar

Yes. I don't know the complexity of your website but you can also do things like that :

Route::get('/company/{name}', function($name)
{
  return Redirect::to($name, 301);
});

Clean.

1 like
MikeHopley's avatar

Agreed, that's very clean.

It depends on the problem you are trying to solve. In my case I had a whole lot of URL "fixing" logic, in order to redirect from old pages to new pages while also maintaining canonical URLs.

However, it might well be that some of the logic I threw into my LinkResolver could be more cleanly expressed via the router.

One consideration is avoiding redirect chains. If you have multiple reasons for creating a redirect -- e.g. fixing old links, aliases, and canonicalisation -- then it is better for performance to do all the logic "in one go" before returning the response. I prefer the user to have at most one 301 delay, rather than a chain of two or three.

findstar's avatar

Now I making CMS Solution base on Laravel5. Then User have installed CMS want to modify predefined url. (like /plugins/shop/footwear => /footwear dynamic handle by user)

I think about appending method '::Alias' on Custom Router that can dynamic handle route by user.

Already other had proposal https://github.com/laravel/framework/issues/2437 but It's not accepted. because of It is my question.

Redirect:: is good. But on my CMS I have to show not Redirected URL but Origin URL. SO I asked.

But on looking your suggestion. It's not easy to extend Router or replace Custom Router. (It's hardcoding on L5). It's so complex to me

nolros's avatar
nolros
Best Answer
Level 23

Laravel has a route customer dispatcher. Why not use that?

// build alias example
$alias = $typeOfApi . '/' . getRegisteredDeveloper($id) . '/' // over compliacted alias example 

$request = Request::create({$alias}, 'GET'); // could replace method with{$methodAlias} 

$response = Route::dispatch($request);

$response will now contain the returned response of the API. Typically this will be returned a JSON encoded string.

The Laravel Route.php method:

    /**
     * Determine if a custom route dispatcher is bound in the container.
     *
     * @return bool
     */
    protected function customDispatcherIsBound()
    {
        return $this->container->bound('illuminate.route.dispatcher');
    }

Alternatively, you will need to extend the Route class and build your own method to match whatever you doing and then build a ServiceProvider to capture that behavior.

exabyssus's avatar

I believe you can do that by modifying your App/Kerner.php file

    public function __construct(Application $app, Router $defaultRouter)
    {
    // Your custom Router
        $router = new \App\Routing\Router($app['events'], $app);

        parent::__construct($app, $router);
    }


    public function handle($request)
    {
        $this->app->singleton('router', function () {
            return $this->router;
        });

       return parent::handle($request);
    }
szabomikierno's avatar

Or you can do it in bootstrap/app.php

$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

$app->singleton('router', function ($app) {
  return new \My\Custom\Router($app['events'], $app);
});
4 likes
sasivarnakumar@gmail.com's avatar

I somehow managed to implement a custom router. I am using Joomla CMS and wanted to pass laravel routes as a url parameter. Laravel to recognize the routes i had to extend Router and override findRoute method and do some hard baking stuff.

Please or to participate in this conversation.