AngryGantz

Заметки о Web-стройках

JCFYii 6. Каталог товаров

| Comments

Так. Пора приступать к каталогу товаров. Буду делать отдельным модулем. В этот же модуль пойдёт всё, что относится к заказам. В конечном итоге У меня будет несколько групп заказчиков и плюс менеджеры, обрабатывающие заказ. Модуль буду делать максимально автономным. В данном случае он будет работать совместно со связкой Y-ii+rights, но в принципе это не обязательно. Просто надо будет реализовать какой-то простой API связи с юзерами и ролями, желательно в одном файле. Офрмлю скорее всего в виде хелпера или отдельного класса типа “ApiToUser” где будет проверка на принадлежность к группе и выдёргивание нужных полей из модуля Yii-User типа имени пользователя и, может быть, ещё каких либо полей профайла. Таким образом можно будет добиться полной автономии модуля от всего, кроме расширения bootstrap. Но на эту зависимость иду вполне осознанно. В принципе можно конечно bootstrap засунуть прямо в модуль, но он в приложении везде использоваться будет. Вобщем если вдруг этот эксперимент нечаянно по прихоти судьбы будет нужен ещё кому-то кроме меня, он сам сможет засунуть bootstrap в модуль и получить полную автономность.

Схема SQL

Нарисуем схемку для каталога.

SQL Схема каталога

Первое. Это не совсем правильная схема -) Она избыточна. Избыточна по наличию полей для фото у товаров, хотя есть отдельная таблица фото, она избыточна наличием таблицы отношений многие ко многим для связки товар-категория и при этом наличием поля в товарах с ИД “главной категории”. Она неправильна тем, что для категорий количество картинок ограничено и введено несколько полей для картинок вместо того, что-бы сделать тоже отдельную таблицу с фото для категорий… Вобщем на экзамене двойка :-) Но мне так удобнее по разным причинам. Так что, что хочу, то и делаю.

Второе. Таблица категорий реализует схему построения дерева “modified pre-order tree traversal algorithm”. На мой взгляд весьма эффективный метод построения деревьев, которым почти всегда пользуюсь. А тут ещё к своему удовольствию нашел расширение для Yii, работающее с этим методом.

Создание и подготовка модуля

Модуль Catalog просто создал через GII. В конфигурации добавил в секцию модулей. Теперь сразу подумаем, что потребуется для работы каталога. Во первых расширение, которое я упомянул выше для работы с деревьями категорий. Второе. В каталоге естественно будут картинки. Их надо будет ресайзить, делать превьюшки и т.д. Терпеть ненавижу практику, когда превьюшки картинок делают не создавая пережатый тумбнэйл, а просто указывая маленький размер для большой картинки. Может это болезнь со старых времен медленного интернета, но вот не приемлю. Мне это бредом кажется. Посему будем делать тумбнэйлы. Для этого использую расширение EWideImage. Кстати, расширения буду загонять в модуль каталога а не в ext приложения для обеспечения мало-мальской автономии создаваемого модуля.

Как подключить расширение внутри модуля? Очевидно нужен свой конфигурационный файл. И его надо как-то подхватывать. По примеру приложения делаю папку config и в ней файл main.php вот такого содержания:

1
2
3
4
<?php
return array(
);
?>

Теперь его как-то надо подхватить. В функции init в начало вставляем:

1
2
3
4
<?php
        $config = require dirname(__FILE__) . DIRECTORY_SEPARATOR . 'config'. DIRECTORY_SEPARATOR  .'main.php';
        $this->configure($config);
?>

Всё. По идее теперь можно туда писать то-же, что и для приложения, только локально для данного модуля.

Для удобства определяю Алиасы на модуль и его расширения. В конфиге Выше возврата массива пишем

1
2
3
4
5
6
<?php
$catalogConfigDir = dirname(__FILE__);
$rootCatalog = $catalogConfigDir . DIRECTORY_SEPARATOR . '..';
Yii::setPathOfAlias('mcatalog', $rootCatalog);
Yii::setPathOfAlias('mext', $rootCatalog . DIRECTORY_SEPARATOR . 'extensions');
?>

