Symfony 2 Joboard: Контроллёр и Представление

Сегодня мы собираемся настроить контроллер для вакансий, который мы создали в прошлой части. Он уже содержит большую часть кода, которая необходима для Joboard:

  • Страница со списком всех вакансий
  • Страница для создания новой вакансии
  • Страница для редактирования вакансии
  • Возможность удаления вакансии

Хотя текущий код уже готов к использованию, мы всё же переделаем шаблоны, чтобы они больше соответствовали макетам Joboard.

Архитектура MVC

В веб-программировании наиболее распространенным решением для организации кода является шаблон проектирования MVC. Этот шаблон разделяет код на три слоя:

Слой модели определяет бизнес-логику (база данных принадлежит к этому слою). Вы уже знаете, что Symfony хранит все классы и файлы, относящиеся к модели в каталоге Entity/ ваших бандлов.

Представление — это то, что пользователь видит в браузере (движок шаблонов является частью этого слоя). В Symfony 2.4 слой представления основан на Twig шаблонизаторе. Шаблоны Twig хранятся в различных каталогах бандлов Resources/views/, а базовые шаблоны обычно хранятся в каталоге app/Resources/views.

Контроллер — это php класс, который вызывает модель, чтобы получить некоторые данные, которые он передает в представление для отрисовки на клиенте. Когда мы установили Symfony в первой части этого туториала, мы увидели, что все запросы обслуживаются фронт-контроллерами (app.php и app_dev.php). Эти фронт-контроллеры делегируют реальную работу контроллёрам, которые выполняют определённые действия в соответствии с запросом — эти действия обычные методы класса контроллёра.

Макет

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

Одним из способов решения этой проблемы является определение заголовка и футера в отдельных файлах и подключать их в других шаблонах. Но самый лучший способ — это воспользоваться функционалом наследования шаблонов (через twig движок). Symfony2 не поставляется с макетом по умолчанию, поэтому мы создадим его и будем использовать его как базовый шаблон.

Для HTML разметки будем использовать bootstrap, перейдите по ссылке и скачайте последнюю версию, затем распакуйте и скопируйте каталоги css, fonts, js в каталог проекта web/bootstrap/ (создайте его).

В каталоге app/Resources/views/ должен быть файл base.html.twig — это базовый шаблон, который остался от AcmeDemoBundle (если его там нет создайте новый файл app/Resources/views/base.html.twig) откройте его и поместите в него следующий код:

<!DOCTYPE html>
<html>
    <head>
    <meta charset="UTF-8">
    <title>{% block title %}Joboard{% endblock %}</title>
    {% block stylesheets %}
        <link rel="stylesheet" href="{{ asset('bootstrap/css/bootstrap.min.css') }}" type="text/css" media="all">
        <link rel="stylesheet" href="{{ asset('bundles/appjoboard/css/main.css') }}" type="text/css" media="all">
    {% endblock %}
    <link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" />
</head>
<body>
    <div id="header" class="container">
        <div class="row">
            <div class="col-lg-6">
                <div class="row">
                    <div class="col-lg-4">
                        <h2 id="logo">Joboard</h2>
                    </div>
                    <div class="col-lg-6">
                        <input type="text" class="form-control" placeholder="Поиск по вакансиям">
                    </div>
                    <div class="col-lg-2">
                        <button class="btn btn-default">Искать</button>
                    </div>
                </div>
            </div>
            <div class="col-lg-6 text-right">
                <a href="" class="btn btn-success">Добавить вакансию</a>
            </div>
        </div>
    </div>
    <div class="container">
        {% block content %}{% endblock %}
    </div>
    <div id="footer" class="container">
        <a href="">О проекте</a>
        <a href="">RSS</a>
        <a href="">API</a>
        <a href="">Партнёрам</a>
    </div>
    {% block javascripts %}
        <script src="{{ asset('bootstrap/js/bootstrap.min.js') }}"></script>
    {% endblock %}
</body>
</html>

Блоки Twig

Twig — это механизм шаблонов в Symfony определённый по умолчанию, вы уже заметили, что в вышеприведённом шаблоне мы определили несколько блоков (title, stylesheets, content, javascripts). Чтобы понять систему блоков в twig шаблоне, представьте, что twig шаблон — это php класс, а блоки это методы этого класса, также как и в обычных php классах мы можем наследоваться от базового класса (т.е. создавать другой шаблон на основе базового) и переопределять методы (переопределять блоки). В Twig мы можем наследоваться от любых шаблонов, и переопределить любой блок, а переопределив блок, также можно вызвать базовый блок с помощью ключевого слова parent() (как и в методе класса), как это сделать вы увидите ниже.

