Пример MVC в php. Вторая статья. Маршрутизация, контролеры, экшены, шаблоны и модели

Содержание цикла статей:

В этой статье мы напишем «каркас» нашего проекта. Под словом «каркас» я подразумеваю рабочий код, который будет иметь в своей основе MVC подход, то есть будет иметь четкое разделение логики на контролеры, экшены, шаблоны (представления) и модели.

И так начнем, как я уже писал в предыдущей статье, паттерн MVC подразумевает одну точку входа – index.php, через это скрипт будут проходить все запросы, через него будет работать вся логика проекта. Для того чтобы реализовать такой подход необходимо настроить сервер, подразумевается, что сайт работает на сервере apache, поэтому нам достаточно создать файл .htaccess, в котором мы укажем правила маршрутизации URL. Помимо определения точки входа, маршрутизация позволяет создавать ЧПУ(человеко-понятные урлы). То есть после правильной настройки, адреса страниц буду выглядеть вот так site.ru/article/new.
Для начала, давайте составим .htaccess, который перенаправит обработку всех страниц на скрипт index.php. Код выглядит вот так:

RewriteEngine on 
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?route=$1 [L,QSA]

Файл .htaccess должен лежать в корневой папке сайта, тут же необходимо создать скрипт index.php, который является точкой входа. Давайте запишем в index.php одну строку, для проверки работы перенаправления:

echo "test";

Теперь можно проверять работу перенаправления, введите любой адрес и посмотрите, что получиться: test-mvc.web/sdf/sdf/ или test-mvc.web/sdf/sdf/2342/не важно, на экране в любом случае, должно появиться «Test». Если Вы увидели эту надпись, значит, у нас все получилось.
Продолжим, давайте для удобства создадим в корне сайта файл config.php, в котором будем задавать различные константы, облегчающие своим существование настройку сайта. Это могут быть различные пути к скриптам, подступы к базе данных и так далее. Сейчас в конфиге давайте зададим следующее:

// Задаем константы:
define ('DS', DIRECTORY_SEPARATOR); // разделитель для путей к файлам
$sitePath = realpath(dirname(__FILE__) . DS);
define ('SITE_PATH', $sitePath); // путь к корневой папке сайта

// для подключения к бд
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_HOST', 'localhost');
define('DB_NAME', 'blog_mvc');

Для того, чтобы константы и другие данные конфига мы могли использовать во всем проекте, в файле index.php необходимо подключить скрипт config.php.
Помимо подключения файла с настройками, в index.php нужно создать подключение к базе данных, подключить скрипт с ядром сайта и запустить роутер, в котором будет происходить маршрутизация.
Теперь по порядку, создание соединения с базой данных будет находиться в index.php для того, чтобы соединение открывалось только один раз. Единожды открыв соединение, мы сможем использовать его во всех контроллерах и моделях, но об этом чуть позже. Сейчас просто создадим соединение с базой. Для работы с бд я решил использовать PDO. Подробнее почитать про PDO можно тут.
Ядро сайта расположим в папке core и назовем скрипт core.php, тут мы напишем функцию, которая будет сама подключать, необходимы для работы классы. Такая функция очень облегчит и упростит нам работу с контролерами, моделями и тд. Поскольку, забегая вперед скажу, что каждый контролер и каждая модель будут представлять собой отдельный класс.
Помимо авто подключения классов, добавим в ядро создания хранилища (реестра), в котором будем хранить все необходимые объекты и переменные, которые могут пригодиться в любом месте проекта.
Роутер тоже подключим в индексном файле, он будет анализировать URL и подключать необходимый контроллер и экшен. Что такое контролер я писал в предыдущей статье, а информацию про экшен я пропустил умышленно, не став нагружать лишней информацией. Так что же такое экшен?
Контролер это класс, в котором заключены различные методы, при MVC подходе каждый метод будет являться экшеном. То есть экшен(action) – это метод класса, который будет обрабатывать данные и передавать их в представление (в шаблон). Может быть, пока не совсем понятно, но после примера все станет на свои места.
На данном этапе теории достаточно, давайте перейдем к практике. Приведу код файлов, работу которых, я описывал выше.
Код скрипта index.php:

// включим отображение всех ошибок
error_reporting (E_ALL); 
// подключаем конфиг
include ('/config.php'); 

// Соединяемся с БД
$dbObject = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB_NAME, DB_USER, DB_PASS);

// подключаем ядро сайта
include (SITE_PATH . DS . 'core' . DS . 'core.php'); 

