Yii - Магазин 2: Каталог

27.02.2012

Сегодня мы разберем работу каталога (в нашем случае - это модель BookType), но перед этим хочу кое-что прокомментировать.

Поступил очень хороший и правильный комментарий к первой статье о магазине от Max Lapko. По-поводу таблиц в базе данных - что нужно добавить ещё одну таблицу tbl_purchases_books и вынести в неё id покупок и id купленных книг. Я с этим полностью согласен и сделал бы так, НО кроме этого было бы правильно вынести в отдельную таблицу Авторов и ещё одну таблицу для связи Авторов с Книгами... а это уже получается 3 дополнительные модели и соответственно логика для этих моделей. Этим я бы просто увеличил работу себе и тратил бы время не на что-то интересное, а на описание отношений и т.п. Связь MANY_MANY описана в офф. документации и разобраться с ней не сложно, поэтому давайте опустим эти таблички и перейдем к делу.

План статьи:

  1. Допиливаем модель BookType
  2. Контроллер BookTypeController

гитхаб - https://github.com/neo-classic/YiiShop

Для начала хочу объяснить, что я понимаю под Каталогом - в нашем случае это таблица, в которой будут храниться типы книг (книги по Php, книги по MySQL и т.п.). Кроме этого можно делать какую-то вложенность, к примеру - книги по программированию -> книги по Php. Эта вложенность может быть бесконечной. (Сейчас, на работе, я пишу сайт для магазина сплит-систем, каталог в этом случае - Бренды, мощность, тип. Т.е. те параметры, по которым отличаются продукты).

1) Допиливаем модель BookType

Модель BookType состоит из полей: id, title и parent_id. Title - название, parent_id - id родительской записи(к примеру - Программирование, для php).

Здесь нам понадобится метод, который будет возвращать название типа по id записи. Для этого в модель добавим:

/*
* Возвращает название родительской категории по id
*/
public function getParentTypeById($id) {
    $title = $this->model()->findByPk($id)->title;
    return $title;
}

И ещё один метод - вернем все категории:

/*
* Возвращает все категории
*/
public function getAllTypes() {
    return CHtml::listData($this->model()->findAll(), 'id', 'title');
}

И самое главное, чуть не забыл - для SEO-шников, добавьте в нашу таблицу tbl_book_type новое поле - url (varchar(255) - как и title), в него перед сохранением новой записи будим записывать транслит из title. Для чего это нужно - для ЧПУ, чтобы адресу у нас получались типа - http://your_domain/types/php а не http://your_domain/types/1.

Но и тут есть второе решение - не хранить url'ы в базе, а просто использовать преобразование во время выполнения(на ходу).

Для записи поля url, используем:

protected function beforeSave() {
    if(parent::beforeSave()) {
        if($this->isNewRecord) {
            $this->url = $this->translit($this->title);
        }
        return true;
    } else {
        return false;
    }
}

Соответственно перед сохранением записи - делаем транслит из титла, теперь нужно описать функцию translit($title). Обычно я выношу её в отдельный класс (class MyActiveRecord extends CActiveRecord), который и наследую - создадим файл MyActiveRecord в папке /protected/components/ со следующим содержимым:

class MyActiveRecord extends CActiveRecord {

    public function translit($str) {
        $translit = array(
            'А' => 'a', 'Б' => 'b', 'В' => 'v', 'Г' => 'g', 'Д' => 'd', 'Е' => 'e', 'Ё' => 'yo', 'Ж' => 'zh', 'З' => 'z',
            'И' => 'i', 'Й' => 'i', 'К' => 'k', 'Л' => 'l', 'М' => 'm', 'Н' => 'n', 'О' => 'o', 'П' => 'p', 'Р' => 'r',
            'С' => 's', 'Т' => 't', 'У' => 'u', 'Ф' => 'f', 'Х' => 'h', 'Ц' => 'ts', 'Ч' => 'ch', 'Ш' => 'sh', 'Щ' => 'sch',
            'Ъ' => '', 'Ы' => 'y', 'Ь' => '', 'Э' => 'e', 'Ю' => 'yu', 'Я' => 'ya',
            'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd', 'е' => 'e', 'ё' => 'yo', 'ж' => 'zh', 'з' => 'z',
            'и' => 'i', 'й' => 'i', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n', 'о' => 'o', 'п' => 'p', 'р' => 'r',
            'с' => 's', 'т' => 't', 'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'ts', 'ч' => 'ch', 'ш' => 'sh', 'щ' => 'sch',
            'ъ' => '', 'ы' => 'y', 'ь' => '', 'э' => 'e', 'ю' => 'yu', 'я' => 'ya',
            ' ' => '-', '!' => '', '?' => '', '('=> '', ')' => '', '#' => '', ',' => '', '№' => '',' - '=>'-','/'=>'-', '  '=>'-',
            'A' => 'a', 'B' => 'b', 'C' => 'c', 'D' => 'd', 'E' => 'e', 'F' => 'f', 'G' => 'g', 'H' => 'h', 'I' => 'i', 'J' => 'j', 'K' => 'k', 'L' => 'l', 'M' => 'm', 'N' => 'n',
            'O' => 'o', 'P' => 'p', 'Q' => 'q', 'R' => 'r', 'S' => 's', 'T' => 't', 'U' => 'u', 'V' => 'v', 'W' => 'w', 'X' => 'x', 'Y' => 'y', 'Z' => 'z'
        );
        return strtr($str, $translit);
    }

