Отношения, связь не по первичному ключу

При использование отношений в ActiveRecord связь по умолчанию устанавливается по первичному ключу, но бывает необходимость установить связь по другому полю. Для этого третий параметр(внешний ключ) в описание отношения оставляем пустой строкой и описываем связь через ‘on’
Чтобы не придумывать пример из головы, его любезно предоставил, из рабочего проекта,rmpic30 из конференции yii@jabber.ru, автор проекта http://itmages.ru:

'comments'=>array(self::HAS_MANY, 'Comments', '', 
    'on'=>'`comments`.`tid` = `t`.`tid`', 'order'=>'comments.created DESC'),

update 18-06-2011: в описание связи, `comments` — это название отношения, если у вас для модели не был задан другой алиас.

Расширение CGridView

В очередной раз используя мощный виджет CGridView, обнаружил, что для строк нет возможности устанавливать id, хотя для class такая возможность предусмотрена. Странно, что разработчики не учли такую возможность из «коробки». Решил расширить виджет и добавить данный функционал.
Не чего сложно поэтому сразу привожу, то что получилось PGridView.php:

<?php
/**
 * Class PGridView
 * @author: Konstantin Perminov (SpiLLeR)
 * @link: http://devkp.ru
 */
Yii::import('zii.widgets.grid.CGridView');

class PGridView extends CGridView {
    /**
     * @var  string a PHP expression that is evaluated for every table body row and whose result
     * is used as the HTML id for the row. In this expression, the variable <code>$row</code>
     * stands for the row number (zero-based), <code>$data</code> is the data model associated with
     * the row, and <code>$this</code> is the grid object.
     */
    public $rowIdExpression;

    /**
     * Renders a table body row.
     * @param integer $row the row number (zero-based).
     */
    public function renderTableRow($row) {
        $idStr = '';
        $classStr = '';

        if($this->rowIdExpression !== null) {
            $data=$this->dataProvider->data[$row];
            $idStr = $this->evaluateExpression($this->rowIdExpression, array('row'=>$row, 'data'=>$data));
        }

        if($this->rowCssClassExpression !== null) {
            $data = $this->dataProvider->data[$row];
            $classStr = $this->evaluateExpression($this->rowCssClassExpression,array('row'=>$row,'data'=>$data));
        } else if(is_array($this->rowCssClass) && ($n = count($this->rowCssClass)) > 0)
            $classStr = $this->rowCssClass[$row%$n];

        echo '<tr'.(!empty($idStr) ? ' id="'.$idStr.'"' : '')
                .(!empty($classStr) ? ' class="'.$classStr.'"' : '').' >' ;

        foreach($this->columns as $column)
            $column->renderDataCell($row);
        echo "</tr>\n";
    }
}

Пример использования CDbCriteria (методы)

В предыдущие заметки, я рассмотрел использование атрибутов класса CDbCriteria. Теперь рассмотрим методы этого класса. Почти все методы последним параметром принимают строку с помощью которой будет происходить конкатенация данного условия с остальной частью запроса, по умолчанию AND. Все будет рассмотрено на примерах.
Понеслась…

// создаем экземпляр класса CDbCriteria
$criteria = new CDbCriteria;
// установим некое начальное условие
$criteria->condition = 'user_id = :userId';
// формирует условие AND price BETWEEN 500 AND 1500
$criteria->addBetweenCondition('price', '500', '1500');
// формирует условие AND (date_create = '2010-12-23' OR status = 'success')
$criteria->addColumnCondition(array('date_create'=>'2010-12-23', 'status'=>'success'), 'OR')
// принимает первым параметром строку или массив строк - условий
// если передан массив строк, то конкатенация будет происходить через 2ой параметр,
// так же как и с остальными условиями, по умолчанию AND
// формирует условие AND (count_viewed <= :countViewed OR count_viewed = '26')
$criteria->addCondition("count_viewed <= :countViewed OR count_viewed = '26'");
// первый параметр, название колонки или валидный SQL
// второй параметр, массив значения
// формирует запрос OR (id IN ('3', '13', '24', '53', '69'))
$criteria->addInCondition('id', array('3', '13', '24', '53', '69'), 'OR');
// тоже самое, только NOT IN
// формирует запрос AND (id NOT IN ('3', '13', '24', '53', '69'))
$criteria->addNotInCondition('id', array('3', '13', '24', '53', '69'));
// первый параметр, название колонки или валидный SQL
// второй параметр, строка поиска, интерпретация зависит от следующих параметров
// третий параметр, экранирование строки поиска. По умолчанию true и строка поиска
// будет обрамлена % на концах. Если false, то строка поиска будет вставлена как есть
// четвертый параметр, строка для конкатенации с другими условиями
// пятый параметр, строка, тип LIKE(по умолчанию), NOT LIKE
$criteria->addSearchCondition('title', 'Some text', true, 'NOT LIKE');
// очень хороший метод, позволяет мержить несколько экземпляров класс CDbCriteria
// первым параметром принимает экземпляр класса с котором надо смержить текущий
// второй параметр, строка для конкатенации условий по умолчанию AND
$criteria1 = new CDbCriteria();
$criteria1->condition = 'date_update < ' . new CDbExpresssion('NOW()');
$criteria->mergeWith($criteria1, 'OR');
// ну и теперь надо исполнить все это))
$count = OrderLog::model()->count($criteria);

