Menu

Lesson 5: Create Category module

p5chi
Attachments
category_details.png (129299 bytes)
edit_category.png (68882 bytes)
list_categories.png (46992 bytes)
new_category.png (71359 bytes)

For the project it is necessary to categorize the product. So for this future a category module will be created that will give the possibility to create/edit/ a category for authenticated users and list the details of the category for both users (authenticated and anonymous).
First step is to create an entity and generate a table in the database. The process of generation of the entity was related in the Lesson 2. The category entity will look almost like the product entity :

<?php
//src/Xshare/ProductBundle/Entity/Category.php

namespace Xshare\ProductBundle\Entity;

use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; 

/**
 * Xshare\ProductBundle\Entity\Category
 *
 * @ORM\Table(name="category", indexes={@ORM\Index(name="search_idx", columns={"name", "created_at"})})
 * @ORM\Entity(repositoryClass="Xshare\ProductBundle\Repository\CategoryRepository")
 * @ORM\HasLifecycleCallbacks()
 * @UniqueEntity(fields="name", message="category.name.not_unique")
 */
class Category
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $category_id;

    /**
     * @ORM\Column(type="string")
     * @Assert\NotBlank(
     *      message="product.not_blank"
     * )
     * @Assert\MaxLength(
     *     limit=100,
     *     message="Category name must have maximum {{ limit }} characters."
     * )
     */
    private $name;

    /**
     * @ORM\Column(type="text", length=1000)
     * @Assert\NotBlank(
     *      message="product.not_blank"
     * )
     */
    private $description;

    /**
     * @ORM\Column(type="string", nullable=true)
     */
    private $image;

    /**
     * @ORM\Column(type="integer", length=1, nullable=true)
     */
    private $status = 0;

    /**
     * @ORM\ManyToOne(targetEntity="Xshare\UserBundle\Entity\User", inversedBy="categories")
     * @ORM\JoinColumn(name="user_id", referencedColumnName="user_id", onDelete="CASCADE", onUpdate="NO ACTION")
     */
    private $user;

     /**
     * @ORM\Column(type="datetime", nullable=true)
     */
    private $created_at;

     /**
     * @ORM\Column(type="datetime", nullable=true)
     *
     */
    private $updated_at;

    /**
     * @ORM\OneToMany(targetEntity="Product", mappedBy="category")
     */
    protected $products;

    /**
     * @Assert\Image()
     */
    public $file;

    /**
     * @ORM\Column(type="integer", nullable=true)
     */
    public $statistics = 0;
...

In the future you will need to get custom data from the database so for that you should create an entity repository:

<?php
//src/Xshare/ProductBundle/Repository/CategoryRepository.php

namespace Xshare\ProductBundle\Repository;

use Doctrine\ORM\EntityRepository;
use MakerLabs\PagerBundle\Adapter\DoctrineOrmAdapter;
use MakerLabs\PagerBundle\Adapter\ArrayAdapter;
use Doctrine\ORM\Query\Expr;

/**
 * CategoryRepository
 */
class CategoryRepository extends EntityRepository {
//here will be custom functions 
//also here we will use MakerLabs Bundle to paginate categories
}

if you want to use Symfony 2 form builder you must create a form type :

<?php
//src/Xshare/ProductBundle/Form/CategoryType.php
namespace Xshare\ProductBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

/**
 * Description of CategoryType
 *
 */
class CategoryType extends AbstractType {
    /**
     * builds the form for a category
     * @param FormBuilder $builder
     * @param array $options 
     */
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('file')
            ->add('name')
            ->add('description')
            ->add('status', 'checkbox', array('required' => false));
    }

    /**
     * returns a unique name of the form
     * @return string 
     */
    public function getName()
    {
        return 'xshare_productbundle_categorytype';
    }
}

If you have category entity and a form type, you can create a controller action and a layout to render the form. The controller action looks like this:

