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(),
);
}
Related
I have a simple website running Laravel Jetstream with Teams enabled. On this website, you can create different "to-do tasks", which are owned by a team. I have a model called Task.
I am trying to create a public facing API, so my users can query their tasks from their own applications. In my routes/api.php file, I have added this:
Route::middleware('auth:sanctum')->group(function(){
Route::apiResources([
'tasks' => \App\Http\Controllers\API\TaskController::class,
]);
});
And then in the TaskController, I have only begun coding the index method:
/**
* Display a listing of the resource.
* #queryParam team int The team to pull tasks for.
* #return \Illuminate\Http\Response
*/
public function index()
{
if(request()->team){
$tasks = Task::where('team_id', request()->team)->get();
return TaskResource::collection($tasks);
}
return response([
'status' => 'error',
'description' => "Missing required parameter `team`."
], 422);
}
Now this works fine. I can make a GET request to https://example.org/api/tasks?team=1 and successfully get all the tasks related to team.id = 1.
However, what if I want to include multiple query parameters - some required, others only optional. For example, if I want to let users access all tasks with a given status:
https://example.org/api/tasks?team=1&status=0
What is the best practices around this? As I can see where things are going now, I will end up with a lot of if/else statement just to check for valid parameters and given a correct error response code if something is missing.
Edit
I have changed my URL to be: https://example.org/api/teams/{team}/tasks - so now the team must be added to the URL. However, I am not sure how to add filters with Spatie's Query Builder:
public function index(Team $team)
{
$tasks = QueryBuilder::for($team)
->with('tasks')
->allowedFilters('tasks.status')
->toSql();
dd($tasks);
}
So the above simply just prints:
"select * from `teams`"
How can I select the relationship tasks from team, with filters?
The right way
The advanced solution, i have built a handful of custom ways of handling search query parameters. Which is what you basically wants to do, the best solution by far is the spatie package Query Builder.
QueryBuilder::for(Task::class)
->allowedFilters(['status', 'team_id'])
->get();
This operation will do the same as you want to do, and you can filter it like so.
?fields[status]=1
In my experience making team_id searchable by team and similar cases is not worth it, just have it one to one between columns and input. The package has rich opportunities for special cases and customization.
The simple way
Something as straight forward like your problem, does not need a package off course. It is just convenient and avoids you writing some boiler plate code.
This is a fairly simple problem where you have a query parameter and a column you need to search in. This can be represented with an array where the $key being the query parameter and $value being the column.
$searchable = [
'team' => 'team_id',
'status' => 'status',
];
Instead of doing a bunch of if statements you can simplify it. Checking if the request has your $searchables and if act upon it.
$request = resolve(Request::class);
$query = Task::query();
foreach ($this->seachables as $key => $value) {
if ($query->query->has($key)) {
$query->where($value, $query->query->get($key))
}
}
$tasks = $query->get();
This is a fairly basic example and here comes the problem not going with a package. You have to consider how to handle handle like queries, relationship queries etc.
In my experiences extending $value to an array or including closures to change the way the logic on the query builder works can be an option. This is thou the short coming of the simple solution.
Wrap up
Here you have two solutions where actually both are correct, find what suits your needs and see what you can use. This is a fairly hard problem to handle pragmatic, as the simply way often gets degraded as more an more explicit search functionality has to be implemented. On the other side using a package can be overkill, provide weird dependencies and also force you into a certain design approach. Pick your poison and hope at least this provides some guidance that can lead you in the right direction.
According to FOSElasticaBundle documentation it is possible to configure application to use custom query builder method like this:
user:
persistence:
elastica_to_model_transformer:
query_builder_method: createSearchQueryBuilder
But is it possible to choose QB method live, e.g. in controller action?
I'd like to be able to control what's being fetched from DB while transforming Elastica results to Doctrine entities. E.g. sometimes I'll want to do eager fetch on some relations, but can't do that by default.
Since FOSElasticaBundle documentation is not very precise, I went through its code and found it impossible to control what query builder is used on controller level.
It is possible to change whole elastica_to_model_transformer to a custom service, but still it's statically defined in configuration. Maybe with some dirty solution it would be possible going this way, but I don't think it's worth it.
I decided to just not using this feature of FOSElasticaBundle. The main problem I had was that when you use fos_elastica.index instead of fos_elastica.finder or elastica repository (in order to get plain not transformed results Elastica\Resultset), there's no findPaginated method with returns Pagerfanta paginator object, which is very helpful in my case.
Fortunately although it's not mentioned in documentation it's possible to create the Pagerfanta this way too, but a little bit more manually.
Here's a code snippet:
//generate ElaticaQuery somehow.
$browseQuery = $browseData->getBrowseQuery();
$search = $this->container->get('fos_elastica.index.indexName.typName');
//create pagerfanta's adapter manually
$adapter = new \Pagerfanta\Adapter\ElasticaAdapterElasticaAdapter($search, $browseQuery);
// now you can create the paginator too.
$pager = new Pagerfanta($adapter);
//do some paging work on it...
$pager->setMaxPerPage($browseData->getPerPage());
try {
$pager->setCurrentPage($browseData->getPage());
} catch(OutOfRangeCurrentPageException $e) {
$pager->setCurrentPage(1);
}
//and get current page results.
/** #var Result[] $elasticaResults */
$elasticaResults = $pager->getCurrentPageResults();
// we have to grab ids manyally, but it's done the same way inside FOSElasticaBundle with previous approach
$ids = array();
foreach($elasticaResults as $elasticaResult) {
$ids[] = $elasticaResult->getId();
}
//use regular Doctrine's repository to fetch Entities any way you want.
$entities = $this->getDoctrine()->getRepository(MyEntity::class)->findByIdentifiers($ids);
This actually has a few advantages. In general it gives you back control over your data and doesn't tie ElasticSearch with Doctrine. Therefore you can resign on fetching data from Doctrine if you have all needed data in ElasticSearch (if they are read only data of course). This lets you optimize your application performance but reducing amount of SQL queries.
The code above may be wrapped with some kind of service in order to prevent making mess in controllers.
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.
I'm developing a Zend Framework 2 application, that should provide:
a search in projects with a result list of projects
a single project view
a search in images with a result list of images (every image belongs to a project)
Also there are two main entities: image and project andthe first idea was to create two modules (Image and Project) and to give to each of them a separate search fuctionaly. But it would mean much of duplicated code. OK, maybe a separate Search module? But then this module would essentially depend on the other modules.
What would be a good approach for such case? Are there a best practice for this?
I have written a ZF2 elastic search module but it is closed source. It enables modules to put searchable data into ES. I can't post the code, but I can explain how it works.
The central module is Search. The search contains two main interfaces: Search\Repository\ItemRepositoryInterface and Search\Service\ItemServiceInterface. The repository is for searching items, the service for storing/removing items.
interface ItemRepositoryInterface
{
public function search($query, $limit = 10);
}
interface ItemServiceInterface
{
public function insert(SearchableInterface $object);
public function remove(SearchableInterface $object);
}
SearchableInterface is an interface my models/entities can use to "make" it searchable. It let's ES set the ES id and grabs the type. Usually, every entity gets its own type (so I can search all images and projects, or query only for image types).
I have implemented this for a blog/event system. The service class which persists the blog article into the database triggers events and ElasticSearch is one of the listeners:
public function onBootstrap(MvcEvent $e)
{
$app = $e->getApplication();
$sm = $app->getServiceManager();
$em = $app->getSharedManager();
$em->attach('Blog\Service\ArticleService', 'create', function($e) use ($sm) {
$searchService = $sm->get('Search\Service\ItemService');
$article = $e->getArticle();
$searchService->insert($article);
});
}
Because Article implements SearchableInterface, this works great. Now my blog doesn't have to deal with search and search doesn't have to deal with the blog. But, how is the Article transformed into a search document, you wonder?
I have a hydrator mechanism which works like the ZF2 hydrator. It does not convert any object into an array (and vice versa). It transforms a SearchableInterface object into an Elastic Search Document (for storing the object) and it transforms an Elastic Search Result (which is returned after a search query) into a SearchableInterface object again.
interface HydratorInterface
{
public function extract(SearchableInterface $object);
public function hydrate(Result $object);
}
Every type has its own hydrator registered. All these different hydrators are collected into a HydratorManager which is basically a Zend\ServiceManager\AbstractPluginManager. This plugin manager is injected into the repository and service.
So, inside the service, the following happens:
The $object->getType() is checked
For its type, the corresponding hydrator is fetched
The hydrator's extract() is called to turn the $object into a $document
The underlying ES client is used to persist the document (either it is added or updated, depending on the result of $object->getElasticSearchId()
For the repository, given a query type:image name:Mountain, the following happens:
The repository's search() is called with given query
The string is used for a ES query object and is executed
The results are iterated
For every result, the type is checked
For its type, the corresponding hydrator is fetched
The hydrator's hydrate() is called to turn the $result into an $object
The collection of objects is returned
I would create some kind of "search interface" that each module would implement to search its own data. Then your search module could check all available modules if they contain this interface, and if they do, use it to search their data...
The drawback I guess is that search code is implemented in each module...
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.