фронт контроллер и роутинг в php
Как работает PHP-роутинг
Уж коли я затронул тему роутинга, то есть смысл немного окунуться в технические детали, поскольку большинство php-библиотек для роутинга представляются загадочными и сложными не только для новичков, но и опытных специалистов. Проблема здесь в том, что каждый разработчик пытается реализовать свои идеи, которые, как он думает, должны подходить для всех и каждого.
Данный подход делает php-разработки сложными и запутанными. PHP — такой язык программирования, который позволяет решать задачи просто и понятно. Во многих случаях не нужна лишняя обвеска и дополнительный уровень абстракции, которая только запутывает код. Работа с HTTP по какой-то мистической причине, часто обвешивается тонной абстракций, хотя всё крутится вокруг простых вещей.
Каркас приложения
Современное php-приложение строится достаточно просто. Все http-запросы отправляются в одну точку входа — это фронт-контролер. Обычный index.php в корне сайта. ЧПУ организуется в .htaccess. Приведу пример файла из одной своей разработки:
В прошлой статье я уже рассказывал как это работает: всё передаётся в индексный файл (index.php).
Следует отметить, что во многих php-фреймворках роутер подключается скрыто где-то в дебрях своих библиотек и модулей. Пользователю остаётся только возможность конфигурации через отдельные файлы. Полноценное управление роутером здесь невозможно. И уж тем более сменить его на какой-то свой вариант.
Запуск роутера
Поскольку app/bootstrap.php является точкой входа для приложения, то в нём и размещается роутер.
Роутер — это самый обычный php-класс, который может размещаться как угодно. Главное, чтобы он соответствовал PSR-4.
Как хранить опции и конфигурации я также рассказывал недавно в своём telegram-канале.
Этот массив мы добавляем как правила роутинга. При этом мы можем прочитать и множество других файлов с правилами, для того, чтобы разделить их по модулям своего приложения, а не кидать всё в одну кучу. Правила роутинга суммируются и накапливаются в его объекте.
После этого можно задать специальное правило для случаев, если никакие другие не сработали. Это будет 404-страница, а точнее его класс и метод.
В коде это выглядит так:
Правила роутинга
Конфигурационный файл возвращает массив, где каждое правило формируется тоже как массив. Просто покажу пример в котором это хорошо видно:
Три основных ключа любого правила:
Лично я предпочитаю для конфигурации использовать именно php-массив, но многие роутинги позволяют добавлять эти же самые правила через строчку. Она парсится и добавляется во внутреннее поле объекта уже как положено в массиве.
В общем не важно как именно добавляются правила в итоге они всё равно должны описывать http-метод, URL-паттерн и действие для выполнения.
Метод setNotFound() описывает только action в том же формате.
Логика работы роутера
В match() проверяется не только паттерн (это обычная регулярка), но и http-метод. Чтобы это сделать для текущего URL формируется массив его данных, который удобен для дальнейшего использования. Вот что-то такое:
Такой вариант позволяет не задавать 404-страницу для роутера.
Задача любого роутера — найти соответствие URL какому-то паттерну и запустить в случае успеха «что-то». Больше в его задачу ничего не входит, поскольку это уже лишнее.
HTTP-методы
В заключении пару слов от http-методах. На практике есть только два варианта: POST и GET. Именно их браузеры и поддерживают, хотя есть некие стандарты, которые предполагают существование других вариантов. Кроме того эти методы завязаны на HTML — в основном это обычные формы, либо Ajax-запросы. Во всех таких случаях, как ни крути, в реальности используется только POST.
На этом базируется концепция RESTful и CRUD, где используется один адрес, но разные http-методы. Это указывается в правилах роутера:
А в форме что-то вроде такого:
В зависимости от сложности проекта, создаётся и роутинг. С моей точки зрения лучшим вариантом будет именно свой «велосипед», поскольку он на 100% покроет реальные задачи. Сторонние библиотеки всегда стараются сделать универсальными, а это приводит к тому, что 80% их возможностей просто не используются.
FrontController vs Router — взаимозаменяемы?
В продолжение разработки своего велосипеда, пришел к выводу:
FrontController и Router — взаимозаменяемы? По сути, FrontController — устаревший вид организации MVC архитектуры, когда парсится URI и подключаются файлы в зависимости от имени первого элемента массива, к примеру /news/ подключит NewsController.
Учитывая PSR-4, FrontController больше не актуален, т.к. Router позволяет задать правила для роутинга вручную, что более безопасно, гибко, лучше контролируется и легко отключаются необходимые разделы сайта.
Изучая данный вопрос и проектируя свое первое приложение, я пришел к выводу что FrontController не нужен, т.к. его работу можно положить на Router. Я прав или на этапе проектирования упустил какой-то функционал, который должен выполнять FrontController?
Учитывая PSR-4, FrontController больше не актуален
Вы говорите о единой точке для входа. Но мне не ясно, зачем вообще создавать класс FrontController, если можно по сути прописать такой роутинг:
Router::get(‘/news/:params’, ‘News’), и класс Router сделает следующее:
1) Проверит uri на совпадение.
2) Проверит второй параметр: функция замыкания (ReflectionFunction::isClosure) или же class_exists() и потом проверит, реализует ли этот контроллер методы интерфейса \Interfaces\Controller. Если да, то создаст новый экземпляр класса и вызовет метод init() который все и активирует.
Эта схема плоха, если да, то чем? Зачем нам класс, который инициирует контроллер, который инициирует контроллер, ведь можно сразу запустить нужный нам контроллер?
Фронт-контроллер и роутинг в PHP
Если вы не работали ранее с регулярными выражениями – пройдите урок по регуляркам в PHP.
Apache RewriteEngine
RewriteEngine – это такой механизм в сервере Apache, который позволяет перенаправлять запросы. А теперь давайте рассмотрим каждую строку файла отдельно.
Опа! Видим текст «Главная страница». Значит мы попали на index.php. Давайте теперь попробуем в index.php вывести GET-параметр route. Уберём пока код, добавленный на предыдущих уроках и оставим только автозагрузку классов и вывод этого GET-параметра.
Снова откроем тот же адрес http://myproject.loc/abracadabra и увидим следующее:
Такие адреса через слэши называются ЧПУ – Человеко Понятные УРЛы. То есть адреса, которые нормально воспринимаются человеком.
Согласитесь
http://myproject.loc/hello/username
лучше чем
http://myproject.loc/?action=hello&name=username
На таких ЧПУ-адресах мы и будем разрабатывать нашу систему.
Роутинг
Ну а теперь мы научимся обрабатывать такие адреса красивым и простым способом – с помощью регулярных выражений.
. Мы выбрали её вместо слэша, чтобы не экранировать слэш в адресной строке. Напомню, что в качестве ограничителя может выступать вообще любой символ.
Перейдём по адресу http://myproject.loc/hello/username и увидим наши совпадения по регулярке:
Нулевой элемент – полное совпадение по паттерну. Первый элемент – значение, попавшее в маску (.*), то есть всё, что идёт после hello/.
Посмотрим, что получилось.
Перейдём теперь на страницу http://myproject.loc/ и увидим сообщение «Главная страница».
Остаётся только добавить обработку случая, когда ни одна из этих регулярок не подошла и просто вывести сообщение о том что страница не найдена.
Давайте просто добавим в конце index.php строку:
А теперь давайте посмотрим на получившийся код и подумаем, нет ли здесь чего-то общего? Да конечно есть! В обоих случаях мы проверяем регулярки и в зависимости от совпадения создаём нужный контроллер, с нужным методом и нужными аргументами. Но механизм-то одинаковый! Значит, будем делать универсальную систему роутинга.
Итак, нам нужно создать такую систему, которая будет на вход получать адрес, а на выходе возвращать имя контроллера, метод и параметры.
Давайте создадим отдельный файл с такой конфигурацией. Пусть это будет файл src/routes.php. Запишем в него следующее содержимое:
src/routes.php
То есть это просто массив, у которого ключи – это регулярка для адреса, а значение – это массив с двумя значениями – именем контроллера и названием метода.
Теперь вернёмся в index.php и научимся обрабатывать этот файл. Для начала давайте просто положим этот массив в отдельную переменную.
Что с этим делать? Да просто пробежаться по нему foreach-ом и найти соответствие по регулярке для текущего адреса. Как только совпадение найдено, нужно остановить перебор. Звучит несложно. Давайте сделаем это!
Всё правильно. Давайте теперь вернёмся на http://myproject.loc/
Видим, что у нас есть имя нужного контроллера и имя метода. Всё, этого достаточно. Вот так это делается в PHP:
Да! Прямо вот так! Переменную можно использовать в качестве имени класса при создании объекта, и даже вместо имени метода!
Зайдите на http://myproject.loc/ и убедитесь, что всё прекрасно работает.
Но у нас осталась еще проблема с аргументами для методов.
Давайте вернёмся к предыдущему варианту кода, где мы просто вывели значения переменных:
Но мы видим, что нужные нам аргументы всегда будут только после нулевого элемента, так как в нём лежит полное совпадение по паттерну. Не беда – просто удаляем этот ненужный элемент:
Получаем следующую картину:
Остаётся только один вопрос – как элементы массива передать в аргументы метода? Для этого в PHP есть специальный оператор троеточия:
Он передаст элементы массива в качестве аргументов методу в том порядке, в котором они находятся в массиве.
Создание простой MVC-системы на PHP 5
Предисловие
В этом руководстве Вы узнаете, как построить простую систему по архитектуре MVC (Model-View-Controller, Модель-Отображение-Контроллер) на PHP 5.1 с использованием возможностей библиотеки SPL (Standard PHP Library, Стандартная Библиотека PHP).
Введение
Добро пожаловать в первое полноценное руководство для PHP 5 на PHPit. Вам понадобится PHP 5.1 с установленной библиотекой SPL, так как мы воспользуемся некоторыми из самых последних возможностей PHP 5.
В этом руководстве я собираюсь показать, как построить простую MVC-систему (архитектура MVC является наиболее распространённым шаблоном проектирования для больших web-приложений). Я проведу Вас через все шаги от начала и до конца создания полноценной MVC-системы.
Одна точка входа
Одной из важных вещей в MVC является одна точка входа в приложение вместо кучи PHP-файлов, делающих примерно следующее:
// Здесь код страницы
У нас будет один файл, обрабатывающий все запросы. Это значит, что нам не придётся мучиться с подключением global.php каждый раз, когда нам нужно создать новую страницу. Эта «одна точка входа» будет называться index.php и на данный момент будет такой:
Как Вы можете заметить, этот скрипт пока ещё ничего не делает, но погодите минутку.
Сперва мы проверяем, существует ли запрашиваемый файл, используя директиву RewriteCond, и, если нет, то перенаправляем запрос на index.php. Такая проверка на существование файла необходима, так как иначе index.php будет пытаться обрабатывать все запросы к сайту, включая запросы на изображения. А это нам как раз и не надо.
Теперь, когда все запросы идут через одну точку входа, мы можем начать написание скрипта index.php. Первая вещь, которую мы должны сделать, это инициализация системы. Создадим директорию includes, а в ней файл startup.php (он будет у нас файлом инициализации). Вставим следующий код в index.php:
// Узнаём путь до файлов сайта
В этом примере мы объявляем некоторую константу, узнаём, где лежат файлы системы, а также проверяем, что версия PHP, ну, хотя бы, 5.1.
Добавьте следующий код в файл startup.php после того кода, что приведён в предыдущем примере:
Если сейчас попробовать запустить систему, то можно увидеть следующую ошибку:
Fatal error: Class ‘Registry’ not found in g:\Projects\PHPit\content\simple mvc php5\demo\includes\startup.php on line 12
Это, конечно, не большой сюрприз для нас, ведь мы ещё не написали сам класс Registry. Файл с классом можно было бы просто подключить, используя функцию include() (Прим. пер.: кстати говоря, include() не такая уж и функция, а всё-таки выражение языка, управляющая структура, если смотреть по ману), но давайте воспользуемся одной из новых возможностей PHP 5: __autoload().
Волшебная функция __autoload() используется для динамической загрузки классов. Когда PHP обнаруживает несуществующий класс, он сначала вызывает функцию __autoload() и только затем выдаёт ошибку. Мы можем воспользоваться такой возможностью для загрузки классов «на лету».
Вставьте этот код перед кодом из предыдущего примера:
Наша функция __autoload() берёт имя класса, переданное ей как аргумент, и проверяет, существует ли файл с похожим именем в директории с классами. Если файла нет, то функция просто вернёт false и выскочит фатальная ошибка. Но если файл существует, он будет загружен. Т.е. объявится необходимый класс, и никакой ошибки не будет.
Мы ещё не создали сам класс Registry, поэтому ошибка всё ещё будет появляться. Давайте же займёмся этим.
Создание класса Registry
Класс Registry используется для передачи глобальных значений между отдельными объектами. Это на самом деле довольно простой класс, в котором нужно реализовать несколько маленьких методов.
Для начала создадим директорию classes и в ней файл registry.php. Вставим следующий код в registry.php:
Теперь у нас есть «скелет» класса Registry и нужно нагрузить его методами. Напишем 2 метода: set(), чтобы устанавливать значения и get(), чтобы значения получать. Также можно написать метод remove() для удаления значений. Добавим эти методы в класс Registry:
Теперь у нас есть полноценный класс Registry, но мы не будем останавливаться на этом. Воспользуемся одной из возможностей библиотеки SPL: ArrayAccess. SPL (сокращённо от Standard PHP Library, Стандартная Библиотека PHP) — это коллекция интерфейсов и классов, предназначенных для решения стандартных проблем. Один из интерфейсов SPL, ArrayAccess, может быть использован, чтобы предоставить доступ к объекту, как к обычному массиву. Посмотрим на такой пример:
// Устанавливаем некоторое значение
// Получаем значение, используя get()
// Получаем значение, используя доступ как к массиву
Ключевое слово «Implements» говорит интерпретатору, что этим классом мы реализуем интерфейс, чем на самом деле ArrayAccess и является.
Класс, реализующий интерфейс ArrayAccess, должен иметь следующие методы:
Эти методы должны быть понятны сами по себе. Дополнительную информацию можно найти в документации SPL.
Теперь, реализовав интерфейс ArrayAccess, мы можем обращаться к объекту, как к обычному массиву. Это наглядно продемонстрировано, как в предыдущем примере, так и в этом:
// Устанавливаем некоторое значение
$registry [ ‘name’ ] = ‘Dennis Pallett’ ;
// Получаем значение, используя get()
// Получаем значение, используя доступ как к массиву
Класс Registry теперь завершён, и, если попробовать запустить систему, всё должно заработать (хотя ещё ничего не будет выводиться). Мы закончили с файлом инициализации и можно приступать к следующему шагу написания нашей MVC-системы: реализация доступа к базе данных, что в архитектуре MVC называется «Model» («Модель»).
Модель
«M» или модель – часть MVC-системы, которая отвечает за запросы к базе данных (или другому внешнему источнику) и предоставление информации контроллеру. Можно было бы загружать необходимую модель в зависимости от запроса, но я предпочитаю немного стереть границы между моделью и контроллером именно в этом месте, т.е. контроллер работает с БД непосредственно через библиотеку взаимодействия с БД, нежели чем через отдельную модель. Может быть, Вам захочется сделать это по-другому, тут дело вкуса.
Нам нужно написать код, необходимый для установки соединения с БД и поместить его в index.php. Существует множество замечательных библиотек для работы с БД (включая мою собственную, AutoCRUD), но в PHP 5 уже есть такая библиотека – PDO. Поэтому нет нужды использовать какую-либо другую.
Вставим следующий код в файл index.php (после подключения файла инициализации):
Модельная компонента нашей системы готова, поэтому давайте перейдём к написанию контроллера.
Класс Router
Класс Router будет разбирать запрос, а потом загружать требуемый контроллер. Создадим «скелет» класса:
Затем добавим следующие строки в index.php:
Мы добавили класс Router в нашу MVC-систему, но он пока что ничего не делает, поэтому давай добавим в него методы, необходимые для работы.
Первая вещь, которую мы напишем, это метод setPath() для установки директории, где будут лежать все наши контроллеры. Метод выглядит следующим образом и должен быть добавлен в класс Router:
Потом добавим следующие строки в index.php:
Теперь, когда мы установили путь до наших контроллеров, напишем сам метод, ответственный за загрузку контроллера. Этот метод будет называться delegate(), он и будет анализировать запрос. Первый кусочек этого метода такой:
Как Вы можете видеть, он использует ещё один метод, getController(), чтобы получить название контроллера и несколько других переменных. Этот метод выглядит так:
// Получаем раздельные части
// Находим правильный контроллер
// Есть ли папка с таким путём?
После цикла мы проверяем переменную с именем контроллера. Если она пустая, то используем контроллер «index», который будет у нас контроллером по умолчанию. Потом метод определяет действие, которое необходимо выполнить. Контроллер – это класс, который состоит из нескольких методов. Действие же указывает на конкретный метод. Если действие не указано, будем использовать «index» — действие по умолчанию.
И, наконец, получаем полный путь до файла контроллера, объединяя три переменные: путь, имя контроллера и расширение «php».
Теперь, когда мы проанализировали запрос, пора вызывать метод delegate() для загрузки контроллера и выполнения действия. Полностью метод delegate() выглядит так:
die ( ‘404 Not Found’ );
// Создаём экземпляр контроллера
die ( ‘404 Not Found’ );
Проанализировав запрос при помощи метода getController(), мы проверяем, существует ли в действительности файл, и, если нет, то возвращаем простое сообщение об ошибке.
После этого мы подключаем файл с контроллером и создаём экземпляр его класса, называться который должен «Controller_[имя]». Чуть позже мы поговорим о контроллерах более подробно.
Потом мы проверяем, есть ли указанное действие (т.е. метод) и возможно ли к нему обратиться (используем для этого функцию is_callable()). Наконец, мы выполняем непосредственно само действие, на чём роль класса Router и завершается.
Написав полностью метод delegate(), добавим следующую строчку в файл index.php:
Если попробовать сейчас запустить систему, то мы увидим следующую ошибку (разумеется, если директории controllers ещё нет):
Fatal error: Uncaught exception ‘Exception’ with message ‘Invalid controller path: `g:\Projects\PHPit\content\simple mvc php5\demo\controllers\`’ in g:\Projects\PHPit\content\simple mvc php5\demo\classes\router.php:18 Stack trace: #0 g:\Projects\PHPit\content\simple mvc php5\demo\index.php(13): Router->setPath(‘g:\Projects\PHP. ‘) #1
thrown in g:\Projects\PHPit\content\simple mvc php5\demo\classes\router.php on line 18
Или же мы увидим ошибку «404 Not Found», так как ещё нет ни одного контроллера. Но этим-то мы сейчас и займёмся.
Контроллер
Контроллеры в нашей MVC-системе будут достаточно простыми и потребуют совсем немного времени. Во-первых, удостоверимся, что директория controllers существует. Создадим файл controller_base.php в директории classes и вставим в него следующий код:
abstract function index ();
Этот абстрактный класс будет родительским классом для всех наших контроллеров. Он будет делать всего лишь две вещи: сохранять локальную копию класса Registry и при помощи абстрактного метода index() заставлять все дочерние контроллеры реализовывать этот метод.
Напишем наш первый контроллер. Создадим файл index.php в директории controllers и вставим в него такой код:
Class Controller_Index Extends Controller_Base <
echo ‘Hello from my MVC system’ ;
Только что мы создали наш первый контроллер и, если попробовать запустить систему, то можно увидеть следующее:
Это означает, что класс Router выполнил свою работу и запустил требуемое действие из требуемого контроллера. Давайте напишем ещё один контроллер, который будет соответствовать запросу «members/view». Создадим файл members.php в директории контроллеров и вставим в него такой код:
Class Controller_Members Extends Controller_Base <
echo ‘Default index of the `members` controllers’ ;
echo ‘You are viewing the members/view request’ ;
Теперь зайдём в нашу MVC-систему по запросу «members/view» или же «index.php?route=members/view». Мы должны увидеть такой результат:
Только лишь написанием нового контроллера и добавлением в него метода, мы смогли создать новую страницу, и ничего не пришлось менять в самой системе. Кроме того, нашим контроллерам не нужно подключать файл global.php или делать что-нибудь в таком роде.
Теперь, когда у нас есть контроллеры, осталась лишь одна вещь: «V» или «View» («Отображение»).
Отображение
Как и в случае с моделями, есть несколько различных вариантов создания компоненты View в MVC-системе. Мы могли бы научить класс Router автоматически загружать ещё один файл, названный как-нибудь так: «view_<имя>.php». Но чтобы сделать руководство более понятным, напишем класс Template, который будет заниматься выводом шаблонов.
Сначала создадим файл template.php в директории classes и вставим в него следующий код:
Теперь у нас есть основная структура нашего класс Template. Следующим шагом добавим такой код в файл index.php прямо перед строками, связанными с классом Router:
Так как нам понадобится использовать значения из моделей и контроллеров, то напишем метод set() для установки переменных, доступных в шаблонах. Посмотрим на пример:
Методы set() и remove() достаточно простые и используются, соответственно, для установки и удаления переменных.
Займёмся написанием метода show(), который будет отображать шаблоны. Простейший путь – это создать отдельную директорию templates, где хранить все файлы шаблонов, и использовать include() для вывода шаблона. Разумеется, Ваш собственный метод show() может быть совершенно другим и загружать шаблоны из базы данных или делать что-нибудь ещё. Посмотрим на кусо
Пример MVC в php. Вторая статья. Маршрутизация, контролеры, экшены, шаблоны и модели
Содержание цикла статей:
Теперь можно проверять работу перенаправления, введите любой адрес и посмотрите, что получиться: test-mvc.web/sdf/sdf/ или test-mvc.web/sdf/sdf/2342/не важно, на экране в любом случае, должно появиться «Test». Если Вы увидели эту надпись, значит, у нас все получилось.
Продолжим, давайте для удобства создадим в корне сайта файл config.php, в котором будем задавать различные константы, облегчающие своим существование настройку сайта. Это могут быть различные пути к скриптам, подступы к базе данных и так далее. Сейчас в конфиге давайте зададим следующее:
Класс хранилища Registry.php, будет находиться в папке /classes/
Код файла router.php, который находиться в папке /classes/
Теперь необходимо создать папки для хранения контроллеров, шаблонов и моделей – в корне создадим три папки controllers, views и models. И создадим несколько тестовых файлов /controllers/index.php, /views/index/index.php и /models/model_users.php, а теперь заполним файлы:
Для контроллера:
Как вы могли заметить, класс контролера наследуется от родительского класса Controller_Base. Это сделано, для того, чтобы упростить класс контролера. Поскольку нам еще необходимо подключать класс для работы с шаблонами, его подключение вынесено в Controller_Base.
Приведу его код, он расположен в папке /classes/ и называется controller_base.php :
Теперь осталось только разобраться с шаблонами. В абстрактном классе Controller_Base мы вызываем класс Template и передаем ему имя шаблона и имя контроллера.
Код класса Template, который лежит тут /classes/ и называется template.php
Если вы внимательно прочитали код, то наверняка поняли, что для отображения на страницах у нас используется шаблон first_layouts и вьюха(отображение) index.php – ее код я приводил чуть выше. Все что нам осталось, это создать файл шаблона first_layouts. Расположим его в папке /views/layouts/first_layouts.php
Шаблон будет содержать вот такой код:
Вот и все, на этом создание «каркаса» закончено. Сейчас у нас получилась самая простая структура, использующая в своей основе паттерн MVC. В этой статье я не стал затрагивать работу с базой данных, только вскользь упомянул ее, поскольку статья и так получилась большая. Непосредственно работу с базой данных я опишу в следующей статье.
На этом статья закончена, скачать исходники можно архивом тут.