Pull to refresh

Особенности национального (создания фреймворков)

image

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

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

Начать пожалуй стоит с файловой системы. На данный момент все выглядит следующим образом:

image

В папках 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 никто ничего такого от меня и не ждет, так что в школе должно выстрелить. Если вдруг по каким то причинам это опубликуют и это даже наберет какой то фидбек, если я не загнусь от тоски ведь я живу в нереальной дыре где ноль перспектив и рабочих мест для 16 летнего программиста не обленюсь, то в следующей статье сделаю нормальную работу с куками и сессиями и всем таким для регистрации и запоминания пользователя, добавлю нормальную обработку ошибок в один класс и нормальные проверки (если вы заметили, почти нигде нет проверок, есть ли такой класс или нет, и все такое, так как предполагается что я не собираюсь объявлять контроллер, которого нет, а кроме меня этим фреймворком никто пользоваться не станет). Всем добра, если у вас есть какая то конструктивная критика или пожелания или идеи, милости прошу в комментарии.
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.