Приступим к подключению расширений, которые будем складывать в catalog/extensions

Расширение Nested Set

Страничка на офф сайте Yii: Nestedset

Реализует алгоритм построения деревьев Modified pre-order tree traversal algorithm Подробности о методе можно почитать здесь

Требования.

Таблица БД с деревом должна иметь следующие поля:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE IF NOT EXISTS `tree` (
  `id` int(11) NOT NULL auto_increment,
  `lft` int(11) NOT NULL,
  `rgt` int(11) NOT NULL,
  `level` int(11) NOT NULL,
  `name` varchar(255) NOT NULL default '',
  PRIMARY KEY  (`id`),
  KEY `lft` (`lft`),
  KEY `rgt` (`rgt`),
  KEY `level` (`level`),
  KEY `name` (`name`)
)

Названия полей не обязательно именно такие. Потом в модели можно будет настроить. В Таблицу до начала работы должна быть добавлена одна запись:

1
2
INSERT INTO `tree` (`id`, `lft`, `rgt`, `level`, `name`) VALUES
(1, 0, 1, 0, 'Root');

Установка.

  1. Кидаем расширение в catalog.extensions
  2. В конфигурации модуля:
1
2
3
4
5
6
7
<?php
return array(
    'import'=>array(
        'mext.nestedset.*'
    ),
);
?>

Собственно пока всё. Когда будет готова модель для категории, туда надо добавить функцию

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
public function behaviors(){
    return array(
        'TreeBehavior' => array(
            'class' => 'application.extensions.nestedset.TreeBehavior',
            '_idCol' => 'id',
            '_leftCol' => 'left',
            '_rightCol' => 'right',
            '_levelCol' => 'level',
        )
    );
}
?>

Расширение EWideImage

Домашняя страница EWideImage Установка - положить в папку расширений модуля и добавить в конфиг в секцию импорта 'mext.EWideImage.EWideImage'

Создание скелета

Удобная всё таки штука - GII. Мне в принципе понравилось, как я переоформил вьюшки модуля Yii и в этом модуле хочу сделать аналогичные. Большая часть вьюшек получается стандартной. Поэтому в папке mcatalog.lib.gii готовлю скелет с нужным оформлением для кодогенератора. За основу беру шаблон расширения Yii-bootstrap и немного переделываю вьюшки. Так же чуть меняю модель, добавляя функцию, возвращающую имя сущности в разных падежах и склонениях. Это нужно для менюшек и крошек во вьюшках. Код шаблонов в репозитории, никаких хитростей и проблем с подготовкой шаблона не было, так что тут и писать особо нечего. Не очень понятно, зачем нужна вьюшка с листом index, всё что надо есть в admin ну пусть будет, дальше посмотрим что из неё можно сделать, глядишь и пригодится. Честно переоформил в «свой» стиль и оставил в шаблоне.

Создаю все модели и CRUD через новый шаблон кодогенератора (путь к новому шаблону указывается в главном конфиге приложения в секции GII). Собственно, скелет создан.

Теперь время приводить в порядок его бакенд. Фронт будем делать потом. Кодогенератор сделал очень большой кусок работы, но её там ещё полно. Начнем с мелочей. Это отображение и редактирование связанных данных, типа вывода Имени Вендора вместо его id в карточке товара и выбор так же по имени. Желательно из ДропДаун листиков. Желательно симпатишных. Желательно с аякс-фильтром при наборе текста. Это наиболее удобно, особенно если в списке больше 10 позиций.

Расширение ESelect2

Домашняя страничка на офф сайте Это Yii врапер для плагина Select2 Jqube. Прост в использовании и удобен. Поэтому нафиг велосипеды, будем пользоваться им. Качаю, распаковываю в mext (напомню, это алиас папки extensions модуля каталога), прописываю в конфиге импорт ‘mext.select2.ESelect2’ . Можно пользоваться. Открываем _form для JProduct и меняем поле для product_vendor на вот такую конструкцию:

1
2
3
4
5
6
7
8
9
10
11
12
</p>Производитель 
<?php
$vendorItems = CHtml::listData(JVendor::model()->findAll(), 'vendor_id', 'vendor_name');
$this->widget('mext.select2.ESelect2',array(
  'model'=>$model,
    'attribute'=>'product_vendor',
    'data'=>$vendorItems,
    'htmlOptions'=>array(
    'class'=>'selectdrop5',
  ),
)); ?>
</p>

То есть сначала создаем полный список производителей в $vendorItems и запускаем расширение, в качестве параметров подсовывая ему модель продукта, созданный список и ключевой столбец ( product_vendor ) из модели для данных о вендорах. Проверяем, всё работает. Красяво, удобно и кода мизер. Полный феншуй.
Поясню о ‘class’=>’selectdrop5’ После запуска без класса выглядело всё не очень хорошо. Создал пакет классов для таких дропбоксов, синхронизируя их по размеру с разметкой bootdtrap:

1
2
3
4
5
[class*="selectdrop"] {
    display: block;
    margin-top: 10px;
}
.selectdrop5 {width:380px;}

Ну и по мере надобности буду добавлять циферки на хвосте класса с соответствующими размерами. .selectdrop5 по ширине соответствует стандартному span5 бутстрапа. эти стили положил в mcatalog.css/fmenu.css. Только теперь уже там не только стили, относящиеся к плавающей менюшке. Поэтому переобзываю файл в jcatalog.css и соответственно подправляю mcatalog.views.layouts.column1

Аналогичным способ меняю всё, где нужны подобные связанные дроплисты. Ну а там, где надо просто показать связанное поле, просто пишем конструкцию типа

1
2
3
<?php
 echo $model->Vendor->vendor_name;
?>

Соответственно строка в функции relations() модели выглядит так:

1
2
3
<?php
 'Vendor' => array(self::BELONGS_TO, 'JVendor', 'product_vendor'),
?>

self::BELONGS_TO - тип связи “многие к одному”. Соответственно для других типов связи такая конструкция как echo $model->Vendor->vendor_name; не прокатит по очевидным причинам. JVendor - класс модели родительской таблицы. product_vendor - внешний ключ.

Как это засунуть например в TbDetailView (это в файле view.php класса JProduct)? вместо ‘product_vendor’ в перечислении атрибутов вставляем

1
2
3
4
5
<?php
array( 'name'=>'product_vendor',
  'value'=>$model->Vendor->vendor_name,
),
?>

Вышесказанное справедливо и для *GridView. Соответствующие манипуляции проделываем везде, где требуется. Не забыть подправить везде сгенерированные функции relations. Генератор в качестве референсной таблицы вбивает собственно имя таблицы, а не класса, который мы создаем на базе этой таблицы. (Ну разве что в качестве имен классов использовать собственно имена таблиц)

Теперь ещё бы неплохо реализовать редактирование полей вроде описаний и характеристик в чём-нибудь ВайСиВинговском.

Подключаем CKEditor

CKEditor - это новая версия всем известного FCKeditor. Какая-то там история была на тему начальных букв “FCK” и вроде как из-за неё поменяли название. Ну не суть. Идём на сайт редактора и скачиваем отсюда последнюю версию. Вот тут отступлю от правила “всё в модуле” и содержимое архива (то есть папку ckeditor) отправляю в корневую директорию сайта. Потом подумаю, можно ли его вообще запустить внутри модуля, который в protected с deny all лежит. Впрочем не сильно это и надо.

Для публикации редактора с нужными полями воспользуемся классом CHtml. Минут 10 раздумывал, куда положить вспомогательный класс, ничего умнее папки helpers не придумал. Там создал маленький класс JCKeditor специально для публикации редактора. Код не привожу, лежит в сорцах. Вызывается

1
<?php   JCKeditor::activeCKEditor($model,'product_review',array('height'=>'200px'));  ?>

