В дополнение к теме Пишем простой админ модуль 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; } } ?>