Пишем свой PHP Framework

PHP Frameworks
Для начинающих “велосипедистов” иль просто любопытствующих…

Данная статья не призыв к действию, а лишь небольшая зарисовка на тему “Как бы я это сделал”. На данный момент у меня в отделе активно используется 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):

Layout Example

О да, в 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

Теперь стоит перейти к самому вкусному – непосредственно к ядру системы, его функционал мы практически уже описали, стоит лишь подвести черту:

  1. При инициализации входящий запрос должен быть обработан всеми правилами Router’ов, дабы объект Request мог вернуть нам запрашиваемое значение по ключу
  2. Объект Request так же должен знать, какой модуль/контроллер/экшен запрашивается
  3. Ядро должно подгрузить необходимый контроллер и вызвать запрашиваемый экшен (метод контроллера)
  4. После отработки контроллера вызывается 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. Еще я достаточно активно зависаю на твитере, так что следуйте за мной…

18 thoughts on “Пишем свой PHP Framework”

  1. Раньше каждый уважающий себя программист должен был написать wrapper для объекта String.

    Сейчас, получается, веяния моды изменились и каждый должен посадить дерево, вырастить сына и написать свой framework?

  2. Неплохо, основы так сказать :)
    Я бы досказал ещё про то что иерархичность не обязательно должна быть в два уровня (controller/action). Я сначала вообще не понимал почему их так назвали. В принципе их может быть бесконечно, просто на свичах или дополнительных классах их сложней реализовывать. У меня в движке это уровень равен трём – я ещё вношу уровень “application”. Что-бы на одном аккаунте был ещё уровень работы скажем с айфоном.. или с админкой.. при этом имея независимую папку, но единое ядро.

    А второе до чего доходят рано или поздно девелоперы цмски и ЧПУ – пихать путь запуска это иерархии для страницы в настройки страницы (БД), тогда решается проблема этого рутинга из кода. В базе помоему лучше хранить такие вещи.. как и переводы например.

    А.. и третье. Фреймворк надо называть правильно. Т.е. $this->partial() не очень понятно. Методы должны быть глаголами, а свойства – существительными. Так что лингвистика тут очень важна

  3. To Be Or Not To Be – решать вам, как по мне – можно смериться с недостатками какого-то одного фреймворка

    Всё-таки, наверное, смИриться.

  4. Все ж тема сисек не раскрыта! ;)
    А вообщем – Тоха, поболее писать о ЗФ!!!
    И… Кто-то забросил фреймворк свой ;)
    написание такого рода приложения влечет за собой ответственность по его поддержке.

  5. Статья интересна, спасибо.
    Но есть несколько вопросов – “почему тот или иной класс содержит описанный фунционал”

    Мне видится немного другой функционал у следующих классов:
    * 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)

    Буду рад комментариям и в особенности авторским :)

  6. Хм, тема вроде интересна, а дискуссия не пошла (

  7. Как по мне Антон, так Вы просто перечислили то, что есть в Zend Framework, но, ИМХО, в ZF – не все так гладко и на нем не сходится мир. У других фреймворков, на сколько я знаю, есть более лучшие решения. С Вашей стороны было бы неплохо вспомнить о достоинствах и решениях в других фреймворках и вывести, так сказать, идеальный вариант фреймворка :)

  8. Очень интересно! Жаль, конечно, что только ZF учавствовал в “обзоре”, но всё равно довольно познавательно. Бовольно объёмный материал хорошо переосмыслен и разложен по полочкм. К сожалению, у меня так не получается пока :(

  9. А я таких людей уважаю. Хоть что-то на пользу другим делают, а не сидят себе тихо забитые и только деяния других обсуждают. Автор молодца:)

  10. Написание своего фреймворка не такая глупая затея, как может показаться. Я пишу свой с намерением сделать его легким. Конечно, многие скажут, что это он изначально будет легкий, а потом превратиться в тот же ZF, но не соглашусь. Нужно уметь во-время остановиться. Лично я считаю, что ZF, как и многие ему подобные, берет на себя массу тех обязанностей, что должны ложиться на приложение более высокого уровня, например, CMS. И даже если “лишний” функционал стараться не использовать, все равно рано или поздно поддашься соблазну. В итоге поимеешь грузные решения для очень простых задач.

  11. Я тоже писал свой движок, правда не все так просто оказалось… С ZF я не знаком был, и пока написал что-то то понял, что нужно все менять и переписывать…

  12. поправь “большинство читателей уже знакома” на
    “большинство читателей уже знакомЫ

  13. Уже несколько лет занимаюсь написанием собственного фреймворка. Складывается ощущение, что это бесконечное занятие. Потому что дойдя до чего-то нового или понимания, что старое можно сделать по другому и лучше, приходится переписывать всё чуть ли не с нуля. Но в результате, на каком-то этапе чувствуешь себя счастливчиком, что у тебя получилось. Пока не поймёшь, что всё твоё “творение” можно сделать гораздо более простыми вещами и начинаешь работу заново.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.