Searching in Online Book Catalog

Searching in Online Book Catalog

How to implement search functionality to provide a better browsing.

search.png
Parent post: Online Book Catalog Application Tutorial

It is time to provide a little search box that visitors can enter keywords to make a search in the Online Book Catalog.

Developing an engaging web site requires a search functionality to provide visitors to find out what they are looking for quickly. I will show you how to build a basic search functionality in this post of the Online Book Catalog Tutorial series.

I will start with placing the search text box at the top right which looks to be a good spot for this purpose. I will update the catalog index view template to implement this.

# /module/Application/view/application/catalog/index.phtml

<div class="row">
    <div class="col-sm-6">
        <h1>Book Listing</h1>
    </div>
    <div class="col-sm-6 pull-right">
        <h4>Make a search in the catalog</h4>
        <form method="get">
            <div class="input-group pull-right">
                <input name="search_keyword" type="text" class="form-control" placeholder="Search...">
                <span class="input-group-btn">
                    <button class="btn btn-default" type="submit">
                        <span class="glyphicon glyphicon-search"></span>
                    </button>
                </span>
            </div>
        </form>
    </div>
</div>
<hr>
<div class="row">

  // ... refer to the previous post in the series for this section

</div>
<div>
<?= $this->paginationControl(
            $this->books, 
            'Sliding', 
            'partial/paginator', 
            []
        );
?>
</div>

Fig1 shows the new look with the Search Box at the top right corner of the page.

Catalog index with Search box
Current look of the catalog index with search box.

I set up the form element in the code to make the request method GET, so when visitor clicks on the search button which is a submit button element, the keyword will be added to the URL as the form will be submitted as a GET request and will be captured in the associated controller action in the next step.

Just like the page value previously obtained from the URL query, I will get the search keyword by using the controller's $this->params()->fromQuery() method and pass it to the view template with searchKeyword parameter as shown in the updated action method. By passing the variable, the view template will use the keyword between pages and the pagination control will be also able to paginate by using the same keyword.

# /module/Application/src/Controller/CatalogController.php

<?php
namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Application\Model\Book\BookMapper;

class CatalogController extends AbstractActionController
{
    // ... refer to the previous post in the series for this section

    public function indexAction()
    {
        // Getting requested page number from the query 
        $pageNumber = (int) $this->params()->fromQuery('page', 1);
        
        // Set total item count per page for pagination
        $count = 3;
        
        // Getting search keyword if any
        $searchKeyword = (string) $this->params()->fromQuery('search_keyword', false);
        
        $books = $this->bookMapper->fetchAll($pageNumber, $count);

        return new ViewModel([
            'books' => $books,
            'searchKeyword' => $searchKeyword,
        ]);       
    }
}

Modified view template using the passed $searchKeyword variable:

# /module/Application/view/application/catalog/index.phtml

<div class="row">
    <div class="col-sm-6">
        <h1>Book Listing</h1>
    </div>
    <div class="col-sm-6 pull-right">
        <h4>Make a search in the catalog</h4>
        <form method="get">
            <div class="input-group pull-right">
                <input name="search_keyword" type="text" class="form-control" placeholder="Search..." value="<?= $this->searchKeyword ?>">
                <span class="input-group-btn">
                    <button class="btn btn-default" type="submit">
                        <span class="glyphicon glyphicon-search"></span>
                    </button>
                </span>
            </div>
        </form>
    </div>
</div>
<hr>
<div class="row">

  // ... refer to the previous post in the series for this section

</div>
<div>
<?= $this->paginationControl(
            $this->books, 
            'Sliding', 
            'partial/paginator', 
            ['searchKeyword' => $this->searchKeyword]
        );
?>
</div>

Note that how I passed searchKeyword to the paginator in the last array parameter so it will be injected into the pagination control object to build the page links by also placing the keyword in the URL.

Following shows the updated paginator.phtml template that I created as partial template in the previous post of this tutorial series. Note that how search_keyword query parameter is populated with the passed searchKeyword variable.

