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 в комментарии!