Symfony2 на node.js

в 7:39, , рубрики: javascript, node.js, symfony, symfony2, фреймворк, метки: , ,

Я люблю node.js за скорость работы и люблю Symfony2 за гибкую архитектуру. Но к сожалению Symfony2 довольно тяжелый фреймворк, т.к. инициализация DI контейнера занимает существенное время, и это происходит при каждом запросе. Тут то мне и пришла в голову идея — а почему бы не реализовать аналог Symfony2 на node.js?
В отличие от php — нам не нужно реализовывать систему кэширования классов и т.п. — все это хранится в памяти и инициализируется только один раз (при старте приложения).
Однако в процессе портирования Symfony2 на node.js возник ряд сложностей, связанных с асинхронном моделью node.js, а также с тем, что массивы и хэши в javascript устроены не одинаково (в отличие от php).

Кому интересно что получилось — прошу под кат.

Надеюсь никто не расценит мой поступок высокомерным или плагиатом, но проект я решил назвать JSymfony. На мой взгляд название отлично отображает обе концепции проекта — JS и Symfony.
Расммотрим наконец этапы разработки, состояние проекта и планы на ближайшее время.

Примечание 1. Примеры кода по различным компонентам в статье приводить не буду, иначе статья займет очень много места. Примеры доступны в соответствующих репозиториях в папке examples

Примечание 2. В посте я буду использовать слово класс вместо функция-конструктор да простят меня гуру javascript (подождем истинной реализации слова class в harmony ecmascript)

Примечание 3. В переписываемых компонентах я старался по максимуму прописывать JSdoc у классов и методов

Примечание 4. В связи с очень большим объемом кода, времени на написание тестов и грамотных README файлов пока что не было. Надеюсь мне помогут с этим заинтересованные люди

Примечание 5. Статья рассчитана на тех, кто понимает хотя бы примерно как устроена Symfony2 изнутри

1. Реализация системы пространств имен (и автозагрузки)

Да, я люблю node.js за скорость, но мне не нравится их подход с системой модулей. Система модулей отлично подходит для небольших проектов, но многие разработчики начинают писать абсолютно не расширяемый код. Чтобы не быть голословным — в конце статьи приведены примеры не расширяемого кода.
Моей идеей было внедрить в node.js аналог пространств имен (namespace). Чтобы в любом коде программы можно было писать

var router = new JSymfony.Routing.Router();
var fileLocator = new JSymfony.Config.FileLocator(__dirname);

//вместо
var JSymfonyRouting = require('jsymfony-routing');
var JSymfonyConfig = require('jsymfony-config')
var router = JSymfonyRouting.Router();
var fileLocator = JSymfonyConfig.FileLocator(__dirname);

// или вместо
var router = new (require('jsymfony-routing').Router)();
var fileLocator = new (require('jsymfony-config').FileLocator)(__dirname);

Кроме того первый вариант гораздо лучше подсвечивается в IDE (я использую phpStorm) и позволяет перейти к файлу с определением «класса» Router, нажав на слово Router в этой строке: var router = new JSymfony.Routing.Router() (в варианте require('smth').Foo.Bar перейти к файлу, в котором описывается Bar часто бывает очень проблематично).

Итогом работы стала реализация модуля, позволяющего легко имитировать namespace в javascript. Исходный код модуля доступен здесь: github.com/jsymfony/autoload

2. Реализация базового модуля

Следующим шагом стало создание базового модуля, содержащего часто используемые функции и стандартные классы ошибок (например RuntimeError, InvalidArgumentError и т.п.) — кто хоть раз пытался наследоваться от стандартного Error, чтобы корректно работал instanceof и стеки — поймут. Также есть класс FunctionReflect, позволяющий из описания функции получить имена аргументов функции (нужно при вызове методов контроллеров с параметрами из роутинга).

Код базового модуля доступен здесь: github.com/jsymfony/base

3. Реализация компонента Config

Следующим этапом стало непосредственное переписывание компонента Config из Symfony2, так как он используется в компоненте Dependency Injection, который в конечном итоге нам и нужен. На этом шаге пришлось столкнуться с проблемой в разном механизме работы с массивами и хэшами в javascript.

Код доступен тут: github.com/jsymfony/config

4. Dependency Injection Component

На мой взгляд самый важный компонент в Symfony2. Именно этот компонент отвечает за наполнение нашего контейнера сервисами и параметрами. Самая большая проблема при написании модуля возникла при реализации scope в контейнере.
Symfony2 (что такое scope и зачем они — тут: symfony.com/doc/2.0/cookbook/service_container/scopes.html)
Поскольку php работает синхронно, то в каждый момент времени контейнер может находится только в одном из scope. Поэтому разработчики Symfony2 при входе в новый scope просто заносят конфликтующие сервисы в стек, а при выходе восстанавливают их обратно. Но в node.js наш DI контейнер может находится одновременно сразу в нескольких scope, поэтому физически невозможно использовать один экземпляр контейнера при работе в node.js. В итоге был реализован механизм, корректно работающий со scope в node.js

Код компонента доступен тут: github.com/jsymfony/dependency-injection

5. Routing

Очередным этапом переписывания компонентов Symfony2 стал компонент Routing. Как обычно пришлось столкнуться с определенными проблемами, на этот раз связанными в основном с реализацией регулярных выражений (нет поддержки именованных параметров и нет поддержки possessive quantifiers).

Код компонента доступен тут: github.com/jsymfony/routing

Текущее состояние и планы

Есть частичная реализация бандлов
framework (сердце Symfony2 — именно тут и происходит вся магия по запуску приложения, инициализации контейнера, обработки запросов)
passportjs (аналог security в Symfony2 и опирающийся на passportjs.org/)
cache (в качестве драйверов реализованы memory и memcache)
cluster (обертка вокруг cluster модуля node.js, добавляющая несколько методов для более удобной коммуникации между master и workerами)
mysql (обертка вокруг github.com/felixge/node-mysql, реализующая также пул коннектов и возможность иметь под одним именем несколько slave и master коннектов)

Коды этих бандлов пока что не находятся в репозитории, т.к. нужно для начала причесать там код, т.к. изначально код писался для себя и в нем почти нет комментариев.

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

Заключение

Проделано много работы по портированию Symfony2 на node.js. К сожалению одному это делать довольно сложно, поэтому предлагаю заинтересовавшимся помочь мне в этом нелегком деле.

Забегая вперед

Наверняка сейчас начнутся комментарии «зачем еще один фреймворк», почему бы не использовать «express» и т.п. Поэтому сразу отвечу
1. Ни один фреймворк для node.js, который я встречал, не обладает такой гибкостью и лаконичностью как Symfony2 для php.
2. Я ни в коем случае не хочу никому навязывать свое мнение, но для меня очень популярный express.js (и connect.js) обладают рядом больших минусов, таких как:
а) невозможность декорировать функцию next в middleware (т.е. написать профайлер запроса станет невозможным)
б) конфиги для middleware часто дублируют друг друга (например для cookieParse и session нужно два раза указывать один и тот же secret). Кроме того нет возможности добавить функционал в текущий middleware (очень хотелось вместо boolean параметра trusted_proxy иметь список ip адресов для trusted_proxies), например, посмотрите это: github.com/senchalabs/connect/blob/master/lib/middleware/session.js
в) функция get одновременно работает и как часть роутера и как getter параметров (на мой взгляд это не самый лучший подход — иметь два абсолютно разных поведения для функции).
г) при передаче параметров в шаблоны эти параметры смешиваются с настройками движка шаблонов, что иногда приводит к неочевидным проблемам

Автор: Sirian

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js