Pull to refresh

Создание простой MVC-системы на PHP 5

Reading time 18 min
Views 208K
Original author: Dennis Pallett

Предисловие


В этом руководстве Вы узнаете, как построить простую систему по архитектуре MVC (Model-View-Controller, Модель-Отображение-Контроллер) на PHP 5.1 с использованием возможностей библиотеки SPL (Standard PHP Library, Стандартная Библиотека PHP).




Введение


Добро пожаловать в первое полноценное руководство для PHP 5 на PHPit. Вам понадобится PHP 5.1 с установленной библиотекой SPL, так как мы воспользуемся некоторыми из самых последних возможностей PHP 5.

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


Одна точка входа


Одной из важных вещей в MVC является одна точка входа в приложение вместо кучи PHP-файлов, делающих примерно следующее:

<?php
include ('global.php'); 

// Здесь код страницы

?>



У нас будет один файл, обрабатывающий все запросы. Это значит, что нам не придётся мучиться с подключением global.php каждый раз, когда нам нужно создать новую страницу. Эта «одна точка входа» будет называться index.php и на данный момент будет такой:

<?php


// Тут чего-нибудь делаем


?>



Как Вы можете заметить, этот скрипт пока ещё ничего не делает, но погодите минутку.

Чтобы направить все запросы на главную страницу, мы воспользуемся mod_rewrite и установим в .htaccess директиву RewriteRule. Вставим следующий код в файл .htaccess и сохраним его в той же директории, что и index.php:

RewriteEngine on 

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

RewriteRule ^(.*)$ index.php?route=$1 [L,QSA]


Сперва мы проверяем, существует ли запрашиваемый файл, используя директиву RewriteCond, и, если нет, то перенаправляем запрос на index.php. Такая проверка на существование файла необходима, так как иначе index.php будет пытаться обрабатывать все запросы к сайту, включая запросы на изображения. А это нам как раз и не надо.

Если у Вас нет возможности использовать .htaccess или mod_rewrite, то Вам придётся вручную адресовать все запросы к index.php. Другими словами, все ссылки должны будут иметь вид «index.php?route=[здесь-идёт-запрос]». Например, «index.php?route=chat/index».

Теперь, когда все запросы идут через одну точку входа, мы можем начать написание скрипта index.php. Первая вещь, которую мы должны сделать, это инициализация системы. Создадим директорию includes, а в ней файл startup.php (он будет у нас файлом инициализации). Вставим следующий код в index.php:

<?php

error_reporting 
(E_ALL);

if (
version_compare(phpversion(), '5.1.0''<') == true) { die ('PHP5.1 Only'); }


// Константы:

define ('DIRSEP'DIRECTORY_SEPARATOR);


// Узнаём путь до файлов сайта

$site_path realpath(dirname(__FILE__) . DIRSEP '..' DIRSEP) . DIRSEP;

define ('site_path'$site_path);


В этом примере мы объявляем некоторую константу, узнаём, где лежат файлы системы, а также проверяем, что версия PHP, ну, хотя бы, 5.1.

Следующая вещь, которую необходимо сделать, это объект Registry (журнал, реестр) для хранения глобальных значений. Он будет передаваться в отдельные объекты системы и использоваться для доступа к глобальным значениям, причём без необходимости обозначать переменные как «global» или обращаться к массиву $GLOBALS. Почитайте статью «Использование глобальных значений в PHP» для более подробной информации об объекте реестра.

Добавьте следующий код в файл startup.php после того кода, что приведён в предыдущем примере:

$registry = new Registry;


Если сейчас попробовать запустить систему, то можно увидеть следующую ошибку:

Fatal error: Class 'Registry' not found in g:\Projects\PHPit\content\simple mvc php5\demo\includes\startup.php on line 12


Это, конечно, не большой сюрприз для нас, ведь мы ещё не написали сам класс Registry. Файл с классом можно было бы просто подключить, используя функцию include() (Прим. пер.: кстати говоря, include() не такая уж и функция, а всё-таки выражение языка, управляющая структура, если смотреть по ману), но давайте воспользуемся одной из новых возможностей PHP 5: __autoload().

Волшебная функция __autoload() используется для динамической загрузки классов. Когда PHP обнаруживает несуществующий класс, он сначала вызывает функцию __autoload() и только затем выдаёт ошибку. Мы можем воспользоваться такой возможностью для загрузки классов «на лету».

Вставьте этот код перед кодом из предыдущего примера:

// Загрузка классов «на лету»

function __autoload($class_name) {

        
$filename strtolower($class_name) . '.php';

        
$file site_path 'classes' DIRSEP $filename;


        if (
file_exists($file) == false) {

                return 
false;

        }


        include (
$file);

}



