В дополнение к теме Пишем простой админ модуль Hello World с примером формы ввода рассмотрю еще один важный вопрос - создание модификации уже существующего модуля. То есть построить свой модуль на базе другого, не испортив исходный модуль.
Предположим, задача стоит такая: нужно добавить в таблицу products базы данных товаров несколько своих колонок
- строковое поле email - емейл поставщика
- строковое поле phone - телефон торгового агента
- строковое поле addr - адрес поставщика
- булевое поле secondhand - флажок "товар бывший в употреблении"
- вещественное поле diagnosis - экспертная оценка внешнего вида товара
- целочисленное поле tenancy - срок аренды в днях
- поле даты postdate - дата публикации товарного предложения
и затем переписать админский модуль Product работы со страницей товара так, чтобы он давал редактировать эти дополнительные поля в админпанели, причем переписанный модуль не должен затираться при обновлениях движка.
В предложенной выше задаче новые колонки специально подобраны разнообразными по типам полей в целях демонстрации. Эти колонки интересны нам лишь согласно поставленной цели, для движка же они являются неконтролируемыми отвлеченными сведениями (контролем будет заниматься модуль), и согласно той же задаче, сами новые поля пока никоим образом не завязаны на логику других колонок в таблице. Сейчас важна лишь способность редактировать в товаре дополнительную информацию для себя, а также чтобы эти сведения можно было вывести в карточке товара на клиентской стороне.
На измененный модуль в принципе можно было бы возложить обязанность отслеживать наличие в таблице необходимых дополнительных колонок и создавать их автоматически, если не найдены. В Impera CMS существуют такие функции, но пока мы отбросим этот вопрос, чтобы до некоторой поры не усложнять код модуля. Вместо этого представим, будто администратор базы данных с помощью phpMyAdmin уже создал следующие колонки в таблице products, и будто эти колонки с настоящего момента всегда существуют в базе товаров:
- email - строка
- phone - строка
- addr - строка
- secondhand - флажок
- diagnosis - вещественное число
- tenancy - число
- postdate - штамп времени
Ясно, что поскольку движок не контролирует дополнительные колонки и на клиентскую сторону отправляет их в том виде, как нашел в записях базы данных, то на клиентской стороне эти сведения вывести в карточке товара можем так, например на странице товара (в шаблоне за ее вид отвечает файл product.tpl, в котором доступна переменная $product с полями записи текущего просматриваемого товара):
Здесь показан также и пример вывода нескольких стандартных важных полей, которыми обычно интересуются верстальщики, но не все знают их обозначения.
{* файл product.tpl - страница товара *} {$rate = $currency->rate_from / $currency->rate_to} <h1> {$product->model} </h1> <h2> Фотография </h2> <img class="photo" src="{$product->large_image|escape}"> <h3> Дополнительные фото </h3> {foreach $product->fotos as $foto} <img class="thumbnail" src="{$foto->filename|escape}"> {/foreach} <h4> Описание </h4> {$product->body} <h4> Характеристики </h4> <div class="properties"> {foreach $product->properties_tree as $property} {$property1 = array_shift($property)} {if !empty($property1->in_product)} {$property1->name}: {$property1->value|default:'-'} {* и остальные значения свойства *} {foreach $property as $propertyN} ● {$propertyN->value|default:'-'} {/foreach} {/if} {/foreach} </div> <div> Поступит в продажу: {$product->coming} </div> <div> Код производителя: {$product->pcode} </div> <div> Гарантия: {$product->guarantee} </div> <div> Категория: {$product->category} </div> <div> Бренд: {$product->brand} </div> <h5> Варианты товара </h5> {foreach $product->variants as $variant} <div class="variant"> Название: {$variant->name} <br> Артикул: {$variant->sku} <br> На складе: {$variant->stock} шт. <br> Цена: {($variant->discount_price * $rate)|string_format:'%1.2f'} {$currency->sign} <a class="button" href="/cart/add/{$variant->variant_id|escape}"> В корзину </a> </div> {/foreach} <h6> SEO текст </h6> {$product->seo_description} {* выводим наши дополнительные поля *} <h6> Поставщик </h6> <div class="supplier"> Емейл: {$product->email} <br> Телефон: {$product->phone} <br> Адрес: {$product->addr} <br> Был в употреблении: {($product->secondhand) ? 'Да' : 'Нет'} <br> Оценка вида: {$product->diagnosis} <br> Срок аренды: {$product->tenancy} <br> Дата предложения: {$product->postdate} </div>
Теперь вернемся непосредственно к задаче. Исходный модуль имеет название Product и всегда доступен в админпанели по ссылке http://сайт/admin?section=Product.
Мы напишем модуль Product2, который породим от исходного модуля, сохраним изменившуюся часть кода в файле Product2.admin.php и разместим в папке http://сайт/objects/product2. В сущности модуль перекроет два метода (posting и update) родительского модуля, чтобы вклиниться в процесс обработки отправленной формы товара, и добавит пять своих небольших методов, чтобы дополнить эту обработку редакторским контролем новых полей:
- isMyFields
- collectMyFields
- validateMyFields
- updateMyFields
- isFormPosted
И еще предоставляет свойство myFields, где можете указать (исправить на) свой список новых полей, который интересует уже исходя из вашей задачи. Все эти поля будут автоматически попадать в обработку редактирования записи о товаре.
Соответственно этот модуль будет всегда доступен в админпанели по ссылке http://сайт/admin?section=Product2 (если интересует транслировать его на URL исходного модуля, чтобы полностью закрыть собой родителя, это решается с помощью соответствующих mod_rewrite-овых инструкций в корневом файле .htaccess админпанели). А полный код модуля представлен ниже:
<?php
// подключаем класс товара
require_once(dirname(__FILE__) . '/../Admin.Products.php');
// =======================================================================
/**
* Наша модификация стандартного админ модуля страницы товара
*/
// =======================================================================
class Product2 extends Product {
// ===================================================================
/**
* Задаем список наших полей в формате:
* ТИПимя поля => дефолтное значение|текст ошибки для незаполненных
* где типы:
* ! = булевое поле
* : = поле даты
* $ = число с плавающей запятой
* # = целое число
* _ = строковое поле
*/
// ===================================================================
protected $myFields = array('_email' => '|',
'_phone' => '|Нужно ввести телефон поставщика.',
'_addr' => '|',
'!secondhand' => '0|',
'$diagnosis' => '5.00|Нужно указать оценку внешнего вида.',
'#tenancy' => '31|Нужно указать срок аренды в днях.',
':postdate' => 'NOW|Нужно указать дату публикации.');
// ===================================================================
/**
* Признак что в опубликованной форме есть все заявленные наши поля
* для конкретной записи
*
* @access protected
* @param integer $id ИД отслеживаемой записи
* @return boolean TRUE если все поля найдены
*/
// ===================================================================
protected function isMyFields ( $id ) {
if (empty($this->myFields) || !is_array($this->myFields)) return FALSE;
foreach ($this->myFields as $field => $error) {
$field = substr($field, 1);
if (!isset($_POST[$field][$id])) return FALSE;
}
return TRUE;
}
// ===================================================================
/**
* Сбор наших полей для конкретной записи из опубликованной формы
*
* @access protected
* @param integer $id ИД отслеживаемой записи
* @param object $item объект записи, куда добавить поля
* @param boolean $test TRUE если это вызов для валидации полей
* @return void
*/
// ===================================================================
protected function collectMyFields ( $id, & $item, $test = FALSE ) {
if (!empty($this->myFields) && is_array($this->myFields)) {
foreach ($this->myFields as $field => $error) {
$default = FALSE;
if (!$test) {
$default = explode('|', $error);
$default = $default[0];
}
$type = substr($field, 0, 1);
$field = substr($field, 1);
switch ($type) {
case '!':
$item->$field = $this->request->getPostRecordFieldAsBoolean($field, $id, $default);
break;
case ':':
if ($default == 'NOW') $default = null;
$item->$field = $this->request->getPostRecordFieldAsDate($field, $id, $default);
break;
case '$':
$item->$field = $this->request->getPostRecordFieldAsFloat($field, $id, $default);
break;
case '#':
$item->$field = $this->request->getPostRecordFieldAsInteger($field, $id, $default);
break;
case '_':
default:
$item->$field = $this->request->getPostRecordField($field, $id, $default);
}
}
}
}
// ===================================================================
/**
* Валидация наших полей для конкретной записи в опубликованной форме
*
* @access protected
* @param integer $id ИД отслеживаемой записи
* @return boolean TRUE если есть ошибка и нужен стоп
*/
// ===================================================================
protected function validateMyFields ( $id ) {
// собираем из формы наши поля
if (!empty($this->myFields) && is_array($this->myFields)) {
$item = new stdClass();
$this->collectMyFields($id, $item, TRUE);
// проверяем заполнение обязательных полей
foreach ($this->myFields as $field => $error) {
$error = explode('|', $error);
$error = isset($error[1]) ? trim($error[1]) : '';
if ($error != '') {
$type = substr($field, 0, 1);
$field = substr($field, 1);
if ($item->$field === FALSE) return $this->push_error($error);
switch ($type) {
case '!':
break;
case ':':
if ($item->$field == '') return $this->push_error($error);
break;
case '$':
if ($item->$field == 0) return $this->push_error($error);
break;
case '#':
if ($item->$field == 0) return $this->push_error($error);
break;
case '_':
default:
if ($item->$field == '') return $this->push_error($error);
}
}
}
}
return FALSE;
}
// ===================================================================
/**
* Обновление наших полей в конкретной записи базы данных
*
* @access protected
* @param integer $id ИД обновляемой записи
* @param object $item объект записи
* @return void
*/
// ===================================================================
protected function updateMyFields ( $id, & $item ) {
if (!empty($this->myFields) && is_array($this->myFields)) {
$query = '';
foreach ($this->myFields as $field => $error) {
$field = substr($field, 1);
$query .= '`' . $field . '` = \'' . $this->db->query_value($item->$field) . '\', ';
}
$query = 'UPDATE `' . $this->dbtable . '` '
. 'SET ' . rtrim($query, ', ') . ' '
. 'WHERE `' . $this->dbtable_field . '` = \'' . $this->db->query_value($id) . '\'';
$this->db->query($query);
}
}
// ===================================================================
/**
* Признак что форма редактирования товаров опубликована
*
* @access protected
* @return boolean TRUE если опубликована
*/
// ===================================================================
protected function isFormPosted () {
$field = 'post';
return isset($_POST[$field]) && is_array($_POST[$field])
&& empty($_POST['ignore_post']);
}
// ===================================================================
/**
* Обработка редактирования записей
*
* @access protected
* @param string $result_page URL страницы возврата после успеха
* @return boolean TRUE если есть ошибка и нужен стоп
*/
// ===================================================================
protected function posting ( & $result_page ) {
// если форма опубликована, проверяем наши поля
if ($this->isFormPosted()) {
$field = 'post_this_one';
foreach ($_POST['post'] as $id => $value) {
if (!isset($_POST[$field]) || isset($_POST[$field][$id])) {
if ($this->isMyFields($id)) {
$cancel = $this->validateMyFields($id);
if ($cancel) return $cancel;
}
}
}
}
// все ОК, тогда пусть родитель обработает редактирование записи
return parent::posting($result_page);
}
// ===================================================================
/**
* Обновление записи в базе данных
*
* @access protected
* @param object $item объект записи
* @return integer ИД измененного товара
*/
// ===================================================================
protected function update ( & $item ) {
// пусть родитель обновит запись в базе
$post_id = !empty($item->product_id) ? $item->product_id : 0;
$id = parent::update($item);
// если обновил, собираем из формы наши поля и обновляем в базе
if (!empty($id)) {
if ($this->isMyFields($post_id)) {
$this->collectMyFields($post_id, $item);
$this->updateMyFields($id, $item);
}
}
return $id;
}
}
?>