Front Controller и Router в MVC

Front Controller и Router



КАРКАС СИСТЕМЫ (заготовка сайта):



Файловая структура сайта test2:


MVC-1-3



Главная идея использования подхода MVC - является разделение обязаностей между частями программы:

модель - работает с данными,

представление - отвечает за визуализацию,

контроллер - обрабатывает действия и связывает все вместе.


Рассмотрим детальнее цикл работы программы:


MVC-2-1



клиент заходит на сайт по определенному адресу (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 Router
{
// массив маршрутов
private $routes;

public function __construct()
{
}

// метод будет принимать управление от фронтконтроллера
public function run()
{
}
}
?>





В фронтконтроллере (index.php):


файл index.php

// - создаем экземпляр класса Router
$router = new Router();

// и запускаем метод run(), тем самым, передав на него управление
$router->run();





Файл index.php


-- файл index.php --

<?php
// Включение отображения ошибок на время разработки сайта
ini_set('display_errors', 1);
error_reporting(E_ALL);

// Подключаем файл Router.php используя следующие команды:
define('ROOT', dirname(__FILE__));
require_once (ROOT . '/components/Router.php');

//проверка: echo ROOT; (C:\OSPanel\domains\test2)

// создаем экземпляр класса Router
$router = new Router();
// и запускаем метод run(), тем самым, передав на него управление
$router->run();
?>





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 NewsController
{
public function actionIndex()
{
return true;
}
}
?>





Файл ProductController.php


-- файл ProductController.php --

<?php
class ProductController
{
public function actionList()
{
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 NewsController
{
public function actionIndex()
{
// Добавим вывод в наш метод и проверим на работоспособность
echo 'NewsController actionIndex';
return true;
}
}
?>





Файл ProductController.php


-- файл ProductController.php --

<?php
class ProductController
{
public function actionList()
{
// Добавим вывод в наш метод и проверим на работоспособность
echo 'ProductController actionList';
return true;
}

}
?>





Итоговый файл Router.php


-- файл Router.php --

<?php
class Router
{
// массив маршрутов
private $routes;

public function __construct()
{
// указываем путь к роутам (ROOT - путь к базовой дериктории,
// '/config/routes.php' - путь к созданному файлу с роутами
$routesPath = ROOT . '/config/routes.php';

// здесь мы присваиваем свойству $this->routes массив,
// который хранится в файле routes.php, при помощи - include
$this->routes = include($routesPath);
}

// 1. Получаем строку запроса

// метод возвращает строку запроса
private function getURI()
{
if(!empty($_SERVER['REQUEST_URI']))
{
return trim($_SERVER['REQUEST_URI'], '/');
}
}

// метод будет принимать управление от фронтконтроллера
// получаем значение URI
public function run()
{
// проверяем наше свойство $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->getURI();
//echo $uri;

// 2. Ищем строку запроса($uri) в наших маршрутах в цикле foreach:

foreach($this->routes as $uriPattern => $path)
{
//проверка: echo "<br>$uriPattern -> $path";

// Сравниваем строку запроса ($uri) и данные,
// которые содержатся в роутах ($uriPattern)

// для этого передаем в preg_match() строку запроса $uri и
// данные из наших роутов ('news' или 'products')
if(preg_match("~$uriPattern~", $uri))
{
// проверка: если есть совпадение, то выведет +
//echo '+';
// проверка: если есть совпадение, то выведет путь из routes.php
//echo $path;

// 3. определяем какой контроллер и action обрабатывают запрос

$segments = explode('/', $path);
// echo '<pre>';
// print_r($segments);
// echo '</pre>';

// 3 - 1. Получаем имя контроллера:

// Получаем имя контроллера:
$controllerName = array_shift($segments) . 'Controller';

// делает первую букву строки заглавной
$controllerName = ucfirst($controllerName);
//echo $controllerName;

// 3 - 2. Точно также получаем название экшена:

// Точно также получаем название экшена:
$actionName = 'action' . ucfirst(array_shift($segments));
//echo '<br>' . $actionName;

// в итоге получаем:
// echo '<br>Класс: ' . $controllerName; // имя класса
// echo '<br>Метод: ' . $actionName; // имя метода

// 4. Подключаем файл класса-контроллера:

// Определяем путь к файлу, который нужно подключить:
$controllerFile = ROOT . '/controllers/' . $controllerName . '.php';

// проверяем: если такой файл существует, то подключаем его
if(file_exists($controllerFile))
{
include_once ($controllerFile);
}

// 5. Создаем объект и вызваем метод (т.е. action)

// создаем объект класса контроллера
$controllerObject = new $controllerName;

// для этого объекта мы вызываем метод
$result = $controllerObject -> $actionName();

// если метод сработал, то мы можем об этом узнать,
// следовательно - оборвать цикл foreach()
if($result != null)
{
break ;
}
}
}
}
}
?>





Файловая структура (FRONT CONTROLLER и ROUTER ) сайта test2:


MVC-2-2






Наверх Наверх