User-filtered Product Listing

Many e-commerce stores let you filter their product listing to help you find what you are looking for faster. You often can filter by things like brand, category and price. This post demonstrates how you can acheive this in Perch with the official Shop app.

There’s already a tutorial on the official Perch documentation on how to create a user-filtered list. This post builds on it as we’ll:

What we are doing in this post can be applied to other contexts too, not just product listing.

In this post there are references to many Perch features and functions including:

Apps required:


The basics

A basic product listing with perch_shop_products() with no filtering:

perch_shop_products([
    'count' => 12,
    'paginate' => true,
]);

The above turns on pagination and lists 12 products per page.

Before we move to coding the solution, let’s explore filtering with hard-coded examples. This is a good approach to figure out what you need before attempting doing something programmatically.

Filter by one Category

To filter by Category, we add the category option. The value of the category option needs to be a Category path string.

Assuming our products Category Set is called Products and we want to filter for a Category inside it called Handbags, the Category path would be products/handbags:

perch_shop_products([
    'count' => 12,
    'paginate' => true,
    'category' => 'products/handbags',
]);

If you wanted to filter for a sub-category of Handbags, the Category path would be products/handbags/your-sub-category.

Filter by multiple categories

To filter by multiple Categories, the value of the category option needs to be an array of Category paths:

perch_shop_products([
    'count' => 12,
    'paginate' => true,
    'category' => ['products/handbags', 'products/sunglasses', 'products/dresses'],
]);

We can control the matching behaviour with the category-match option. If set to all, it will match the products that fall under all the listed Categories. If set to any, it will match the items any product that falls under any of the listed Categories.

Which one to use depends on your context. For example, listing apps that fall under all the listed Categories: Fitness, Android and iOS:

perch_shop_products([
    'count' => 12,
    'paginate' => true,
    'category' => ['products/apps/fitness', 'products/apps/android', 'products/apps/ios'],
    'category-match' => 'all',
]);

Listing tops that fall under any of the listed Categories:

perch_shop_products([
    'count' => 12,
    'paginate' => true,
    'category' => ['products/tops/t-shirts', 'products/tops/polo'],
    'category-match' => 'any',
]);

Filter by one Brand

perch_shop_products() uses the same filtering options as perch_content_custom(). So we use the filter, match and value options.

To filter by a Brand called Givenchy using the Brand’s slug:

perch_shop_products([
    'count' => 12,
    'paginate' => true,
    'filter' => 'brand.slug',
    'match' => 'eq',
    'value' => 'givenchy',
]);

Filter by multiple Brands

To filter by multiple Brands, we use 'match' => 'in', and set the value of the value option to a comma separated list of Brand slugs:

perch_shop_products([
    'count' => 12,
    'paginate' => true,
    'filter' => 'brand.slug',
    'match' => 'in',
    'value' => 'givenchy,ralph-lauren,zara',
]);

The above displays any Product that falls under any of the listed Brands.

Multiple filters

The filter option can take an array of filters. Each filter inside the array is also an array containing the filter, match and value options.

We also need to add the filter-mode option (as of Perch 3.0.2) if we have more than one filter.

Here we are applying 2 filters. The first is the same as the previous example and the second is for Products with a price less than or equal to 400.

perch_shop_products([
    'count' => 12,
    'paginate' => true,
    'filter-mode' => 'ungrouped',
    'filter' => [
        [
            'filter' => 'brand.slug',
            'match' => 'in',
            'value' => 'givenchy,ralph-lauren,zara',
        ],
        [
            'filter' => 'price',
            'match' => 'lte',
            'value' => 400,
        ],
    ],
]);

It is important to realise that the value of the filter option is an array and can be replaces it with an array variable $filters. And the perch_shop_products() function’s options is also an array which can be replaced with an array variable too.

The above is really the same as the following:

// options
$opts = [
    'count' => 12,
    'paginate' => true,
];

// set filters
$filters[] = [
    'filter' => 'brand.slug',
    'match' => 'in',
    'value' => 'givenchy,ralph-lauren,zara',
];

$filters[] = [
    'filter' => 'price',
    'match' => 'lt',
    'value' => 400,
];

$opts['filter'] = $filters;

// if we have more than one filter, add the filter-mode option
if(count($filters) > 1) {
    $opts['filter-mode'] = 'ungrouped';
}

// display product listing
perch_shop_products($opts);

