Антон Шевчук // Web-разработчик

Пишем свой PHP Framework // PHP

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. Еще я достаточно активно зависаю на твитере, так что следуйте за мной…

© Антон Шевчук 2007-2016