Контролер

13/05/2015 0 symfony, контролер, Response, Request

Контролер - це PHP-функція, яку ви створюєте, щоб отримати інформацію з HTTP-запиту і на її основі створити HTTP-відповідь (у вигляді об'єкту Symfony Response). Відповідь може бути HTML-сторінкою, XML-документом, серіалізованим JSON-масивом, зображенням, перенаправленням, помилкою 404 або будь-чим іншим, на що здатна ваша фантазія. Контролер може містити будь-яку логіку вашого додатку, необхідну для відображення вмісту сторінки.

Щоб побачити, наскільки все це просто, давайте розглянемо Symfony Контролер в дії. Це відобразить сторінку зі славнозвісним Hello world!:

use Symfony\Component\HttpFoundation\Response;

public function helloAction()
{
    return new Response('Hello world!');
}

Мета у Контролера завжди одна: створити і повернути об'єкт Response. В процесі цього він може зчитувати інформацію із запиту, завантажувати ресурси з бази даних, відправляти електронну пошту або записувати інформацію в сесію користувача. Але в кожному випадку Контролер врешті-решт поверне об'єкт Response, який буде відправлено клієнтові.

Не хвилюйтесь, тут немає магії чи якихось надскладних вимог! Ось декілька типових прикладів:

1) Контролер А готує об'єкт Response, що містить контент для головної сторінки сайту.

2) Контролер B зчитує з запиту параметр slug, щоб завантажити запис блогу з бази даних, і створює об'єкт Response, що відображає цей блог. Якщо вказаний slug не може бути знайдений в базі даних, він створює і повертає об'єкт Response з кодом статусу 404.

3) Контролер С обробляє відправлену форму контактів. Вона читає інформацію про форму з запиту, зберігає контактну інформацію в базу даних і відправляє контактну інформацію вам. Нарешті, він створює об'єкт Response, який перенаправляє браузер клієнта на сторінку “Thank you!”.

Життєвий цикл Запит-Контролер-Відповідь

Кожен запит, що обробляється проектом Symfony, проходить один і той самий простий життєвий цикл. Фреймворк бере на себе всі повторювані задачі, вам просто потрібно написати свій код в функції Контролера:

1) Кожен запит обробляється одним файлом фронт-контролера (наприклад, app.php або app_dev.php), який завантажує додаток;

2) Router зчитує інформацію з запиту (наприклад, URI), знаходить маршрут, що відповідає вказаній інформації, і отримує параметр _controller з маршруту;

3) Контролер, що відповідає маршруту, виконується, і код всередині контролера створює і повертає об'єкт Response;

4) HTTP-заголовки і вміст об'єкта Response відправляються назад клієнтові.

Створення сторінки - це так само легко, як створення контролера (#3) і маршруту, який прив'язує до контролера певний URL (#2).

! Незважаючи на схожість у назві, фронт-контролер відрізняється від тих контролерів, про які ми говоримо в цій главі. Фронт-контролер - це короткий файл PHP, що знаходиться в веб-директорії і обробляє всі вхідні запити. Типовий додаток матиме фронт-контролер prod (наприклад, app.php) і фронт-контролер dev (наприклад, app_dev.php). Швидше за все, вам ніколи не доведеться редагувати, переглядати чи взагалі якось турбуватися про фронт-контролери у вашому додатку.

Простий Контролер

У той час як контролер може бути PHP-функцією, метод об'єкту, або замиканням (Closure), контролер - це, як правило, метод усередині класу контролера. Контролери також називаються діями (actions).

// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;

use Symfony\Component\HttpFoundation\Response;

class HelloController
{
    public function indexAction($name)
    {
        return new Response('<html><body>Hello '.$name.'!</body></html>');
    }
}

! Зверніть увагу, що контролер - це метод indexAction, який живе всередині класу контролера (HelloController). Не переплутайте: клас контролера - це просто зручний спосіб згрупувати кілька контролерів/дій разом. Як правило, клас контролера містить кілька контролерів/дій (наприклад, updateAction, deleteAction, і т.д.).

Цей контролер доволі простий і однозначний:

1) Рядок 4: Symfony використовує простори імен PHP для всього класу контролера. Ключове слово use імпортує клас Response, який контролер повинен повернути.