Модель понятно, второй параметр - атрибут модели, для которого вызываем редактор, 3 параметр - массив с настройками редактора. на формах _form соответственно добавляем вызов редактора там, где надо.

Связанные данные где надо показали, редактор к нужным полям прикрутили. Осталось только чуть переформатировать “простыни” форм, где они слишком длинные. Делаю это бутстраповской стандартной вёрсткой для наведения окончательного феншуя в бэкенде.

Забыл… Картинки!!! С ними надо что-то делать. И показывать и загружать и удалять… Ну что же, приступим.

Операции с фото.

Для начала определим базовую директорию для всех картинок товаров. Не буду извращаться (или это наоборот извращение?) Определим прямо в файле модуля:

1
2
3
<?php
public static $imgBase = 'images/catalog';
?>

То есть images лежит в корне сайта, как обычно. Переменная доступна из любого места вызовом

1
2
3
<?php
CatalogModule::$imgBase;
?>

Все картинки для товаров должны лежать строго в определенных местах. Места сии прописаны в категориях относительно базового пути для фото.

Порывшись на офф сайте нашел вполне подходящий инструмент.

Расширение lcswfupload

Страничка на оффсайте

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

Очень долго возился с ajax, хотелось всё сделать строго в идеологии фрэймворка. Вроде-бы получилось. В конце концов всё сделал почти так, как хотелось. Подробные комментарии написал в функциях контроллеров продукта и фотографий, а так же очень подробно в представлениях form и frameAddPhoto (оба относятся к контроллеру JProductController. Поэтому здесь подробно не описываю, должно быть понятно по коду и подробным комментариям в нём. Единственное, чем остался не совсем удовлетворен, это тем что ограничил работу с фото нового продукта. Все операции с фото после сохранения нового продукта. Можно сделать, но чувствую долго и нудно надо копаться. В остальном админка продуктов почти готова.

В целом фрэймворк пока нравится. Легкий, удобный, хорошо расписан API. Урок по созданию блога конечно ни о чём, но тут уж селяви.

Фронт одного продукта.

Люблю писать код мелкими кусками и потом собирать эти куски “рекваре онцами”. Вот и здесь не буду отступать от правила. Основным файлом будет views/jProduct/view.php. А в него уже кусками будем стыковать блоки. Фронт вообще надо писать более вдумчиво, чем админку. Тут MVC должно проявляться во всей красе, кода должно быть как можно меньше, один сплошной дизайн, дабы легко его можно было менять. Начнем с блока показа фото.

Блок показа фото _frontBlockPhoto.php

Надо сделать стильненько. Ну это CSS и немножко JQuery. Но сначала надо подготовить все данные в “готовом виде. В представление из контроллера будем отдавать несколько обьектов и массивов:

  • $model - собственно продукт, обьект JProduct
  • $addPhotosURL массив с готовыми URL с “дополнительными” фото продукта. (или “большими тумбами”, если размер самих фото не стандартизован)
  • $tumbsUrl - массив url к тумбнэйльчикам нужного для представления размера
  • $corePhotosURL массив URL к “основной” и “технической” фото продукта.

И готовить всё это удовольствие будем не в контроллере даже, а в модели. Ведь по сути идеология MVC сводится в основном к правилу «Делай модель как можно толще, а остальное как можно тоньше». Несколько утрированно, но по сути верно.

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

Какое-то время соображал, как лучше всего реализовать «перемещение» по тумбнэйлам в боксе. Что-бы и просто было с точки зрения кода и нагрузки лишней не создавало. В конце концов решил просто передать массив с «большими» фотографиями яваскрипту тупо через JSON (1 строка) и кодом JQuery (1 строка) отреагировать на клик по тумбнэйлу с запихиванием в div с большой фото данные из массива. В клике передаю просто индекс элемента. Вобщем по мне так симпапушно вполне. Вообще, хочу заметить, JQery хотя-бы в начальном приближении любой современный сайто строитель по моему знать должен. К стыду своему, я его знаю в очень очень начальном приближении. Иногда вместо того, что-бы искать и подрубать к сайту всякие монстро-плагины, по идее быстрее и красивее самому написать несколько строк. Но мозгов на это, к сожалению, хватает далеко не всегда.

Ну в данном случае даже моих хватило, что-бы нарисовать симпатишный бокс показа фото: ;-)

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$i = 0;
foreach ($tumbsUrl as $tumb) {
  echo '<li><img src="' . $tumb . '" OnClick="ChangeImg(' . $i . ')"></li>';
  $i++;
}
?>
<script>
    // Передача в JS массива с URL фотографий
    var bigImgs = JSON.parse('<?php echo json_encode($addPhotosURL) ?>');
    // При клике на тумбале выводим большое фото в img с id  "bigImg"
    function ChangeImg(id){ $("#bigImg").attr("src", bigImgs[id]); }
