Контроллер и Router в MVC

Контроллер и Router



КОНТРОЛЛЕР (class NewsController)



Типичная задача веб-программирования - информационный раздел (статьи, новости, книги и др.)

Этот раздел состоит из двух частей:

1 - список записей,

2 - просмотр одной записи.


Расмотрим эти две задачи, для этого - добавим два экшена в наш контроллер, который будет обрабатывать эти страницы.


Допустим это контроллер - NewsController.php и экшены - actionIndex и actionView.


Файл NewsController.php


-- файл NewsController.php --

<?php
class NewsController
{
public function actionIndex()
{
echo 'Список новостей';
return true;
}
public function actionView()
{
echo 'Просмотр одной новости';
return true;
{
}
?>





Для того, чтобы эти экшены заработали, нам нужно сделать определенные записи в роуты (routes.php)


первая запись (просмотр одной новости):


'news/77' => 'news/view'

'news/15' => 'news/view'


вторая запись (список новостей):


'news' => 'news/index', // actionIndex в NewsController


Так, как наш класс Router по очереди идет по этому массиву, и применяет первое правило в котором совпадает запрос, правила 'news/77' => 'news/view' и 'news/15' => 'news/view'

- должны расположить выше, для того, чтобы они могли заработать.


Новостей на сайте может быть сотни и тысячи, поэтому писать правила для каждой новости нецелесообразно.

Для решения этой задачи нужны регулярные выражения.

Вместо индификатора новости напишем регулярное выражение. Правило будет выглядеть примерно вот так:


'news/([0-9]+)' => 'news/view'


В нашем actionView мы собираемся получить одну новость из БД и отобразить ее на странице.

Для этого нам нужен ее индификатор, нам нужно его получить, и для этого нам нужно модифицировать наш роутер (Router.php).


Передача параметров при использовании ЧПУ ( Человеко-понятный URL).


Не ЧПУ:


Ранее мы передавали параметры в строке запроса, при этом на сервере они попадали в суперглобальный массив $_GET:


http://test2/news/?id-1235&category=sport -> $_GET


и мы могли их получить по ключам:


$_GET['id']

$_GET['category']


ЧПУ:


Особенностью использования ЧПУ состоит в том, что мы будем использовать адреса такого вида:


http://test2/news/sport/1235


Они красивее и понятней для пользователя, но менее удобней для программистов.

При такой записи строки запроса данные не будут попадать в массив $_GET.

Будем использовать функции работы со строками, чтобы получить данные из строки запроса.


Расмотрим этот процесс на примере адреса : http://test2/news/sport/1235

Для него запишем определенный маршрут в наших роутах:


'news/([a-z]+)/([0-9]+)' => 'news/view/$1/$2',


В этом адресе есть два параметра: ([a-z]+) и ([0-9]+), которые соответствуют категории и индификатору .

Мы получим эти параметры и передадим их методу класса (экшену).

Все это мы сделаем в классе Router, так как именно там происходит обработка запроса.

Для этого нужно выполнить следующие три шага:


Шаг первый:


Нам нужно из нашего запроса, к примеру :

// news/sport/114 ,

вырезать два параметра

sport и 114

и подставить на места ссылок

$1 и $2,

это нужно для того, чтобы передать их на нужный экшен.

Это сделаем при помощи кода в классе Router:


-- файл Router.php --

// Получаем внутренний путь из внешнего согласно правилу
$internalRoute = preg_replace("~$uriPattern~", $path, $uri);
echo '<br><br>Нужно сформировать: ' . $internalRoute;




- При помощи функции preg_replace:

в строке запроса $uri (news/sport/114) мы ищем параметры -

- sport и 114 по определенному шаблону $uriPattern,

который у нас содержится в routes.php:

- ([a-z]+)/([0-9]+);


Если мы находим эти параметры, то в строку $path , которая описывается таким образом:

- $1/$2,

мы их подставляем, ($internalRoute вместо $path)


в итоге мы получим, так называемый, внутренний маршрут:

news/view/sport/114


Используя этот внутренний путь, мы можем узнать:

Контроллер: news

Экшен: view

И получить из строки параметры: sport и 114,

которые потом мы будем передавать в экшен


Шаг второй:

Определяем контроллер, action, параметры


Контроллер и экшен мы определяем из старого кода:


-- файл Router.php --

// Если есть совпадение, определить какой контроллер и action обрабатывают запрос
$segments = explode('/', $internalRoute);// $internalRoute вместо $path

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

// получаем название экшена
$actionName = 'action' . ucfirst(array_shift($segments));




Далее, в массиве ($segments) останутся параметры.

Мы специально использовали функцию array_shift(), которая получает из массива название контроллера и экшена, и удаляет его.

В итоге, в массиве ($segments) останутся только параметры:


-- файл Router.php --

echo '<br>controller name: ' . $controllerName ;
echo '<br>action name: ' . $actionName ;
$parameters = $segments;// массив с параметрами
echo '<pre>';
print_r($parameters);
die ;




Шаг третий:

Вызвать экшен с параметрами.


Для этого есть все необходимые данные:


название контроллера: $controllerName

название экшена: $actionName

и массив с параметрами: $parameters

Этот массив ($parameters) мы можем просто передать нашему экшену, используя ранее написаный код:


-- файл Router.php --

// передаем массив с параметрами($parameters) - нашему экшену
$result = $controllerObject -> $actionName($parameters);




При этом, для того, чтобы получить параметры в нашем экшене (actionView() из класса NewsController)-

- мы также будем использовать массив $params , обращаясь к его элементам:


-- файл NewsController.php --

public function actionView($params)// параметром - массив $params
{
echo '<br><br>' . $params[0];
echo '<br>' . $params[1];
echo '<br>' . $params[2];
// - если в строке запроса добавить еще некоторые некоторые элементы,
// они бы тоже появились в нашем массиве и мы могли бы их распечатать
return true;
}




- соответственно номер элемента в массиве, будет отвечать его позиции в запросе.

Если здесь мы добавили еще некоторые элементы в запросе, они бы тоже появились в нашем массиве и мы могли бы их распечатать


Это один способ.

Здесь совершенно не нравятся названия параметров(какой-то массив($params), с каким-то индексом [0]).

Если вдруг забыть порядок следования параметров в запросе, то не будем знать к какому элементу обратиться.

Поэтому в роутере (Router.php) можно использовать функцию: call_user_func_array():

- $result = call_user_func_array(array($controllerObject, $actionName), $parameters );


Эта функция вызывает экшен с именем, которое содержится в переменной $actionName,

у объекта - $controllerObject, при этом передает ему массив с параметрами ($parameters)

В принципе, практически, одно и то-же.

Разница состоит лишь в том, как эти параметры будут переданы в наш экшен.

В этом случае они будут переданы как переменные и доступ к ним мы можем получать через переменные:


-- файл NewsController.php --

// при использовании функции: call_user_func_array() в классе Router
public function actionView($category, $id)
{
echo '<br>' . $category;
echo '<br>' . $id;
return true;
}




Проверим этот способ - тоже работает, но теперь мы можем использовать красивые, нормальные имена для переменных.



Файл routes.php


-- файл routes.php --

<?php
// маршруты
return array
(
'news/([a-z]+)/([0-9]+)' => 'news/view/$1/$2',
'news/([0-9]+)' => 'news/view', // actionView в NewsController
'news' => 'news/index', // actionIndex в NewsController
'products' => 'product/list', // actionList в ProductController
);
// - где 'news' - строка запроса
// 'news/index' - имя контроллера и экшена для обработки этого запроса (путь обработчика)
?>





Файл NewsController.php


-- файл NewsController.php --

<?php
class NewsController
{
public function actionIndex()
{
echo 'Список новостей';
return true;
}

/*
public function actionView($params)
{
// echo '<br><br>Просмотр одной новости';
echo '<br><br>' . $params[0];
echo '<br>' . $params[1];
echo '<br>' . $params[2];
// - если в строке запроса добавить еще некоторые некоторые элементы
// они бы тоже появились в нашем массиве и мы могли бы их распечатать
return true;
}
*/

public function actionView($category, $id)
{
echo '<br>' . $category;;
echo '<br>' . $id ;
return true;
}
}
?>





Router


Файл 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
// выведет массив: Array ( [news] => news/index [products] => products/list )
//print_r($this->routes);

// проверка (после запуска из файла 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))
{
/*
// 11111111111111111111

// проверка: если есть совпадение, то выведет +
//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 ;
}
// 11111111111111111111
*/

// 22222222222222222222

// echo '<br>Где ищем (запрос, который набрал пользователь): ' . '$uri';
// echo '<br>Что ищем (совпадение из правила):' . '$uriPattern';
// echo '<br>Кто обрабатывает :' . '$path';

// Получаем внутренний путь из внешнего согласно правилу
$internalRoute = preg_replace("~$uriPattern,~", $path, $uri);
// echo '<br><br>Нужно сформировать: ' . $internalRoute;

// Определяем контроллер, action, параметры
// Контроллер и action мы определяем с помощью старого кода
// ($internalRoute вместо $path)

// Если есть совпадение, определить какой контроллер и
// action обрабатывают запрос
$segments = explode('/', $internalRoute);

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

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

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

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

// В итоге, в массиве ($segments) останутся только параметры
// массив с параметрами:
$parameters = $segments;
// echo '<pre>';
// print_r($parameters);

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

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

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

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

// для этого объекта мы вызываем метод (action)
// массив ($parameters) мы можем просто передать нашему экшену,
// используя ранее написаный код:
//$result = $controllerObject -> $actionName($parameters);

// используем функцию: call_user_func_array()
// Эта функция вызывает экшен с именем,
// которое содержится в переменной $actionName,
// у объекта - $controllerObject,
// при этом передает ему массив с параметрами ($parameters)
$result = call_user_func_array(array($controllerObject, $actionName), $parameters );
var_dump (array($controllerObject, $actionName));

die;
// 22222222222222222222
}
}
}
}
?>








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