Наша функция __autoload() берёт имя класса, переданное ей как аргумент, и проверяет, существует ли файл с похожим именем в директории с классами. Если файла нет, то функция просто вернёт false и выскочит фатальная ошибка. Но если файл существует, он будет загружен. Т.е. объявится необходимый класс, и никакой ошибки не будет.

Мы ещё не создали сам класс Registry, поэтому ошибка всё ещё будет появляться. Давайте же займёмся этим.


Создание класса Registry


Класс Registry используется для передачи глобальных значений между отдельными объектами. Это на самом деле довольно простой класс, в котором нужно реализовать несколько маленьких методов.

Для начала создадим директорию classes и в ней файл registry.php. Вставим следующий код в registry.php:

<?php


Class Registry {

        
private $vars = array();


}


?>



Теперь у нас есть «скелет» класса Registry и нужно нагрузить его методами. Напишем 2 метода: set(), чтобы устанавливать значения и get(), чтобы значения получать. Также можно написать метод remove() для удаления значений. Добавим эти методы в класс Registry:

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]);

}


?>



Эти методы простые, они устанавливают, получают и удаляют элементы из массива $vars, который является атрибутом класса. В методе set() мы заодно проверяем, не существует ли уже значение с указанным ключом, и, если существует, то мы генерируем исключение. Это нужно, чтобы избежать случайной перезаписи значений.

Теперь у нас есть полноценный класс Registry, но мы не будем останавливаться на этом. Воспользуемся одной из возможностей библиотеки SPL: ArrayAccess. SPL (сокращённо от Standard PHP Library, Стандартная Библиотека PHP) — это коллекция интерфейсов и классов, предназначенных для решения стандартных проблем. Один из интерфейсов SPL, ArrayAccess, может быть использован, чтобы предоставить доступ к объекту, как к обычному массиву. Посмотрим на такой пример:

<?php


$registry 
= new Registry;


// Устанавливаем некоторое значение

$registry->set ('name''Dennis Pallett');


// Получаем значение, используя get()

echo $registry->get ('name');


// Получаем значение, используя доступ как к массиву

echo $registry['name']


?>



Фокус в том, что $registry становится как бы массивом, хотя на самом деле это объект. Конечно, ArrayAccess не даёт никаких особых преимуществ, но он позволяет сократить объём кода, так как не придётся каждый раз писать «->get()». Чтобы воспользоваться этим интерфейсом, нужно исправить первую строчку класса («Class Registry») таким образом:

Class Registry Implements ArrayAccess {



Ключевое слово «Implements» говорит интерпретатору, что этим классом мы реализуем интерфейс, чем на самом деле ArrayAccess и является.

Класс, реализующий интерфейс ArrayAccess, должен иметь следующие методы:

function offsetExists($offset) {

        return isset(
$this->vars[$offset]);

}


function 
offsetGet($offset) {

        return 
$this->get($offset);

}


function 
offsetSet($offset$value) {

        
$this->set($offset$value);

}


function 
offsetUnset($offset) {

        unset(
$this->vars[$offset]);

}




Эти методы должны быть понятны сами по себе. Дополнительную информацию можно найти в документации SPL.

Теперь, реализовав интерфейс ArrayAccess, мы можем обращаться к объекту, как к обычному массиву. Это наглядно продемонстрировано, как в предыдущем примере, так и в этом:

<?php


$registry 
= new Registry;


// Устанавливаем некоторое значение

$registry['name'] = 'Dennis Pallett';


// Получаем значение, используя get()

echo $registry->get ('name');


// Получаем значение, используя доступ как к массиву

echo $registry['name']


?>



Класс Registry теперь завершён, и, если попробовать запустить систему, всё должно заработать (хотя ещё ничего не будет выводиться). Мы закончили с файлом инициализации и можно приступать к следующему шагу написания нашей MVC-системы: реализация доступа к базе данных, что в архитектуре MVC называется «Model» («Модель»).



Модель


«M» или модель – часть MVC-системы, которая отвечает за запросы к базе данных (или другому внешнему источнику) и предоставление информации контроллеру. Можно было бы загружать необходимую модель в зависимости от запроса, но я предпочитаю немного стереть границы между моделью и контроллером именно в этом месте, т.е. контроллер работает с БД непосредственно через библиотеку взаимодействия с БД, нежели чем через отдельную модель. Может быть, Вам захочется сделать это по-другому, тут дело вкуса.

Нам нужно написать код, необходимый для установки соединения с БД и поместить его в index.php. Существует множество замечательных библиотек для работы с БД (включая мою собственную, AutoCRUD), но в PHP 5 уже есть такая библиотека – PDO. Поэтому нет нужды использовать какую-либо другую.

Вставим следующий код в файл index.php (после подключения файла инициализации):

# Соединяемся с БД

$db = new PDO('mysql:host=localhost;dbname=demo''[user]''[password]');

$registry->set ('db'$db);



В этом примере мы сначала создаём новый экземпляр библиотеки PDO и соединяемся с нашей БД MySQL. Потом делаем переменную $db доступной глобально при помощи нашего класса Registry.

Модельная компонента нашей системы готова, поэтому давайте перейдём к написанию контроллера.

Написание контроллера подразумевает также и написание класса Router, ответственного за загрузку нужного контроллера в зависимости от запроса (вспомните, в index.php через URL передаётся переменная $route).



Класс Router


Класс Router будет разбирать запрос, а потом загружать требуемый контроллер. Создадим «скелет» класса:

<?php


Class Router {

        
private $registry;

        
private $path;

        
private $args = array();


        function 
__construct($registry) {

                
$this->registry $registry;

        }


}


?>



Затем добавим следующие строки в index.php:

# Загружаем router

$router = new Router($registry);

$registry->set ('router'$router);



Мы добавили класс Router в нашу MVC-систему, но он пока что ничего не делает, поэтому давай добавим в него методы, необходимые для работы.

Первая вещь, которую мы напишем, это метод setPath() для установки директории, где будут лежать все наши контроллеры. Метод выглядит следующим образом и должен быть добавлен в класс Router:

function setPath($path) {

        
$path trim($path'/\\');

        
$path .= DIRSEP;


        if (
is_dir($path) == false) {

                
throw new Exception ('Invalid controller path: `' $path '`');

        }


        
$this->path $path;

}




Потом добавим следующие строки в index.php:

$router->setPath (site_path 'controllers');



Теперь, когда мы установили путь до наших контроллеров, напишем сам метод, ответственный за загрузку контроллера. Этот метод будет называться delegate(), он и будет анализировать запрос. Первый кусочек этого метода такой:

function delegate() {

        
// Анализируем путь

        
$this->getController($file$controller$action$args);



Как Вы можете видеть, он использует ещё один метод, getController(), чтобы получить название контроллера и несколько других переменных. Этот метод выглядит так:

private function getController(&$file, &$controller, &$action, &$args) {

        
$route = (empty($_GET['route'])) ? '' $_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 DIRSEP;

                        
array_shift($parts);

                        continue;

                }


                
// Находим файл

                
if (is_file($fullpath '.php')) {

                        
$controller $part;

                        
array_shift($parts);

                        break;

                }

        }


        if (empty(
$controller)) { $controller 'index'; };


        
// Получаем действие

        
$action array_shift($parts);

        if (empty(
$action)) { $action 'index'; }


        
$file $cmd_path $controller '.php';

        
$args $parts;

}




Пробежимся по этому методу. Сначала он берёт значение переменной $route из запроса, потом разбивает его на части с помощь функции explode(). Например, запрос «members/view» преобразуется в такой массив: array(‘members’, ‘view’).

Потом при помощи цикла foreach он проходит по каждой части и проверяет, является ли эта часть директорией. Если является, то он приписывает её к пути до файла и проверяет следующую часть. Это позволяет поместить контроллеры в поддиректориях и, таким образом, получить иерархию контроллеров. Если же текущая часть запроса не является директорией, но является файлом, она сохраняется в переменную $controller, и мы выходим из цикла, так как нашёлся контроллер, который нам нужен.

После цикла мы проверяем переменную с именем контроллера. Если она пустая, то используем контроллер «index», который будет у нас контроллером по умолчанию. Потом метод определяет действие, которое необходимо выполнить. Контроллер – это класс, который состоит из нескольких методов. Действие же указывает на конкретный метод. Если действие не указано, будем использовать «index» — действие по умолчанию.

И, наконец, получаем полный путь до файла контроллера, объединяя три переменные: путь, имя контроллера и расширение «php».

Теперь, когда мы проанализировали запрос, пора вызывать метод delegate() для загрузки контроллера и выполнения действия. Полностью метод delegate() выглядит так:

function delegate() {

        
// Анализируем путь

        
$this->getController($file$controller$action$args);


        
// Файл доступен?

        
if (is_readable($file) == false) {

                die (
'404 Not Found');

        }


        
// Подключаем файл

        
include ($file);


        
// Создаём экземпляр контроллера

        
$class 'Controller_' $controller;

        
$controller = new $class($this->registry);


        
// Действие доступно?

        
if (is_callable(array($controller$action)) == false) {

                die (
'404 Not Found');

        }


        
// Выполняем действие

        
$controller->$action();

}




Проанализировав запрос при помощи метода getController(), мы проверяем, существует ли в действительности файл, и, если нет, то возвращаем простое сообщение об ошибке.

