
Здравствуйте! Вышло так, что скоро у меня защита проекта в 10 классе (в конце декабря). А так как все мои достижения можно пересчитать по пальцам одной руки, я решил сваять свой сайт-электронный дневник, со всякими фичами и приколами, в общем то, что у меня получается максимально хорошо. Уже в процессе выбора, как собственно делать то этот проект, зародилась крутая идея запилить свой максимально легковесный php-фреймворк, о чем я и хотел вам поведать...
Важно!
Я не имею какого либо специального образования, так что просьба не ругаться на какие то очевидные для программиста (наверное) ошибки, а просто на них указать, спасибо. Так же ни в коем случае не стоит воспринимать это как пособие или гайд. Здесь я просто расскажу о проблемах, которые у меня возникли в процессе создания фреймворка, и о своих впечатлениях от процесса.
Начать пожалуй стоит с файловой системы. На данный момент все выглядит следующим образом:

В папках app хранится основная часть фреймворка, в resources ресурсы (скрипты js, стили и картинки). В свою очередь, папка app/components содержит классы, которые пользователь никогда трогать не будет (автозагрузка, ядро, родительские классы и методы базы данных). Просто в app лежат папки с классами посредника(middleware), модели, контроллеров и вида. Как видите, структура проста как три копейки. Перейдем к следующей части.
Автозагрузка
Я либо гений, либо идиот, но везде где бы я ни смотрел, автозагрузкой не пользовались, а предпочитали использовать namespace. Я сделал это по другому, смотрите. Так как у меня несложная файловая система, то все пути по которым могут лежать классы, можно записать в виде массива в отдельном файле, app/components/autoload/classpathes.php:
$pathes = [
'app/components/autoload',
'app/components/Kernel',
'app/components/PapaClasses',
'app/components/database',
'app/components/Options',
'app/Middleware',
'app/Models',
'app/Controllers',
'app/Views',
];
return $pathes;
Окей, теперь функция автозагрузки, в отдельном файле app/components/autoload/Autoload.php:
<?php
spl_autoload_register(function($class) {
$pathes = include 'classpathes.php';
foreach($pathes as $path) {
if (file_exists($path.'/'.$class.'.php')) {
include $path.'/'.$class.'.php';
}
}
});
Как видите, элементарная функция, загружает файл classpathes.php, который передает массив доступных путей, затем скрипт просто перебирает пути, и если класс найден, то он его загружает, все. Таким образом мне не нужно нигде писать namespace и use, и я могу любой класс использовать в любом месте приложения. Я искренне не вижу тут подвоха, но если он есть, укажите на него пожалуйста, спасибо.
Точка входа в приложение
Это, очевидно, файл index.php. Выглядит следующим образом:
require 'app/components/autoload/Autoload.php';
Kernel::start('Dd');
Кода мало, зато каков код! Первая строчка, подключение автозагрузки, а вторая собственно запускает приложение (переданный параметр — название модели, которая будет использована в приложении, не передадим ничего — модели не будет). .htaccess немного необычный, так как когда то еще в первой версии фреймворка возникла забавная проблема: если браузер не мог найти какой-либо ресурс (картинку, фавикон или скрипт), он почему-то второй раз вызывал index.php, из-за чего дублировалось все, и запросы в базу данных, и текст. Удалось решить проблему во-первых разумеется догрузкой необходимых ресурсов, во-вторых следующим содержимым .htaccess:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
# Next line is to solve the Chrome and favicon.ico file issue.
# Without it browser sends two requests to script.
RewriteCond %{REQUEST_FILENAME} !favicon.ico
# RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
Роутинг и ядро
Охохохохо, ну и намучался же я с этим роутингом, 7 раз переписывал, каждый раз недовольный результатом и желая добавить новые возможности для красивого и аккуратного app/web.php (вы вообще заметили, что в моем фреймворке много чего из ларавель)? Я подумал и не стал изобретать велосипед, а просто повторил роутинг из ларавеля, ну точнее его часть). Кстати, выполняются запросы на добавление новых адресов тоже к ядру, так как мне было лень подружить между собой классы Route и Kernel, я просто весь функционал роутинга пихнул в ядро, за-то посмотрите что мой роутинг умеет (app/web.php):
Kernel::new('/', 'TestController/show')
простейший запрос на привязку к адресу '/' метода show контроллера TestController
Kernel::new('/account', function() {
echo 'Страница account';
});
думаю вы догадались
Kernel::new('/account/{name}', function($name) {
echo 'Здравствуй, '.$name;
});
нет нет, я не разбирал код ларавел, а писал обработку этого добра сам. Как видите, мой фреймворк поддерживает переменные в запросе
Kernel::new('/feed/{name}/type/{type}/data/{data}', 'FeedController/moredata');
мнооого переменных
Kernel::middleware('checkuser', ['/']);
ура, посредники. Первый параметр название, второй массив адресов, на которые этот посредник будет применяться. Подобный метод можно было объявить хоть в самом начале скрипта, до объявления адресов, т.к. все обращения к методу middleware записываются в ядре в буфер, и выполняются уже после всех выполненных регистраций адресов
Kernel::middleware('checkuser', ['/account'])
стоит упомянуть, что этот метод перезаписывает результаты предыдущих вызовов, то есть теперь посредник 'checkuser' применится только к адресу '/account'
Kernel::middleware('bababooey', Kernel::without(['/', '/account']))
создать и привязать можно к одному адресу сколько душе угодно посредников. Метод without вернет все зарегистрированные адреса для привязки к посреднику, кроме переданных ему массивом
Kernel::new('/', 'TestController/show')->middleware('bababooey');
воистину подобен чуду ум ребенка! Чтобы сделать вид, что мой фреймворк чего то стоит и что то умеет, я прикрутил возврат методом new класса controller_shape, у которого будет всего один метод, middleware. Кстати, этот middleware, в отличие от статического метода ядра, не перезаписывает а добавляет адреса к посреднику.
Как видите, мой роутинг какого-то мощного функционала по типу группового добавления адресов и всего такого не имеет, но по моим прикидкам, на обычный такой проект без приколов разных его впринципе должно хватить. Код ядра содержит целых 198 (!) строк и смысла я его выкладывать не вижу, но если вдруг вы какой-нибудь эстет плохого кода или мой будущий работодатель, то милости прошу.
app/components/Kernel/Kernel.php
class Kernel {
public static $model; //объект модели
public static $controller; //контроллер
public static $uri; //uri
public static $values = [];
public static $queue = []; //тут находятся вообще все пути и экшены, которые доступны в текущем проекте
public static $buffer = []; //тут находятся вообще все пути и экшены, которые доступны в текущем проекте с переменными
public static $chosen = []; //а тут уже только тот путь и экшн, который совпадает с uri
public static $mgroup_pathes = []; //все миддлеваре => путь
public static $chosen_middleware = []; //тут только путь => массив middlewares для подходящего к uri пути (self::$chosen)
public static $use_middleware = [];
public static function final() {
if (!empty(self::$chosen)) {
if (!empty(self::$chosen_middleware)) {
foreach(self::$chosen_middleware as $key => $value) {
$method = array_key_first($value);
Middleware::$method();
}
}
if (!empty(self::$model)) {
self::$model::init();
}
$path = self::$chosen[0];
$action = self::$chosen[1];
$empty = false;
if (stripos($path, '{') === false) {
if (is_string($action)) {
$controller = explode('/', $action)[0];
$action = explode('/', $action)[1];
$controller = new $controller;
$controller->$action();
} else {
$action();
}
} else {
if (is_string($action)) {
$controller = explode('/', $action)[0];
$action = explode('/', $action)[1];
foreach(self::$values as $valelem) {
if(empty($valelem)) {
$empty = true;
}
}
if ($empty) {
foreach(self::$values as $key => $value) {
self::$values[$key] = urldecode($value);
}
$controller = new $controller;
$controller->$action(...self::$values);
} else {
echo 404;
}
} else {
foreach(self::$values as $valelem) {
if(empty($valelem)) {
$empty = true;
}
}
if (!$empty) {
foreach(self::$values as $key => $value) {
self::$values[$key] = urldecode($value);
}
$action(...self::$values);
} else {
echo 404;
}
}
}
} else {
echo 404;
}
}
public static function start($model = '') {
self::$uri = $_SERVER['REQUEST_URI'];
if (!empty($model)) {
self::$model = $model;
}
include_once 'function.php';
include 'app/web.php';
self::$queue = array_merge(self::$queue, self::$buffer);
foreach(self::$queue as $qe) {
if(uri_match(self::$uri, $qe[0])) {
self::$chosen = $qe;
}
}
if (!empty(self::$use_middleware)) {
foreach(self::$use_middleware as $elem) {
$elem->go();
}
}
foreach(self::$mgroup_pathes as $elem) {
$key = array_key_first($elem);
foreach($elem[$key] as $elemlv2) {
if (self::$chosen[0] == $elemlv2) {
self::$chosen_middleware[] = [$key => $elemlv2];
}
}
}
Kernel::final();
}
public static function new($path, $action) {
if (stripos($path, '{') === false) {
self::$queue = array_merge(self::$queue, [0 => [0 => $path, 1 => $action]]);
} else {
self::$buffer = array_merge(self::$buffer, [0 => [0 => $path, 1 => $action]]);
}
$c = new controller_shape($path, $action);
return $c;
}
public static function middleware_normal($middleware_name, $pathes) {
$array = self::$mgroup_pathes;
if (empty($array)) {
$array[] = [$middleware_name => $pathes];
} else {
$key_arr = [];
$value_arr = [];
$want_to_new = true;
foreach($array as $elem) {
$key_arr[] = array_key_first($elem);
$value_arr[] = $elem[array_key_first($elem)];
}
foreach($key_arr as $key => $key_elem) {
if ($key_elem == $middleware_name) {
$value_arr[$key] = $pathes;
$want_to_new = false;
}
}
$new_arr = [];
foreach($key_arr as $num => $key) {
$new_arr[] = [$key => $value_arr[$num]];
}
if ($want_to_new) {
$new_arr[] = [$middleware_name => $pathes];
}
$array = $new_arr;
}
self::$mgroup_pathes = $array;
}
public static function middleware_normal_1($middleware_name, $pathes) {
$array = self::$mgroup_pathes;
if (empty($array)) {
$array[] = [$middleware_name => $pathes];
} else {
$key_arr = [];
$value_arr = [];
$want_to_new = true;
foreach($array as $elem) {
$key_arr[] = array_key_first($elem);
$value_arr[] = $elem[array_key_first($elem)];
}
foreach($key_arr as $key => $key_elem) {
if ($key_elem == $middleware_name) {
$temp = diff($value_arr[$key], $pathes);
$value_arr[$key] = array_merge($value_arr[$key], $temp);
$want_to_new = false;
}
}
$new_arr = [];
foreach($key_arr as $num => $key) {
$new_arr[] = [$key => $value_arr[$num]];
}
if ($want_to_new) {
$new_arr[] = [$middleware_name => $pathes];
}
$array = $new_arr;
}
self::$mgroup_pathes = $array;
}
public static function middleware($middleware_name, $pathes) {
$um = new use_middleware_shape($middleware_name, $pathes);
self::$use_middleware = array_merge(self::$use_middleware, [$um]);
}
public static function middleware_1($middleware_name, $pathes) {
$um = new use_middleware_shape($middleware_name, $pathes, true);
self::$use_middleware = array_merge(self::$use_middleware, [$um]);
}
public static function without($excep_pathes) {
$func = function($excep_pathes) {
$pathes = [];
foreach(self::$queue as $qe) {
$pathes[] = $qe[0];
}
$arr = array_diff($pathes, $excep_pathes);
return $arr;
};
return ([$func, $excep_pathes]);
}
}
Контроллеры, модели и виды
Вообще ничего особенного или сложного, модель указывается в index.php, контроллер и экшн в роутинге, отрисовкой страницы занимается View::render, который ничего особо из себя не представляет:
class View {
public static function render($title, $page, $layout = 'default') {
if (file_exists('app/Views/layouts/'.$layout.'.php')) {
if (file_exists('app/Views/pages/'.$page.'.php')) {
ob_start();
include 'app/Views/pages/'.$page.'.php';
$content = ob_get_clean();
include 'app/Views/layouts/'.$layout.'.php';
}
}
}
}
Вызвать этот метод можно где угодно, в контроллере или в функции, которую вы установили в роутинге. Модель же использует написанные вами собственноручно методы по работе с базами данных. Например:
class Dd {
public static function checkUser($name) {
$result = Db::prep_query('SELECT COUNT(*) FROM user WHERE BINARY name=?', [$name]); //самописный метод оболочки для подготовленных запросов
if ($result[0]['COUNT(*)'] == 0) {
return false;
else {
return true;
}
}
}
Вызвать это можно где угодно очевидно так: Dd::checkUser('Vasya').
Конец
Вот и все собственно, подобного рода работа заняла у меня всего пару дней, но подарила массу нового опыта и ощущений — когда все заработало как надо, плясать хотелось от радости.
Я не питаю никаких иллюзий, цена такому достижению весьма сомнительна, но я думаю в 16 никто ничего такого от меня и не ждет, так что в школе должно выстрелить. Если вдруг по каким то причинам это опубликуют и это даже наберет какой то фидбек, если я