Symfony2 form choice field - symfony

I have a Product and a Category entity.
Category entity is a adjacency list/materialized-path model.
Product has a relation to Category which has a relation back to Product
In my ProductType class I would like a selectmenu with all categories at a specific level grouped by parent name.
$builder->add('category', 'entity', array(
'label' => 'Category',
'class' => 'Test\AdminBundle\Entity\Category',
'property' => 'name',
'group_by' => 'parentName',
'query_builder' => function(\Doctrine\ORM\EntityRepository $er) {
$qb = $er->createQueryBuilder('c');
return $qb->where($qb->expr()->like('c.path', ':path'))
->orderBy('c.path', 'ASC')
->setParameter('path', '%/%/%');
},
));
Category got a getParentName method:
public function getParentName()
{
if (null === $this->getParent()) {
return null;
}
return $this->getParent()->getName();
}
It works as expected but a query is executed for every parent (a lot). If I make a join with parent the parent will also be selectable in the selectmenu which I don't want.
How do I limit the queries?
Or filter a joined result?

I was not fetchjoining... adding a select to the querybuilder made it work:
$qb = $er->createQueryBuilder('c');
return $qb->select('c, p')
->leftJoin('c.parent', 'p')
->where($qb->expr()->like('c.path', ':path'))
->orderBy('c.path', 'ASC')
->setParameter('path', '%/%/%');

Related

Symfony ChoiceType returning wrong values in dropdown

