Недавно, прочитав про мини-фреймворк Silex, я подумал: а что в нем сложного? Попробовал написать нечто подобное и получилось довольно легко.
В основе подобных мини-фреймворков обычно лежат следущие правила для mod_rewrite:
Они перенаправляют любой запрос на index.php, в котором подключается фреймворк и происходит обработка этого запроса с вызовом функций-кэллбеков. Так что для начала напишем простую функцию для обработки адреса:
При вызове функция проверяет адрес, к которому обратился пользователь, и если он совпадает с переданным в функцию, то вызывает кэллбек. Это уже можно считать базовым функционалом. Однако здесь нет одной нужной вещи — извлечения переменных из адреса. Кроме того, адреса example.com/something и example.com/something функция сочтет разными. Решить это можно парсингом адреса и использованием array_filter:
Теперь можно писать код примерно такого вида:
А теперь покосимся в сторону Silex и посмотрим, чего нам не достает. В первую очередь это assert'ы (проверка переменной пути на соответствие регулярному выражению), проверка метода вызова (GET, POST, PUT и т. п.) и объектно-ориентированная модель. Первые два добавить довольно просто, нужно лишь дописать пару проверок:
Впрочем, второе тоже довольно легко сделать. Передачу $method, $path, $callback сделаем в конструкторе, для assert'ов напишем отдельную функцию-обертку, а всю работу запихнем в функцию run(), не забыв подставить $this-> для вышеуказанных переменных:
Вроде бы все красиво, за исключением двух моментов. Первый момент чисто эстетический — теперь для обработки одного запроса надо написать две строки кода:
Но к счастью, PHP позволяет использовать одинаковые названия для классов и функций, так что напишем функцию-оберку, которая будет создавать и возвращать экземпляр класса:
Теперь можно опять писать все компактно и удобно:
Второй неприятный момент заключается в отсутствии какого-либо единого центра, где запросы будут храниться и обрабатываться. Зачем нам вызывать функцию run() постоянно? Но даже если сделать подобный единый центр, то будет гораздо проще, если новые запросы сами будут добавляться в его очередь. Плюс ко всему, двух центров быть не должно, поэтому нужно реализовать синглтон. Итак, напишем уже его!
Также слегка поменяем класс Request:
Последнее нужно для того, чтобы была возможность использовать несколько запросов для обработки одного пути. Если кэллбек возвращает false, то будут обработаны следующие запросы, иначе все завершится.
В-общем, использование нашего мини-фреймворка выглядит довольно красиво и просто:
Тем не менее, можно все упростить еще сильнее. Среди настроек PHP существуют две замечательные опции: auto_prepend_file и auto_append_file, которые позволяют подключать проивзольные PHP-скрипты до и после выполнения основного скрипта. Мы можем вынести фреймворк в отдельный файл и подключать его с помощью этих функций. При первом подключении объявим классы и создадим объект Application, а при втором — вызовем Application::run(). Определить, запускается скрипт впервые или нет, можно с помощью проверки, существует ли класс Application или класс Request:
Таким образом мы избавились от двух «лишних» строк.
Вот так можно написать более-менее функциональный мини-фреймворк. До Silex оно не дотягивает, но использовать его вполне можно. Полный исходник (слегка доработанный) и пример использования лежат на http://fw.nizarium.com/, который работает на этом же мини-фреймфорке.
Пожалуйста, не рассматривайте это как нечто серьезное. Это просто пример, писавшийся изначально чисто для себя. Если в нем есть какие-то ошибки, я готов их исправить.
В основе подобных мини-фреймворков обычно лежат следущие правила для mod_rewrite:
Copy Source | Copy HTML
- <IfModule mod_rewrite.c>
- RewriteEngine On
- RewriteBase /
- RewriteCond %{REQUEST_FILENAME} !-f
- RewriteCond %{REQUEST_FILENAME} !-d
- RewriteRule ^(.*)$ index.php [QSA,L]
- </IfModule>
Они перенаправляют любой запрос на index.php, в котором подключается фреймворк и происходит обработка этого запроса с вызовом функций-кэллбеков. Так что для начала напишем простую функцию для обработки адреса:
Copy Source | Copy HTML
- function Request($path, $callback)
- {
- if ($path == $_SERVER['REQUEST_URI']) return call_user_func($callback);
- }
При вызове функция проверяет адрес, к которому обратился пользователь, и если он совпадает с переданным в функцию, то вызывает кэллбек. Это уже можно считать базовым функционалом. Однако здесь нет одной нужной вещи — извлечения переменных из адреса. Кроме того, адреса example.com/something и example.com/something функция сочтет разными. Решить это можно парсингом адреса и использованием array_filter:
Copy Source | Copy HTML
- function array_filter_callback_no_empty_str($value)
- {
- return $value != '';
- }
-
- function Request($path, $callback)
- {
- // Переменные запроса для передачи в кэллбек
- $args = array();
-
- // Разбиваем адрес, к которому обратился пользователь (URI), на части
- $uri = explode('/', $_SERVER['REQUEST_URI']);
- // То же самое проделываем с путем запроса
- $path = explode('/', $path);
-
- // Удаляем пустые части обоих массивов
- $uri = array_values(array_filter($uri, array_filter_callback_no_empty_str));
- $path = array_values(array_filter($path, array_filter_callback_no_empty_str));
-
- // Если количество частей в URI и пути разное, выходим
- if (count($uri) != count($path))
- return false;
-
- // Проходим по всем частям пути запроса
- for($i = 0; $i < count($path); $i++)
- {
- // Проверяем, является ли переменной данная часть пути
- // Переменные пути оформляются в фигурные скобки, что и проверяет регулярное выражение
- if (preg_match('|^\{(.*)\}$|', $path[$i], $match))
- {
- // Если является, то добавим эту переменную в массив
- $args[$match[1]] = $uri[$i];
- }
- else
- {
- // Если часть запроса не является переменной, просто сравниваем URI с запросом
- // Если они не совпадают - выходим
- if ($uri[$i] != $path[$i])
- return false;
- }
- }
-
- // После всех проверок вызываем кэллбек, передавая ей массив с переменными запроса
- return call_user_func_array($callback, $args);
- }
Теперь можно писать код примерно такого вида:
Copy Source | Copy HTML
- function Hello($who)
- {
- print "Hello, $who";
- }
-
- Request('/hello/{who}', Hello);
А теперь покосимся в сторону Silex и посмотрим, чего нам не достает. В первую очередь это assert'ы (проверка переменной пути на соответствие регулярному выражению), проверка метода вызова (GET, POST, PUT и т. п.) и объектно-ориентированная модель. Первые два добавить довольно просто, нужно лишь дописать пару проверок:
Copy Source | Copy HTML
- function Request($method, $path, $callback, $asserts = array())
- {
- // Проверяем метод обращения к серверу.
- // Если в запросе указан какой-то конкретный метод и он не совпадает с тем, который был использован, выходим
- if ($method != '' && strtolower($_SERVER['REQUEST_METHOD']) != $method)
- return false;
- <...>
- // Теперь проверяем, является ли переменной данная часть пути
- if (preg_match('|^\{(.*)\}$|', $path[$i], $match))
- {
- // Если является, то смотрим, есть ли регулярное выражение для ее проверки,
- // и если есть, то проверяем на соответствие ему соответствующую часть URI
- if (!isset($asserts[$match[1]]) || preg_match($asserts[$match[1]], $uri[$i]))
- {
- // Если все правильно, добавим эту переменную вместе с ее значением в массив
- $args[$match[1]] = $uri[$i];
- }
- else
- {
- // Если значение не соответствует регулярному выражению, выходим
- return false;
- }
- }
- <...>
- }
Впрочем, второе тоже довольно легко сделать. Передачу $method, $path, $callback сделаем в конструкторе, для assert'ов напишем отдельную функцию-обертку, а всю работу запихнем в функцию run(), не забыв подставить $this-> для вышеуказанных переменных:
Copy Source | Copy HTML
- class Request
- {
- public $method; // Метод запроса (GET, POST, PUT и т. п.)
- public $path; // Путь запроса
- public $callback; // Кэллбек
- public $asserts = array(); // Регулярные выражения для проверки переменных пути
-
- // Конструктор класса
- public function __construct($method, $path, $callback)
- {
- $this->method = strtolower($method);
- $this->path = $path;
- $this->callback = $callback;
- }
-
- // Добавление регулярного выражения re для проверки переменной пути с названием name
- public function assert($name, $re)
- {
- $this->asserts[$name] = $re;
-
- // Возвращаем текущий экземляр класса
- // Это дает возможность написания подобного кода: $reg->assert('id', '|^\d+$|')->run();
- return $this;
- }
-
- // Функция обработки запроса
- public function run()
- {
- <...>
- }
- }
Вроде бы все красиво, за исключением двух моментов. Первый момент чисто эстетический — теперь для обработки одного запроса надо написать две строки кода:
Copy Source | Copy HTML
- $req = new Request('/user/{id}', UserProfile);
- $req->assert('|^\d+$|')->run();
Но к счастью, PHP позволяет использовать одинаковые названия для классов и функций, так что напишем функцию-оберку, которая будет создавать и возвращать экземпляр класса:
Copy Source | Copy HTML
- function Request($method, $path, $callback)
- {
- return new Request($method, $path, $callback);
- }
Теперь можно опять писать все компактно и удобно:
Copy Source | Copy HTML
- Request('/user/{id}', UserProfile)->assert('|^\d+$|')->run();
Второй неприятный момент заключается в отсутствии какого-либо единого центра, где запросы будут храниться и обрабатываться. Зачем нам вызывать функцию run() постоянно? Но даже если сделать подобный единый центр, то будет гораздо проще, если новые запросы сами будут добавляться в его очередь. Плюс ко всему, двух центров быть не должно, поэтому нужно реализовать синглтон. Итак, напишем уже его!
Copy Source | Copy HTML
- class Application
- {
- public $requests = array();
-
- ///---
- // Реализуем синглтон
- protected static $instance;
-
- private function __construct()
- {
- }
-
- private function __clone()
- {
- }
-
- public static function getInstance()
- {
- if (!is_object(self::$instance))
- {
- self::$instance = new self;
- }
-
- return self::$instance;
- }
-
- public static function init()
- {
- self::getInstance();
- }
- ///---
-
- // Внутренняя функция для обработки всех запросов
- private function i_run()
- {
- foreach($this->requests as &$request)
- {
- $done = $request->run($params);
- if ($done) return true;
- }
-
- return false;
- }
-
- // Внешняя статическая функция-обертка над i_run
- // Нужна исключительно для эстетики: Application::run() смотрится красивее, чем Application::getInstance()->run()
- public static function run()
- {
- return Application::getInstance()->i_run();
- }
- }
Также слегка поменяем класс Request:
Copy Source | Copy HTML
- class Request
- {
- <...>
- // Конструктор класса
- public function __construct($method, $path, $callback)
- {
- $this->method = strtolower($method);
- $this->path = $path;
- $this->callback = $callback;
-
- // Добавляем этот запрос в очередь к Application
- Application::getInstance()->requests[] = $this;
- }
- <...>
- public function run()
- {
- <...>
- // После всех проверок вызываем кэллбек, передавая ей массив с переменными запроса
- $result = call_user_func_array($this->callback, $this->args);
-
- // Если кэллбек возвратил булевое значение, возвращаем его
- if (is_bool($result))
- return $result;
- // Иначе возвращаем true
- else
- return true;
- }
- }
Последнее нужно для того, чтобы была возможность использовать несколько запросов для обработки одного пути. Если кэллбек возвращает false, то будут обработаны следующие запросы, иначе все завершится.
В-общем, использование нашего мини-фреймворка выглядит довольно красиво и просто:
Copy Source | Copy HTML
- new Application();
-
- Request('/user/{id}', UserProfile)->assert('|^\d+$|')->run();
-
- Application::run();
Тем не менее, можно все упростить еще сильнее. Среди настроек PHP существуют две замечательные опции: auto_prepend_file и auto_append_file, которые позволяют подключать проивзольные PHP-скрипты до и после выполнения основного скрипта. Мы можем вынести фреймворк в отдельный файл и подключать его с помощью этих функций. При первом подключении объявим классы и создадим объект Application, а при втором — вызовем Application::run(). Определить, запускается скрипт впервые или нет, можно с помощью проверки, существует ли класс Application или класс Request:
Copy Source | Copy HTML
- if (!class_exists('Application'))
- {
- // Если класс Application еще не объявлен, скрипт запускает впервые
-
- class Request
- {
- <...>
- }
-
- function Request($method, $path, $callback)
- {
- <...>
- }
-
- class Application
- {
- <...>
- }
-
- // Инициализируем Application,
- Application::init();
- }
- else
- {
- // Скрипт запускается не в первый раз
- Application::run();
- }
Таким образом мы избавились от двух «лишних» строк.
Вот так можно написать более-менее функциональный мини-фреймворк. До Silex оно не дотягивает, но использовать его вполне можно. Полный исходник (слегка доработанный) и пример использования лежат на http://fw.nizarium.com/, который работает на этом же мини-фреймфорке.
Пожалуйста, не рассматривайте это как нечто серьезное. Это просто пример, писавшийся изначально чисто для себя. Если в нем есть какие-то ошибки, я готов их исправить.