Injecting Doctrine Entity into Symfony controller based on route parameters - php

I want to inject Doctrine entities into controller actions based on the route parameters in an attempt to reduce the insane amount of code duplication inside my controllers.
For example I have the following route
product:
path: /product/edit/{productId}
defaults: { _controller: ExampleBundle:Product:edit }
Instead of my current approach
public function editAction($productId)
{
$manager = $this->getDoctrine()->getManager();
$product = $manager->getRepository('ExampleBundle:Product')
->findOneByProductId($productId);
if (!$product) {
$this->addFlash('error', 'Selected product does not exist');
return $this->redirect($this->generateUrl('products'));
}
// ...
}
I'd like this to be handled else where as it's repeated in at least 6 controller actions currently. So it would be more along the lines of
public function editAction(Product $product)
{
// ...
}
It seems this has in fact been done before and the best example I can find is done by the SensioFrameworkBundle http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html
I'd use this but were not using annotations in our Symfony projects so need to look at alternatives. Any suggestions on how this can be achieved?

If you read the docs carefully, you'll learn that param converters actually work without annotations:
To detect which converter is run on a parameter the following process is run:
If an explicit converter choice was made with #ParamConverter(converter="name") the converter with the given name is chosen.
Otherwise all registered parameter converters are iterated by priority. The supports() method is invoked to check if a param converter can convert the request into the required parameter. If it returns true the param converter is invoked.
In other words if you don't specify a param converter in an annotation, Symfony will iterate through all registered converters and find the most appropriate one to handle your argument (based on a type hint).
I prefer to put an annotation in order to:
be explicit
save some processing time

Related

Custom collection operation and IRI conversion problem

