
Я хочу привести простой и рабочий шаблон проектов, с которым любой новичок в программировании на PHP сможет создать свое веб приложение и заодно втянуться в тему MVC.
Статья ориентирована на новичков, т.е. ничего нового в ней нет, просто несколько идей собраны в рабочий проект, решающий большинство задач.
Проект начинается со структуры. Простая и логичная структура – залог
Подробно, что такое MVC, можно ознакомиться тут и тут.
А если кратко — это разделение кода на Модель(логику), Представление и Контроллер.
Чуть подробнее
Контроллер реагирует на действия пользователя. Это некоторый управленец, он не умеет ни создавать, ни преподнести товар, зато знает, кто умеет создавать (модель), и преподносить (представление). Контроллер не обрабатывает данные, максимум складывает результат из нескольких вызовов модели.
Модель содержит в себе логику и данные приложения. Некий Сан Саныч, что у него контроллер закажет, то он и со склада достанет, по необходимости доработав напильником. Модель взаимодействует с базой данных, и обрабатывает результат. Важно обрабатывать данные в модели, на эту тему есть статья https://habrahabr.ru/post/175465/ . Если кратко, когда логика в моделях, её проще пере использовать как внутри проекта, так и переносить в другие.
Представление – ну сущий продавец. Не слишком смышлёный и знает только то, что доверил кон��роллер. Задача представления – только представить информацию. В него входит вёрстка (шаблоны).
Модель содержит в себе логику и данные приложения. Некий Сан Саныч, что у него контроллер закажет, то он и со склада достанет, по необходимости доработав напильником. Модель взаимодействует с базой данных, и обрабатывает результат. Важно обрабатывать данные в модели, на эту тему есть статья https://habrahabr.ru/post/175465/ . Если кратко, когда логика в моделях, её проще пере использовать как внутри проекта, так и переносить в другие.
Представление – ну сущий продавец. Не слишком смышлёный и знает только то, что доверил кон��роллер. Задача представления – только представить информацию. В него входит вёрстка (шаблоны).
В архитектуре MVC важно, что компоненты ничего не знают друг о друге, а потому – независимы. Если мы захотим поменять внешний вид формы на сайте, это должно затрагивать только представление. Если вносим изменения в базу данных, то это должно касаться только модели.
Плюсы такой архитектуры: можно тестировать компоненты по отдельности – проще искать баги, поправить вёрстку на сайте сможет и дизайнер, логику можно будет перенести и в другой проект.
Я предлагаю в качестве архитектуры простого приложения использовать:

Контроллеры отдельно, модели отдельно, представление сложено по папкам (в папке base универсальные пере используемые шаблоны, такие как пагинация). В папке s – ресурсы: картинки, JavaScript и стили.
В первую очередь правила для сервера в htacess:
#Кодировка AddDefaultCharset utf-8 RewriteEngine on RewriteBase / #Запрет перенаправления если файл есть RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d #Перенаправление со страниц вида /employee/ RewriteRule ^([A-z]*)\/$ ?type=page&controller=$1&action=main [L,QSA] #Обращение к контролеру RewriteRule ^([A-z]*)\/([A-z]*)\/$ ?type=page&controller=$1&action=$2 [L,QSA] #Ajax RewriteRule ^ajax\/([A-z]*)\/([A-z]*)\/$ ?type=ajax&controller=$1&action=$2 [L,QSA] ErrorDocument 404 /404.html
У приложения будет единая точка входа (то есть все запросы к сайту, за исключением файлов, будут перенаправлены на 1 скрипт) – index.php. В нём:
<? session_start(); //Определяем тип запроса $type = $_REQUEST['type']; //Подключаем настройки проекта include $_SERVER['DOCUMENT_ROOT']. "/config.php"; //Загружаем базовые функции include $_SERVER['DOCUMENT_ROOT']."/model/functions.php"; //Загружаем класс для работы с базой данных include $_SERVER['DOCUMENT_ROOT']."/model/db.php"; //Загружаем реестр include $_SERVER['DOCUMENT_ROOT']."/model/reestr.php"; //Сохранение данных запроса r::g('Controller_Main')->setRequest($_REQUEST); if ($type == 'ajax') { r::g('Controller_Main')->ajax($_REQUEST['controller'], $_REQUEST['action'], $_REQUEST['send']); } else { r::g('Controller_Main')->page($_REQUEST['controller'], $_REQUEST['action'],$_REQUEST); }
У нашего приложения будет 2 типа запросов: страницы и ajax запросы, оба типа обрабатываются в main контролере, соответствующими функциями.
Файл конфига
<?
//Настройки подключения к базе MySQl
define(«sql_user», «root»);//Логин
define(«sql_host», «localhost»);//Адрес сервера
define(«sql_pass», "");//Пароль
define(«sql_table», «loumvc»);//Таблица базы данных
define(«title», «Лёгкий MVC»);//Базовый тайтл
//Настройки подключения к базе MySQl
define(«sql_user», «root»);//Логин
define(«sql_host», «localhost»);//Адрес сервера
define(«sql_pass», "");//Пароль
define(«sql_table», «loumvc»);//Таблица базы данных
define(«title», «Лёгкий MVC»);//Базовый тайтл
Классы будем подгружать при инициализации, с помощью автозагрузчика. Т.е при инициализации класса new Controller_main(), при условии что файл с таким классом не подключен, 'Controller_main' будет передано в нашу функцию загрузчик, а она разобъёт название класса по "_" и попытается подключить файл /сontroller/main.php. Если не удастся — перенаправит пользователя на 404 страницу. Объявление и подключения автозагручика, пропишем в файле /model/function.php, который мы подключили вручную.
<?php function autoload($class_name) { $class_name = mb_strtolower($class_name); $arPath = explode("_", $class_name); if (file_exists($_SERVER['DOCUMENT_ROOT'] . "/" .implode("/", $arPath) . ".php")) { include_once($_SERVER['DOCUMENT_ROOT'] . "/" .implode("/", $arPath) . ".php"); }else{ //Не найден контроллер r::g('Controller_main')->error404(); die(); } } //Прописываем автозагрузку классов по необходимости spl_autoload_register('autoload');
Ещё в файле необязательные, но весьма полезные функции дебага
//Простейший дебаг, принтит все переменные в него переданные function pr() { $args = func_get_args(); foreach ($args as $item) { echo "<pre>"; print_r($item); echo "</pre>"; } } //Логирование данных в базу function dlog($name,$text){ if(!is_string($text)){ $text = print_r($text,true); } db::insertArr('log',array( 'name'=>$name, 'val'=>$text ),1); }
Для работы с базой данных, класс со статическими функциями. Его можно будет использовать в любом месте программы. (Это сделано чтобы проще было использовать функцию логирования, обращаться к базе данных вне модели – не стоит.).
Код
<? class db{ //Соединение с БД private static $connect = null; //Последний добавленный id private static $lastId = 0; //Логирование private static $log = 0; //Запрос к базе данных static function query($query,$skipLog=0){ if(self::$connect==null){ self::$connect = new mysqli(sql_host, sql_user, sql_pass, sql_table); if(self::$connect->connect_errno){ die('База данных не доступна'); } } //При включённом логировании, id затрётся запписью лога, поэтому вручную его запишем и передадим self::$lastId = 0; $res = self::$connect->query($query); $lastId = self::$connect->insert_id; //При включённом логировании, логируем каждый запрос,исключая запись лога if(!$skipLog && self::$log){ dlog('query',$query); } self::$lastId = $lastId; return $res; } static function insertArr($table,$arr,$skipLog=0){ $sql = "INSERT INTO `$table` "; //Экранируем переменные массива foreach($arr as $key=>$val){ $val = self::$connect->mysqli_escape_string($val); } $sql .= "(`".implode("`,`",array_keys($arr))."`) VALUES ('".implode("','",array_values($arr))."')"; self::query($sql,$skipLog); return self::insertId(); } //Возвращает id последней добавленной записи static function insertId(){ return self::$lastId; } static function insert($query){ self::query($query); //Возвращает id последней добавленной записи return self::insertId(); } static function selectCell($query){ $row = self::selectRow($query); if($row){ return reset($row); } } static function selectRow($query){ $res = self::select($query); if($res){ return reset($res); } } static function select($query){ $res = self::query($query); $mas = array(); if($res) { while ($row = $res->fetch_assoc()){ if($row['ARRAY_KEY']){ $key = $row['ARRAY_KEY']; unset($row['ARRAY_KEY']); $mas[$key]=$row; }else{ $mas[]=$row; } } return $mas; } } static function update($table,$mass,$id){ $sql = "UPDATE `$table` SET "; foreach($mass as $key=>$it){ $it = self::$connect->mysqli_escape_string($it); $sql .= " $key='$it',"; } $sql = substr($sql, 0,-1); $sql .= " where id = $id"; return self::query($sql); } }
Вместо
Код
class db { protected static $connect = null; static function getConnect(){ if(self::connect == null){ require($_SERVER['DOCUMENT_ROOT'] . '/libs/DbSimple/Generic.php'); self::$connect = DbSimple_Generic::connect("mysql://" . sql_user . ":" . sql_pass . "@" . sql_host . "/" . sql_table); if(!self::$connect){ die("Нет подключения к бд"); } self::$connect->query("SET NAMES UTF8"); } return self::$connect; } //Меняем функции query static function query() { $res = call_user_func_array(array(self::getConnect(), 'query'), func_get_args()); return $res; } //Меняем функции select static function select() { $res = call_user_func_array(array(self::getConnect(), 'select'), func_get_args()); return $res; } //По умолчанию вызовы пойдут в библиотеку static function __callStatic($name, $arguments) { if(method_exists(self, $name)){ return call_user_func_array(array(self, $name), $arguments); }else{ return call_user_func_array(array(self::getConnect(), $name), func_get_args()); } } }
В приложении используется реестр — реализация паттерна singleton – одиночка, хранящий все инстансы ( инициализированные объекты классов ) всех моделей и контроллеров. Благодаря ему каждый класс в проекте будет инициализирован только один раз, и мы легко получим к нему доступ. Для этого обращаться к классам надо так:
r::g("Название класса")
Реализация реестра:
<? //Реестр для сохранения инстансов объектов class r { //Объявление статической переменной доступной только в классе protected static $instance = array();//Хранилище классов //Функция получения класса static function g($name){ if(self::$instance[$name]){ return self::$instance[$name]; }else{ //Создание и сохранение объекта класса return self::add($name, new $name() ); } } //Сохранение объекта класса в хранилище static function add($name,$val){ return self::$instance[$name] = $val; } }
Каждый контроллер унаследует базовый, в котором реализуем функцию выводящую представление
<?php class controller_base{ function view($name,$args=null,$isBase=0){ ob_start(); if($isBase){ $tpl = $_SERVER['DOCUMENT_ROOT']. "/view/base/{$name}.php"; }else{ //Название вызывающего класса $class = explode("_",get_class($this)); $tpl = $_SERVER['DOCUMENT_ROOT']. "/view/{$class[1]}/{$name}.php"; } if(file_exists($tpl)){ if($args){ //Распаковка переданных в шаблонизатор переменных, в область видимости шаблона extract($args); } include $tpl; }else{ echo "no tpl {$class[1]}/{$name}"; } return ob_get_clean(); }
Ещё в базовом контролере реализуем реестр для хранения заголовка страницы, с возможностью изменить его из любого контролера.
Код
private static $title = ''; function getTitle(){ if(!self::$title){ //В конфиге прописан базовый return title; } return self::$title; } function setTitle($title){ self::$title = $title; }
И сохранение запроса
//Сохранение исходного вида запроса к приложению private static $request = null; function setRequest($param){ self::$request = $param; } function getRequest(){ return self::$request; }
Главный контролер, в него будут первично приходить все вызовы
class Controller_main extends Controller_base{ function __construct(){ $this->model = r::g('Model_main'); }
В нём, и каждом другом контроллере создаём ссылку, на его модель.Обработку запросов на построение страницы доверим функции page:
function page($controller,$action,$param){ //По умолчанию главный контроллер (индексная страница) if(!$controller){ $controller = 'main'; } // И метод main if(!$action){ $action = 'main'; } $method_name = "page_".$action; $controller_name = 'Controller_'.$controller; if(method_exists(r::g($controller_name), $method_name)){ //Создаётся центральная часть ob_start(); r::g($controller_name)->{$method_name}($param); $html = ob_get_clean(); //Выводится вся страница echo $this->view('page',array( 'html' => $html, 'title' => $this->getTitle() )); }else{ //Перебрасываем пользователя на страницу 404 $this->error404(); } }
В ней проверятся существования запрошенного метода в запрошенном контроллере, ну и при попытке подключить несуществующий контролер, выпадет 404 (это мы устроили в загрузчике классов).
Тело страницы генерируется методами page_{action}. Так и визуально сразу видно, какие функции отвечают за генерацию странницы, и методы c именем dropDataBase – защищены от вызова извне. Пример такой функции:
function page_main($param){ echo $this->view('main'); }
Код основного шаблона страницы
<html> <head> <title> <?= $title ?> </title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <script src="//ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script> <script src="/s/js/base.js"></script> <script src="/s/js/main.js"></script> <script src="/s/js/employee.js"></script> <link href="/s/css/index.css" rel="stylesheet"> </head> <body> <div class="menu"> <?= $this->widget_menu() ?> </div> <?= $html ?> </body> </html>
Код виджета меню в main контроллере, используемого в основном шаблоне
function widget_menu(){ //Достаём запрос, чтобы определит, где в меню мы находимся $request = $this->getRequest(); //Из модели получаем структуру меню, с отмеченными пунктами, где мы $menu = $this->model->getMenu($request['controller'],$request['action']); //Строим меню и возвращаем return $this->view('menu',[ 'list'=>$menu ]); }
Ajax запросы обрабатываются функцией:
function ajax($controller,$action,$send){ $method_name = "ajax_".$action; $controller_name = 'Controller_'.$controller; if(method_exists(r::g($controller_name), $method_name)){ $res = r::g($controller_name)->$method_name($send); if($res){ echo "<ja>" . json_encode($res) . "</ja>"; } } }
Она работает аналогично page, за небольшим исключением:
- Ищет методы с приставкой ajax_
- Не буферизует вывод, а сразу выводит напечатанное ajax_{action} методом
- Если ajax_{action} метод возвращает данные — сериализует эти данные и обрамляя тегами ja, печатает.
Последнее нужно, если нам надо вернуть ещё и структурированные данные в javascript. В вызываемом ajax_{action} методе надо просто вернуть массив, а дальше функция Controller_main->ajax «сама разберётся», как передать данные фронтенду.
Теги ja нужны, чтобы легко разобрать ответ на фронтенде. Получим отдельно текст, отдельно сериализованный массив. На этом сэкономим трафика и CPU, тем, что не будем кодировать/раскодировать html страницу. И ещё защищены от ошибок, никакой лишний вывод не сломает нам фронтенд.
Пример из класса сотрудников, метод открывающий форму для создания сотрудников:
А страничный метод выглядит так:
Виджет который используется в обоих случаях:
function ajax_add(){ echo $this->widgetAdd(); return ['status'=>1]; }
А страничный метод выглядит так:
function page_add(){ $this->setTitle('Добавление сотрудника'); echo $this->view('page_add',[ 'html' => $this->widgetAdd() ]); }
Виджет который используется в обоих случаях:
function widgetAdd(){ return $this->view('add',[ 'divisions'=>$this->model->getDivisions() ]); }
Настоятельно рекомендую называть шаблоны по имени той функции которую они исполняют, или того метода, в котором используются. И не только шаблоны, но и функции в js.
Получается: в javascript создаёте метод «addEmployee», он запрос посылает на «ajax_addEmployee», а в функции используется шаблон «addEmployee». Потом намного проще будет их искать. Захотите потом поправить шаблон, посмотрите в js название функции и сразу можете представление править, не надо в контроллере искать.
Немного о фронтенде:
Папка с скриптами будет похожа на папку контроллеров:

Также базовый скрипт, который будут наследовать все контроллеры JS.
function base(){ this.post = function(method,send,callback){ var controller = this.url ; var url = "/ajax/"+controller+"/"+method+"/"; return $.post(url,{ send : send },function (re) { var res = re.split('<ja>'); if (res[1] != undefined) { var text = res[0]; res = res[1].split('</ja>'); text += res[1]; re = text; var mas = $.parseJSON(res[0]); } else { var mas = {}; } if (typeof (callback) == 'function') { callback(re, mas) } }) } this.createPop = function(title, html) {} } }base = new base();
Код функции создающей окошки, дешёвый аналог fancybox, с методами и плюшками.
this.createPop = function(title, html) { //Создаём само окно, при помощи библиотеки jqary var $win = $("<div/>", { class: 'Pop' }); //Создаём box c содержимым окна var $box = $("<div/>", { class: 'winBox' }); //Верхняя часть окна, var $top = $("<div/>", { class: "winTop" }); //Заголовок окна var $title = $('<div/>', { class: "winTitle", html: title }); //Заголовок будет внутри верхней части $top.append($title); //Создаём кнопку закрытия окна, то-же поместим в верхнюю часть var $close = $("<div/>", { class: 'winClose', html: "x" }); $top.append($close); //И созадим на кнопке событие - реакцию на клик - закрытия окна $close.on('click', function () { $win.remove(); }); //Закрывает окошко $win.close = function () { $win.remove(); } // Если содержимое null, создаст сообщение, о загрузке if (html == null) { //Можно сюда впихнуть лоадер, чтоб было веселее html = "<div class='help'> Загрузка данных </div>"; } //Вставляем в тело окна содержимое $box.html(html); //Вставляем в обёртку окна верхнюю часть и тело $win.append($top); $win.append($box); //Создаём в обёртке функцию, обновления содержимого $win.update = function (html) { $box.html(html); } //ПРоверяем, есть ли уже на странице блок для окон if ($('.BoxPop').length == 0) { //Если нет - создадим его $('body').append($('<div/>', { class: 'BoxPop' })); } //Вставляем обёртку в блок для окон, окно появится для пользователя $('.BoxPop').append($win); //Возвратим объект окна для дальнейшей работы return $win; }
Собственно наследование и использование ajax:
function employee(){ //Контроллер обработчик this.url = "employee"; //Замыкание на себя var self = this; this.add = function(){ var $win = self.createPop('Добавление сотрудника',null); self.post('add',{},function(re){ $win.update(re); }) } } employee.prototype = base; employee = new employee();
И ещё, дабы защитить свой проект от доступа к скриптам, в папки controller, model и. view надо положить .htacess с содержанием:
Deny from all
Используя данный шаблон, вы сможете реализовать большинство задач, для веб приложений. И главное, любой другой программист, знакомый с MVC, сможет легко разобраться в вашем проекте.
→ Весь исходный код