После этого мы подключаем файл с контроллером и создаём экземпляр его класса, называться который должен «Controller_[имя]». Чуть позже мы поговорим о контроллерах более подробно.

Потом мы проверяем, есть ли указанное действие (т.е. метод) и возможно ли к нему обратиться (используем для этого функцию is_callable()). Наконец, мы выполняем непосредственно само действие, на чём роль класса Router и завершается.

Написав полностью метод delegate(), добавим следующую строчку в файл index.php:

$router->delegate();



Если попробовать сейчас запустить систему, то мы увидим следующую ошибку (разумеется, если директории controllers ещё нет):

Fatal error: Uncaught exception 'Exception' with message 'Invalid controller path: `g:\Projects\PHPit\content\simple mvc php5\demo\controllers\`' in g:\Projects\PHPit\content\simple mvc php5\demo\classes\router.php:18 Stack trace: #0 g:\Projects\PHPit\content\simple mvc php5\demo\index.php(13): Router->setPath('g:\Projects\PHP...') #1 {main} thrown in g:\Projects\PHPit\content\simple mvc php5\demo\classes\router.php on line 18


Или же мы увидим ошибку «404 Not Found», так как ещё нет ни одного контроллера. Но этим-то мы сейчас и займёмся.



Контроллер


Контроллеры в нашей MVC-системе будут достаточно простыми и потребуют совсем немного времени. Во-первых, удостоверимся, что директория controllers существует. Создадим файл controller_base.php в директории classes и вставим в него следующий код:

<?php


Abstract 
Class Controller_Base {

        
protected $registry;


        function 
__construct($registry) {

                
$this->registry $registry;

        }


        
abstract function index();

}


?>



Этот абстрактный класс будет родительским классом для всех наших контроллеров. Он будет делать всего лишь две вещи: сохранять локальную копию класса Registry и при помощи абстрактного метода index() заставлять все дочерние контроллеры реализовывать этот метод.

Напишем наш первый контроллер. Создадим файл index.php в директории controllers и вставим в него такой код:

<?php


Class Controller_Index Extends Controller_Base {


        function 
index() {

                echo 
'Hello from my MVC system';

        }


}


?>



Только что мы создали наш первый контроллер и, если попробовать запустить систему, то можно увидеть следующее:


(Полный размер, 1024x357, 115 КБ)


Это означает, что класс Router выполнил свою работу и запустил требуемое действие из требуемого контроллера. Давайте напишем ещё один контроллер, который будет соответствовать запросу «members/view». Создадим файл members.php в директории контроллеров и вставим в него такой код:

<?php


Class Controller_Members Extends Controller_Base {


        function 
index() {

                echo 
'Default index of the `members` controllers';

        }


        function 
view() {

                echo 
'You are viewing the members/view request';

        }


}


?>



Теперь зайдём в нашу MVC-систему по запросу «members/view» или же «index.php?route=members/view». Мы должны увидеть такой результат:



(Полный размер, 1024x320, 117 КБ)


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

Теперь, когда у нас есть контроллеры, осталась лишь одна вещь: «V» или «View» («Отображение»).



Отображение


Как и в случае с моделями, есть несколько различных вариантов создания компоненты View в MVC-системе. Мы могли бы научить класс Router автоматически загружать ещё один файл, названный как-нибудь так: «view_{имя}.php». Но чтобы сделать руководство более понятным, напишем класс Template, который будет заниматься выводом шаблонов.

Сначала создадим файл template.php в директории classes и вставим в него следующий код:

<?php


Class Template {

        
private $registry;

        
private $vars = array();


        function 
__construct($registry) {

                
$this->registry $registry;

        }


}


?>



Теперь у нас есть основная структура нашего класс Template. Следующим шагом добавим такой код в файл index.php прямо перед строками, связанными с классом Router:

# Создаём объект шаблонов

$template = new Template($registry);

$registry->set ('template'$template);



Так как нам понадобится использовать значения из моделей и контроллеров, то напишем метод set() для установки переменных, доступных в шаблонах. Посмотрим на пример:

function set($varname$value$overwrite=false) {

        if (isset(
$this->vars[$varname]) == true AND $overwrite == false) {

                
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 
remove($varname) {

        unset(
$this->vars[$varname]);

        return 
true;

}



Методы set() и remove() достаточно простые и используются, соответственно, для установки и удаления переменных.

Займёмся написанием метода show(), который будет отображать шаблоны. Простейший путь – это создать отдельную директорию templates, где хранить все файлы шаблонов, и использовать include() для вывода шаблона. Разумеется, Ваш собственный метод show() может быть совершенно другим и загружать шаблоны из базы данных или делать что-нибудь ещё. Посмотрим на кусо
Tags:
Hubs:
+11
Comments 116
Comments Comments 116

Articles