Wednesday, November 23, 2011

Организация кода на PHP

What-is-PHP

У интерпретируемых языков есть одна общая проблема, нет готовой жесткой системы организации кода. Что я имею ввиду:

  • Каждый класс, контроллер, вьювер в отдельном файле. (на практике люди умудряются в один файл все приложение запихать на 5000 строк)
  • На один файл – одна точка входа. (люди же обычно обработку запросов строют через условия if-else, либо switch-case)
  • Хорошая система вывода сгенерированных на сервере данных. (передал классу данные, готовые к выводу и указал формат, вот идеальный вариант)

Использовать готовый фреймворк для решения небольшой задачи, это равносильно подключение jQuery для выборки по id. Что есть небольшая задача для меня: промо-сайт, api сервис, сайт с логикой на клиенте (js RIA), где PHP как прослойка между базой и приложением на JS.

Типичный файл index.php которые я встречал при работе с чужим кодом. Максимум логику одного кейса вынесут либо в отдельный файл через include, либо оформят в статический класс.

$action = @$_REQUEST['action'];
switch($action){
	case "list":
		$filter = @$_REQUEST['filter'];
		//...
		echo json_encode($result);
		break;
	case "get":
		$id = @$_REQUEST['id'];
		//...
		echo $result;
		break;
	case "add":
		$name = @$_REQUEST['name'];
		//...
		echo "ok";
		break;
	default:
		//404
		break;
}

Предлагаю элегантное решение проблемы в виде роутера запросов.

$route = @$_REQUEST['r'];
$format = @$_REQUEST['f'];

unset ($_REQUEST['r']);
unset ($_REQUEST['f']);

$view = new View();
$api = new Api($view);
$api->call($route, $_REQUEST, $format);

Класс View занимается только форматом вывода, т.е. представлением. Одни и те же данные мы можем вывести как JSON, HTML, Шаблон Smarty или еще как-то.

Вот примерная реализация на все случаи жизни.

class View implements IView
{
	public $modes = array(
		'html' => 'html',
		'text' => 'html',
		'tpl' => 'tpl',
		'js' => 'js',
		'json' => 'json',
		'result' => 'result',
		'debug' => 'debug'
	);

	public function html($result)
	{
		Header("content-type: text/html; charset=utf-8");
		echo $result;
	}

	public function tpl($result)
	{
		require('Smarty.class.php');
		$smarty = new Smarty();

		$smarty->setTemplateDir('smarty/templates');
		$smarty->setCompileDir('smarty/templates_c');
		$smarty->setCacheDir('smarty/cache');
		$smarty->setConfigDir('smarty/configs');

		$smarty->assign('item', $result['data']);
		Header("content-type: text/html; charset=utf-8");
		echo $smarty->fetch($result['tpl']);
	}

	public function debug($result)
	{
		Header("content-type: text/html; charset=utf-8");
		print_r($result);
	}

	public function json($result)
	{
		Header("content-type: application/json; charset=utf-8");
		return json_encode($result);
	}

	public function js($result)
	{
		Header("content-type: application/javascript; charset=utf-8");
		echo $result;
	}

	public function result($result)
	{
		$json = $result === true ? array('result' => true) : array('result' => false);
		return $this->json($json);
	}

	public function output($result, $mode, $callback = null)
	{
		if (isset($callback) && ($mode != 'json' || $mode != 'result')) {
			$mode = 'json';
		}
		$raw = '';
		if (array_key_exists($mode, $this->modes)) {
			$methodName = $this->modes[$mode];
			$raw = $this->$methodName($result, null);
		}
		if (isset($callback)){
			echo $callback . '(' . $raw . ')';
		}else if (isset($raw)){
			echo $raw;
		}
	}
}

Класс Api наследует App класс, в котором описана все логика. Сам же Api описывает только кейсы.

class Api extends App
{
	public $export = array(
		'list' => 'getList',
		'add' => 'addItem',
		'get' => 'getItem'
	);

	public function getList()
	{
		$filter = $this->arg('filter', '')
		$items = array();
		return $items;
	}

	public function addItem()
	{
		$name = $this->arg('name', 'default name')
		return true;
	}

	public function getItem()
	{
		$id = $this->arg('id', null)
		return array(
			'name' => 'Name',
			'url' => 'URL'
		);
	}
}

Класс App реализует всю внутреннюю логику работы с таблицей доступа

abstract class App
{
	public $view;
	public $args;
	public $export = array();

	public function __construct($view = null)
	{
		if (isset($view)) {
			$this->view = $view;
			if (!($this->view instanceof IView)) {
				throw new Exception('View must implement IView interface');
			}
		}
	}

	public function arg($name, $default)
	{
		return (isset($this->args[$name]) && !empty($this->args[$name]) ? urldecode($this->args[$name]) : $default);
	}

	public function call($route, $args, $format = null)
	{
		if (!isset($route)) throw new Exception('Route is not exists');

		$this->args = $args;
		$callback = $this->arg('jsoncallback', null);

		if (array_key_exists($route, $this->export)) {
			$methodName = $this->export[$route];
			$result = $this->$methodName();
			if (!isset($this->view)) {
				return $result;
			} else {
				if (!isset($format)) throw new Exception('Format is not exists');

				$this->view->output($result, $format, $callback);
			}
		}
	}
}

Api можно использовать и без представления, в случае, если у нас api используется не только как сервис.

$api = new Api();
$api->call('list', array('filter' = > '100'));

Примеры и сама реализация лежат на github

No comments:

Post a Comment