Фреймворк своими руками на чистом php​В сети большое количество мануалов по созданию сайтов на готовой 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% процентов успеха в любой разработке.

 

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

Данный цикл статей продолжиться. Следите за новостями. Желаю удачных велосипедов. 

Оставлять комментарии могут только зарегистрированные пользователи

Комментарии  

NajjZ
# NajjZ 07.12.2014 18:22
Отличная статья! Особенно для начинающих изучение работы с фреймворками.

Вот только есть несколько вопросов:

1) Ждать ли продолжение и когда?

2) Осталось непонятным роль и использование файлов конфига в приложении.

Сам пишу фреймворк, используя современные концепции, такие как namespace и DI. В моем случае индекс уже был создан, там создается экземпляр класса Application и в его конструктор передается файл конфига. Было бы замечательно, если бы вы развили тему с конфигом в конструкторе и написали о DI.

Спасибо заранее.
Leroy
# Leroy 08.12.2014 11:50
http://xdan.ru/kak-napisat-svoj-frejmvork-na-php-chast-2.html вторая часть. Пишите вопросы если что-то не понятно, может быть ответ опять будет на отдельный пост
Alex_1984
# Alex_1984 23.07.2015 15:30
Цитирую Leroy:
http://xdan.ru/kak-napisat-svoj-frejmvork-na-php-chast-2.html вторая часть. Пишите вопросы если что-то не понятно, может быть ответ опять будет на отдельный пост


Объясните пожалуйста строчку $controller = app::gi(Router::gi()->controller.'Controller');
после ее выполнения var_damp($controller) показывает object(app)#1 (0) { }
соответственно следующая строка $controller->__call('action_'.router::create()->ac tion); не выполняется потому что в классе app нет метода __call()
Dimitri Schreibär
# Dimitri Schreibär 16.02.2015 23:19
хороший пример... а на конкретном примере обяснить можно... куда подключить класс для работы с БД и класс для работы с шаблонами
Leroy
# Leroy 23.03.2015 17:08
xdan.ru/kak-napisat-svoj-frejmvork-na-php-model.html вот про БД и модели
вот про шаблонизатор xdan.ru/kak-napisat-svoj-frejmvork-na-php-chast-3.html
Dobr
# Dobr 28.03.2015 19:14
Не понятно, зачем нужен файл
\ideal\classes\Config.php
в статье об этом ни слова
xDan xdanru
# xDan xdanru 28.03.2015 19:36
читайте весь цикл статей, там есть про конфиги
Dobr
# Dobr 28.03.2015 21:00
Удобство в том, что экземпляр любого класса, который наследует от Singleton, можно получить из любого места программы через его метод gi()

Написал свой класс My сделал его наследуемым от сингл тона.
Не фига во вью не вызывается. Толи лыжы не едут толи ...
xDan xdanru
# xDan xdanru 29.03.2015 00:59
код в студию
Dobr
# Dobr 29.03.2015 13:52
к примеру я создаю в папке с классами idela\clasess My.php
вот с таким кодом:

class My extends Singleton{
function kuku(){
return 1111111111;
}
}

если следовать перу автора то я его должен поч вызвать в любом месте типо такого
echo app::gi()->kuku();
возникает ошибка