# /module/Application/view/partial/paginator.phtml

<?php if ($this->pageCount): ?>
    <ul class="pagination pagination-sm">
        <?php if (!isset($this->previous)): ?>
            <li class="disabled">
                <a>«</a>
                <a>‹</a>
            </li>
        <?php else: ?>
            <li>
                <a href="<?= $this->url($this->route, [], 
                    ['query' => [
                        'page' => $this->first, 
                        'search_keyword' => $this->searchKeyword
                    ]]); ?>">«</a>
                <a href="<?= $this->url($this->route, [], 
                    ['query' => [
                        'page' => $this->previous, 
                        'search_keyword' => $this->searchKeyword
                    ]]); ?>">‹</a>
            </li>
        <?php endif; ?>
        
        <!-- Numbered page links -->
        <?php foreach ($this->pagesInRange as $page): ?>
            <?php if ($page == $this->current): ?>
                <li class="active">
                    <a><?= $page ?></a>
                </li>
            <?php else: ?>
                <li>
                    <a href="<?= $this->url($this->route, [], 
                        ['query' => [
                            'page' => $page, 
                            'search_keyword' => $this->searchKeyword
                        ]]); ?>">
                        <?= $page; ?>
                    </a>
                </li>
            <?php endif; ?>
        <?php endforeach; ?>
    
        <?php if (!isset($this->next)): ?>
            <li class="disabled">
                <a>›</a>
                <a>»</a>
            </li>
        <?php else: ?>
            <li>
                <a href="<?= $this->url($this->route, [], 
                    ['query' => [
                        'page' => $this->next, 
                        'search_keyword' => $this->searchKeyword
                    ]]); ?>">›</a>
                <a href="<?= $this->url($this->route, [], 
                    ['query' => [
                        'page' => $this->last, 
                        'search_keyword' => $this->searchKeyword
                    ]]); ?>">»</a>
            </li>
        <?php endif; ?>
    </ul>
<?php endif; ?>

It looks that our model is about to return filtered results but it won't happen itself unless BookMapper is informed about this request and return filtered results instead of returning the entire table content as it is currently doing.

I am going to achieve this by passing the $searchKeyword variable to the mapper's fetchAll() method to query database by using this value to search in the title field. I will use where method of the Zend\Db\Sql\Select element.

# /module/Application/src/Model/Book/BookMapper.php

<?php
namespace Application\Model\Book;
 
use Zend\Db\TableGateway\TableGateway;
use Zend\Db\Sql\Select;

class BookMapper
{
    // ... refer to the previous post in the series for this section

    /**
     * FetchAll for Book Model
     * 
     * @param int $pageNumber
     * @param int|null $count
     * @param string|null $keyword
     * @return \Zend\Paginator\Paginator \Application\Model\Book\BookEntity[]
     */
    public function fetchAll($pageNumber = 1, $count = null, $keyword = null)
    {
        $select = new Select($this->tableGateway->table);
        $select->order('publish_date DESC');
        
        if (!is_null($keyword)) {
            $select->where
                ->like('title', sprintf('%%%s%%', $keyword));
        }
        
        $rowset = $this->tableGateway->selectWith($select);
        
        return $rowset;
    }
}

And the final edit will be done in the controller action. I will make the $bookMapper->fetchAll() call by also passing the $searchKeyword parameter this time.

# /module/Application/src/Controller/CatalogController.php

    // ..
    
    $books = $this->bookMapper->fetchAll($pageNumber, $count, $searchKeyword);
    
    // ..

Finally Fig2 shows how it works when I make a search for php keyword. I will also see how pagination works great when my catalog grows up with more book entries.

Catalaog index with search results
Current look of the catalog with filtered results.

I didn't add any new file in this post, but simply changed the existing controller, mapper, and view templates to provide simple search functionality.

This could be improved to make the search against more fields other than only the "title" field, make it searching for more keywords, and ideally search keyword can be stored in a session variable to not pass it with the URL but along with the session.


Published on Jan 20, 2017