Video Tutorial Create a product filter


In this video I propose to discover together how to create a product filter system on the Symfony framework. the objective is to allow the user to select the products according to the different categories, a minimum and maximum price and to be able to organize the products by price or by promotion.

This tutorial will be followed by another tutorial devoted to the implementation of a dynamic filter using JavaScript (which will refresh the listing products without necessarily having to submit the form or reload the page).

The search filter

The key point of our system is the design of the filter that will allow the user to search the products. To create this system we will start by creating an object that will represent the data of the search. This object will be a simple PHP object that will have as property the various search options.

<? Php
namespace App  Data;

use App  Entity  Category;

class SearchData
{

    / **
     * @var int
     * /
    public $ page = 1;

    / **
     * @var string
     * /
    public $ q = '';

    / **
     * @var Category ()
     * /
    public $ categories = ();

    / **
     * @var null | integer
     * /
    public $ max;

    / **
     * @var null | integer
     * /
    public $ min;

    / **
     * @var boolean
     * /
    public $ promo = false;

}

The creation of such an object allows to know the form of the parameters that will be passed to the search system (compared to the use of a simple table).
Then we will have to create the form that will complete our search:

add ('q', TextType :: class, (
                'label' => false,
                'required' => false,
                'attr' => (
                    'placeholder' => 'Search'
                )
            ))
            -> add ('categories', EntityType :: class, (
                'label' => false,
                'required' => false,
                'class' => Category :: class,
                'expanded' => true,
                'multiple' => true
            ))
            -> add ('min', NumberType :: class, (
                'label' => false,
                'required' => false,
                'attr' => (
                    'placeholder' => 'Min price'
                )
            ))
            -> add ('max', NumberType :: class, (
                'label' => false,
                'required' => false,
                'attr' => (
                    'placeholder' => 'Max price'
                )
            ))
            -> add ('promo', CheckboxType :: class, (
                'label' => 'In promotion',
                'required' => false,
            ))
        ;
    }

    public function configureOptions (OptionsResolver $ resolver)
    {
        $ Resolver-> setDefaults ((
            'data_class' => SearchData :: class,
            'method' => 'GET',
            'csrf_protection' => false
        ));
    }

    public function getBlockPrefix ()
    {
        return '';
    }

}

The advantage here is that we can rely on the types of fields offered by Symfony to directly have the necessary filters. Note the use of a GET method that will pass the parameters in the url and the method getBlockPrefix () which allows you to remove the prefix in order to have the simplest possible parameters.

Treatment of research

The processing of the search will be done simply at the level of the controller thanks to the use of the class of form that one created previously.


    
    / **
     * @Route ("/", name = "product")
     * /
    public function index (ProductRepository $ repository, Request $ request)
    {
        $ data = new SearchData ();
        $ data-> page = $ request-> get ('page', 1);
        $ form = $ this-> createForm (SearchForm :: class, $ data);
        $ Form-> handleRequest ($ request);
        $ products = $ repository-> findSearch ($ data);
        return $ this-> render ('product / index.html.twig', (
            'products' => $ products,
            'form' => $ form-> createView ()
        ));
    }

The advantage is that we can now send the object representing our research to our repository to perform the product search.

        / **
     * Collect products related to a search
     * @return PaginationInterface
     * /
    public function findSearch (SearchData $ search): PaginationInterface
    {

        $ query = $ this
            -> createQueryBuilder ( 'p')
            -> select ('c', 'p')
            -> join ('p.categories', 'c');

        if (! empty ($ search-> q)) {
            $ query = $ query
                -> andWhere ('p.name LIKE: q')
                -> setParameter ('q', "% {$ search-> q}%");
        }

        if (! empty ($ search-> min)) {
            $ query = $ query
                -> andWhere ('p.price> =: min')
                -> setParameter ('min', $ search-> min);
        }

        if (! empty ($ search-> max)) {
            $ query = $ query
                -> andWhere ( 'p.price <= :max')
                ->setParameter ('max', $ search-> max);
        }

        if (! empty ($ search-> promo)) {
            $ query = $ query
                -> andWhere ('p.promo = 1');
        }

        if (! empty ($ search-> categories)) {
            $ query = $ query
                -> andWhere ('c.id IN (: categories)')
                -> setParameter ('categories', $ search-> categories);
        }

        return $ this-> paginator-> paginate (
            $ Query,
            $ SEARCH-> page,
            9
        );
    }

    private function getSearchQuery (SearchData $ search, $ ignorePrice = false): QueryBuilder
    {
    }

