В сети большое количество мануалов по созданию сайтов на готовой CMS или фреймворке. Однако, работая фрилансером, часто встречаю сайты на самописных системах. Программисты пишут их не от хорошей жизни. В зависимости от степени простоты(сложности) проекта чрезмерно или наоборот недостаточно, применение готовой системы, и на ее переделки уходит больше времени, чем на создание сайта с нуля. К примеру, для сайта одностраничника не нужно тяжелой системы типа Joomla или фреймворка типа Yii, а у CMS типа Texpattern может не хватить функционала. Плюс задачи, которые ставит заказчик, могут быть весьма специфичными, и достаточно тяжело реализуемыми на готовой системе.
Для примера можно взять работы с моделями в Yii. Речь идет об ActiveRecord. У Yii на официальном сайте есть отличный мануал по созданию блога. Если придерживаться его, и делать все, как написано, то через пару часов изучения, можно получить полноценный блог. С категориями, метками, пользователями и административной панелью.
Проблемы начинаются тогда, когда вы попытаетесь сделать что-то, что не входит в мануал. Вывести под статьей похожие материалы. Для этого, надо составлять реляционную модель, гораздо более сложную, чем представлена в примерах.
Как сказал один наш комик - "тут начинается вестерн". То, что в SQL бы заняло 3 строчки кода, в Active Record займет пару ночей чтения мануалов, и экспериментов. Потому что, на первый взгляд тривиальная задача, вдруг вызывает необъяснимый баг Yii, о котором слышали полтора человека и оба китайцы. Пример не надуманный, те кто программировал на Yii используя Actve Record поддержат.
Эта методология работы с данными очень удобна, когда дело касается одной таблицы. Или даже двух, когда связь идет один ко многим, по одному полю. Настоящий ад начинается, когда нужно получить данные из нескольких таблиц.
В мире CMS тоже далеко ходить не надо. При работе над модулем Яндекс карт для Joomla нужно было подключить в админке сайта javascript файл. Недельное изучение системы ничего не дало. Такого функционала в модулях попросту нет. Надо сказать, что я выкрутился используя функционал расширенных полей подключил нужный файл. Но то, сколько времени у меня на это ушло, несоизмеримо с тем, если бы система была построена по моим законам, и я знал, что и где в ней подключается.
Об этом расскажу в этой статье. Как написать php фреймворк с нуля. Опишем основные техники проектирования MVC фреймворков на чистом php без использования сторонних библиотек.
Фреймворки пишут такие же программисты, как и вы. Нет ничего невозможного. Написать для себя каркас на котором потом можно будет строить конструкции нетипичных сайтов.
Хочу предостеречь вас, не стоит писать свой велосипед, без особой надобности и опыта. Большую часть кода придется писать руками. Тут не будет готовых модулей и расширений.
Однако и плюсы тоже есть, система будет полностью под вашим контролем. Никаких ограничений API и требований системы. Только чистый vanilaPHP и ничего лишнего.
Оставим лирику, приступим наконец к коду.
Исходные материалы к статье скачать и посмотреть на github. Статьи по этой теме будут публиковаться с тегом ideal, а последнюю версию фреймворка можно будет найти на gihub
Точка входа
Любой сайт, CMS или фреймворк начинается с точки входа. Обычно это index.php в корне сайта. Но мы не будем так делать. Чтобы программист, который работает с нашим фреймворком сразу разобрался, что все данные идут через точку входа, назовем файл main.php. Тогда будет очевидно - все запросы перенаправляются через .htaccess
Код .htaccess
AddDefaultCharset utf-8 RewriteEngine on php_value upload_max_filesize 50M php_value post_max_size 50M php_value display_errors 1 DirectoryIndex main.php?controller=index ErrorDocument 404 /main.php?controller=error RewriteRule ^index.html$ main.php RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ main.php?route=$1 [L,QSA]
Первые 8 строк - это вспомогательные настройки, которые пригодятся для фреймворка в будущем. Мы установили кодировку utf-8 по умолчанию, сайт будет работать на ней. Включили модуль apache Rewrite, для того чтобы перенаправить все не статичные запросы на main.php. Это и делают 3 последние строчки. Вся строка запроса, которая идет после домена, будет передана в переменную $_GET['route']. Т.е. запрос http://sitename.ru/kolesa/perellli/?id=5 превратится в http://sitename.ru/main.php?route=kolesa/perellli/&id=5
Дальнейшие манипуляции с разбором URL производятся с помощью PHP. Это распространенный прием, вы легко можете воспользоваться им в других системах. Так работает и Yii. Но наш фреймворк в отличие от Yii без .htaccess работать не сможет.
Файл main.php не должен делать много, он лишь определит константы путей, подключит фреймворк и запустит приложение
<?php define('ROOT',dirname(__FILE__).'/'); define('IDEAL',dirname(__FILE__).'/ideal/'); define('APP',dirname(__FILE__).'/application/'); include IDEAL.'framework.php'; app::gi()->start();
Структура
Теперь продумаем структуру нашего фреймворка. Удобно, чтобы его файлы лежали в отдельной папке. Сделаем такую структуру
- application -- controllers -- models -- views -- config.php - ideal -- classes -- framework.php -- config.php .htaccess main.php
в папке ideal будет лежать наш идеальный =) движок, а в папке application файлы пользователя.
в файле framework.php определим автозагрузчики классов, для того чтобы не писать руками каждый раз include 'classname.php'; Я уже писал про эту технику
<?php function class_autoload($class_name) { $file = IDEAL . 'classes/'.$class_name.'.php'; if( file_exists($file) == false ) return false; require_once ($file); } function controller_autoload($class_name) { $file = APP . 'controllers/'.$class_name.'.php'; if( file_exists($file) == false ) return false; require_once ($file); } function model_autoload($class_name) { $file = APP . 'models/'.$class_name.'.php'; if( file_exists($file) == false ) return false; require_once ($file); } spl_autoload_register('class_autoload'); spl_autoload_register('controller_autoload'); spl_autoload_register('model_autoload');
в этом файле будут еще и другие системные действия. Но пока хватит и этого.
Автозагрузчики срабатывают при использовании класса. Сперва ищется в папке фреймворка, потом в папке контроллеров, затем в моделях. Соответственно называть контроллер также, как и класс, или модуль нельзя.
Теперь опишем класс App, он находится в папке classes движка.Его метод start и будет запускать наше приложение.
<?php class App extends Singleton{ function start(){ Router::gi()->parse(); $controller = app::gi(Router::gi()->controller.'Controller'); $controller->__call('action'.Router::gi()->action); } }
Класс наследует абстрактный класс Singleton. Удобство в том, что экземпляр любого класса, который наследует от Singleton, можно получить из любого места программы через его метод gi() К примеру, экземпляр нашего приложения можно получить App::gi(). Этот метод вернет единственный экземпляр класс App. Один экземпляр может создать проблему, когда нужно использовать несколько баз данных. Поэтому к классу db его лучше не применять.
В коде появляются класс Router. Он будет парсить переменную $_GET['route'] и и возвращать контроллер и action - т.е. метода этого контроллера. Так как пока мы пишем лишь каркас, то этот класс ничего делать не будет. Лишь заполнит свои соответствующие поля.
<?php class Router extends Singleton{ public $action = 'index'; public $controller = false; function parse(){ if( isset($_REQUEST['controller']) ) $this->controller = $_REQUEST['controller']; if( isset($_REQUEST['action']) ) $this->action = $_REQUEST['action']; } }
Далее используя метод App::gi() как фабрику, создается экземпляр класса контроллера. А затем вызывается его метод. Класс контроллера наследуется от класса classes/Controller.php
<?php class Controller extends Singleton{ function __call( $methodName,$args=array() ){ if( is_callable( array($this,$methodName) ) ) return call_user_func_array(array($this,$methodName),$args); else throw new Except('In controller '.get_called_class().' method '.$methodName.' not found!'); } }
Каркас фреймворка готов. Теперь создадим один жизненно важный контроллер application/controllers/UserController.php
Пользователи нужны в любой системе.
<?php class UserController extends Controller{ function actionIndex(){ $model = new User(); include ROOT.'application/views/user/index.php'; } }
Он пока ничего не делает. Лишь создает модель User и выводит свое представление на экран. Модель User наследует класс classes/Model.php
<?php class Model{ private $data = array(); function __get($name){ return isset($this->data[$name])?$this->data[$name]:null; } function __set($name,$value){ $this->data[$name] = $value; } }
и код модели application/models/User.php
<?php class User extends Model{ public $name = 'Valeriy'; }
Как видите он тоже ничего особого не делает.
Содержание application/views/user/index.php может быть любым. Но чтобы продемонстрировать взаимодействие контроллера и представления оно будет таким
Hello <?=$model->name?>!!!
Таким образом структура нашего фреймворка примет такой вид
- application -- controllers --- UserController.php -- models --- User.php -- views --- user ---- index.php -- config.php - ideal -- classes -- framework.php -- config.php .htaccess main.php
Запустив в адресной строке
http://sitename/index.html?controller=user&action=index в браузере мы увидим заветное
Hello Valeriy!!!
Вам сейчас кажется, что все это ерунда и можно было сделать все это проще. Но если вдуматься, у нашего фреймворка огромные возможности. Он полностью структурирован. Расширить его возможности не составит большого труда. Добавляем класс для работы с БД и класс для работы с шаблонами и вот у нас полноценный фреймоворк. Самое важное, что вы поймете написав такой велосипед, это принцип работы MVC фреймворков таких как Zend или Yii. А понимание принципов работы это 99% процентов успеха в любой разработке.
Давно хотелось создать что-нибудь подобное. В дальнейшем планирую развивать его, добавляя новые функции и сделать действительно удобный функционал.
Данный цикл статей продолжиться. Следите за новостями. Желаю удачных велосипедов.
Комментарии
Вот только есть несколько вопросов:
1) Ждать ли продолжение и когда?
2) Осталось непонятным роль и использование файлов конфига в приложении.
Сам пишу фреймворк, используя современные концепции, такие как namespace и DI. В моем случае индекс уже был создан, там создается экземпляр класса Application и в его конструктор передается файл конфига. Было бы замечательно, если бы вы развили тему с конфигом в конструкторе и написали о DI.
Спасибо заранее.
Объясните пожалуйста строчку $controller = app::gi(Router::gi()->controller.'Controller');
после ее выполнения var_damp($controller) показывает object(app)#1 (0) { }
соответственно следующая строка $controller->__call('action_'.router::create()->ac tion); не выполняется потому что в классе app нет метода __call()
вот про шаблонизатор xdan.ru/kak-napisat-svoj-frejmvork-na-php-chast-3.html
\ideal\classes\Config.php
в статье об этом ни слова
Написал свой класс My сделал его наследуемым от сингл тона.
Не фига во вью не вызывается. Толи лыжы не едут толи ...
вот с таким кодом:
class My extends Singleton{
function kuku(){
return 1111111111;
}
}
если следовать перу автора то я его должен поч вызвать в любом месте типо такого
echo app::gi()->kuku();
возникает ошибка
Fatal error: Call to undefined method App::kuku() in ...
Fatal error: Call to undefined method App::__call() in \newsite.lc\ideal\classes\App.php on line 8
Internal Server Error
The server encountered an internal error or misconfiguration and was unable to complete your request.
Please contact the server administrator, .....@localhost and inform them of the time the error occurred, and anything you might have done that may have caused the error.
More information about this error may be available in the server error log.
Где может быть моя ошибка???
не понимаю данные строчки в App.php
$controller = App::gi(Router::gi()->controller.'Controller'); в скобках получаем controllerfalse (догадываюсь что получаем объект контроллер но как не понял и зачем этот параметр тоже не понял)
$controller->__call('action'.Router::gi()->action); - вызываем метод __call с параметром actionindex - зачем (и кстати из за него ошибка:
Fatal error: Call to undefined method App::__call() in W:\domains\localhost\core\baseApp.php on line 10 (я немного изменил структуру так как мне нужно поэтому мето вызова другое)
содержимое baseApp:
2 Поменяйте index на Index
class App extends Singleton{
public $config = "0";
function start(){
$this->config = include CONF.'config.php'; //полключаем конфиг
Router::gi()->parse();
$cn = Router::gi()->controller; $controller = ucfirst($cn);
$ac = Router::gi()->action; $action = "action_".$ac;
if(method_exists($controller, $action)) {
$controller = new $controller;
$controller->$action();
}
else{
//вывести страницу по дефолту
$cn = Router::gi()->def_controller; $controller = ucfirst($cn);
$ac = Router::gi()->def_action;$action = "action_".$ac;
if(method_exists($controller, $action)) {
$controller = new $controller;
$controller->$action();
}
}// END ELSE СТРАНИЦЫ ПО ДЕФОЛТУ
}//END START
}
А в парсере тоже устроил по другому (решил что буду только ЧПУ учитывать все остальное скидывать в дефолт):
собственно вот так я сделал
сейчас вопрос по двум пунктам: проработка views и шаблонов к ним, а так же общая авторизация и вопрос загрузки не загрузки контроллеров в случае запрета этого дела
У меня при запуске возникает та же ошибка, ч то и у Fedya
Fatal error: Call to undefined method App::__call() in /Applications/MAMP/htdocs/framework.loc/myframe/cl asses/app.php on line 7
В контроллере метод __call описан уже
Скачал с гита фреймворк, поставил на хост и получил ошибку:
Internal Server Error
The server encountered an internal error or misconfiguration and was unable to complete your request.
С чем это связано?
Например actionUser($id){}?
Просто не понял как вызывается метод Controller::__call с параметрами в функции call_user_func_array()
Код:
$controller->__call('action'.Router::gi()->action);
В то же время, из-за этого magic метода is_callable всегда возвращает true:
Код:
function call($methodName, $args = array()) { if (is_callable(array($this, $methodName))) return call_user_func_array(array($this, $methodName), $args); else throw new Except('In controller ' . get_called_class() . ' method ' . $methodName . ' not found!'); }
, что приводит к зацикливанию, в случае, если запрос в адресной строке будет, например, /index.html?controller=user&action=index2
(срабатывает магия __call, которая всегда будет пытаться запустить метод, которого нет в контексте)..
Server error!
The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there was an error in a CGI script.
If you think this is a server error, please contact the webmaster.
Error 500
testsite.com
Apache