Pull to refresh

Мини-фреймворк своими руками

Reading time 11 min
Views 13K
Недавно, прочитав про мини-фреймворк Silex, я подумал: а что в нем сложного? Попробовал написать нечто подобное и получилось довольно легко.

В основе подобных мини-фреймворков обычно лежат следущие правила для mod_rewrite:
Copy Source | Copy HTML
  1. <IfModule mod_rewrite.c>
  2.     RewriteEngine On
  3.     RewriteBase /
  4.     RewriteCond %{REQUEST_FILENAME} !-f
  5.     RewriteCond %{REQUEST_FILENAME} !-d
  6.     RewriteRule ^(.*)$ index.php [QSA,L]
  7. </IfModule>

Они перенаправляют любой запрос на index.php, в котором подключается фреймворк и происходит обработка этого запроса с вызовом функций-кэллбеков. Так что для начала напишем простую функцию для обработки адреса:
Copy Source | Copy HTML
  1. function Request($path, $callback)
  2. {
  3.     if ($path == $_SERVER['REQUEST_URI']) return call_user_func($callback);
  4. }

При вызове функция проверяет адрес, к которому обратился пользователь, и если он совпадает с переданным в функцию, то вызывает кэллбек. Это уже можно считать базовым функционалом. Однако здесь нет одной нужной вещи — извлечения переменных из адреса. Кроме того, адреса example.com/something и example.com/something функция сочтет разными. Решить это можно парсингом адреса и использованием array_filter:
Copy Source | Copy HTML
  1. function array_filter_callback_no_empty_str($value)
  2. {
  3.     return $value != '';
  4. }
  5.  
  6. function Request($path, $callback)
  7. {
  8.     // Переменные запроса для передачи в кэллбек
  9.     $args = array();
  10.  
  11.     // Разбиваем адрес, к которому обратился пользователь (URI), на части
  12.     $uri = explode('/', $_SERVER['REQUEST_URI']);
  13.     // То же самое проделываем с путем запроса
  14.     $path = explode('/', $path);
  15.  
  16.     // Удаляем пустые части обоих массивов
  17.     $uri = array_values(array_filter($uri, array_filter_callback_no_empty_str));
  18.     $path = array_values(array_filter($path, array_filter_callback_no_empty_str));
  19.  
  20.     // Если количество частей в URI и пути разное, выходим
  21.     if (count($uri) != count($path))
  22.         return false;
  23.  
  24.     // Проходим по всем частям пути запроса
  25.     for($i =  0; $i < count($path); $i++)
  26.     {
  27.         // Проверяем, является ли переменной данная часть пути
  28.         // Переменные пути оформляются в фигурные скобки, что и проверяет регулярное выражение
  29.         if (preg_match('|^\{(.*)\}$|', $path[$i], $match))
  30.         {
  31.             // Если является, то добавим эту переменную в массив
  32.             $args[$match[1]] = $uri[$i];
  33.         }
  34.         else
  35.         {
  36.             // Если часть запроса не является переменной, просто сравниваем URI с запросом
  37.             // Если они не совпадают - выходим
  38.             if ($uri[$i] != $path[$i])
  39.                 return false;
  40.         }
  41.     }
  42.  
  43.     // После всех проверок вызываем кэллбек, передавая ей массив с переменными запроса
  44.     return call_user_func_array($callback, $args);
  45. }

Теперь можно писать код примерно такого вида:
Copy Source | Copy HTML
  1. function Hello($who)
  2. {
  3.     print "Hello, $who";
  4. }
  5.  
  6. Request('/hello/{who}', Hello);

