Пара слов для вступления
Приветствую.
В этом уроке речь пойдет об одном из способов организации веб-приложения. О всем известной хотя бы на слух MVC. Оговорюсь заранее, что для того, чтобы понять содержание урока, вам будет необходимо изучить предыдущие уроки этой ветки форума и, быть может, не раз обратиться к документации http://php.net
Немного терминов
Начинать стоит, конечно же, с самого MVC. Расшифровка её такова: Model-View-Controller. Иными словами, веб-приложение, созданное на основе этой архитектуры, можно логически поделить на 3 группы:
Model - часть скриптов, которая отвечает за реализацию работы с исходными ресурсами. Иными словами, этот функционал описывает их поведение в приложении в соответствии с бизнес-логикой. Очень часто к ним относятся различные классы, реализующие работу с данными в БД. Но это далеко не единственный вид моделей. Вообще, модель - это совокупность описывающих какой-то процесс правил и свойств, так что в эту категорию попадает широкий спектр классов реализуемого приложения.
View - отвечает за представление данных. Неважно какими способами и из каких источников они получены, как обработаны и хранятся, в конечном итоге цель приложения - отобразить данные пользователю. И архитектура MVC разделяет логику и представление. К этой части относятся шаблоны. В них определяется конечный вид на веб-станицах - определяются html-элементы, подключаются стили и т.п. Однако сейчас все чаще шаблоны начинают отвечать и за функциональную часть приложения. Например, в них встраиваются элементы, выполняющие AJAX-запросы на сервер, являясь таким образом уже частью функциональности. Это не идет вразрез с идеологией MVC, просто расширяет понятие View
Controller - служит "мостом" между View и Model. Это тот функционал приложения, который реализует его бизнес-процессы. Он отвечает за прием и обработку пользовательских данных, обращается к моделям (Model) приложения и использует представления (View) для вывода. Классически, контроллер делится на Action - действия. Действие - это часть контроллера, направленная на выполнение конкретного функционала. Например, "добавить пользователя", "сохранить запись" и т.п.
Далее, после того, как мы несколько познакомились с частями MVC, расскажу еще об одной популярной идее по реализации этой архитектуры. А именно - о фронт-контроллере. Фронт-контроллер (Front Controller) это некоторый управляющий контроллер. Через него идут все обращения к веб-приложению, по сути он - единая точка входа. Есть несколько способов реализации этого, однако чаще всего это делается через соответствующую настройку веб-сервера с перехватом запросов и перенаправлением их конкретному скрипту.
Пожалуй, стоит рассказать еще об одной части приложения - о маршрутизаторе. Ведь после того, как мы получили запрос, его принял фронт-контроллер, он должен что-то с ним делать. Так вот, чаще всего, он поручает эту задачу маршрутизатору. Тот разбирает пришедший запрос и на основании полученных данных обращается к соответствующему контроллеру, вызывая нужное его действие. Нередко фронт-контроллер объединяется с маршрутизатором, и так будет сделано в приведенном здесь примере, но это не очень хорошая практика. Здесь я приведу это скорее для того, чтобы читатели смогли самостоятельно разделить их, приведя таким образом архитектуру к более стройному виду.
Реализация
Теперь мы уже можем подойти собственно к самому процессу реализации. Приведу скрипт index.php, на который перенаправляются все запросы, пришедшие приложению:
PHP:
<?PHP
error_reporting(0);
define('APPLICATION_PATH', realpath(dirname(__FILE__)));
define('VIEWS_PATH', realpath(dirname(__FILE__)).'/Web/Views/');
set_include_path(
APPLICATION_PATH . '/Lib' . PATH_SEPARATOR .
APPLICATION_PATH . '/Web' . PATH_SEPARATOR .
APPLICATION_PATH . '/Core' . PATH_SEPARATOR .
'./bbcode/' .PATH_SEPARATOR. get_include_path());
function __autoload($className)
{
$fname = str_replace('_', '/', $className) . '.php';
$result = include_once($fname);
return $result;
}
$router = new Router();
$router->run();
?>
Пара слов об __autoload. Это - функция, которая служит для динамического подключения скриптов "по требованию". Обычно в имя класса закладывается некоторая логика, чтобы такое подключения могло быть разобрано относительно легко, и класс подключился через эту функцию. Я в этом примере использовал имя класса, разделенное "_", указывающее на подкаталоги.
Для того, чтобы перенаправление происходило, веб-сервер настраивается через .htaccess файл примерно так:
CODE (text):
ErrorDocument 404 /
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [L]
RewriteRule ^.*$ index.php [L]
Таким образом мы разрешаем доступ к файлам "напрямую". Иными словами, если веб-доступный файл существует и к нему обратились напрямую, вызова index.php не произойдет, а файл будет обработан веб-сервером. Здесь можно возразить, что это нарушает принцип фронт-контроллера, и отчасти это верно. Но для огранизации удобных скачиваний, подключений стилей и т.п. можно пойти на такой компромисс (в противном случае нужно было бы все такие запросы пропускать через фронт-контроллер только для того, чтобы отдать контент).
Перейдем к реализации маршрутизатора (который в нашем примере есть еще и фронт-контроллер по сути)
PHP:
<?PHP
class Router
{
private $routes;
function __construct()
{
$this->_load_defaults();
}
function getURI()
{
$uri=$this->_getURI();
$webRoot=Config_Handler::getInstance()->getWebpath();
return ltrim(preg_replace('#^'.$webRoot.'#i', '', $uri), '/');
}
//
function run()
{
$uri = $this->getURI();
foreach($this->routes as $route=>$destination)
{
if(preg_match($route, $uri))
{
$internalRoute = preg_replace($route, $destination, $uri);
$segments = explode('/', $internalRoute);
$controller = 'Controller_'.ucfirst(array_shift($segments));
$method = array_shift($segments).'Action';
$parameters = $segments;
//launch controller:
if(class_exists($controller))
{
$obj = new $controller;
if(method_exists($obj, $method))
{
call_user_func_array(array($obj, $method), $parameters);
return;
}
else
{
$this->_default_error();
}
}
else
{
$this->_default_error();
}
}
}
return;
}
protected function _default_error()
{
header("HTTP/1.0 404 Not Found");
exit("<table style='width:100%; height: 100%'><tr><td align='center' valign='center'><h1>404 Not Found</h1></td></tr></table>");
}
protected function _load_defaults()
{
$this->routes=array(
'#([-_a-z0-9]+)/([-_a-z0-9]+)/([-_a-z0-9]+)[\?]?(.*)#' => '$1/$2/$3/$4',
'#([-_a-z0-9]+)/([-_a-z0-9]+)[\?]?(.*)#' => '$1/$2/$3',
'#([-_a-z0-9]+)[\?]?(.*)#' => '$1/$2',
);
return true;
}
protected 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'], '/');
}
}
}
?>
Маршрутизатор этот очень прост. По сути, он ничего не делает с пришедшим запросом, а просто разбивает его по "/" и делит на составляющие, интерпретируя их соответственно как имя контроллера и его действие. То есть /users/list будет приведено к Controller_Users, и вызвано его действие listAction, вот и все.
Для того, чтобы не заботиться о том, откуда берутся маршруты, сделан метод _load_defaults, который содержит в себе их массив. Задача читателям будет преобразовать это к более удобному - настраиваемому - виду.
В маршрутизаторе используется класс Config_Handler, который будет рассмотрен позднее. Надобность в нем обусловлена тем, что для правильной работы необходимо знать каталог от корня, в котором располагается веб-приложение. В самом деле, если виртуальный хост сделан так, что его имя simplemvc.dev и указывает он, например, на
/var/www/projects/simplemvc
А проект мы расположим в
/var/www/projects/simplemvc/we/d ont/care/about/path
То чтобы вызвать его через веб, нам нужно указать
http://simplemvc[dot]dev/we/dont/car[dot][dot][dot]rname/actionname
- что, согласитесь, затрудняет задачу разбора URI для разбиения на имя контроллера и действия. Поэтому у веб-приложения есть конфигурационный файл, в котором указывается путь до него, который и отрезается от пришедшего URI.
Контроллеры
Сейчас, когда мы уже знаем, как происходит вызов контроллеров, пора их описать. В примере, который используется здесь, все контроллеры наследуются от некоторого базового. Так достигается удобство того, что общие задачи всех контроллеров доступны им всем сразу, и сосредоточены в одном месте. Это достигается базовым классом:
PHP:
<?PHP
class Controller_Base
{
protected $view;
protected $dbInstance;
protected $currentUser;
function __construct()
{
session_start();
$rDb = new Db_Controller();
$this->view = new View_Base();
$this->dbInstance = $rDb->getAdapter();
/* handle possible auth, for example:
if(isset($_SESSION['login']))
{
$this->currentUser=$_SESSION['login'];
}
*/
}
function redirect($url)
{
if($url[0]!='/')
{
$url[0]='/';
}
header('Location: '.Config_Handler::getInstance()->getWebroot().$url);
exit();
}
function getParam($param)
{
$mValue = $this->_get_param($param, $_GET);
if(!$mValue)
{
$mValue = $this->_get_param($param, $_POST);
}
return $mValue;
}
function loginUser($rgUser)
{
/* here we are saving login of user, for example:
$_SESSION['login']=$rgUser['login'];
*/
}
function logoutUser()
{
/* here we are doing logout of user, for example:
unset($_SESSION['login']);
*/
}
protected function _get_param($param, $array)
{
if(!array_key_exists($param, $array))
{
return null;
}
return $array[$param];
}
}
?>
Как видно, ряд общих вещей вынесено в базовый контроллер. У него есть два важных свойства - соединение с БД и текущее представление. Оба этих класса мы тоже рассмотрим, а пока что их инициализацию можно увидеть в конструкторе. Кроме прочего, так как чаще всего у приложения есть часть, предназначенная для авторизованных пользователей, контроллер предоставляет свойство currentUser - содержащее текущего пользователя, в случае, если он авторизован.
Приведу пример контроллера, который является конечным и действия которого уже вызываются:
PHP:
<?PHP
class Controller_Test extends Controller_Base
{
function showAction($url = null)
{
if(!$this->currentUser)
{
$this->redirect('/test/auth');
}
$rgPages=$this->dbInstance->select('some_table', array(
'id',
'name'
));
if(!$rgPages)
{
$rgPages=array();
}
$this->view->rgPages=$rgPages;
$this->view->render('showview');
}
function updateAction()
{
$id=$this->getParam('id');
$rgFields=array(
'name' => $this->getParam('name')
);
if(!$this->currentUser)
{
$this->redirect('/test/auth');
}
if(!$id)
{
$this->redirect('/test/show');
}
$this->dbInstance->update('some_table', $rgFields, array('id'=>$id));
$this->redirect('/test/show');
}
function addAction()
{
if(!$this->currentUser)
{
$this->redirect('/test/auth');
}
$rgFields=array(
'name' => $this->getParam('name')
);
$this->dbInstance->insert('some_table', $rgFields);
$this->redirect('/test/show');
}
function deleteAction()
{
if(!$this->currentUser)
{
$this->redirect('/test/auth');
}
$id = $this->getParam('id');
if(!$id)
{
$this->redirect('/test/show');
}
$this->dbInstance->delete('some_table', array('id'=>$id));
$this->redirect('/test/show');
}
function authAction()
{
$this->view->render('auth');
}
function loginAction()
{
/*
Here we log in user. If success, redirect to /test/show
If fails, redirect to /test/auth
*/
}
function logoutAction()
{
$this->logoutUser();
$this->redirect('/test/auth');
}
}
?>
Этот контроллер делает стандартные вещи - обновление, добавление, удаление данных. Таблица для этого some_test, которая состоит из 2-х колонок id и name. Авторизация пользователя опущена (так как войдет в задачи к уроку). Остальные же методы, уверен, говорят сами за себя.
Представления
Идеология устройства View схожа с контроллерами. Существует некоторый базовый класс. Однако в рамках этого примера используется уже сразу он, так как никаких специальных операций с представлениями не требуется. Все просто. Класс этот предполагает, что в приложении есть некоторый layout - часть представлений, общая для них всех. Это очень удобно - можно сохранять в конкретном представлении только то, что там действительно должно быть, не заботясь о "обвеске". Сам класс:
PHP:
<?PHP
class View_Base
{
function render($template, $rgParams = array())
{
echo $this->_fetch($template, $rgParams);
}
protected function _fetch_segment($template, $rgParams = array())
{
extract($rgParams);
ob_start();
include VIEWS_PATH.$template.'.phtml';
return ob_get_clean();
}
/*For debug purposes: */
protected function _render_segment($template, $rgParams = array())
{
echo $this->_fetch_segment($template, $rgParams);
}
protected function _fetch($template, $rgParams = array())
{
$content = $this->_fetch_segment($template, $rgParams);
return $this->_fetch_segment('layout', array('content' => $content));
}
}
?>
В классе виден существенный недостаток - имя layout зашито жестко. Когда мы рассмотрим класс Config_Handler, думаю, с этой задачей справиться будет легко. Основное назначение - метод render, который подключает требуемый файл представления, layout и выводит пользователю. Для полноты картины стоит привести пример layout:
PHP:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>Simple MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link href="<?=Config_Handler::getInstance()->getWebroot()?>/style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<?=$content?>
</body>
</html>
Обратите внимание на то, что вложенное представление будет выведено через переменную $content
Модели
Теперь мы знаем, как управлять данными и как их представлять пользователю. Но их самих у нас еще нет. Рассмотрим модели, используемые в этом демонстрационном приложении. Первая - это работа с конфигурационным файлом. В коде вызов Config_Handler происходил уже не раз.
В этом классе реализуется паттерн Singleton (Одиночка). Этот паттерн предполагает, что экземпляр одиночки всегда один в пределах веб-приложения. Конфигурационный файл у нас один, логично предположить, что экзепмляр конфигуратора должен быть одиночкой:
PHP:
<?PHP
class Config_Handler
{
static protected $rInstance = null;
protected $section='main';
protected $rgIni;
public static function getInstance()
{
if(!self::$rInstance)
{
self::$rInstance = new Config_Handler();
}
return self::$rInstance;
}
function __construct($path=null)
{
if(!$path)
{
$path=realpath(dirname(__FILE__)).'/../../Config/application.ini';
}
$this->rgIni=parse_ini_file($path, $this->section);
}
public function getDb()
{
return $this->rgIni[$this->section]['dbtype'];
}
public function getCredentials()
{
return array(
'host' => $this->rgIni[$this->section]['dbhost'],
'port' => $this->rgIni[$this->section]['dbport'],
'user' => $this->rgIni[$this->section]['dbuser'],
'password' => $this->rgIni[$this->section]['dbpassword'],
'database' => $this->rgIni[$this->section]['dbname']
);
}
public function getWebpath()
{
$webRoot = preg_replace('#^http\:\/\/#i', '', $this->rgIni[$this->section]['web_root']);
$webRoot = preg_replace('#^'.$_SERVER['HTTP_HOST'].'#i', '', $webRoot);
$webRoot = ltrim($webRoot, '/');
return $webRoot;
}
public function getWebRoot()
{
return $this->rgIni[$this->section]['web_root'];
}
}
?>
Класс полагается на функцию parse_ini_file. Если Вы планируете модифицировать application.ini, стоит прочесть про ограничения, накладываемые этой функцией. В конструкторе определяется путь по-умолчанию. Это неизбежно, так как если у нас еще нет конфигурационного файла, эти данные нужно откуда-то взять. Но можно делать вызов и с использованием явного указания пути. Предполагается, что директивы, определенные в конфигурационном файле, задаются администратором и потому корректны, однако простейшая проверка все же есть.
Обратите внимание так же на стандартную реализацию паттерна Singleton - это классический подход в PHP для такой реализации.
И, наконец, работа с БД. В рамках этого приложения я решил продемострировать паттерн Adapter (адаптер). Он основывается на том, что есть некоторый абстрактный класс(или же интерфейс) для работы с БД, а реализация нужных методов - определяется его потомками.
Для начала стоит привети класс, определяющий текущий адаптер на основе конфигурационного файла:
PHP:
<?PHP
class Db_Controller
{
function __construct()
{
;
}
public function getAdapter()
{
$adapter = Config_Handler::getInstance()->getDb();
$class = 'Db_'.$adapter.'_Adapter';
//this fails in php <5.3:
/*
return $class::getInstance();
*/
return call_user_func(array(
$class,
'getInstance'
));
}
public function getCredentials()
{
return Config_Handler::getInstance()->getCredentials();
}
}
?>
В комментарии есть одна любопытная деталь: до php 5.3 имя класса нельзя было указывать как переменную, если выполнялся вызов статического метода. Остальное - простая работа с классом Config_Handler. Имя соответствующего класса генерируется, ориентируясь на __autoload.
Далее абстрактный класс адаптера:
PHP:
<?PHP
abstract class Db_Adapter
{
static protected $rInstance = null;
static protected $rLink = null;
protected $lastInsertId;
protected $iAffectedRows;
abstract public static function getInstance();
abstract public function select(
$rgFrom,
$rgCols=array('*'),
$rgWhere=null,
$rgGroup=null,
$rgOrder=null,
$rgHaving=null,
$rgLimit=null);
abstract public function update($table, $rgFields, $rgWhere=null);
abstract public function replace();
abstract public function insert($table, $rgFields, $sPrimaryField=null);
abstract public function delete($table, $rgWhere);
abstract public function query($sql);
abstract protected function _escape_string($string);
public function lastInsertId()
{
return $this->lastInsertId;
}
public function affectedRows()
{
return $this->iAffectedRows;
}
protected function _generic_escape($string)
{
return addslashes($string);
}
protected function _get_delete_sql(
$table,
$rgWhere
)
{
if(!$table)
{
return false;
}
$rgDelete=array('DELETE FROM');
$rgDelete[]=$table;
if($rgWhere)
{
$rgDelete[]=$this->_get_sql_where($rgWhere);
}
return join(' ', $rgDelete);
}
protected function _get_insert_sql(
$table,
$rgFields
)
{
if(!$table || !is_array($rgFields) || !count($rgFields))
{
return false;
}
$rgTableKeys=array();
$rgTableValues=array();
foreach(array_keys($rgFields) as $field)
{
$rgTableKeys[]=$field;
}
foreach(array_values($rgFields) as $value)
{
$rgTableValues[]="'".$this->_escape_string($value)."'";
}
$rgInsert=array('INSERT INTO');
$rgInsert[]=$table;
$rgInsert[]='('.join(',', $rgTableKeys).')';
$rgInsert[]='VALUES';
$rgInsert[]='('.join(',', $rgTableValues).')';
return join(' ', $rgInsert);
}
protected function _get_update_sql(
$table,
$rgFields,
$rgWhere
)
{
if(!$table || !is_array($rgFields) || !count($rgFields))
{
return false;
}
$rgUpdate=array('UPDATE');
$rgUpdate[]=$table;
$rgUpdate[]='SET';
$rgUpdateStr=array();
foreach($rgFields as $field=>$value)
{
$rgUpdateStr[]=$field.'='."'".$this->_escape_string($value)."'";
}
$rgUpdate[]=join(',', $rgUpdateStr);
if($rgWhere)
{
$rgUpdate[]=$this->_get_sql_where($rgWhere);
}
return join(' ', $rgUpdate);
}
protected function _get_select_sql(
$rgFrom,
$rgCols=array('*'),
$rgWhere=null,
$rgGroup=null,
$rgOrder=null,
$rgHaving=null,
$rgLimit=null)
{
if(!$rgFrom)
{
return false;
}
$rgSelect=array('SELECT');
$rgSelect[]=is_array($rgCols)?join(',', $rgCols):$rgCols;
$rgSelect[]='FROM';
if(!is_array($rgFrom))
{
$rgSelect[]=$rgFrom;
}
if($rgWhere)
{
$rgSelect[]=$this->_get_sql_where($rgWhere);
}
//the rest is TODO
return join(' ', $rgSelect);
}
protected function _get_sql_where($rgWhere)
{
$strWhere=array();
if($rgWhere)
{
$strWhere[]='WHERE';
$rgWhereStr=array();
if(is_array($rgWhere))
{
foreach($rgWhere as $field=>$condition)
{
if(is_array($condition))
{
//TODO
}
else
{
$rgWhereStr[]='('.$field."='".$this->_escape_string($condition)."')";
}
}
$strWhere[]=join(' AND ', $rgWhereStr);
}
else
{
$strWhere[]=$rgWhere;
}
}
return join(' ', $strWhere);
}
}
?>
Здесь снова видна реализация паттерна Singleton. Кроме прочего, объявляются стандартные метода для работы с sql а так же определяется генерация самого SQL-текста запроса. в TODO помечено то, что не реализовано (скажем, GROUP BY, HAVING и т.п.)
Некоторые методы обязаны быть определены в потомках. Собственно, они и могут быть определены только там (как, к примеру, _escape_string - мы заранее не знаем, какой адаптер будет реализовывать работу)
В приложении-примере я использую работу с sqlite 2 БД. И класс, соответствующий такой работе, сделан таким образом:
PHP:
<?PHP
class Db_Sqlite_Adapter extends Db_Adapter
{
protected $lastError;
public static function getInstance()
{
if(!parent::$rInstance)
{
parent::$rInstance = new Db_Sqlite_Adapter();
}
return parent::$rInstance;
}
function __construct()
{
if(!$this->_connect())
{
return false;
}
}
function select($rgFrom,
$rgCols=array('*'),
$rgWhere=null,
$rgGroup=null,
$rgOrder=null,
$rgHaving=null,
$rgLimit=null)
{
if(!parent::$rLink)
{
return false;
}
$sql=$this->_get_select_sql($rgFrom, $rgCols, $rgWhere, $rgGroup, $rgOrder, $rgHaving, $rgLimit);
if(!$sql)
{
return false;
}
$rgResult = sqlite_array_query(parent::$rLink, $sql);
if(!$rgResult)
{
$rgResult=false;
}
return $rgResult;
}
public function replace()
{
}
public function update($table, $rgFields, $rgWhere=null)
{
if(!is_array($rgFields)||!count($rgFields))
{
return false;
}
$sql=$this->_get_update_sql($table, $rgFields, $rgWhere);
$bResult = $this->query($sql);
return $bResult;
}
public function insert($table, $rgFields, $sPrimaryField='id')
{
if(!array_key_exists($sPrimaryField, $rgFields))
{
$rgFields[$sPrimaryField]=$this->_get_next_id($table, $sPrimaryField);
}
$sql=$this->_get_insert_sql($table, $rgFields);
$bResult = $this->query($sql);
return $bResult;
}
public function delete($table, $rgWhere)
{
$sql=$this->_get_delete_sql($table, $rgWhere);
$bResult = $this->query($sql);
return $bResult;
}
public function query($sql)
{
return sqlite_exec($sql, parent::$rLink);
}
protected function _get_next_id($table, $sPrimaryField='id')
{
$rgMax=$this->select($table, array('MAX('.$sPrimaryField.')'));
return $rgMax[0][0]+1;
}
protected function _connect()
{
$rDb = new Db_Controller();
$rgCredentials = $rDb->getCredentials();
if(!parent::$rLink)
{
try
{
parent::$rLink=sqlite_open($rgCredentials['database'], 0666, $sLastError);
if(!parent::$rLink)
{
$this->lastError=$sLastError;
return false;
}
}
catch(Exception $e)
{
return false;
}
}
return true;
}
protected function _escape_string($string)
{
return sqlite_escape_string($string);
}
}
?>
Метод replace не реализован вовсе, сделан он потому, что должны быть реализованы все абстрактные методы.
Об организации
Итак, мы рассмотрели все три части архитектуры MVC. Осталось сказать лишь несколько слов об организации - том, как хранятся те или иные ресурсы в этом примере. Начну с конфигурационного файла:
CODE (text):
[main]
dbtype = Sqlite
dbname = /var/www/data/simplemvc/Db/appdb
dbhost = 127.0.0.1
dbuser = NOUSER
dbpassword = NOPASSWORD
dbport = NOPORT
web_root = http://simplemvc.dev
Соответственно, для правильно работы нужно создать виртуальный хост так, как видно из файла.
Далее, о структуре каталогов. Здесь предполагается, что "/" есть корень расположения проекта:
CODE (text):
Config/ - каталог с конфигурационными файлами (здесь расположен application.ini)
Db/ - каталог с БД. Т.к. Sqlite хранит данные в файле, то этот каталог необходим
Lib/ - каталог для хранения моделей и базовых классов
Web/ - каталог для хранения контроллеров и представлений
images/ - каталог с изображениями
index.php - файл-обработчик запросов
style.css - таблица стилей
Дальнейшая структура определяется исходя из того, как __autoload ожидает их найти. Например, в /Lib структура такова:
CODE (text):
Config/
Controller/
Db/
View/
Router.php
Дальнейшее, думаю, уже понятно - в этих каталогах располагаются свои подкаталоги и т.п.
Итоги
Располагая моделями, представлениями и контроллерами, уже можно говорить о том, что структура, отвечающая MVC, реализована. Может показаться, что это несколько сложно, однако это не так - более того, масштабировать эту структуру очень просто.
К примеру, если нужна работа с MySQL - нужно всего пронаследовать адаптер и реализовать в нем работу с этой СУБД. Остальные части приложения останутся незатронутыми.
Необходимые директивы можно добавлять в конфигурационный файл и использовать по требованию.
И не стоит забывать, что MVC - не панацея, а просто способ организации архитектуры. На текущий момент он один из наиболее удачных, и я надеюсь, что данный урок раскрывает необходимые для её понимания аспекты.
Задачи к уроку
0. Реализовать разделение маршрутизатора и фронт-контроллера в приложении
1. Добавить возможность настройки маршрутов для маршрутизатора
2. Реализовать в базовом контроллере авторизацию пользователя
3. Сделать настраиваемым имя layout
4. Реализовать метод replace для Sqlite