I have a problem with Api Platform and custom collection operation, when I need to manually require an argument in the route.
My first need is to GET on this route: query/userjob/[USER UUID] and retrieve a collection of all jobs for the given user.
My second need is to be able to GET on query/userjob/[USER UUID]/[JOB UUID] and retrieve details for the given user's job.
It might be important to say that I have no Api resource nor entity User, so I exclude all kind of subresource mapping or query.
So, let's say i have a UserJob ApiResource mapped as below:
App\Domain\User\Projection\UserJob:
itemOperations:
get:
method: 'GET'
path: '/userjob/{userId}/{jobId}'
requirements:
userId: '%uuid_regex%'
jobId: '%uuid_regex%'
collectionOperations:
get:
method: 'GET'
path: '/userjob/{userId}'
requirements:
userId: '%uuid_regex%'
attributes:
route_prefix: "/query"
In the class, I have:
final class UserJob
{
public $id; //int Auto inc
public $userId; //a UUID
public $jobId; //a UUID
public function __construct($userId, $jobId)
{
$this->userId = $userId;
$this->jobId = $jobId;
}
public function getId(): int
{
return $this->id;
}
public function getUserId()
{
return $this->userId;
}
public function getJobId()
{
return $this->jobId
}
I built a custom data provider for this class, in which I wrote the way to get the resource from the giver parameter (userId):
public function getCollection(string $resourceClass, string $operationName = null)
{
$userId = $this->request->getCurrentRequest()->attributes->get('userId');
return $this->repository->entityManager->getRepository($resourceClass)->findByUserId($userId);
}
When i make a GET call to, let's say, query/userjob/148e3200-f793-447b-bde8-af6b7b27372c it throws an exception:
Unable to generate an IRI for App\Domain\User\Projection\UserJob
And if I debug deeper, in the IRIConverter class, I find that the original exception is thrown from Router:
Some mandatory parameters are missing ("userId") to generate a URL for route "api_user_jobs_get_collection".
Nevertheless, if i dump the result of $this->repository->entityManager->getRepository($resourceClass)->findByUserId($userId);, all the elements that i'm looking for are well fetched from database.
So my intuition is that somehow ApiPlatform process fails to build the collection IRI that we usually can find at the beginning of the payload, and which in my case would be query/userjob/148e3200-f793-447b-bde8-af6b7b27372c.
And it fails while on the normalization or serialization process, because the "extra" param of my custom operation (the user UUID) is not passed to the collection normalizer, iri converter classes, so it has no way to give to the router the missing param to build the "api_user_jobs_get_collection" route.
What am I missing here? Is this a well-known problem that has a readymade solution that I missed ?
Or do I have to look for:
decorate the IRI converter?
use a custom normalizer?
do something with composite ids?
something else?
Your use case may have more solutions, and it depends what is preferred:
decorate iri converter, as you are using identifier in collection and this is not supported out of the box by API platform, as per my knowledge. And this is best choice if url like this are the style of your api.
use custom controller action with custom url style (docs: https://api-platform.com/docs/core/controllers/#creating-custom-operations-and-controllers), best if this is rare url in your api
Annotate your ids as identifiers in your api resource class (doc: https://api-platform.com/docs/core/identifiers/#custom-identifier-normalizer)
/**
* #var Uuid
* #ApiProperty(identifier=true)
*/
public $code;
but I haven't tried this with multiple ids, and this may work only for item url.
You may try to use custom data provider (docs: https://api-platform.com/docs/core/data-providers/). This will need to be done per resource or globally (supports() method) and you will need somehow (regex?) extract ids from url from $context array in getCollection() and getItem() methods. But as ApiPlatform will try to generate item iri, you may still end up with decorating iri converter.
Note: Using id in collection url may lead to other problems, like OpenAPI documentation generation. You may consider if what you want is not filtering of collection by "id" field, nicely supported, or retrieving collection of only "your" items. Which can be done by your data provider injecting security or by doctrine query extensions if someone uses doctrine.

Controller as Service - How to pass and return values in an advanced case?

Using Symfony, I am displaying a table with some entries the user is able to select from. There is a little more complexity as this might include calling some further actions e. g. for filtering the table entries, sorting by different criteria, etc.
I have implemented the whole thing in an own bundle, let's say ChoiceTableBundle (with ChoiceTableController). Now I would like to be able to use this bundle from other bundles, sometimes with some more parametrization.
My desired workflow would then look like this:
User is currently working with Bundle OtherBundle and triggers chooseAction.
chooseAction forwards to ChoiceTableController (resp. its default entry action).
Within ChoiceTableBundle, the user is able to navigate, filter, sort, ... using the actions and routing supplied by this bundle.
When the user has made his choice, he triggers another action (like choiceFinishedAction) and the control flow returns to OtherBundle, handing over the results of the users choice.
Based on these results, OtherBundle can then continue working.
Additionally, OtherOtherBundle (and some more...) should also be able to use this workflow, possibly passing some configuration values to ChoiceTableBundle to make it behave a little different.
I have read about the "Controller as Service" pattern of Symfony 2 and IMHO it's the right approach here (if not, please tell me ;)). So I would make a service out of ChoiceTableController and use it from the other bundles. Anyway, with the workflow above in mind, I don't see a "good" way to achieve this:
How can I pass over configuration parameters to ChoiceTableBundle (resp. ChoiceTableController), if neccessary?
How can ChoiceTableBundle know from where it was called?
How can I return the results to this calling bundle?
Basic approaches could be to store the values in the session or to create an intermediate object being passed. Both do not seem particularly elegant to me. Can you please give me a shove in the right direction? Many thanks in advance!
The main question is if you really need to call your filtering / searching logic as a controller action. Do you really need to make a request?
I would say it could be also doable just by passing all the required data to a service you define.
This service you should create from the guts of your ChoiceTableBundleand let both you ChoiceTableBundle and your OtherBundle to use the extracted service.
service / library way
// register it in your service container
class FilteredDataProvider
{
/**
* #return customObjectInterface or scallar or whatever you like
*/
public function doFiltering($searchString, $order)
{
return $this->filterAndReturnData($searchString, $order)
}
}
...
class OtherBundleController extends Controller {
public function showStuffAction() {
$result = $this->container->get('filter_data_provider')
->doFiltering('text', 'ascending')
}
}
controller way
The whole thing can be accomplished with the same approach as lipp/imagine bundle uses.
Have a controller as service and call/send all the required information to that controller when you need some results, you can also send whole request.
class MyController extends Controller
{
public function indexAction()
{
// RedirectResponse object
$responeFromYourSearchFilterAction = $this->container
->get('my_search_filter_controller')
->filterSearchAction(
$this->request, // http request
'parameter1' // like search string
'parameterX' // like sorting direction
);
// do something with the response
// ..
}
}
A separate service class would be much more flexible. Also if you need other parameters or Request object you can always provide it.
Info how to declare controller as service is here:
http://symfony.com/doc/current/cookbook/controller/service.html
How liip uses it:
https://github.com/liip/LiipImagineBundle#using-the-controller-as-a-service

Using custom ParamConverter for POST Request in Symfony 2

I'm using Symfony 2.6 and the FOS Rest Bundle.
Param converters for PATCH , DELETE and GET requests work nicely and reduce the code in the controller actions. However for POST requests I have a problem. The default \Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter gets called every time. This results in an exception:
Unable to guess how to get a Doctrine instance from the request information.
I checked the \Sensio\Bundle\FrameworkExtraBundle\EventListener\ParamConverterListener and saw that it's always including the Doctrine param converter in the onKernelController method. From the documentation it seems that the doctrine param converter is automatically applied for all type hinted controller actions unless you set it to off:
sensio_framework_extra:
request:
converters: true
auto_convert: false
I found a kind of hacky way to prevent this. The array of param converters to be applied will be indexed by the name of the type hinted argument in the controller method (which symfony gets by reflection). If I just name my param converter the same as this type hint then the default doctrine param converter will not be added to the list of param converters. For example:
...
* #ParamConverter(
* "MyEntity",
* class="Foo\Bar\MyEntity",
* converter="my_custom_converter"
* )
*
* #param MyEntity $myEntity
* #return MyEntity
*/
public function postMyEntityAction(MyEntity $myEntity)
{
I sort of wrote this question as I was digging deeper into the code and I'm not even really sure what my question is anymore. I guess it's "Is it logical to apply multiple param converters?" or would also like to know if it's possible to turn off param converters for certain actions. Maybe my logic is completely wrong here and this isn't what param converters were intended for.
I'd appreciate any advice.
Alright, I realized where I was going wrong. It was a simple case of not returning true from my custom paramConverter apply method. If it does return true then the doctrine param converter won't be applied.

How do I pass a parameter to a validation constraint in Symfony2 - in yml

I am trying to add a bundle-wide parameter to my application so that I can add it to my Validation Constraint file (validation.yml):
myApp\myBundle\Entity\Contact:
properties:
name:
- NotBlank: { message: "%myvariable%" }
I added my parameter normally in config.yml:
parameters:
# Validation config
myvariable: Please tell us your name.
But the page just renders the %myvariable% text, rather than the desired string. I also wish to use this parameter in my FormBuilderInterface when adding the validation messages to the page for usage in JavaScript. Does yml allow this? If not, how do I include such a parameter at a higher level?
No, it's not currently possible.
It has nothing to do with YAML or XML or even service definitions. Validator component reads validation rules by itself - as you can see, the structure is quite different than for service definitions. Unfortunately, it does not replace the parameters in constraints.
The main logic resides in \Symfony\Component\Validator\Mapping\Loader\YamlFileLoader which is created by \Symfony\Component\Validator\ValidatorBuilder::getValidator.
You could make this happen by:
Overriding definition of validator.builder service.
It's constructed using %validator.builder.factory.class%::createValidatorBuilder, but as you have to get parameter bag somehow, there is not enough dependencies - class factory is in use, not service factory.
Creating new class, which extends ValidatorBuilder.
It should take parameter bag into constructor or via setter. It should be configured in step (1) to be passed here.
This class would create file loaders of another class (see 3), also pass that parameter bag into it.
Creating new classes for YamlFileLoader and YamlFilesLoader. Additional 2 for each format that you would want to support.
It would additionally take parameter bag into constructor and override some functionality. For example, I think all parameter handling could be done in newConstraint method - iterate through options, resolve parameters, then call parent method with replaced options.
It's nice that Symfony could be extended like that (possibly not so nicely in this use-case), but I guess it would be easier to just write your own constraint with custom constraint validator, which would inject that parameter into it.
Also consider a wrapper around validator service - if you just need to replace the validation messages, you could replace the validator service, injecting original one into it. See http://symfony.com/doc/current/service_container/service_decoration.html for more information.

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