<?php
//src/Xshare/ProductBundle/Controllers/CategoryController.php
namespace Xshare\ProductBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use Xshare\ProductBundle\Entity\Category;
use Xshare\ProductBundle\Form\CategoryType;
use Xshare\ProductBundle\Form\CategoriesList;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\HttpFoundation\Request;
use MakerLabs\PagerBundle\Pager;
use Xshare\ProductBundle\ProductGeneral;

/**
 * Description of CategoryController
 *
 */
class CategoryController extends Controller {

    /**
     * shows the form and creats a new category
     * @Route("category/create", name="product_category_create")
     * @Method({"GET", "POST"})
     */
    public function createAction() {

        //breadcrumbs 
        $breadcrumbs = $this->get("white_october_breadcrumbs");
        $breadcrumbs->addItem($this->get('translator')->trans('Home'), $this->get("router")->generate("xshare_general_default_index"));
        $breadcrumbs->addItem($this->get('translator')->trans('Categories'), $this->get("router")->generate("category_list"));
        $breadcrumbs->addItem($this->get('translator')->trans('New category'), null);

        //cheks if the user is authenticated
        if(!$this->container->get('security.context')->isGranted('IS_AUTHENTICATED_FULLY') ){
            $this->get('session')->setFlash('logerr', $this->container->getParameter('not_login_error'));
            return $this->redirect($this->generateUrl("xshare_general_default_index"));
        }
        //create a new category object
        $category = new Category();
        //get the object of the user that is logged in the system now
        $user = $this->get('security.context')->getToken()->getUser();
        if (is_object($user)) {
            $category->setUser($user);
        }
        $category->setStatus("1");
        $form = $this->createForm(new CategoryType(), $category);

        //check if the category form was submitted
        $request = $this->getRequest();
        if ($request->getMethod() == 'POST') {
            $form->bindRequest($request);
            //validate the form
            if ($form->isValid()) {
                //if form is valid the category is saved into the database
                $em = $this->getDoctrine()
                    ->getEntityManager();
                $em->persist($category);
                $em->flush();

                //display a message to the user and redirect
                $this->get('session')->setFlash('category-add-notice', 'The category has been successfully added');
                return $this->redirect($this->generateUrl('product_category_edit', array('id' => $category->getCategoryId())));
            }
        }
        //if the form wasn't submitted or it is not valid the form is displayed
        return $this->render('XshareProductBundle:Category:categoryForm.html.twig', array(
            'form'   => $form->createView(),
            'title' => $this->get('translator')->trans('New category'),
            'menu'=>array('categories'=>1),
            'pagetitle' => 'Add a new category'
        ));
    }

The block of code :

 if(!$this->container->get('security.context')->isGranted('IS_AUTHENTICATED_FULLY') ){
            $this->get('session')->setFlash('logerr', $this->container->getParameter('not_login_error'));
            return $this->redirect($this->generateUrl("xshare_general_default_index"));
        }

is used to check if the user is authenticated, in case he is not, the controller will redirect the user to main page and show a flash message. After the data was collected, you need to create a layout to display the form:

{% extends '::base.html.twig' %}

{% block title %}{{ title }}{% endblock %}

{% block stylesheets %}
    {{ parent() }}
    <link rel="stylesheet" href="{{ asset('bundles/xshareproduct/css/category.css') }}" type="text/css" media="all" />
{% endblock %}

{% block maincontent %}
    {% block categoryform %}
        <h2>{{pagetitle|trans}}</h2>
        <div class="entity-form category-form">
            {% if app.session.hasFlash('category-add-notice') %}
                <div class="product-result-notice">
                    {{ app.session.flash('category-add-notice')|trans }}
                </div>
            {% endif %}
            <form action="
                {% if category is not defined %}
                    {{ path('product_category_create') }}
                {% else %}
                    {{ path('product_category_update', { 'id' : category.getCategoryId() }) }}
                {% endif %}
                " 
                method="post" {{ form_enctype(form) }} >
                {{ form_errors(form) }}

