В прошлой заметке мы остановились на создании входного скрипта для нашего MVC-фреймворка на PHP index.php:

<?php
//получим URL-адрес сайта
define('__SITE_LIVE',$_SERVER["REQUEST_SCHEME"].'://'.$_SERVER['HTTP_HOST'].rtrim($_SERVER['PHP_SELF'],'index.php'));
//получим абсолютный путь к папке сайта и определим соотв. глобальную константу
$site_path = realpath(dirname(__FILE__));
define ('__SITE_PATH', $site_path);
//зададим путь и подключим файл ядра нашего фреймворка
define ('__VMVC_PATH', '../vmvc');
include __VMVC_PATH.'/core.php';
/*** и, пожалуй, начнем ***/ 
vmvc::Go();
?>


И все! Такой краткий входной скрипт. Круто, не правда ли?! Не вижу смысла в нем мудрить.

Итак, все сводится к тому, что мы определяем парочку полезных констант, подключаем файл ядра фреймворка и стартуем сам фреймворк. Инструкция vmvc::Go(); говорит о том, что в ядре определен метод, который позволяет вызвать себя из класса статически, т.е. без создания объекта класса, для чего потребовалась бы дополнительная команда вида new vmvc.

Приступим к написанию ядра нашего фреймворка. Создаем файл core.php в папке vmvc, которая расположена на уровень выше директории www, где находится входной скрипт.

core.php

<?
Class vmvc {
public static function Go() {
echo 'Поехали';
}
}
?>


На экран будет выведено 'Поехали'.

Основная задача нашего фреймворка - отделить логику работы от представления или, другими словами, программный код от дизайна. Напомню, что за логику работы будут отвечать у нас контроллеры (controllers), а за дизайн - виды (или представления - views).

Идея контроллеров состоит в том, что они являются классами, реагирующими на определенные действия пользователей. Непосредственно в контроллерах описывается, что именно делает скрипт, когда пользователь загружает ту или иную страницу, выполняет то или иное действие (Action). Одним из часто встречающихся примеров является работа с материалами (статьи, новости) сайта. Для подобных операций мы можем определить класс-контроллер (или, если угодно, - компонент) Class articleController {...} , в котором создать методы-экшены: вывод перечня статей из базы, добавление новой статьи, правка статей и удаление. Например, главный метод вывода всех статей может быть определен так: public function indexAction() {...}

Здесь мы пойдем на некоторые условности для удобства дальнейшей разработки. Имена классов контроллеров мы будем составлять из непосредственно названия реализуемого компонента (как выше - articles) и суффикса Controller - articlesController. Таким образом, при беглом взгляде на код нам будет сразу понятно, что из себя представляет данный класс.
А имена методов, обрабатывающих действия - название_действияAction (например, deleteAction). Обратите внимания, имена у нас начинаются с маленькой буквы.

Когда должен начать работу тот или иной контроллер (компонент) мы будем определять из строки URL. Ведь очевидно, что сайт должен вывести перечень статей, когда пользователь попадает на страницу http://vmvc.net/articles , например. Мы зададим считывание различных частей URL и будем передавать дальнейшее выполнение программы одноименному контроллеру (в данном случае articlesController).

Аналогично с действиями. Помимо того, что из URL мы определяем к какому контроллеру обратиться, также мы должны определить, какой экшен-метод в этом контроллере надлежит выполнить. Его мы определим в следующей части URL (через слеш) так: http://vmvc.net/articles/update . Т.е. если пользователь попал на этот адрес, фреймворк обратится к контроллеру articlesController и выполнит в нем метод updateAction(). Условимся, что если в URL экшен не указан, то по-умолчанию будет выполнен метод indexAction(). Аналогично и с контроллером: если Url состоит только из http://vmvc.net , то обращение будет производиться к контроллеру indexController с вызовом метода indexAction(). Таким образом, обязательным условием создания работающего приложения на нашем фреймворке должно быть как минимум наличие описания контроллера indexController, а метод indexAction() должен присутствовать во всех классах контроллеров, определенных в приложении.

В последствии мы сделаем возможность настроки в конфиге контроллера и экшена, запускающихся по-умолчанию, а также предусмотрим контроллер-обработчик для ошибок 404, когда пользователь заходит по адресу, для которого нет своего контроллера или он еще не создан.

Чтобы вести работу с красивыми Url'ами мы должны написать указание серверу с правилами их обработки. В www-директории для этой цели мы поместим файл .htaccess следующего содержания:

AddDefaultCharset UTF-8
<files *.ini>
order deny,allow
deny from all
</files>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?$1 [L,QSA]


В этом файле мы указали, что: работаем в общепринятой кодировке UTF-8, запрещаем доступ к конфигурационным файлам (файлы с расширением .ini), включаем модуль перенаправляний URL, а все обращения в URL переадресовываем на входной скрипт index.php Напомню, входной скрипт запускает наш фреймворк и передает ему дальнейшую работу, которая в простейшем случае заключается в вызове экшена из контроллера, определяемых на основании URL. Экшен выполняет какие-либо логические действия, обрабатывает данные и выводит результат на экран. Такая общая схема или проще:
пользователь заходит на сайт -> index.php -> vmvc -> Controller->Action().

