Symfony2 → Symfony2 и Ember.js

Внимание, данная запись устарела. Работоспособность модуля ember-precompile под вопросом начиная с Emberjs 1.9, в связи с переходом на handlebars 2.0.0, а с версии эмбера 1.10+ шаблоны рендерятся с помощью HTMLBars, об этом напишу в ближайшее время.

Эту тему, конечно, в двух словах не опишешь, но попробую. Что это такое и с чем его едят - рассказывать не буду, т.к. раз уж вы сюда попали, то слова эти для вас не пустой звук. Расписывать, почему именно Symfony2 и Ember.js - тоже. Просто так сложилось. Ладно, вступление окончено.

Ember.js и Symfony2

В данной записи будет затронута только серверная часть, хотя не исключено, что когда-нибудь дойдут руки и до статеек по эмберу. Собственно, Symfony-приложение предоставит яваскриптовому приложению REST API, а так же неким образом "подготовит" его и выдаст клиенту в браузер.

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

Поехали.

1
2
npm install bower -g
composer require sp/bower-bundle

Обновим AppKernel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php
// app/AppKernel.php
// ...
public function registerBundles()
{
    $bundles = array(
        // ...
        new Sp\BowerBundle\SpBowerBundle(),
    );
    // ...
}
// ...

и конфигурацию

1
2
3
4
# app/config/config.yml
sp_bower:
    bundles:
        SuperPuperBundle: ~

Установим, собственно, зависимости для bower

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// src/Super/PuperBundle/Resources/config/bower/
{
    "name": "super-puper-bundle",
    "dependencies": {
        "jquery": "~1.11",
        "ember": "~1.7.0",
        "ember-data": "1.0.0-beta.9",
        "blueimp-file-upload": "9.7.0",
        "moment": "2.8.3",
        "es5-shim": "4.0.3",
        "typeahead.js": "0.10.5"
    }
}

Для установки вендорких файлов достаточно выполнить команду

1
app/console sp:bower:install

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// composer.json
{
    // ...
    "scripts": {
        "post-install-cmd": [
            // ...
            "Sp\\BowerBundle\\Composer\\ScriptHandler::bowerInstall"
        ],
        "post-update-cmd": [
            // ...
            "Sp\\BowerBundle\\Composer\\ScriptHandler::bowerInstall"
        ]
    }
}

Следующее, в чём поможет Symfony2, это собрать все файлы яваскрипта до кучи, ведь не секрет, что у одностраничных приложений получается много этих самых файлов, а загружать их все по отдельности накладно. Специально для этих целей (сборка в один файл, минимизация и т.п.) из коробки уже имеется AsseticBundle. Не буду особо про него разглагольствовать, всё довольно-таки подробно описано уже в документации. Отдельно только отмечу один фильтр, который в контексте этой заметки никак не помешает. Это фильтр ассетика emberprecompile. Суть его заключается в компиляции Handlebars-шаблонов, используемых Ember.js, в JavaScript-код. Этого, конечно, можно и не делать, т.к. эмбер производит эту компиляцию на лету, в памяти. Но зачем? Почему бы не сделать предварительную работу сразу на сервере, единоразово, зачем тратить лишние десятки миллисекунд времени клиента при каждом запросе? Электроэнергия, опять же, зря расходуется на лишние вычисления :)

Устанавливаем модуль

1
npm install ember-precompile -g

Подключаем фильтр

1
2
3
4
5
6
# app/config/config.yml

assetic:
    bundles: [ SuperPuperBundle ]
    filters:
        emberprecompile: ~

Применяем

1
2
3
4
{% javascripts filter='emberprecompile'
    '@SuperPuperBundle/Resources/public/app/templates/*.hbs' %}
<script type="text/javascript" src="{{ asset_url }}"></script>
{% endjavascripts %}

Ну а теперь перейдём к самой главной, наверное, функции серверноего приложения, к API. Для эмбера существуют разные адаптеры для доступа к серверу, где данные хранятся, однако чаще используется REST API. В Symfony2 имеются готовые решения и на этот случай, например FOSRestBundle вместе с JMSSerializerBundle. Для каких-нибудь немаленьких проектов использование таких монструозных бандлов оправдано, чтобы избежать большой рутинной работы или обеспечить одним взмахом левой пятки возможность отдавать и JSON, и XML, и XHTML по запросу приложения-клиента. Но я напишу как можно обойтись и без них.

При использовании эмберовского DS.RESTAdapter каждая модель извлекается/сохраняется/создаётся/удаляется по определённым URL-ам с использованием специфических методов GET, PUT, POST, и DELETE, данные при этом передаются в теле запроса в виде JSON. Из коробки такая схема не работает, но есть решение (подсмотренное мной в FOSRestBundle).

Создаём EventListener, который будет просматривать объект запроса и искать в нём JSON-данные

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php

namespace Super\PuperBundle\EventListener;

use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

class JsonBodyListener
{
    /**
     * @param GetResponseEvent $event
     * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
     */
    public function onKernelRequest(GetResponseEvent $event)
    {
        $request = $event->getRequest();
        $method = $request->getMethod();

        if (!count($request->request->all())
            && in_array($method, array('POST', 'PUT', 'DELETE'))
        ) {
            $contentType = $request->headers->get('Content-Type');

            $format = null === $contentType
                ? $request->getRequestFormat()
                : $request->getFormat($contentType);

            if ($format == 'json') {
                $content = $request->getContent();
                if (!empty($content)) {
                    $data = json_decode($content, true);
                    if (is_array($data)) {
                        $request->request = new ParameterBag($data);
                    } else {
                        throw new BadRequestHttpException(
                            'Invalid ' . $format . ' message received'
                        );
                    }
                }
            }
        }
    }
}

И подключаем его

1
2
3
4
5
6
7
# src/Super/PuperBundle/Resources/config/services.yml

services:
    super_puper.json_body_listener:
        class: Super\PuperBundle\EventListener\JsonBodyListener
        tags:
            - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 10 }

Вот, собственно, и всё. Теперь при сохранении эмберовской модели с именем status, например, можно выбирать данные из запроса.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php
// src/Super/PuperBundle/Controller/MegaController.php

/**
 * @Route("/status/{id}", requirements={"id": "\d+"})
 * @Method("PUT")
 *
 * @param Request $request
 * @param Status $entity
 *
 * @return JsonResponse
 */
public function saveStatusAction(Request $request, Status $entity)
{
    $statusData = $request->request->get('status');
    // ...
}

На сегодня хватит :)

Комментарии

avatar
Alex
avatar
мало, давай еще
ответить
avatar
morontt
avatar
Будет ещё. Тут вот про сам эмбер ни слова нет.
ответить
avatar
Хуторная Елена
avatar
Давненько ты сюда не заходил )))
ответить
avatar
morontt
avatar
Знаю, самого плющит :) Идеи на записи возникают еженедельно. Но никак не могу сесть и сделать. То одно, то другое, то просто спать уже охота.
ответить
4 комментария Написать что-нибудь
Или войдите, чтобы не заполнять форму:
Адрес электронной почты нигде не отображается, необходим только для обратной связи.
Напрограммировано на Go 1.23.2, версия движка 7738e0f