symfony routing, use value from Request as default - symfony

I've got the following route definition
my_route:
path: /actual-path/
defaults:
_controller: MyBundle:MyController:detail
id: application_id
requirements:
methods: GET
id: \d+
The controller requires a parameter called $id.
But I don't want to use the $id in the url, I want to use a value that is available in $request->attributes->get('application_id')
There is a listener that will inject two parameters (application_id and application) into the request object as attributes prior to the routing process, so this value is in there. (It would be easy to also inject it into the RequestContext).
Is there a way I can use attributes values from the Request or RequestContext object in my routing as defaults?
Now I could simply do $request->attributes->get('application_id') in my controller. But this controller will be used in several cases. In other cases the $id is to be passed through the url. I find it cleaner to set the id in the routing than build a if-else clause in the controller somewhere.

It appears not to be possible to do "mapping" between variables in the request object and the parameters you need to be required in your route/controller-action. I think this would be a good thing, as it would become quite complex otherwise.
I went with the solution to extend the controller and build in a small switch there.
Basically, if the route specifies id === null, it will do a fallback to application_id that is in the Request object. Otherwise it will use the value provided. I just need to set a requirement on id on the routes that must not use the fallback.
All without running an additional Listener which might be a bit "expensive" in processing time (for each request).
Example of routes:
my_route:
path: /actual-path/
defaults:
_controller: MyBundle:MyController:detail
id: null
requirements:
methods: GET
my_other_route:
path: /other-path/{id}
defaults:
_controller: MyBundle:MyController:detail
requirements:
methods: GET
id: \d+
And how to handle this in your controller:
// fallback to system id
if ($id === null) {
$id = $request->attributes->get('administration_id', null);
}
Because you extend the controller/action, you could also change the name of the parameter in the action (as long as the type does not change).
I did not do this, as I could quite easily put the switch between the provided id and the fallback from the listener in another method.

Related

MVC 5 Multiple Routes to Same Controller