// Загружаем router
$router = new Router($registry);
// записываем данные в реестр
$registry->set ('router', $router);
// задаем путь до папки контроллеров.
$router->setPath (SITE_PATH . 'controllers');
// запускаем маршрутизатор
$router->start();

Скрипт core.php:

// Загрузка классов "на лету"
function __autoload($className) {
	$filename = strtolower($className) . '.php';
	// определяем класс и находим для него путь
	$expArr = explode('_', $className);
	if(empty($expArr[1]) OR $expArr[1] == 'Base'){
		$folder = 'classes';			
	}else{			
		switch(strtolower($expArr[0])){
			case 'controller':
				$folder = 'controllers';	
				break;
				
			case 'model':					
				$folder = 'models';	
				break;
				
			default:
				$folder = 'classes';
				break;
		}
	}
	// путь до класса
	$file = SITE_PATH . $folder . DS . $filename;
	// проверяем наличие файла
	if (file_exists($file) == false) {
		return false;
	}		
	// подключаем файл с классом
	include ($file);
}

// запускаем реестр (хранилище)
$registry = new Registry;

Класс хранилища Registry.php, будет находиться в папке /classes/

// Класс хранилища
Class Registry {
	private $vars = array();
	
	// запись данных
     function set($key, $var) {
        if (isset($this->vars[$key]) == true) {
			throw new Exception('Unable to set var `' . $key . '`. Already set.');
        }
        $this->vars[$key] = $var;
        return true;
	}

	// получение данных
	function get($key) {
		if (isset($this->vars[$key]) == false) {
			return null;
		}
		return $this->vars[$key];
	}

	// удаление данных
	function remove($var) {
		unset($this->vars[$key]);
	}
}

Код файла router.php, который находиться в папке /classes/

// класс роутера

Class Router {

	private $registry;
	private $path;
	private $args = array();

	// получаем хранилище
	function __construct($registry) {
		$this->registry = $registry;
	}

	// задаем путь до папки с контроллерами
	function setPath($path) {
        $path = trim($path, '/\\');
        $path .= DS;
		// если путь не существует, сигнализируем об этом
        if (is_dir($path) == false) {
			throw new Exception ('Invalid controller path: `' . $path . '`');
        }
        $this->path = $path;
	}	
	
	// определение контроллера и экшена из урла
	private function getController(&$file, &$controller, &$action, &$args) {
        $route = (empty($_GET['route'])) ? '' : $_GET['route'];
		unset($_GET['route']);
        if (empty($route)) {
			$route = 'index'; 
		}
		
        // Получаем части урла
        $route = trim($route, '/\\');
        $parts = explode('/', $route);

        // Находим контроллер
        $cmd_path = $this->path;
        foreach ($parts as $part) {
			$fullpath = $cmd_path . $part;

			// Проверка существования папки
			if (is_dir($fullpath)) {
				$cmd_path .= $part . DS;
				array_shift($parts);
				continue;
			}

			// Находим файл
			if (is_file($fullpath . '.php')) {
				$controller = $part;
				array_shift($parts);
				break;
			}
        }

		// если урле не указан контролер, то испольлзуем поумолчанию index
        if (empty($controller)) {
			$controller = 'index'; 
		}

        // Получаем экшен
        $action = array_shift($parts);
        if (empty($action)) { 
			$action = 'index'; 
		}

        $file = $cmd_path . $controller . '.php';
        $args = $parts;
	}
	
	function start() {
        // Анализируем путь
        $this->getController($file, $controller, $action, $args);
		
        // Проверка существования файла, иначе 404
        if (is_readable($file) == false) {
			die ('404 Not Found');
        }
		
        // Подключаем файл
        include ($file);

        // Создаём экземпляр контроллера
        $class = 'Controller_' . $controller;
        $controller = new $class($this->registry);
		
        // Если экшен не существует - 404
        if (is_callable(array($controller, $action)) == false) {
			die ('404 Not Found');
        }

        // Выполняем экшен
        $controller->$action();
	}
}

Теперь необходимо создать папки для хранения контроллеров, шаблонов и моделей – в корне создадим три папки controllers, views и models. И создадим несколько тестовых файлов /controllers/index.php, /views/index/index.php и /models/model_users.php, а теперь заполним файлы:
Для контроллера:

// контролер
Class Controller_Index Extends Controller_Base {	
	// шаблон
	public $layouts = "first_layouts";
	
	// экшен
	function index() {
		$model = new Model_Users();
		$userInfo = $model->getUser();
		$this->template->vars('userInfo', $userInfo);
		$this->template->view('index');
	}	
} 