Our search is relatively complex and may contain several parameters we will prefer to set up a custom query rather than rely on what is offered by default bundle paginator. On the other hand for the organization part of the contents we will let KnpPaginatorBundle manage things.

The price filter

To offer a more pleasant user interface in terms of price selection we will use a slider system. This system will allow the user to change the minimum price and the maximum price by simply drag and drop.

import noUiSlider from 'nouislider'
import 'nouislider / distribute / nouislider.css'

const slider = document.getElementById ('price-slider')

if (slider) {
    const min = document.getElementById ('min')
    const max = document.getElementById ('max')
    const minValue = Math.floor (parseInt (slider.dataset.min, 10) / 10) * 10
    const maxValue = Math.ceil (parseInt (slider.dataset.max, 10) / 10) * 10
    const range = noUiSlider.create (slider, {
        start: (min.value || minValue, max.value || maxValue),
        connect: true,
        step: 10,
        tidy: {
            'min': minValue,
            'max': maxValue
        }
    })
    range.on ('slide', function (values, handle) {
        if (handle === 0) {
            min.value = Math.round (values ​​(0))
        }
        if (handle === 1) {
            max.value = Math.round (values ​​(1))
        }
    })
    range.on ('end', function (values, handle) {
        if (handle === 0) {
            min.dispatchEvent (new Event ('change'))
        } else {
            max.dispatchEvent (new Event ('change'))
        }
    })
}

The problem is that we have to find the minimum price and the maximum price of our product listing. We can rest for this on the research that we have already done (by removing the criteria related to the price). This will allow us to extract a minimum price and a maximum price that we can use at our slider.

        / **
     * Collect products related to a search
     * @return PaginationInterface
     * /
    public function findSearch (SearchData $ search): PaginationInterface
    {
        $ query = $ this-> getSearchQuery ($ search) -> getQuery ();
        return $ this-> paginator-> paginate (
            $ Query,
            $ SEARCH-> page,
            9
        );
    }

    / **
     * Get the minimum and maximum price for a search
     * @return integer ()
     * /
    public function findMinMax (SearchData $ search): array
    {
        $ results = $ this-> getSearchQuery ($ search, true)
            -> select ('MIN (p.price) as min', 'MAX (p.price) as max')
            -> getQuery ()
            -> getScalarResult ();
        return ((int) $ results (0) ('min'), (int) $ results (0) ('max'));
    }

    private function getSearchQuery (SearchData $ search, $ ignorePrice = false): QueryBuilder
    {
        $ query = $ this
            -> createQueryBuilder ( 'p')
            -> select ('c', 'p')
            -> join ('p.categories', 'c');

        if (! empty ($ search-> q)) {
            $ query = $ query
                -> andWhere ('p.name LIKE: q')
                -> setParameter ('q', "% {$ search-> q}%");
        }

        if (! empty ($ search-> min) && $ ignorePrice === false) {
            $ query = $ query
                -> andWhere ('p.price> =: min')
                -> setParameter ('min', $ search-> min);
        }

        if (! empty ($ search-> max) && $ ignorePrice === false) {
            $ query = $ query
                -> andWhere ( 'p.price <= :max')
                ->setParameter ('max', $ search-> max);
        }

        if (! empty ($ search-> promo)) {
            $ query = $ query
                -> andWhere ('p.promo = 1');
        }

        if (! empty ($ search-> categories)) {
            $ query = $ query
                -> andWhere ('c.id IN (: categories)')
                -> setParameter ('categories', $ search-> categories);
        }

        return $ query;
    }

And we use the attributes "data" to send this information to our javascript.

{{form_start (form, {attr: {class: 'filter js-filter-form'}})}}

  

  

  {{form_row (form.q)}}

  

Categories

{{form_row (form.categories)}}

Price

{{form_row (form.min)}}
{{form_row (form.max)}}

Specials

{{form_row (form.promo)}} {{form_end (form)}}