This is my code in the FormType class
$builder
->add ( 'projects' , ChoiceType::class , ['choices' => $this->getAllProjects ()
with above I am only able to get a 0-4 number in the dropdown (5 records in DB)
instead of showing these numbers I want to show available names
->add('projects', ChoiceType::class, [
'choices' => $this->getAllProjects(),
'choice_label' => function($choice, $key, $value) {
return $value->getName(); // you know your getter
//return $value['name']; //if the return of the getAllProjects is an associative array
},
])
The point is that, the $value inside the anonymous function is an elem from your countable (from the 'choices'):
symfony.com/forms/types/choice.html#choice-label

A single EntityType generating a big set of queries

I'm trying to provide a selection with >300 choices
$form->add('products', EntityType::class, array(
'class' => Product::class,
'query_builder' => function (ProductRepository $er) use ($customerId) {
return $er->QBByCustomer($customerId);
},
'choice_label' => 'l.name',
));
The QueryBuilder:
public function QBByCustomer($customer = null)
{
return $this->QB()
->addSelect('p.name AS HIDDEN name')
->join('p.customer', 'c')
->join('p.label', 'l')
->where('c.customer = :customer')
->setParameter('customer', $customer)
->addOrderBy('name')
;
}
When I render the form, Doctrine generates >300 queries to load each related objects.
Is there a way to tell Doctrine to use the labels from the QueryBuilder I gave, instead of firing as much query as selectable items ?
Since the displayed label is fetched from a linked entity (label), we have to help Doctrine to load it.
Here is how the QueryBuilder should look like:
'query_builder' => function (ProductRepository $er) use ($customerId) {
$qb = $er->QBByCustomer($customerId);
$qb->addSelect('partial p.{id}');
$qb->addSelect('partial l.{id, name}');
return $qb;
},
'choice_label' => 'label.name',

Symfony: how to add a sub set of items from big collection to entity type?

A car machine oil is applicable to several car models, but in my case, there are hundreds of car models in our system, I don't want to load them all to a page, then , let a user to choose specific car models, it would be better to use ajax call to get car model by car brand , car series. the user choose a sub collection of items , post these selected to server.
In the form type ,I add a form field like this below, as we know, if I don't set its option choices as empty array, the entity field will get all car models , which will result in huge performance penalty.
->add('applicableModels', 'entity', array(
'class' => 'VMSP\CarBundle\Entity\CarModel',
'choices'=>array(),
'multiple'=>true,
'property_path' => "modelName",
'label' => 'vmsp_product.product.form.applicable_model',
)
)
So, how do you add a sub set of items from a big collection , and assign these selected items to entity type?
The simple answer is that you can define a custom query builder for each element, you add to the form, i.e.
$data = $builder->getData();
\\...
->add('applicableModels', 'entity', array(
'class' => 'VMSP\CarBundle\Entity\CarModel',
'multiple' => true,
'property_path' => "modelName",
'label' => 'vmsp_product.product.form.applicable_model',
'query_builder' => function (EntityRepository $er) use ($data) {
$qb = $er->createQueryBuilder('e')
->where('e.manufacturer IN (:manufacturer)')
->setParameter('manufacturer', $data['manufacturer']);
return $qb->orderBy('e.name');
},
))
So you may have a sort of a "wizard" where user select a manufacturer, the page reloads, and the cars shown are only a subset.
In a very similar situation I ended up doing things a little differently, though. In the form, the choices are set to an empty array. On frontend, the options are filled via Ajax calling a separate API. Then on the form "pre-submit" event, I fill the field with the selected option.
$builder->addEventListener(
FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
if (!empty($data['applicable_model'])) {
$form->add('applicable_model', 'entity', array(
'class' => 'VMSP\CarBundle\Entity\CarModel',
'query_builder' => function (EntityRepository $er) use ($data) {
$qb = $er->createQueryBuilder('e')
->where('e.id IN (:applicable_model)')
->setParameter('applicable_model', $data['applicable_model']);
return $qb;
},
));
}
Update: I learned that the addEventListener part can probably be replaced by a DataTransforme instead, see https://stackoverflow.com/a/31938905/410761

Sonata admin, custom query in filter

I'm using SonataAdminBundle and I have a question about filters in the class MyEntityAdmin.
I have a first function protected function configureFormFields(FormMapper $formMapper) for list all fields to be shown on create/edit forms.
If I have a field type entity, I can do something like this:
->add('commercial', 'entity', array(
'class' => 'MyBundle:User',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('u')
->groupBy('u.id')
->orderBy('u.id', 'ASC')
->setParameters(array(1 => 'Commercial'));
},)
)
But I have another function protected function configureDatagridFilters(DatagridMapper $datagridMapper) for Fields to be shown on filter forms, and I have do to the same thing, a custom query on an entity field type, but if I do the same, I have the error:
No attached service to type named `entity`
How can I do that ?
Filters configuration is quite different than form configuration in sonata admin bundle.
Look at documentation: http://sonata-project.org/bundles/doctrine-orm-admin/master/doc/reference/filter_field_definition.html
When you add new filter through configuratDataFilters it receives parameters: field name, filter type, filter configuration, form field type and form field configuration.
So if you want only to override query_buider for entity choice type you should try call like this:
->add('commercial', null, array(), 'entity', array(
'class' => 'MyBundle:User',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('u')
->groupBy('u.id')
->orderBy('u.id', 'ASC')
->setParameters(array(1 => 'Commercial'));
}
))

Symfony2 - Give a default filter in a list of elements of Sonata Admin

I have a list of elements of type Vehicle and I show these elements with Sonata Admin. I allow to filter these elements by the "status" field, but I want that, when the list is showed, only the active vehicles are showed, and if somebody wants to see the inactive vehicles, uses the filter and select the inactive status. I would like to know if somebody Knows the way to apply filters by default for a list of elements using Sonata Admin.
Here is my code:
public function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('name')
->add('status')
;
}
protected function configureDatagridFilters(DatagridMapper $mapper)
{
$mapper
->add('name')
->add('status')
;
}
Is there any option that can be added to the status field in configureDatagridFilters() to achieve this goal? Other options?
Thanks in advance.
You have to override $datagridValues property as following (for status > 0 if it's an integer) :
/**
* Default Datagrid values
*
* #var array
*/
protected $datagridValues = array (
'status' => array ('type' => 2, 'value' => 0), // type 2 : >
'_page' => 1, // Display the first page (default = 1)
'_sort_order' => 'DESC', // Descendant ordering (default = 'ASC')
'_sort_by' => 'id' // name of the ordered field (default = the model id field, if any)
// the '_sort_by' key can be of the form 'mySubModel.mySubSubModel.myField'.
);
source: Configure the default page and ordering in the list view
You can also use this method
public function getFilterParameters()
{
$this->datagridValues = array_merge(
array(
'status' => array (
'type' => 1,
'value' => 0
),
// exemple with date range
'updatedAt' => array(
'type' => 1,
'value' => array(
'start' => array(
'day' => date('j'),
'month' => date('m'),
'year' => date('Y')
),
'end' => array(
'day' => date('j'),
'month' => date('m'),
'year' => date('Y')
)
),
)
),
$this->datagridValues
);
return parent::getFilterParameters();
}
Using both above suggested approaches will break the filters "reset" behaviour since we are always forcing the filter to filter by a default value. To me, i think the best approach is to use the getFilterParameters function (since we can add logic in there instead of statically add the value) and check if the user clicked the "Reset button"
/**
* {#inheritdoc}
*/
public function getFilterParameters()
{
// build the values array
if ($this->hasRequest()) {
$reset = $this->request->query->get('filters') === 'reset';
if (!$reset) {
$this->datagridValues = array_merge(array(
'status' => array (
'type' => 1,
'value' => 0
),
),
$this->datagridValues
);
}
}
return parent::getFilterParameters();
}
Since sonata-admin 4.0, the function getFilterParameters() is tagged as final and the $datagridValues doesn't exist anymore.
So you need to override the configureDefaultFilterValues() function
protected function configureDefaultFilterValues(array &$filterValues): void
{
$filterValues['foo'] = [
'type' => ContainsOperatorType::TYPE_CONTAINS,
'value' => 'bar',
];
}
More details: https://symfony.com/bundles/SonataAdminBundle/current/reference/action_list.html#default-filters
Another approach is to use createQuery and getPersistentParameters to enforce invisible filter. This approach is best to have fully customizable filters. See my articles here:
http://www.theodo.fr/blog/2016/09/sonata-for-symfony-hide-your-filters/

Resources