📜 ⬆️ ⬇️

LexikFormFilterBundle, create filter forms even faster

Immediately, briefly and in the case of what types of filters you can use out of the box with the customization already created:

BooleanFilterType
CheckboxFilterType
ChoiceFilterType
CollectionAdapterFilterType
DateFilterType
DateRangeFilterType
DateTimeFilterType
DateTimeRangeFilterType
DocumentFilterType
EmbeddedFilterTypeInterface
EntityFilterType
NumberFilterType
NumberRangeFilterType
SharedableFilterType
TextFilterType

Judging from the names, it should be approximately clear what exactly this or that filter does, and nothing prevents you from creating your own filters or expanding existing ones.

I will not dwell here on how to create tables from our entities.
The article is aimed at people who own at least the basics of working with this wonderful framework.

TK


I will consider an example where the situation with the requirement for the filter is as complicated as possible. We will have to face the task: to create a search form for events on the site with various filters and make it as flexible as possible.
')
In this case, the filter should filter out the data in the database by the following criteria:

1) The period of time when this event should take place.
2) According to the status and category of the event, which are related entities.
3) For a certain number of connections with another entity, we will make a slider that allows you to filter out the activities according to the minimum and maximum number of current participants registered in the system.
4) At the minimum and maximum price.

Installation


Load the necessary dependencies

composer require lexik/form-filter-bundle 

