====== Шаг4. Создание и отображение списка товаров для администраторов ====== ===== Часто используемые шаблоны ===== Так как мы уже умеем создавать модели, контроллеры и шаблоны, то позволим себе немного расслабится, и воспользоваться возможностью [[limb3:ru:packages:constructor#%D0%B3%D0%B5%D0%BD%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F_%D1%81%D1%83%D1%89%D0%BD%D0%BE%D1%81%D1%82%D0%B5%D0%B9_%D0%BF%D1%80%D0%B8%D0%BB%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F| генерации сущностей]]: $ php ./lib/limb/limb.php entity_create Project directory [/www/limb-example-shop]: Limb directory [/www/limb-example-shop/lib/]: Table name (could be 'all'): product После выполнения этих действий у нас должны были появится модель, контроллер и шаблоны, построенные по таблице **product**. ===== Класс Product ===== Немного доработаем сгенерированную модель. Файл shop/src/model/Product.class.php: 'ASC'); protected function _createValidator() { $validator = new lmbValidator(); $validator->addRequiredRule('title'); $validator->addRequiredRule('description'); $validator->addRequiredRule('price'); return $validator; } function getImagePath() { if(($image_name = $this->getImageName()) && file_exists(lmb_env_get('PRODUCT_IMAGES_DIR') . $image_name)) return LIMB_HTTP_BASE_PATH . 'product_images/' . $image_name; else return LIMB_HTTP_BASE_PATH . '/shared/cms/images/icons/cancel.png'; } } Мы установили сортировку продуктов по-умолчанию при помощи атрибута **$_default_sort_params**. Теперь при выборках продукты будут сортироваться по заголовку в алфавитном порядке. Подробнее о сортировке по-умолчанию можно узнать в разделе [[limb3:ru:packages:active_record:find|"Поиск и сортировка объектов"]]. Мы также создали метод **getImagePath()**. Этот метод по сути вводит новое **виртуальное поле $image_path**, которое можно использовать в шаблонах (пример будет показан ниже). Поле $image_path мы ввели для того, чтобы не выносить знания о папке product_images/ в шаблоны, и при желании эту папку можно будет легко сменить. Если товар не имеет изображения, тогда будет выводиться иконка cancel.png из набора, входящего в состав пакета CMS. ===== Создание, редактирование и удаление товаров из панели управления ===== Благодаря генерации у нас уже есть контроллер AdminProductController и заготовки шаблонов для панели управления. Мы не будем подробно описывать наши действия, надеясь на опыт создания подобного функционала для сущности User. ==== Доработка контроллера AdminProductController ==== Уже сейчас мы можем создавать и редактировать товары, благодаря использованию lmbAdminObjectController, в качестве базового. Но, к сожалению, он не знает как мы храним изображения. И именно этому его придется научить. Файл %%shop/src/controller/AdminProductController.class.php%%: _uploadImage($this->item, $this->request->get('image')); } function _uploadImage($item, $uploaded_image) { if(!$uploaded_image) return; if(!$uploaded_image['name'] || !$uploaded_image['tmp_name']) return; $file_name = $uploaded_image['name']; $file_path = $uploaded_image['tmp_name']; $dest_path = lmb_env_get('PRODUCT_IMAGES_DIR') . $file_name; lmbFs :: cp($file_path, $dest_path); unlink($file_path); $item->setImageName($file_name); } } Метод _onAfterImport() является специальным методом (без реализации) в lmbAdminObjectController и вызывается непосредственно после импортирования свойств в сохраняемый объект (см. lmbAdminObjectController :: _import()). Непосредственно обработку изображения мы выделили в отдельный метод _uploadImage(). Переменная $_FILES обрабатывается в классе lmbHttpRequest, поэтому мы можем получить данные по изображению через объект $request. Загруженное изображение сохраняется в папке, определенной переменной окружения PRODUCT_IMAGES_DIR, о которой мы говорили в [[step2|шаге 2]]. ==== Первый результат ==== Попробуйте теперь зайти на страницу /admin_product и "поиграть" с ней, например, создать с десяток товаров, отредактировать и удалить некоторых из них. Если у вас нет желания добавлять товары самостоятельно, то можете скопировать содержимое shop_example_dir/www/product_images к себе в проект и залить db.mysql из shop_example_dir/init/ к себе в базу данных проекта. Мы позволили себе немного позаимствовать описания книг с Amazon.com ;-) ===== Смена статуса доступности товара ===== Иногда владельцу магазина необходимо временно снять с продажи сразу несколько товаров. Снимать галочки в форме редактирования для каждого товара - занятие довольно утомительное, поэтому мы реализуем возможность смены статуса доступности товаров сразу для нескольких товаров прямо из списка товаров. Для этого нам необходимо будет немного модифицировать шаблон shop/template/admin_product/display.html: [...]
{{apply template="selectors_button" action="set_available" title="Set available" /}} {{apply template="selectors_button" action="set_unavailable" title="Set unavailable" /}}
{{list:item}} {{/list:item}} {{list:empty}}
Empty
{{/list:empty}}
{{apply template="selectors_toggler"/}} #ID Title Price Image Availability Decsription Actions
{{apply template="selector" value="{$item.id}"/}} #{$item.id} {$item.title} {$item.price} {$item.description|raw|nl2br} {{apply template="object_action_edit" item="{$item}" icon="page_white_edit" /}} {{apply template="object_action_delete" item="{$item}" icon="page_white_delete" /}}
{{apply template="selectors_button" action="set_available" title="Set available" /}} {{apply template="selectors_button" action="set_unavailable" title="Set unavailable" /}}
[...]
Мы добавили div'ы с кнопками сверху и снизу списка, а так же checkbox-ы для каждой строки. Примененный нами (через тег apply) шаблон **selectors_button** предназначен как раз для таких групповых действий. Он рисует кнопку выводящую popup-окно для указанного действия (set_available и set_unavailable в нашем случае) Итак, теперь нужно добавить два новых действия в контроллер %%AdminProductController%%: class AdminProductController extends lmbController { [...] function doSetAvailable() { return $this->_changeAvailability(true); } function doSetUnavailable() { return $this->_changeAvailability(false); } protected function _changeAvailability($is_available) { if(!$ids = $this->request->getArray('ids')) { $this->_endDialog(); return; } $products = lmbActiveRecord :: findByIds('Product', $ids); foreach($products as $product) { $product->setIsAvailable((int) $is_available); $product->save(); } $this->_endDialog(); } } Здесь мы воспользовались find()-методом класса lmbActiveRecord :: **findByIds($class_name, $ids, $params = array())**, который позволяет загрузить сразу несколько объектов по их идентификаторам. Справедливости ради отметим, что в данном случае, возможно, можно было обойтись простым UPDATE запросом к базе данных, чем сначала загружать объекты в память и изменять их по одному. Да, в простых случаях так действовать можно для оптимизации быстродействия, однако если с изменением статусов связаны какие-либо бизнес-правила, мы рекомендуем все же использовать API класса lmbActiveRecord. Покажем как бы выглядел код данного метода, если бы мы гнались за скоростью выполнения: [...] protected function _changeAvailability($is_available) { if(!$ids = $this->request->getArray('ids')) { $this->_endDialog(); return; } $is_available = (int) $is_available; $sql = 'UPDATE product SET is_available = ' . $is_available. ' WHERE id IN (' . implode(',', array_map('intval', $ids)) . ')'; $this->toolkit->getDefaultDbConnection()->execute($sql); $this->_endDialog(); } [...] Для выполнения запроса к базе данных напрямую нам потребовался явный доступ к объекту $connection, который можно получить из toolkit-а при помощи метода **getDefaultDbConnection()**. Для безопасности мы также пропустили каждый элемент массива $ids через метод intval. ===== Далее ===== Итак, следующий шаг: [[step5|Шаг5. Отображение списка товаров для покупателей. Поиск товаров]]