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 %}

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:

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 %}

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"> | </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"> | </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 %}

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