How to properly extend the Laravel 5.7 Router? - php

This question has been discussed many times here, here or here but no elegant solutions were mentioned.
One particular use case would be to allow to load and route old PHP files with Laravel. I am for instance migrating a very old (> 20 years) code base into Laravel and most pages are regular PHP files that I would like to render into a particular Blade template.
To do this it would be elegant to do:
Router::php('/some/route/{id}', base_path('legacy/some/page.php'));
Behind the scenes all I need is to pass the captured variables to the PHP page, evaluate and grab the content of it and eventually return a view instance.
As Laravel claims itself to be a SOLID framework, I thought extending the Router is trivial so I wrote this:
namespace App\Services;
class Router extends \Illuminate\Routing\Router
{
public function php($uri, $filename, $template='default') {
...
return view(...
}
}
Then I tried to extend my Http Kernel with this:
namespace App\Http;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use App\Services\Router;
class Kernel extends HttpKernel
{
public function __construct(Application $app, Router $router) {
return parent::__construct($app, $router);
}
}
But it is not working it seems the Application is building the Kernel with the wrong dependency. In Application#registerCoreContainerAliases I see the core alias router is hard coded and since this method is called in the Application's constructor, I am doomed.
The only solution that remains is to override the router before loading the Kernel as follow:
$app = new Application($_ENV['APP_BASE_PATH'] ?? dirname(__DIR__));
$app->singleton('router', \App\Services\Router::class);
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
But this looks a bit ugly. Is there a better way to achieve this?

Since the Router class is macroable, you may be able to do something like:
Router::macro('php', function ($uri, $filepath) {
return $this->addRoute(['GET', 'POST', etc...], $uri, function () use ($filepath) {
// here you might use the blade compiler to render the raw php along with any variables.
//
// See: https://laravel.com/api/5.7/Illuminate/View/Compilers/Concerns/CompilesRawPhp.html
//
$contents = file_get_contents($filepath);
// return compiled $contents...
});
});

Related

How do I create middleware specific to route not whole controller class

I have an api and some routes are public some need to be protected via auth. I want to have them in one controller class as they are related. I can extend the controller and have beforeRoute function but it runs for any route that is in that controller. is it possible to add a middleware only to specific routes? I'm a js dev and in express I can just pass middleware functions for any route, even multiple middlewares.
class Clanky /*extends \controllers\ProtectedController */{
public function post_novy_clanek(\Base $base) {
//needs to be protected
}
public function get_clanky(\Base $base) {
}
public function get_clanek(\base $base) {
}
public function get_kategorie(\Base $base) {
}
}
PHP is new to me, I just want to know how I can implement the concepts I know from other languages and frameworks in this weird fatfree framework. Thanks.
Use can use f3-access plugin for that purpose https://github.com/xfra35/f3-access
Fatfree is not opinionated about how to do this.. other options to solve this ask might be:
Use php8 attributes on the method and check these in beforeroute.
Consider an own named route naming schema like #admin_routename and apply checking auth in beforeroute
Use f3-middleware plugin and add auth there
Extend an other admin controller that provides auth in beforeroute or use a trait.

Routes inside controllers with laravel?

I have been declaring all the routes for my application inside web.php , but it is now getting quite large. I find that I am losing a lot of time shifting between web.php and each controller and this is hurting productivity.
I feel like it would be better to define routes inside of the controller, perhaps ideally delegating some URL to a controller and then allowing the controller to handle the "sub routes" since this would allow me to use inheritance when I have two similar controllers with similar routes.
It is not possible given how laravel works. Every request is passed onto router to find its designated spot viz. the controller with the method. If it fails to find the route within the router, it just throws the exception. So the request never reaches any controller if the route is not found. It was possible in earlier versions on Symphony where you would configure the route in the comment of a particular controller method.
Sadly with laravel it works how it works.
But for me, I just like to have the routes in a separate file.
Alternate solution, easier way to sort all the routes.
You can move your route registration into controllers if you use static methods for this. The code below is checked in Laravel 7
In web.php
use App\Http\Controllers\MyController;
.....
MyController::registerRoutes('myprefix');
In MyController.php
(I use here additional static methods from the ancestor controller also posted below)
use Illuminate\Support\Facades\Route;
.....
class MyController extends Controller {
......
static public function registerRoutes($prefix)
{
Route::group(['prefix' => $prefix], function () {
Route::any("/foo/{$id}", self::selfRouteName("fooAction"));
Route::resource($prefix, self::selfQualifiedPath());
}
public function fooAction($id)
{
........
}
In Controller.php
class Controller extends BaseController {
....
protected static function selfAction($actionName, $parameters = [], $absolute = false)
{
return action([static::class, $actionName], $parameters, $absolute);
}
protected static function selfQualifiedPath()
{
return "\\".static::class;
}
protected static function selfRouteName($actionName)
{
//classic string syntax return "\\".static::class."#".$actionName;
// using tuple syntax for clarity
return [static::class, $actionName];
}
}
selfAction mentioned here is not related to your question, but mentioned just because it allows making correct urls for actions either by controller itself or any class using it. This approach helps making action-related activity closer to the controller and avoiding manual url-making. I even prefer making specific functions per action, so for example for fooAction
static public function fooActionUrl($id)
{
return self::selfAction('foo', ['id' => $id]);
}
Passing prefix into registerRoutes makes controller even portable in a sense, so allows inserting it into another site with a different prefix in case of conflict

Laravel: How to change UrlGenerator (Core Binding) against custom Implementation?

I need to use a custom Implementation of UrlGenerator. So how can I change the default binding of laravel, that is implemented somewhere deep in the core as
'url' => ['Illuminate\Routing\UrlGenerator', 'Illuminate\Contracts\Routing\UrlGenerator'],
against my own implementation?
Furthermore I am not shure. I assume this line above does actually two things. it will store the bindinung under the key "url" and it will also do the mapping of the Interface to the class. So I actually need to override both! How to do that? Furthemore how to find out if this must be bound as "shared"(singleton) or "new instance every time"?
Thanks very much!
Take a look at the Service Container guide http://laravel.com/docs/5.1/container
In this specific case I think all you need to do is to tell the app to replace the alias that already exists.
To do that I would recommend creating a ServiceProvider, registering int the config/app.php file and inside that one in the register method put something like:
$this->app->bind('Illuminate\Routing\UrlGenerator', 'yourownclasshere');
Let us know if it works.
Update: I removed the option that didn't work and left only the one that worked.
I did what Nestor said in his answer, but it didn't quite work for me. So this is what I did to make it work.
Inside my service provider in method register I first tried this:
$this->app->bind('url', MyCustomProvider::class);
This did register my URL provider instead of the default one. The problem was that now my provider didn't have any access to routes. I checked the Laravel code for \Illuminate\Routing\RoutingServiceProvider because it has a method registerUrlGenerator for registering the URL provider. This method did a direct instantiation of the Laravel URL generator Illuminate\Routing\UrlGenerator and giving proper parameters in the constructor.
So, I did the same in my service provider. Instead of doing $this->app->bind I did $this->app->singleton('url', function ($app) { ... }) and provided basically the same code in the closure function as in RoutingServiceProvider::registerUrlGenerator but created the instance of my URL generator. This then worked properly, and my generator is now called every time. The final code was this:
// the code is copied from the \Illuminate\Routing\RoutingServiceProvider::registerUrlGenerator() method
$this->app->singleton('url', function ($app) {
/** #var \Illuminate\Foundation\Application $app */
$routes = $app['router']->getRoutes();
$app->instance('routes', $routes);
// *** THIS IS THE MAIN DIFFERENCE ***
$url = new \My\Specific\UrlGenerator(
$routes,
$app->rebinding(
'request',
static function ($app, $request) {
$app['url']->setRequest($request);
}
),
$app['config']['app.asset_url']
);
$url->setSessionResolver(function () {
return $this->app['session'] ?? null;
});
$url->setKeyResolver(function () {
return $this->app->make('config')->get('app.key');
});
$app->rebinding('routes', static function ($app, $routes) {
$app['url']->setRoutes($routes);
});
return $url;
});
I hate copying the code, so it seems to me that the problem is in the base implementation. It should take the correct contract for URL generator instead of making direct instantiation of a base class.
I tried the Kosta's approach but it didn't fully work for me because it somehow created an endless recursion loop in the framework. Nonetheless, I ended up with this code:
namespace App\Providers;
use App\Routing\UrlGenerator;
use Illuminate\Support\ServiceProvider;
class UrlGeneratorServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton("url", function($app) {
$routes = $app['router']->getRoutes();
return new UrlGenerator( // this is actually my class due to the namespace above
$routes, $app->rebinding(
'request', $this->requestRebinder()
), $app['config']['app.asset_url']
);
});
}
protected function requestRebinder()
{
return function ($app, $request) {
$app['url']->setRequest($request);
};
}
}
And of course, registered the above provider in config/app.php under 'providers'

Extend Laravels Router class (4.1)

I would like to extend Laravels Router class (Illuminate\Routing\Router) to add a method I need a lot in my application.
But sadly I can't get this to work. I already extended other classes successfully so I really have no idea where my wrong thinking comes from.
Anyway, right into the code:
<?php
namespace MyApp\Extensions;
use Illuminate\Routing\Router as IlluminateRouter;
class Router extends IlluminateRouter
{
public function test()
{
$route = $this->getCurrentRoute();
return $route->getParameter('test');
}
}
So as you see I want to get the parameter set by {test} in routes.php with a simple call like:
Router::test();
Not sure how to go on now. Tried to bind it to the IOC-Container within my ServiceProvider in register() and boot() but I got no luck.
Whatever I try I get either a constructor error or something else.
All solutions I found are too old and the API has changed since then.
Please help me!
edit:
I already tried binding my own Router within register() and boot() (as said above) but it doesn't work.
Here is my code:
<?php
namespace MyApp;
use Illuminate\Support\ServiceProvider;
use MyApp\Extensions\Router;
class MyAppServiceProvider extends ServiceProvider {
public function register()
{
$this->app['router'] = $this->app->share(function($app)
{
return new Router(new Illuminate\Events\Dispatcher);
}
// Other bindings ...
}
}
When I try to use my Router now I have the problem that it needs an Dispatcher.
So I have to do:
$router = new Router(new Illuminate\Events\Dispatcher); // Else I get an exception :(
Also it simply does nothing, if I call:
$router->test();
:(
And if I call
dd($router->test());
I get NULL
Look at: app/config/app.php and in the aliases array. You will see Route is an alias for the illuminate router via a facade class.
If you look at the facade class in Support/Facades/Route.php of illuminate source, you will see that it uses $app['router'].
Unlike a lot of service providers in laravel, the router is hard coded and cannot be swapped out without a lot of work rewiring laravel or editing the vendor source (both are not a good idea). You can see its hardcoded by going to Illuminate / Foundation / Application.php and searching for RoutingServiceProvider.
However, there's no reason i can think of that would stop you overriding the router class in a service provider. So if you create a service provider for your custom router, which binds to $app['router'], that should replace the default router with your own router.
I wouldn't expect any issues to arise from this method, as the providers should be loaded before any routing is done. So overriding the router, should happen before laravel starts to use the router class, but i've not this before, so be prepared for a bit of debugging if it doesn't work straight away.
So I was asking in the official Laravel IRC and it seems like you simply can't extend Router in 4.1 anymore. At least that's all I got as a response in a pretty long dialogue.
It worked in Laravel 4.0, but now it doesn't. Oh well, maybe it will work in 4.2 again.
Other packages suffer from this as well: https://github.com/jasonlewis/enhanced-router/issues/16
Anyway, personally I'll stick with my extended Request then. It's not that much of a difference, just that Router would've been more dynamic and better fitting.
I'm using Laravel 4.2, and the router is really hard coded into the Application, but I extended it this way:
Edit bootstrap/start.php, change Illuminate\Foundation\Application for YourNamespace\Application.
Create a class named YourNamespace\Application and extend \Illuminate\Foundation\Application.
class Application extends \Illuminate\Foundation\Application {
/**
* Register the routing service provider.
*
* #return void
*/
protected function registerRoutingProvider()
{
$this->register(new RoutingServiceProvider($this));
}
}
Create a class named YourNamespace\RoutingServiceProvider and extend \Illuminate\Routing\RoutingServiceProvider.
class RoutingServiceProvider extends \Illuminate\Routing\RoutingServiceProvider {
protected function registerRouter()
{
$this->app['router'] = $this->app->share(function($app)
{
$router = new Router($app['events'], $app);
// If the current application environment is "testing", we will disable the
// routing filters, since they can be tested independently of the routes
// and just get in the way of our typical controller testing concerns.
if ($app['env'] == 'testing')
{
$router->disableFilters();
}
return $router;
});
}
}
Finally, create YourNamespace\Router extending \Illuminate\Routing\Router and you're done.
NOTE: Although you're not changing the name of the class, like Router and RoutingServiceProvider, it will work because of the namespace resolution that will point it to YourNamespace\Router and so on.

Inject other parameters to an action when using controllers in classes pattern?

I have a class called ApplicationDecorator, which inherits Application and adds some often used methods.
At the moment each controller action contains at the beginning a line like
$appDec = new ApplicationDecorator($app);
Is it possible to tell Silex to pass the instance as parameter to the action like it is done for Application and Request?
So it would look like the following:
public function switchAction(ApplicationDecorator $appDec, Request $request) {
I am already using Controllers in classes and want to inject an inherited class of Application.
You can use Request and Silex\Application type hints to get $request
and $app injected.
At the moment only Request and Application are supported.
Is there any possibility to extend the possible values?
You are looking for controllers in classes:
$app->get('/', 'Igorw\\Foo::bar');
use Silex\Application;
use Symfony\Component\HttpFoundation\Request;
namespace Igorw
{
class Foo
{
public function bar(Request $request, Application $app)
{
...
}
}
}

Categories