While we’re still hard-coding the values here, what we’ll end up doing is basically the same thing. We check what the user is filtering for and add the filters to the function’s options.


The Form and URL parameters

Again let’s start by hard-coding our form. We’ll assume our product listing is on /shop.

We want to allow users to filter by more than one Brand or one Category at a time. Checkboxes is the perfect input type for this.

Side note:

In some cases it makes sense to only filter by one Category at a time. Sometimes it also makes sense to have the Category filters as links rather than checkboxes or radio buttons, and then filter by other things such as Brand on the Category “page”. What approach you use depends on your use case.

If you have one checkbox:

<form id="filters" method="get">
    <div>
        <input type="checkbox" id="brand" name="brand" value="givenchy">
        <label for="brand">Givenchy</label>
    </div>
    <div><input type="submit" value="Filter" /></div>
</form>

After checking the box and submitting the form, you get following URL: /shop?brand=givenchy.

Now what if we have multiple brands?

<form id="filters" method="get">
    <fieldset>
            <legend>Brands</legend>
            <ul>
                <li>
                    <input type="checkbox" id="brand_adidas" name="brand" value="adidas">
                    <label for="brand_adidas">Adidas</label>
                </li>
                <li>
                    <input type="checkbox" id="brand_nike" name="brand" value="nike">
                    <label for="brand_nike">Nike</label>
                </li>
                <li>
                    <input type="checkbox" id="brand_puma" name="brand" value="puma">
                    <label for="brand_puma">Puma</label>
                </li>
            </ul>
    </fieldset>
    <div>
        <input type="submit" value="Filter" >
    </div>
</form>

We have multiple checkboxes with the same name attribute, but each holds a different value. If you check more than one box, you get /shop?brand=adidas&brand=puma. You can’t get both values in PHP with $_GET['brand'] (or perch_get('brand')) this way and won’t be able to filter by more than one brand.

What we can do is change name="brand" to name="brand[]". This way your URL will be /shop?brand%5B%5D=adidas&brand%5B%5D=puma and when you use $_GET['brand'] you get an array of the values.

<form id="filters" method="get">
    <fieldset>
            <legend>Brands</legend>
            <ul>
                <li>
                    <input type="checkbox" id="brand_adidas" name="brand[]" value="adidas">
                    <label for="brand_adidas">Adidas</label>
                </li>
                <li>
                    <input type="checkbox" id="brand_nike" name="brand[]" value="nike">
                    <label for="brand_nike">Nike</label>
                </li>
                <li>
                    <input type="checkbox" id="brand_puma" name="brand[]" value="puma">
                    <label for="brand_puma">Puma</label>
                </li>
            </ul>
    </fieldset>
    <div>
        <input type="submit" value="Filter" >
    </div>
</form>

And basically you’d get the values like so:

$brands = $_GET['brand'];
$categories = $_GET['category'];

Note that $brands and $categories are arrays.

Why use $_GET instead of perch_get()

perch_get():

The perch_get() function does the same job as $_GET in PHP, but has some convenient safeguards and options built in.

PHP has a standard way to read these values into the page. If you’ve done any PHP development at all, you’ll be familiar with using something like $_GET['s'] to read a value from the GET request.

You can do that in Perch, too. However, $_GET can be hard to use, because if you try to read in a value that hasn’t been set, PHP will throw an error. This means you need to test for it being set, and decide whether to read it or not. To make life easier, Perch has perch_get().

So it is a utitility function that makes our lives easier and allows us to write less code.

Unfortunately it doesn’t help in this case as it always expects the parameters to be strings rather than arrays, so we have to use $_GET.

So we need to check whether tha values are set first as the URL parameters don’t always exists. perch_get() often handles this for us, but here we have to do this ourselves.

if (isset($_GET['brand'])) {
    $brands = $_GET['brand'];
}

Now that we have gone through the basics, we can finally start building.


Setting up the Form

We’ll split the form into 3 <fieldset>s. One for Categories, one for Brands and one for prices.

Categories fieldset

We can get a list of all our Categories from the Products set using perch_categories().

We’ll create a template in /perch/templates/categories/ and let’s call it _filters_fieldset.

// categories
$cats_fieldset = perch_categories([
    'set' => 'products',
    'template' => '_filters_fieldset',
], true);

// set a variable to pass it into our main form template
PerchSystem::set_var('cats_fieldset', $cats_fieldset);