А теперь покосимся в сторону Silex и посмотрим, чего нам не достает. В первую очередь это assert'ы (проверка переменной пути на соответствие регулярному выражению), проверка метода вызова (GET, POST, PUT и т. п.) и объектно-ориентированная модель. Первые два добавить довольно просто, нужно лишь дописать пару проверок:
Copy Source | Copy HTML
  1. function Request($method, $path, $callback, $asserts = array())
  2. {
  3.     // Проверяем метод обращения к серверу.
  4.     // Если в запросе указан какой-то конкретный метод и он не совпадает с тем, который был использован, выходим
  5.     if ($method != '' && strtolower($_SERVER['REQUEST_METHOD']) != $method)
  6.         return false;
  7. <...>
  8.         // Теперь проверяем, является ли переменной данная часть пути
  9.         if (preg_match('|^\{(.*)\}$|', $path[$i], $match))
  10.         {
  11.             // Если является, то смотрим, есть ли регулярное выражение для ее проверки,
  12.             // и если есть, то проверяем на соответствие ему соответствующую часть URI
  13.             if (!isset($asserts[$match[1]]) || preg_match($asserts[$match[1]], $uri[$i]))
  14.             {
  15.                 // Если все правильно, добавим эту переменную вместе с ее значением в массив
  16.                 $args[$match[1]] = $uri[$i];
  17.             }
  18.             else
  19.             {
  20.                 // Если значение не соответствует регулярному выражению, выходим
  21.                 return false;
  22.             }
  23.         }
  24. <...>
  25. }

Впрочем, второе тоже довольно легко сделать. Передачу $method, $path, $callback сделаем в конструкторе, для assert'ов напишем отдельную функцию-обертку, а всю работу запихнем в функцию run(), не забыв подставить $this-> для вышеуказанных переменных:
Copy Source | Copy HTML
  1. class Request
  2. {
  3.     public $method; // Метод запроса (GET, POST, PUT и т. п.)
  4.     public $path; // Путь запроса
  5.     public $callback; // Кэллбек
  6.     public $asserts = array(); // Регулярные выражения для проверки переменных пути
  7.  
  8.     // Конструктор класса
  9.     public function __construct($method, $path, $callback)
  10.     {
  11.         $this->method = strtolower($method);
  12.         $this->path = $path;
  13.         $this->callback = $callback;
  14.     }
  15.  
  16.     // Добавление регулярного выражения re для проверки переменной пути с названием name
  17.     public function assert($name, $re)
  18.     {
  19.         $this->asserts[$name] = $re;
  20.  
  21.         // Возвращаем текущий экземляр класса
  22.         // Это дает возможность написания подобного кода: $reg->assert('id', '|^\d+$|')->run();
  23.         return $this;
  24.     }
  25.  
  26.     // Функция обработки запроса
  27.     public function run()
  28.     {
  29. <...>
  30.     }
  31. }

Вроде бы все красиво, за исключением двух моментов. Первый момент чисто эстетический — теперь для обработки одного запроса надо написать две строки кода:
Copy Source | Copy HTML
  1. $req = new Request('/user/{id}', UserProfile);
  2. $req->assert('|^\d+$|')->run();

Но к счастью, PHP позволяет использовать одинаковые названия для классов и функций, так что напишем функцию-оберку, которая будет создавать и возвращать экземпляр класса:
Copy Source | Copy HTML
  1. function Request($method, $path, $callback)
  2. {
  3.     return new Request($method, $path, $callback);
  4. }

Теперь можно опять писать все компактно и удобно:
Copy Source | Copy HTML
  1. Request('/user/{id}', UserProfile)->assert('|^\d+$|')->run();