Register the bundle in the application:

 $bundles = array( ... new Lexik\Bundle\FormFilterBundle\LexikFormFilterBundle(), 

In our case, the MySQL database, so the minimum configuration for a bundle is as follows:

 lexik_form_filter: listeners: doctrine_orm: true doctrine_dbal: false doctrine_mongodb: false 

That's all, now we have to spend quite a bit of time trying to make our filter form.

Implementation


To begin, we will create our entities and database:

The essence of the event (Meet):

Code
 namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * Meet * * @ORM\Table(name="meet") * @ORM\Entity(repositoryClass="AppBundle\Repository\MeetRepository") */ class Meet { /** * @var int * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(type="string") */ private $title; /** * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Category") */ private $category; /** * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Status") */ private $status; /** * @ORM\Column(type="decimal") */ private $price = 0; /** * @ORM\Column(type="datetime") */ private $startDate; /** * @ORM\Column(type="datetime") */ private $endDate; /** * @ORM\OneToMany(targetEntity="AppBundle\Entity\Participant", mappedBy="meet") */ private $participants; /** * Get id * * @return int */ public function getId() { return $this->id; } /** * Constructor */ public function __construct() { $this->participants = new \Doctrine\Common\Collections\ArrayCollection(); } /** * Set title * * @param string $title * * @return Meet */ public function setTitle($title) { $this->title = $title; return $this; } /** * Get title * * @return string */ public function getTitle() { return $this->title; } /** * Set price * * @param string $price * * @return Meet */ public function setPrice($price) { $this->price = $price; return $this; } /** * Get price * * @return string */ public function getPrice() { return $this->price; } /** * Set startDate * * @param \DateTime $startDate * * @return Meet */ public function setStartDate($startDate) { $this->startDate = $startDate; return $this; } /** * Get startDate * * @return \DateTime */ public function getStartDate() { return $this->startDate; } /** * Set endDate * * @param \DateTime $endDate * * @return Meet */ public function setEndDate($endDate) { $this->endDate = $endDate; return $this; } /** * Get endDate * * @return \DateTime */ public function getEndDate() { return $this->endDate; } /** * Set category * * @param \AppBundle\Entity\Category $category * * @return Meet */ public function setCategory(\AppBundle\Entity\Category $category = null) { $this->category = $category; return $this; } /** * Get category * * @return \AppBundle\Entity\Category */ public function getCategory() { return $this->category; } /** * Set status * * @param \AppBundle\Entity\Status $status * * @return Meet */ public function setStatus(\AppBundle\Entity\Status $status = null) { $this->status = $status; return $this; } /** * Get status * * @return \AppBundle\Entity\Status */ public function getStatus() { return $this->status; } /** * Add participant * * @param \AppBundle\Entity\Participant $participant * * @return Meet */ public function addParticipant(\AppBundle\Entity\Participant $participant) { $this->participants[] = $participant; return $this; } /** * Remove participant * * @param \AppBundle\Entity\Participant $participant */ public function removeParticipant(\AppBundle\Entity\Participant $participant) { $this->participants->removeElement($participant); } /** * Get participants * * @return \Doctrine\Common\Collections\Collection */ public function getParticipants() { return $this->participants; } } 


The essence of the status of the event (Status):

Code
 namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * Status * * @ORM\Table(name="status") * @ORM\Entity(repositoryClass="AppBundle\Repository\StatusRepository") */ class Status { /** * @var int * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(type="string") */ private $title; /** * Get id * * @return int */ public function getId() { return $this->id; } /** * Set title * * @param string $title * * @return Status */ public function setTitle($title) { $this->title = $title; return $this; } /** * Get title * * @return string */ public function getTitle() { return $this->title; } } 


Essence of category of event (Category):

Code
 namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * Category * * @ORM\Table(name="category") * @ORM\Entity(repositoryClass="AppBundle\Repository\CategoryRepository") */ class Category { /** * @var int * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(type="string") */ private $title; /** * Get id * * @return int */ public function getId() { return $this->id; } /** * Set title * * @param string $title * * @return Category */ public function setTitle($title) { $this->title = $title; return $this; } /** * Get title * * @return string */ public function getTitle() { return $this->title; } } 


An intermediate entity that connects the user and many-to-many activities (turns the user into a participant) (Participant):

Code
 namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * Participant * * @ORM\Table(name="participant") * @ORM\Entity(repositoryClass="AppBundle\Repository\ParticipantRepository") */ class Participant { /** * @var int * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\ManyToOne(targetEntity="AppBundle\Entity\User", inversedBy="participants") */ private $user; /** * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Meet", inversedBy="participants") */ private $meet; /** * Get id * * @return int */ public function getId() { return $this->id; } /** * Set user * * @param \AppBundle\Entity\User $user * * @return Participant */ public function setUser(\AppBundle\Entity\User $user = null) { $this->user = $user; return $this; } /** * Get user * * @return \AppBundle\Entity\User */ public function getUser() { return $this->user; } /** * Set meet * * @param \AppBundle\Entity\Meet $meet * * @return Participant */ public function setMeet(\AppBundle\Entity\Meet $meet = null) { $this->meet = $meet; return $this; } /** * Get meet * * @return \AppBundle\Entity\Meet */ public function getMeet() { return $this->meet; } } 

The very essence of the user (User):

 namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * User * * @ORM\Table(name="user") * @ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository") */ class User { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; } 


Great, we have created a structure with which you can already work. Now you can start creating the class of our form:

 namespace AppBundle\Filter; use Lexik\Bundle\FormFilterBundle\Filter\Query\QueryInterface; use AppBundle\Entity\Category; use AppBundle\Entity\Status; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Lexik\Bundle\FormFilterBundle\Filter\Form\Type as Filters; class MeetFilter extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder->setMethod('GET'); $builder ->add('category', Filters\EntityFilterType::class, [ 'data_class' => Category::class, 'class' => Category::class ]) ->add('status', Filters\EntityFilterType::class, [ 'data_class' => Status::class, 'class' => Status::class ]) ->add('startDate', Filters\DateTimeRangeFilterType::class) ->add('participant_count', Filters\NumberRangeFilterType::class, [ 'apply_filter' => function (QueryInterface $filterQuery, $field, $values) { if (empty($values['value']['left_number'][0]) && empty($values['value']['right_number'][0])) { return null; } $start = !empty($values['value']['left_number'][0]) ? $values['value']['left_number'][0] : null; $end = !empty($values['value']['right_number'][0]) ? $values['value']['right_number'][0] : null; $paramName = sprintf('p_%s', str_replace('.', '_', $field)); $filterQuery->getQueryBuilder() ->leftJoin('meet.participants', 'pp') ->addSelect(sprintf('COUNT(pp) AS %s', $paramName)) ->addGroupBy('meet.id') ; if($start && $end) { $filterQuery->getQueryBuilder() ->having(sprintf('%s > %d AND %s < %d', $paramName, $start, $paramName, $end)); } elseif($start && !$end) { $filterQuery->getQueryBuilder() ->having(sprintf('%s > %d', $paramName, $start)); } elseif(!$start && $end) { $filterQuery->getQueryBuilder() ->having(sprintf('%s < %d', $paramName, $end)); } } ]) ->add('price', Filters\NumberRangeFilterType::class); } public function getBlockPrefix() { return 'item_filter'; } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'csrf_protection' => false, 'validation_groups' => array('filtering') // avoid NotBlank() constraint-related message )); } } 

