Идеологический вопрос по реализации модульности для php-фреймворка?
Я большой фанат Codeigniter, но у него, как известно, отсутствует такая вещь как HMVC (т.е. ни о какой модульности из коробки нет речи). Есть ужасные монстроподобные расширения, но мне они не нравятся.
Поэтому я пошел по пути «хочешь сделать что-тохорошо — сделай это сам». Нашел простое и элегантное решение, делающее игнайтер очень даже модульным.
Но есть несколько идеологических вопросов (которые влияют на синтаксис вызова модулей и не только), на которые я никак сам себе не могу ответить:
1. При вызове метода модуля — он обязательно должен возвращать контент (html/json/xml — не важно), или стоит предусмотреть вариант, что он вернет не контент, а данные, или просто сделает что-то и не вернет ничего?
2. Доступность модуля по URL — в передигме HMVC это одиз из атрибутов. Но меня он сильно смущает. Написать modulerouter — вообще не проблема, но я откровенно не понимаю, зачем? Для ajax-приложений? Но для таких целей, мне кажется, правильнее писать отдельный ajax-интерфейс, в котором будет только то, что нужно для приложения с обработкой post данных и т.д. и т.п. К тому же, сильно беспокоит вопрос безопасности при доступе модулейпо url. Было бы очень интересно услышать мнения на этот счет.
3. В случае REST-style вызовов модулей — как было бы лучше? modulename/methodname/viewname?param1=x?
1. Модуль возвращает данные и только данные. Не важно в каком из форматов (JSON/XML/etc), главное что там нет разметки отвечающей за представления вида данных. HTTP заголовки тоже данные.
2. Ни каких отдельных интерфейсов AJAX/не-AJAX. Поэтому как с точки зрения сервера все эти запросы это HTTP запросы. И AJAX по сути существует на уровне клиента, этого слоя не должно быть в архитектуре серверной части.
Возможность доступа к модулю по URL оказывается полезной когда приложение разрастается до уровня больше чем один сервер. Если об этом не думать изначально на уровне архитектуры, то придется реализовывать «отдельный интерфейс». Если подумать сразу, то можно получить его сразу, искаропки так сказать.
3. Приведенный пример больше RPC, чем REST.
В доступе к модулю по HTTP не должно быть ни чего криминального если он реализован корректно. Дернули снаружи код модуля, а сами аутентификацию не прошли? Значит, если работаем в REST стиле, возращаем 403 статус ответа.
1. «главное что там нет разметки отвечающей за представления вида данных» — почему? О_о Ведь самый кайф модулей — это уомплектованные и легко переносимые «пакеты», у которых помимо всего прочего есть и свои views (которые, при необходимости, можно перекрыть, но «в комплекте» все равно есть стандартные)
2. Но ведь какие-то модули (в данном приложении) могут быть доступны гостям, какие-то юзерам (которые авторизируются ведь в приложении, а не http-авторизацией), какие-то вообще определенным группам юзеров. Получается, эти правила нужно учитывать на уровне самих модулей? Но тогда ухудшается переносимость, т.к. если у одного приложения политика доступов одна, у другого — другая, то прийдется и модули модифицировать — не комильфо…
3. Модет быть, но тем не менее, вопрос «как скорее всего будет удобнее» открытый)
1.
а) размер данных, меньше размер данных — больше можно положить в кэш;
б) у модуля может быть множество клиентов с множеством вариантов представления данных, но его это не заботит, конечный вид данных не его забота и значит код модуля проще и кода становится меньше, он более понятен, его проще поддерживать; в дефолтном представлении данных не вижу смысла, все равно коду-клиенту модуля понадобится какая-то другая структура (другая логика разметки, дополнительные классы, прочее), а если вид на столько прост что подошел бы и дефолтный, вот так пусть в коде-клиенте он и будет.
Веб проекты в свое развитии как правило эволюционные и «необходимость перекрытия» возникает очень быстро. Поэтому лично в моей практике дополнительная сущность «дефолтный вид» очень быстро становиться атавизмом мещающим поддержке кода модуля.
2. У класса модуля можно определить свойство в котором хранить объект (инстанс другого модуля по сути) определяющий правила доступа и для значимых действий (добавить/удалить/изменить) обращаться, допустим, к методу isAllow() этого объекта. При этом модулю совершенно не важно на основании чего принимается решение, он лишь получает булево значение можно/нельзя. А это объект может данные уже брать из файла конфига, из базы, принимать решение на основании IP клиента или еще исходя из других соображений, все это не важно в рамках текущего модуля. Такая архитектура приложения позволяет упростить код самого модуля, его проще отлаживать и поддерживать.
В любом случае модуль не может быть абсолютно независим (т.е. невозможно вытащить его из системы А и запихать в систему Б и при этом не потянуть за собой необходимость зависимостей), но его можно сделать слабо связанным и приведенный мою пример это один из вариантов этого.
3. Просто methodname уже говорит, что это не REST, а RPC. При REST у нас значимые действия задаются в виде HTTP методов.
Ну, возьмем модуль User. При твоем подходе, чисто гипотетически исходя из твоей схемы, для задач:
— получить список пользователей вызываем User/getList/viewGrid?filter[name]=Joe
— получить конкретного пользователя вызываем User/getProfile/viewList?user_id=20
— удалить конкретного пользователя вызываем User/delete/?user_id=20
А это RPC-style. Заметь, при такой схеме мне пришлось user_id вынести в параметры запроса. А теперь как теже задачи реализуются у меня:
— получить список пользователей шлем запрос GET методом на User/?filter[name]=Joe
— получить конкретного пользователя шлем запрос GET методом на User/20
— удалить конкретного пользователя шлем запрос DELETE методом на User/20
И ни каких видов внутри самого модуля. Данные генерированные модулем могут уйти в шаблонный движок, или на клиент или еще куда, не важно, в самом модуле у нас нет бизнес логики работы с видом. Опять же повторюсь, код модуля становиться проще, он более понятен, его проще отлаживать и поддерживать.
Удачность того или иного архитектурного решения проявляется как раз на уровне поддержки кода (на сколько сложно его модифицировать под новые условия, как сложно дебажить) и в меньшей степени его «переносимостью» потому как в вебе, особо в случае PHP код твоих модулей все равно будет работать в рамках какой либо системы/фреймворка/etc и его без напильника перенести на другую систему не получится.
1. и 3 (про представления):
— согласен практически полностью, кроме того, что наличие встроенных представлений усложняет код модуля. Эти представления лежат ведь себе в отдельной подпапке внутри папки модуля и на логику модуля не влияют. И взаимодейтсвие с представлением — не на уровне модуля, а на уровне класса, который отвечает за загрузку и выполнение методов модуля (modulerouter тобишь).
Т.е. у меня сейчас как: при вызове модуля ( $this->load->module(%modulename%) *это codeigniter-style, все через суперобъект проходит, а классы загружаются loader'ом* ) возвращается объект класса module, у которого есть вспомогательные методы и метод run, в который передется имя метода, параметры и (опционально) представление. Если представление небыло передано — возвращаются данные (json?), если было — то производится поиск представлений с названием %modulename%_%viewname% в папке модуля и в общей папке представлений приложения (у него более высокий приоритет) и сразу возвращается html. Мне кажется, это довольно жизнеспособный вариант…
ХОТЯ чем дальше думаю над этим всем, тем больше склоняюсь к вашему мнению, что наличие этих шаблонов довольно сомнительно…
2. Вот за объект с правами — огромное спасибо. Это шикарная идея. У меня какие-то схожие мысли просачивались, но как-то не сформировались.
3. При RPC / REST запросах — тут конечно без видов, только json/xml.
Вообще, огромное спасибо, вы очень помогаете прийти к «истине»
1. Любой вариант жизнеспособен, даже Joomla живет и процветает. Но опять же, каждый исходит из своей практики. Мой опыт показывает, что необходимость в дефолтный видах очччень быстро вырождается. И получается, что поиск «в папке модуля» это лишняя дисковая операция. А самое узкое место в приложениях у нас по прежнему I/O на диске. Оно конечно решаемое, к примеру, держать в той же shared memory результат поисков (как кэш файлов nginx-а), но усложняет код пусть не модуля, но приложения в целом. А в итоге нафига, если клиентам нашего модуля все равно нужна своя разметка.
Поэтому у меня модули генерят данные и только данные. Они делают одну простую задача, каждый из них решает свою задачу, но делают это хорошо. Как результат при одном же серверном коде у каждого юзера в браузере может быть свой скин абсолютно не похожий на другие. А на сервере в кэше компактно лежат данные и только данные.
3. Вот видишь, а тут у тебя выходит «уже без видов». Выкидываем вид вообще, делаем модули доступными по REST и у нас из модуля или приложения в целом (ибо если даже код не в модуле, он все равно есть в приложении) выносится нафиг целая прослойка.
Просто наблюдая разные системы могу заметить, что народ наворачивает разные уровни абстракций, сдабривает разными паттернами, наворачивает разные ООП примочки, но мало кто думает, ну нафига все это. В результате такой код потом дороже в саппорте. Лично я агитирую за максимальную простату и специализированность кода. Модуль должен решать одну задачу и только её, код метода не должен быть больше 3-4 экранов, все связи модуля с другими модулями должны быть очевидны и понятны без сокрытия их в недрах приложения, т.е. минимизация использования разных «магических» методов. Все это справедливо в бОльшей степени для проектов ориентированных на группу разработчиков, но и для варианта когда с фрейворком работаешь только ты сам так же имеет вес. Потому как даже сам создатель и архитектор проекта по мере развития начинает забывать о конкретных деталях реализации того или иного участка системы. Цена кода, с точки зрения бизнеса, определяется вложениями не на первичную архитектуру, а на цену саппорта этого кода в дальнейшем.
Кстати. Внесу наверное последних пять копеек в вопрос доступности модуля извне. Он должен быть доступен снаружи приложения, но не напрямую, а через модуль-роутер. А мотивация к этому не столько желания ограничения, сколько масштабируемость у целом. Потому как роутер знает входящий URI и знает в какой метод какого модуля ему соответствует, он может проводить нормализацию URI (к примеру, приводя ЧПУ к единому внутреннему формату), проводить проверку прав. Это позволяет не меняя кода самого модуля повесить его вызов на какой угодно адрес (если правила роутинга хранятся, к примеру, в базе, то и код роутера не меняется). Что не исключает варианта прямого вызова. В общем тут достаточно вариантов для маневра исходя из конкретное задачи.
Не, ну точто доступ извне только через модульроутер — это понятно. Прямой вызов извне, имхо, это очень плохо. Банально опасно (а, кстати, в ключе Codeigniter, о котором речь — невозможно :) ).
И вот сильно подмывает переложить ограничения доступа именно на роутер…
ЗЫ
благодаря этому обсуждению класс для организации модулей в CI созрел и, по-моему, стал отлично юзабелен, так что спасибо) Еще обкатаю и покажу на хабре.
Вопрос с ограничениями доступа, все же, никак не могу утрясти — то-ли на уровне модулей, то-ли на уровне роутера. плюсы явно есть у обоих подходов…
Насчет опасности при доступе снаружи не согласен. Имхо, она возникает если нет уверенности в архитектуре проекта («ой, а вдруг, что-то где-то не экранируется...»).
Плюсы и минусы есть и там и там и они в принципе равноценные, но лично я схему работы с правами не сосредотачиваю ни в модуле, ни в роутере. Кстати, я различаю понятие доступа к модулю и права доступа. Доступ к модулю это возможность вызвать методы модуля в принципе. Этим занимается роутер. Он может преобразовать входящий URI в метод нужного модуля и обеспечивает передачу параметров в него. Модуль роутера абсолютно ни чего не знает про права, текущий модуль ни чего не знает про роутер. В итоге и модуль роутера и текущий модуль имееют очень низкую степень связанности. Как результат — переход, к примеру, от RPC к REST требует лишь смены роутера. Правами же доступа рулит третий модуль обеспечивающий ACL. Вот инстанс его объекта находится в свойстве текущего модуля и реализует метод isAllow(). Связанность и тут получается низкой с хорошей инкапсуляцией. Смена ACL модуля ни как не затрагивает связанный код, источник данных для ACL модуля так же личное дело самого ACL модуля и смена хранения инфы в файле может быть просто сменена на схему хранения в БД или вообще в AD, затрагивает это только код самого модуля.
В общем получается, что ограничение доступа реализуется ни в самом модуле, ни даже в роутере. Имхо, это упрощает код приложения в целом. Каждый модуль решает свою одну задача, pure unix way.
Всё имхо.
1. Иногда модуль должен делать перенаправление, не возвращая ничего. Либо возвращать JSON стакой командой.
2. Для распределённых приложений, чтобы с другой машины обращаться к первой за модулем, будучи уверенным, что данные придут. Допустим основная машина — сам сайт, вторая — модуль аукциона с высокими затратами, и она дёргается извне первой машиной.
2. Тогда это получается по большому счету автоматическая реализация REST API и modulerouter должен пускать к себе только разрешенные коннекты или только к публичным модулям?
2. Модуль приложения вообще не должен заботиться вопросами доступа. Он получил запрос, он обязан на него ответить. Если запрос требует аутентификации, то модуль так и должен отвечать. А ограничением доступа должен заниматься веб сервер или фаервол. Просто исходя из соображения, что у них это потребует меньше ресурсов, чем у приложения.