                <div class="entity-image">
                    <div class="status-input">
                        {{ form_label(form.status,"Available"|trans) }}
                        {{ form_widget(form.status) }}
                    </div>
                    {% if category is defined %}
                        <img src="/uploads/categories/{{ category.image }}"
                        alt="{{ category.name }} logo" />
                    {% else %}
                        <img src="{{ asset('/images/category-icon.png') }}" alt="category logo" />
                    {% endif %}                    
                </div>
                <div class="entity-details">
                    <div class="entity-name-input">
                        <span class="error-div">{{ form_errors(form.name) }}</span>
                        {{ form_label(form.name,"Name"|trans) }}
                        {{ form_widget(form.name) }}
                    </div>                    
                    <div class="clear"></div>
                    <span class="error-div">{{ form_errors(form.description) }}</span>
                    {{ form_label(form.description,"Details"|trans) }}
                    {{ form_widget(form.description) }}
                </div>
                    <div class="clear"></div>     
                <span class="error-div">{{ form_errors(form.file) }}</span>
                <div class="entity-options">                    
                    {{ form_label(form.file,"Image"|trans) }}
                    {{ form_widget(form.file) }}
                </div>
                <div class="entity-options">
                    <input class="save" type="submit" value="{{ "Save"|trans }}" />
                    {% if category is defined %}
                        <a class="clear-form" href="{{ path('product_category_edit', { 'id': category.getCategoryId() }) }}" >{{ "Cancel"|trans }}</a>
                    {% else %}
                        <a class="clear-form" href="{{ path('product_category_create') }}" >{{ "Cancel"|trans }}</a>
                    {% endif %}
                    <div class="clear"></div>
                </div>
                <div class="clear"></div>
                {{ form_rest(form) }}
            </form>
            <div class="add-entity" ><a href="{{path("new_product")}}">{{ "New product"|trans }}</a></div>
            <div class="clear"></div>
        </div>
    {% endblock %}

    {% block categorylist %}
        {% render 'XshareProductBundle:Category:userCategories' %}
    {% endblock %}

    <script src="{{ asset('js/jquery.url.js') }}" type="text/javascript"></script>

