Today we will make the Category page like it is described in the second day’s requirements: “The user sees a list of all the jobs from the category sorted by date and paginated with 20 jobs per page“.
First, let’s think about the route we will use for our category page.
We will define a pretty URL that will contain the category slug: /category/{slug} named category.show.
To have the slug of categories we will need StofDoctrineExtensionsBundle that wraps DoctrineExtensions package. It consists of different useful extensions, but we will use Sluggable only for now.
First let’s install the bundle:
composer require stof/doctrine-extensions-bundleThis bundle has recipe and symfony will ask you to run this recipe, because it’s not official one. Type y and accept it:
Symfony operations: 1 recipe (3c3199f3aa23ea62ee911b3d6fe61a93)
- WARNING stof/doctrine-extensions-bundle (>=1.2): From github.com/symfony/recipes-contrib:master
The recipe for this package comes from the "contrib" repository, which is open to community contributions.
Do you want to execute this recipe?
[y] Yes
[n] No
[a] Yes for all packages, only for the current installation session
[p] Yes permanently, never ask again for this project
(defaults to n): Read about Flex system to know more about recipes.
Activate sluggable extension in config/packages/stof_doctrine_extensions.yaml:
stof_doctrine_extensions:
default_locale: en_US
orm:
default:
sluggable: trueSlug will be stored in DB, and we need field for it. Add slug field in Category entity:
// ...
use Gedmo\Mapping\Annotation as Gedmo;
class Category
{
// ...
/**
* @var string
*
* @Gedmo\Slug(fields={"name"})
*
* @ORM\Column(type="string", length=128, unique=true)
*/
private $slug;
// ...
/**
* @return string|null
*/
public function getSlug() : ?string
{
return $this->slug;
}
/**
* @param string $slug
*/
public function setSlug(string $slug): void
{
$this->slug = $slug;
}
// ...
}Pay attention to @Gedmo\Slug annotation.
Generate migration that will add slug field in category table:
bin/console doctrine:migrations:diffIf we run migration now, we will see error, because we have several categories in DB without slug, that is required. First of all we should drop database:
bin/console doctrine:schema:drop --force --full-databaseRun migrations:
bin/console doctrine:migration:migrateRun fixtures:
bin/console doctrine:fixtures:loadAnd check that categories have slug:
bin/console doctrine:query:sql 'SELECT * from categories'The result should be similar:
array(4) {
[0]=>
array(3) {
["id"]=>
string(1) "1"
["name"]=>
string(6) "Design"
["slug"]=>
string(6) "design"
}
...The main advantage of this bundle for us is that slug is generated automatically. We don’t call setSlug anywhere.
It’s now time to create the category controller. Create a new CategoryController.php file in your Controller directory:
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class CategoryController extends AbstractController
{
}Add the following code to the CategoryController.php file:
// ...
use App\Entity\Category;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;
class CategoryController extends AbstractController
{
/**
* Finds and displays a category entity.
*
* @Route("/category/{slug}", name="category.show", methods="GET")
*
* @param Category $category
*
* @return Response
*/
public function show(Category $category) : Response
{
return $this->render('category/show.html.twig', [
'category' => $category,
]);
}
}The last step is to create the templates/category/show.html.twig template:
{% extends 'base.html.twig' %}
{% block title %}
Jobs in the {{ category.name }} category
{% endblock %}
{% block body %}
<h4>{{ category.name }}</h4>
<table class="table text-center">
<thead>
<tr>
<th class="active text-center">City</th>
<th class="active text-center">Position</th>
<th class="active text-center">Company</th>
</tr>
</thead>
<tbody>
{% for job in category.activeJobs %}
<tr>
<td>{{ job.location }}</td>
<td>
<a href="{{ path('job.show', {id: job.id}) }}">
{{ job.position }}
</a>
</td>
<td>{{ job.company }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}Notice that we have copied and pasted the <table> tag that create a list of jobs from the job list.html.twig template. That’s bad.
When you need to reuse some portion of a template, you need to create a new twig template with that code and include it where you need.
Create the templates/job/table.html.twig file:
<table class="table text-center">
<thead>
<tr>
<th class="active text-center">City</th>
<th class="active text-center">Position</th>
<th class="active text-center">Company</th>
</tr>
</thead>
<tbody>
{% for job in jobs %}
<tr>
<td>{{ job.location }}</td>
<td>
<a href="{{ path('job.show', {id: job.id}) }}">
{{ job.position }}
</a>
</td>
<td>{{ job.company }}</td>
</tr>
{% endfor %}
</tbody>
</table>Notice that we changed one thing: we use to iterate jobs instead of category.activeJobs. It will help us in next step.
You can include a template by using the {% include %} statement.
Replace the
templates/category/show.html.twig with the include function:{% extends 'base.html.twig' %}
{% block title %}
Jobs in the {{ category.name }} category
{% endblock %}
{% block body %}
<h4>{{ category.name }}</h4>
{% include 'job/table.html.twig' with {'jobs': category.activeJobs} only %}
{% endblock %}and templates/job/list.html.twig
{% extends 'base.html.twig' %}
{% block body %}
{% for category in categories %}
<h4>{{ category.name }}</h4>
{% include 'job/table.html.twig' with {
'jobs': category.activeJobs|slice(0, max_jobs_on_homepage)
} only %}
{% endfor %}
{% endblock %}We included table template with keywords with and only. That means that we pass to table template only jobs variables.
Read also about include function
Now, edit the templates/job/list.html.twig template of the job controller to add the link to the category page:
- <h4>{{ category.name }}</h4>
+ <h4>
+ <a href="{{ path('category.show', {slug: category.slug}) }}">{{ category.name }}</a>
+ </h4>Now you can go from categories page to specific category page.
To implement pagination we will use KnpPaginatorBundle.
First, let’s install the bundle:
composer require knplabs/knp-paginator-bundleBundle is installed and ready to use.
As you can see in documentation of the bundle, paginator consumes doctrine query, not the result.
We need to create the new method in job repository src/Repository/JobRepository.php:
// ...
use App\Entity\Category;
use Doctrine\ORM\AbstractQuery;
class JobRepository extends EntityRepository
{
// ...
/**
* @param Category $category
*
* @return AbstractQuery
*/
public function getPaginatedActiveJobsByCategoryQuery(Category $category) : AbstractQuery
{
return $this->createQueryBuilder('j')
->where('j.category = :category')
->andWhere('j.expiresAt > :date')
->setParameter('category', $category)
->setParameter('date', new \DateTime())
->getQuery();
}
}This method create query which will get all active jobs by category. But where is pagination?
Let’s do it in controller src/Controller/CategoryController.php:
// ...
use App\Entity\Job;
use Knp\Component\Pager\PaginatorInterface;
class CategoryController extends AbstractController
{
/**
* Finds and displays a category entity.
*
* @Route("/category/{slug}", name="category.show", methods="GET")
*
* @param Category $category
* @param PaginatorInterface $paginator
*
* @return Response
*/
public function show(Category $category, PaginatorInterface $paginator) : Response
{
$activeJobs = $paginator->paginate(
$this->getDoctrine()->getRepository(Job::class)->getPaginatedActiveJobsByCategoryQuery($category),
1, // page
10 // elements per page
);
return $this->render('category/show.html.twig', [
'category' => $category,
'activeJobs' => $activeJobs,
]);
}
}We added PaginatorInterface in parameters of the method and autowire component will inject paginator service automatically.
Also we call paginator and pass query from repository, page (for now let’s get only first) and how many element we want per page.
The result we send to template. Let’s use it there templates/category/show.html.twig:
- {% include 'job/table.html.twig' with {'jobs': category.activeJobs} only %}
+ {% include 'job/table.html.twig' with {'jobs': activeJobs} only %}If now you open the browser, you will see only 10 jobs on the page, but what about pagination? How to access second page? And what if we want to have 20 elements on the page?
First let’s define new parameter in config/services.yaml:
parameters:
# ...
max_jobs_on_category: 20and now some changes in src/Controller/CategoryController.php:
namespace App\Controller;
use App\Entity\Category;
use App\Entity\Job;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;
class CategoryController extends Controller
{
/**
* Finds and displays a category entity.
*
* @Route(
* "/category/{slug}/{page}",
* name="category.show",
* methods="GET",
* defaults={"page": 1},
* requirements={"page" = "\d+"}
* )
*
* @param Category $category
* @param PaginatorInterface $paginator
* @param int $page
*
* @return Response
*/
public function show(
Category $category,
PaginatorInterface $paginator,
int $page
) : Response {
$activeJobs = $paginator->paginate(
$this->getDoctrine()->getRepository(Job::class)->getPaginatedActiveJobsByCategoryQuery($category),
$page,
$this->getParameter('max_jobs_on_category')
);
return $this->render('category/show.html.twig', [
'category' => $category,
'activeJobs' => $activeJobs,
]);
}
}We added page in the URL path and defined default value, in case when page is not defined in the URL (ex: /category/design).
Variable $page is added in arguments of the method. It will be injected automatically by name in path.
Also we need parameter max_jobs_on_category and getParameter methods to access it.
That’s why this controller extends now Symfony\Bundle\FrameworkBundle\Controller\Controller but not Symfony\Bundle\FrameworkBundle\Controller\AbstractController.
Now let’s render page selector in template templates/category/show.html.twig:
{% extends 'base.html.twig' %}
{% block title %}
Jobs in the {{ category.name }} category
{% endblock %}
{% block body %}
<h4>{{ category.name }}</h4>
{% include 'job/table.html.twig' with {'jobs': activeJobs} only %}
<div class="navigation text-center">
{{ knp_pagination_render(activeJobs) }}
</div>
{% endblock %}Pagination will work but will look not in style of Bootstrap 3. Let’s configure it.
Create file knp_paginator.yml in config/packages and past there next code to change the style of paginator:
knp_paginator:
template:
pagination: "@KnpPaginator/Pagination/twitter_bootstrap_v3_pagination.html.twig"Notice: don’t forget to clear cache after that.
Now it should look like that:
That’s all for today, you can find the code here: https://github.com/gregurco/jobeet/tree/day7
See you tomorrow!
Continue this tutorial here: Jobeet Day 8: The Forms
Previous post is available here: Jobeet Day 6: More with the Entity
Main page is available here: Symfony 4.2 Jobeet Tutorial
