Простая реализация модели MVC с поддержкой иерархии шаблонов

в 8:07, , рубрики: mvc, php, web-разработка, Веб-разработка, шаблонизатор, шаблоны

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

Немного о MVC

Паттерн разработки MVC обсуждался многократно и подробно описывать его вряд-ли есть смысл. Для ознакомления с предметом можно почитать:

Поэтому только кратко упомянем ключевые компоненты этой системы:

  • Контроллер (controller) — Важнейший компонент системы. Именно набор контроллеров и обрабатываемых ими команд (actions) определяют, каким именно функционалом будет обладать система. Контроллер является первым элементом реализации бизнес-логики приложения и должен определять что произошло в системе, и каким способом на это отреагировать. В частности — контроллер определяет какое представление (View) потребуется для отображения состояния системы и каким образом (Model) эти данные должны быть получены. Однако, контроллер, как хороший командир, не должен вникать в то, как его подчиненные будут выполнять задачу. Его дело — выбрать исполнителей и отдать им приказы. Важно учитывать, что даже в рамках обработки одной команды контроллер не привязан к одному представлению и одной модели. Наоборот, он может выбирать ил «на лету». Для иллюстрации рассмотри следующий пример: авторизация пользователя (псевдокод):
    <code>
    userController->actionAuth($login,$pass)
    {
    $model=new userModel();
    if($model->authorize($login,$pass) {
    $view="authok";
    }
    else {
    $view="authfailure";
    }
    processView($view);
    }
    </code>
    

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

  • Представление (View) — Отвечает за представление данных пользователю. Представление не имеет ни малейшего представления (простите за каламбур) о том, каким образом получены данные, которые ему нужны. Оно знает только о том, откуда эти данные можно взять и как их представить пользователю. Как правило, основным источником данных для представления является модель. Однако, как мы увидим дальше — модель не всегда способна предоставить данные в полном объеме. Важно, что с одной и той же моделью могут взаимодействовать несколько представлений. Как было показано в примере и представление «authok» и представление «authfailure» будут использовать данные одной модели $model.
  • Модель (model) — Очень важный компонент, однако несколько напоминающий рассеянного ученого, который способен с одинаковым интересом решать проблемы бессмертия и уничтожения всего живого. Иными словами, модель должна уметь собрать необходимые данные или произвести обработку входящих данных. Это ее конек. Однако, модель как правило не имеет представления о том, в каком контексте ее вызвали, в ответ на какую команду, что происходит в системе в целом. Более того, модель не может предполагать, понадобятся ли системе для обработки команды дополнительные данные, или нет. Очевидно, что собирать все мыслимые данные на «всякий случай» ни одна модель ни в состоянии.

Попытка собрать все данные в одном представлении может привести и к многократному дублированию кода. Например, данные профиля пользователя некого сайта могут понадобиться в рамках самых разных задач (от редактирования профиля пользователем, до указания ссылки на автора опубликованного материала. Если каждая модель будет самостоятельно запрашивать такие данные, сложность и дублирование кода будет возрастать экспоненциально.
Если представление сможет запрашивать данные порциями, определяя каждый раз кто может эти данные предоставить, жизнь программиста станет легка и безоблачна, так как можно будет сосредоточиться на решении отдельной задачи, без необходимости держать в голове весь проект в целом. Однако, это предполагает необходимость организации иерархической обработки представлений. Если мы сможем из какого-то места шаблона запросить данные другого контроллера и получить готовый фрагмент выходного кода (например уже отформатированный html) нам станет легче. Однако, для этого нам в какой-то момент потребуется приостановить обработку текущего потока и передать управление другому обработчику. Проблема в том, что с такой задачей PHP справляется, мягко говоря, не блестяще. Данные через интерпретатор проходят только однократно и конструкции типа echo('<p><?php echo($msg'); ?></p>'); работать не будут. Внутренне обращение к интерпретатору не пройдет.

Отчасти, проблему блочного построения вывода помогают решить шаблонизаторы. Их достаточно много, разных по возможностям и удобству работы. Однако, они имеют один общий недостаток — они являются надстройкой над PHP и вводят свой язык разметки, требующий изучения. В тоже время, такой функционал в рамках PHP является заведомо избыточным, так как этот язык сам по себе обеспечивает вполне неплохие возможности работы с шаблонами. Думаю, всем приходилось писать конструкции типа:

<code>
<html>
...
<?php echo($msg); ?> 

или более сложные

<table>
<? php foreach($array as $val) { ?>
<tr><td>$val</td></tr>
<?}?>
</table>
</code>

В таких конструкциях как раз и используются возможности PHP как шаблонизатора. Кстати, дизайнеры к таким вставкам относятся без испуга.
Теперь давайте перейдем к практической части, и посмотрим как в рамках шаблона страницы можно реализовать вставку целого блока, который в свою очередь может быть составлен из целого набора под-блоков. Это может быть реализовано на чистом PHP, без привлечения дополнительного языка разметки.

Практика

Мы рассмотрим упрощенную версию системы, в которой опущены такие моменты, как работа с ЧПУ (человеко-понятные урл) и настройки файла .htaccess для обеспечения единой точки входа в систему. Эти вопросы широко освещены в сети и смысла повторяться нет. Также не будем рассматривать вопросы старательного раскладывания компонентов системы по каталогам, так как это по сути личное дело каждого. Сосредоточимся на решении проблемы вложенности шаблонов.
Саму систему в действии можно увидеть здесь

Главным компонентом системы и главной точкой входа является файл application.php:

<code>
<?php


	class webApplication
	{
		protected $dataBuf;
		
		protected static $_classInstance;
		protected $defaultHandler;
		protected $sefRequestParams; //To support SEF technology
		
		private function __construct()
		{
			$this->dataBuf="";
			$this->defaultController="mainpage";
			$currentURL = $_SERVER['REQUEST_URI'];
			$this->sefRequestParams=explode("/",$currentURL);
			
			//It could be a good idea to establish database connection here
		}
		
		private function __clone()
		{
    	}
    	
    	public function getSEFParams() //sef params need to be accessible for any parts of thew app
    	{
    		return $this->sefRequestParams;
    	}
    	
		public static function getApp()		
		{
        	if (null === self::$_classInstance) 
            	self::$_classInstance = new self();

        	return self::$_classInstance;
        }
		
		public function handle($controller,$action)
		{
			if(!isset($controller) || $controller=="")
				$controller=$this->defaultController;
			$val=$controller.'.php';
			$res=require_once($val);
			if($res!=1)
			{
				echo("requested controller not found!");
				return 0;
			}
			$controlClass=new $controller();
			if($controlClass==NULL)
			{
				echo("Controller initialization error!");
				return 0;
			}
			ob_start();
			$controlClass->dispatchAction($action,&$this);
			$this->dataBuf=ob_get_contents();
			ob_end_clean();
			echo($this->dataBuf);
			return 1;
		}
		
		public function handleHttp()
		{
			$controller=$_REQUEST['controller'];
			$action="";
			if(!isset($controller) || $controller=="") //Assume we're using SEF technics
			{
				$controller=$this->sefRequestParams[0];
				$action=$this->sefRequestParams[1];
			}
			else
			{
				$action=$_REQUEST['action'];
			}
			return $this->handle($controller,$action);
		}
	}
	
	$app=webApplication::getApp();
	$app->HandleHttp();
?>

</code>

Класс webApplication является базовой точкой входа в систему. Как видно из представленного кода, этот класс реализует паттерн Singleton. В рамках работы системы у нас всегда присутствует экземпляр данного класса, причем всегда только один. Такое свойство делает его крайне удобным для хранения всех глобальных данных системы. В данном случае импорт настроек системы и их использование опущены, так как их легко реализовать самостоятельно, в зависимости от своих потребностей.

Ключевой функцией класса является метод handle($controller,$action). Эта функция принимает на вход имя контроллера (первый параметр) и название действия (action), которое надо выполнить. В соответствии с практикой хорошего программирования предполагаем, что имя класса контроллера совпадает с именем файла, в котором он храниться. Разумеется, по желанию, строку $val=$controller.'.php'; можно модифицировать: $val=CONTROLLER_PATH.$controller.'.php'
Важным является то, эта функция позволяет вызвать нужный контроллер по его имени. Для того, чтобы контроллеры могли корректно взаимодействовать с системой, они должны реализовывать интерфейс iHandler, который определен следующим образом:

<code>
ihandler.php:

<?php
	interface iHandler
	{
		public function dispatchAction($action,&$app);
		public function actionDefault(&$app);
	}
?>
</code>
 

Этот интерфейс вводит два обязательных метода: dispatchAction($action,&$app) и public function actionDefault(&$app);
Оба метода в качестве одного из параметров принимают ссылку на класс webApplication (параметр &$app). Это сделано для того, чтобы избежать привычки PHP делать полную копию объекта. Да и иметь нужный класс в качестве параметра несколько удобнее, чем писать global $app; $app->…

Вернемся к нашему примеру. В нашем случае будет вызван контроллер welcome (welcome.php):

<code>
<?php
require_once("ihandler.php");

class welcome implements iHandler
{

	public function __construct()
	{
		//Nothing to do here	
	}
		
	public function dispatchAction($action,&$app)
	{
		$this->actionDefault(&$app);
	}
	
	public function actionDefault(&$app)
	{
		include("welcome.html");
	}
	
}
?>
</code>  

Этот контроллер написан сугубо в рамках примера и можно считать, что не делает вообще ничего. Только загружает некий HTML-файл. Откуда же тогда берутся данные? Можно предположить, что загружаемый файл является шаблоном и как-то их запрашивает. Так ли это? Смотрим код:

<code>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Template handler output test page</title>
</head>

<body>
<p>Test page for template handler</p>


<h3>Starting the test</h3>
<?php $app->handle("hello","say");?> 
</body>
</html>

</code>

И это действительно так!.. Смотрим на строку <?php $app->handle(«hello»,«say»);?>. Вот он лев! Мы обратились к нашему классу webApplication и попросили вызвать нужный нам контроллер. Причем система обеспечит подготовку и вставку нужного нам HTML автоматически. Никаких возвратов строк. И задействовали мы ту же функцию handle, которую разбирали выше. Мы просим вызвать контроллер hello, который очевидно расположен в hello.php

<code>
<?php
require_once("ihandler.php");

class hello implements iHandler
{

	public function __construct()
	{
		//Nothing to do here	
	}
		
	public function dispatchAction($action,&$app)
	{
		
		switch($action)
		{
			case 'say':
				$this->actionSay(&$app);
				break;
			default:
				$this->actionDefault(&$app);
			break;
		}
	}
	
	public function actionSay(&$app)
	{
		require_once("saymodel.php");
		$model=new sayModel();
		$model->prepareString($_REQUEST['name']);
		include("hello.tpl");
	}
	
	public function actionDefault(&$app)
	{
		//Nothing to do by default
	}
	
}
?>
</code> 

В данном контроллере у нас реализован метод, отвечающий за обработку действия «say» — actionSay.
Этот метод выполняет типичную для модели MVC последовательность действий: создает модель, передает ей на обработку данные, загружает представление.

Сначала посмотрим на модель (это только пример, поэтому она крайне проста).

<code>
<?php
	class sayModel
	{
		public $msg;
		
		public function __construct()
		{
			$this->msg="";
		}
		
		public function prepareString($name)
		{
			$this->msg="Hello $name!";
		}
	}

?>
</code>

Понятно, что реальная модель будет намного сложнее. Эта модель предоставляет данные в виде доступной (public) строковой переменной $msg. Как представление hello.tplб загружаемое контроллером использует эти данные? Очень просто:

hello.tpl
<code>
<p color="#ff0000"><?php echo($model->msg); ?></p>
</code>

Как видно, данное представление — всего навсего фрагмент HTML кода, со вставкой PHP, обращающейся к данным модели. Эти данные уйдут в поток и после обработки интерпретатором PHP попадут в нужное место первого представления. Понятно, что в целом уровень вложенности и количество вызовов контроллеров не ограничено.

Все! Мы реализовали многоуровневую систему шаблонов, используя модель MVC для каждого из них. Предлагаемая система может послужить неплохим скелетом для реализации сложных приложений.

Автор: LeoSad

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js