Есть еще метод toArray(), который возвращает массив из объекта.
Вот и все, не чего сложно, но надо знать про существование этого хорошего класса, про его методы и атрибуты, которые позволяет облегчить жизнь при работе с ActiveRecord.

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

Сегодня хочу описать пример использования такого мощного класса, как CDbCriteria, который предназначен для построения критериев запроса.
Постараюсь описать все атрибуты и методы этого класса. Критерий буду составлять к некой таблице user. И так начнем…

// создаем экземпляр  класса CDbCriteria
$criteria = new CDbCriteria;
// указываем алиас таблицы
$criteria->alias = 'user';
// условие то, что относится к WHERE
// можно использовать плейсхолдеры
$criteria->condition = 'id = :userId AND date_create < NOW()';
// если мы хотим отфильтровать дубликаты, 
// то distinct устанавливаем в значение true
// по умолчанию значение false;
$criteria->distinct = true;
// указываем поля по которым делать группировку, GROUP BY
$criteria->group = 'date_update';
// значения HAVING
$criteria->having = 'MAX(salary) > 10000';
// указываем индекс результирующего массива, полезная фишка, появилась с версии 1.1.5
// по умолчанию значение NULL и будут численный индекс начиная с 0
$criteria->index = 'full_name';
// указываем как приджойнить другую(ие) таблицу(ы)
// джойним некую таблицу 
$criteria->join = 'LEFT JOIN `profile` ON `user`.`id` = `profile`.`user_id`';
// максимальное количество записей которое может вернуть запрос
// если меньше 0, вернет все записи
$criteria->limit = '20';
// смещение
$criteria->offset = '3';
// сортировка
$criteria->order = 'date_create DESC, first_name ASC';
// параметры для плейсхолдеров
$criteria->params = array(':userId'=>$userId);
// поля для выборки, по умолчанию *
$criteria->select = 'id, first_name, last_name, last_visit';
// или
$criteria->select = array('id', 'first_name', 'last_name', 'last_visit');
// если значение true то данные из внешних(связанных) таблицы будут 
// выбраны одним запросом через JOIN. Работает только с отношениями в ActiveRecord
$criteria->together = true;
// отношения, работает только в ActiveRecord
$criteria->with = array('profile', 'comments', 'posts');
$users = User::model()->findAll($criteria);

Вот и все атрибуты данного класса. В следующей заметке постараюсь описать все методы.

Новый вид сообщений об ошибках в Yii

Сегодня Sam Dark сообщил о том, что в SVN выложено новое отображение ошибок в профайлере Yii.
Прочитать пост и посмотреть скриншот можно здесь. Обновляемся и пишем фитбеки разработчикам.

Мульти автокомплит с помощью CJuiAutoComplete

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

$autocompleteConfig = array(
    'model'=>$model,
    'attribute'=>'customers',
    'value'=>$model->customers,
    'source' =>'js:function(request, response) {
        $.getJSON("'.$this->createUrl('customers/autocomplete').'", {
            term: request.term.split(/,s*/).pop()
        }, response);
    }',
    'options' => array(
        'minLength' => '2',
        'showAnim' => 'fold',
        'search' =>'js: function() {
            var term = this.value.split(/,s*/).pop();
            if(term.length < 2)
                return false;
         }',
        'focus' =>'js: function() {
            return false;
         }',
        'select' =>'js: function(event, ui) {
            var terms =  this.value.split(/,s*/);
            terms.pop();
            terms.push(ui.item.value);
            terms.push("");
            this.value = terms.join(", ");
            return false;
        }',
    ),
    'htmlOptions' => array(),
);