    <script src="{{ asset('bundles/xshareproduct/js/categories.js') }}" type="text/javascript"></script>
{% endblock %}

New Category

next step you can create the "Edit" action that is almost the same as the "Create" action except some modifications:

/src/Xshare/ProductBundle/Controllers/CategoryController.php
...
/**
     * creates the form for editing a category
     * @Route("category/{id}/edit", name="product_category_edit", requirements={"id" = "\d+"})
     * @Method({"GET"})
     * 
     * @param $id - the id of the category
     * return string
     */
    public function editAction($id) {

        //cheks if the user is authenticated
        if(!$this->container->get('security.context')->isGranted('IS_AUTHENTICATED_FULLY') ){
            $this->get('session')->setFlash('logerr', $this->container->getParameter('not_login_error'));
            return $this->redirect($this->generateUrl("xshare_general_default_index"));
        }

        $category = $this->getCategory($id);

        if (!$category) {            
            $this->get('session')->setFlash('logerr', $this->get('translator')->trans('Category not found'));
            return $this->redirect($this->generateUrl("xshare_general_default_index"));
        }

        //the object of the current logged in user
        $user   = $this->get('security.context')->getToken()->getUser();
        $userid = $user->getUserId();

        if ($category->getUser()->getUserId() == $userid) {
            //breadcrumbs 
            $breadcrumbs = $this->get("white_october_breadcrumbs");
            $breadcrumbs->addItem($this->get('translator')->trans('Home'), $this->get("router")->generate("xshare_general_default_index"));
            $breadcrumbs->addItem($this->get('translator')->trans('Categories'), $this->get("router")->generate("category_list"));
            $breadcrumbs->addItem($category->getName(), $this->get("router")->generate("product_category_show", array('id' => $id)));
            $breadcrumbs->addItem($this->get('translator')->trans('Edit'), null);

            $category = $this->getCategory($id);
            $form   = $this->createForm(new CategoryType(), $category);
            return $this->render('XshareProductBundle:Category:categoryForm.html.twig', array(
                'category' => $category,
                'form'   => $form->createView(),
                'title' => $this->get('translator')->trans('Edit category'),
                'menu'=>array('categories'=>1),
                'pagetitle' => 'Edit category'
            ));
        } else {
            $this->get('session')->setFlash('logerr', $this->container->getParameter('no_access_error'));
            return $this->redirect($this->generateUrl("xshare_general_default_index"));
        }

    }

As you can see there is a condition that checks if the logged user is the owner, else the user will be redirected to home page:

...
if ($category->getUser()->getUserId() == $userid) {
            ...
 } else {
          $this->get('session')->setFlash('logerr', $this->container->getParameter('no_access_error'));
          return $this->redirect($this->generateUrl("xshare_general_default_index"));
 }
...

The update action that validate the input data :

/src/Xshare/ProductBundle/Controllers/CategoryController.php
...
/**
     * updates a category
     * @Route("category/update/{id}", name="product_category_update", requirements={"id" = "\d+"})
     * @Method({"POST"})
     * @param $id - the id of the category
     */
    public function updateAction($id) {

        $category = $this->getCategory($id);        
        $form = $this->createForm(new CategoryType(), $category);

        //checks if the update form was sumbitted
        $request = $this->getRequest();
        if ($request->getMethod() == 'POST') {
            $form->bindRequest($request);

            if ($form->isValid()) {
                //if the form is valid the category is saved into the database
                $em = $this->getDoctrine()
                    ->getEntityManager();

                if ($category->file != null) {
                    $category->setImage(uniqid().'.'.$category->file->guessExtension());
                    $category->file->move($category->getUploadRootDir(), $category->getImage());
                }
                $em->persist($category);
                $em->flush();

                //a message is displayed to the user and a redirect is performed
                $this->get('session')->setFlash('category-add-notice', 'The category has been successfully updated');
                return $this->redirect($this->generateUrl('product_category_edit', array('id' => $category->getCategoryId())));
            }
        }

        //display the edit form
        return $this->render('XshareProductBundle:Category:categoryForm.html.twig', array(
            'category' => $category,
            'form'   => $form->createView(),
            'title' => $this->get('translator')->trans('Edit category'),
            'pagetitle' => 'Edit category'
        ));
    }
...

The edit will look like:

Edit Category

In order to create a show action that will show the category details and statistics

/**
     * displays the page with the details of a category
     * @Route("category/{id}", name="product_category_show", requirements={"id" = "\d+"})
     * @Method({"GET"})
     * @param $id - the id of the category
     * @return string
     */
    public function showAction($id) {

        $em = $this->getDoctrine()
                    ->getEntityManager();
        $em->getRepository('XshareProductBundle:Category')->addViewer($id);

        $category = $this->getCategory($id);

        if (!$category) {            
            $this->get('session')->setFlash('logerr', $this->get('translator')->trans('Category not found'));
            return $this->redirect($this->generateUrl("xshare_general_default_index"));
        }

        //breadcrumbs 
        $breadcrumbs = $this->get("white_october_breadcrumbs");
        $breadcrumbs->addItem($this->get('translator')->trans('Home'), $this->get("router")->generate("xshare_general_default_index"));
        $breadcrumbs->addItem($this->get('translator')->trans('Categories'), $this->get("router")->generate("category_list"));
        $breadcrumbs->addItem($category->getName(), null);

        //current month
        $m = date('m');
        $y = date('Y');
        $monthDate = $y . '-' . $m . '-' . '01 00:00:00'; //creates a date equal to the fist day of the current month

        $wD = date('N');
        $monthDateInt = mktime(0, 0, 0, (int) $m, (int) date('d'), (int) $y) - ($wD - 1) * (60*60*24); //creates the time equal to the first day of the current week
        $weekDate = date('Y-m-d H:i:s', $monthDateInt);

        $thisWeek = $em->getRepository('XshareProductBundle:Product')->getCategoryProdsSinceDate($id, $weekDate);
        $thisMonth = $em->getRepository('XshareProductBundle:Product')->getCategoryProdsSinceDate($id, $monthDate);

        //the first 4 products of the category ordered by date descending
        $products = $em->getRepository('XshareProductBundle:Product')->getCategoryProducts($id, 4);
        $products_availability = array();

        //get the number of rested days after that product will be available
        if ($products) {
            $products_availability = ProductGeneral::getAvailableInXDays($products);
        }

        return $this->render('XshareProductBundle:Category:categoryShow.html.twig', array(
            'category' => $category,
            'products' => $products,
            'thisWeek' => $thisWeek,
            'thisMonth' => $thisMonth,
            'prod_available' => $products_availability,
            'helper'=>new ProductGeneral(),
            'menu'=>array('categories'=>1),
        ));
    }

It's important to pay attention that each action have routing setting that use annotations.
$em = $this->getDoctrine()->getEntityManager();
This piece of code will get the doctrine entity manager that you can use to get/set data from database ex:

/**
     * a list of categories beloging to a given user id
     * @param int $user_id
     * @return \MakerLabs\PagerBundle\Adapter\DoctrineOrmAdapter
     */
    public function getUserCategories($user_id) {

        $qb = $this->createQueryBuilder('c')
            ->where('c.user = :user_id')
            ->setParameter('user_id', $user_id)
            ->orderBy('c.created_at', 'DESC');

        return new DoctrineOrmAdapter($qb);
    }

The function will return the list of user categories.

You can find more about doctrine queryBuilder, accessing this link:
http://doctrine-orm.readthedocs.org/en/2.0.x/reference/query-builder.html

In order to get data from the product repository :
$em->getRepository('XshareProductBundle:Product')->getCategoryProdsSinceDate($id, $weekDate);

The next function get the number of products that had been borrowed starting with a given date till now.

To create a layout for category details :

{#/src/Xshare/ProductBundle/Resources/views/Category/entityShow.html.twig#}
{% extends 'XshareProductBundle::entityShow.html.twig' %}

{% block title %}{{ "Detalii categorie"|trans }}{% endblock %}
{% block entityimage %}
    {% if category.image != '' %}
        <img src="/uploads/categories/{{ category.image }}"
        alt="{{ category.name }} logo" />
    {% else %}
        <img src="{{ asset('/images/category-icon.png') }}" alt="{{ category.name }} logo" />
    {% endif %}
{% endblock %}
 {% block entitydetails %}
    <h3>{{ category.name }}</h3>
    <div>
        <div class="label strong-text">{{ "Category owner"|trans }}:</div>
            <a href="{{ path('user_details', { 'id' : category.getUser().getUserId() }) }}">{{ category.getUser().getUsername() }}</a>
    </div>
    <div>
        <div class="label strong-text">{{ "Add date"|trans }}:</div>
        {{ category.createdat|date('d')}} {{category.createdat|date('F')|trans|lower}} {{category.createdat|date('Y') }}
    </div>
    <div>
        <div class="label strong-text">{{ "Total products"|trans }}:</div>
        {{ category.getProducts()|length }}
    </div>
{% endblock %}
{% block entityoptions %}
    {#If the user is the author of the category#}
    {% if (app.user) %}
        {%if (category.getUser().getUserId() == app.user.getUserId())%}
            <a class="clear-form" href="{{ path('product_category_edit', {'id' : category.getCategoryId()}) }}" >{{ "Edit"|trans }}</a>
        {% endif %}    
            <a class="clear-form" href="{{ path('new_product') }}">{{ "New product"|trans }}</a>    
        {% if (category.getUser().getUserId() == app.user.getUserId()) %}
            <a class="clear-form" href="{{ path('product_category_delete', {'id' : category.getCategoryId()}) }}" >{{ "Delete"|trans }}</a>
    {% endif %}
    {% endif %}
{% endblock %}
{% block entitystatistics %}
    <div><div class="label">{{ "Borrowed this month"|trans }}:</div><span class="strong-text">{{ thisMonth }}</span></div>
    <div><div class="label">{{ "Borrowed this week"|trans }}:</div><span class="strong-text">{{ thisWeek }}</span></div>
    <div><div class="label">{{ "Views"|trans }}:</div><span class="strong-text">{{category.getStatistics}}</span></div>
    <div class="clear"></div>
{% endblock %}
{% block entityaddinfo %}
    {% include 'XshareProductBundle:Product:productBlockList.html.twig' with { 'products': products, 'prod_available': prod_available, 'cat_id' : category.getCategoryId() } %}
{% endblock %}

Category Details

To create a list of categories you need to create a controller action :

/**
     * displays all the categories by page and filter
     * @Route("category/list", name="category_list", defaults={"page" = 1})
     * @Route("category/list/{page}", name="category_list_page", requirements={"page" = "\d+"} )
     * @Method({"GET"})
     * 
     * @param type $page - the current page
     * @param type $data
     * @param type $title
     * @param type $unities
     * @return type 
     */
    public function listAction($page, $data = null, $title = null, $unities = null)
    {
        //breadcrumbs 
        $breadcrumbs = $this->get("white_october_breadcrumbs");
        $breadcrumbs->addItem($this->get('translator')->trans('Home'), $this->get("router")->generate("xshare_general_default_index"));
        $breadcrumbs->addItem($this->get('translator')->trans('Categories'), $this->get("router")->generate("category_list"));

        $data = $this->getRequest()->get('data');
        $title = $this->getRequest()->get('title');
        $unities = $this->getRequest()->get('unities');

        $em = $this->getDoctrine()->getEntityManager();
        $number_of_categories_per_page = $this->container->getParameter('max_categories_on_category_list_page');
        $block_length = $this->container->getParameter('pager_block');

        $adapter = $em->getRepository('XshareProductBundle:Category')
                      ->getCategoryList($data, $title, $unities);
        $pager = new Pager($adapter, array('page' => $page, 'limit' => $number_of_categories_per_page));

        return $this->render('XshareProductBundle:Category:listCategory.html.twig', array(
            'pager' => $pager,
            'data' => $data,
            'title' => $title,
            'unities' => $unities,
            'block_length' => $block_length,
            'menu'=>array('categories'=>1),
        ));
    }

The list could be ordered by date, title and product units. To do this you should create a repository method:

//src/Xshare/ProductBundle/Repository/CategoryRepository.php
...
public function getCategoryList($date = null, $title = null, $unities = null) {
        $em = $this->getEntityManager();
        $qb = $em->createQueryBuilder()
                ->select('c.category_id, c.name, c.created_at, u.user_id, u.sex, count(b.product_id) as units')
                ->from('XshareProductBundle:Category', 'c')
                ->where('c.status = :status')
                ->setParameter('status', '1')
                ->leftJoin('c.products', 'b')
                ->join('c.user', 'u')
                ->groupBy('c.category_id');

        if ($date != null) {
            $qb->orderBy('c.created_at', $date);
        } else {
            $date = 'desc';
            $qb->orderBy('c.created_at', $date);
        }
        if ($title != null) {
            $qb->orderBy('c.name', $title);
        }
        if ($unities != null) {
            $qb->orderBy('units', $unities);
        }
        $res = $qb->getQuery()->getArrayResult();

        $adapter = new ArrayAdapter($res);
        return $adapter;
    }
...

and create a layout:

{% extends '::base.html.twig' %}

{% block title %}{{ "Categories list"|trans }}{% endblock %}

{% block stylesheets %}
    {{ parent() }}
    <link rel="stylesheet" href="{{ asset('bundles/xshareproduct/css/category.css') }}" type="text/css" media="all" />
{% endblock %}

{% block maincontent %}
{% if app.session.hasFlash('category-deleted') %}
    <div class="product-result-notice">
        {{ app.session.flash('category-deleted')|trans }}
    </div>
{% endif %}
<h2 class="header" id="categories-list-header">{{"Categories"|trans}}</h2>
<div class="filters">
    <ul class="filter filter-category fright">
        <li id="filter-date">
            <a class="{{ (title != null or unities != null) ? 'shown' : 'hidden' }}"  href="{{ path('category_list', {'data': 'asc'}) }}">{{ "Date"|trans }}</a>
            <a class="{{ (data == 'asc') ? 'shown' : 'hidden' }}"  href="{{ path('category_list', {'data': 'desc'}) }}" title="{{"Ascending sort"|trans}}" >
                {{ "Date"|trans }}<img src="{{ asset('images/sort-asc.png') }}" class="sort-image"/>
            </a>
            <a class="{{ (data == 'desc' or data == null and title == null and unities == null) ? 'shown' : 'hidden' }}"  href="{{ path('category_list', {'data': 'asc'}) }}" title="{{"Descending sort"|trans}}" >
                {{ "Date"|trans }}<img  src="{{ asset('images/sort-desc.png') }}" class="sort-image"/>
            </a>     
        </li><span class="delimiter">&nbsp;|&nbsp;</span>

        <li id="filter-title"> 
            <a class="{{ (data != null or unities != null or title == null ) ? 'shown' : 'hidden' }}"  href="{{ path('category_list', {'title': 'asc'}) }}">{{ "Title"|trans }}</a>
            <a class="{{ (title == 'asc') ? 'shown' : 'hidden' }}"  href="{{ path('category_list', {'title': 'desc'}) }}" title="{{"Ascending sort"|trans}}" >
                {{ "Title"|trans }}<img src="{{ asset('images/sort-asc.png') }}" class="sort-image"/>
            </a>
            <a class="{{ (title == 'desc') ? 'shown' : 'hidden' }}"  href="{{ path('category_list', {'title': 'asc'}) }}" title="{{"Descending sort"|trans}}" >
                {{ "Title"|trans }}<img  src="{{ asset('images/sort-desc.png') }}" class="sort-image"/>
            </a>           
        </li><span class="delimiter">&nbsp;|&nbsp;</span>

        <li id="filter-unities">
            <a class="{{ (data != null or title != null or unities == null ) ? 'shown' : 'hidden' }}"  href="{{ path('category_list', {'unities': 'asc'}) }}">{{ "Units"|trans }}</a>
            <a class="{{ (unities == 'asc') ? 'shown' : 'hidden' }}"  href="{{ path('category_list', {'unities': 'desc'}) }}" title="{{"Ascending sort"|trans}}" >
                {{ "Units"|trans }}<img src="{{ asset('images/sort-asc.png') }}" class="sort-image"/>
            </a>
            <a class="{{ (unities == 'desc') ? 'shown' : 'hidden' }}"  href="{{ path('category_list', {'unities': 'asc'}) }}" title="{{"Descending sort"|trans}}" >
                {{ "Units"|trans }}<img  src="{{ asset('images/sort-desc.png') }}" class="sort-image"/>
            </a>    
        </li>
    </ul>
    <div class="clear"></div>
</div>
<br/>
<div class="category-list" id="categories"> 
    <ul class="entity-items">
        {% for category in pager.getResults %}
            <li>
                <div class="entity-name"><a href="{{ path('product_category_show', {'id': category['category_id']}) }}">{{ category.name }}</a></div>
                <div class="entity-added"> - {{"added at"|trans}} {{ category['created_at']|date('d/m/Y') }}</div>
                <div class="entity-unities"> - {{ category['units'] }} {{"units"|trans}}</div>
                <div class="entity-links">
                    <a href="{{ path('user_details', {'id': category['user_id'] }) }}">
                        {% if(category['sex'] == 'm') %}
                            <img src="{{ asset('images/user.png') }}">
                        {% else %}
                            <img src="{{ asset('images/user_woman.png') }}">
                        {% endif %}
                    </a>
                    <a href="{{ path('product_category_show', {'id': category['category_id']}) }}">
                        <img src="{{ asset('images/info-icon.png') }}">
                    </a>
                </div>
                <div class="clear"></div>
            </li>

        {% endfor %}
    </ul>
    {% if pager.isPaginable %}
            {{ paginate(pager, 'category_list_page', block_length, {'data': data, 'title': title, 'unities': unities}) }}           
    {% endif %}    
</div>
{% endblock %}

Category List

To see all features that contain this module, please download the source code.