2) Рядок 6: ім'я класу - це поєднання імені для класу контролера (наприклад, Hello) і слова Controller. Це угода, що забезпечує логічність у найменуваннях контролерів і дозволяє посилатися на клас тільки за першою частиною імені (наприклад, Hello) в конфігурації маршрутизатора.

3) Рядок 8: Кожна дія в класі контролера має суфікс Action і згадується в налаштуваннях маршрутизації тільки за ім’ям (index). У наступному розділі ви створите маршрут, який прив’яже URI до цієї дії. Ви дізнаєтеся, як заповнювачі для імені в маршруті ({name}) стануть аргументами методу дії ($name).

4) Рядок 10: контролер створює і повертає об'єкт Response.

Прив'язування URL до контролера

Новий контролер повертає просту HTML-сторінку. Щоб переглянути цю сторінку в своєму браузері, вам потрібно створити маршрут, який прив'язує певний шаблон URL до контролера.

Annotations:

// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;

use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

class HelloController
{
    /**
     * @Route("/hello/{name}", name="hello")
     */
    public function indexAction($name)
    {
        return new Response('<html><body>Hello '.$name.'!</body></html>');
    }
}

YAML:

# app/config/routing.yml
hello:
    path:      /hello/{name}
    # uses a special syntax to point to the controller - see note below
    defaults:  { _controller: AppBundle:Hello:index }

XML:

<!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing
        http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="hello" path="/hello/{name}">
        <!-- uses a special syntax to point to the controller - see note below -->
        <default key="_controller">AppBundle:Hello:index</default>
    </route>
</routes>

PHP:

// app/config/routing.php
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

$collection = new RouteCollection();
$collection->add('hello', new Route('/hello/{name}', array(
    // uses a special syntax to point to the controller - see note below
    '_controller' => 'AppBundle:Hello:index',
)));

return $collection;

