<rmcreative>

RSS

MVC: Front Controller, Controller и Router

12 февраля 2010

В прошлый раз я описал построение простейшего, но довольно функционального компонента View. В этот раз займёмся Front Controller, Controller и Router. Код, приведённый ниже может не запускаться, не является безопасным, но объясняет общие принципы работы большинства MVC-фреймворков.

Front Controller является диспетчером запросов и, в зависимости от URL запускает нужный контроллер с нужными параметрами. В этом ему помогает Router, занимающийся непосредственно разбором URL и применением различных правил.

Для начала необходимо перенаправить все запросы на Front Controller, т.е. index.php. Для Apache это можно сделать через файл .htaccess, расположенный в той же директории, что и index.php:

RewriteEngine on
# если папка или файл реально существуют, используем их
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# если нет — отдаём всё index.php
RewriteRule . index.php

Далее опишем index.php:

// подключаем необходимые файлы
define('ROOT', dirname(__FILE__));
require_once(ROOT.'/../lib/Router.php');
 
// подключаем конфигурацию URL
$routes=ROOT.'/../app/config/routes.php';
 
// запускаем роутер
$router = new Router($routes);
$router->run();

Конфигурация в routes.php будет выглядеть следующим образом:

return array(
    'about' => 'page/show/about',
    'page/([-_a-z0-9]+)' => 'page/show/$1',
    'users/([-_a-z0-9]+)' => 'users/show/$1',
);

Что должен сделать роутер?

  1. Получить URI.

  2. Применить первое совпавшее правило из конфигурации. Вызвать соответствующий правилу контроллер с нужными параметрами.

class Router {
    // Хранит конфигурацию маршрутов.
    private $routes;
 
    function __construct($routesPath){
        // Получаем конфигурацию из файла.
        $this->routes = include($routesPath);
    }
 
    // Метод получает URI. Несколько вариантов представлены для надёжности.
    function getURI(){
        if(!empty($_SERVER['REQUEST_URI'])) {
            return trim($_SERVER['REQUEST_URI'], '/');
        }
 
        if(!empty($_SERVER['PATH_INFO'])) {
            return trim($_SERVER['PATH_INFO'], '/');
        }
 
        if(!empty($_SERVER['QUERY_STRING'])) {
            return trim($_SERVER['QUERY_STRING'], '/');
        }
    }
 
    function run(){
        // Получаем URI.
        $uri = $this->getURI();
 
        // Пытаемся применить к нему правила из конфигуации.
        foreach($this->routes as $pattern => $route){
            // Если правило совпало.
            if(preg_match("~$pattern~", $uri)){
                // Получаем внутренний путь из внешнего согласно правилу.
                $internalRoute = preg_replace("~$pattern~", $route, $uri);
                // Разбиваем внутренний путь на сегменты.
                $segments = explode('/', $internalRoute);
                // Первый сегмент — контроллер.
                $controller = ucfirst(array_shift($segments)).'Controller';
                // Второй — действие.
                $action = 'action'.ucfirst(array_shift($segments));
                // Остальные сегменты — параметры.
                $parameters = $segments;
 
                // Подключаем файл контроллера, если он имеется
                $controllerFile = ROOT.'../app/controllers/'.$controller.'.php';
                if(file_exists($controllerFile)){
                    include($controllerFile);
                }
 
                // Если не загружен нужный класс контроллера или в нём нет
                // нужного метода — 404 
                if(!is_callable(array($controller, $action))){
                    header("HTTP/1.0 404 Not Found");
                    return;
                }
 
                // Вызываем действие контроллера с параметрами
                call_user_func_array(array($controller, $action), $params);
            }
        }
 
        // Ничего не применилось. 404.
        header("HTTP/1.0 404 Not Found");
        return;
    }
}

Базовый класс контроллера будет выглядеть примерно так:

class Controller {
    protected $view;
 
    function __construct(){
        // используем наш View, описанный ранее
        $this->view = new View();
    }
 
    // другие полезные методы вроде redirect($url);
}

Сам контроллер:

class PageController extends Controller {
    // параметр отдаётся из правила 'page/([-_a-z0-9]+)' => 'page/show/$1',
    function actionShow($url = null){
        // получаем страницу
        $page = $this->getPage($url);
 
        // выводим её при помощи View
        $this->view->render('page', array('page' => $page));
    }
}

