PHP MVC
Front Controller и Router
КАРКАС СИСТЕМЫ (заготовка сайта):
Файловая структура сайта test2:
Главная идея использования подхода MVC - является разделение обязаностей между частями программы:
модель - работает с данными,
представление - отвечает за визуализацию,
контроллер - обрабатывает действия и связывает все вместе.
Рассмотрим детальнее цикл работы программы:
клиент заходит на сайт по определенному адресу (test2/news)
его запрос перенаправляется на фронтконтроллер (Front Controller - файл index.php)
(- обозначенную часть /news - мы будем называть строкой запроса)
Front Controller (Фронтконтроллер) выполняет задачи:
1. Общие настройки (включение-выключение ошибок, установка констант и др.)
2. Подключение файлов системы
3. Установка соединения с базой данных
4. Вызов Router
Далее вступает в работу компонент роутер (файл Router.php) - это класс, которым мы будем пользоваться в нашем фронтконтроллере.
- этот класс выполняет задачи:
1. Анализ запроса и определение контроллера, который будет обрабатывать этот запрос
(если строка запроса - /news, то обрабатывать этот запрос должен контроллер NewsController.php,
если стока запроса - /article, то обрабатывать этот запрос должен контроллер ArticleController.php и т. д.)
2. Затем роутер подключает файл с классом контроллера
3. После этого он передает ему управление (например - NewsController.php )
Контроллер (класс NewsController.php) с помощью своих методов будет выполнять разные действия:
actionList() - этот метод будет отвечать за просмотр списка новостей
actionView() - за просмотр новости полностью
actionArchiveList() - за просмотр архивных новостей и т. д.
Такие методы, которые отвечают за формирование страницы, будем называть - экшенами.
Экшены будут иметь специальные названия, для того, чтобы мы их отличали от других методов класса контроллера (action...)
Компонент Роутер (класс Router.php)
1. Роутер анализирует запрос и передает управление на контроллер - - для этого он вызывает нужный метод в классе контроллера
2. Роутер узнает какой метод нужно вызвать с помощью специальных правил - их называют роуты (routes) или - маршруты.
Они будут содержаться в отдельном файле (routes.php). Их составляет программист на этапе разработки.
В роутах ( файл routes.php), каждому значению строки запроса, соответствует путь к экшену:
/news/list -> news/list;
/news/archive -> news/archiveList.
- Путь к экшену состоит из двух частей:
название контроллера (NewsController), без слова контроллер(Controller) -> (news),
и название экшена (actionList) без слова экшен(action) -> (list).
Именно так роутер будет узнавать, какой экшен вызвать.
Например:
Введена строка запроса - /news/archive;
Роутер (Router.php) ее получает и обращается к роутам (routes.php),
находит ее здесь: /news/archive и узнает, какой метод нужно вызвать: news/archiveList
и вызывает его (в NewsController.php).
Компонент Экшен
Мы можем обратиться к модели (Model) и получить из нее данные,
с этими данными мы можем обратиться к представлению (View).
Оно, в свою очередь, организует визуализацию страницы в целом.
Далее пользователь получит эту страницу.
РЕАЛИЗАЦИЯ КАРКАСА
FRONT CONTROLLER (index.php - в корневой папке проекта)
У нас сервер настроен так, что все запросы попадают в этот файл.
Фронтконтроллер должен выполнить следующие задачи:
1. Общие настройки
2. Подключение файлов системы
3. Установка соединения с БД
4. Вызов Router
1. Общие настройки (настройки, которые касаются всего сайта):
Включение отображения ошибок на время разработки сайта:
файл index.php
ini_set('display_errors', 1);
error_reporting(E_ALL);
2. Подключение файлов системы
Создадим папку components и в ней создадим класс Router.php
В индексном файле подключаем файл Router.php используя следующие команды:
файл index.php
// создадим константу ROOT
define('ROOT', dirname(__FILE__));
// - где dirname(__FILE__) - полный путь к файлу на диске
// (функция dirname, псевдоконстанта __FILE__);
// функция dirname — возвращает имя родительского каталога из указанного пути
// подключаем файл Router.php
require_once (ROOT . '/components/Router.php');
3. Установка соединения с БД
Соединение с базой данных, на данном этапе, мы останавливать не будем.
4. Вызов Router
В классе Router.php создадим:
свойство private $routes; - массив, в котором будут храниться маршруты
метод public function __construct();
метод public function run(); - будет принимать управление от фронтконтроллера
Файл Router.php
-- файл Router.php --
<?php
class
{
// массив маршрутов
private $routes;
public function ()
{
}
// метод будет принимать управление от фронтконтроллера
public function ()
{
}
}
?>
В фронтконтроллере (index.php):
файл index.php
// - создаем экземпляр класса Router
$router = new Router();
// и запускаем метод run(), тем самым, передав на него управление
$router->run();
Файл index.php
-- файл index.php --
<?php
// Включение отображения ошибок на время разработки сайта
('display_errors', 1);
( );
// Подключаем файл Router.php используя следующие команды:
('ROOT', (__FILE__));
require_once ( . '/components/Router.php');
//проверка: echo ROOT; (C:\OSPanel\domains\test2)
// создаем экземпляр класса Router
$router = new ();
// и запускаем метод run(), тем самым, передав на него управление
$router-> ();
?>
ROUTER (Router.php)
1). Маршруты у нас будут храниться в массиве в отдельном файле.
Создадим в нашем проекте папку config и в ней файл routes.php.
В папке config мы можем хранить м другие файлы с настройками систем.
В файле routes.php мы пишем роуты(маршруты), они представляют из себя - пару в массиве:
'news' => 'news/index', // actionIndex в NewsController
'products' => 'product/list', // actionList в ProductController
и состоят из запроса ('news' - это то, что набирает в адресной строке пользователь)
и строки ('news/index'), по которой мы узнаем, где обрабатывается запрос.
В файле мы возвращаем массив.
Файл routes.php
-- файл routes.php --
<?php
// маршруты
return array
(
'news' => 'news/index', // actionIndex в NewsController
'products' => 'product/list', // actionList в ProductController
);
// - где 'news' - строка запроса
// 'news/index' - имя контроллера и экшена для обработки этого запроса (путь обработчика)
?>
Анализируем запрос и определяем обработчик:
При помощи наших роутов, мы увидим, что запросу 'news' соответствует строка 'news/index', которая означает, что будет вызван метод actionIndex в контроллере NewsController.php
Запросу 'products' соответствует строка 'product/list', которая означает, что будет вызван метод actionList в контроллере ProductController.php и т. д.
Здесь мы сами решаем, какой контроллер и какой метод этого контроллера будет обрабатывать запрос. Делаем мы это на этапе разработки нашего сайта.
2). Маршруты у нас есть. Теперь нужно заставить класс Router.php прочитать их и помнить на время выполнения кода.
Специально для хранения маршрутов создано свойство $routes и конструктор, в котором мы прочитаем и запомним роуты.
Пишем две строки в методе __construct класса Router.php:
файл Router.php
public function __construct()
{
// указываем путь к роутам (ROOT - путь к базовой дериктории
// '/config/routes.php' - путь к созданному файлу с роутами
$routesPath = ROOT . '/config/routes.php';
// здесь мы присваиваем свойству $this->routes массив,
// который хранится в файле routes.php, при помощи - include
$this->routes = include($routesPath);
}
- теперь в наше свойство попадет нужный нам массив.
Теперь мы можем проверить наше свойство $this->routes, обратившись к нему в методе run() класса Router.php:
print_r($this->routes);
3). Реализуем метод run() - это та часть, которая отвечает за анализ запроса и передачу управления.
В этом методе мы должны получить:
1. Получить строку запроса
2. Проверить наличие такого запроса в routes.php
3. Если есть совпадение, определить какой контроллер и экшен (action) обрабатывают запрос
4. Подключить файл класса-контроллера
5. Создать объект, вызвать метод (т.е. action)
1. Получаем строку запроса (метод run() класса Router.php):
-- файл Router.php --
public function run()
{
if(!empty($_SERVER['REQUEST_URI']))
{
$uri = trim($_SERVER['REQUEST_URI'], '/');
}
echo $uri;
}
- $_SERVER - суперглобальный массив
- 'REQUEST_URI' - ключ, по которому получаем строку запроса
- функция trim - удаляет пробелы или другие символы из начала и конца строки,
(вторым параметром - указываем те символы , которые хотим удалить - '/')
Что-бы сделать код понятнее и красивее вынесем этод код в отдельный метод getURI():
-- файл Router.php --
// 1. Получаем строку запроса
private function getURI()
{
if(!empty($_SERVER['REQUEST_URI']))
{
return trim($_SERVER['REQUEST_URI'], '/');
}
}
А в методе run() обратимся к этому методу:
-- файл Router.php --
public function run()
{
// обратимся к методу getURI()
$uri = $this->getURI();
echo $uri;
}
Метод getURI() объявлен приватным, и обращаться к нему можно только из нашего класса Router.php
2. Теперь поищем такую строку запроса($uri) в наших маршрутах, для этого напишем цикл foreach:
-- файл Router.php --
// 2. Ищем строку запроса($uri) в наших маршрутах в цикле foreach:
foreach($this->routes as $uriPattern => $path)
{
echo "<br>$uriPattern -> $path";
}
Получаем:
news -> news/index
products -> products/list
- здесь, для каждого маршрута ($this->routes), который находится в нашем массиве,
мы помещаем в переменную $uriPattern строку запроса из routes.php (например: 'news'),
- а в переменную $path мы помещаем путь обработчика (например: 'news/index').
Сравниваем строку запроса ($uri) и данные, которые содержатся в роутах ($uriPattern)
(внутри цикла foreach() !!!):
-- файл Router.php --
foreach($this->routes as $uriPattern => $path)
{
// Сравниваем строку запроса ($uri) и данные, которые содержатся в роутах ($uriPattern)
// для этого передаем в preg_match() строку запроса $uri и
// данные из наших роутов ('news' или 'products')
if(preg_match("~$uriPattern~", $uri))
{
// проверка: если есть совпадение, то выведет +
echo '+';
// проверка: если есть совпадение, то выведет путь из routes.php
echo $path;
}
}
- где функция preg_match() - ищет в заданном тексте $uri совпадения с шаблоном $uriPattern.
Если условие соблюдается, то в переменной $path будет находиться имя контроллера и экшена для обработки этого запроса (проверка: echo $path;)
3. Если есть совпадение, определить какой контроллер и action обрабатывают запрос (внутри if):
-- файл Router.php --
foreach($this->routes as $uriPattern => $path)
{
// Сравниваем строку запроса ($uri) и данные, которые содержатся в роутах ($uriPattern)
// для этого передаем в preg_match() строку запроса $uri и
// данные из наших роутов ('news' или 'products')
if(preg_match("~$uriPattern~", $uri))
{
// 3. Определяем какой контроллер и action обрабатывают запрос
$segments = explode('/', $path);
echo '<pre>';
print_r($segments);
echo '</pre>';
}
}
Получим массив:
Array
(
[0] => products - первый полученный элемент относится к контроллеру
[1] => list - второй элемент относится к экшену.
)
- где функция explode разделяет строку на две части.
Получаем имя контроллера:
-- файл Router.php --
// 3 - 1. Получаем имя контроллера:
// функция array_shift - получает значение первого элемента в массиве и
// удаляет его из массива. К этому значению мы добавляем слово Controller
$controllerName = array_shift($segments) . 'Controller';
// делает первую букву строки заглавной
$controllerName = ucfirst($controllerName);
// выводим имя контроллера
echo $controllerName;
// Проверка работы функции array_shift($segments):
echo '<pre>';
print_r($segments);
echo '</pre>';
- где функция array_shift - получает значение первого элемента в массиве и удаляет его из массива,
функция ucfirst - делает первую букву строки заглавной
Точно также получаем название экшена (добавляем к названию экшена слово 'action'):
-- файл Router.php --
// 3 - 2. Точно также получаем название экшена:
$actionName = 'action' . ucfirst(array_shift($segments));
// выводим название экшена
echo '<br>' . $actionName;
echo '<pre>';
print_r($segments);
echo '</pre>';
в итоге получаем:
-- файл Router.php --
echo '<br>Класс: ' . $controllerName; // имя класса
echo '<br>Метод: ' . $actionName; // имя метода
Далее опишем контроллеры (NewsController.php и ProductController.php), чтобы было, что вызывать.
В файлы NewsController.php и ProductController.php добавим по одному классу, они содержат по методу, которые пустые и просто возвращают true.
Файл NewsController.php
-- файл NewsController.php --
<?php
class
{
public function ()
{
return true;
}
}
?>
Файл ProductController.php
-- файл ProductController.php --
<?php
class
{
public function ()
{
return true;
}
}
?>
Продолжаем работу над методом run() нашего роутера (Router.php)
4. Подключаем файл класса-контроллера:
Определяем путь к файлу, который нужно подключить. Для этого мы прописываем путь к нему, используя имя класса.
-- файл Router.php --
// 4. Подключаем файл класса-контроллера:
// Определяем путь к файлу, который нужно подключить (путь к файлу класса)
$controllerFile = ROOT . '/controllers/' . $controllerName . '.php';
// затем проверяем: если такой файл существует, то подключаем его
if(file_exists($controllerFile))
{
include_once ($controllerFile);
}
5. Создаем объект и вызваем метод (т.е. action)
Создаем объект класса контроллера. Вместо имени класса, мы подставляем переменную $controllerName, которая содержит строку с именем этого класса.
Для этого объекта мы вызываем метод с переменной $actionName, которая содержит строку с названием нужного метода.
-- файл Router.php --
// 5. Создаем объект и вызваем метод (т.е. action)
// создаем объект класса контроллера
$controllerObject = new $controllerName;
// для этого объекта мы вызываем метод
$result = $controllerObject -> $actionName();
В наших методах мы специально вписали return true, для того, чтобы мы могли передать это значение определенной переменной.
В результате, если метод сработал, то мы можем об этом узнать, следовательно - оборвать цикл, который ищет совпадения в наших маршрутах (цикл foreach()).
-- файл Router.php --
// если метод сработал, то мы можем об этом узнать,
// следовательно - оборвать цикл foreach()
if($result != null)
{
break;
}
Добавим выводы в наши методы (в классах NewsController.php и ProductController.php)и проверим на работоспособность:
Файл NewsController.php
-- файл NewsController.php --
<?php
class
{
public function ()
{
// Добавим вывод в наш метод и проверим на работоспособность
echo 'NewsController actionIndex';
return true;
}
}
?>
Файл ProductController.php
-- файл ProductController.php --
<?php
class
{
public function ()
{
// Добавим вывод в наш метод и проверим на работоспособность
echo 'ProductController actionList';
return true;
}
}
?>
Итоговый файл Router.php
-- файл Router.php --
<?php
class
{
// массив маршрутов
private $routes;
public function ()
{
// указываем путь к роутам (ROOT - путь к базовой дериктории,
// '/config/routes.php' - путь к созданному файлу с роутами
$routesPath = . '/config/routes.php';
// здесь мы присваиваем свойству $this->routes массив,
// который хранится в файле routes.php, при помощи - include
$this->routes = include($routesPath);
}
// 1. Получаем строку запроса
// метод возвращает строку запроса
private function ()
{
if(!empty($_SERVER['REQUEST_URI']))
{
return ($_SERVER['REQUEST_URI'], '/');
}
}
// метод будет принимать управление от фронтконтроллера
// получаем значение URI
public function ()
{
// проверяем наше свойство $this->routes
//print_r($this->routes);
// - выведет массив:
// Array ( [news] => news/index [products] => products/list )
// проверка (после запуска из файла index.php получим:
// Class Router, method ran)
//echo 'Class Router, method ran';
// Получаем строку запроса:
//if(!empty($_SERVER['REQUEST_URI']))
//{
// $uri = trim($_SERVER['REQUEST_URI'], '/');
//}
// проверка (в строке запроса добавим: /news)
// echo $uri;
// обратимся к методу getURI() (этот метод возвращает строку запроса)
$uri = $this-> ();
//echo $uri;
// 2. Ищем строку запроса($uri) в наших маршрутах в цикле foreach:
foreach($this->routes as $uriPattern => $path)
{
//проверка: echo "<br>$uriPattern -> $path";
// Сравниваем строку запроса ($uri) и данные,
// которые содержатся в роутах ($uriPattern)
// для этого передаем в preg_match() строку запроса $uri и
// данные из наших роутов ('news' или 'products')
if( ("~$uriPattern~", $uri))
{
// проверка: если есть совпадение, то выведет +
//echo '+';
// проверка: если есть совпадение, то выведет путь из routes.php
//echo $path;
// 3. определяем какой контроллер и action обрабатывают запрос
$segments = ('/', $path);
// echo '<pre>';
// print_r($segments);
// echo '</pre>';
// 3 - 1. Получаем имя контроллера:
// Получаем имя контроллера:
$controllerName = ($segments) . 'Controller';
// делает первую букву строки заглавной
$controllerName = ($controllerName);
//echo $controllerName;
// 3 - 2. Точно также получаем название экшена:
// Точно также получаем название экшена:
$actionName = 'action' . ( ($segments));
//echo '<br>' . $actionName;
// в итоге получаем:
// echo '<br>Класс: ' . $controllerName; // имя класса
// echo '<br>Метод: ' . $actionName; // имя метода
// 4. Подключаем файл класса-контроллера:
// Определяем путь к файлу, который нужно подключить:
$controllerFile = . '/controllers/' . $controllerName . '.php';
// проверяем: если такой файл существует, то подключаем его
if( ($controllerFile))
{
include_once ($controllerFile);
}
// 5. Создаем объект и вызваем метод (т.е. action)
// создаем объект класса контроллера
$controllerObject = new $controllerName;
// для этого объекта мы вызываем метод
$result = $controllerObject -> $actionName();
// если метод сработал, то мы можем об этом узнать,
// следовательно - оборвать цикл foreach()
if($result != null)
{
break ;
}
}
}
}
}
?>
Файловая структура (FRONT CONTROLLER и ROUTER ) сайта test2:
Наверх Наверх