Kohana, Проекты → Милли-фреймворк по следам kohana
Мне очень нравится (или нравился, еще не определился) фреймворк kohana. Удобный, легко расширяемый, довольно легкий… Но, каким бы легким он не был, все-равно иногда использовать его не правильно, как из пушки по воробьям. Вот, для примера, такой случай: решил я провести пару опытов над поисковиками и сделать пару мелких сайтов для этих тестов. Сами тесты, в данный момент, абсолютно не принципиальны (о них, возможно, расскажу как-нибудь потом). Важно — сайты для тестов ОЧЕНЬ простые: 1 контроллер с 3-4 методами, 1 модель с 2-3 методами, 3-4 вьюшки, автообновление через полу-универсальный парсер… Использовать полноценный фреймворк с кучей неиспользуемых библиотек и пр. не правильно, на мой взгляд.
Изначально, как и любой программист наверное, пошел на поиски микро-фреймворка. Пересмотрел наверное десяток, но ни один не понравился. Самый удачный из всех, на мой взгляд, F3. Возможно и использовал бы его, но… Вот пара основных минусов для меня:
1. Роуты. Возможно, кому-то удобно подобное, но для меня это ад какой-то:
F3::route('<HTTP method> <path>', <function>);
Возможно, для кого-то необходимо определять какой именно запрос пришел (GET, POST, PUT и т.д.) именно в роуте, но для меня намного логичнее это делать в методе контроллера. Еще больше негатива добавляет возможность использовать параметры запроса в роутах. Сравните:
F3::route('GET /@controller/@action','{{@PARAMS.controller}}->{{@PARAMS.action}}');
Route::set('default', '(<controller>(/<action>))');
А еще, я иногда делаю вычисляемые роуты, т.е. контроллер и экшен формируются на основе http запроса и данных из базы, например. Возможно это и можно сделать на F3, но искать я уже не стал, после ужаса выше ). Но это я так, уже просто придираюсь…
Можно еще несколько моментов про роуты написать, но они уже второстепенны.
2. Глобальность. F3::set по всему коду еще можно потерпеть (хотя и сложно), но то, что этот метод добавляет переменную в глобальную область — для меня, перебор. Так ли это необходимо?
В общем, поизучав так несколько мини-фреймворков, я понял — для меня было бы очень удобно оставить кохану, но вырезать все лишнее из нее. Если быть точнее — вырезать из ko3 только самое необходимое для себя. Так получился милли-фреймворк, который здесь и представляю. Думаю, он более полезен для новичков — поможет разобраться в устройстве коханы в частности и фреймворков в общем.
Итак, начнем разбор по пунктам. Первым пунктом, конечно-же, будет структура файлов. Я оставил практически стандартную от ko3. Исключил только modules, т.к. для меня они не нужны, в данный момент.
В корне содержится 2 файла: .htaccess от коханы и index.php. Второй выглядит примерно так (есть некоторые упрощения):
<?php
$time = microtime(TRUE);
$memory = memory_get_usage();
error_reporting(E_ALL | E_STRICT) ;
ini_set('display_errors', 'On');
define('DOCROOT', realpath(dirname(__FILE__)) . DIRECTORY_SEPARATOR);
$application = '../application';
$system = '../system';
define('APPPATH', realpath(DOCROOT . $application) . DIRECTORY_SEPARATOR);
define('SYSTEM', realpath(DOCROOT . $system) . DIRECTORY_SEPARATOR);
unset($application, $system);
require SYSTEM . 'base.php';
require APPPATH . 'bootstrap.php';
echo '<!-- time: ' . (microtime(TRUE) - $time) . '; memory: ' . (memory_get_usage() - $memory) . ' -->';
Как можете видеть, все стандартно. Почти ).
Первым вызывается base.php из папки system. По сути, это подключение основных, самых необходимых, функций и методов. У меня все просто:
<?php
class Base {
protected static $_init = FALSE;
protected static $_paths = NULL;
public static function init() {
if (self::$_init)
return;
self::$_init = TRUE;
self::$_paths = array(APPPATH, SYSTEM);
}
public static function auto_load($class) {
try {
$file = str_replace('_', '/', strtolower($class));
if ($path = Base::find_file('classes', $file)) {
require $path;
return TRUE;
}
return FALSE;
} catch (Exception $e) {
throw $e;
die;
}
}
public static function find_file($dir, $file, $ext = NULL, $array = FALSE) {
if ($ext === NULL)
$ext = '.php';
elseif ($ext)
$ext = ".{$ext}";
else
$ext = '';
$file = str_replace('_', '/', $file);
$path = $dir . DIRECTORY_SEPARATOR . $file . $ext;
if ($array || $dir == 'config' || $dir == 'messages') {
$paths = array_reverse(Base::$_paths);
$found = array();
foreach ($paths as $dir) {
if (is_file($dir . $path))
$found[] = $dir . $path;
}
} else {
$found = FALSE;
foreach (Base::$_paths as $dir) {
if (is_file($dir . $path)) {
$found = $dir.$path;
break;
}
}
}
return $found;
}
}
Оба метода выдрал из коханы. Идем дальше: system/classes/config.php:
<?php
class Config {
protected static $_instances = NULL;
public static function instance($name) {
if (!isset(self::$_instances[$name]))
self::$_instances[$name] = new Config($name);
return self::$_instances[$name];
}
protected $_config;
public function __construct($name) {
if (!($file = Base::find_file('config', $name)))
throw new Exception('Config file ' . $name . ' not found');
if (is_array($file)) {
$this -> _config = array();
foreach ($file as $f)
$this -> _config += include $f;
} else
$this -> _config = include $file;
}
public function __get($name) {
return $this -> get($name);
}
public function __set($name, $value) {
$this -> _config[$name] = $value;
}
public function __toString() {
return serialize($this -> _config);
}
public function as_array() {
return $this -> _config;
}
public function get($path = NULL, $default = NULL) {
if (NULL === $path)
return $this -> _config;
$arr = explode('.', $path);
$result = $this -> _config;
foreach ($arr as $item) {
if (!isset($result[$item])) {
unset($result);
break;
}
$result = $result[$item];
}
return isset($result) ? $result : $default;
}
}
Контроллер system/classes/controller.php:
<?php
class Controller {
public function before() {}
public function after() {}
}
Модель system/classes/model.php, пустой класс, по сути. В моем случае, прописаны методы для работы с данными (open, save и пр.), т.к. работаю с файлами, а не базой. По этой причине, тут не привожу код.
Класс работы у роутами почти полностью взят из коханы (system/classes/route.php):
<?php
class Route {
const REGEX_KEY = '<([a-zA-Z0-9_]++)>';
const REGEX_SEGMENT = '[^/.,;?\n]++';
const REGEX_ESCAPE = '[.\\+*?[^\\]${}=!|]';
public static $default_protocol = 'http://';
public static $localhosts = array(FALSE, '', 'local', 'localhost');
public static $default_action = 'index';
protected static $_routes = array();
public static function process_uri($uri, $routes = NULL) {
$routes = (empty($routes)) ? Route::all() : $routes;
$params = NULL;
foreach ($routes as $name => $route) {
if ($params = $route -> matches($uri)) {
return array(
'params' => $params,
'route' => $route,
);
}
}
return NULL;
}
public static function run() {
if(!empty($_SERVER['REQUEST_URI']))
$uri = trim($_SERVER['REQUEST_URI'], '/');
if(!empty($_SERVER['PATH_INFO']))
$uri = trim($_SERVER['PATH_INFO'], '/');
if(!empty($_SERVER['QUERY_STRING']))
$uri = trim($_SERVER['QUERY_STRING'], '/');
if (!isset($uri))
throw new Exception('Oops!!! URI not detected!');
$uri = trim($uri, '/');
$processed_uri = self::process_uri($uri);
if (empty($processed_uri))
throw new Exception('File not found', 404);
$params = $processed_uri['params'];
$prefix = 'controller_' . (
isset($params['directory'])
? str_replace(array('\\', '/'), '_', trim($params['directory'], '/')) . '_'
: ''
);
$controller = $params['controller'];
$action = isset($params['action']) ? $params['action'] : self::$default_action;
unset($params['controller'], $params['action'], $params['directory']);
$file = Base::find_file('classes', $prefix . $controller);
if (empty($file))
throw new Exception('Controller ' . $controller . ' not found', 404);
require $file;
if (!class_exists($controller))
throw new Exception('Controller ' . $controller . ' not found', 404);
$class = new ReflectionClass($controller);
if ($class -> isAbstract())
throw new Exception('Cannot create instances of abstract ' . $controller, 403);
$controller = $class -> newInstance();
$class -> getMethod('before') -> invoke($controller);
if (!$class -> hasMethod('action_' . $action))
throw new Exception('The requested URL ' . $uri . ' was not found on this server.', 404);
$method = $class -> getMethod('action_' . $action);
$method -> invokeArgs($controller, $params);
$class -> getMethod('after') -> invoke($controller);
}
public static function set($name, $uri_callback = NULL, $regex = NULL) {
return Route::$_routes[$name] = new Route($uri_callback, $regex);
}
public static function get($name) {
if (!isset(Route::$_routes[$name]))
throw new Exception('The requested route does not exist: ' . $name);
return Route::$_routes[$name];
}
public static function all() {
return Route::$_routes;
}
public static function name(Route $route) {
return array_search($route, Route::$_routes);
}
public static function compile($uri, array $regex = NULL) {
if (!is_string($uri))
return;
$expression = preg_replace('#'.Route::REGEX_ESCAPE.'#', '\\\\$0', $uri);
if (strpos($expression, '(') !== FALSE)
$expression = str_replace(array('(', ')'), array('(?:', ')?'), $expression);
$expression = str_replace(array('<', '>'), array('(?P<', '>'.Route::REGEX_SEGMENT.')'), $expression);
if ($regex) {
$search = $replace = array();
foreach ($regex as $key => $value) {
$search[] = "<$key>" . Route::REGEX_SEGMENT;
$replace[] = "<$key>$value";
}
$expression = str_replace($search, $replace, $expression);
}
return '#^'.$expression.'$#uD';
}
protected $_callback;
protected $_uri = '';
protected $_regex = array();
protected $_defaults = array('action' => 'index', 'host' => FALSE);
protected $_route_regex;
public function __construct($uri = NULL, $regex = NULL) {
if ($uri === NULL)
return;
if (!is_string($uri) && is_callable($uri)) {
$this -> _callback = $uri;
$this -> _uri = $regex;
$regex = NULL;
} elseif (!empty($uri))
$this -> _uri = $uri;
if (!empty($regex))
$this->_regex = $regex;
$this -> _route_regex = Route::compile($uri, $regex);
}
public function defaults(array $defaults = NULL) {
$this -> _defaults = $defaults;
return $this;
}
public function matches($uri) {
if ($this -> _callback) {
$closure = $this -> _callback;
$params = call_user_func($closure, $uri);
if (!is_array($params))
return FALSE;
} else {
if (!preg_match($this -> _route_regex, $uri, $matches))
return FALSE;
$params = array();
foreach ($matches as $key => $value) {
if (is_int($key))
continue;
$params[$key] = $value;
}
}
foreach ($this -> _defaults as $key => $value) {
if (!isset($params[$key]) OR $params[$key] === '')
$params[$key] = $value;
}
return $params;
}
public function is_external() {
$host = isset($this -> _defaults['host']) ? $this -> _defaults['host'] : FALSE;
return !in_array($host, Route::$localhosts);
}
}
Ну и последний из необходимых для меня классов — вьюшка (system/classes/view.php):
<?php
class View {
protected static $_global_data = array();
public static $template = 'default';
public static function factory($file = NULL, array $data = NULL) {
return new View($file, $data);
}
protected static function capture($view_filename, array $view_data) {
extract($view_data, EXTR_SKIP);
if (View::$_global_data)
extract(View::$_global_data, EXTR_REFS);
ob_start();
try {
include $view_filename;
} catch (Exception $e) {
ob_end_clean();
throw $e;
}
return ob_get_clean();
}
public static function set_global($key, $value = NULL) {
if (is_array($key)) {
foreach ($key as $key2 => $value)
View::$_global_data[$key2] = $value;
} else {
View::$_global_data[$key] = $value;
}
}
public static function bind_global($key, & $value) {
View::$_global_data[$key] =& $value;
}
protected $_file;
protected $_data = array();
public function __construct($file = NULL, array $data = NULL, $template = NULL) {
if ($file !== NULL)
$this -> set_filename($file);
if ($data !== NULL)
$this -> _data = $data + $this -> _data;
if ($template !== NULL)
self::$template = $template;
}
public function & __get($key) {
if (array_key_exists($key, $this -> _data)) {
return $this -> _data[$key];
} elseif (array_key_exists($key, View::$_global_data)) {
return View::$_global_data[$key];
} else {
throw new Exception("View variable is not set: $key");
}
}
public function __set($key, $value) {
$this -> set($key, $value);
}
public function __isset($key) {
return (isset($this -> _data[$key]) || isset(View::$_global_data[$key]));
}
public function __unset($key) {
unset($this -> _data[$key], View::$_global_data[$key]);
}
public function __toString() {
return $this -> render();
}
public function set_filename($file) {
$filename = TEMPLATE . self::$template . DIRECTORY_SEPARATOR . $file . '.php';
if (!file_exists($filename))
throw new Exception('The requested view ' . $file . ' could not be found');
$this -> _file = $filename;
return $this;
}
public function set($key, $value = NULL) {
if (is_array($key)) {
foreach ($key as $name => $value)
$this -> _data[$name] = $value;
} else {
$this -> _data[$key] = $value;
}
return $this;
}
public function bind($key, & $value) {
$this -> _data[$key] =& $value;
return $this;
}
public function render($file = NULL) {
if ($file !== NULL)
$this -> set_filename($file);
if (empty($this -> _file))
throw new Exception('You must set the file to use within your view before rendering');
return View::capture($this -> _file, $this -> _data);
}
}
В принципе — это все, весь «фреймворк» ). Есть конечно-же еще один файл, application/bootstrap.php:
<?php
define('BASEURL', 'http://' . $_SERVER['HTTP_HOST'] . '/');
spl_autoload_register(array('base', 'auto_load'));
ini_set('unserialize_callback_func', 'spl_autoload_call');
date_default_timezone_set('Europe/Moscow');
setlocale(LC_ALL, 'ru_RU.utf-8');
Base::init();
Route::set('default', '')
-> defaults(array(
'controller' => 'main',
'action' => 'index',
));
Route::run();
Base::init и Route::run можно было бы и вынести из bootstrap, но я не стал, не критично.
Посмотрели код? Почитали? Тогда Вам должно быть понятно, что «фреймворк» не делает вообще ничего, кроме запуска нужного метода в нужном контроллере. Все остальные действия отданы на откуп приложению. Поэтому и назвал «милли-фреймворк» ). Вот, для примера, расширение контроллера в приложении (application/classes/template.php):
<?php
class Template extends Controller {
public $layout = 'layout';
public $auto_render = TRUE;
protected $_data = array();
public function before() {
$config = Config::instance('global');
View::$template = $config -> get('template');
parent::before();
View::set_global('place', NULL);
if ($this -> auto_render === TRUE)
$this -> layout = View::factory($this -> layout);
$this -> title = $config -> get('title');
$this -> description = $config -> get('description');
$this -> keywords = $config -> get('keywords');
}
public function after() {
if ($this -> auto_render === TRUE) {
foreach ($this -> _data as $name => $value)
$this -> layout -> $name = $value;
echo $this -> layout -> render();
}
parent::after();
}
public function __set($name, $value) {
$this -> _data[$name] = $value;
}
public function __get($name) {
return isset($this -> _data[$name]) ? $this -> _data[$name] : NULL;
}
public static function url($url) {
return str_replace(DIRECTORY_SEPARATOR, '/', BASEURL . str_replace(DOCROOT, '', TEMPLATE . View::$template . '/' . $url));
}
}
Согласитесь, не сложно. В итоге, я очень доволен: все привычно, легко доработать или перенести нужные классы из коханы и т.д.
Если есть какие-то вопросы или предложения по улучшению (точнее — упрощению) кода: welcome в комментарии!