Symfony2 forms, search and SEO friendly Urls - php

Ok, this is the problem:
I have a search form where I have several input fields to specify a search request. I use Symfony2 forms to benefit from validation, CSRF-protection and all the good jazz.
Now, I want the URLs of the search results to be both SEO friendly and bookmarkable.
e.g.
http://www.example.com/blue-car-in-Berlin-200km/
My current approach after doing some research is building the desired search slug and redirecting to another action like so:
//some form code
if($searchForm->isValid()){
$searchWidget = $searchForm->getData();
if(!empty($searchWidget->getSearch()))
$slug = $searchWidget->getSearch();
if(!empty($searchWidget->getPlace()))
$slug .= '-in-' . $searchWidget->getPlace()->getName());
if(!empty($searchWidget->getDistance()))
$slug .= '-' . $searchWidget->getDistance().'km';
return $this->redirectToRoute('app_search', array('search'=>$slug));
}
With the second controller which is basically supposed to look like this:
public function searchAction(Request $request, $search)
{
//extract actual terms out of the $search - slug
....
//find a way to inject the terms in the current request-Object (dirty)
...
//do all validation again
}
As already stated in the code this feels really inefficient and cumbersome. Isn't there a better way to do all this? Like having a URL representation which is independent of the actual logic?
Furthermore, is there even a clean solution to use all the benefits of symfony form without actual request parameters but with that request slug?.
Thanks

