Для начинающих “велосипедистов” иль просто любопытствующих…
Данная статья не призыв к действию, а лишь небольшая зарисовка на тему “Как бы я это сделал”. На данный момент у меня в отделе активно используется Zend Framework, и именно с ним я лучше всего знаком, поэтому не пугайтесь параллелей, это не реклама, ведь большинство фреймворков в равной степени сочетают в себе плюсы и минусы, а нам нужны лишь преимущества…
Правила
Начал бы с регламентирования правил:
- Стандарты кодирования – лучше воспользоваться существующими, советую стандарты Zend Framework’а
- Процесс добавления кода в репозиторий (даже если вы сами в проекте – это будет хорошо дисциплинировать), только не перегибайте палку, иначе это замедлит развитие проекта
Не выработав данных правил, вы рискуете превратить фреймворк в помойку. Так же, настоятельно рекомендую писать юнит тесты – они помогут сэкономить уйму времени.
Архитектура
Надеюсь большинство читателей уже знакома с патерном MVC (Model-View-Controller) – так давайте на нем и базировать наш фреймворк, использования чего-то иного, боюсь, будет отпугивать пользователей (тут я подразумеваю программистов :) ).
Model
В типовом проекте модель завязывается на одну таблицу в базе данных, но исключений хватает, поэтому не следует принимать данное утверждение за аксиому. Наша модель должна с легкостью работать с различными хранилищами данных, будь то БД, файлы, или память.
Давайте представим как мы будем пользоваться такой моделью:
// модель User использует в качестве хранилища БД class Model_User extends Framework_Model_Database { $_table = "users"; $_pkey = "id"; function getByLogin($login) { /*...*/ } function getByEmail($email) { /*...*/ } } // модель MainConfig использует в качестве хранилища ini файл class Model_MainConfig extends Framework_Model_Ini { protected $_file = "application.ini"; function setOption($key) { /*...*/ } function getOption($key) { /*...*/ } } // модель Registry использует в качестве хранилища память - некая альтернатива глобальным переменным class Model_Registry extends Framework_Model_Memory { function setOption($key) { /*...*/ } function getOption($key) { /*...*/ } } // модель Session использует в качестве хранилища файлы сессии class Model_Session extends Framework_Model_Session { protected $_namespace = "global"; function setOption($key) { /*...*/ } function getOption($key) { /*...*/ } }
В действительности такими примерами я сильно коверкаю представление о MVC – ведь зачастую под моделью подразумевают некую бизнес модель, но никак не сессию или конфигурационный файл.
View
Каковы нынче требования к шаблонизатору? Лично для меня нативный PHP синтаксис, поддержка различного рода хелперов и фильтров. Так же должен быть реализован паттерн “двухэтапного представления” (Two Step View pattern), в ZF для этого служат два компонента – Zend_View и Zend_Layout.
Приведу пример такого представления:
<?php if ($this->books): ?> <!-- Таблица из нескольких книг. --> <table> <tr> <th>Author</th> <th>Title</th> </tr> <?php foreach ($this->books as $key => $val): ?> <tr> <td><?php echo $this->escape($val['author']) ?></td> <td><?php echo $this->escape($val['title']) ?></td> </tr> <?php endforeach; ?> </table> <?php else: ?> <p>Нет книг для отображения.</p> <?php endif; ?>
Пример использование layout’ов (взят из документации по Zend_Layout):
О да, в Zend Framework’е удачная реализация представления, она мне нравится, конечно, не без мелких нареканий, но в целом – это пять.
Controller
Контроллер должен выполнять свои обязанности – обрабатывать запрос, пинать модель и представление – дабы пользователь получил желаемый результат.
Давайте попробуем среагировать на запрос пользователя следующего вида:
http://example.com/?controller=users&action=profile&id=16
Так, проведем разбор – у нас просят показать профайл пользователя с id=16. Соответственно напрашивается существование контроллера users с методом profile, который бы смог получить в качестве параметра некий id:
// название контроллера должно содержать префикс - дабы чего не напутать class Controller_Users extends Framework_Controller_Action { public function actionProfile() { // получаем данные из запроса $id = $this->request->get('id'); // пинаем модель $user = new Model_User(); $user -> getById($id); // закидываем данные в представление $this->view->user = $user; } }
Естественно, на плечи контроллера так же ложится обязанность изменять формат представление, т.е. если нам надо вернуть данные в JSON формате, то никакого иного вывода быть не должно (это и так подразумевается самим MVC, но стоит лишний раз напомнить).
Кто повнимательней увидит в данном примере появление некого Request’a – это объект который занимается разбором входящего запроса. Зачем он нужен – об этом чуть далее.
Routers
Теперь немного о требованиях со стороны конечных пользователей – в частности о ЧПУ. Сравните следующие варианты ссылок:
http://example.com/?controller=users&action=profile&id=16 http://example.com/users/profile/id/16/ // стандартная схема построения URL'a в ZF http://example.com/users/profile/16/ // CodeIgniter http://example.com/profile/16/ // возможное пожелание заказчика, и его нужно выполнять
Для генерации/разбора подобного входящего запроса в ZF используются роутеры – по факту – это правила построения URL’ов, нам так же придется их реализовать – и с этим сложно поспорить.
Мне больше по душе передача именнованых параметров – такой URL легче читаем, сравните:
http://example.com/users/list/page/2/limit/20/filter/activeи
http://example.com/users/list/2/20/active
Вы наверное захотите сразу засунуть данный функционал непосредственно в класс Request, но не спешите, ведь нам еще потребуется генерировать правильные URL во View – а вызывать там объект Request – немного не логично, давайте таки оставим это на совести отдельного класса, к которому может обращаться как Request так и View
Request & Response
С назначением класса Request думаю проблем не возникает – в его функции входит не так много:
- обработка входящих параметров всеми правилами из Router’а
- отдавать параметры в контроллер по требованию
Но есть еще Response – о нем я как то не упоминал ранее, что же он должен делать:
- формировать заголовок ответа
- формировать ответ – т.е. брать view, оборачивать в некий layout и на выход
Мне не очень нравится реализация Response в ZF – слишком много лишнего в нем
Modules
Фреймворк должен быть модульным, т.е. написав какой-то модуль (блог, форум, и т.д.) вы сможете с легкостью использовать данный код в других приложениях. Для этого нам понадобится лишь отделить MVC каждого модуля в свою директорию, при этом какой-то модуль останется за главного.
Core
Теперь стоит перейти к самому вкусному – непосредственно к ядру системы, его функционал мы практически уже описали, стоит лишь подвести черту:
- При инициализации входящий запрос должен быть обработан всеми правилами Router’ов, дабы объект Request мог вернуть нам запрашиваемое значение по ключу
- Объект Request так же должен знать, какой модуль/контроллер/экшен запрашивается
- Ядро должно подгрузить необходимый контроллер и вызвать запрашиваемый экшен (метод контроллера)
- После отработки контроллера вызывается Response и ставит точку
Для гибкости в систему стоит добавить либо хуки, либо плагины на каждый из перечисленных этапов.
Вспомогательный классы
Если вы захотите потренироваться в написании “велосипедов”, то можете начать отсюда:
- Работа с БД – необходима поддержка MySQL, SQLite, PostgreSQL (это минимум), а в целом стоит уделить этому пункту много внимания, т.к. он один может привлечь множество пользователей
- Валидаторы – необходимая вещь, для экономии времени при написании форм (см. Zend_Validate)
- Транслятор – для реализации мультиязычности в системе, возможно хватит и gettext’a, но не стоит на это надеяться
- Почта – можно обойтись лишь функцией mail, но это как-то не по-взрослому
- Пагинатор – для решения тривиальной задачи – разбиение по страницам (см. Zend_Paginator)
- Навигатор – построение меню, карты сайта и “хлебных крошек” (см. Zend_Navigation)
- Кэширование – без него никуда (см. Zend_Cache)
- Конфигурационные файлы – Zend_Config слишком большой для того, чтобы обрабатывать лишь один ini файл, тут можете попрактиковаться, но все же посматривайте на Zend_Config_Ini
- Автозагрузчик – очень полезная вещь, и главное удобное – Zend_Loader
- ACL – возможно потребуется – по крайней мере, распределение прав по запросу модуль/контроллер/экшен лучше пусть будет зашит в системе
Я не случайно привожу ссылки на пакеты Zend Framewrok’а – они вполне адекватны и самостоятельны, могут быть использованы сами по себе, т.е. никто ведь не мtшает вам построить свой фреймворк из кубиков Zend’a (и вот тому пример: ZYM engine)
Тривиальные задачи
В фреймворке должен быть заложен функционал для решения следующих тривиальных задач (мелких и не очень):
- Redirect – самый обычный, вызывается из контроллера
- Forward – это пересылка с одного модуль/контроллер/экшен на другой без перезагрузки страницы
- Messages – различные сообщения, с возможностью получения их после перезагрузки страницы
- Scaffold – быстрый способ построения приложения для редактирования записей в базе данных (утрированно)
Еще лучше, если с фреймворком будет поставляться готовая к использованию CMS система – она позволит популяризировать ваше детище, и возможно привлечет сторонних разработчиков.
Возможно чего забыл из “тривиального” – пишите…
Структура каталога
И так, что у нас получается, если взглянуть на файловую систему (в document_root должна лежать лишь папка public):
project |-- application | |-- configs | |-- layouts | |-- controllers | |-- models | |-- views | `-- modules | `-- <module_name> | |-- layouts | |-- controllers | |-- models | `-- views |-- data | |-- cache | |-- logs | `-- sessions |-- library | `-- Framework |-- public | |-- styles | |-- scripts | |-- images | |-- uploads | |-- .htaccess | `-- index.php `-- tests
Вывод
To Be Or Not To Be – решать вам, как по мне – можно смириться с недостатками какого-то одного фреймворка, и наслаждаться его преимуществами. Возможно, вы попытаетесь написать свое решение или скрестить существующие, но не забываете – написание такого рода приложения влечет за собой ответственность по его поддержке.
P.S. Для всех моих читателей – RSS канала доступен по адресу http://anton.shevchuk.name/feed/ (если Вы используете какой иной – исправьте).
P.P.S. Еще я достаточно активно зависаю на твитере, так что следуйте за мной…
Раньше каждый уважающий себя программист должен был написать wrapper для объекта String.
Сейчас, получается, веяния моды изменились и каждый должен посадить дерево, вырастить сына и написать свой framework?
Я бы сказал иначе, каждый PHP разработчик должен:
В свободное от основной работы время?
Неплохо, основы так сказать :)
Я бы досказал ещё про то что иерархичность не обязательно должна быть в два уровня (controller/action). Я сначала вообще не понимал почему их так назвали. В принципе их может быть бесконечно, просто на свичах или дополнительных классах их сложней реализовывать. У меня в движке это уровень равен трём – я ещё вношу уровень “application”. Что-бы на одном аккаунте был ещё уровень работы скажем с айфоном.. или с админкой.. при этом имея независимую папку, но единое ядро.
А второе до чего доходят рано или поздно девелоперы цмски и ЧПУ – пихать путь запуска это иерархии для страницы в настройки страницы (БД), тогда решается проблема этого рутинга из кода. В базе помоему лучше хранить такие вещи.. как и переводы например.
А.. и третье. Фреймворк надо называть правильно. Т.е. $this->partial() не очень понятно. Методы должны быть глаголами, а свойства – существительными. Так что лингвистика тут очень важна
+ Забыли еще о CMS ;)
Всё-таки, наверное, смИриться.
Все ж тема сисек не раскрыта! ;)
А вообщем – Тоха, поболее писать о ЗФ!!!
И… Кто-то забросил фреймворк свой ;)
написание такого рода приложения влечет за собой ответственность по его поддержке.
Статья интересна, спасибо.
Но есть несколько вопросов – “почему тот или иной класс содержит описанный фунционал”
Мне видится немного другой функционал у следующих классов:
* Request – обработать, распарсить полученный запрос (ex: /cms/users.list.html), т.е. получить path (/cms/users/), action (list), view type (html)
* Router – на основе path+action определить какой контроллер необходимо подключить + какой его метод запустить: Users.list. В случае невозможности определить – контроллер и метод выставляются в Errors.notfound (404)
* Response – отвечает за хранение результатов работы контроллера (некий объект доступный всем остальным)
* View – применяет шаблон, учитывая view type (ex: Users.list.html)
Хотя здесь всплывает ещё класс Application который:
1) запустил обработку запроса (Request)
2) запустил определение контроллера и метода (Router)
2.1) проверка доступа и переопределение контроллера и метода на Errors.deny (403) – возможно данный пункт стоит перенести в Router
3) подключил и запустил контроллер.метод
4) отдал ответ (View)
Буду рад комментариям и в особенности авторским :)
Хм, тема вроде интересна, а дискуссия не пошла (
Как по мне Антон, так Вы просто перечислили то, что есть в Zend Framework, но, ИМХО, в ZF – не все так гладко и на нем не сходится мир. У других фреймворков, на сколько я знаю, есть более лучшие решения. С Вашей стороны было бы неплохо вспомнить о достоинствах и решениях в других фреймворках и вывести, так сказать, идеальный вариант фреймворка :)
Очень интересно! Жаль, конечно, что только ZF учавствовал в “обзоре”, но всё равно довольно познавательно. Бовольно объёмный материал хорошо переосмыслен и разложен по полочкм. К сожалению, у меня так не получается пока :(
Это не свой Framework, а чужой всё таки :)
А я таких людей уважаю. Хоть что-то на пользу другим делают, а не сидят себе тихо забитые и только деяния других обсуждают. Автор молодца:)
Написание своего фреймворка не такая глупая затея, как может показаться. Я пишу свой с намерением сделать его легким. Конечно, многие скажут, что это он изначально будет легкий, а потом превратиться в тот же ZF, но не соглашусь. Нужно уметь во-время остановиться. Лично я считаю, что ZF, как и многие ему подобные, берет на себя массу тех обязанностей, что должны ложиться на приложение более высокого уровня, например, CMS. И даже если “лишний” функционал стараться не использовать, все равно рано или поздно поддашься соблазну. В итоге поимеешь грузные решения для очень простых задач.
Я тоже писал свой движок, правда не все так просто оказалось… С ZF я не знаком был, и пока написал что-то то понял, что нужно все менять и переписывать…
поправь “большинство читателей уже знакома” на
“большинство читателей уже знакомЫ“
Уже несколько лет занимаюсь написанием собственного фреймворка. Складывается ощущение, что это бесконечное занятие. Потому что дойдя до чего-то нового или понимания, что старое можно сделать по другому и лучше, приходится переписывать всё чуть ли не с нуля. Но в результате, на каком-то этапе чувствуешь себя счастливчиком, что у тебя получилось. Пока не поймёшь, что всё твоё “творение” можно сделать гораздо более простыми вещами и начинаешь работу заново.