Представление редактирование отношения MANY_MANY в Yii

Давно лежит данная заметка в черновиках. Решил наконец выложить…

Почему-то часто вызывает сложности редактирования связи MANY_MANY, но самое просто это использовать мультиселект. Когда нужно выбирать из больших списков, то это действительно становится не удобным и лучше использовать какие-то расширения. Самые главные плюсы мультиселекта, что это быстро, просто и работает из коробки)) Ну и естественно пример:

<?php echo $form->labelEx($model, 'categories'); ?>
<?php echo $form->dropDownList($model, 'categories', CHtml::listData(
    Category::model()->active()->findAll(), 'id', 'title'
), array('multiple'=>'multiple', 'size'=>'7', 'encode'=>false)); ?>
<?php echo $form->error($model, 'categories'); ?>

Поясню что здесь написано.
categories — Это отношение MANY_MANY в модели $model.

Авторизация для различных сабдоменов

Задача: необходимо для разных доменов иметь разную авторизацию, т.е. при входе на одном домене, мы не должны входить на другом. Соответственно при выходе, на другом домене мы должны оставаться авторизованным на других доменов кроме этого.

Сходу не получилось решить данную задачу. В сети тоже не нашел ответа, а решается очень просто в конфиге для каждого домена необходимо указать две вещи:

1. Помогает иметь различную авторизацию на разных доменах, но при этом если выйти с какого-либо, выйдет везде.

            'user'=>array(
                ...
                'stateKeyPrefix'=>'yourDifferentPrefixForEveryDomain',
                ...
            ),

2. Чтобы выход не происходил сразу со всех доменов, необходимо:

            'session' => array (
                ...
                'sessionName' => 'differentSessionNameForEveryDomain',
                ...
            ),

Вот и все :).

YiiTalk подкаст о Yii фреймворке, или разговоры ни о чем ;)

Совсем не давно Юрий Беляков(belyakov.su) начал серию подкастов по Yii фреймворку. Изначально была идея вести это дело совместно, но в силу различных обстоятельств, которые как обычно не зависят от нас 😉 не получилось. Это наверное и к лучшему, двум ведущим там делать не чего :).

Был записан пилотный выпуск, затем в скором времени подкаст с Александром Макаровым(rmcreative.ru), кто не знает это наш соотечественник, который участвует в разработке ядра Yii. Ну и третий выпуск был записан со мной, до сих пор не понимаю чем удостоен такой чести ;). В плане запись подкаста с одним из активных участником yii сообщества Ekstazi.

Послушать все это дело можно здесь: yiitalk.podfm.ru yiitalk.rpod.ru

Хочу добавить к тому что было сказано в подкасте, а именно про книги. Как мне было подмечено по пхп стоит почитать: Котерова PHP5(у меня есть такая книга, читал давно и просто забыл уже про нее.) и PHP. Объекты, шаблоны и методики программирования(ее не читал, но все говорят хорошая).

З.Ы. не забывайте писать комменты, фитбэки. Для нас это очень важно, чтобы понять куда двигаться дальше 😉

Использование CDbCriteria в DAO

Не знал какое название дать посту, получилось, что получилось.
Очередной рабочий вечер, работа над интернет-магазином. Для уменьшения нагрузки отказываюсь в некоторых местах от использования AR. Естественно для выборки данных с использованием AR использую класс CDbCriteria. Но отказываясь от AR приходится отказаться и от CDbCriteria. Решил «поплакаться» на этот счет одному из знакомых разработчиков, который подсказал поковырять фреймворк и посмотреть действительно ли необходимо отказаться от CDbCriteria, когда не используешь AR. И как оказалось все не так плохо, иначе бы этот пост и не появился ;).
Ну а теперь к практическому решению:

criteria = new CDbCriteria;
// идет некий сумасшедший сбор критерии.
...
// Дальше необходимо создать экземпляр класса CDbCommandBuilder
$builder = new CDbCommandBuilder(Yii::app()->db->getSchema());
// Дальше если необходимо сделать выборку, то используем createFindCommand
// Если что-то удалить, то createDeleteCommand и т.п.
// Первым параметром передаем название таблицы или объект CDbTableSchema
$command = $builder->createFindCommand('product', $criteria);
// дальше можно использовать queryColumn(), queryAll() и т.п.
$productIds = $command->queryColumn();

Возможно это слишком низкоуровневое решение, но оно работает.

Игры в модуль, подмодуль в Yii

Поскольку Yii дает возможность из коробки использовать «любую» вложенность модулей, я решил опробовать написать админку в виде модуля и подмодулей.
Единый лейаут для модуля и его подмодулей.
Возникла логичная необходимость в том, что леайт модуля должен использоваться и в его подмодулях. Сделал я это следующим образом:

class AdminModule extends BaseWebModule {
	public function init() {
            Yii::app()->setViewPath($this->getViewPath());
	}
}