Комментарии RSS

  1. №2243
    aktuba
    aktuba 12 февр. 2010 г., 19:07:56

    Еще бы объединить все куски в один простенький фреймворк - было бы отлично!

  2. №2244
    Dr.Death
    Dr.Death 12 февр. 2010 г., 19:43:12

    Поддерживаю!

  3. №2246
    Exel
    Exel 12 февр. 2010 г., 21:23:53

    aktuba, Dr.Death, Yii вам в помощь!

    Угадывается неизгладимое влияние от него(Yii) на автора :-)

    Sam, нет желания также, на пальцах, рассказать об устройстве ActiveFinder'а? В Yii он крайне интересен!

  4. №2248
    aktuba
    aktuba 13 февр. 2010 г., 13:52:44

    Yii меня только расстроил... Не понравился. Вернулся с него на CI, а теперь перехожу на кохану.

  5. №2249
    Сергей
    Сергей 13 февр. 2010 г., 16:10:08

    меня смущает вот что:

    "Front Controller является диспетчером запросов и, в зависимости от URL запускает нужный контроллер с нужными параметрами" - но запускает и создает контроллер роутер.

    мне кажется не его (Роутера) задача создавать объект контроллера - он должен распарсить правила и отдать параметры.

    а уже фронтконтроллер создает необходимый контроллер.

    разве не так?

  6. №2251
    Sam
    Sam 13 февр. 2010 г., 21:18:29

    Сергей

    Обычно кроме роутера есть ещё несколько звеньев.

    aktuba, Dr.Death

    Фреймворк уже давно написан, но поддерживать его оказалось не таким простым делом. Намного приятнее пользоваться CodeIgniter или Yii и помогать их авторам.

    Exel

    Об устройстве подобия ActiveFinder'а как-нибудь расскажу. Но это опять же будет не Yii, а нечто попроще.

  7. №2253
    Vereschagin
    Vereschagin 15 февр. 2010 г., 7:57:17

    В принципе смысла статьи это не меняет, но есть ошибочка:

    if(!empty($_SERVER['QUERY_STRING'])) {
     
         return trim($_SERVER['PATH_INFO'], '/');
     
     }
  8. №2256
    plandem
    plandem 15 февр. 2010 г., 16:54:37

    Фреймворк уже давно написан, но поддерживать его оказалось не таким простым делом. Намного приятнее пользоваться CodeIgniter или Yii и помогать их авторам.

    это основная причина по которой я забросил когда-то свой фреймворк и перешел на Yii :)

  9. №2296
    гость
    гость 16 февр. 2010 г., 21:17:13

    ошибок много

  10. №2298
    cooper
    cooper 17 февр. 2010 г., 12:58:00

    Я все же думал вы реализуете как в Yii, т.е. экшены будут получать не просто массив ($parameters = $segments) с параметрами, а роутер будет распарсивать урл и вытаскивать данные в массив $_GET.

  11. №2301
    texel
    texel 17 февр. 2010 г., 14:56:25
    '/page/([-_a-z0-9]+)/' => 'page/show/$1'

    так наверно будет, чтоб без ошибок

    и у меня есть подозрения что $route[0] это будет обращение к символу строки тк $route на тот момент будет содержать строку

  12. №2302
    texel
    texel 17 февр. 2010 г., 15:01:53

    да и хотелось бы уточнить... обычно бывает два входных файла index.php и admin.php.. всё будет автоматом уходить на index.php. Хотелось бы более подробно об этом)

  13. №2460
    denis909
    denis909 13 апр. 2010 г., 18:43:50

    Хорошо написано, поможет начинающим не просто работать с готовыми решениями, но и понимать их внутренние механизмы работы. С удовольствием бы почитал аналогичную статью о внутреннем устройстве множественного наследования в php на примере поведений в Yii и CakePHP.

  14. №3682
    Dima
    Dima 09 янв. 2011 г., 14:26:12

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

  15. №3757
    Toxa
    Toxa 17 янв. 2011 г., 11:48:31

    Спасибо большое! Как жаль, что инфы по MVC так мало толковой в нете. Я тоже только перехожу на MVC и многие моменты для меня остаются непонятными. Пытался разобраться с зендом, но там уж очень все намудрено. Честно говоря до конца не понял, зачем нужен frontController, в данном примере все делает роутер

  16. №3880
    Ден
    Ден 10 февр. 2011 г., 2:21:32

    if(preg_match($route[0]), $uri) пишет синтаксическую ошибку ..? ...

  17. №3958
    проходил мимо
    проходил мимо 24 февр. 2011 г., 5:10:55

    Дену: скобка закрывающая не там стоит, сляпой чтоль... ёптить...

  18. №3984
    BOLVERIN
    BOLVERIN 01 марта 2011 г., 9:40:08

    foreach($this->routes as $route){ надо заменить на foreach($this->routes as $route[0]=>$route[1]){

    private $routes - точку с запятой добавить

    } }

        // Ничего не применилось. 404.
        header("HTTP/1.0 404 Not Found");
        die();
    }
    

    } вызовет ошибку

  19. №3996
    Максим
    Максим 03 марта 2011 г., 20:40:02

    Я позволили себе подправить.

    class router 
        {
            private $routes;
     
            function __construct () 
            {
                $this -> routes = include (dirname(__FILE__).'/route.cnf.php');
            }
     
            function uriGet () 
            {
                $segments = parse_url($_SERVER['REQUEST_URI']);
                return trim($segments['path'],'/');
            }
     
            function run() 
            {
                $access = false;
     
                $uri = $this -> uriGet ();
     
                foreach ($this -> routes as $patternRoute => $internalRoute) 
                {
                    if (!preg_match ($patternRoute,$uri)) continue;
     
                    $access = preg_replace ($patternRoute,$internalRoute,$uri);
                }
     
                if (!$access) die (header('HTTP/1.0 404 Not Found'));
     
                list ($controller,$method) = $segments = explode ('/',$access);
     
                $controllerFile = dirname(__FILE__).'/modules/'.$controller.'.php';
     
                include ($controllerFile);
     
                $parameters = array_slice ($segments,2);
     
                file_exists ($controllerFile) or die (header('HTTP/1.0 404 Not Found'));
     
                is_callable (array($controller,$method)) or die (header('HTTP/1.0 404 Not Found'));
     
                call_user_func_array (array($controller,$method),$parameters);
            }
        }
  20. №6321
    серега
    серега 07 июня 2012 г., 13:17:08

    а как будет выглядеть getPage?

  21. №6537
    Anton
    Anton 01 авг. 2012 г., 15:37:51

    Да, метод со этой строчки, как выглядит ? $page = $this->getPage($url);

  22. №6538
    Sam
    Sam 01 авг. 2012 г., 16:21:07

    Anton, примерно так:

    function getPageCount($pageSize = 10) {
      $itemCount = db_query("select count(*) from posts");
      return (int)(($itemCount+$pageSize-1)/$pageSize);
    }
     
    function getPage() {
      $total = getPageCount();
      $page = empty($_GET['page']) ? 1 : $_GET['page'];
      if($page>$total) {
         $page = $total;
      }
      if($page<1) {
        $page = 1;
      }
      return $page;
    }
  23. №6542
    Anton
    Anton 06 авг. 2012 г., 16:42:56

    Что то не получается собрать в мини каркас, у кого есть исходный код ? Спасибо

  24. №7439
    Дмитрий
    Дмитрий 22 янв. 2013 г., 10:05:23

    Каким образом, как я полагаю при помощи регулярки, избавиться от лишних параметров? Что бы если я написал, site.ru/about/some/params/, выдавало ошибку?

  25. №9937
    Сергей
    Сергей 18 авг. 2015 г., 20:45:29

    Всем привет. А регулярка в самом начале роута не губит производительность?

  26. №9938
    Sam
    Sam 19 авг. 2015 г., 13:29:56

    Может и губит. Надо замерять...

  27. №10135
    Артём
    Артём 21 дек. 2015 г., 23:57:35

    Каким образом, как я полагаю при помощи регулярки, избавиться от лишних параметров? Что бы если я написал, site.ru/about/some/params/, выдавало ошибку?

    Также интересует этот вопрос.

  1. Почта опубликована не будет.

  2. Можно использовать синтаксис Markdown или HTML.

  3. Введите ответ в поле. Щёлкните, чтобы получить другую задачу.