Для отображения(/views/index/index.php)

Test view <br/>
id: <?=$userInfo['id'];?><br/>
name: <?=$userInfo['name'];?>

И модель:

// модель
Class Model_Users{
	public function getUser(){
		return array('id'=>1, 'name'=>'test_name');
	}		
}

Как вы могли заметить, класс контролера наследуется от родительского класса Controller_Base. Это сделано, для того, чтобы упростить класс контролера. Поскольку нам еще необходимо подключать класс для работы с шаблонами, его подключение вынесено в Controller_Base.
Приведу его код, он расположен в папке /classes/ и называется controller_base.php :

// абстрактый класс контроллера
Abstract Class Controller_Base {

	protected $registry;
	protected $template;
	protected $layouts; // шаблон
	
	public $vars = array();

	// в конструкторе подключаем шаблоны
	function __construct($registry) {
		$this->registry = $registry;
		// шаблоны
		$this->template = new Template($this->layouts, get_class($this));
	}

	abstract function index();	
}

Теперь осталось только разобраться с шаблонами. В абстрактном классе Controller_Base мы вызываем класс Template и передаем ему имя шаблона и имя контроллера.
Код класса Template, который лежит тут /classes/ и называется template.php

// класс для подключения шаблонов и передачи данных в отображение
Class Template {

	private $template;
	private $controller;
	private $layouts;
	private $vars = array();
	
	function __construct($layouts, $controllerName) {
		$this->layouts = $layouts;
		$arr = explode('_', $controllerName);
		$this->controller = strtolower($arr[1]);
	}
	
	// установка переменных, для отображения
	function vars($varname, $value) {
		if (isset($this->vars[$varname]) == true) {
			trigger_error ('Unable to set var `' . $varname . '`. Already set, and overwrite not allowed.', E_USER_NOTICE);
			return false;
		}
		$this->vars[$varname] = $value;
		return true;
	}
	
	// отображение
	function view($name) {
		$pathLayout = SITE_PATH . 'views' . DS . 'layouts' . DS . $this->layouts . '.php';
		$contentPage = SITE_PATH . 'views' . DS . $this->controller . DS . $name . '.php';
		if (file_exists($pathLayout) == false) {
			trigger_error ('Layout `' . $this->layouts . '` does not exist.', E_USER_NOTICE);
			return false;
		}
		if (file_exists($contentPage) == false) {
			trigger_error ('Template `' . $name . '` does not exist.', E_USER_NOTICE);
			return false;
		}
		
		foreach ($this->vars as $key => $value) {
			$$key = $value;
		}

		include ($pathLayout);                
	}
	
}

Если вы внимательно прочитали код, то наверняка поняли, что для отображения на страницах у нас используется шаблон first_layouts и вьюха(отображение) index.php – ее код я приводил чуть выше. Все что нам осталось, это создать файл шаблона first_layouts. Расположим его в папке /views/layouts/first_layouts.php
Шаблон будет содержать вот такой код:

<h1> header </h1>
<?php
	include ($contentPage);
?>
<h1> footer </h1>

Вот и все, на этом создание «каркаса» закончено. Сейчас у нас получилась самая простая структура, использующая в своей основе паттерн MVC. В этой статье я не стал затрагивать работу с базой данных, только вскользь упомянул ее, поскольку статья и так получилась большая. Непосредственно работу с базой данных я опишу в следующей статье.
На этом статья закончена, скачать исходники можно архивом тут.

Рассказать друзьям:


Пример MVC в php. Вторая статья. Маршрутизация, контролеры, экшены, шаблоны и модели: 23 комментария

  1. Зачем в проверке на тру или фолс приравнивать, ведь можно просто обойтись if (isset($this->some_var)) {} или if (!isset($this-some_var)) {}

  2. Здравствуйте, очень помогла статья, начал потихоньку осваиваться, добавил свою модель контроллер и вьюху, но вот какой вопрос, не работает роутинг на *nix системе, изначально router.php не мог найти путь к контроллерам, пришлось поменять $path = trim($path, ‘/\\’); на $path = trim($path, ‘/var/www/eve’); теперь страница открывается, но при переходе на localhost/my_controller я получаю всё ту же страницу index. Подскажите пожалуйста в чем может быть беда?

    С уважением Колмаков Юрий.

    1. Функция trim($path, ‘/\\’) обрезает первый слеш и получается путь var/www , а нужно последний(для достоверности) и будет /var/www/test. Решением будет использовании функции rtrim. Получится просто $path = rtrim($path, «/\\»);

  3. В файле config.php строчку
    $sitePath = realpath(dirname(__FILE__) . DS);
    надо заменить на
    $sitePath = realpath(dirname(__FILE__) . DS) . DS;
    Иначе ошибка будет вылетать…

    1. Думаю правильнее так , ибо realpath все равно режет слеш .
      $sitePath = realpath(dirname(__FILE__)).DS

  4. А если я желаю получить структуру каталогов следующим образом
    /components/com_base/controller.php
    /components/com_base/models.php
    /components/com_base/view.php (или index.php)
    Как мне поменять данную MVC?

  5. Великолепный цикл статей об MVC, очень многое взял к себе на заметку, но возникли вопросы, можно ли пояснить:
    -Зачем в роутер передаем реестр и есть ли в этом смысл
    $router = new Router($registry);
    -Зачем передаем в реестр обьект роутер
    $registry->set (‘router’, $router);
    -И соответственно зачем постоянно свойство реестр используем
    $this->registry = $registry;

    Спасибо.

    1. Здравствуйте.
      Статьи писались по ходу написания самой структуры mvc. Изначально реестр планировалось использовать как хранилище данных, переменных. Но с в процессе написания, было принято решение передавать переменные по другому: в шаблоны, через класс Template, а между контроллерами и моделями через базовые(родительские) классы. Получается, что реестр стал пережитком, от него можно избавиться. Я планирую скоро написать еще одну статью по mvc, в которой выложу исходники уже исправленной структуры. Так же сейчас наблюдаются проблемы при работе на *nix системах, поскольку разработка велась под windows и в некоторых местах я ошибся с реестов, что сказывается на работе кода в *nix системах.
      В ближайшем будущем будет пятая статья про mvc с исправлениями. Прошу прощения за ожидание.

  6. Здравствуйте. Хорошие статьи по MVC. У меня пока только один вопрос назрел:
    если написать роутер, как синглтон, то можно обойтись и без хранилища, не так ли?

  7. Добрый день, а можно, для начинающих разбираться в MVC, получить дамп базы и подробней остановиться на БД в MVC. Хочеться получить по окончанию статься результат без ошибок.

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

  8. Добрый день, сразу извеняюсь за глупость вопроса, я совсем начинающий, но с чего эта ошибка Fatal error: Class ‘Registry’ not found in L:\home\localhost\www\war\core\core.php on line 35? я даже скачал ваши исходники, все равно, мб у меня что-то с настройками?

  9. Здравствуйте, почему бы это не заменить
    $sitePath = realpath(dirname(__FILE__) . DS) . DS;
    на это:
    $sitePath = realpath(__DIR__).DS;

  10. И тут не понимаю зачем лишний слэш
    include (SITE_PATH . DS . ‘core’ . DS . ‘core.php’);
    не правильней так?
    include (SITE_PATH. ‘core’ . DS . ‘core.php’);

  11. Здравствуйте! Я совсем начинающий разработчик и хотел бы задать вопрос.
    А как передавать параметры в вашей MVC системе? Я имею ввиду, что у меня есть вьюшка lk в которой есть страничка юзера user. То есть выглядит все вот так http://www.mysite.ru/lk/user/?id=1, но я хотел бы иметь возможность передавать параметры без ?id, то есть http://www.mysite.ru/lk/user/1. Как сказать контроллеру, что единичка в конце — это айдишник? Заранее спасибо….

  12. Добрый день! Огромное спасибо за статью. Можно ли на странице сделать несколько контентов — для категорий и для статей отдельно?

  13. Вот это

    $route = (empty($_GET[‘route’])) ? » : $_GET[‘route’];
    unset($_GET[‘route’]);
    if (empty($route)) {
    $route = ‘index’;
    }

    Можно заменить одной строкой

    $route = (!empty($_GET[‘route’])) ? $_GET[‘route’] : ‘index’;

  14. А можно подробнее, почему здесь ‘Base’ с большой буквы?
    if(empty($expArr[1]) OR $expArr[1] == ‘Base’)
    (строка 7 файла core.php)

    1. Имя класса, от которого наследуются контроллеры, вот такое: Controller_Base. Тут Base с большой буквы, поэтому и проверка с большой буквы. Этот класс лежит в папке /classes/ и вызывается при вызове контроллера, в нем описаны свойства, которые используются во всех контроллерах, это сделано для упрощения классов самих контроллеров, чтобы не описывать каждый раз одинаковые свойства

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*