Skip to content

prasol/kohana-presentation

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Kohana Presentation Layer

Данный модуль реализует основные идеи Presentation Model Мартина Фаулера (http://martinfowler.com/eaaDev/PresentationModel.html) для Kohana Framework с учетом особенностей PHP и стиля кодирования Kohana.

Требования

Модуль работает под Kohana 3.3.x и требует PHP 5.3 и выше. Примеры внизу используют синтаксис PHP 5.4 для наглядности, но сам модуль этого не делает и вы можете использовать синтаксис PHP 5.3 в своих проектах. Классы модуля используют собственное просмотранство имен (Yup) и не конфликтуют с другими модулями.

Существущая проблема

Типичная задача -- отображение данных модели из БД в HTML-представлении. Имеется таблица следующей структуры:

users
int id
varchar first_name
varchar last_name
enum ('m','f') gender
timestamp last_login

Модель:

classes/Model/User.php
class Model_User extends \Yup\ORM {

    // Some data access and domain logic
}

Когда нужно вывести данные о сущности на страницу, обычно используется такой подход:

classes/Controller/User.php
$this->template->user = new Model_User(1);
$this->template->gender_captions = ['m' => __('Male'), 'f' => __('Female')];
views/user_display.php
<h3>User <?= HTML::chars($user->first_name) > <?= HTML::chars($user->last_name) ?></h3>
<table>
<tr>
<td>Gender: <?= $gender_captions[$user->gender] ?></td>
<td>Last login: <?= $user->last_login === '0000-00-00 00:00:00' ? 'Never' : Date::formatted_time($user->last_login) ?></td>
</tr>
</table>

Недостатками такого подхода является сильное связывание HTML-разметки и данных предоставляемых моделью. В шаблон разметки встроена также логика преобразования самих данных из формата, хранящегося в БД, что ухудшает читаемость и усложняет поддержку и изменение самой разметки. Ситуация усугубляется необходимостью использования одних и тех же данных в разных шаблонах. При этом логика отображения на них накладывается та же самая, что приводится к дублированию и копипасту кода. Например, список пользователей:

classes/Controller/Users.php
$this->template->users = (new Model_User())->find_all();
$this->template->gender_captions = ['m' => __('Male'), 'f' => __('Female')];
views/user_list.php
<table>
<tr>
<th>Name</th>
<th>Gender</th>
<th>Last login</th>
</tr>
<? foreach ($this->users as $user): ?>
<tr>
<td><?= HTML::chars($user->first_name) > <?= HTML::chars($user->last_name) ?></td>
<td>Last login: <?= $user->last_login === '0000-00-00 00:00:00' ? 'Never' : Date::formatted_time($user->last_login) ?></td>
<td>Gender: <?= $gender_captions[$user->gender] ></td>
</tr>
<? endforeach; >
</table>

Также возможны и другие контексты использования. Повторяющиеся методы, нужные лишь представлению, переносятся в саму модель, либо слабоструктурированные хелперы, при этом повышая связанность кода и не давая достаточной гибкости. К примеру, если нужно вывод между именем и фамилией отчества пользователя в готовой системе, что не было предусмотрено изначально, приходить делать Find/Replace в куче шаблонов. Также растет вероятность человеческой ошибки: при большом количестве одинаковых вызовов легко забыть поставить htmlspecialchars, например.

Предлагаемое решение

Данный модуль предлагает следующее решение: введение дополнительного класса-декоратора (Presentation Model), привязанного к ORM-модели (или просто согласованному набору произвольных данных), который и содержит все презентационную логику данных. Класс позволяет:

  • Накладывать фильтры на значения полей модели перед выводом;
  • Создавать виртуальные поля, нужные исключительно для целей отображения и отсутствующие в модели;
  • Производить данную фильтрацию максимально прозрачно как для программиста модели, так и для верстальщика шаблона, применяя инкапсуляцию.

Применение

Предлагаемая архитектура будет особенно полезна в следуюшщих случаях:

  • Получение данных и разметку шаблонов выполняют разные люди;
  • В проекте используется сложная или часто модифицируемая разметка;
  • Одни и те же структуры данных используются для вывода в множестве шаблонов;
  • В системе производятся регулярные изменения.

Пример

В примере с пользователем добавляется класс, Presentation_Model_User:

classes/Presentation/Model/User.php
class Presentation_Model_User extends \Yup\Presentation_Model {
	
	public function rules()
    {
        return [
            'first_name' => 'HTML::chars',
            'last_name'  => 'HTML::chars',
            'last_login' => function($value){
                return ($value === '0000-00-00 00:00:00') ? 'Never' : Date::formatted_time($value);
            },
            'gender' => $this->replace([
                'm' => __('Male'),
                'f' => __('Female'),
            ]),
        ];
    }
    
    protected function field_full_name()
    {
        return $this->first_name . ' ' . $this->last_name;
    }
}

Код модели не меняется, кроме наследования от \Yup\ORM:

classes/Model/User.php
class Model_User extends \Yup\ORM {

	// Some data access and domain logic
}

В контроллере добавляется создание класса-обертки:

classes/Controller/User.php
$user = new Model_User(1);
$this->template->user = Present::model($user);

Шаблон при этом лишается логики представление данных и содержит лишь разметку:

views/user_display.php
<h3>User <?= $user->full_name ?></h3>
<table>
<tr>
<td>Gender: <?= $user->gender ?></td>
<td>Last login: <?= $user->last_login ?></td>
</tr>
</table>

Использование в контексте вывода списка:

classes/Controller/Users.php
$users = (new Model_User())->find_all();
$this->template->users = Present::db_result($users);
views/user_list.php
<table>
<tr>
<th>Name</th>
<th>Gender</th>
<th>Last login</th>
</tr>
<? foreach ($this->users as $user): ?>
<tr>
<td><?= $user->full_name ?></td>
<td>Last login: <?= $user->last_login ?></td>
<td>Gender: <?= $user->gender ></td>
</tr>
<? endforeach; >
</table>

Получаемые значения кешируются, и не будут высчитываться при повторных вызовах.

Классы и методы

\Yup\Presentation

Базовый абстрактный класс слоя представления.

rules


Description: Метод предназначен для перекрытия в потомках. Содержит массив правил, применяемых к полям исходной сущности. Ключи массива -- названия полей, которым будут применять правила преобразования. Это могут быть как оригинальные поля модели, так и вычисилимые поля, добавленые классом презентации через методы field*. Сами преобразования будут вызваны в момент запроса значения (см. get) Преобразования могут быть следующших типов:

  • Анонимная функция с одним входным аргументом, например function($v){ return $v*2; };
  • Строковое имя внешней функции либо статического метода любого класса (можно использовать алиас self для вызова текущего класса, например, self::format_time). Преобразуемое значение будет всегда передано первым аргументом. Можно использовать массив, где первым аргументом передать имя функции, а последующими -- список всех аргументов, кроме первого;
  • Массив значений из предыдущих двух пунктов. В этом случае они будут применены последовательно. Например, если объявлено правило для 'note' => ['HTML::chars', 'nl2br'], то при запросе поля note к нему будет применен сначала метод HTML::chars, а потом к результату функция nb2br, значение которой и будет возвращено.
Parameters

no parameters

Return value

array

Example
class Presentation_Model_Order extends \Yup\Presentation_Model {
    
    public function rules()
    {
        return [
            'created'      => 'Date::formatted_time',
			'total_amount' => [['number_format', 2, '.', '']],
            'state'   => $this->replace([
                'new'        => __('New'),
                'processing' => __('In process'),
                'completed'  => __('Done'),
                'cancelled'  => __('Cancelled'),
            ]),
            'note'    => ['HTML::chars', 'nl2br'],            
        ];
    }
}

get


Description_: Возвращает преобразованное и пригодное к отображению значение поля. После первого вызова значение кэшируется. Вызывается магическим методом _get(). Можно добавлять вычисляемые поля, создавая в классе методы с названием field{имя поля}. Если поле к которому происходит обращение, является ORM-объектом (например, при использовании belongs to), то на него при вызове также будет наложена обертка Presentation Model.

Parameters

string: field Имя поля.

Return value

mixed

Example
echo $user->name; // same as $user->get('name');

raw


Description_: Возвращает оригинальное значение поля, без преобразований.

Parameters

string: field Имя поля.

Return value

mixed

Example
echo $user->raw('name');

replace


Description_: Вспомогательный метод для использования в правилах преобразования в потомках. Возвращает функцию, заменяющую исходное значение на значение элемента массива replacements при вызове.

Parameters

array: replacements Ассоциативный массив для заменяемых значений.

Return value

string: Результат замены, или оригинальная строка, если соотвествия не найдено.

as_array


Description_: Возвращает обработанные данные в виде массива.

clear_cache


Description_: очищает внутренний кэш для преобразованных и рассчитанных полей.

$_context


Description_: Статичное свойство, позволяет менять префикс для создаваемых классов, в случаа нескольких различных контекстов вывода (например, для XML или JSON).

Example
\Yup\Presentation::$_context = 'XML';

\Yup\Presentation_Model

Реализация Presentation для обработки ORM-моделей.

factory


Description_: Статичный метод, создающий экземпляр конкретного класса.

Parameters

mixed: model Не обязательный параметр. Если задан, класс определяется на основе класса модели. Можно передавать как имя класса, так и экземпляр модели. Если не задан, создается экземпляр класса, метод которого вызван.

Return value

\Yup\Presentation_Model

Example
$user_model = new Model_User(1);

$user = Presentation_Model_User::factory()->set_model($user_model);
равнозначно
$user = \Yup\Presentation_Model::factory($user_model);

set_model


Description_: Сеттер, задающий значение модели.

Parameters

\Yup\ORM: model

Return value

\Yup\Presentation_Model: $this

make


Description_: Создает экземпляр модели из набора данных.

Parameters

array: $fields

Return value

\Yup\Presentation_Model: $this

Example
$product = Presentation_Model_Product::factory()->make([
    'id'  =>  $some_id,            
    'name'=> $some_name,    
]);

\Yup\Presentation_Database_Result

Явялется оберткой для класса Database_Result, использующегося в Kohana для обработки результатов списковых запросов и иметт одинаковый с ним интерфейс. При итерировании создает и возвращает Presentation Model для результатов запроса.

\Yup\Presentation_Data

Позволяет создавать Presentation Model для данных, не имеющих конкретной модели (например, составленных из выборок нескольких таблиц) и работать с ними как с объектами из шаблонов.

описание предвидится

\Yup\Presentation_List

Реализует итератор, интерфейсно аналогичный Database_Result, для большей консистентности. Применяется при листинге наборов произвольных данных, оборачивая их в декораторы.

описание предвидится

\Yup\NS

Хелпер, предоставляющий функции обработки имен классов с учетом пространств имен.

\Yup\ORM

Наследует ORM, добавляя методы, нужные для презентации, а также упрощает работу с листингом значений enum-полей. Например, если в таблице orders содержится поле state типа enum со значениями ('new','processing','completed'), в модели Model_Order после загрузки станет доступно дополнительное поле states возвращающие массив:

[
'new' => 'new',
'processing' => 'processing',
'ordering' => 'ordering',
]

Если в Presentation Model задано преобразование подписей для этого поля, то список возможных значений также будет обработан. Пример:

class Presentation_Model_Order extends \Yup\Presentation_Model {
	
	public function rules()
    {
        return [
            'state' => $this->replace([
                'new'        => __('New'),
                'processing' => __('In process'),
                'completed'  => __('Done'),
            ]),
        ];
    }
}

В шаблоне теперь можно удобно формировать, например, выпадающие списки в формах редактирования:

<?= Form::select('state', $order->states, $order->raw('state')) ?>

Выведет код (если значение order->state равно processing):

<select name="state">
<option name="new">New</option>
<option name="processing" selected="selected">In process</option>
<option name="completed">Done</option>
</select>

\Present

Набор статичных методов, предоставляющих упрощенные вызовы для создания презентационных классов.

model


Description_: Метод для создания Presentation Model на осове ORM-модели.

Parameters

\Yup\ORM: model

Return value

\Yup\Presentation_Model

Example
$user = new Model_User(1);
$this->template->user = Present::model($user);

data


Description_: Метод для создания Presentation Model на осове произвольных именованных данных.

Parameters

string: name array: data

Return value

\Yup\Presentation_Data

Example
$data = ['currency_id' => 1, 'amount' => 5.5];
$this->template->currency = Present::data('currency', $data);

db_result


Description_: Метод для создания обертки для списковых данных, полученных из БД.

Parameters

\Database_Result: db_result

Return value

\Yup\Presentation_Database_Result

Example
$users = (new Model_User())->find_all();
$this->template->users = Present::db_result($users);

data_list


Description_: Метод для создания обертки-итератора для произвольных списковых данных.

Parameters

array: list

Return value

\Yup\Presentation_List

Example
$products = array(
    Present::data('product', $this->get_product_data(1)),
    Present::data('product', $this->get_product_data(2)),
);
$this->template->products = Present::data_list($products);

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages