Symfony2 → Symfony2 и Ember.js
Внимание, данная запись устарела. Работоспособность модуля ember-precompile под вопросом начиная с Emberjs 1.9, в связи с переходом на handlebars 2.0.0, а с версии эмбера 1.10+ шаблоны рендерятся с помощью HTMLBars, об этом напишу в ближайшее время.
Эту тему, конечно, в двух словах не опишешь, но попробую. Что это такое и с чем его едят - рассказывать не буду, т.к. раз уж вы сюда попали, то слова эти для вас не пустой звук. Расписывать, почему именно Symfony2 и Ember.js - тоже. Просто так сложилось. Ладно, вступление окончено.
В данной записи будет затронута только серверная часть, хотя не исключено, что когда-нибудь дойдут руки и до статеек по эмберу. Собственно, Symfony-приложение предоставит яваскриптовому приложению REST API, а так же неким образом "подготовит" его и выдаст клиенту в браузер.
Начать можно с установки необходимых файлов и поможет в этом bower, пакетный менеджер яваскриптовых файлов, оптимизированный для фронтенд-разработки (не знаю, что это значит, но так написано на официальном сайте). Для Symfony2 имеется и специальный бандл, чтобы с bower-ом управляться, SpBowerBundle. Конечно, никто вас не заставляет этим пользоваться, можете и дальше искать и качать нужные библиотеки вручную, как в программистском средневековье.
Поехали.
npm install bower -g composer require sp/bower-bundle
Обновим AppKernel
// app/AppKernel.php // ... public function registerBundles() { $bundles = array( // ... new Sp\BowerBundle\SpBowerBundle(), ); // ... } // ...
и конфигурацию
# app/config/config.yml sp_bower: bundles: SuperPuperBundle: ~
Установим, собственно, зависимости для bower
// 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" } }
Для установки вендорких файлов достаточно выполнить команду
app/console sp:bower:install
Для того, чтобы установка или обновление библиотек происходило при запуске композера, нужно добавить ещё пару строк
\\ 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-код. Этого, конечно, можно и не делать, т.к. эмбер производит эту компиляцию на лету, в памяти. Но зачем? Почему бы не сделать предварительную работу сразу на сервере, единоразово, зачем тратить лишние десятки миллисекунд времени клиента при каждом запросе? Электроэнергия, опять же, зря расходуется на лишние вычисления :)
Устанавливаем модуль
npm install ember-precompile -g
Подключаем фильтр
# app/config/config.yml assetic: bundles: [ SuperPuperBundle ] filters: emberprecompile: ~
Применяем
{% 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-данные
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' ); } } } } } }
И подключаем его
# 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, например, можно выбирать данные из запроса.
// 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'); // ... }
На сегодня хватит :)
Адрес электронной почты нигде не отображается, необходим только для обратной связи.
Веб-сайт вводите в формате http://example.org, при желании, конечно.