Fatal error: Call to undefined method App::kuku() in ...
Leroy
# Leroy 30.03.2015 08:19
My::gi()->kuku() в любом месте. Читайте внимательнее.
Dobr
# Dobr 30.03.2015 11:00
Из статьи этого вообще не понять. В общем я не фига не понял одни больше вопросов чем ответов. :D
Leroy
# Leroy 30.03.2015 14:05
так вы бы почитали другие статьи серии. Их уже 5. Там все подробно разжевывается и используется. Это была вводная статья, здесь мы ничего не делали, только костяк, который даже толком не работал
Fedya
# Fedya 08.04.2015 15:43
После запуска отдает ошибку (
Fatal error: Call to undefined method App::__call() in \newsite.lc\ideal\classes\App.php on line 8
Leroy
# Leroy 08.04.2015 16:22
а вы какую версию скачали?
Fedya
# Fedya 13.04.2015 17:47
Код что в этой статье. копи/паст
Leroy
# Leroy 14.04.2015 09:03
В статье приводиться код прототипа контроллера. В нем метод __call присутствует
Игорь
# Игорь 21.04.2015 19:05
Откуда берется метод gi() ,если из класса синглтон ,то какой функционал этого метода?
Leroy
# Leroy 22.04.2015 06:37
Это из класса xdan.ru/Kak-poluchit-dostup-k-jekzempljaru-klassa-na-php-iz-ljubogo-mesta-skripta.html в статье есть ссылка
idkvik
# idkvik 07.05.2015 11:49
При запуске примера получаю:

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.

Где может быть моя ошибка???
Игорь
# Игорь 13.05.2015 13:15
Спасибо очень нравится статься, помогите не пинайте сильно:
не понимаю данные строчки в 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:
Игорь
# Игорь 13.05.2015 13:35
а еще если на сайте пишешь ответить с цитатой открывается две формы для ввода текста и соответсвенно две формы для ввода проверочного кода и т.п. всего по два ))
Макс
# Макс 14.05.2015 06:17
1 Метод gi() класса Singletone фабричный и возвращает экземпляр класса имя которого передано gi() в параметрах.
2 Поменяйте index на Index
Игорь
# Игорь 14.05.2015 09:25
Огромное спасибо за ответ, выкрутился немного по другому, точнее совсем по другому))
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
}
А в парсере тоже устроил по другому (решил что буду только ЧПУ учитывать все остальное скидывать в дефолт):
Игорь
# Игорь 14.05.2015 09:26
если есть замечания по моим решениям тоже бы выслушал с удовольствием
Игорь
# Игорь 14.05.2015 09:38
https://github.com/axedream/idea.git
собственно вот так я сделал
сейчас вопрос по двум пунктам: проработка views и шаблонов к ним, а так же общая авторизация и вопрос загрузки не загрузки контроллеров в случае запрета этого дела
Leroy
# Leroy 14.05.2015 16:27
осваивайте Pull request'ы) Будем пилить Ideal всем миром. Несказанно рад, что кого-то еще интересует велосипедостроение
Игорь
# Игорь 15.05.2015 20:43
да с радость, но я уже на столько изменил вашу первоначальную идею что стоит либо создать новую ветку либо х.б.з... посмотрим что выйдет
desce
# desce 07.11.2015 20:05
Немного не понял насчет абстрактного класса Singleton. Откуда он вообще берется? Это встроенный класс php? На сайте о нем информации нет. Объясните, пожалуйтса
Leroy
# Leroy 08.11.2015 12:56
В статье даже ссылка указана. Что не понятно?
desce
# desce 12.11.2015 23:17
Да, теперь разобрался) Просто затупил.
У меня при запуске возникает та же ошибка, ч то и у Fedya
Fatal error: Call to undefined method App::__call() in /Applications/MAMP/htdocs/framework.loc/myframe/cl asses/app.php on line 7

В контроллере метод __call описан уже
Leroy
# Leroy 17.11.2015 07:29
Это ведь не метод контрола, а целиком приложения. Читайте другие статьи и скачивайте другие версии. Там исправлено все
Casper
# Casper 10.11.2015 10:50
Здравствуйте. У меня тут проблема с данным фрэймворком. На OpenServer вылетает апач. Не подскажите в чем проблема. Скачал с гита, версия 1.0.0
Leroy
# Leroy 10.11.2015 18:19
Она содержала ошибку, скачайте следующую версию.
nicroza
# nicroza 27.11.2015 12:58
Здравствуйте Leroy!
Скачал с гита фреймворк, поставил на хост и получил ошибку:

Internal Server Error
The server encountered an internal error or misconfiguration and was unable to complete your request.

С чем это связано?
nicroza
# nicroza 27.11.2015 13:09
Спасибо! Разобрался!
Mr.Andey
# Mr.Andey 21.10.2016 19:59
А как вызывать action с параметрами?
Например actionUser($id){}?
Просто не понял как вызывается метод Controller::__call с параметрами в функции call_user_func_array()
Mr.Andey
# Mr.Andey 21.10.2016 20:49
Ой, все, понял :lol:
Mr.Andey
# Mr.Andey 22.10.2016 10:38
А нет, нифига не понял) Как работать с GET запросами?)
Илья123
# Илья123 31.07.2019 19:44
Почему в классе Controller используется метод __call()? он ведь вызывается из app.php
Код:$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, которая всегда будет пытаться запустить метод, которого нет в контексте)..
gggggggggggggggg
# gggggggggggggggg 06.08.2019 23:09
У меня одного нихера не работает? Структуру так же как и весь код полностью идентичен. Пишу в адресную строку http://sitename/index.html?controller=user&action=index подставляя свое имя сайта. И получаю ошибку
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