The _filters_fieldset template:

<perch:before>
    <fieldset>
    <legend>Categories</legend>
    <ul>
</perch:before>
        <li>
            <input type="checkbox" id="<perch:category id="catPath" type="text" />" name="category[]" value="<perch:category id="catPath" type="text" />" >
            <label for="<perch:category id="catPath" type="text" />"><perch:category id="catTitle" type="text" /></label>
        </li>
<perch:after>
    </ul>
    </fieldset>
</perch:after>

As mentioned in the hard-coded examples, we need Category paths for filtering. That’s why we’re using catPath for the value attribute. This will result in long URLs since each path would be something like products%2Fhandbags%2F, but this is absolutely fine.

If you’re picky and want to use we use the catID instead, you’ll need to write more code to get the Category paths. Either way it’s unlikely the user of the website would care about this.

Brands fieldset

We can get a list of all our Brands using perch_shop_brands().

We’ll create a template in /perch/templates/shop/brands/ and let’s also call it _filters_fieldset.

// return the value of the function instead of outputting it
$brands_fieldset = perch_shop_brands([
    'template' => 'brands/_filters_fieldset',
], true);

// set a variable to pass it into our main form template
PerchSystem::set_var('brands_fieldset', $brands_fieldset);

We can use the Brand slug or ID for filtering. In this example, let’s use slugs.

The _filters_fieldset template:

<perch:before>
    <fieldset>
    <legend>Brands</legend>
    <ul>
</perch:before>
        <li>
            <input type="checkbox" id="<perch:shop id="slug" type="slug" />" name="brand[]" value="<perch:shop id="slug" type="slug" />" >
            <label for="<perch:shop id="slug" type="slug" />"><perch:shop id="title" type="text" /></label>
        </li>
<perch:after>
    </ul>
    </fieldset>
</perch:after>

Prices fieldset

Unlike Categories and Brands, we’ll only filter by one price range at a time. So in this case we’ll use radio buttons.

We can create an editable Region for the prices, but to not make this post longer than it already is, we’ll hard-code them in this example.

We can add multiple options using a single perch:input radio tag:

<fieldset>
    <legend>Prices</legend>
    <ul>
        <perch:input id="price" type="radio" options="25-100, 100-200, 200-400, 400-600, 600" class="radio" wrap="li" />
    </ul>
</fieldset>

We separate the minimum price and maximum price with a hyphen minPrice-maxPrice. Let’s leave the last one with just a minimun price (to filter for items that cost 600 or more).

What prices you use is up to you. You should take into consideration the prices of the items in the shop you’re building.

This <fieldset> can be added directly to our main form template.

The Form

We’ll use perch_form() to display our form:

perch_form('product_filters.html');

Let’s put together the above <fieldsets> in one form in a template. Remember the variables we set earlier? We’ll use them here. We’ll also add our prices <fieldset>.

Create a new template /templates/forms/product_filters:

<perch:form id="filters" method="get">
    <perch:forms id="cats_fieldset" html="true" />
    <perch:forms id="brands_fieldset" html="true" />
    <fieldset>
        <legend>Prices</legend>
        <ul>
            <perch:input id="price" type="radio" options="25-100, 100-200, 200-400, 400-600, 600" class="radio" wrap="li" />
        </ul>
    </fieldset>
    <div>
        <input type="submit" value="Apply">
        <a href="<perch:forms id="perch_page_path" />">Clear</a>
    </div>
</perch:form>

perch_page_path is a special ID value that gets you the current page as Perch sees it. Getting the current page like this instead of hard-coding it /shop allows you to use the same template on different pages.


Getting the values and filtering the list

Now that we set the form, we can work on getting the values from the URL query string.

As explained earlier, we’ll use $_GET instead of perch_get() for Categories and Brands.

// array for filters
$filters = array();

// default options - you can add other options here like 'template' and 'sort'
$opts = $default_opts = [
    'paginate' => true,
    'count' => 12,
];

/* ---------- BRANDS ---------- */
if(isset($_GET['brand'])) {

    // handle things differently if array or string
    if(is_array($_GET['brand'])) {
        // ?brand%5B%5D=
        // glue together the slugs as 'brand1,brand2,brand3'
        $brand_slugs = implode(",", $_GET['brand']);
    } else {
        // ?brand=
        $brand_slugs = $_GET['brand'];
    }

    // add a filter
    $filters[] = [
        'filter' => 'brand.slug',
        'match' => 'in',
        'value' => $brand_slugs,
    ];

}