Here the most interesting is Callback's apply_filter in option to participantCount filter.

This filter sets the minimum and maximum number of participants for the sample, which data is taken from the form. Next, we change the request, telling Doctrine that we want the request to now have data on the number of Meet's links with the Participant:

 $paramName = sprintf('p_%s', str_replace('.', '_', $field)); $filterQuery->getQueryBuilder() ->leftJoin('meet.participants', 'pp') ->addSelect(sprintf('COUNT(pp) AS %s', $paramName)) ->addGroupBy('meet.id') ; 

And we can only set the following conditions:

 if($start && $end) { $filterQuery->getQueryBuilder() ->having(sprintf('%s > %d AND %s < %d', $paramName, $start, $paramName, $end)); } elseif($start && !$end) { $filterQuery->getQueryBuilder() ->having(sprintf('%s > %d', $paramName, $start)); } elseif(!$start && $end) { $filterQuery->getQueryBuilder() ->having(sprintf('%s < %d', $paramName, $end)); } 

Which, regarding the situation, must perform:

1) If both parameters are set: maximum and minimum number of conditions - use both.
2) If only the minimum number of participants is specified - we are looking for events with a large number of the specified.
3) If only the maximum number of participants is specified - we are looking for events with a smaller number of the specified.

A form is created in the same simple way as any other form in symfony. From the new we see here only a set of new for us FieldType classes that will help us solve almost any data filtering problem, these types were discussed at the beginning of the article.

Now we write our controller:

 public function indexAction(Request $request) { $repository = $this->getDoctrine() ->getRepository('AppBundle:Meet'); $form = $this->get('form.factory')->create(MeetFilter::class); if ($request->query->has($form->getName())) { $form->submit($request->query->get($form->getName())); $filterBuilder = $repository->createQueryBuilder('meet'); $this->get('lexik_form_filter.query_builder_updater')->addFilterConditions($form, $filterBuilder); $filterBuilder->join('meet.status', 's'); $query = $filterBuilder->getQuery(); $form = $this->get('form.factory')->create(MeetFilter::class); } else { $query = $repository->createQueryBuilder('meet') ->join('meet.status', 's') ->getQuery(); } $meets = $query->getResult(); } 

Everything is simple: we check that if any data from the form has been sent, then we run addFilterConditions, telling it the form data and QueryBuilder from the Meet repository.

Further, the bundle service will deliver the necessary conditions to QueryBuilder itself, with which we will work.

As a result, the standard representation of our form looks like this:

image

I love it when everything is beautiful, so I just leave it here:

image

This bundle only complements our QueryBuilder object, allowing us later to decide what to do with it: transfer it to pagination or supplement it with some custom things.

As a result, our DQL will look something like this:

 SELECT meet, COUNT(pp) AS p_meet_participant_count FROM AppBundle\Entity\Meet meet LEFT JOIN meet.participants pp INNER JOIN meet.status s WHERE meet.category = :p_meet_category AND meet.status = :p_meet_status AND (meet.startDate <= '2017-01-31 00:00:00' AND meet.startDate >= '2017-01-01 00:00:00') AND (meet.price >= :p_meet_price_left AND meet.price <= :p_meet_price_right) GROUP BY meet.id HAVING p_meet_participant_count > 90 AND p_meet_participant_count < 256 

Github

Source: https://habr.com/ru/post/319346/


All Articles