The problem
I have a web-server that generates an index page that runs Angular, which uses API calls to display data.
I have meta tags which are being set by Angular API calls too.
Now if I will post my website on Facebook, the tags will not be initialized because Facebook scrawler does not support Javascript.
Solution
Detect Facebook user-agent server-sided, and dedicate specific routes that will use a controller action that returns an index page already initialized with that meta data that facebook needs, using the API calls server sided.
I have tried using the condition option that I read here in Symfony router but I am getting the following error:
Resources/config/routing.yml"
contains unsupported keys for "bot_facebook_post": "condition"
And that's how my route looks in yml:
bot_facebook_post:
pattern: /locations/{location}
condition: "request.headers.get('User-Agent') matches 'facebookexternalhit/1.1'"
defaults: {
_controller: Main:bot, slug: ""
}
Related
I have a single route defined like this:
Route::resource('problem', 'ProblemController');
The moment I POST to /problem, a ProblemController#store method is fired.
Now what I want is to return a JSON response if it's an API call or a view (or maybe redirect) if I'm on the "web-side" of my application. How can I approach this problem?
Should I create separate controllers? Should I (in every method/controller) detect the type of the request and respond accordingly? Should I use middlewares? Route groups? Separate application?
The main goal is to have multiple application types (API + versioning + web) in one package but share the business logic, models and most of the code (to avoid repeating).
I am using Laravel 5.2.
Thank you!
Request object offers a method wantsJson() that checks Accept header of the request and returns TRUE if JSON was requested.
In your controller you can do the following:
if( request()->wantsJson() )
{
return ['foo' => 'bar'];
}
return view('foo.bar');
You can read more about content negotiation in Laravel here: http://fideloper.com/laravel-content-negotiation
You can create a route group like this:
Route::group(['prefix'=>'api'], function(){
//All routes in this route become domain.com/api/route
});
This makes the most sense to me because a route that returns a view and an API route are two separate things. You should have a controller for the pages and views you want to show in your app, and another one for the api routes that update and change your data, returning JSON.
I'm trying to setup routes for my Symfony2 single page app and I'm not sure how to go about it correctly without it feeling super hacky.
Here is what I need it to do and how I've attempted to set it up:
When Authenticated
Any route requesting application/jsonshould hit the routes they have been setup for as usual.
Any route that is entered not requesting application/json should load a controller that renders a twig file containing all the JS for my single page app.
Any static resource that doesn't exist and ends looking for a symfony route eg [.js, .css, .jpeg, etc] should return 404.
When NOT Authenticated
Anything requesting application/json should return 403
Anything NOT requesting application/json should return to the login page
Here is what i've attempted so far:
Setup routes with the FOSRestBundle for each service
Setup a listener that returns the base controller html if the request isn't application/json
if (!in_array('application/json', $request->getAcceptableContentTypes())) {
$fakeRequest = $event->getRequest()->duplicate(
null,
null,
array('_controller' => 'HvHDashboardBundle:Dashboard:index')
);
$controller = $this->resolver->getController($fakeRequest);
$event->setController($controller);
}
Setup a bunch of 'catch all' routes to fake a 404 if the static resource doesn't exist.
# routing.yml
# Catch any files that are meant to be their own static resource and return 404
catch_all_fail:
pattern: /{uri}.{_format}
defaults: { _controller: MyBundle:Dashboard:return404 }
requirements:
_format: js|hbs|css|jpg|gif|jpeg|png
Issues
This approach feels like a massive hack, and is not how the Symfony routing system is intended to work
The base controller page is returned even if you aren't authenticated because the type listener is being hit before the security context and forcing that controller to render.
Question:
How do other solve this issue with routing and single page apps with Symfony where they initially need to render HTML with twig, then JS takes over and requests JSON?
Only make an API, no static pages at all. Trust me, I recently did a moderate sized API with Symfony and that's the way to go. It will simplify your backend security a lot, if you do not mix the API with static pages. Ofcourse you can still have static pages, if you want to have some sort of Landing page or something. but try not to mix them with the main app.
Even for login, do not make a static page, but instead have an api route that will validate username/password and return the user the auth token in response. One user can have multiple tokens for example (can be logged in at multiple locations), and the token is sent in the request headers everytime.
If the token is validated ok, symfony will know which user it belongs, so you will know the 'User'. If no token is present, it should return "Not Authenticated", and if token is invalid also something like that or 'Bad request'.
One thing that I had to do when doing with APIs is that I had to write a request listener to accept JSON content and transform it into request object, so I could access data with $request->request.
If you have any questions, let me know in comment and I can help.
As far as routing is concerned, follow the REST rules and you will be good to go.
I heve an embedded controller in my base template. It's a search bar.
For the search bar controller, I have a route "myProject/search".
What I would like is that this route will be taken only when the template where I am embedding the controller (base.html.twig) will call it, and not when i manually put in the browser: "myproject/search".
Any idea on how to do that.
I think, since some time you can't do it:
http://symfony.com/doc/current/book/templating.html#embedding-controllers
quote from the docs:
Even though this controller will only be used internally, you'll need
to create a route that points to the controller
(...)
Since Symfony 2.0.20/2.1.5, the Twig render tag now takes an absolute
url instead of a controller logical path. This fixes an important
security issue (CVE-2012-6431) reported on the official blog. If your
application uses an older version of Symfony or still uses the
previous render tag syntax, you should upgrade as soon as possible.
Anyway, I guess, you can try do it yourself by passing some "secret" argument to search action when you call it from your template. Next in the action you check if the argument was passed to it, and if not you throw 404.
Another way to achieve your goal is use .htaccess file.
You can restrict your route to a certain method by _method option in your routing configuration:
your_rote:
pattern: /myProject/search
defaults: { _controller: YourBundle:YourController:YourAction }
requirements:
_method: POST
I am trying to access my slug parameters outside my controller.
I have a Routing YML like this:
adgroup:
pattern: /adgroup/{id}
defaults: { _controller: ExampleBundle:AdGroup:index }
requirements:
id: \d+
..and a URL like this:
http://example.com/adgroup/25
I need a way to access the {id} variable without getting it from my controller, my controller is working perfectly. However, I am trying to build an Object that will rely heavily based on which Slugs are being passed in.
So far scouring the docs and many, many examples has left me no where.
What Ive tried:
I've var_dumped the entire Request::createFromGlobals method, and it's children to see if its stored in there in anyway. It does not appear so.
Also, since I am on a development environment, the debug toolbar in symfony clearly shows Request: id: 25 in the profiler.
So the question is... How do I get my slugs / slug values from outside the controller?
I have way too many controllers to attempt to pass them in from there 1 by 1, and hacking it from exploding the URL is just a bad idea. :-)
I imagine there is some method I am unaware of to access these?
I think this code will help you
$params = $container->get('request')->attributes->get('_route_params');
$id = $params['id'];
I have an application with people and groups in Symfony, where a person may have a membership with multiple groups. To remove a person from a group, I currently have an action in a 'groupMembers' module that takes a 'delete' method and removes the pair from a many-to-many entity 'group_membership'. The route currently looks something like this:
remove_membership:
url: /group-membership/:group_id/:person_id
class: sfPropelRoute
options: { model: GroupMembership, type: object }
param: { module: groupMembers, action: remove }
requirements:
sf_method: [delete]
To perform this action, the user needs to be logged in, so I restricted it using sfGuard in the module's security.yml:
remove:
is_secure: true
So after clicking a 'remove' link, a user who isn't logged in is taken to the log in screen, but after clicking 'submit' the request is no longer a 'delete' but a 'get', meaning the similar add_to_group route is called instead!
add_to_group:
url: /group-membership/:group_id/:person_id
param: { module: groupMembers, action: create }
...
Is there any way to make sfGuard emulate a delete action and pass the parameters properly, or will I have to use a different route?
It seems that there is no way to achieve this without writing custom code or editing code of sfGuard plugin.
See plugins/sfGuardPlugin/modules/sfGuardAuth/lib/BasesfGuardAuthActions.class.php (its executeSignin method for details) how sign in is handled.
sfGuard gets user referrer and performs redirect with appropriate method of sfAction. This redirect is performed by using http header Location. So browser will use GET method to receive url content.
You can override default signin action and perform redirects from remove_membership route to an action which will use sfBrowser component to emulate POST request, but I highly recommend you to change routing scheme.