    public function cutText($str) {
        
    }

}

Поясню - у нас есть массив "символ"=>"преобразование", преобразуем через функцию strtr(). Кроме этого, в классе есть пустая функция cutText() - её пока не используем, но можно добавить урезание title до определенного количества символов или что-то подобное.

И не забываем указать в модели BookType extends MyActiveRecord.

На этом с моделью закончено(пока что), перейдем к контроллеру.

2) Контроллер BookTypeController

Этот контроллер нам понадобиться только в админке(для добавления, редактирования и удаления типов книг), т.к. сортировка книг по типу будет работать через экшен index контроллера BookController.

Просто удалим ненужные нам экшены: view и index. И почистим метод accessRules: удаляем ненужное, а остальное переносим к разрешенному только админу:

public function accessRules()
{
     return array(
          array('allow', // allow admin user to perform 'admin' and 'delete' actions
               'actions'=>array('admin','delete','create','update'),
               'users'=>array('admin'),
          ),
          array('deny',  // deny all users
               'users'=>array('*'),
          ),
     );
}

Так же почистим views: удаляем _view, index и view. Теперь давайте введем какую-нибудь корневую директорию. Для этого перейдем http://your_domain/index.php?r=bookType/create и получаем:

Создание категории 1

Title: Корень
Parent: 1

После добавления появилась ошибка, т.к. мы удалили экшен и представление view, поправим редиректы в контроллере BookTypeController(не буду сюда выкладывать код, посмотрите, пожалуйста, на гитхабе). Вот что получаем в экшене admin:

Управление категориями 1

Тут я уже поправил attributeLabels. На этой странице осталось исправить 2 вещи - в колонке Родительская категория вместо цифр выведем название и в колонке управления уберем кнопку view.
Первый недостаток исправляем методом, который объявили в модели выше - getParentTypeById: правим /views/bookType/admin.php

array(
    'name' => 'parent_id',
    'value' => 'BookType::model()->getParentTypeById($data->parent_id)'
),

Второе - нужно переопределить свойство template класса - CButtonColumn:

array(
    'class'=>'CButtonColumn',
    'template' => '{update}{delete}',
),

Код выше указывает, что нужно использовать 2 кнопки - update и delete.
Заодно давайте, на эту же страничку, добавим ссылку на добавление записи:

echo CHtml::link('Создать', array('/bookType/create'), array('style' => 'margin-left: 15px;'));

Получаем:

Управление категориями 2

Нажмем на ссылку Созать, и получим страничку на которой поле Родительская категория принимает только цифры, исправим это на выпадающий список. Воспользуемся вторым методом в модели BookType - getAllTypes(): правим /views/bookType/_form.php

echo $form->dropDownList($model, 'parent_id', BookType::model()->getAllTypes());

Получаем:

Добавление категории 2

Вроде бы всё! Давайте попробуем добавить корневую категорию - Программирование. И подкатегорию Программирования - Php.
Получаем:

Управление категориями 3

Как видим - всё работает. На сегодня это всё!
По поводу админки - сейчас мы написали Каталог, в следующем посте Продукт и уже после продукта объединим всё управление в отдельный модуль admin.

И, кстати, проверим что у нас там в базе:

Таблица в БД

Все четко ;)

blog comments powered by Disqus
Наверх