Тепер ви можете перейти на /hello/ryan (наприклад, http://localhost:8000/hello/ryan, якщо ви використовуєте вбудований веб-сервер) - і Symfony запустить HelloController::indexAction() і привласнить змінній $name значення ryan. Створення “сторінки” - це, по суті, створення методу контролера і відповідного маршруту.

Просто, правда?

! AppBundle:Hello:index controller syntax

Якщо ви використовуєте формати YML або XML, посилатися на контролер потрібно з допомогою спеціального синтаксису: AcmeBundle:Hello:index. Для більш детальної інформації про формат контролера дивіться підрозділ "Шаблон найменування контролера" у розділі "Маршрутизація".

Детальніше про систему маршрутизації дізнавайтеся у розділі "Маршрутизація, частина 1", "Маршрутизація, частина 2".

Параметри маршруту в якості аргументів Контролера

Ви вже знаєте, що маршрут посилається на метод HelloController::indexAction(), який знаходиться всередині AppBundle. Дуже цікавим є аргумент, що передається в цей метод:

// src/AppBundle/Controller/HelloController.php
// ...
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

/**
 * @Route("/hello/{name}", name="hello")
 */
public function indexAction($name)
{
    // ...
}

Контролер має один аргумент - $name, який відповідає параметру {name} з маршруту (ryan у випадку з /hello/ryan). При виконанні контролера, Symfony кожному аргументу контролера ставить у відповідність параметр з маршруту. Таким чином, значення {name} передається до $name.

Візьмемо наступний, більш цікавий приклад:

Annotations:

// src/AppBundle/Controller/HelloController.php
// ...

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

class HelloController
{
    /**
     * @Route("/hello/{firstName}/{lastName}", name="hello")
     */
    public function indexAction($firstName, $lastName)
    {
        // ...
    }
}
 

YAML:

# app/config/routing.yml
hello:
    path:      /hello/{firstName}/{lastName}
    defaults:  { _controller: AppBundle:Hello:index }

XML:

<!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing
        http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="hello" path="/hello/{firstName}/{lastName}">
        <default key="_controller">AppBundle:Hello:index</default>
    </route>
</routes> 

PHP:

// app/config/routing.php
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

$collection = new RouteCollection();
$collection->add('hello', new Route('/hello/{firstName}/{lastName}', array(
    '_controller' => 'AppBundle:Hello:index',
)));

return $collection;

Тепер контролер може приймати два аргументи:

 public function indexAction($firstName, $lastName)
{
    // ...
}

Встановлення відповідності між параметрами маршруту і аргументами контролера - легкий і гнучкий процес. Під час розробки дотримуйтесь нижчевказаних рекомендацій:

Порядок аргументів контролера не має значення

Symfony встановлює відповідність між іменами параметрів маршруту і іменами змінних контролера. Аргументи контролера можна як завгодно міняти місцями - і вони все одно будуть працювати бездоганно:

public function indexAction($lastName, $firstName)
{
    // ...
}

Кожен обов'язковий аргумент контролера повинен відповідати параметру маршрута

У наступному прикладі виникне виключення RuntimeException, так як в маршруті не визначений параметр foo:

public function indexAction($firstName, $lastName, $foo)
{
    // ...
}

Цей аргумент абсолютно спокійно можна зробити необов'язковим. В наступному прикладі не буде виникати виключення:

public function indexAction($firstName, $lastName, $foo = 'bar')
{
    // ...
}

Не всі параметри маршрутизації обов’язково повинні бути представлені у вигляді аргументів контролера

Якби, наприклад, параметр lastName не був важливим для вашого контролера, ви могли б опустити його повністю:

public function indexAction($firstName)
{
    // ...
}

! Кожен маршрут має спеціальний параметр _route, який містить значення, рівне його імені (наприклад, hello). Це значення зазвичай не використовується, проте цей параметр також доступний в якості аргументу контролера. Ви можете передати інші змінні з маршруту до аргументів контролера.

Див. Як передати додаткову інформацію з маршруту до контролера.

Запит в якості аргументу Контролера

Що робити, якщо вам потрібно прочитати параметри запиту, заголовок запиту або отримати доступ до завантаженого файлу? Вся ця інформація зберігається в об'єкті Symfony RequestЩоб помістити його у ваш контролер, просто додайте його в якості аргументу і введіть його з класом Request:

// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class HelloController extends Controller
{
    // ...
}

Хочете дізнатися більше про отримання інформації із запиту? Дивіться інформацію про запит до ступу.

Базовий клас Контролер

Для зручності, Symfony включає в себе опційний базовий клас Controller. Якщо ви розширите його, то отримаєте доступ до ряду допоміжних методів і до всіх ваших ресурсів через “container” (див. у підрозділі "Доступ до інших сервісів", що нижче). Додайте вираз use на початку класу Controller і модифікуйте HelloController, щоб розширити його:

 // src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class HelloController extends Controller
{
    // ...
}

Це насправді не змінює нічого в роботі контролера - це просто надає вам доступ до допоміжних методів, які базовий клас контролера робить доступними. Це просто ярлички для використання основного функціоналу Symfony, що доступний вам як з використанням базового класу Controller, так і без нього. Чудовий спосіб побачити основний функціонал в дії - зазирнути в Клас контролера.

! Якщо вам цікаво дізнатися, як би працював контролер, який не розширив базовий клас, дивіться Контролери як сервіси. Це необов'язково, але це може дати вам більше контролю над точними об'єктами/залежностями, які вводяться в ваш контролер.

Перенаправлення

Якщо ви хочете перенаправити користувача на іншу сторінку, використовуйте метод redirectToRoute():

public function indexAction()
{
    return $this->redirectToRoute('homepage');

    // redirectToRoute is equivalent to using redirect() and generateUrl() together:
    // return $this->redirect($this->generateUrl('homepage'), 301);
}

Метод redirectToRoute() був доданий у Symfony 2.6. Раніше (власне, як і зараз) ви могли використовувати redirect() і generateUrl() одночасно для цього (див. приклад вище).

Або, якщо ви хочете зробити зовнішнє перенаправлення, просто використовуйте  redirect() для URL:

public function indexAction()
{
    return $this->redirect('http://symfony.com/doc');
}

За замовчуванням, метод redirectToRoute() виконує 302 (тимчасовий) редирект. Щоб виконати 301 (постійний) редирект, змініть третій аргумент:

public function indexAction()
{
    return $this->redirectToRoute('homepage', array(), 301);
}

! Метод redirectToRoute() - це просто ярлик, що створює об'єкт Response, який спеціалізується на перенаправленні користувача. Це еквівалентно наступному:

use Symfony\Component\HttpFoundation\RedirectResponse;

public function indexAction()
{
    return new RedirectResponse($this->generateUrl('homepage'));
}

Рендеринг шаблонів

Якщо ви працюєте з HTML, вам знадобиться рендеринг шаблонів. Метод render() рендерить шаблон і розміщує його вміст в об'єкт Response для вас:

// renders app/Resources/views/hello/index.html.twig
return $this->render('hello/index.html.twig', array('name' => $name));

Ви також можете розміщувати шаблони в глибших підкаталогах. Намагайтесь уникати створення надто глибоких структур:

// renders app/Resources/views/hello/greetings/index.html.twig
return $this->render('hello/greetings/index.html.twig', array(
    'name' => $name
));

Шаблонізатор Symfony описується дуже докладно в одному з наступних розділів "Створення та використання шаблонів".

! Посилання на шаблони, які знаходяться всередині пакета (bundle)

Ви також можете розмістити шаблони в каталог Resources/views пакета і посилатися на них, використовуючи синтаксис BundleName:DirectoryName:FileName. Наприклад, AppBundle:Hello:index.html.twig звернулося до шаблону, розташованого в Ssrc/AppBundle/Resources/views/Hello/index.html.twig. Дивіться підрозділ "Посилання на шаблони в пакеті" у розділі "Створення та використання шаблонів".

Доступ до інших сервісів

Symfony комплектується безліччю корисних сервісів. Вони використовуються для рендерингу шаблонів, відправлення електронних листів, запитів до бази даних і будь-яких інших задач, які вам тільки можуть знадобитися. При встановленні нового пакета, швидше за все, сервісів стає ще більше.

При розширенні базового класу контролера, ви можете отримати доступ до будь-якого сервісу Symfony, використовуючи метод get(). Ось декілька популярних послуг, які вам можуть знадобитись.

$templating = $this->get('templating');

$router = $this->get('router');

$mailer = $this->get('mailer');

Які ще послуги існують? Для списку послуг використовуйте консольну команду debug:container.

$ php app/console debug:container

До Symfony 2.6 ця команда називалася container:debug.

Для отримання більш детальної інформації дивіться один з наступних розділів книги "Сервіс Контейнером".

Розбираємось із помилками і сторінкою 404

Коли щось не можна знайти, ви повинні добре “погратися” з протоколом HTTP і повернути відповідь 404. Щоб зробити це, згенеруйте особливий тип винятку. Якщо ви розширюєте базовий клас контролера, зробіть наступне:

public function indexAction()
{
    // retrieve the object from database
    $product = ...;
    if (!$product) {
        throw $this->createNotFoundException('The product does not exist');
    }

    return $this->render(...);
}

Метод createNotFoundException() - це ярлик для створення спеціального об'єкту NotFoundHttpException, що в кінцевому підсумку викликає HTTP-відповідь 404 в Symfony.

Звичайно, ви можете згенерувати будь-який клас Exception у своєму контролері - Symfony автоматично поверне код HTTP 500.

throw new \Exception('Something went wrong!');

У будь-якому випадку користувач побачить з тією чи іншою помилкою, а розробник побачить повну сторінку з інформацією щодо налагодження (тобто якщо ви використовуєте app_dev.php - дивіться Середовища і фронт-контролери).

Вам потрібно буде налаштувати сторінку помилок, яку ваш користувач бачить. Для цього дивіться розділ “Як налаштовувати сторінки помилок" в Довіднику.

Керування сесією

Symfony надає вам об'єкт, який ви можете використовувати для зберігання інформації про користувача (це може бути реальна людина, яка використовує браузер, бот, або ж веб-сервіс) між запитами. За замовчуванням, Symfony зберігає атрибути в куках (cookies), використовуючи нативні сесії PHP.

Зберігання та вилучення інформації з сесії легко можна здійснити з будь-яким контролером:

use Symfony\Component\HttpFoundation\Request;

public function indexAction(Request $request)
{
    $session = $request->getSession();

    // store an attribute for reuse during a later user request
    $session->set('foo', 'bar');

    // get the attribute set by another controller in another request
    $foobar = $session->get('foobar');

    // use a default value if the attribute doesn't exist
    $filters = $session->get('filters', array());
}

Ці атрибути будуть залишатися на користувачі до кінця його сесії.

Flash-повідомлення

Ви можете також зберігати невеликі повідомлення в сесії користувача для рівно одного додаткового запиту. Це зручно при обробці форми: ви зробити перенаправлення і відобразити особливе повідомлення, що буде показуватись на наступній сторінці. Ці типи повідомлень називаються flash-повідомленнями.

Наприклад, уявіть, що ви обробляєте відправлення форми:

use Symfony\Component\HttpFoundation\Request;

public function updateAction(Request $request)
{
    $form = $this->createForm(...);

    $form->handleRequest($request);

    if ($form->isValid()) {
        // do some sort of processing

        $this->addFlash(
            'notice',
            'Your changes were saved!'
        );

        // $this->addFlash is equivalent to $this->get('session')->getFlashBag()->add

        return $this->redirectToRoute(...);
    }

    return $this->render(...);
}

Після обробки запиту контролер встановлює флеш-повідомлення notice в сесії, а потім здійснює перенаправлення. Ім'я (notice) не встановлюється жорстко - це лише позначені типи повідомлення.

У шаблоні наступної дії ви можете використовувати наступний код для відображення повідомлення notice:

Twig:

{% for flashMessage in app.session.flashbag.get('notice') %}
    <div class="flash-notice">
        {{ flashMessage }}
    </div>
{% endfor %}

PHP:

<?php foreach ($view['session']->getFlash('notice') as $message): ?>
    <div class="flash-notice">
        <?php echo "<div class='flash-error'>$message</div>" ?>
    </div>
<?php endforeach ?>

За задумом, флеш-повідомлення повинні оброблятися лише один раз. Це означає, що вони зникають з сесії автоматично коли їх добувають з флеш-сумки, використовуючи метод get().

Натомість, ви можете використовувати метод peek() щоб добути повідомлення, коли воно знаходиться в сумці.

 Об’єкт Відповідь (Response)

Єдина вимога для контролера - повернути об'єкт Response. Клас Response являє собою PHP-абстракцію HTTP-відповіді - текстового повідомлення, що складається з HTTP-заголовків і контенту, який повертається клієнту.

use Symfony\Component\HttpFoundation\Response;

// create a simple Response with a 200 status code (the default)
$response = new Response('Hello '.$name, Response::HTTP_OK);

// create a JSON-response with a 200 status code
$response = new Response(json_encode(array('name' => $name)));
$response->headers->set('Content-Type', 'application/json');

Headers - це об’єкт HeaderBag, що містить методи для читання і змін заголовків відповіді Response. Імена заголовків нормалізовані, тому і Content-Type, i content-type і навіть content_type еквівалентні.

Є також спеціальні класи, створені для того, щоб робити певні варіанти відповідей простішими:

1) Для JSON - JsonResponse. Див. Створення відповіді JSON.

2) Для файлів - BinaryFileResponse. Див. Обслуговування файлів.

3) Для потокових відповідей - StreamedResponse. Див. Потокові відповіді.

! Можете не хвилюватись - є значно більше інформації про об'єкт відповіді в документації. Див. Відповідь.

Об’єкт Запит (Request)

Крім значень заповнювачів маршрутизації, контролер також має доступ до об'єкту Request. Фреймворк вводить до контролера об'єкт Request, якщо змінна асоціюється із запитом:

 use Symfony\Component\HttpFoundation\Request;

public function indexAction(Request $request)
{
    $request->isXmlHttpRequest(); // is it an Ajax request?

    $request->getPreferredLanguage(array('en', 'fr'));

    $request->query->get('page'); // get a $_GET parameter

    $request->request->get('page'); // get a $_POST parameter
}

По аналогії до об'єкта Response хедери запитів знайти неважко: вони зберігаються в обєкті HeaderBag.

! Можете не хвилюватись: є значно більше інформації про об'єкт запиту в документації. Див. Запит.

Створення статичних сторінок

Ви можете створити статичну сторінку навіть без створення контролера (необхідні тільки маршрут і шаблон).

Читайте про те, як створювати шаблон без типового контролера.

Переадресація на інший Контролер

Хоча це не потрібно часто, ви також можете робити перенаправлення на інший контролер з допомогою методу forward(). Замість того, щоб перенаправляти браузер користувача, він робить внутрішній підзапит і викликає контролер. Метод forward() повертає об'єкт Response, який повертається з того контролера:

public function indexAction($name)
{
    $response = $this->forward('AppBundle:Something:fancy', array(
        'name'  => $name,
        'color' => 'green',
    ));

    // ... further modify the response or return it directly

    return $response;
}

Зверніть увагу, що метод forward() використовує спеціальне строкове представлення контролера (див. Шаблон найменування контролера). У цьому випадку, функція заданого контролера буде SomethingController::fancyAction() усередині AppBundle. Масив, що передається методу, стає аргументом в кінцевому контролері. Ця сама ідея використовується при вбудовуванні контролера в шаблони (див. Вбудовування контролерів). Метод заданого контролера буде виглядати так:

public function fancyAction($name, $color)
{
    // ... create and return a Response object
}

Так само, як при створенні контролера для маршруту, порядок аргументів fancyAction не має значення. Symfony підбирає ключові назви (наприклад, name) з назвами аргументу методу (наприклад, $name). Якщо ви зміните порядок аргументів, Symfony все одно надасть правильне значення кожній змінній.

Висновки

Коли ви створюєте сторінку, зрештою вам буде потрібно написати код, який містить логіку цієї сторінки. У Symfony це називається "контролером", і являє собою PHP-функцію, яка виконує всі необхідні дії для того, щоб повернути об'єкт Response, який буде відправлено користувачеві.

Щоб полегшити собі життя, ви можете розширити базовий клас Controller, який містить ярлики для типових завдань, що вирішуються контролером. Наприклад, так як ви не хочете повертати HTML код - ви можете використовувати метод render() і повертати контент з шаблону.

В інших розділах ви дізнаєтеся, як контролер може бути використаний для збереження і отримання об'єктів з бази даних, обробляти відправлення форм, працювати з кешем і багато іншого.

Додатково в Довіднику:

   Як налаштувати сторінки помилок

   Як визначати Контролери в якості сервісів

Поділитися