Напишем класс в файле core.php для вычленения элементов из Url:

<?
//класс для получения фрагментов URL (разделенных "/")
Class Uri extends Base {
//массив фрагментов URL
public static $fragments = array();
 protected function __construct()
{
//преобразуем строку URL-запроса в массив и размещаем на хранение
self::$fragments = explode('/', $_SERVER['QUERY_STRING']);
}
 /**
* @метод получения определенного фрагмента URL по ключу (порядковый номер в массиве)
*
* @доступ публичный
*
* @параметр типа string $key:ключ uri
*
* @возвращает строку в случае, если ключ существует
*
* @иначе возвращает false
*
*/
public function fragment($key)
{
if(array_key_exists($key, self::$fragments))
{
return self::$fragments[$key];
}
return false;
}
}
?>


В момент создания объекта класса Uri (метод __construct) мы разбираем URL на части, разделенные слешем и помещаем их в статичный массив фрагментов $fragments. Чтобы получить в дальнейшем тот или иной фрагмент, мы должны обратиться к методу fragment($key), например, так для получения самой первой части URL:
$controllerName = $uri->fragment(0). В этом примере для URL'а http://vmvc.net/articles/update переменная $controllerName получит значение "articles".

Прежде чем мы оттестируем работу этой библиотеки, давайте обратим внимание на конструкцию Class Uri extends Base в определении класса. Она говорит нам о том, что данный класс является потомком некоего класса-родителя Base. Мы его еще пока не написали, но сейчас это сделаем, поскольку в нем мы определим наиболее часто встречающиеся инструкции, которые впоследствии можно будет свободно использовать во всех его классах-потомках, как-то: быстрое создание новых экземпляров классов, присвоение свойств объектам и т.д.

<?
// базовый класс с основными методами, от которого будут наследоваться остальные
class Base {
//массив для хранения получаемых и отдаваемых значений
protected $vars = array();
 protected static $instances=array();

//базовый конструктор
protected function __construct($vars=null) {$this->vars = $vars;}
 //здесь у нас фабрика
static public function F($vars=null) { return new static($vars); }

//здесь у нас синглетон
static public function S($vars=null) {
static $instance=null;
if ($instance === null) $instance = new static($vars);
return $instance;
}
 //а здесь у нас фабрика синглетонов 
static public function G($id=null, $vars=null){
if(!$id) $id = get_called_class();
if(!isset(static::$instances[$id])) static::$instances[$id] = new static($vars);
return static::$instances[$id];
}
 //сеттер и геттер для хранения переменных
public function set($index, $value) { $this->vars[$index] = $value; }
public function get($index) { return isset($this->vars[$index]) ? $this->vars[$index] : NULL; }
}
?>


Итак, вот он наш базовый класс.

В начале класса мы определяем массивы для хранения данных (свойств) объекта. С помощью сеттеров и геттеров мы сможет быстро сохранить и получить значения этих переменных в различных частях приложения.

В функцию конструктора принимается параметр $vars, который потом записывается во внутренний массив переменных $this->vars. Таким образом, при создании экземпляра класса мы можем задать какие-либо начальные значения. Например, при создании дополнительного объекта для подключения ко второй базе данных мы можем передать через конструктор параметры подключения: хост, имя пользователя, пароль и т.д.

Шаблон проектирования “Синглетон” создает один и только один объект класса. При последующем обращении к синглетону всегда возвращается только этот объект. Например, объект базы данных должен быть создан единожды. В нем единожды создается подключение к БД и оно же потом всегда используется. Создание нескольких объектов подобного рода с генерацией новых подключений слишком накладно с точки зрения ресурсов и скорости работы, поэтому удобно использовать синглетон (подробнее с различными шаблонами проектирования вы можете познакомиться в статье "Шаблоны проектирования (design patterns) на PHP. Часть 1: фабрика, синглетон, наблюдатель, цепочка команд и стратегия" на моем сайте).

В синглетоне мы объявляем статическую переменную $instance, которая будет хранить объект класса на всем протяжении работы приложения. Если объект не был создан ранее, он создается и помещается в эту переменную. Если создан, то он уже в ней хранится и ничего делать не надо. В любом случае, в конце метод возвращает значение этой переменной.

А теперь мы проверим работу наших классов: базового Base и его потомков vmvc и Uri. Видоизменим класс vmvc:

<?
Class vmvc extends Base {
  public static function Go() {
    echo 'Контроллер: '.Uri::S()->fragment(0).', экшн: '.Uri::S()->fragment(1);
  }
}
?>

