I recently wrote a post about request forwarding in Silex, which used a blog example to explain sub requests in Silex.
I use a slightly modified version of this example for a domain controller.
The path to the domain endpoint = /product/domain
A domain can also have a webhosting package attached to it.
The url path for this endpoint would be /product/domain/(id)/webhosting/
You can fetch info about a webhosting package by using the url path.
The url path for this endpoint would be /product/domain/(id)/webhosting/(id)
To handle these sub requests, I have a method called forwardRequest, which has no parameters in it's method signature, but uses func_get_args to keep it dynamic.
Unfortunately this doesn't work as Silex uses the named parameters in your route to call your method. So if you have /product/domain/domain_id/webhosting/webhosting_id, your method should have a signature of method($domain_id, $webhosting_id), which is a PITA if you want to forward multiple endpoints through one method. If you have additional /product/domain/domain_id/emailhosting and /product/domain/domain_id/dns endpoints, you have to create a method for each in order to forward the request.
Does anyone have a solution in which I can use only 1 method to forward all these sub requests?
Note: I'm using PHP 5.3.
The part of silex that decides which arguments to pass to the controller is called the "controller resolver". The default controller resolver uses reflection. You can override the controller_resolver service with a custom implementation though.
Defining a custom controller resolver that wraps the existing one but replaces the arguments with a single one, the request:
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
class RequestArgumentControllerResolver implements ControllerResolverInterface
{
protected $resolver;
public function __construct(ControllerResolverInterface $resolver)
{
$this->resolver = $resolver;
}
public function getController(Request $request)
{
return $this->resolver->getController($request, $controller);
}
public function getArguments(Request $request, $controller)
{
return [$request];
}
}
Extend the existing controller resolver with the newly defined decorator:
$app['controller_resolver'] = $app->share($app->extend('controller_resolver', function ($resolver, $app) {
return new RequestArgumentControllerResolver($resolver);
}));
Note: This is just one way of doing it. You don't have to decorate, you can also replace the resolver completely if you like. And obviously this is just a very basic example of only passing a single arg to the controller, you can do something more sophisticated.
Related
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
According to :
Silex - Service Controller Doc
I can define a route like this (after a couple of extra code of corse):
$app->get('/posts.json', "posts.controller:indexJsonAction");
But ... how can I pass the url used to the indexJsonAction function?
You should be mapping that directly to the route, such as:
$app->get('/posts.json/{param1}/{param2}, 'posts.controller:indexJsonAction');
This way, in your controller, you can expect those parameters:
public function indexJsonAction($param1, $param2) {
//now you have access to these variables.
}
Furthermore, silex uses Symfony's request under the hood, so you could also just inject the Request into the controller and get any input from the Request;
public function indexJsonAction(Request $request) {
// use $request->get('param1'); etc
}
I am busy building a Restful API in Laravel 5.1 where the API version is passed through the header. This way I can version the features rather than copy and pasting a whole route group and increment the version number.
The problem I'm having is that I would like to have versioned methods, IE:
public function store_v1 (){ }
I have added a middleware on my routes where I capture the version from the header, but now need to modify the request to choose the correct method from the controller.
app/Http/routes.php
Route::group(['middleware' => ['apiversion']], function()
{
Route::post('demo', 'DemoController#store');
}
app/Http/Middleware/ApiVersionMiddleware.php
public function handle($request, Closure $next)
{
$action = app()->router->getCurrentRoute()->getActionName();
// dd($action)
// returns "App\Http\Controllers\DemoController#store"
}
From here on, I would attach the header version to the $action and then pass it through the $request so that it reaches the correct method.
Well, that is the theory anyway.
Any ideas on how I would inject actions into the Route?
I think Middleware might not be the best place to do that. You have access to the route, but it doesn't offer a away to modify the controller method that will be called.
Easier option is to register a custom route dispatcher that handling the logic of calling controller methods based on the request and the route. It could look like that:
<?php
class VersionedRouteDispatcher extends Illuminate\Routing\ControllerDispatcher {
public function dispatch(Route $route, Request $request, $controller, $method)
{
$version = $request->headers->get('version', 'v1'); // take version from the header
$method = sprintf('%s_%s', $method, $version); // create target method name
return parent::dispatch($route, $request, $controller, $method); // run parent logic with altered method name
}
}
Once you have this custom dispatcher, register it in your AppServiceProvider:
public function register() {
$this->app->singleton('illuminate.route.dispatcher', VersionedRouteDispatcher::class);
}
This way you'll overwrite the default route dispatcher with your own one that will suffix controller method names with the version taken from request header.
Sort of a gross alternative is to create symlinks in your public folder that point back to the public folder. Use middleware to read the url and set a global config that can be used in your controllers to determine what to show. Not ideal but it works.
I'm a CI user now moving on to Laravel. Regarding routing, how do I make a method accessible via the method name alone for instance:
/controller/mymethod
becomes accessible as
/mymethod
In short I'm looking for the Laravel equivalent of CI's
$route['(method1|method2)'] = 'controller/$1';
You need to register a single GET route.
Route::get('method', 'controller#method');
You can create more complex routes to match multiple methods using back references.
Route::get('(method1|method2|method3)', 'controller#(:1)');
In Laravel 4 you take a different approach. You would instead register the controller itself with the router, and list the URI for it to respond to as /.
Route::controller('/', 'HomeController');
This HomeController would then contain methods prefixed with the HTTP verb you want them to respond to.
class HomeController extends Controller {
public function getIndex()
{
return 'Home Page';
}
public function getAbout()
{
return 'About Page';
}
}
The getIndex method will respond to root requests, e.g., localhost/your-project/public. All other requests will be something like localhost/your-project/public/about, etc.
Take for example the following controller/action:
public function indexAction()
{
return $this->render('TestBundle:TestController:index.html.twig');
}
I would like to write the template expression (or whatever it's name is) this way:
public function indexAction()
{
return $this->render('*:TestController:index.html.twig');
}
So that symfony knows I'm looking for a template in this very bundle. Having to write the whole Owner + Bundle for every template/action/repository I want to refer is very annoying. Even more so considering most of the time I refer to actions and templates in the same bundle.
NOTE: I know templates can be put at the app level and be refernced like this:
'::index.html.twig'
But that is not what I need.
It's possible with a bit of custom code.
Basically, you want to override the controller's render() method and include logic to fetch the name of the current bundle.
Note that instead of my controllers extending Symfony\Bundle\FrameworkBundle\Controller\Controller, they extend a custom controller (which then extends Symfony's controller). This allows you to conveniently give the controller more ability by adding your own methods.
Ex:
MyBundle\Controller\MyController\ extends MyCustomBaseController which extends Symfony\Bundle\FrameworkBundle\Controller\Controller.
So, in my custom controller I have these two methods:
public function render($view, array $parameters = array(), Response $response = null) {
$currentBundle = $this->getCurrentBundle();
$view = str_replace('*', $currentBundle, $view);
return parent::render($view, $parameters, $response);
}
public function getCurrentBundle() {
$controller = $this->getRequest()->attributes->get('_controller');
$splitController = explode('\\', $controller);
return $splitController[1];
}
Take a look at render(). It fetches the current bundle name and uses it to build the $view variable. Then it just calls parent::render() and it's as if you had manually defined the bundle name in the render statement.
The code here is very simple, so you should be able to easily extend it to do other things, such as allow you to also avoid typing the controller name.
Important: If you do use a custom controller, make sure you use Symfony\Component\HttpFoundation\Response, otherwise PHP will complain that the method signatures for render() don't match.