Второй неприятный момент заключается в отсутствии какого-либо единого центра, где запросы будут храниться и обрабатываться. Зачем нам вызывать функцию run() постоянно? Но даже если сделать подобный единый центр, то будет гораздо проще, если новые запросы сами будут добавляться в его очередь. Плюс ко всему, двух центров быть не должно, поэтому нужно реализовать синглтон. Итак, напишем уже его!
Copy Source | Copy HTML
  1. class Application
  2. {
  3.     public $requests = array();
  4.  
  5.     ///---
  6.     // Реализуем синглтон
  7.     protected static $instance;
  8.  
  9.     private function __construct()
  10.     {
  11.     }
  12.  
  13.     private function __clone()
  14.     {
  15.     }
  16.  
  17.     public static function getInstance()
  18.     {
  19.         if (!is_object(self::$instance))
  20.         {
  21.             self::$instance = new self;
  22.         }
  23.  
  24.         return self::$instance;
  25.     }
  26.  
  27.     public static function init()
  28.     {
  29.         self::getInstance();
  30.     }
  31.     ///---
  32.  
  33.     // Внутренняя функция для обработки всех запросов
  34.     private function i_run()
  35.     {
  36.         foreach($this->requests as &$request)
  37.         {
  38.             $done = $request->run($params);
  39.             if ($done) return true;
  40.         }
  41.  
  42.         return false;
  43.     }
  44.  
  45.     // Внешняя статическая функция-обертка над i_run
  46.     // Нужна исключительно для эстетики: Application::run() смотрится красивее, чем Application::getInstance()->run()
  47.     public static function run()
  48.     {
  49.         return Application::getInstance()->i_run();
  50.     }
  51. }

Также слегка поменяем класс Request:
Copy Source | Copy HTML
  1. class Request
  2. {
  3. <...>
  4.     // Конструктор класса
  5.     public function __construct($method, $path, $callback)
  6.     {
  7.         $this->method = strtolower($method);
  8.         $this->path = $path;
  9.         $this->callback = $callback;
  10.  
  11.         // Добавляем этот запрос в очередь к Application
  12.         Application::getInstance()->requests[] = $this;
  13.     }
  14. <...>
  15.     public function run()
  16.     {
  17. <...>
  18.         // После всех проверок вызываем кэллбек, передавая ей массив с переменными запроса
  19.         $result = call_user_func_array($this->callback, $this->args);
  20.  
  21.         // Если кэллбек возвратил булевое значение, возвращаем его
  22.         if (is_bool($result))
  23.             return $result;
  24.         // Иначе возвращаем true
  25.         else
  26.             return true;
  27.     }
  28. }

Последнее нужно для того, чтобы была возможность использовать несколько запросов для обработки одного пути. Если кэллбек возвращает false, то будут обработаны следующие запросы, иначе все завершится.
В-общем, использование нашего мини-фреймворка выглядит довольно красиво и просто:
Copy Source | Copy HTML
  1. new Application();
  2.  
  3. Request('/user/{id}', UserProfile)->assert('|^\d+$|')->run();
  4.  
  5. Application::run();

Тем не менее, можно все упростить еще сильнее. Среди настроек PHP существуют две замечательные опции: auto_prepend_file и auto_append_file, которые позволяют подключать проивзольные PHP-скрипты до и после выполнения основного скрипта. Мы можем вынести фреймворк в отдельный файл и подключать его с помощью этих функций. При первом подключении объявим классы и создадим объект Application, а при втором — вызовем Application::run(). Определить, запускается скрипт впервые или нет, можно с помощью проверки, существует ли класс Application или класс Request:
Copy Source | Copy HTML
  1. if (!class_exists('Application'))
  2. {
  3.     // Если класс Application еще не объявлен, скрипт запускает впервые
  4.  
  5.     class Request
  6.     {
  7. <...>
  8.     }
  9.  
  10.     function Request($method, $path, $callback)
  11.     {
  12. <...>
  13.     }
  14.  
  15.     class Application
  16.     {
  17. <...>
  18.     }
  19.  
  20.     // Инициализируем Application, 
  21.     Application::init();
  22. }
  23. else
  24. {
  25.     // Скрипт запускается не в первый раз
  26.     Application::run();
  27. }

Таким образом мы избавились от двух «лишних» строк.

Вот так можно написать более-менее функциональный мини-фреймворк. До Silex оно не дотягивает, но использовать его вполне можно. Полный исходник (слегка доработанный) и пример использования лежат на http://fw.nizarium.com/, который работает на этом же мини-фреймфорке.
Пожалуйста, не рассматривайте это как нечто серьезное. Это просто пример, писавшийся изначально чисто для себя. Если в нем есть какие-то ошибки, я готов их исправить.
Tags:
Hubs:
+46
Comments 67
Comments Comments 67

Articles