/* ---------- CATEGORIES ---------- */
if(isset($_GET['category'])) {

    // 'category' option takes string or array; no need to implode()
    $cat_paths = $_GET['category'];

    // add the category option to $opts
    $opts['category'] = $cat_paths;
    $opts['category-match'] = 'any';

}

Unlike Categories and Brands, we can use perch_get() for price since it’s not an array:

/* ---------- PRICES ---------- */
if(perch_get('price')) {
    $price = perch_get('price');

    // if numeric, we have one number (?price=minPrice)
    if(is_numeric($price)) {
        // filter for greater or equal to
        $filters[] = [
            'filter' => 'price',
            'match' => 'gte',
            'value' => $price,
        ];
    } elseif(substr_count($price, '-') == 1) {
        // we have a signle hyphen (hopefully ?price=minPrice-maxPrice)

        // split
        $prices = explode('-', $price);

        // check both are numeric
        if(is_numeric($prices[0]) && is_numeric($prices[1])) {
            $min_price = $prices[0];
            $max_price = $prices[1];

            // filter for items between min and max inclusively
            $filters[] = [
                'filter' => 'price',
                'match' => 'eqbetween',
                'value' => $min_price . ',' . $max_price,
            ];
        }
    }
}

Now let’s get our listing:

// add the filter option if we have some
if(count($filters) > 0) {
    $opts['filter'] = $filters;
}

// add filter-mode option if we have multiple filters
if(count($filters) > 1) {
    $opts['filter-mode'] = 'ungrouped';
}

// get filtered listing, but don't output it
$products = perch_shop_products($opts, true);

// output filtered listing if the result isn't empty
// otherwise display unfiltered listing
if($products) {
    echo $products;
} else {
    echo "<div>Your search didn't match any items</div>";
    perch_shop_products($default_opts);
}

Setting the selected options

We got the form to a good functional state. For a better user experience, let’s reflect the selected filters so the user can quickly see what filters are applied.

We’ll pass the selected values to the templates and check for them using conditional tags.

First pass the selected values to the template:

$filters[] = [
    'filter' => 'brand.slug',
    'match' => 'in',
    'value' => $brand_slugs,
];

PerchSystem::set_var('selected_brands', $brand_slugs);

Then add the following to the checkbox input:

<perch:if id="slug" match="in" value="{selected_brands}">checked="true"</perch:if>

We are using the id, match and value attributes to check whether the slug matched a value in the selected_brands variable we passed which holds the value of comma separated slugs like givenchy,ralph-lauren,zara.

<input type="checkbox" id="<perch:shop id="slug" type="slug" />" name="brand[]" value="<perch:shop id="slug" type="slug" />" <perch:if id="slug" match="in" value="{selected_brands}">checked</perch:if>>

And we do the same thing for Categories:

$cat_paths = $_GET['category'];

$opts['category'] = $cat_paths;
$opts['category-match'] = 'any';

if(is_array($_GET['category'])) {
    // ?category%5B%5D=
    $selected_cats = implode(",", $cat_paths);
} else {
    // ?category=
    $selected_cats = $cat_paths;
}

PerchSystem::set_var('selected_cats', $selected_cats);
<input type="checkbox" id="<perch:category id="catPath" type="text" />" name="category[]" value="<perch:category id="catPath" type="text" />" <perch:if id="catPath" match="in" value="{selected_cats}">checked</perch:if>>

It’s slightly different for prices.

PerchSystem::set_var('selected_price', $min_price . '-' . $max_price);

Add the following to the perch:input tag:

<perch:if exists="selected_price">value="<perch:forms id="selected_price" />"</perch:if>
<perch:input id="price" type="radio" options="25-100, 100-200, 200-400, 400-600, 600" class="radio" wrap="li" <perch:if exists="selected_price">value="<perch:forms id="selected_price" />"</perch:if> />

Final solution

You can find the files on GitHub.

Last words

This post shouldn’t just show you how to create a filtering form for a product listing, but also expose you to some of Perch’s features. If you’ve been working with Perch a while you may have used most of the features in this post. If not, hopefully you realise how useful they can be.

Your project may have a different set up and different requirements, so you may not be able to just copy and paste everything as is. You should try to understand it and implement it in a way that suits your project.

link