Symfony 2 Joboard : Изменяем страницу с категориями

Маршрут категорий

Для начала добавим маршрут для 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 }) }}">
                &lt;&lt;
            </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}) }}">
                &gt;&gt;
            </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 %}

Вот и всё страница категории с пагинацией готова. Увидимся в следующей части!

Статьи из серии

Комментарии

0