Генерация ссылок в подмодуль.
Из коробки ий может генерировать ссылки в в подмодуль только по абсолютному пути:

$this->createUrl('/module/submodule/controller/action')

НО! Возникают проблемы при подключение в подмодулях CGridView и т.п. Для этого я создал базовый контроллер для всех контроллеров в AdminModule и его подмодулей, переопределив метод CController.createUrl следующим образом:

    public function createUrl($route,$params=array(),$ampersand='&') {
        if($route==='')
            $route=$this->getId().'/'.$this->getAction()->getId();
        else if(strpos($route,'/')===false)
            $route=$this->getId().'/'.$route;
        if($route[0]!=='/') {
            if(($module=$this->getModule())!==null) {
                $route = $module->getId().'/'.$route;
                $route = $this->_getParentModuleRecursive($module, $route);
            }
        }
    
        return Yii::app()->createUrl(trim($route,'/'),$params,$ampersand);
    }
    
    private function _getParentModuleRecursive(CModule $module, $route) {
        if(($parentModule = $module->getParentModule()) !== null) {
            $route = $parentModule->getId().'/'.$route;
            $this->_getParentModuleRecursive($parentModule, $route);
        }
        return $route;
    }

По мере моего изучения возможности Yii — модуля, подмодуля буду пополнять данную заметку различной информацией.

Вышел Yii 1.1.8

Вышел Yii 1.1.8. По словам разработчиков внесено более 80 улучшений, изменений и фиксов багов. Из интересного:

    • Использование классов в правилах роутинга в urlManager’е
    • Улучшены автозагрузчик
    • Отправка сообщений в лог в реальном времени
    • Работаем со счётчиками в БД через AR
      Сомнительная фишка добавленная в AR, которая даже вызвала некоторые споры.
    • В clips теперь можно использовать параметры

Анонс можно прочитать на русском здесь.

CListView пример использования

Недавно в конференции были вопросы по CListView. Поэтому решил написать небольшой пример использования данного класса. Он предназначен для вывода сущностей(в частности моделей) в виде списка(упорядоченных блоков). Допустим у нас товары и их надо вывести в категории, по 12штук на странице с возможность сортировки по названию и цене товара. Код моделей опущу, т.к. он прост и не принципиален.
Контроллер отвечающий за вывод категории, ни чего сложно. Принимает параметром $alias — псевдоним категории. Производит простые манипуляции, получает данные от модели и отдает их в отображение. Все остальные значимые пометки сделал в коде.
CategoriesController.php

public function actionView($alias) {
    $category = Category::model()->getByAlias($alias);

    if(!$category)
        throw new CHttpException(404, 'Такой страницы не существует.');

    // заполняем заголовок страницы, хлебные крошки,
    // устанавливаем различные значения для СЕО и т.п.
    ...
    // формируем критерий для выборки товаров
    $criteria = new CDbCriteria();
    $criteria->condition = 'category_id = :categoryId';
    $criteria->params = array(':categoryId'=>$category->id);
    // создаем экземпляр CSort для сортировки в CGridView,
    // можно так же описать простым массивом
    $sort = new CSort();
    // имя $_GET параметра для сортировки,
    // по умолчанию ModelName_sort
    $sort->sortVar = 'sort';
    // сортировка по умолчанию 
    $sort->defaultOrder = 'good.final_price ASC';
    // включает поддержку мультисортировки, 
    // т.е. можно отсортировать сразу и по названию и по цене
    $sort->multiSort = true;
    // здесь описываем аттрибуты, по которым будет сортировка
    // ключ может быть произвольный, это будет $_GET параметр
    $sort->attributes = array(
                    'title'=>array(
                        'label'=>'названию',
                        'asc'=>'good.title ASC',
                        'desc'=>'good.title DESC',
                        'default'=>'desc',
                    ),
                    'price'=>array(
                        'asc'=>'good.final_price ASC',
                        'desc'=>'good.final_price DESC',
                        'default'=>'desc',
                        'label'=>'цене',
                    ),
                );
    $dataProvider = new CActiveDataProvider(Good::model()->active()->with('category'), 
            array(
                'criteria'=>$criteria,
                'sort'=>$sort,
                'pagination'=>array(
                    'pageSize'=>12,
                ),
            )
    );
    $this->render('view', array('model'=>$category, 'dataProvider'=>$dataProvider));
}

Представление view.php:

<?php $this->widget('zii.widgets.CListView', array(
    'dataProvider'=>$dataProvider,
    'itemView'=>'_good', // представление для одной записи
    'ajaxUpdate'=>false, // отключаем ajax поведение
    'emptyText'=>'В данной категории нет товаров.',
    'summaryText'=>"{start}&mdash;{end} из {count}",
    'template'=>'{summary} {sorter} {items} <hr> {pager}',
    'sorterHeader'=>'Сортировать по:',
    // ключи, которые были описаны $sort->attributes
    // если не описывать $sort->attributes, можно использовать атрибуты модели
    // настройки CSort перекрывают настройки sortableAttributes
    'sortableAttributes'=>array('title', 'price'),
    'pager'=>array(
        'class'=>'CLinkPager',
        'header'=>false,
        'cssFile'=>'/css/pager.css', // устанавливаем свой .css файл
        'htmlOptions'=>array('class'=>'pager'),
    ),
)); ?>

Представление _good.php не буду приводить, т.к. в нем нет ни чего сложно. В нем доступен объект $data. Вот и весь пример использования.

Алиасы моделей в Yii

При работе с моделями, yii сам формирует алиасы для таблиц. Для обычных запросов это t0, t1, t2, t3 и т.п. В отношениях алиас для связанной таблицы — это название отношения. Но для меня это не очень удобно, т.к иногда взглянув на сложный запрос в профайлере сложно понять что к чему. Поэтому я устанавливаю для всех моделей алиас равный названию таблицы. Казалось бы просто в методе init() или __construct() нужно вызвать setTableAlias() и передать ему $this->tableName(), но почему у меня так не удалось сделать, я не стал с этим разбиратся и устанавливаю алиас таблицы через defaultScope() моей базовой модели.

class BaseModel extends CActiveRecord { 
    ...
    public function defaultScope() {
        return array(
            'alias' => $this->tableName(),
        );
    }
    ...
}

Самое главное помнить, что если вы захотите использовать в какой-то модели defaultScope(), то надо смержить с базовым чтобы не утратить данное поведение, CMap::mergeArray() в помощь)

События в Yii

На днях решил освоить события в Yii. После мозгового штурма с знакомым, родилась некая схема работы.
Рассмотрим на примере, после успешной авторизации пользователя необходимо обновить поле даты последнего посещения(User->last_visit).
Для этого в класс идентификации необходимо объявить событие onAuthenticate:
PUserIdentity.php

    public function onAuthenticate(CEvent $event) {
        $this->raiseEvent('onAuthenticate', $event);
    }

Затем после успешной аутентификации пользователя необходимо вызвать событие:
PUserIdentity.php

    /**
     * Аутентификация пользователя
     * @return int
     */
    public function authenticate() {
        $user = User::model()->find('LOWER(email) = ?', array(strtolower($this->email)));

        if($user === null) {
            $this->errorCode = self::ERROR_EMAIL_INVALID;
        } else if(PPassword::encrypt($this->password, $user->salt) !== $user->password) {
            $this->errorCode = self::ERROR_PASSWORD_INVALID;
        } else {
            $this->_id = $user->id;
            $this->username = $user->username;
            $this->errorCode = self::ERROR_NONE;
            // вызываем событие
            $this->onAuthenticate(new CEvent($user));
        }

        return !$this->errorCode;
    }

Теперь самое интересное, все обработчики централизовано хранятся в массивеconfig/handlers.php:


return array(
    'onAuthenticate'=>array(
        array('User', 'updateLastVisit'),
    ),
);

Метод updateLastVisit в модели User в качестве параметра должен принимать экземпляр класса CEvent. Описание данного метода я не стану приводит.
Для того чтобы зарегистрировать обработчики событий в классе, напишем статический метод, который будет это делать и вызывать его при инициализации класса. У меня он расположен в хелпере.

    /**
     * Навещивает обработчики событий
     * @static
     * @param CComponent $object
     * @return object
     */
    public static function attacheHandlers(CComponent &$object) {
        $config = PConfig::getEventsConfig();
        foreach($config as $event=>$handlers) {
            if(!method_exists(get_class($object), $event))
                continue;

            foreach($handlers as $handler)
                $object->$event = $handler;
        }
    }

Теперь в нужном классе его нужно вызвать при инициализации:
PUserIdentity.php

    public function __construct($email, $password) {
        $this->email = $email;
        $this->password = $password;
        // PUtils хелпер где распологается метод
        PUtils::attacheHandlers($this);
    }

Вышел Yii 1.1.7

Вышел в свет Yii 1.1.7. По словам разработчиков внесено более 90 улучшений, изменений и фиксов багов. На мой взгляд самое вкусное это кэширование запросов, пример:

$posts = Post::model()->cache(1000)->findAll();

Так же реализована интересная вещь в CUrlManager, теперь в правилах валидации можно указаьть типа запроса: GET, POST, PUT, пример:

'rules'=>array(
    '<controller:w>/<id:d+>' => array('<controller>/view', 'verb'=>'GET'),
    '<controller:w>/<id:d+>' => array('<controller>/update', 'verb'=>'PUT, POST'),
),

Официальный анонс можно прочитать здесь.