</script>

Ну и вокруг обвязка CSS, с которой собственно и провозился львиную долю времени подготовки это блока. Около часа наверное сидел «подгонял». Всё то одно не нравилось, то другое. Сейчас вроде нормально.

Блок показа описания и характеристик _frontBlockOverwiew.php

Ну тут делов было ровно на 10 минут - бутстраповские «табочки», вывод в лоб нужных полей и всё. Благо CSS уже готов для всей страницы. Собственно всё. Ещё блок цен (розничной и оптовой, с показом последней только Дилерам), но там вёрстки и на 5 минут не набралось. Стандартные алерты бутстрапа вполне сгодились.

Ещё убрал «второе меню» со всех глаз, кроме админских. А Админу оставил. Можно в правку заходить в «лоб», с фронта. Я считаю это удобным. Секурность вроде в порядке, так что мона.

Ну вот и готова первая «фронтальная» страничка. Потом правда придется к ней вернуться, когда с заказами и корзинками буду возиться.

Продолжим с фронтом. Идём снизу вверх, на очереди

Показ категории

Двигаем теперь от контроллера JCategoryController. Вобще-то хотелось при показе категорий избавиться от обращений «наверх», например для вытаскивания фото товаров и работать в представлении только с тем, что «спустили сверху». Но нам надо кучу товаров показывать в одном месте… Вариантов несколько. Первый это плюнуть на это желание и в представлении таки лезть наверх. В конце концов невилико увеличение кода от строчки $corePhotosURL= $model->getUrlCorePhoto(); Но оно же только начать… А через надцать промежутков времени черт ногу сломит. Вариант 2. Расширить модель продукта и запихнуть фото в неё. Вариант 3. Не впихивать в базовую модель сторонние объекты, а унаследоваться от неё и засунуть всю эту радость уже в наследника.

А если идти дальше, то можно вообще «родить» ещё от категории и туда засунуть атрибутами продукты, в которые запихнули картинки. Правда это монстро-обьект… Хотя ещё ведь вендор сверху есть, так что путь по умонстрению классов ещё открыт и широк. ;-).

Шутка конечно. Укрупнять классы нет никакой необходимости, $corePhotosURL= $model->getUrlCorePhoto(); вполне себе пойдёт в представлении. Ничем это не менее прозрачно, чем $prdName= $model->product\_name; Какая разница, к методу обращаться или атрибуту. Скорее наоборот «разрядить» - выделить например всю работу с картинками в отдельный класс. Просто пока смысла нет - модель ещё не такая уж толстая, а этот гипотетический класс больше и использовать негде. Хотя… Вот у меня для категории предусмотрено несколько картинок… Ладно, дальше видно будет.

А пока готовим вывод категории с теми классами что есть.

Задача: красивенько вывести весь товар из категории с полем show=>true и симпатишненько его изобразить. Ну и фото категорий куда-нибудь пристроить, для примера. Пока не трогаем мэни-мэни модель и отрабатываем только принадлежность товара к главной категории. мэни-мэни - просто столб на будущее развитие, если таковое будет.

Пока для данного проекта ни к чему.

Comments