Маршрут категорий
Для начала добавим маршрут для URL категорий. Добавьте его в начале файла src/App/JoboardBundle/Resources/config/routing.yml:
Заметка: slug- это уникальный идентификатор для записи, используется вместо id, необходим для читабельности в url, а так же в целях безопасности, чтобы не раскрывать айдишники (id) из базы данных.
# ...
AppJoboardBundle_category:
pattern: /category/{slug}/
defaults: { _controller: AppJoboardBundle:Category:show }
Чтобы получить slug категории нам надо добавить метод getSlug() в класс модели Category (src/App/JoboardBundle/Entity/Category.php):
<?php
# src/App/JoboardBundle/Entity/Category.php
use App\JoboardBundle\Utils\Joboard as Joboard;
class Category
{
// ...
public function getSlug()
{
return Joboard::slugify($this->getName());
}
}
Ссылка на категорию
Теперь изменим файл src/App/JoboardBundle/Resources/views/Job/index.html.twig
, замените содержимое блока content
следующим кодом:
<div id="jobs">
{% for category in categories %}
<div>
<div class="category">
<div class="feed">
<a href="">Feed</a>
</div>
<h1><a href="{{ path('AppJoboardBundle_category', {'slug': category.slug}) }}">{{ category.name }}</a></h1>
</div>
<table class="jobs">
{% for entity in category.activejobs %}
<tr class="{{ cycle(['even', 'odd'], loop.index) }}">
<td class="location">{{ entity.location }}</td>
<td class="position">
<a href="{{ path('app_job_show', { 'id': entity.id, 'company': entity.companyslug, 'location': entity.locationslug, 'position': entity.positionslug }) }}">
{{ entity.position }}
</a>
</td>
<td class="company">{{ entity.company }}</td>
</tr>
{% endfor %}
</table>
{% if category.morejobs %}
<div class="more-jobs text-right">
и <a href="{{ path('AppJoboardBundle_category', {'slug': category.slug}) }}">{{ category.morejobs }}</a>
ещё вакансии...
</div>
{% endif %}
</div>
{% endfor %}
</div>
В этом шаблоне мы использовали category.morejobs (это метод модели Category), поэтому давайте опишем необходимые методы:
<?php
# src/App/JoboardBundle/Entity/Category.php
class Category
{
// ...
private $moreJobs;
// ...
public function setMoreJobs($jobs)
{
$this->moreJobs = $jobs >= 0 ? $jobs : 0;
}
public function getMoreJobs()
{
return $this->moreJobs;
}
}
Свойство moreJobs
хранит количество активных вакансий минус число вакансий на домашней странице. Теперь в JobController
следует установить значение moreJobs
для каждой категории:
<?php
# src/App/JoboardBundle/Controller/JobController.php
// ...
public function indexAction()
{
$em = $this->getDoctrine()->getManager();
$categories = $em->getRepository('AppJoboardBundle:Category')->getWithJobs();
foreach($categories as $category) {
$category->setActiveJobs($em->getRepository('AppJoboardBundle:Job')->getActiveJobs(
$category->getId(),
$this->container->getParameter('max_jobs_on_homepage'))
);
$activeJobsCount = $em->getRepository('AppJoboardBundle:Job')->countActiveJobs($category->getId());
if ($activeJobsCount >= $this->container->getParameter('max_jobs_on_homepage')) {
$activeJobsCount -= $this->container->getParameter('max_jobs_on_homepage');
$category->setMoreJobs($activeJobsCount);
}
}
return $this->render('AppJoboardBundle:Job:index.html.twig', array(
'categories' => $categories
));
}
// ...
Функцию countActiveJobs
следует добавить в JobRepository
:
<?php
# src/App/JoboardBundle/Repository/JobRepository.php
// ...
public function countActiveJobs($categoryId = null)
{
$qb = $this->createQueryBuilder('j')
->select('count(j.id)')
->where('j.expires_at > :date')
->setParameter('date', date('Y-m-d H:i:s', time()));
if($categoryId)
{
$qb->andWhere('j.category = :category_id')
->setParameter('category_id', $categoryId);
}
$query = $qb->getQuery();
return $query->getSingleScalarResult();
}
// ...
Теперь в браузере можно просмотреть результат:
Создание контроллера категорий
Настало время создать контроллер для категорий. Создайте файл CategoryController.php
в каталоге Controller
:
<?php
# src/App/JoboardBundle/Controller/CategoryController.php
namespace App\JoboardBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use App\JoboardBundle\Entity\Category;
/**
* Category controller
*
*/
class CategoryController extends Controller
{
}
Мы могли бы воспользоваться командой doctrine:generate:crud
, как в случае с контроллером для вакансий, но 90% из созданного кода, нам не надо, поэтому мы можем просто создать новый контроллер с нуля.
Обновление базы данных
Нам необходимо добавить поле slug
для таблицы категорий и lifecycle
функции для его обновления:
# src\App\JoboardBundle\Resources\config\doctrine\Category.orm.yml
App\JoboardBundle\Entity\Category:
type: entity
repositoryClass: App\JoboardBundle\Repository\CategoryRepository
table: category
id:
id:
type: integer
generator: { strategy: AUTO }
fields:
name:
type: string
length: 255
unique: true
slug:
type: string
length: 255
unique: true
oneToMany:
jobs:
targetEntity: Job
mappedBy: category
manyToMany:
affiliates:
targetEntity: Affiliate
mappedBy: categories
lifecycleCallbacks:
prePersist: [ setSlugValue ]
preUpdate: [ setSlugValue ]
Удалите метод getSlug()
из сущности Category
и выполните команду Doctrine для обновления классов сущностей:
php app/console doctrine:generate:entities AppJoboardBundle
Теперь вы должны увидеть следующие изменения в файле Category.php
:
<?php
// ...
/**
* @var string
*/
private $slug;
/**
* Set slug
*
* @param string $slug
* @return Category
*/
public function setSlug($slug)
{
$this->slug = $slug;
return $this;
}
/**
* Get slug
*
* @return string
*/
public function getSlug()
{
return $this->slug;
}
Измените функцию setSlugValue():
<?php
// ...
class Category
{
// ...
public function setSlugValue()
{
$this->slug = Joboard::slugify($this->getName());
}
}
Теперь надо удалить БД и создать её заново. После создания базы необходимо загрузить фикстуры, для этого выполните команды в консоли:
php app/console doctrine:database:drop --force
php app/console doctrine:database:create
php app/console doctrine:schema:update --force
php app/console doctrine:fixtures:load
Страница категории
Мы подготовили всё необходимое для создания метода showAction()
. Добавьте следующий код в файл CategoryController.php
:
<?php
# src/App/JoboardBundle/Controller/CategoryController.php
// ...
public function showAction($slug)
{
$em = $this->getDoctrine()->getManager();
$category = $em->getRepository('AppJoboardBundle:Category')->findOneBySlug($slug);
if (!$category) {
throw $this->createNotFoundException('Такая категория не найдена.');
}
$category->setActiveJobs($em->getRepository('AppJoboardBundle:Job')->getActiveJobs($category->getId()));
return $this->render('AppJoboardBundle:Category:show.html.twig', array(
'category' => $category,
));
}
// ...
Последний шаг — создание шаблона show.html.twig (src/App/JoboardBundle/Resources/views/Category/show.html.twig):
{% extends '::base.html.twig' %}
{% block title %}
Вакансий в категории {{ category.name }}
{% endblock %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('bundles/appjoboard/css/jobs.css') }}" type="text/css" media="all" />
{% endblock %}
{% block content %}
<div class="one-category">
<div class="feed">
<a href="">Feed</a>
</div>
<h1>{{ category.name }}</h1>
</div>
<table class="jobs">
{% for entity in category.activejobs %}
<tr class="{{ cycle(['even', 'odd'], loop.index) }}">
<td class="location">{{ entity.location }}</td>
<td class="position">
<a href="{{ path('app_job_show', { 'id': entity.id, 'company': entity.companyslug, 'location': entity.locationslug, 'position': entity.positionslug }) }}">
{{ entity.position }}
</a>
</td>
<td class="company">{{ entity.company }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}
Подключение других шаблонов Twig
Обратите внимание, мы скопировали содержимое тэга <table class="jobs">
(из шаблона index.html.twig), который отображает список вакансий. Так делать не стоит. Когда вам требуется заново использовать какую-либо часть шаблона, следует создать новый шаблон с этим html кодом и подключить его при необходимости. Создайте файл list.html.twig
(src/App/JoboardBundle/Resources/views/Job/list.html.twig):
<table class="jobs">
{% for entity in jobs %}
<tr class="{{ cycle(['even', 'odd'], loop.index) }}">
<td class="location">{{ entity.location }}</td>
<td class="position">
<a href="{{ path('app_job_show', { 'id': entity.id, 'company': entity.companyslug, 'location': entity.locationslug, 'position': entity.positionslug }) }}">
{{ entity.position }}
</a>
</td>
<td class="company">{{ entity.company }}</td>
</tr>
{% endfor %}
</table>
Подключить шаблон вы можете при помощи twig функции include
. Замените HTML код таблицы в обоих шаблонах на вышеуказанную функцию:
# src/App/JoboardBundle/Resources/views/Job/index.html.twig
{{ include ('AppJoboardBundle:Job:list.html.twig', {'jobs': category.activejobs}) }}
# src/App/JoboardBundle/Resources/views/Category/show.html.twig
{{ include ('AppJoboardBundle:Job:list.html.twig', {'jobs': category.activejobs}) }}
Пагинация списка
На данный момент Symfony2 не предлагает встроенного функционала пагинации, поэтому для решения этой проблемы мы используем классический метод. Сначала, добавим параметр page
в маршрут AppJoboardBundle_category. По умолчанию значение этого параметра должно быть 1.
# src/App/JoboardBundle/Resources/config/routing.yml
AppJoboardBundle_category:
pattern: /category/{slug}/{page}
defaults: { _controller: AppJoboardBundle:Category:show, page: 1 }
# …
Очистите кэш:
php app/console cache:clear --env=dev
php app/console cache:clear --env=prod
Количество вакансий на странице будет задано параметром в файле app/config/parameters.yml
:
# ...
parameters:
max_jobs_on_homepage: 10
max_jobs_on_category: 20
Измените метод getActiveJobs из класса JobRepository
, чтобы он использовал параметр $offset
при получении вакансий из БД:
<?php
# src/App/JoboardBundle/Repository/JobRepository.php
// ...
public function getActiveJobs($categoryId = null, $max = null, $offset = null)
{
$qb = $this->createQueryBuilder('j')
->where('j.expires_at > :date')
->setParameter('date', date('Y-m-d H:i:s', time()))
->orderBy('j.expires_at', 'DESC');
if($max) {
$qb->setMaxResults($max);
}
if($offset) {
$qb->setFirstResult($offset);
}
if($categoryId) {
$qb->andWhere('j.category = :category_id')
->setParameter('category_id', $categoryId);
}
$query = $qb->getQuery();
return $query->getResult();
}
// ...
Измените метод showAction
в CategoryController на следующее:
<?php
# src/App/JoboardBundle/Controller/CategoryController.php
// ...
public function showAction($slug, $page)
{
$em = $this->getDoctrine()->getManager();
$category = $em->getRepository('AppJoboardBundle:Category')->findOneBySlug($slug);
if (!$category) {
throw $this->createNotFoundException('Такая категория не найдена.');
}
$totalJobs = $em->getRepository('AppJoboardBundle:Job')->countActiveJobs($category->getId());
$jobsPerPage = $this->container->getParameter('max_jobs_on_category');
$lastPage = ceil($totalJobs / $jobsPerPage);
$previousPage = $page > 1 ? $page - 1 : 1;
$nextPage = $page < $lastPage ? $page + 1 : $lastPage;
$activeJobs = $em->getRepository('AppJoboardBundle:Job')
->getActiveJobs($category->getId(), $jobsPerPage, ($page - 1) * $jobsPerPage);
$category->setActiveJobs($activeJobs);
return $this->render('AppJoboardBundle:Category:show.html.twig', array(
'category' => $category,
'lastPage' => $lastPage,
'previousPage' => $previousPage,
'currentPage' => $page,
'nextPage' => $nextPage,
'totalJobs' => $totalJobs
));
}
// ...
Наконец, обновим шаблон src/App/JoboardBundle/Resources/views/Category/show.html.twig
{% extends '::base.html.twig' %}
{% block title %}
Вакансии в категории {{ category.name }}
{% endblock %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('bundles/appjoboard/css/jobs.css') }}" type="text/css" media="all" />
{% endblock %}
{% block content %}
<div class="category">
<div class="feed">
<a href="">Feed</a>
</div>
<h1>{{ category.name }}</h1>
</div>
{{ include ('AppJoboardBundle:Job:list.html.twig', {'jobs': category.activejobs}) }}
{% if lastPage > 1 %}
<div class="pagination">
<a href="{{ path('AppJoboardBundle_category', {'slug': category.slug, 'page': 1}) }}">
В начало
</a>
<a href="{{ path('AppJoboardBundle_category', {'slug': category.slug, 'page': previousPage }) }}">
<<
</a>
{% for page in 1..lastPage %}
{% if page == currentPage %}
{{ page }}
{% else %}
<a href="{{ path('AppJoboardBundle_category', {'slug': category.slug, 'page': page}) }}">{{ page }}</a>
{% endif %}
{% endfor %}
<a href="{{ path('AppJoboardBundle_category', {'slug': category.slug, 'page': nextPage}) }}">
>>
</a>
<a href="{{ path('AppJoboardBundle_category', {'slug': category.slug, 'page': lastPage}) }}">
В конец
</a>
</div>
{% endif %}
<div class="pagination_desc">
<strong>{{ totalJobs }}</strong> вакансии в категории
{% if lastPage > 1 %}
- страница <strong>{{ currentPage }}/{{ lastPage }}</strong>
{% endif %}
</div>
{% endblock %}
Вот и всё страница категории с пагинацией готова. Увидимся в следующей части!