In our RouteConfig.cs file we have the following default route:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Original", action = "Index", id = UrlParameter.Optional },
namespaces: new[] { "Path.To.Controllers" }
);
Our application is split into several different "Areas". This particular route works perfectly fine.
We were asked to change one of our URLs, however the underlying codebase is the same. In an effort to avoid breaking existing links out there I'd like to setup my controller to support two different routes:
Here's an example of what the original URL looks like:
website.com/MyArea/Original
With the aforementioned "Default" route in place, this will be directed to the OriginalController in the MyArea Area, and will hit the default Index action since none was specified.
My goal is to setup another URL that will also direct itself to the OriginalController. That is,
website.com/MyArea/Other
Should route to the OriginalController in the MyArea Area, and hit the default Index action.
I've added the following to the RouteConfig.cs file:
routes.MapRoute(
name: "Other",
url: "Other/{action}/{id}",
defaults: new { controller = "Original", action = "Index", id = UrlParameter.Optional },
namespaces: new[] { "Path.To.Controllers" }
);
What I'm finding is that the Default route config is always used in favor of the Other route config, which causes a binding issue stating "OtherController could not be found". I suspect this is because they have a very similar signature in the url template, but I'm not entirely sure how to get around that.
I'm aware that there's a Route attribute also, which I'm not opposed to using. I unfortunately was unsuccessful in getting that setup correctly though.
After researching and attempting several different combinations I still can't get both URLs to route to one controller.
What am I doing wrong?
I was able to get the expected result using RouteAttribute on the controller itself (thank you #Michael for the resources and making me take another look at the RouteAttribute), rather than conventional MapConfig routing. As I described in my question above, I was having difficulties when attempting the Route approach in that I was receiving 404 errors stating the "resource could not be found".
It turns out the above errors were due to the fact that my attribute routing wasn't being wired up in the correct order, which forced the conventional route to be used (e.g. Default MapConfig) over my Route attributes.
I stumbled upon this SO post which states:
You are probably combining convention based routing with attribute
routing, and you should register your areas after you map the
attribute routes.
The line
AreaRegistration.RegisterAllAreas(); should be called AFTER this line:
routes.MapMvcAttributeRoutes();
When working with Areas, you must register those areas after you register the attribute routing. I was originally registering my areas in the Application_Start method of Globas.asax.cs, which is called before the RouteConfig.RegisterRoutes. I moved this registration to right below my MapMvcAttributeRoutes call in the RouteConfig.cs file, which allowed the following route attribute on the controller to work as expected:
[RouteArea("MyArea")]
[Route("Original/{action=index}", Order = 1)]
[Route("Other/{action=index}", Order = 0)]
public class OriginalController : Controller {
...
...
public async Task<ActionResult> Index() { ... }
}
With the above in place, I can now navigate to either of the below URLs which will properly route to the "Index" action of my OriginalController:
website.com/MyArea/Original
website.com/MyArea/Other
This works. However, I do have another action defined that choked up the attribute routing and caused the conventional Default route (defined via the MapConfig function) to be used. My action signature:
public async Task<ActionResult> Details(int id) {
...
}
The route to this action is: website.com/MyArea/Original/Details/123, which also satisfies the default conventional route of {area}/{controller}/{action}/{id}.
The way around this was to go a step further with defining route attributes at the action level:
[Route("Original/Details/{id:int}")]
[Route("Other/Details/{id:int}")]
public async Task<ActionResult> Details(int id) {
...
}
Now my Route Attributes are found first, then the conventional route is used.

How to add MessageHandler for a specific controller that is using Routing Attributes in ASP.NET WebAPI 2?

It is possible to add a MessageHandler only for a specific controller that is using Route Attributes?
I want to cut the request earlier in the pipeline if it doesn't contain certain headers. I want to
mention that:
I can't add another route in WebApiConfig, we must use the Routing Attributes from the controller.
I don't want to add the MessageHandler globally.
It has to be a MessageHandler (early in the pipeline). We have alternatives for this but we are trying to do this more efficient.
For example, I've decorated the controller with the following RoutePrefix: api/myapicontroller and one action with Route(""). (I know it is strange, we are selecting a different action based on querystring)
Then, I've added
config.Routes.MapHttpRoute(
name: "CustomRoute",
routeTemplate: "api/myapicontroller/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: null,
handler: new myMessageHandler()
);
If I put this code before config.MapHttpAttributeRoutes(); the myMessageHandler is executing but I get this Message:
No action was found on the controller 'myapicontroller' that matches
the request
If I put config.MapHttpAttributeRoutes(); first, the myMessageHandler is never executed but the my action inside myapicontroller is called.
Unfortunately, you can not set any handlers via AttributeRouting. If you want to assign handler to specific routes you have to register it via MapHttpRoute only. Except you need to add your controller name in defaults section like in Ajay Aradhya's answer and remove Route attribute from your action, because you are allowed to register routes either with Route attribute or MapHttpRoute method, but not both at the same time.
Also note that you need to create pipeline, otherwise you handler will work but request will not hit the controller action.
See my answer on similar question for details.
This article from MS docs explains the same. At last they provide a way to have controller specific handlers.But thats for the conventional routing. I don't know if this helps you or not.
config.Routes.MapHttpRoute(
name: "MyCustomHandlerRoute",
routeTemplate: "api/MyController/{id}",
defaults: new { controller = "MyController", id = RouteParameter.Optional },
constraints: null,
handler: HttpClientFactory.CreatePipeline(new HttpControllerDispatcher(config), new MyCustomDelegatingMessageHandlerA());
);

Symfony redirect to route passing url slug

I've searched this on SO previously, but it seems that the only answers given are when parameters are passed as a query string, which is not what I want.
I have a page in my Symfony 3 CRM with a route app_litter. There is a required URL slug called litter_id which needs to be passed to the route in order to determine which litter data to show, as follows:
/litter/1
My route definiton in the routing file is:
app_view_litter:
path: /litter/{id}
defaults: { _controller: AppBundle:Litter:view, id: null }
requirements:
id: \d+
There is a function which allows a user to add puppies to their litter, which is a form outside of this page - once the puppies are successfully saved, I want to redirect the user back to the litter in question, so I did this:
return $this->redirectToRoute('app_litter', array('litter_id' => $litter->getId()));
Where $litter is the object retrieved from Symfony. However, this redirects to:
/litter?litter_id=1
Which does not work, as it expects a slug. Is it possible to use redirectToRoute() (or any other method) to render the route with a slug instead of a query string?
Because your route definition is:
app_view_litter:
path: /litter/{id}
defaults: { _controller: AppBundle:Litter:view, id: null }
requirements:
id: \d+
When using the route generator you need to provide an associated array with keys corresponding to the name of your placeholders which is in your case id.
Therefore, it must be:
$this->redirectToRoute('app_litter', array('id' => $litter->getId()));
If you want to use a slug, and there something which not only composed of digits (\d+), you have to either define a new route or modify the existing.
app_view_litter_using_slug:
path: /litter/{slug}
defaults: { _controller: AppBundle:Litter:view, slug: null }
requirements:
id: .*
And you something like:
$this->redirectToRoute('app_litter_using_slug', array('slug' => $litter->getSlug()));
Could it be that you are using the wrong route? Try using app_view_litter instead of app_litter:
return $this->redirectToRoute('app_view_litter', array('id' => $litter->getId()));

Symfony2, no route found and defaults

Simple question but I have issues with it.
We have simple route
profile_api_info:
pattern: /api/info/{apiID}
defaults: { _controller: SiteProfileBundle:Api:info, apiID: null}
When we use such url as
http://some.site/api/info/123
we'll get proper result of controller.
But when we use this one
http://some.site/api/info/
we'll have an error, why?
No route found for "GET /profile/api/info/"
We'll have already setuped 'defaults' for our 'apiID' but symfony2 says that no route. Can someone suggest how to deal with it? I want routes
http://some.site/api/info
http://some.site/api/info/
have the same controller as
http://some.site/api/info/123
but with 'apiID' = null or false, no matter.
You have two options.
Option 1:
Pass your default parameter.
profile_api_info:
pattern: /api/info/{apiID}
defaults: { _controller: SiteProfileBundle:Api:info, apiID: null}
However you will not be able to have trailing /.
This would be correct: http://some.site/api/info
This would be incorrect http://some.site/api/info/
Option 2:
set up an additional route.
(This would be my preference.)
profile_api_info_woId:
pattern: /api/info/
defaults: { _controller: SiteProfileBundle:Api:info}
In your controller make sure set the default for $apiID to null.
public function infoAction($apiID = null){...}
Using two routes with one controller method should work for all of the following urls:
http://some.site/api/info
http://some.site/api/info/
http://some.site/api/info/123
I've ran into this multiple times and clearing the cache so that Symfony can rebuild the route definitions usually fixes the issue.
Syntax and everything looks correct. However, the route with the trailing slash (http://some.site/api/info/) won't work but http://some.site/api/info should.

Symfony2 multi-level dynamic router

I have a current project that has to displays both defined pages with specific entities, what is very easy to manage with Symfony2, and content pages on different layouts, what is - I guess - a bit less common.
I get in trouble trying to build the routing system.
For instance, if I have to display a page with some news,
I would like to update the router of my bundle with a new route like :
my_bundle_news_page:
pattern: /news
defaults:
_controller: MyBundle:NewsController:indexAction
But how to manage a dynamic router that could have a totally custom URL on many levels ?
Let's imagine I've got a "Page" Entity, that is self-references for an optionnal "parent-child" relation.
I don't think I can just use any config YAML file for this specific routing ?!
my_bundle_custom_page:
pattern: /{slug}
defaults:
_controller: MyBundle:PageController:showAction
This would bind all the first-level pages:
/projects
/about
/contact
/our-project
What about a page that would be displayed with, for instance, a slug like:
/our-project/health
In fact any URL...
/{slug-level1}/{slug-level2}/{slug-level3} etc.
Cause the pages are supposed to change and be updated from webmastering.
I guess the best way would be to have a router that compare the {slug} with a database field (entity property)
I read in the Symfony-CMF doc that it is possible to write a service based a route provider:
namespace MyBundle\Routing;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route as SymfonyRoute;
use MyBundle\Entity\PageRepository;
class RouteProvider extends PageRepository {
public function findPageBySlug($slug)
{
// Find a page by slug property
$page = $this->findOneBySlug($slug);
if (!$page) {
// Maybe any custom Exception
throw $this->createNotFoundException('The page you are looking for does not exists.');
}
$pattern = $page->getUrl(); // e.g. "/first-level/second-level/third-level"
$collection = new RouteCollection();
// create a new Route and set our page as a default
// (so that we can retrieve it from the request)
$route = new SymfonyRoute($pattern, array(
'page' => $page,
));
// add the route to the RouteCollection using a unique ID as the key.
$collection->add('page_'.uniqid(), $route);
return $collection;
}
}
But how to set it up as a service ? Are there some requirements ?
How could this kind of thing work, does it add a route to the RouteCollection when request is called ?
And will I be able to bind any route in this way ?
EDIT : services.yml of my bundle
parameters:
cmf_routing.matcher.dummy_collection.class: Symfony\Component\Routing\RouteCollection
cmf_routing.matcher.dummy_context.class: Symfony\Component\Routing\RequestContext
cmf_routing.generator.class: Symfony\Cmf\Bundle\RoutingBundle\Routing\ContentAwareGenerator
cmf_routing.nested_matcher.class: Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
cmf_routing.url_matcher.class: Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
fsbcms.chain_router.class: Symfony\Cmf\Component\Routing\ChainRouter
fsbcms.route_provider.class: FSB\CMSBundle\Routing\RouteProvider
fsbcms.dynamic_router.class: Symfony\Cmf\Component\Routing\DynamicRouter
fsbcms.route_entity.class: null
services:
fsbcms.router:
class: %fsbcms.chain_router.class%
arguments:
- "#logger"
calls:
- [setContext, ["router.request_context"]]
fsbcms.route_provider:
class: "%fsbcms.route_provider.class%"
arguments:
- "#doctrine"
cmf_routing.matcher.dummy_collection:
class: "%cmf_routing.matcher.dummy_collection.class%"
public: "false"
cmf_routing.matcher.dummy_context:
class: "%cmf_routing.matcher.dummy_context.class%"
public: false
cmf_routing.generator:
class: "%cmf_routing.generator.class%"
arguments:
- "#fsbcms.route_provider"
- "#logger"
calls:
- [setContainer, ["service_container"]]
- [setContentRepository, ["cmf_routing.content_repository"]]
cmf_routing.url_matcher:
class: "%cmf_routing.url_matcher.class%"
arguments: ["#cmf_routing.matcher.dummy_collection", "#cmf_routing.matcher.dummy_context"]
cmf_routing.nested_matcher:
class: "%cmf_routing.nested_matcher.class%"
arguments: ["#fsbcms.route_provider"]
calls:
- [setFinalMatcher, ["cmf_routing.url_matcher"]]
fsbcms.dynamic_router:
class: "%fsbcms.dynamic_router.class%"
arguments:
- "#router.request_context"
- "#cmf_routing.nested_matcher"
- "#cmf_routing.generator"
tags:
- { name: router, priority: 300 }
I suggest taking a look at the Symfony CMF routing component and the CmfRoutingBundle (to implement the component in symfony).
The Routing component uses a chain router, which is irrelevant for this question but it's good to know. The chain router chains over a queue of routers. The component provides a DynamicRouter that uses a NestedMatcher. That's exactly what you want.
The NestedMatcher uses a Route provider to get the routes from a dynamic source (e.g. a database). You are showing an example of a Route provider in your question.
Furthermore, it uses a FinalMatcher to match the route. You can just pass an instance of Symfony\Cmf\Component\Routing\NestedMatcher\UrlMatcher, as you are doing not too difficult things.
Take a look at the docs of the RoutingBundle to learn how to activate the chain router and then create a route provider which loads the routes, make a service:
acme_routing.route_provider:
class: Acme\RoutingBundle\Provider\DoctrineOrmProvider
arguments: ["#doctrine"]
Now, you can create a NestedMatcher service:
acme_routing.url_matcher:
class: Symfony\Cmf\Component\Routing\NestedMatcher\UrlMatcher
arguments: ["#cmf_routing.matcher.dummy_collection", "#cmf_routing.matcher.dummy_context"]
acme_routing.nested_matcher:
class: Symfony\Cmf\Component\Routing\NestedMatcher
arguments: ["#acme_routing.route_provider"]
calls:
- [setFinalMatcher, ["acme_routing.url_matcher"]]
Now, register the DynamicRouter and put it in the chain:
acme_routing.dynamic_router:
class: Symfony\Cmf\Component\Routing\DynamicRouter
arguments:
- "#router.request_context"
- "#acme_routing.nested_matcher"
- "#cmf_routing.generator"
tags:
- { name: router, priority: 300 }
Now, it should work and should load the routes from the database and match them against the request.

Resources