Теперь, чтобы использовать базовый шаблон app/Resources/views/base.html.twig, который мы создали, нам нужно будет отредактировать все шаблоны вакансий (главная страница, страница редактирования, страница создания новой вакансии и страница самой вакансии) в src/App/JoboardBundle/Resources/views/Job/ расширить родительский шаблон и переопределить содержимое блока content. В каждый файл в самое начало добавьте строчку:

{% extends "::base.html.twig" %}

Блок {% block body -%} замените на {% block content %}

Таблицы стилей, изображения и JavaScript файлы

Создайте в каталоге src/App/JoboardBundle/Resources/public/css/ четыре файла: admin.css, job.css, jobs.css и main.css. main.css необходим на всех страницах Joboard, поэтому мы включили его по умолчанию в шаблоне в блоке stylesheets. В остальных более специфичных css файлах мы будем нуждаться только на отдельных страницах (в windows запустите эту команду с правами администратора).

Теперь запустите

php app/console assets:install web --symlink

эта команда берёт статические файлы (css, js, images) в вашем бандле и делает их публичными, она копирует их в директорию web/bundles/<bandle_name>/, а если указан параметр --symlink, то команда просто создаёт символические ссылки на эти файлы. В symfony приложении публичным является всего один каталог — web. Все остальные каталоги закрыты для доступа из вне. Поэтому статические файлы бандлов нужно копировать в каталог web. Вы можете сразу поместить файлы в каталог web, но если вы разместите статические файлы в соотвествующем бандле, то этим вы говорите другим программистам, что эти файлы используются только в этом бандле, плюс этот бандл можно спокойно переносить между разными приложениями. Файлы bootstrap мы поместили сразу в каталог web, потому что они являются общими для всех бандлов.

Чтобы добавить новый CSS файл в шаблон, мы переопределим блок стилей, но вызовем родительский блок перед добавлением нового css файла, таким образом будут загружены базовые файлы (те которые определены по умолчанию в базовом шаблоне).

Для jobs.css в src/App/JoboardBundle/Resources/views/Job/index.html.twig

{% extends "::base.html.twig" %}
{% block stylesheets %}
    {{ parent() }}
    <link rel="stylesheet" href="{{ asset('bundles/appjoboard/css/jobs.css') }}" type="text/css" media="all">
{% endblock %}

Для job.css в src/App/JoboardBundle/Resources/views/Job/show.html.twig

{% extends "::base.html.twig" %}
{% block stylesheets %}
    {{ parent() }}
    <link rel="stylesheet" href="{{ asset('bundles/appjoboard/css/job.css') }}" type="text/css" media="all">
{% endblock %}

Действие для главной страницы

Каждое действие представляет публичный метод класса, который должен заканчиваться словом Action. Для главной страницы этим классом является JobController и метод indexAction(). Он извлекает все вакансии из базы данных.

<?php
// ...
public function indexAction()
{
    $em = $this->getDoctrine()->getManager();
    $entities = $em->getRepository('AppJoboardBundle:Job')->findAll();
    return $this->render('AppJoboardBundle:Job:index.html.twig', array(
        'entities' => $entities,
    ));
}
// ...

Давайте ближе посмотрим на код: метод indexAction() возвращает объект менеджера сущностей Doctrine, который отвечает за обработку, процесс сохранения и извлечения объектов из базы данных, а затем возвращает объект репозитория, который создаёт запрос для получения всех вакансий. Он возвращает объекты вакансий (Doctrine ArrayCollection), которые передаются в представление (View).

Шаблон главной страницы

Шаблон src/App/JoboardBundle/Resources/views/Job/index.html.twig создает HTML-таблицу для всех заданий. Вот текущий код шаблона:

{% extends "::base.html.twig" %}
{% block stylesheets %}
    {{ parent() }}
    <link rel="stylesheet" href="{{ asset('bundles/appjoboard/css/jobs.css') }}" type="text/css" media="all">
{% endblock %}
{% block content %}
<h1>Job list</h1>
<table class="records_list">
    <thead>
        <tr>
            <th>Id</th>
            <th>Type</th>
            <th>Company</th>
            <th>Logo</th>
            <th>Url</th>
            <th>Position</th>
            <th>Location</th>
            <th>Description</th>
            <th>How_to_apply</th>
            <th>Token</th>
            <th>Is_public</th>
            <th>Is_activated</th>
            <th>Email</th>
            <th>Expires_at</th>
            <th>Created_at</th>
            <th>Updated_at</th>
            <th>Actions</th>
        </tr>
    </thead>
    <tbody>
    {% for entity in entities %}
        <tr>
            <td><a href="{{ path('app_job_show', { 'id': entity.id }) }}">{{ entity.id }}</a></td>
            <td>{{ entity.type }}</td>
            <td>{{ entity.company }}</td>
            <td>{{ entity.logo }}</td>
            <td>{{ entity.url }}</td>
            <td>{{ entity.position }}</td>
            <td>{{ entity.location }}</td>
            <td>{{ entity.description }}</td>
            <td>{{ entity.howtoapply }}</td>
            <td>{{ entity.token }}</td>
            <td>{{ entity.ispublic }}</td>
            <td>{{ entity.isactivated }}</td>
            <td>{{ entity.email }}</td>
            <td>{% if entity.expiresat %}{{ entity.expiresat|date('Y-m-d H:i:s') }}{% endif %}</td>
            <td>{% if entity.createdat %}{{ entity.createdat|date('Y-m-d H:i:s') }}{% endif %}</td>
            <td>{% if entity.updatedat %}{{ entity.updatedat|date('Y-m-d H:i:s') }}{% endif %}</td>
            <td>
            <ul>
                <li>
                    <a href="{{ path('app_job_show', { 'id': entity.id }) }}">show</a>
                </li>
                <li>
                    <a href="{{ path('app_job_edit', { 'id': entity.id }) }}">edit</a>
                </li>
            </ul>
            </td>
        </tr>
    {% endfor %}
    </tbody>
</table>
<ul>
    <li>
        <a href="{{ path('app_job_new') }}">
            Create a new entry
        </a>
    </li>
</ul>
{% endblock %}

Давайте уберём некоторые столбцы. Замените содержимое twig блока content следующим кодом:

{% block content %}
<table class="records_list table table-bordered">
    <thead>
        <tr>
            <th>Адрес</th>
            <th>Вакансия</th>
            <th>Компания</th>
        </tr>
    </thead>
    <tbody>
    {% for entity in entities %}
        <tr class="{{ cycle(["even", "odd"], loop.index) }}">
            <td>{{ entity.location }}</td>
            <td><a href="{{ path('app_job_show', { 'id': entity.id }) }}">{{ entity.position }}</a></td>
            <td>{{ entity.company }}</td>
        </tr>
    {% endfor %}
    </tbody>
</table>
{% endblock %}

Шаблон главной страницы

Шаблон страницы вакансии

Теперь давайте настроим шаблон страницы просмотра вакансии. Откройте файл src/App/JoboardBundle/Resources/views/Job/show.html.twig и замените его содержимое следующим кодом:

{% extends "::base.html.twig" %}
{% block stylesheets %}
    {{ parent() }}
    <link rel="stylesheet" href="{{ asset('bundles/appjoboard/css/job.css') }}" type="text/css" media="all">
{% endblock %}
{% block content %}
<div class="row">
    <div class="col-lg-9">
        <h3>{{ entity.company }}</h3>
        <h4>{{ entity.location }}</h4>
        <hr>
        <h4>{{ entity.position }} - {{ entity.type }}</h4>
        <hr>
        <div>
            {{ entity.description }}
        </div>
    </div>
    <div class="col-lg-3 text-right">
        <img src="http://placehold.it/200x200">
    </div>
</div>
{% endblock %}

Шаблон страницы вакансии

Страница вакансии

Страница вакансии генерируется действием show, определенным в методе showAction() контроллёра JobController (src\App\JoboardBundle\Controller\JobController.php):

<?php
public function showAction($id)
{
    $em = $this->getDoctrine()->getManager();
    $entity = $em->getRepository('AppJoboardBundle:Job')->find($id);
    if (!$entity) {
        throw $this->createNotFoundException('Unable to find Job entity.');
    }
    $deleteForm = $this->createDeleteForm($id);
    return $this->render('AppJoboardBundle:Job:show.html.twig', array(
        'entity' => $entity,
        'delete_form' => $deleteForm->createView(),
    ));
}

Также как и в действии index объект репозитория используется для извлечения вакансии, но на этот раз, используется метод find(). Параметр этого метода является уникальным идентификатором вакансии, это её первичный ключ. Этот метод возвращает только одно значение из базы данных. Следующая часть этого туториала объяснит, почему параметр $id в функции actionShow() содержит первичный ключ вакансии и объяснит то, как он туда попадает.

Если вакансия не существует в базе данных, нам нужно перенаправить пользователя на страницу 404, для этого нужно бросить исключение через метод контроллёра $this->createNotFoundException(). Что касается исключений, страница 404 отображаемая для пользователя отличается в продуктовой среде окружения (prod) и в среде окружения разработки (dev).

404 продуктовое окружение

Шаблон страницы вакансии

404 окружение разработки

Шаблон страницы вакансии

Это все на сегодня! В следующей части мы познакомимся с функционалом маршрутизации. И если что не понятно пишите в комментариях.