Экшен нам отдает строку разделителем в которой является запятая. После отправки формы в атрибуте customers будет строка, которую можно распарсить и дальше делать, все что надо :).

Прикручиваем elFinder к CKEditor в Yii

Сейчас прикручивал к WYSIWYG редактору CKEditor файловый менеджер elFinder, т.к. дефолтный файловый менеджер для CKEditor платный. И по горячим следам решил написать заметку как это делать, потому что не так и не нашел внятного описания как же это сделать.

Для подключения CKEditor, я использовал расширения из yiiext. Скачиваем его и кладем его в папку с расширениями. У меня получилось:

application.extensions.ckeditor

Теперь создаем в этой папке, папку assets и кладем туда файлы самого WYSIWYG редактора CKEditor, которые скачиваем с официально сайта. Но виджет отказался запускаться из коробки. Чтобы он заработал открываем файл ECKEditor.php идем на строку 69 и меняем:

private $skin='kama';

на

public $skin='kama';

Дальше функция setLanguage($value), меняем:

if(isset($this->allowedLanguages[$language]))
    $this->language=$language;

на

if(isset($this->allowedLanguages[$lang]))
    $this->language=$lang;

Теперь виджет рабочий и его можно подключать в нужно месте:

<?php $this->widget('application.extensions.ckeditor.ECKEditor', array(
        'model'=>$model,
        'attribute'=>'content',
        'language'=>'ru',
        'editorTemplate'=>'full',
)); ?>

Надеюсь к моменту, когда эту заметку кто-то будет читать, ему не надо будет вносить изменения в код виджета.

Теперь будет прикручивать elFinder. Я использовал расширение опять же из yiiext. Скачиваем его и кладем в папку с расширениями. У меня это выглядит так:

application.extensions.elfinder

Здесь так же создаем папку assets и складываем туда папки css, js, images из архива elFinder скаченного с их сайта.

Теперь в контроллере, который у нас будет работать с загрузкой файлов, необходимо добавить в метод actions() следующие:

public function actions() {
    return array(
        'fileManager'=>array(
            'class'=>'application.extensions.elfinder.ElFinderAction',
         ),
    );
}

Создаем сам экшен:


public function actionBrowse() {
    // я создал специально отдельный пустой лейаут
    $this->layout='//layouts/clear';
    $this->render('browser');
}

Отображение:

<?php
$this->widget('application.extensions.elfinder.ElFinderWidget',array(
    'lang'=>'ru',
    'url'=>CHtml::normalizeUrl(array('site/fileManager')),
    'editorCallback'=>'js:function(url) {
        var funcNum = window.location.search.replace(/^.*CKEditorFuncNum=(\d+).*$/, "$1");
        window.opener.CKEDITOR.tools.callFunction(funcNum, url);
        window.close();
    }',
))?>

В подключение виджета ECKEditor, добавляем:

        'options'=>array(
            'filebrowserBrowseUrl'=>CHtml::normalizeUrl(array('controllerId/browse')),
        ),

controllerId — ваш контроллер в котором находится экшен browse.
Осталось последние, в корне, паблик части, создаем папку upload и на этом все. Пользуемся.

Надеюсь эта заметка кому-то сэкономит его время.

P.S. Да, и главное не забудь раставить права на доступ.

Обращение к атрибуту модели, как к элементу массива

Часто стал встречать, не понимание, в вопросе обращения к атрибутам модели в AR.
Если ваша модель унаследована от класса CActiveRecord, то к атрибуту можно обратится, как к элементу массива.
Пример:

$page = Page::model()->findByk(1);
echo $page->id . ' ' $page->title;
echo $page['id'] . ' ' . $page['title'];

Maintenance mode в Yii

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

'catchAllRequest'=>array(
    'controllerId/actionId',
    // можно передать параметры
    'param1'=>value1,
    'param2'=>value2,
),

controllerId — контроллер, допустим SiteController
actionId — экшен, в контроллере SiteController, допустим actionMaintenance. В экшене рендрим отображение для нашего режима.