You could define the route for the search action like that (I'm using annotations here):
/**
* #Route("/search/{search}-in-{place}-{distance}km", name="app_search")
* #Method({"GET"})
*/
public function searchAction(Request $request, $search, $place, $distance)
{
// Your code here
}
Of course this is valid if those three search parameters are the only one needed and only if they are all three mandatory; if the search parameters are not mandatory, you have to define more routes to match all the possible combinations (and I think this is not the right way).
A simpler solution is maybe to create a dynamic slug (it can have one or more values) with some sort of fixed formatting from which you can easily extract all the desired search values. But at this point I would like to ask: why don't you use some simple GET parameters (something like /search?search=blue+car&place=berlin&distance=200)?
Update
Expanding the idea of a flexible search string, you could try something like that:
/**
* #Route("/search/{searchString}", name="app_search")
* #Method({"GET"})
*/
public function searchAction(Request $request, $searchString)
{
// Your code here
}
searchString can be something like that ("<...>" is a placeholder for the real data like "city-berlin"):
city-<...>/distance-<...>/place-<...>
You just have to explode by '/' and then, for each piece, explode by '-' and use the first piece as ID for what to search. The first explode should return something like that:
[
'city-<...>',
'distance-<...>',
'place-<...>'
]
The second explode on each of those elements should return
[ 'city', '<...>' ]
This method is the most flexible because the order of the search parameters doesn't matter and no parameters is mandatory for the analysis.

I think you need SEO bundle for this.

Related

How To Implement "defaults()" For Multiple Params In A Route - Laravel

So I have a POST URL with two parameters and I want to assign default values for both parameters .
I know you can implement this way for a URL with a single param:
Route::post('activity-log/datatable/{tag_access?}/{page_access?}',
'SettingsController#datatable_activity_log')
->defaults('tag_access', 'activity-log');
But how do i go about it with a URL that looks like this:
Route::post('activity-log/datatable/{tag_access?}/{page_access?}',
'SettingsController#datatable_activity_log')
You can achieve it by following way:
Keep your route as you want like this:
Route::post('activity-log/datatable/{tag_access?}/{page_access?}','SettingsController#datatable_activity_log')
Now, In controller function you can take these parameters with default value like this,
public function datatable_activity_log($tag_access='activity-log', $page_access='activity-log', Request $request){
// Here write your logic
}
This may not be the best way to achieve what you want but this is one of the way.
From what I see regarding the usage of defaults you can either do one at a time:
Route::post('activity-log/datatable/{tag_access?}/{page_access?}',
'SettingsController#datatable_activity_log')
->defaults('tag_access', 'activity-log')
->defaults('page_access', 'defaultValue');
An alternative (since defaults is public) is to do:
$route = Route::post('activity-log/datatable/{tag_access?}/{page_access?}',
'SettingsController#datatable_activity_log');
$route->defaults = [ 'tag_access' => 'activity-log', 'page_access' => 'defaultValue' ];
My personal favourite is what #Sagar Gautam suggest which is to use default function parameters.

Symfony 2 URL Parameters?

I am used to Yii's URL management:
site.com/controller/action/var1/value1/var2/value2
That way I can put my variables in any order in the URL or omit one and having a default value asigned:
public function actionFoo( $var1=22, $var2 ) { //Var1 is optional, Var2 must come.
}
Is there such thing in Symfony? I've searched but I found only hard-coded URL positions, like CodeIgniter, and that's something I find very annoying, because if I have a parameter in the middle of the URL I need to necessarily give it a value.
Example:
site.com/controller/action/false/false/value3
Maybe what I am used too is a bad practice, and I am open to learn other way.
Thanks
Symfony doesn't work that way.
If you need to map a value to a variable name then you need to use query strings: ?var1=value1&var2=value2.
Then, in your controller you can do $this->get('request')->query->get('var1', false) (false being the default value if var1 isn't set).
Otherwise you can define a default value in your route but it doesn't work the way you expect it to work.
You could extend the Routing component to mimic Yii's routing system but I wouldn't advise it because it would require some time.
All-in-all query strings will do exactly what you expect.
I have also found it to be a bit annoying as well, but with a little bit of work you can make the URLs work in Symfony by setting multiple routes to the same controller method. The catch is the order does matter.
This example use annotation but you could do the same thing with YAML, XML and PHP.
/**
* #Route("/whatever/{var2}/{var1}", name="whatever_allvars")
* #Route("/whatever/{var2}", name="whatever_onevar")
* #Template()
*/
public function actionFoo( $var1=22, $var2 ) {
//Var1 is optional, Var2 must come.
}
this would allow the following URLs:
site.com/whatever/var2/var1
site.com/whatever/var2
if you really want to make it match the sample URL give you could set it up like this.
/**
* #Route("/controller/action/var1/{var1}/var2/{var2}", name="whatever_allvars")
* #Route("/controller/action/var1/{var1}", name="whatever_onevar")
* #Template()
*/
public function actionFoo( $var1, $var2=22 ) {
//Var2 is optional, Var1 must come.
}
This would allow the following URLs:
site.com/controller/action/var1/value1/var2/value2
site.com/controller/action/var1/value1
Hopefully this gives you an idea what can be done in Symfony.

How to define routes with multiple parameters without pretty urls in Laravel

I am using Laravel. I would like users to be able to perform a search on my website using up to 3 criteria. These criteria are: Class, Brand and Model.
They should be free to use any or all of them when searching. As the relationship between these isn't as simple as Many->1, Many->1, Many->1, and also given the criteria will be numbered if blank, I dont want to use pretty urls to post the search criteria as they would look like this:
/SearchResults/0/BMW/0
which is meaningless to users and search engines. I therefore want to use normal dynamic addresses for this route as follows:
/SearchResults/?Class=0&Brand="BMW"&Model=0
How do I define a route that allows me to extract these three criteria and pass it to a custom method in my resource controller?
I have tried this but it isnt working:
Route::get('/SearchResults/?Class={$class}&Brand={$brand}&Model={$type}', 'AdvertController#searchResults');
Many thanks
The Symfony Routing components fetch the REQUEST_URI server variable for matching routes, and thus Laravel's Route Facade would not pick up URL parameters.
Instead, make use of Input::get() to fetch them.
For example, you would start by checking if the class param exists by using Input::has('class'), and then fetching it with Input::get('class'). Once you have all three, or just some of them, you'd start your model/SQL query so that you may return your results to the user.
You will need to route all to the same method and then, within the controller, reroute that given action to the correct method within the controller.
For that, I recommend using the strategy pattern (read more here).
I would do something like this:
route.php
Route::get('/SearchResults', 'AdvertController#searchResults');
AdvertController.php
use Input;
...
private $strategy = [];
public function __construct(){
$strategy = [
/*class => handler*/
'0'=> $this->class0Handler,
'1'=>$this->class1Handler,
...];
}
private function class0Handler(){
//your handler method
}
public function searchResults(){
if( !array_key_exists(Input::get('class'),$this->strategy))
abort(404);
return $this->strategy[Input::get('class')]();
}
In case you are breaking down search by other types, you define the handler in the $strategy variable.
Strategy pattern has a lot of benefits. I would strongly recommend it.

Codeigniter variable-length parameter list

I'm making a tutorialsystem with Codeigniter, but I'm a bit stuck with using subcategories for my tutorials.
The URL-structure is like this: /tutorials/test/123/this-is-a-tutorial
tutorials is the controller
test is a shortcode for the category
123 is the tutorial ID (used in the SQL query)
this-is-a-tutorial is just a slug to prettify the URL
What I do is passing the category as a first parameter and the ID as a second parameter to my controller function:
public function tutorial($category = NULL, $tutorial_id = NULL);
Now, if I want subcategories (unlimited depth), like: /tutorials/test/test2/123/another-tutorial. How would I implement this?
Thanks!
For reading infinite arguments, you have at least two useful tools:
func_get_args()
The URI class
So in your controller:
Pop the last segment/argument (this is your slug, not needed)
Assume the last argument is the tutorial ID
Assume the rest are categories
Something like this:
public function tutorial()
{
$args = func_get_args();
// ...or use $this->uri->segment_array()
$slug = array_pop($args);
$tutorial_id = array_pop($args); // Might want to make sure this is a digit
// $args are your categories in order
// Your code here
}
The rest of the code and the validation depends on what specifically you want to do with the arguments.
If you need variable categories you could use the URI class: $this->uri->uri_to_assoc(n)
Another option you may want to consider is CodeIgniter's controller function remapping functionality which you can use to override the default behaviour. You would be able to define a single function inside a controller that would handle all the calls to that controller and have the remaining URI parameters passed in as an array. You could then do whatever you want with them.
See here for the docs reference on the matter.

Filtering with symfony2

Is there any open source (or example) code for Symfony2 which can filter certain model using multiple parameters? A good example of what I'm looking for can be seen on this Trulia web page.
http://www.trulia.com/for_sale/30000-1000000_price/10001_zip/
http://www.trulia.com/for_rent/Chicago,IL/#for_rent/Chicago,IL/0-500_price/wd,dw_amenities/sm_dogs_pets"
http://www.trulia.com/for_rent/Chicago,IL/#for_rent/Chicago,IL/400-500_price/wd,dw_amenities
http://www.trulia.com/for_rent/Chicago,IL/#for_rent/Chicago,IL/wd,dw_amenities"
http://www.trulia.com/for_rent/Chicago,IL/#for_rent/Chicago,IL/400p_price/dw,cs_amenities
http://www.trulia.com/for_rent/Chicago,IL/#for_rent/Chicago,IL/1p_beds/1p_baths/400p_price/dw,cs_amenities
Note how URL are build when clicking in the form, I guess is using one controller for all this routes, How is it done?.
I Don't think it will be redirecting all the possible routes to a specific controller, (shown below), maybe some sort of dynamic routing?
/**
* #Route("/for_rent/{state}/{beds}_beds/{bath}_bath/{mix_price}-{max_price}_price /{amenities_list}
* #Route("/for_rent/{state}/{mix_price}-{max_price}_price/{amenities_list}
* #Route("/for_rent/{state}/{bath}_bath/{mix_price}-{max_price}_price/{amenities_list}
* #Route("/for_rent/{state}/{mix_price}_price/{amenities_list}
* #Route("/for_rent/{state}/{beds}_beds/{bath}_bath/{amenities_list}
* ........
*/
public function filterAction($state, $beds, $bath, $min_price, $max_price ....)
{
....
}
Thanks.
For simple queries (i.e. where you don't need to have a data range, such as min-max value), you can use the entity repository to find entities by the request parameters given. Assuming that your entity is Acme\FooBundle\Entity\Bar:
$em = $this->getDoctrine()->getEntityManager();
$repo = $em->getRepository('AcmeFooBundle:Bar');
$criteria = array(
'state' => $state,
'beds' => $beds,
// and so on...
);
$data = $repo->findBy($criteria);
When building the $criteria array, you'll probably want some logic so that you only sort by criteria that have been provided, instead of all possible values. $data will then contain all entities that match the criteria.
For more complex queries, you'll want to look into DQL (and perhaps a custom repository) for finer-grained control of the entities that you're pulling out.
To construct your routes, i'm sure you had a look at the Routing page of the documentation, but did you notice that you can put requirements on routes? This page explains how to do it with annotations.
As for the filtering, I suppose DQL would be ok, but you can also write straight up SQL with Doctrine, and map the results of your query to one or more entities. This is described here. It may be more flexible than DQL.
csg, your solution is good (with #Route("/search/{q}) if you only need to use routing in "one-way". But what if you will need to print some price filter links on page accessible by url:
http://www.trulia.com/for_sale/30000-1000000_price/10001_zip/
In case of #Route("/search/{q} you will not be able to use route method url generate with params.
There is a great Bundle called LexikFormFilterBundle "lexik/form-filter-bundle": "~2.0" that helps you generate the complex DQL after the Filter form completed by the user.
I created a Bundle, that depends on it, that changes the types of a given FormType (like the one generated by SencioGeneratorBundle) So you can display the right FilterForm and then create the DQL after it (with Lexik).
You can install it with Composer, following this README.md
All it does is override the Doctrine Type Guesser, that suggests the required FormType for each Entity field, and replace the given Type by the proper LexikFormFilterType. For instance, replaces a simple NumberType by a filter_number which renders as two numbers, Max and Min interval boundaries.
private function createFilterForm($formType)
{
$adapter = $this->get('dd_form.form_adapter');
$form = $adapter->adaptForm(
$formType,
$this->generateUrl('document_search'),
array('fieldToRemove1', 'fieldToRemove2')
);
return $form;
}
Upon form Submit, you just give it to Lexik and run the generated query, as shown in my example.
public function searchAction(Request $request)
{
// $docType = new FormType/FQCN() could do too.
$docType = 'FormType/FQCN';
$filterForm = $this->createFilterForm($docType);
$filterForm->handleRequest($request);
$filterBuilder = $this->getDocRepo($docType)
->createQueryBuilder('e');
$this->get('lexik_form_filter.query_builder_updater')
->addFilterConditions($filterForm, $filterBuilder);
$entities = $filterBuilder->getQuery()->execute();
return array(
'entities' => $entities,
'filterForm' => $filterForm->createView(),
);
}

Categories