Symfony 2 Joboard : RSS

Во время поиска работы вы, наверняка, хотели бы получать свежие вакансии сразу же после их публикации. Согласитесь, довольно неудобно каждый раз проверять сайт, поэтому мы создадим несколько новостных лент (RSS), чтобы наши пользователи всегда были в курсе событий.

Формат шаблонов

При помощи шаблонов мы можем показать содержание страницы в любом формате. Хотя чаще всего они используются для генерации HTML, мы вполне можем применять шаблоны чтобы получить JavaScript, CSS, XML или любой другой формат.

Например, один и тот же источник можно отобразить в разных форматах. Чтобы сгенерировать денту для страницы index, просто используйте название формата в имени шаблона:

  • Имя шаблона XML: AcmeArticleBundle:Article:index.xml.twig
  • Имя файла XML: index.xml.twig

Но это все лишь формат имен, который не имеет ничего общего с процессом генерации файла. Чаще всего используется один метод в контроллере, который генерирует разные форматы в зависимости от типа полученного запроса. Приведем, часто встречающийся паттерн:

<?php
public function indexAction()
{
   $format = $this->getRequest()->getRequestFormat();
   return $this->render('AcmeBlogBundle:Blog:index.'.$format.'.twig');
}

По-умолчанию метод getRequestFormat объекта Request возвращает HTML, но в зависимости от типа запроса он может вернуть любой тип. Обычно, тип возвращаемого ресурса указывается в файле маршрутизации, где мы можем назначить маршруту /contacts тип HTML, а /contacts.xml — XML. Чтобы получить ссылки с указанием формата необходимо указать переменную _format:

<a href="{{ path('article_show', {'id': 123, '_format': 'pdf'}) }}">
   PDF Version
</a>

Новостные ленты

Лента последних опубликованных вакансий

Поддержка различных форматов не /var/www/joboard/src/App/JoboardBundle/Resources/views/Job/index.atom.twig:

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <title>Joboard</title>
    <subtitle>Последние вакансии</subtitle>
    <link href="" rel="self"/>
    <link href=""/>
    <updated></updated>
    <author><name>Joboard</name></author>
    <id>Unique Id</id>
    <entry>
        <title>Название вакансии</title>
        <link href="" />
        <id>Идентификатор</id>
        <updated></updated>
        <summary>Описание</summary>
        <author><name>Компания</name></author>
    </entry>
</feed>

В футере страницы обновите ссылку на ленту:

<!-- ... -->
<a href="{{ path('app_job', {'_format': 'atom'}) }}">RSS</a>
<!-- ... -->

Добавьте тег в раздел , чтобы браузер автоматически определял нашу ленту:

<!-- ... -->
<link rel="alternate" type="application/atom+xml" title="Последнии вакансии" href="{{ url('app_job', {'_format': 'atom'}) }}">
<!-- ... -->

В контроллере JobController отредактируйте метод indexAction, чтобы тот выдавал различные результаты в зависимости от параметра _format:

<?php
# src/App/JoboardBundle/Controller/JobController.php
// ...
$format = $this->get('request')->getRequestFormat();
return $this->render('AppJoboardBundle:Job:index.'.$format.'.twig', array(
    'categories' => $categories
));
// ...

Замените шапку в шаблоне index.atom.twig на следующий код:

<!-- ... -->
<title>Joboard</title>
<subtitle>Последние вакансии</subtitle>
<link href="{{ url('app_job', {'_format': 'atom'}) }}" rel="self"/>
<link href="{{ url('app_joboard_homepage') }}"/>
<updated>{{ lastUpdated }}</updated>
<author><name>Joboard</name></author>
<id>{{ feedId }}</id>
<!-- ... -->

Нам необходимо отправить параметры lastUpdated и feedId из метода indexAction:

<?php
# src/App/JoboardBundle/Controller/JobController.php
// ...
        $latestJob = $em->getRepository('AppJoboardBundle:Job')->getLatestPost();
        if($latestJob) {
            $lastUpdated = $latestJob->getCreatedAt()->format(DATE_ATOM);
        } else {
            $lastUpdated = new \DateTime();
            $lastUpdated = $lastUpdated->format(DATE_ATOM);
        }
        $format = $this->getRequest()->getRequestFormat();
        return $this->render(
            'AppJoboardBundle:Job:index.'.$format.'.twig',
            [
                'categories'  => $categories,
                'lastUpdated' => $lastUpdated,
                'feedId'      => sha1($this->generateUrl('app_job', ['_format'=> 'atom'], true))
            ]);
// ...

Нам потребуется создать метод getLatestPost() в репозитории JobRepository, чтобы тот возвращал дату последней опубликованной вакансии:

<?php
// ...
    public function getLatestPost($categoryId = null)
    {
        $query = $this->createQueryBuilder('j')
            ->where('j.expires_at > :date')
            ->setParameter('date', date('Y-m-d H:i:s', time()))
            ->andWhere('j.is_activated = :activated')
            ->setParameter('activated', 1)
            ->orderBy('j.expires_at', 'DESC')
            ->setMaxResults(1);
        if($categoryId) {
            $query->andWhere('j.category = :category_id')
                ->setParameter('category_id', $categoryId);
        }
        try{
            $job = $query->getQuery()->getSingleResult();
        } catch(\Doctrine\Orm\NoResultException $e){
            $job = null;
        }
        return $job;
    }
// ...

Сами записи в ленте мы можем создавать посредством следующего кода:

{% for category in categories %}
    {% for entity in category.activejobs %}
        <entry>
            <title>{{ entity.position }} ({{ entity.location }})</title>
            <link href="{{ url('app_job_show', { 'id': entity.id, 'company': entity.companyslug, 'location': entity.locationslug, 'position': entity.positionslug }) }}" />
            <id>{{ entity.id }}</id>
            <updated>{{ entity.createdAt.format(constant('DATE_ATOM')) }}</updated>
            <summary type="xhtml">
                <div xmlns="http://www.w3.org/1999/xhtml">
                    {% if entity.logo %}
                    <div>
                        <a href="{{ entity.url }}">
                        <img src="http://{{ app.request.host }}/uploads/jobs/{{ entity.logo }}" alt="{{ entity.company }} logo" />
                        </a>
                    </div>
                    {% endif %}
                    <div>
                        {{ entity.description|nl2br }}
                    </div>
                    <h4>Как соискатель может ответить на вакансию?</h4>
                    <p>{{ entity.howtoapply }}</p>
                </div>
            </summary>
            <author><name>{{ entity.company }}</name></author>
        </entry>
    {% endfor %}
{% endfor %}
Лента новостей, содержащая последние вакансии в одной из категорий

Суть всего проекта заключается в том, чтобы помочь людям найти вакансии, которые подошли бы именно им. Поэтому создадим ленту для каждой категории. Для начала обновим ссылку на ленту в каждой категории (src/App/JoboardBundle/Resources/views/Category/show.html.twig):

<div class="feed">
   <a href="{{ path('AppJoboardBundle_category', { 'slug': category.slug, '_format': 'atom' }) }}">Feed</a>
</div>

Исправьте метод showAction в 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);
        $latestJob = $em->getRepository('AppJoboardBundle:Job')->getLatestPost($category->getId());
        if($latestJob) {
            $lastUpdated = $latestJob->getCreatedAt()->format(DATE_ATOM);
        } else {
            $lastUpdated = new \DateTime();
            $lastUpdated = $lastUpdated->format(DATE_ATOM);
        }
        $format = $this->getRequest()->getRequestFormat();
        return $this->render('AppJoboardBundle:Category:show.'.$format.'.twig', [
            'category'     => $category,
            'lastPage'     => $lastPage,
            'previousPage' => $previousPage,
            'currentPage'  => $page,
            'nextPage'     => $nextPage,
            'totalJobs'    => $totalJobs,
            'feedId'       => sha1($this->generateUrl('AppJoboardBundle_category', ['slug' => $category->getSlug(), 'format' => 'atom'], true)),
            'lastUpdated' => $lastUpdated
        ]);
    }

И, наконец, создадим шаблон show.atom.twig:

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <title>Joboard ({{ category.name }})</title>
    <subtitle>Последние вакансии</subtitle>
    <link href="{{ url('AppJoboardBundle_category', { 'slug': category.slug, '_format': 'atom' }) }}" rel="self" />
    <updated>{{ lastUpdated }}</updated>
    <author><name>Joboard</name></author>
    <id>{{ feedId }}</id>
    {% for entity in category.activejobs %}
        <entry>
            <title>{{ entity.position }} ({{ entity.location }})</title>
            <link href="{{ url('app_job_show', { 'id': entity.id, 'company': entity.companyslug, 'location': entity.locationslug, 'position': entity.positionslug }) }}" />
            <id>{{ entity.id }}</id>
            <updated>{{ entity.createdAt.format(constant('DATE_ATOM')) }}</updated>
            <summary type="xhtml">
                <div xmlns="http://www.w3.org/1999/xhtml">
                    {% if entity.logo %}
                        <div>
                            <a href="{{ entity.url }}">
                                <img src="http://{{ app.request.host }}/uploads/jobs/{{ entity.logo }}" alt="{{ entity.company }} logo" />
                            </a>
                        </div>
                    {% endif %}
                    <div>
                        {{ entity.description|nl2br }}
                    </div>
                    <h4>Как соискатель может ответить на вакансию?</h4>
                    <p>{{ entity.howtoapply }}</p>
                </div>
            </summary>
            <author><name>{{ entity.company }}</name></author>
        </entry>
    {% endfor %}
</feed>