Если в адресе указаны контроллер и экшн (http://vmvc.net/articles/update), то программа выведет:
Контроллер: articles, экшн: update

Так на данном этапе выглядит файл core.php:

<?
// базовый класс с основными методами, от которого будут наследоваться остальные
Class Base {
//массив для хранения получаемых и отдаваемых значений
protected $vars = array();
 protected static $instances=array();

//базовый конструктор
protected function __construct($vars=null) {$this->vars = $vars;}
 //здесь у нас фабрика
static public function F($vars=null) { return new static($vars); }

//здесь у нас синглетон
static public function S($vars=null) {
static $instance=null;
if ($instance === null) $instance = new static($vars);
return $instance;
}
 //а здесь у нас фабрика синглетонов 
static public function G($id=null, $vars=null){
if(!$id) $id = get_called_class();
if(!isset(static::$instances[$id])) static::$instances[$id] = new static($vars);
return static::$instances[$id];
}
 //сеттер и геттер для хранения переменных
public function set($index, $value) { $this->vars[$index] = $value; }
public function get($index) { return isset($this->vars[$index]) ? $this->vars[$index] : NULL; }
}
//класс для получения фрагментов URL (разделенных "/")
Class Uri extends Base {
//массив фрагментов URL
public static $fragments = array();
 protected function __construct()
{
//преобразуем строку URL-запроса в массив и размещаем на хранение
self::$fragments = explode('/', $_SERVER['QUERY_STRING']);
}
 /**
* @метод получения определенного фрагмента URL по ключу (порядковый номер в массиве)
*
* @доступ публичный
*
* @параметр типа string $key:ключ uri
*
* @возвращает строку в случае, если ключ существует
*
* @иначе возвращает false
*
*/
public function fragment($key)
{
if(array_key_exists($key, self::$fragments))
{
return self::$fragments[$key];
}
return false;
}
}
Class vmvc extends Base {
public static function Go() {
echo 'Контроллер: '.Uri::S()->fragment(0).', экшн: '.Uri::S()->fragment(1);
}
}
?>


Следующим этапом мы реализуем работу с контроллерами. Но прежде мы напишем еще две вспомогательные библиотеки. Первая - автозагрузчик классов. Ее назначение понятно из названия. Во-первых, она не позволит захламлять память классами, которые в данном месте работы приложения не нужны. И во-вторых, нужные классы будут загружены автоматически в момент обращения, и для этого нам не понадобится заранее прописывать их подключение через include или require.

<?
//автозагрузчик классов на основе стандартной PHP-библиотеки SPL
Class Loader {
public static function registerAutoload()
{
return spl_autoload_register(array(__CLASS__, 'includeClass'));
}

public static function unregisterAutoload()
{
return spl_autoload_unregister(array(__CLASS__, 'includeClass'));
}
/*класс включается по схеме: если в коде вызвали незагруженный ранее класс, проверяем либо в папке приложения <__APP_PATH/имяКласса/имяКласса.php>,
либо в папке lib ядра фреймворка*/
public static function includeClass($class)
{
$class = str_replace(__NAMESPACE__ . "\\","",$class);
//если это контроллер, обрубаем суффикс 'Controller' в названии класса и смотрим в папку приложения
if(strpos($class,'Controller')) {
$class = substr($class,0,strpos($class,'Controller'));
if(file_exists(__APP_PATH . '/' .$class.'/'.$class.'.php')) {require(__APP_PATH . '/' .$class.'/'.$class.'.php');}
else return false;
} else {
//иначе смотрим в папку lib ядра
if(file_exists(__VMVC_PATH . '/lib/' . strtr($class, '_\\', '//') . '.php')) require(__VMVC_PATH . '/lib/' . strtr($class, '_\\', '//') . '.php');
else return false;

}
}
}
?>


В новых версиях интерпретатора PHP наличествует библиотека SPL, через которую мы регистрируем методы, автоматически вызываемые впоследствии этой библиотекой в случае если идет обращение к неподключенному ранее классу. В зарегистрированных методах мы прописываем правила подключения этих классов, в результате чего класс подключается, и программа продолжает свою работу без предупреждений программисту. Таким образом, если мы укажем в этих методах, что контроллер articlesController должен быть прописан в файле articles.php, который должен находиться в папке articles внутри папки приложения app, то при обращении к классу и его методу articlesController->updateAction(), SPL-библиотека вызовет метод includeClass с параметром $class='articlesController'. В этом методе будет выполнено подключение нужного класса инструкцией require('app/articles/articles.php'). В самом начале мы условились, как у нас будет устроено дерево папок фреймворка и приложений к нему.

Для прочих классов (не контролллеров) будет прозведено обращение к папке lib ядра фреймворка. Т.е. например, класс моделей Model автозагрузчик будет искать в /vmvc/lib/Model.php

Обратите внимание на инструкцию $class = str_replace(__NAMESPACE__ . "\\","",$class); и на стандартную глобальную константу PHP __NAMESPACE__ , в которой хранится текущее название пространства имен. Классы могут вызываться из различных пространств имен, особенно если наш фреймворк будет использоваться как подключаемое дополнение к сторонней CMS или к стороннему фреймворку. В моем случае это была совместная работа с Drupal. И PHP приписывает через обратные слэши к названию класса то пространство имен, в котором этот класс определен. Соотв. нам нужно избавиться от этой информации, чтобы сформировать корректный путь до файла класса.

Наверх