Фреймворк — это хорошо, это здорово, это возможность сэкономить кучу времени на раздумьях над архитектурой будущего приложения, но… Фреймворк как таковой — это каркас. И, на примере Kohana 3.0, о которой в данной статье пойдет речь, каркас этот надо, в той или иной степени, допиливать.
Теперь давайте по-порядку, чем мы сейчас займемся:
Итак, начнем…
Предполагается, что мы уже сделали пару простейших действий — создали файл .htaccess по образу и подобию example.htaccess, поставив в нем необходимый путь до нашей рабочей директории; изменили параметры инициализации в методе Kohana::init (также прописали путь к рабочей директории и выставили (по вкусу) 'index_file' => FALSE; подключили необходимые для последующей работы модули…
Теперь смотрим в конце bootstrap.php туда, где устанавливается роут default — сколько обычно в приложении роутов? Десять? Двадцать? В моем последнем приложении на Ko3.0 их было порядка 30. Дофига, в общем, для хранения прямо здесь, вперемешку с теми данными, которые, собственно, хранятся в bootstrap.php. Выход? Выносим их все в отдельный файл и инклудим. Допустим, так:
Создаем новый файл в папке application с названием routes.php и переносим туда весь Route::set('default')… а на его бывшем месте в bootstrap.php просто пишем require_once APPPATH.'routes.php';
Теперь давайте вспомним, что у класса контроллера (того, что Kohana_Controller) есть замечательные методы before() и after(), которые выполняются, соответственно, до и после «тела» контроллера. Теперь вспомним о том, что в большинстве приложений мы будем подключать модуль авторизации (auth) — как минимум, для административного входа в систему. К чему это я?
А давайте ка переопределим базовый Kohana_Controller и прямо там начнем работу с (возможным) пользователем.
И сохраняем это добро в папке application/classes под именем controller.php
Теперь подумаем, что еще нам может потребоваться в абсолютно любом месте любого контроллера в будущем? Редирект! Даже два! Собственно — редирект на главную и редирект назад. Возможные ситуации необходимости описывать не буду, просто если Вы согласны с необходимостью редиректов — читаем про них дальше:
Редирект на главную подразумевает редирект пользователя по адресу, который соответствует дефолтному роуту без параметров. Редирект назад подразумевает редирект по адресу из строки реферера, если это вообще ссылка. Если же нет — делаем редирект на главную при помощи описанного выше редиректа. Также у нас может быть 2 типа редиректа (пока мне это ни разу не пригодилось, но возможность, мне кажется, полезная) — редирект текущего запроса (HMVC и все такое...) и редирект основного запроса. По-умолчанию будем выполнять редирект основного запроса. Собственно, реализацию описанного выше я предлагаю следующую (пишем все в том же application/classes/controller.php после метода before()):
Немного поясню методы:
в методе go_home() мы получаем URL дефолтного роута и вызываем метод go()
в методе go_back() мы проверяем на валидность URL из Request::$referrer и если он не проходит проверку — выполняем метод go_home() на котором выполнение и прерывается
в методе go() мы определяем, какой запрос редиректить (по-умолчанию — Request::instance() — основной запрос, но можно и $this->request)
Теперь, раз уж зашла речь о различных возможных запросах (HMVC/простой) — давайте устраним небольшое упущение в классе Request. Упущение состоит в том, что в Kohana 3.0, в отличии от Kohana 3.1, нет метода для определения принадлежности реквеста — только Request::$is_ajax.
Создаем файл application/classes/request.php и пишем:
Так как Request::current() возвращает экземпляр основного запроса (Singletone), то достаточно проверить, является ли таковым текущий объект Request — $this. Этот метод пригодится нам в дальнейшем.
Дальше на очереди у нас расширение простого контроллера — контроллер, который будет не просто выполнять какие-то действия, но и работать с View. Назовем его Controller_Front (хотя Kohana предлагает нечто отдаленно похожее под названием Controller_Template — это дело вкуса, от названия ничего не зависит — лишь то, от какого класса Вы будете наследовать остальные контроллеры).
Controller_Front у нас будет обеспечивать разбиение всего View на «обертку» и на «контент». Обертка — это стандартная разметка, характерная для всех страниц нашего проекта — там будет доктайп, подключение стилей и все-все-все, что должно быть на всех страницах. Контент — это результат работы конкретного контроллера. Давайте немного отвлечемся от нашего Controller_Front и сразу их создадим, чтобы понимать, о чем, собственно, речь:
Вот такая у нас будет обертка, в которую будет вкладываться контент ($content), полученный в результате работы контроллера.
Сохраним это как application/views/index.php
Теперь давайте создадим контент-вью для роута, предлагаемого Kohana по-умолчанию, welcome/index
и сохраним это как application/views/welcome/index.php
Собственно, к этой строке статьи Вы уже должны понять, если по каким-то причинам не получалось раньше, что есть обертка, а что — контент.
Но вернемся к нашему Controller_Front. Давайте по-умолчанию будем рендерить все в обертку index, если в контроллере имя обертки не было переопределено (мало-ли какая логика работы Вашего приложения).
В этом контроллере я, надеюсь, все понятно из комментариев, кроме работы с каким-то View::$view_path — что это такое? Это то, что позволит нам не писать в пределах одного контроллера в разных экшнах строки типа $this->content = View::factory('user/edit'); а заменить их на более компактные $this->content = View::factory('edit');
Давайте, кстати, сам View расширим — иначе как мы это реализуем?
Думаю, что после того, как была расписана работа с этим измененным классом из Controller_Front, останавливаться тут будет излишне, так что сохраняем этот файл как application/classes/view.php, сохраняем Controller_Front как application/classes/controller/front.php и «едем дальше».
Открываем контроллер welcome (application/classes/controller/welcome.php) и меняем extends Controller на extends Controller_Front, а в методе index заменяем строку $this->request->response = 'hello, world!'; на $this->content = View::factory('index');
Теперь, если все сделано верно, открываем браузер, заходим по адресу проекта и видим наш Views/welcome/index.php View-файл, обрамленный Views/index.php лэйоутом.
Статья пролежала в папке «дописать и опубликовать» без малого месяц, но наконец у меня дошли руки хоть как-то упорядочить информацию в ней и, собственно, опубликовать. Если Вы прочитали ее и какие-то моменты остались для вас непонятными, или я допустил ошибки в коде, или вы все сделали по статье, но ничего не заработало — незамедлительно пишите в комментариях, отвечу при первой возможности. ;)
P.S.: думаю, когда-нибудь у меня дойдут руки и до остальных статей из папки «дописать и опубликовать», поэтому… Эта статья — только начало, дальше сделаем простенькое приложение (пишите пожелания в комментариях) на базе того, что мы успели сделать в этой статье — это позволит оценить удобства, которые мы только что создали.
UPD:
View-файл views/errors.php может быть таким:
Теперь давайте по-порядку, чем мы сейчас займемся:
- -Расширим базовый контроллер, добавив в него жизненно необходимые методы и работу с юзерами (которая присутствует в 99% проектов, хотя бы на уровне административного логина)
- -Создадим свой фронт-контроллер для более удобной и красивой работы с вью-файлами
- -Реализуем вывод ошибок валидации через фронт-контроллер
- -Улучшим базовый класс View
- -Ну и еще кое-какие полезные мелочи
Итак, начнем…
Предполагается, что мы уже сделали пару простейших действий — создали файл .htaccess по образу и подобию example.htaccess, поставив в нем необходимый путь до нашей рабочей директории; изменили параметры инициализации в методе Kohana::init (также прописали путь к рабочей директории и выставили (по вкусу) 'index_file' => FALSE; подключили необходимые для последующей работы модули…
Теперь смотрим в конце bootstrap.php туда, где устанавливается роут default — сколько обычно в приложении роутов? Десять? Двадцать? В моем последнем приложении на Ko3.0 их было порядка 30. Дофига, в общем, для хранения прямо здесь, вперемешку с теми данными, которые, собственно, хранятся в bootstrap.php. Выход? Выносим их все в отдельный файл и инклудим. Допустим, так:
Создаем новый файл в папке application с названием routes.php и переносим туда весь Route::set('default')… а на его бывшем месте в bootstrap.php просто пишем require_once APPPATH.'routes.php';
Теперь давайте вспомним, что у класса контроллера (того, что Kohana_Controller) есть замечательные методы before() и after(), которые выполняются, соответственно, до и после «тела» контроллера. Теперь вспомним о том, что в большинстве приложений мы будем подключать модуль авторизации (auth) — как минимум, для административного входа в систему. К чему это я?
А давайте ка переопределим базовый Kohana_Controller и прямо там начнем работу с (возможным) пользователем.
<?php
class Controller extends Kohana_Controller {
/**
* @var auth property with instance of "Auth" module
*/
public $auth = NULL;
/**
* @var user property with object of user
*/
public $user = FALSE;
public function before()
{
parent::before();
$this->auth = Auth::instance();
$this->user = $this->auth->get_user();
}
public function after()
{
parent::after();
}
}
И сохраняем это добро в папке application/classes под именем controller.php
Теперь подумаем, что еще нам может потребоваться в абсолютно любом месте любого контроллера в будущем? Редирект! Даже два! Собственно — редирект на главную и редирект назад. Возможные ситуации необходимости описывать не буду, просто если Вы согласны с необходимостью редиректов — читаем про них дальше:
Редирект на главную подразумевает редирект пользователя по адресу, который соответствует дефолтному роуту без параметров. Редирект назад подразумевает редирект по адресу из строки реферера, если это вообще ссылка. Если же нет — делаем редирект на главную при помощи описанного выше редиректа. Также у нас может быть 2 типа редиректа (пока мне это ни разу не пригодилось, но возможность, мне кажется, полезная) — редирект текущего запроса (HMVC и все такое...) и редирект основного запроса. По-умолчанию будем выполнять редирект основного запроса. Собственно, реализацию описанного выше я предлагаю следующую (пишем все в том же application/classes/controller.php после метода before()):
public function go_home($current_request_only = FALSE)
{
$url = Route::url('default', NULL, TRUE);
$this->go($url, $current_request_only);
}
public function go_back($current_request_only = FALSE)
{
Validate::url(Request::$referrer) OR $this->go_home($current_request_only);
$this->go(Request::$referrer, $current_request_only);
}
private function go($url, $current_request_only)
{
$request = ($current_request_only) ? $this->request : Request::instance();
$request->redirect($url);
}
Немного поясню методы:
в методе go_home() мы получаем URL дефолтного роута и вызываем метод go()
в методе go_back() мы проверяем на валидность URL из Request::$referrer и если он не проходит проверку — выполняем метод go_home() на котором выполнение и прерывается
в методе go() мы определяем, какой запрос редиректить (по-умолчанию — Request::instance() — основной запрос, но можно и $this->request)
Теперь, раз уж зашла речь о различных возможных запросах (HMVC/простой) — давайте устраним небольшое упущение в классе Request. Упущение состоит в том, что в Kohana 3.0, в отличии от Kohana 3.1, нет метода для определения принадлежности реквеста — только Request::$is_ajax.
Создаем файл application/classes/request.php и пишем:
class Request extends Kohana_Request {
public function is_initial()
{
return $this === Request::instance();
}
}
Так как Request::current() возвращает экземпляр основного запроса (Singletone), то достаточно проверить, является ли таковым текущий объект Request — $this. Этот метод пригодится нам в дальнейшем.
Дальше на очереди у нас расширение простого контроллера — контроллер, который будет не просто выполнять какие-то действия, но и работать с View. Назовем его Controller_Front (хотя Kohana предлагает нечто отдаленно похожее под названием Controller_Template — это дело вкуса, от названия ничего не зависит — лишь то, от какого класса Вы будете наследовать остальные контроллеры).
Controller_Front у нас будет обеспечивать разбиение всего View на «обертку» и на «контент». Обертка — это стандартная разметка, характерная для всех страниц нашего проекта — там будет доктайп, подключение стилей и все-все-все, что должно быть на всех страницах. Контент — это результат работы конкретного контроллера. Давайте немного отвлечемся от нашего Controller_Front и сразу их создадим, чтобы понимать, о чем, собственно, речь:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta name="author" content="Roman Chvanikoff" />
<title><?php echo $title; ?></title>
</head>
<body>
<?php echo $content; ?>
</body>
</html>
Вот такая у нас будет обертка, в которую будет вкладываться контент ($content), полученный в результате работы контроллера.
Сохраним это как application/views/index.php
Теперь давайте создадим контент-вью для роута, предлагаемого Kohana по-умолчанию, welcome/index
<h1>Welcome!</h1>
и сохраним это как application/views/welcome/index.php
Собственно, к этой строке статьи Вы уже должны понять, если по каким-то причинам не получалось раньше, что есть обертка, а что — контент.
Но вернемся к нашему Controller_Front. Давайте по-умолчанию будем рендерить все в обертку index, если в контроллере имя обертки не было переопределено (мало-ли какая логика работы Вашего приложения).
class Controller_Front extends Controller {
/**
* @var layout wrapper of content for final output
*/
public $layout = 'index';
/**
* @var content controller-generated output
*/
public $content;
/**
* @var errors all logic errors (including Validate errors) should be stored here
*/
public $errors;
/**
* @var post Validate object of _POST
*/
public $post;
/**
* @var view_path define what folder should be used to generate Views
* values:
* NULL - View::path will be generated as name of called controller
* FALSE - View::path will not affect views generation
* string - View::path will have value of the view_path property
*/
public $view_path = NULL;
public function before()
{
parent::before();
$this->layout = View::factory($this->layout);
$this->layout->set_global('user', $this->user);
$this->post = Validate::factory($_POST);
// Doubts? Look at view_path property definition
if (is_null($this->view_path))
{
View::$view_path = $this->request->controller;
}
elseif ($this->view_path)
{
View::$view_path = $this->view_path;
}
}
public function after()
{
/**
* Clear View "environment"
*/
View::$view_path = NULL;
if ( ! Validate::not_empty($this->errors))
{
$this->errors = NULL;
}
else
{
// $this->errors should be an array to pass it as argument to View.
is_array($this->errors) OR $this->errors = array($this->errors);
// $this->errors is a View now
$this->errors = View::factory('errors',
array('errors' => $this->errors));
}
// $this->content can be a simple string or something like that so we check if it is a View file
if ($this->content instanceof View)
{
// Append post-data
$this->content->post = $this->post;
// Append errors
$this->content->errors = $this->errors;
}
// If request is initial - return layout with attached content
if ($this->request->is_initial() AND ! Request::$is_ajax)
{
// Append content to layout
$this->layout->content = $this->content;
// Set response
$this->request->response = $this->layout;
}
else
{
// Set response as controller-generated output
$this->request->response = $this->content;
}
parent::after();
}
}
В этом контроллере я, надеюсь, все понятно из комментариев, кроме работы с каким-то View::$view_path — что это такое? Это то, что позволит нам не писать в пределах одного контроллера в разных экшнах строки типа $this->content = View::factory('user/edit'); а заменить их на более компактные $this->content = View::factory('edit');
Давайте, кстати, сам View расширим — иначе как мы это реализуем?
class View extends Kohana_View {
/**
* @staticvar view_path a directory that will be used to generate views
*/
public static $view_path = NULL;
/**
* Sets the view filename.
*
* $view->set_filename($file);
*
* @param string view filename
* @return View
* @throws Kohana_View_Exception
*/
public function set_filename($file)
{
$directory = 'views';
if ( ! is_null(View::$view_path))
{
$directory .= DIRECTORY_SEPARATOR.View::$view_path;
}
if (($path = Kohana::find_file($directory, $file)) === FALSE)
{
throw new Kohana_View_Exception('The requested view :file could not be found in :directory', array(
':file' => $file,
':directory' => $directory,
));
}
// Store the file path locally
$this->_file = $path;
return $this;
}
}
Думаю, что после того, как была расписана работа с этим измененным классом из Controller_Front, останавливаться тут будет излишне, так что сохраняем этот файл как application/classes/view.php, сохраняем Controller_Front как application/classes/controller/front.php и «едем дальше».
Открываем контроллер welcome (application/classes/controller/welcome.php) и меняем extends Controller на extends Controller_Front, а в методе index заменяем строку $this->request->response = 'hello, world!'; на $this->content = View::factory('index');
Теперь, если все сделано верно, открываем браузер, заходим по адресу проекта и видим наш Views/welcome/index.php View-файл, обрамленный Views/index.php лэйоутом.
Послесловие
Статья пролежала в папке «дописать и опубликовать» без малого месяц, но наконец у меня дошли руки хоть как-то упорядочить информацию в ней и, собственно, опубликовать. Если Вы прочитали ее и какие-то моменты остались для вас непонятными, или я допустил ошибки в коде, или вы все сделали по статье, но ничего не заработало — незамедлительно пишите в комментариях, отвечу при первой возможности. ;)
P.S.: думаю, когда-нибудь у меня дойдут руки и до остальных статей из папки «дописать и опубликовать», поэтому… Эта статья — только начало, дальше сделаем простенькое приложение (пишите пожелания в комментариях) на базе того, что мы успели сделать в этой статье — это позволит оценить удобства, которые мы только что создали.
UPD:
View-файл views/errors.php может быть таким:
<ul class="errors">
<?php foreach ($errors as $error) : ?>
<li><?php echo $error; ?></li>
<?php endforeach; ?>
</ul>