Скорость eval и анонимных функций

Всё ещё используете eval там, где его можно не использовать? Вот ещё одна причина посмотреть в сторону PHP 5.3 и анонимных функций:

$count = 50;
$start = microtime(true);
$y = 0;
for($i = 0; $i < $count; $i++)
{
    eval('++$y;');
}
 
printf("#1, result is %d, done in %f\n", $y, microtime(true) - $start);
 
$start = microtime(true);
$y = 0;
for($i = 0; $i < $count; $i++)
{
    $f = function() use (&$y)
    {
        return ++$y;
    };
    $f();
}
 
printf("#2, result is %d, done in %f\n", $y, microtime(true) - $start);

На выходе получаем:

d:\src>php callback_performance.php
#1, result is 50, done in 0.000415
#2, result is 50, done in 0.000270

d:\src>php callback_performance.php
#1, result is 50, done in 0.000413
#2, result is 50, done in 0.000261

d:\src>php callback_performance.php
#1, result is 50, done in 0.000412
#2, result is 50, done in 0.000273

Кроме того, что анонимные функции удобней, они ещё и быстрее.

Добавлено: 20 Мая 2018 07:37:39 Добавил: Андрей Ковальчук

PHP и юникод в стрэктрейсе

Какое-то время назад creocoder наткнулся на ????? вместо значения параметра в логах ошибок Yii. После анализа проблемы стало ясно, что нашёлся баг в PHP.

Exception::getTraceAsString и Exception::__toString не работают с юникодом в значениях параметров при построении stacktrace. В результате для

<?php
function test($arg){
    throw new Exception();
}
 
try {
    test('тест');
}
catch(Exception $e) {
    echo $e->getTraceAsString();
    echo (string)$e;
}

получаем
d:\web\usr\local\php54>php.exe d:\src\exception_wrong_trace\test.php
#0 D:\src\exception_wrong_trace\test.php(7): test('????')
#1 {main}exception 'Exception' in D:\src\exception_wrong_trace\test.php:3
Stack trace:
#0 D:\src\exception_wrong_trace\test.php(7): test('????')
#1 {main}

Обойти можно собрав строку руками на основе массива, полученного через Exception::getTrace.

Добавлено: 17 Мая 2018 11:21:01 Добавил: Андрей Ковальчук

Потребление памяти и длина имени переменной в PHP

Недавно всплыло обсуждение именования переменных в Yii, а именно

class CComponent
{
    private $_e;
    private $_m;

Я согласен, что выглядит плохо, но именно в данном случае такие имена переменных более-менее оправданы и в Yii2 останутся примерно такими же:
class Component extends \yii\base\Object
{
    /**
     * @var Vector[] the attached event handlers (event name => handlers)
     */
    private $_e;
    /**
     * @var Behavior[] the attached behaviors (behavior name => behavior)
     */
    private $_b;

Дело в том, что каждый экземпляр класса с нормальными именами переменных будет кушать больше памяти. Например, $_behaviors и $_events скушают 8 байт на экземпляр.

Конечно, 8 байт ничто и сокращать таким образом переменные в обычных приложениях определённо не стоит. Но не в случае самого-самого базового класса фреймворка. В зависимости от приложения, наследников Component может быть довольно много. Например, на 1000 объектах AR накладной расход выльется в 8 килобайт.

Добавлено: 07 Мая 2018 06:39:10 Добавил: Андрей Ковальчук

Использование Google Map API для поиска координат точки по ее адресу.

Простой пример метода модели фреймворка Yii, для пооиска координат точки на карте по ее адресу. Используется Google Map API для поиска и CURL для обработки его результатов.

Одно маленькое замечание: частой проблемой при использовании поискового API - нессответствие кодировок. Рекомендую адрес который передается в метод searchPoint передавать посредством POST, так как в этом случае не будет бится кодировка (через GET бывают такие проблемы).

public function searchPoint($address=''){
 $key = Yii::app()->params['gmap'];
 $address = urldecode($address);

 $url = "http://maps.google.com/maps/geo?q={$address}&output=json&key={$key}"; 

 $ch = curl_init(); 

 curl_setopt($ch, CURLOPT_URL, $url); 
 curl_setopt($ch, CURLOPT_HEADER,0);
 curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER["HTTP_USER_AGENT"]);
 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

 $data = curl_exec($ch); 
 curl_close($ch);

 $data = CJSON::decode($data); 

 $coord = $data['Placemark'][0]['Point']['coordinates'];
 return array('lng'=>$coord[0],'lat'=>$coord[1]); 
}

Добавлено: 02 Мая 2018 18:33:04 Добавил: Андрей Ковальчук

Контроль доступа с использованием ролей (RBAC)

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

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

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

if (!Yii::app()->user->checkAccess('createUser')) {
    throw new CHttpException(403, 'Forbidden');
}
//остальной код…

В теории всё просто. Но на практике, документации и примеров по этой теме практически нет (надеюсь, это скоро изменится).

Основные источники информации (на русском): Аутентификация и авторизация и RBAC и описание ролей в файле. На английском хороших и подробных примеров, к сожалению, я не нашел.

Примечание. Очень советую прочитать эти статьи, прежде чем переходить к моему примеру.

Когда я первый раз решил использовать RBAC, то выяснилось, что есть множество нюансов, которые приходится учитывать при работе с этой библиотекой. Ничего запредельно сложного и недоступного для понимания, но «ковырялся» я довольно долго ;)

В этой статье мы рассмотрим пример создания несложной системы управления пользователями (идея взята из одного web приложения).

Постановка задачи.

Есть три типа пользователей.

Обычный (user) – может создавать и редактировать свои данные (например, список контактов) и изменять свои логин/пароль.

Администратор (admin) – может создавать новых пользователей, но не может изменять их данные. Он может изменить свои собственные логин и пароль, но не может изменить роль.

Суперпользователь (root) – может выполнять любые операции с пользователями.

Ни admin, ни root не имеют доступа к персональным данным пользователя (которые он создаёт при работе с приложением).

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

Шаг первый. Выбираем тип хранилища для ролей и операций.

Для этих целей Yii позволяет использовать PHP файл (CPhpAuthManager) или базу данных (CDbAuthManager).

Если вы предполагаете, что количество пользователей будет большим, то лучше использовать БД. Но для знакомства с библиотекой лучше использовать PHP файл, т.к. читать его легче. К тому же перейти от одного типа хранилища к другому совсем несложно.

Для того, чтобы Yii «узнал» о вашем выборе, в массив с настройками (config/main.php) нужно добавить следующий элемент.

'components'=>array(
…
    'authManager'=>array(
        'class' => 'CPhpAuthManager',
    ),
),

Шаг второй. Создадим таблицу в БД для хранения пользователей.

Эта таблица будет состоять из шести полей.

u_id – первичный ключ;
u_name – имя пользователя;
u_email – адрес почты (используется как логин);
u_pass – пароль;
u_state – статус (активен, заблокирован);
u_role – роль пользователя (root, admin, user).

В данном примере для нас играет роль последнее поле (u_role).

Шаг третий. Создаём операции, роли и задачи.

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

Сразу перейдём к созданию файла с этими настройками.

Т.к. в качестве хранилища мы выбрали PHP файл, то можно создать его вручную (этот способ подробно рассмотрен в статье RBAC и описание ролей в файле).

Но, на мой взгляд, гораздо удобнее использовать API. Поэтому создадим небольшой инсталляционный скрипт (контроллер SiteController метод actionInstall).

Примечание. Данные будут сохранены в файле protected/data/auth.php. Файл будет создан автоматически, поэтому запись в эту папку должна быть разрешена.

public function actionInstall() {
 
    $auth=Yii::app()->authManager;
     
    //сбрасываем все существующие правила
    $auth->clearAll();
     
    //Операции управления пользователями.
    $auth->createOperation('createUser', 'создание пользователя');
    $auth->createOperation('viewUsers', 'просмотр списка пользователей');
    $auth->createOperation('readUser', 'просмотр данных пользователя');
    $auth->createOperation('updateUser', 'изменение данных пользователя');
    $auth->createOperation('deleteUser', 'удаление пользователя');
    $auth->createOperation('changeRole', 'изменение роли пользователя');
     
    $bizRule='return Yii::app()->user->id==$params["user"]->u_id;';
    $task = $auth->createTask('updateOwnData', 'изменение своих данных', $bizRule);
    $task->addChild('updateUser');
 
    //создаем роль для пользователя admin и указываем, какие операции он может выполнять
    $role = $auth->createRole('admin');
    $role->addChild('createUser');
    $role->addChild('viewUsers');
    $role->addChild('readUser');
    $role->addChild('updateOwnData');
     
    //все пользователи будут создаваться по-умолчанию с ролью user,
    //только root может менять роль другого пользователя
     
    //создаем роль для пользователя root 
    $role = $auth->createRole('root');
    //наследуем операции, определённые для admin'а и добавляем новые
    $role->addChild('admin');
    $role->addChild('updateUser');
    $role->addChild('deleteUser');
    $role->addChild('changeRole');
     
    //создаем операции для user'а
    $bizRule='return Yii::app()->user->id==$params["contact"]->c_user_id;';
     
    $auth->createOperation('createContact','создание контакта');
    $auth->createOperation('viewContacts','просмотр списка контактов');
    $auth->createOperation('readContact','просмотр контакта', $bizRule);
    $auth->createOperation('updateContact','редактирование контакта',$bizRule);
    $auth->createTask('deleteContact','удаление контакта',$bizRule);
     
    //создаем роль user и добавляем операции для неё
    $user = $auth->createRole('user');
 
    $user->addChild('createContact');
    $user->addChild('viewContacts');
    $user->addChild('readContact');
    $user->addChild('updateContact');
    $user->addChild('deleteContact');
    $user->addChild('updateOwnData');
 
    //создаем пользователя root (запись в БД в таблице users)
    //тут используем DAO, т.к. AR автоматически назначит пользователю роль user
    $sql = 'INSERT INTO users(u_name, u_email, u_pass, u_state, u_role)'
        .' VALUES ("root", "test@test.ru", "'.md5('11111')
        .'", '.Users::STATE_ACTIVE.', "'.Users::ROLE_ROOT.'")';
    $conn = Yii::app()->db;
    $conn->createCommand($sql)->execute();
     
    //связываем пользователя с ролью
    $auth->assign('root', $conn->getLastInsertID());
 
    //сохраняем роли и операции
    $auth->save();
     
    $this->render('install');
}

Метод получился довольно объемный, но в нём большую часть занимаю вызовы createOperation и addChild, которые создают операции и связывают их с ролями.

Большинство операций в этом примере соответствуют методам контроллера (CRUD), но они могут быть любыми. Например, такими как changeRole, позволяющими изменять одно единственное поле записи.

Обратите внимание. После создания операций и ролей доступ ограничен не будет. Вы должны будете сами проверить у пользователя наличие прав с помощью метода checkAccess.

Отдельного внимания заслуживает использование бизнес правил (bizRule) в операциях.

Бизнес правило представляет собой обычный PHP код, который должен возвращать true или false. Этот код может получить массив с данными, который будет доступен через переменную $params.

Рассмотрим правило
$bizRule='return Yii::app()->user->id==$params["user"]->u_id;';

Здесь мы проверяем, соответствуют ли id текущего пользователя (который выполнил вход) и id записи в таблице пользователей, которую он хочет изменить. Таким образом, это правило позволит пользователю редактировать только свои данные.

В конце метода мы создаём пользователя root. Дело в том, что все пользователи будут создаваться с ролью user и только root может изменять роль. Поэтому мы создаём его сразу при инсталляции.

После создания пользователя назначаем ему роль (метод assign) и сохраняем изменения
$auth->save();

Шаг четвертый. Создание модели для работы с пользователями.

Как обычно, создаём модель с помощью генератора (gii) и затем вносим свои изменения.

Весь код модели я приводить здесь не буду (в конце статьи есть ссылка на архив с примером). Рассмотрим только измененные методы.
public function beforeSave() {
    parent::beforeSave();
    $this->u_pass = md5($this->u_pass);
    /*
     * Если пользователь не имеет права изменять роль, то мы должны
     * установить роль по-умолчанию (user)
     */
    if (!Yii::app()->user->checkAccess('changeRole')) {
        if ($this->isNewRecord) {
            //ставим роль по-умолчанию user
            $this->u_role = Users::ROLE_USER;
        }
    }
    return true;
}

Перед созданием новой записи мы проверяем, имеет ли текущей пользователь право изменять роли, если нет, то ставим роль по-умолчанию (user).

После сохранения (или создания) записи, нужно назначить пользователю роль. Права пользователя мы уже проверили и роль установили, поэтому сейчас просто назначаем пользователю роль (метод assign).

Предварительно, с помощью метода revoke удаляем связь между пользователем и ролью (если такая существовала). Если связь не удалить, то когда root будет изменять роли, у нас появятся пользователи с несколькими ролями.
public function afterSave() {
    parent::afterSave();
    //связываем нового пользователя с ролью
    $auth=Yii::app()->authManager;
    //предварительно удаляем старую связь
    $auth->revoke($this->prevRole, $this->u_id);
    $auth->assign($this->u_role, $this->u_id);
    $auth->save();
    return true;
}

При удалении пользователя не забываем удалить связь между ним и ролью.
public function beforeDelete() {
    parent::beforeDelete();
    //убираем связь удаленного пользователя с ролью
    $auth=Yii::app()->authManager;
    $auth->revoke($this->u_role, $this->u_id);
    $auth->save();
    return true;
}

Как видите, принцип работы достаточно простой. Главное, не забывайте вызывать $auth->save(); чтобы сохранить изменения.

Шаг пятый. Контроллер и представления.

Как и в случае с моделью, создаем контроллер и представления с помощью генератора (gii).

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

Опять же, весь контроллер целиком рассматривать нет смысла, он есть в архиве с исходниками. В качестве примера рассмотрим метод обновления данных пользователя.

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

В первый раз проверяем, пытается ли изменить роль пользователь, у которого на это нет прав.

С помощью второй проверки убеждаемся, что у пользователя есть права на изменение данной записи.
public function actionUpdate()
{
    $model=$this->loadModel();
     
    //проверяем, можно ли пользователю изменять роль
    if (isset($_POST['Users']['u_role']) && !Yii::app()->user->checkAccess('changeRole')) {
        throw new CHttpException(403,'Forbidden');
    }
 
    //проверяем, может ли пользователь изменять данную запись
    if (!Yii::app()->user->checkAccess('updateUser')
            && !Yii::app()->user->checkAccess('updateOwnData', array('user'=>$model))) {
        throw new CHttpException(403,'Forbidden');
    }
     
    if(isset($_POST['Users']))
    {
        $model->prevRole = $model->u_role;
        $model->attributes=$_POST['Users'];
        if($model->save())
            $this->redirect(array('view','id'=>$model->u_id));
    }
 
    $this->render('update',array(
        'model'=>$model,
    ));
}

В представлении (views/users/_form.php) убираем из формы поле «Роль» для пользователя у которого нет прав её изменять.
…
<?php if (Yii::app()->user->checkAccess('changeRole')) { ?>
    <div class="row">
        <?php echo $form->labelEx($model,'u_role'); ?>
        <?php echo $form->dropDownList($model,'u_role',array(
                Users::ROLE_USER=>Users::ROLE_USER,
                Users::ROLE_ADMIN=>Users::ROLE_ADMIN,
                Users::ROLE_ROOT=>Users::ROLE_ROOT,
            ));
        ?>
        <?php echo $form->error($model,'u_role'); ?>
    </div>
<?php } ?>
…

На этом мы остановимся.

Добавлено: 22 Апреля 2018 11:37:12 Добавил: Андрей Ковальчук

Создание запросов с условием IN

Последнее время мне довольно часто приходится работать с фреймворком Yii. И иногда возникают вопросы, ответы на которые не очевидны (во всяком случае для меня).

Попробую объяснить. Сам фреймворк, на мой взгляд, достаточно удобный и документация подробная. Но классов много, и формат передачи данных их методам часто сильно влияет на результат.

Рассмотрим небольшой пример – использование оператора IN.

Т.е. нужно сформировать примерно такой запрос.

SELECT * FROM tbl_users WHERE id IN (1, 2, 3)


В Полном руководстве на эту тему информации я не нашел, а копаться в методах CActiveRecord времени не было.

Вообще, я очень не люблю такие ситуации. С одной стороны, понятно как нужно написать запрос, с другой – хочется сделать это красиво, используя встроенную библиотеку. Копаться в документации не хочется, ведь на это уходит время, а запрос можно составить за считанные минуты. Но не все так просто.

Сначала покажу решение «в лоб».

$ids = array(1, 2, 3);
 
$dataProvider=new CActiveDataProvider('User',array(
    'criteria'=>array(
        'condition'=>'id IN ('.implode(',', $ids).')',
    )
));
 
$this->render('admin',array(
    'model'=>$dataProvider,
));

Тут мы формируем массив со значениями, которые будут перечислены после IN. А затем, с помощью функции implode преобразуем его в строку и просто вставляем в запрос.

Недостатки такого подхода очевидны.

Во-первых, выглядит код не очень красиво.

Во-вторых, в реальном приложении значения для массива $ids будут откуда-то поступать, возможно, из параметров запроса. И если массив $ids окажется пустым, то при выполнении запроса возникнет ошибка.

В-третьих, полученные значения нужно проверить. А написать что-то вроде

'params'=>array(':id'=>implode(', ', $ids)),

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

Как видите, возникают проблемы практически на пустом месте. Нужно написать код проверок, решить, что делать с пустым массивом, затем протестировать этот код. И все ради решения тривиальной задачи.

Решение с помощью библиотеки Yii.

Оно выглядит гораздо проще.

$model = User::model()->findAllByAttributes(array('id'=>array(1, 2, 3)));
$dataProvider=new CActiveDataProvider('User');
$dataProvider->setData($model);
 
$this->render('index',array(
    'dataProvider'=>$dataProvider,
));

Метод findAllByAttributes класса CActiveRecord позволяет искать записи в БД по названию поля и значению. При этом если значение является массивом, то используется оператор IN.

Этот же самый код можно записать немного иначе.

$dataProvider=new CActiveDataProvider('User');
$dataProvider->criteria->addInCondition('id', array(1,2,3));
 
$this->render('index',array(
    'dataProvider'=>$dataProvider,
));

Тут уже используется метод addInCondition класса CDbCriteria. Если вы работаете с CActiveDataProvider, то получить доступ к объекту CDbCriteria можно с помощью свойства criteria.

Теперь проведем несколько экспериментов.

Попробуем в массиве передать строку.

$dataProvider->criteria->addInCondition('id', array(1,2,'test'));

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

SELECT * FROM 'tbl_user' WHERE "id" IN (1, 2, 0)

Т.е. текстовое значение было заменено нулем, что вполне логично, т.к. поле id имеет тип INT.

Если запрос будет выполняться к полю текстового типа, то текстовые и числовые значения будут вставлены в кавычках.

$dataProvider->criteria->addInCondition('id', array(1, 'admin','user'));

сформирует такой запрос

SELECT * FROM 'tbl_user' WHERE "id" IN ('1','admin', 'user')

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

$dataProvider->criteria->addInCondition('id', array());

SELECT * FROM 'tbl_user' WHERE 0=1

Как видите, использование встроенных библиотек всё-таки оправдывает время, потраченное на их изучение ;)

Только не забывайте при работе с документацией Yii учитывать один момент. Классы библиотеки тесно связаны между собой, поэтому искать нужно не только в методах класса, с которым вы непосредственно работаете, но и в методах объектов, которые использует этот класс.

Так CActiveDataProvider использует CDbCriteria для формирования условий, CPagination – для разбивки на страницы, CSort – для сортировки и т.д.

Добавлено: 22 Апреля 2018 11:31:25 Добавил: Андрей Ковальчук

Связанные таблицы и limit

По умолчанию для всех отношений, включенных в 'жадную' загрузку, будет сгенерировано и выполнено одно выражение с использованием JOIN. Если в основной таблице есть опции запроса LIMIT или OFFSET, то сначала будет выполнен этот запрос, а затем другой SQL-запрос, который возвращает все связанные объекты. Раньше, в версии 1.0.x, по умолчанию вызывалось N+1 SQL-запросов, если 'жадная' загрузка включала N отношений HAS_MANY или MANY_MANY.
(перевод взят отсюда
)[/QUOTE]

В моем примере с игровым сайтом как раз возникла такая ситуация. Есть две таблицы, с играми (ygs_games) и их жанрами (ygs_types). Отношение между таблицами многие-ко-многим.

При этом необходимо выводить игры определённого жанра с разбивкой на страницы (пагинацией), т.е. использовать в запросе limit и offset.

Yii позволяет сформировать запросы на получение этих данных двумя способами, которые называются: «жадная» загрузка и «ленивая» загрузка. Первый предполагает формирование одного запроса, в котором будут получены все необходимые данные. В этом запросе будут использованы объединения (JOINs). Во втором случае используется несколько запросов. Сначала выбираются нужные записи из первой таблицы (первый запрос), затем — данные из связанной таблицы (для каждой записи из первой таблицы выполняется дополнительный запрос).

Проблема, с которой я столкнулся.

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

$criteria=new CDbCriteria;
$criteria->condition = 't_id=:t_id';
$criteria->params = array(':t_id'=>$_GET['type_id']);
$criteria->with = array('ygs_types'=>array('together'=>true));
 
$pages=new CPagination(Games::model()->published()->count($criteria));
$pages->pageSize=self::PAGE_SIZE;
//этот метод добавляет параметры limit и offset в объект $criteria, т.е. в запрос
$pages->applyLimit($criteria);
 
$models=Games::model()->findAll($criteria);

здесь t_id — первичный ключ в таблице жанров.
ygs_types — название элемента в массиве, который возвращает метод relations() модели (этот элемент просто описывает отношение многие-к-многим).

При этом формировался один запрос по методу «жадной» загрузки, который возвращал все необходимые данные.

Но в новых версиях Yii этот код не работает.

Дело в том, что как только мы указываем LIMIT или OFFSET в запросе, который включает связанные таблицы, библиотека Yii разбивает запрос на два. Сначала выполняется запрос только к первой таблице и именно к нему применяется LIMIT.
SELECT `t`.`g_id` AS `t0_c0`, ... FROM `ygs_games` `t`  WHERE ((g_state=0) AND
(t_id=:t_id)) LIMIT 10. Bind with parameter :t_id='2'

И сразу же возникает ошибка.

Column not found: 1054 Unknown column 't_id' in 'where clause'


Причина ошибки в том, что yii пытается вставить параметр для поля t_id, которого нет в таблице ygs_games. Таблица ygs_types будет присоединена в следующем запросе (с помощью JOIN), но LIMIT применяется именно в первом запросе к первой таблице, а нужно, чтобы он применялся к результату объединённого запроса.

Отключить это поведение, судя по всему, нельзя.

Но можно использовать «ленивую» загрузку, и при этом будет выполняться также два запроса.

Первым запросом мы находим нужный жанр в таблице ygs_types.

$type = Types::model()->findByPk($_GET['type_id']);

Тут выполняется следующий запрос

SELECT * FROM `ygs_types` `t` WHERE `t`.`t_id`=8 LIMIT 1

Затем, получаем связанные с этим жанром игры.
Здесь есть нюанс. Вызов

$type->ygs_games

нам не подходит, т.к. мы не сможем указать параметры, например тот же limit (если нужно его изменять).

Примечание. Можно, конечно, указать параметры в массиве, который возвращает метод relations, но этот метод имеет свои недостатки. Например, чтобы изменить параметры (тот же limit), их придется хранить в отдельном массиве, и его нужно будет объединять с массивом, который возвращает relations().

Поэтому, на мой взгляд, удобнее использовать метод [URL=http://www.yiiframework.com/doc/api/CActiveRecord#getRelated-detail]getRelated
, в его третьем параметре можно передать массив с настройками.
Код будет выглядеть так.

$params = array(
        'limit'=>self::PAGE_SIZE,
        'condition'=>'g_state='.Games::PUBLISHED,
        'order'=>'g_added DESC',
);
$games = $type->getRelated('ygs_games',false, $params);

При этом Yii формирует следующий запрос (self::PAGE_SIZE = 10)

SELECT `ygs_games`.`g_id` AS `t1_c0`, ... FROM `ygs_games` `ygs_games` INNER JOIN
`ygs_games_types` `ygs_games_ygs_games` ON
(`ygs_games_ygs_games`.`gt_type_id`=:ypl0) AND
(`ygs_games`.`g_id`=`ygs_games_ygs_games`.`gt_game_id`) WHERE (g_state=0)
ORDER BY g_added DESC LIMIT 10. Bind with parameter :ypl0='8'

Т.е. именно то, что нам нужно.

Тут есть один недостаток — усложняется код настройки пагинации. Мы должны вручную установить параметры limit и offset.

Полностью метод, формирующий страницу с играми одного жанра выглядит так.

public function actionShowGames()
{
    if (isset($_GET['type_id']) && is_numeric($_GET['type_id'])) {
        $criteria = new CDbCriteria;
         
        $type = Types::model()->findByPk($_GET['type_id']);
        //нужно использовать "ленивую" загрузку (не использовать with)
        //иначе не получится указать limit для связанной таблицы
        $params = array(
            'limit'=>self::PAGE_SIZE,
            'condition'=>'g_state='.Games::PUBLISHED,
            'order'=>'g_added DESC',
        );
        //настраиваем пагинацию
        if (isset($_GET['page']) && is_numeric($_GET['page'])) {
            $params['offset'] = ($_GET['page'] - 1) * self::PAGE_SIZE;
        }
        $pages=new CPagination(count($type->getRelated('ygs_games')));
        $pages->pageSize=self::PAGE_SIZE;
         
        $games = $type->getRelated('ygs_games',false, $params);
         
        $this->render('showGames'
            ,array('games'=>$games, 'pages'=>$pages));
    } else {
        $this->redirect('/games/list');
    }
}

Обратите внимание на строки 16 и 18. В них мы устанавливаем смещение (offset) и создаём объект CPagination. Последнему передаём количество всех игр данного жанра. Для этого просто используем функцию count.

Если кому-то захочется поэкспериментировать, выкладываю архив с изменённым примером.

Чтобы удобнее было сравнивать работу библиотеки, я не удалял старый код создания страниц с жанрами (GamesController.php метод actionList). Новый метод находится в контроллере TypesController.php (метод actionShowGames).

Добавлено: 22 Апреля 2018 11:25:28 Добавил: Андрей Ковальчук

Использование jqGrid вместе с Yii фреймворком

В этой статье речь пойдёт о том как использовать Yii PHP framework и плагин к jQuery под названием jqGrid. Я не буду повторяться и рассказывать о том, что из себя представляют Yii и jqGrid и зачем они нужны.

Когда я проводил этот эксперимент, меня интересовали два момента:

1) подключение jgGrid;

2) преобразование данных, полученных с помощью CActiveDataProvider в формат понятный для jgGrid.

С первым пунктом всё более-менее понятно. jqGrid представляет собой набор JS и CSS файлов, которые нужно подключить к странице.

Для этих целей в Yii предусмотрены специальные методы: registerScriptFile и registerCssFile. Можно, конечно, просто прописать теги script в представлении, но делать этого не рекомендуется, т.к. в этом случае фреймоворк не сможет отслеживать повторные подключения скриптов.

Т.к. я просто экспериментировал, я сделал тестовое представление (testjqgrid.php)

//подключение CSS файлов и JS скриптов
$cs = Yii::app()->clientScript;
 
$cs->registerCssFile(Yii::app()->request->baseUrl.'/jqgrid/css/ui.jqgrid.css');
$cs->registerCssFile(Yii::app()->request->baseUrl.'/jqgrid/css/ui-lightness/jquery-ui-1.7.2.custom.css');
 
$cs->registerScriptFile(Yii::app()->request->baseUrl.'/jqgrid/js/jquery-1.3.2.min.js');
$cs->registerScriptFile(Yii::app()->request->baseUrl.'/jqgrid/js/i18n/grid.locale-ru.js');
$cs->registerScriptFile(Yii::app()->request->baseUrl.'/jqgrid/js/jquery.jqGrid.min.js');
?>

<table id="list"></table> 
<div id="pager"></div> 
 
<script type="text/javascript">
$(function() {
    jQuery("#list").jqGrid( {
        url : '<?php echo $this->createUrl('blogs/jqgriddata'); ?>',
        datatype : 'json',
        mtype : 'GET',
        colNames : [ '#', 'Name', 'URL', 'API' ],
        colModel : [ {
            name : 'b_id',
            index : 'b_id',
            width : 60
        }, {
            name : 'b_name',
            index : 'b_name',
            width : 120
        }, {
            name : 'b_url',
            index : 'b_url',
            width : 150,
            align : 'right'
        }, {
            name : 'b_api',
            index : 'b_api',
            width : 80,
            align : 'right'
        } ],
        pager : '#pager',
        rowNum : 10,
        rowList : [ 10, 20, 30 ],
        sortname : 'invid',
        sortorder : 'desc',
        viewrecords : true,
        caption : 'Blogs'
    });
});
</script>

Как видите, сначала мы регистрируем скрипты и файлы стилей, затем вставляем тег для таблицы (строка 12) и тег для панели навигации.

Примечание. В этом примере использован первый попавшийся контроллер (BlogsController). Кроме того, для получения данных используется модель Blogs. Зачем она нужна, в данном случае не принципиально, мы просто будем отображать записи из таблицы с помощью jqGrid.

В конце представления находится код настройки jqGrid. Таблица будет содержать 4 колонки с названиями: «#», «Name», «URL» и «API», а соответствующие поля в таблице имеют названия: «b_id», «b_name», «b_url» и «b_api».

И, конечно, нужно указать адрес скрипта, который будет формировать данные для таблицы (строка 18, параметр url).

Формирование таблицы.

Т.к. jqGrid получает данные с помощью AJAX запросов, то мы добавим два метода в контроллер. Первый – actionJqgrid, будет формировать страницу с таблицей, т.е. просто загружать представление. Второй – actionJqgriddata, предназначен для обработки AJAX запросов.
public function actionJqgrid() {
    $this->render('testjqgrid');
}
 
public function actionJqgriddata() {
    $dataProvider=new CActiveDataProvider('Blogs', array(
        'pagination'=>array(
            'pageSize'=>$_GET['rows'],
            'currentPage'=>$_GET['page']-1,
        ),
    ));
    $responce->page = $_GET['page'];
    $responce->records = $dataProvider->getTotalItemCount();
    $responce->total = ceil($responce->records / $_GET['rows']);
    $rows = $dataProvider->getData();
    foreach ($rows as $i=>$row) {
        $responce->rows[$i]['id'] = $row['b_id'];
        $responce->rows[$i]['cell'] = array($row->b_id, $row->b_name, $row->b_url, $row->b_api);
    }
    echo json_encode($responce);
}

Рассмотрим их подробнее. Как я уже говорил, метод actionJqgrid просто загружает представление. Получать данные из базы нам не нужно, т.к. при создании таблицы jqGrid всё равно отправит AJAX запрос на их получение.

С методом actionJqgriddata ситуация немного сложнее. Дело в том, что нам нужно преобразовать данные, которые возвращает CActiveDataProvider в формат, понятный jqGrid.

Для этого мы создаём объект $responce и в нём сохраняем:

1) page – номер текущей страницы;

2) records – общее количество записей;

3) total – общее количество страниц;

4) rows – массив с данными. Каждый элемент этого массива должен содержать id текущей записи и массив cell с данными ячеек. Этот массив мы формируем с помощью цикла (строки 16-19).

После это преобразовываем $responce в JSON формат и возвращаем браузеру.

Как видите, принцип достаточно простой, но код получается объёмный. В принципе, есть расширение eziiui, которое упрощает работу с jqGrid, но судя по отзывам, оно ограничивает ваши возможности в использовании jqGrid, поэтому я им не пользовался.

Может быть стоит написать расширение, которое будет преобразовывать данные из CActiveDataProvider в JSON формат? Или есть другие идеи?

Добавлено: 22 Апреля 2018 11:21:02 Добавил: Андрей Ковальчук

Запускаем Curl из консоли

Сегодня я продолжу рассказывать об использовании фреймворка Yii. На этот раз речь пойдёт о создании консольных приложений. Их удобно запускать с помощью планировщика, поэтому чаще всего они используются выполнения действий вроде отправки отчетов на eMail, получения данных и т.п. Вашу фантазию тут никто не ограничивает ;)

Главное отличие от обычного web приложения в том, что фреймворк не будет выполнять действий, связанных с формированием страницы, а просто выполнит ваш скрипт. При этом у вас остаётся доступ ко всем возможностям фреймоворка, что значительно упрощает работу.

Рассмотрим небольшой пример. Допустим, нам нужно периодически проверять содержимое какой-нибудь web страницы, и записывать его в базу.

Для получения страницы удобно использовать cURL, о котором я неоднократно рассказывал, а для работы с БД используем библиотеку фреймоворка.

Создаём входной скрипт.

Я предполагаю, что вы уже создали обычное web приложение (например, с помощью команды yiic webapp). Входной скрипт web приложения называется index.php. Создаём в этой же папке файл console.php и копируем в него содержимое index.php. После этого изменяем имя файла конфигурации и в последней строке указываем, что нам нужно создать консольное приложение (Yii::createConsoleApplication).

В результате должно получиться примерно следующее.

$yii=dirname(__FILE__).'/path_to_yii/framework/yii.php';
$config=dirname(__FILE__).'/protected/config/console.php';
 
// remove the following line when in production mode
defined('YII_DEBUG') or define('YII_DEBUG',true);
 
require_once($yii);
Yii::createConsoleApplication($config)->run();

Кстати, файл конфигурации для консольного приложения автоматически создаётся утилитой yiic.

Создаём команду.

Команды (потомки класса CConsoleCommand) используются для выполнения действий из консоли. В данном случае мы создадим файл protected/commands/CurlCommand.php и объявим в нём класс CurlCommand.
class CurlCommand extends CConsoleCommand {
    public function run($args) {
        $ch = curl_init();    // инициализация
        curl_setopt($ch, CURLOPT_URL, 'http://www.google.com'); // устанавливаем URL
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);// разрешаем редирект
        curl_setopt($ch, CURLOPT_RETURNTRANSFER,1); // указывает, что функция curl_exec должна вернуть полученный ответ, а не отправить его сразу браузеру
        $result = curl_exec($ch); // запуск
        curl_close($ch);
        echo 'We got '.strlen($result).' bytes.';
    }
}

Имя класса формируется из имени команды и суффикса Command. Чтобы команда заработала необходимо реализовать метод run, который будет выполнять все действия. В данном случае, мы настраиваем cURL и получаем главную страницу google.com и выводим её размер. Но настройка и использование cURL – не тема этой статьи, вернёмся к методу run.

При вызове команды мы можем передать ей дополнительные агрументы, например, так:
php console.php command par1 par2
При этом фреймворк создаст массив с par1 и par2, и передаст его в $args. Таким образом, можно получить доступ к этим параметрам внутри метода run().

Работа с базой данных

Осуществляется точно так же как и в обычном web приложении. Главное указать в файле конфигурации необходимые настройки.

Для подключения к базе данных в файле protected/config/console.php создаём массив components с элементом db, в котором указываем настройки подключения к базе.
'components'=>array(
    'db'=>array(
        'connectionString' => 'sqlite:protected/data/pf.db',
        'tablePrefix' => 'pf_',
    ),
),

Кроме того, тут же можно подключить файлы с моделями.
'import'=>array(
    'application.models.*',
),

После этого можно работать с БД. Например, если есть у вас есть модель Users, то создать нового пользователя можно с помощью следующего кода.
$newUser = new Users;
$newUser->u_name = 'dfsg';
$newUser->u_pass = md5('dfg');
$newUser->save();

Запуск

Тут всё просто. Вводим в консоли

php console.php curl

И смотрим на результат. У меня скрипт вывел «We got 8571 bytes».
Эту же команду можно указать при создании задачи для планировщика.

Как видите, разработчики Yii неплохо продумали работу со скриптами из консоли. Главное, вы имеете доступ ко всем компонентам и библиотекам фреймворка, но можете управлять их использованием независимо от основного web приложения.

Добавлено: 22 Апреля 2018 11:17:41 Добавил: Андрей Ковальчук

Создание кнопок с помощью CButtonColumn

Тему этого поста подсказал мне читатель по имени Alex, за что ему большое спасибо.

Речь о компонентах zii, которые, начиная с версии 1.1, входят в состав фреймворка, и активно используются утилитой yiic при генерации кода.

К сожалению, документация по этим компонентам есть только в виде API (комментарии к исходникам) и её явно недостаточно.

Ситуация следующая. Если вас полностью устраивает код, который генерирует yiic — никаких проблем. Но вот что-то изменить или добавить какие-нибудь возможности уже сложнее.

Рассмотрим такую ситуацию. Для одной из таблиц в БД вы создали стандартный набор CRUD операций. И вам нужно в таблицу с перечнем записей добавить дополнительную кнопку. На первый взгляд, задача довольно простая, т.к. таблица генерируется с помощью виджета 'zii.widgets.grid.CGridView' и среди компонентов zii есть CButtonColumn, который специально предназначен для создания колонок с кнопками. Т.е. задача заключается в настройке этого компонента.

Чтобы было понятнее, рассмотрим небольшой пример.

Создаём новое приложение
yiic webapp .
По-умолчанию в нём есть sqlite база с одной таблицей (данными пользователей)

Создаём модель
yiic shell
model User tbl_user

и CRUD интерфейс
crud User

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

При этом страница управления записями (index.php?r=user/admin) содержит таблицу с перечнем записей и столбцов с кнопками «View», «Update», «Delete».

Взгляните на код, который создаёт стандартную таблицу с тремя кнопками.

<?php $this->widget('zii.widgets.grid.CGridView', array(
    'dataProvider'=>$dataProvider,
    'columns'=>array(
        'id',
        'username',
        'password',
        'email',
        array(
            'class'=>'CButtonColumn',
        ),
    ),
)); ?>

В массиве columns перечисляем названия столбцов таблицы. Если имя столбца совпадает с полем таблице, то оно будет заполнено соответствующими данными. В последнем элементе указан массив с одним элементом 'class'=>'CButtonColumn'. Этого достаточно для создания трёх стандартных кнопок.

Попробуем добавить ещё один столбец с кнопкой «AJAX запрос». Для этого добавим ещё один элемент в массив 'columns'
array(
    'class'=>'CButtonColumn',
    'buttons'=>array(
        'preview'=>array(
            'label'=>'AJAX запрос',
            'url'=>'…',
            'click'=>'…',
        ),
    ),
    'template'=>'{preview}',
),

Принцип следующий. В элементе 'buttons' нужно указать массив с новыми кнопками. При этом ключ каждого элемента этого массива является названием кнопки. Его мы должны использовать в элементе 'template' для того, чтобы показать кнопку в таблице. Название необходимо заключит в фигурные скобки. При этом можно добавить в одну ячейку сразу несколько кнопок, например, так:
'{view} {update} {delete}'.

Для каждой кнопки необходимо указать массив с параметрами.
'label' – содержит текст, который будет отображаться на кнопке.
'url' – PHP выражение, которое сформирует ссылку для данной кнопки.
'click' – JS функция, которая будет назначена в качестве обработчика клика по кнопке.

Кроме того, можно указать картинку и массив с html атрибутами с помощью параметров 'imageUrl' и 'options'. Но сейчас речь не о них. У меня больше всего вопросов вызвали 'url' и 'click'.

Расписывать свои ковыряния в исходниках я не буду. Лучше сразу покажу решение.

В параметре 'url' нужно записать PHP выражение, которое сформирует URL, в виде строки, т.е. в кавычках. Например,
'url'=>'Yii::app()->createUrl("user/getuser")'

При этом будут доступны две переменные: $row и $data. Первая содержит номер строки, вторая — объект с данными текущей записи. Т.е. добавить в запрос GET параметр с email’ом пользователя можно так:
'url'=>'Yii::app()->createUrl("user/getuser", array("email"=>$data->email))'

Для того, чтобы проверить отправку AJAX запросов я добавил в контроллер метод actionGetuser. Он ищет пользователя по его email'у.
public function actionGetuser() {
    if (isset($_GET['email'])) {
        $user = User::model()->find('email=:email', array(':email'=>$_GET['email']));
        if (null !== $user) {
            echo $user->username;
        } else {
            echo 'unknown';
        }
    } else {
        echo 'unknown';
    }
}

Напишем функцию, которая будет выполнять отправку запроса. Я добавил её прямо в представление.
<?php
$js_preview =<<< EOD
function() {
    var url = $(this).attr('href');
    $.get(url, function(response) {
        alert(response);
    });
    return false;
}
EOD;
?>

<?php $this->widget('zii.widgets.grid.CGridView', array(
    'dataProvider'=>$dataProvider,
    'columns'=>array(
        'id',
        'username',
        'password',
        'email',
        array(
            'class'=>'CButtonColumn',
        ),
        array(
            'class'=>'CButtonColumn',
            'buttons'=>array(
                'preview'=>array(
                    'label'=>'AJAX запрос',
                    'url'=>'Yii::app()->createUrl("user/getuser", array("email"=>$data->email))',
                    'click'=>$js_preview,
                ),
            ),
            'template'=>'{preview}',
        ),
    ),
)); ?>

Как видите, текст JS функции присвоен переменной $js_preview. Эту переменную мы и указываем в параметре 'click'.

Принцип работы следующий.

1) Получаем URL данной кнопки (с помощью $(this).attr('href')). Он уже содержит email в качестве GET параметра.

2) Отправляем AJAX запрос (с помощью $.get).

3) Показываем результат (alert(response)).

Как видите, кода нужно написать минимум, но, повторюсь, очень хотелось бы почитать подробное руководство от разработчиков о компонентах zii ;)

Добавлено: 22 Апреля 2018 11:15:45 Добавил: Андрей Ковальчук

Именованные группы условий (scope) в yii

Для создания групп услови запроса в Yii предусмотрен удобный механизм Scope, который позволяет на уровне класса CActiveRecord задавать предварительные условия. В этой статье я покажу на примере, как использовать Scope в Yii.

Группа условий по умолчанию

class Content extends CActiveRecord
{
    public function defaultScope()
    {
        return array(
           'condition'=>'status=1',
           'order'=>'create_time DESC',
           'limit'=>5,
        );
    }
}

Теперь при выполнении $posts = Content::model()->findAll() будут применятся условия, описанные выше в модели.

Если требуется отменить фильтр по умолчанию (по какой-либо причине), необходимо воспользоваться в запросах методом resetScope(). Пример: $posts = Content::model()->resetScope()->findAll()

Именованные Scope
Кроме Scope по умолчанию, вы можете задать любое число специфичных фильтров. Они задаются через переопределение метода scopes. Пример, который реализует три фильтра: students, prepods и abiturients

public function scopes()
{		
	return array(
        'students'=>array('condition'=>"role='student'"),
        'prepods'=>array('condition'=>"role='prepods'"),
        'abiturients'=>array('condition'=>"role='abiturients'"),
    );
}
//вызов
$students = Users::model()->students()->findAll("active=1"); // все активные студенты
$prepods= Users::model()->prepods()->findAll("active=1"); // все активные преподаватели
$abiturients = Users::model()->abiturients()->findAll("active=1"); // все активные абитуриенты

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

public function scopes()
{		
	return array(
        'students'=>array('condition'=>"role='student'"),
        'prepods'=>array('condition'=>"role='prepods'"),
        'abiturients'=>array('condition'=>"role='abiturients'"),
        'only10'=>array('limit'=>10, 'order'=>'id desc'),
    );
}
//вызов
$students = Users::model()->students()->only10()->findAll("active=1"); // не более 10 активных студентов из последних добавленных	

Существует альтернативный способ создания дополнительного фильтра, который позволяет использовать параметры при вызове фильтра

public function onlyXY($limit, $order)
{
    $this->getDbCriteria()->mergeWith(array(
        'limit' => $limit,
        'order' => $order
    ));
    return $this;
}
//вызов
$students = Users::model()->students()->onlyXY(10, 'id desc')->findAll("active=1"); // не более 10 активных студентов из последних добавленных

Добавлено: 20 Апреля 2018 18:48:03 Добавил: Андрей Ковальчук

Дерево категорий в yii с помощью Nested Set

В этой статье речь пойдет о том, как сформировать дерево категорий в yii с помощью компонента Nested Set.

Как известно, вывод вложенных категорий можно сделать с помощью рекурсии, но такой способ весьма ресурсоемкий и подходит лишь для небольших вложенностей. Если дело доходит до серьезной структуры, то наилучшим решением будет использовать алгоритм вложенные деревья(они же вложенные множества) или Nested Set (Nested set model).

Nested Set подразумевает присвоение каждому узлу в дереве двух дополнительных ключей left key и right key. Для заполнения этих ключей нужно полностью обойти всё дерево дважды посещая каждый из узлов. В результате выборка из дерева будет происходить довольно быстро.

Плюсы

В базах, которые не поддерживают рекурсивные запросы (MySQL) выборка из дерева происходит быстрее, чем если бы она делалась с помощью хранимой процедуры или вывода рекурсивно.

Минусы

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

Что касается, yii, то здесь мы имеем отличный компонент nested-set-behavior. Качаем и устанавливаем так, как написано в описании.

Далее я покажу как я делал crud для работы с категориями у себя в админке.

Для начала, код SQL самой таблицы категорий:

CREATE TABLE IF NOT EXISTS `tbl_category` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `lft` int(11) NOT NULL,
  `rgt` int(11) NOT NULL,
  `level` int(4) NOT NULL,
  `name` varchar(150) NOT NULL,
  `alias` varchar(150) NOT NULL,
  `title` varchar(150) NOT NULL,
  `meta_k` varchar(255) NOT NULL,
  `meta_d` text NOT NULL,
  `img` text NOT NULL,
  `order` tinyint(4) NOT NULL DEFAULT '0',
  `show` tinyint(1) NOT NULL DEFAULT '1',
  `txt` text,
  `cssclass` varchar(100) NOT NULL,
  `htmlview` varchar(100) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB	 DEFAULT CHARSET=utf8 AUTO_INCREMENT=48 ;

После создания таблицы, нужно создать модель Category:

class Category extends CActiveRecord
{
	public $parent_id;
 
	public function behaviors()
	{
		return array(
			'nestedSetBehavior'=>array(
				'class'=>'application.components.NestedSetBehavior',
				'leftAttribute'=>'lft',
				'rightAttribute'=>'rgt',
				'levelAttribute'=>'level',
			),
		);
	}
 
	public function tableName()
	{
		return '{{category}}';
	}
 
	public function rules()
	{
		return array(
			array('alias,name', 'required'),
			array('parent_id', 'numerical', 'integerOnly'=>true),
			// alias естесвенно должен быть обязательным, безопасным и уникальным	
			array('alias', 'match', 'pattern' => '/^[A-z\-\_]+$/'),
			array('alias','unique',
				'caseSensitive'=>true,
				'allowEmpty'=>false,
			),
			array('txt', 'safe'),
			//array('parent','length', 'max'=>1),
			array('lft, rgt, level, order, show', 'numerical', 'integerOnly'=>true),
			array('name, alias, title', 'length', 'max'=>150),
			array('meta_k', 'length', 'max'=>255),
			array('cssclass, htmlview', 'length', 'max'=>100),
			array('id, name, alias', 'safe', 'on'=>'search'),
		);
	}
 
	public function relations()
	{		
		return array();
	}
 
	public function attributeLabels()
	{
		return array(
			'id' => 'ID',
			'lft' => 'Lft',
			'rgt' => 'Rgt',
			'level' => 'Level',
			'name' => 'Name',
			'alias' => 'Alias',
			'title' => 'Title',
			'meta_k' => 'Meta K',
			'meta_d' => 'Meta D',
			'img' => 'Img',
			'order' => 'Order',
			'show' => 'Show',
			'txt' => 'Txt',
			'cssclass' => 'Cssclass',
			'htmlview' => 'Htmlview',
			'parent' => 'parent'
		);
	}
 
	public function search()
	{
		$criteria=new CDbCriteria;
 
		$criteria->compare('id',$this->id);
		// и т.д.
 
		return new CActiveDataProvider($this, array(
			'criteria'=>$criteria,
		));
	}
 
	public static function model($className=__CLASS__)
	{
		return parent::model($className);
	}
 
	/**
	 * Создает корневую категорию либо возвращает уже имеющуюся
	 * @param Category $model
	 * @return mixed
	 */
	public static function getRoot(Category $model){
		$root = $model->roots()->find();
		if (! $root){
			$model->name = 'Категории';
			$model->alias = 'Root';
			$model->title = 'Категории';
			$model->meta_k = 'Категории';
			$model->meta_d = 'Категории';
			$model->txt = 'Категории';
			$model->saveNode();
			$root = $model->roots()->find();
		}
		return $root;
	}
}

Теперь настала очередь контроллера Category.

class CategoryController extends AdminController
{
	public function actionView($id)
	{
		$arr_ancestors = array();
 
		$category = Category::model()->find(array(
			'condition' => 'id=:id',
			'params' => array(':id' => $id),
		));
 
		$ancestors = $category->ancestors()->findAll();
 
		foreach($ancestors as $ancestor){
			$arr_ancestors[] = $ancestor->name;
		}
 
		$this->render('view',array(
			'arr_ancestors' => $arr_ancestors,
			'model'=>$this->loadModel($id),
		));
	}
 
 
	public function actionCreate()
	{
		$model = new Category;
		$root = Category::getRoot($model);
		$descendants = $root->descendants()->findAll();
 
		if(isset($_POST['Category']))
		{
			$parent_id = (int)$_POST['Category']['parent_id'];
			$root = Category::model()->findByPk($parent_id);
			$model->attributes = $_POST['Category'];
			if($model->appendTo($root)){
				$this->redirect(array('view','id'=>$model->id));
			}
		}
 
		$this->render('create',array(
			'model'=>$model,
			'root' => $root,
			'categories' => $descendants,
			'parent_id' => null,
			'id' => null,
		));
	}
 
 
	public function actionUpdate($id)
	{
		$root = Category::getRoot(new Category);
		$descendants = $root->descendants()->findAll();
 
		$model = $this->loadModel($id);
 
		$parent = $model->parent()->find();
		$parent_id = $parent ? $parent->id : null;
 
		if(isset($_POST['Category']))
		{
			$parent_id = (int)$_POST['Category']['parent_id'];
 
			$node = Category::model()->findByPk($parent_id);
 
			$model->attributes = $_POST['Category'];
 
			if($model->lft == 1 || $model->id == $node->id){
				if($model->saveNode()){
					Yii::app()->user->setFlash('category_error', "Структура дерева не изменена.");
					$this->redirect(array('view','id'=>$model->id));
				}
			}
			else{
				if($model->saveNode()){
					if($node->isDescendantOf($model)){
						Yii::app()->user->setFlash('category_error', "Структура дерева не изменена.");
					}
					else{
						$model->moveAsLast($node);
					}
					$this->redirect(array('view','id'=>$model->id));
				}
			}
		}
 
		$this->render('update',array(
			'model'=> $model,
			'root' => $root,
			'categories' => $descendants,
			'parent_id' => $parent_id,
			'id' => $id,
		));
	}
 
 
	public function actionDelete($id)
	{
		$this->loadModel($id)->deleteNode();
		// if AJAX request (triggered by deletion via admin grid view), we should not redirect the browser
		if(!isset($_GET['ajax']))
			$this->redirect(isset($_POST['returnUrl']) ? $_POST['returnUrl'] : array('index'));
	}
 
 
	public function actionIndex()
	{
		$model = new Category;
		$root = Category::getRoot($model);
		$descendants = $root->descendants()->findAll();
 
		$this->render('index',array(
			'root' => $root,
			'categories' => $descendants,
		));
	}
 
 
	public function actionAdmin()
	{
		$model=new Category('search');
		$model->unsetAttributes();
		if(isset($_GET['Category']))
			$model->attributes=$_GET['Category'];
 
		$this->render('admin',array(
			'model'=>$model,
		));
	}
 
	public function loadModel($id)
	{
		$model=Category::model()->findByPk($id);
		if($model===null)
			throw new CHttpException(404,'The requested page does not exist.');
		return $model;
	}
 
}

Что касается вьюшек. Итак, вид index. Здесь список формируется тегв ul li:

<div class="admin category index">
  <?php
  $this->breadcrumbs=array(
   'Дерево категорий',
  );
  $this->menu=array(
   array('label'=>'Создать', 'url'=>array('create')),
   array('label'=>'Менеджер', 'url'=>array('admin')),
  );
  ?>
  <h1>Дерево категорий</h1>
  [<?=$root->id?>] <?=$root->name?>
  <a class="view" title="View" href="<?=Yii::app()->createUrl('/admin/category/view', array('id'=>$root->id))?>">view</a>
  <a class="update" title="Update" href="<?=Yii::app()->createUrl('/admin/category/update', array('id'=>$root->id))?>">update</a>
  <?php echo CHtml::link('delete" alt="Delete">',"#", array("submit"=>array('delete', 'id'=>$root->id), 'confirm' => 'Are you sure?')); ?>
  <?
  $level=0;
  foreach($categories as $n=>$category)
  {
  if($category->level==$level)
  echo '</li>';
  else if($category->level>$level)
  echo '<ul>';
  else
  {
  echo '</li>';
  for($i=$level-$category->level;$i;$i--)
  {
  echo '</ul>';
  echo '</li>';
  }
  }
  ?>
  <li>
  [<?=$category->id?>] <?=$category->name?>
  <a class="view" title="View" href="<?=Yii::app()->createUrl('/admin/category/view', array('id'=>$category->id))?>">view</a>
  <a class="update" title="Update" href="<?=Yii::app()->createUrl('/admin/category/update', array('id'=>$category->id))?>">update</a>
  <?php echo CHtml::link('delete',"#", array("submit"=>array('delete', 'id'=>$category->id), 'confirm' => 'Are you sure?')); ?>
  <?
  $level=$category->level;
  }
  ?>
  <? for($i=$level;$i;$i--): ?>
  </li>
  </ul>
  <? endfor;?>
</div>

Также рекомендую использовать jquery плагин treeview для более удобного и красивого вывода деревав вашем html-виде.

А теперь вид _form. Здесь важен только вывод выпадающего списка, но я уж приведу всё:

<?php
  /* @var $this CategoryController */
  /* @var $model Category */
  /* @var $form CActiveForm */
  ?>
<div class="form">
<?php $form=$this->beginWidget('CActiveForm', array(
  'id'=>'category-form',
  // Please note: When you enable ajax validation, make sure the corresponding
  // controller action is handling ajax validation correctly.
  // There is a call to performAjaxValidation() commented in generated controller code.
  // See class documentation of CActiveForm for details on this.
  'enableAjaxValidation'=>false,
  )); ?>
 <p class="note">Fields with <span class="required">*</span> are required.</p>
 <?php echo $form->errorSummary($model); ?>
 <div class="row">
  <?php echo $form->labelEx($model,'name'); ?>
  <?php echo $form->textField($model,'name',array('size'=>60,'maxlength'=>150)); ?>
  <?php echo $form->error($model,'name'); ?>
  </div>
 <div class="row">
  <?php echo $form->labelEx($model,'alias'); ?>
  <?php echo $form->textField($model,'alias',array('size'=>60,'maxlength'=>150)); ?>
  <?php echo $form->error($model,'alias'); ?>
  </div>
 <div class="row">
  <?php echo $form->labelEx($model,'title'); ?>
  <?php echo $form->textField($model,'title',array('size'=>60,'maxlength'=>150)); ?>
  <?php echo $form->error($model,'title'); ?>
  </div>
 <div class="row">
  <?php echo $form->labelEx($model,'meta_k'); ?>
  <?php echo $form->textField($model,'meta_k',array('size'=>60,'maxlength'=>255)); ?>
  <?php echo $form->error($model,'meta_k'); ?>
  </div>
 <div class="row">
  <?php echo $form->labelEx($model,'meta_d'); ?>
  <?php echo $form->textArea($model,'meta_d',array('rows'=>6, 'cols'=>50)); ?>
  <?php echo $form->error($model,'meta_d'); ?>
  </div>
 <div class="row">
  <?php echo $form->labelEx($model,'order'); ?>
  <?php echo $form->textField($model,'order'); ?>
  <?php echo $form->error($model,'order'); ?>
  </div>
 <div class="row">
  <?php echo $form->labelEx($model,'show'); ?>
  <?php echo $form->textField($model,'show'); ?>
  <?php echo $form->error($model,'show'); ?>
  </div>
 <div class="row">
  <?php echo $form->labelEx($model,'txt'); ?>
  <?php echo $form->textArea($model,'txt',array('rows'=>6, 'cols'=>50)); ?>
  <?php echo $form->error($model,'txt'); ?>
  </div>
 <div class="row">
  <?php echo $form->labelEx($model,'cssclass'); ?>
  <?php echo $form->textField($model,'cssclass',array('size'=>60,'maxlength'=>100)); ?>
  <?php echo $form->error($model,'cssclass'); ?>
  </div>
 <div class="row">
  <?php echo $form->labelEx($model,'htmlview'); ?>
  <?php echo $form->textField($model,'htmlview',array('size'=>60,'maxlength'=>100)); ?>
  <?php echo $form->error($model,'htmlview'); ?>
  </div>
 <div class="row">
  <?php echo $form->labelEx($model,'parent_id'); ?>
  <select name="Category[parent_id]" id="Category_parent">
   <option value="<?=$root->id ?>"><?=$root->name?></option>
   <? if (!empty($categories)) : ?>
	<? foreach ($categories as $category) : ?>
	 <option value="<?=$category->id ?>"
	 <?=!empty($_POST['parent']) && $_POST['parent']== $category->id || $parent_id == $category->id? 'selected="selected"' : ''?>>
	 <?=str_repeat('-', $category->level), $category->name?>
	</option>
   <? endforeach; ?>
   <? endif;?>
  </select>
 </div>
 <div class="row buttons">
  <?php echo CHtml::submitButton($model->isNewRecord ? 'Create' : 'Save'); ?>
  </div>
<?php $this->endWidget(); ?>
</div><!-- form -->

На этом всё. Удачи, други!

Добавлено: 20 Апреля 2018 18:45:54 Добавил: Андрей Ковальчук

Создание своего виджета в Yii

В этой статье я расскажу, как создать свой виджет (widget) в Yii.

В директории extensions создадим папку mywidgets. В ней мы будем хранить все виджеты, сделанные непосредственно нами.

Далее, в папке mywidgets создадим папку views, где будем хранить вьюсы для наших виджетов.

Как и у контроллера, у виджета может быть собственное представление. По умолчанию файлы представлений виджета находятся в поддиректории views директории, содержащей файл класса виджета. Эти представления можно рендерить при помощи вызова CWidget::render() точно так же, как и в случае с контроллером. Единственное отличие состоит в том, что для представления виджета не используются макеты. Также следует отметить, что $this в представлении указывает на экземпляр виджета, а не на экземпляр контроллера.

В папке views, создаим файл вида goper.php

Итак, прнивожу пример своего виджета:

class Goper extends CWidget{
    // этот метод будет вызван внутри CBaseController::endWidget()
    public function run()
    {
    	// можель формы
        $model = new Login;
        // этот метод будет вызван внутри CBaseController::beginWidget()
        $this->render('goper',  array('model' => $model));
    }
}

Файл вида goper.php

<div class="form">
    <? $form=$this->beginWidget('CActiveForm', array(
        'id'=>'login-form',
        'enableClientValidation'=>true,
        'action' => 'site/login',
        'clientOptions'=>array(
            'validateOnSubmit'=>true,
        ),
    )); ?>
 
    <p class="note">Поля с <span class="required">*</span> обязательны для заполнения.</p>
 
    <?=$form->errorSummary($model); ?>
 
    <div class="row">
        <?=$form->labelEx($model,'username'); ?>
        <?=$form->textField($model,'username'); ?>
        <?=$form->error($model,'username'); ?>
    </div>
 
    <div class="row">
        <?=$form->labelEx($model,'password'); ?>
        <?=$form->passwordField($model,'password'); ?>
        <?=$form->error($model,'password'); ?>
    </div>
 
    <div class="row rememberMe">
        <?=$form->checkBox($model,'rememberMe'); ?>
        <?=$form->label($model,'rememberMe'); ?>
        <?=$form->error($model,'rememberMe'); ?>
    </div>
 
    <div class="row buttons">
        <?php echo CHtml::submitButton('Login'); ?>
    </div>
 
    <? $this->endWidget(); ?>
</div><!-- form -->

Вставка виджета.

$this->widget('ext.mywidgets.Goper');

Вот и всё.

Добавлено: 20 Апреля 2018 18:43:40 Добавил: Андрей Ковальчук

Модель в Yii

Общий обзор работы модели фрэймворка Yii.

Для начала отметим, что Yii использует паттерн MVC.

Модель (model) в Yii — это экземпляр класса CModel или класса, унаследованного от него.

Yii предоставляет два типа моделей: модель формы (CFormModel) и Active Record (CActiveRecord ). Оба типа являются наследниками базового класса CModel.

Модель формы CFormModel используется для хранения данных, введённых пользователем. Как правило, мы получаем эти данные, обрабатываем, а затем избавляемся от них. Например, на странице авторизации модель такого типа может быть использована для представления информации об имени пользователя и пароле.

Модели обычно располагаются в папке protected/models и называются аналогично таблице с которой работают. К примеру, модель которая работает с таблицей post должна быть названа "Post.php".

Вид типичной модели от CActiveRecord.

class User extends CActiveRecord
{
    /**
     * @return имя таблицы
     */
    public function tableName()
    {
        return '{{user}}';
    }
 
    /**
     * Правила для валидации
     * @return array validation rules for model attributes.
     */
    public function rules()
    {
        return array(
        	array('email, username', 'unique'),
        	// и т.д.
        );
    }
 
    /**
     * Связи таблиц
     * @return array relational rules.
     */
    public function relations()
    {
        // NOTE: you may need to adjust the relation name and the related
        // class name for the relations automatically generated below.
        return array();
    }
 
    /**
     * Перевод названий атрибутов
     * @return array customized attribute labels (name=>label)
     */
    public function attributeLabels()
    {
        return array(
            'id' => 'ID',
            'username' => 'Аккаунт',
            // и т.д.
        );
    }
    
    /**
     * Фильтр и поиск
     */
    public function search()
    {
        // @todo Please modify the following code to remove attributes that should not be searched.
 
        $criteria = new CDbCriteria;
 
        $criteria->compare('id', $this->id);
        // и т.д.
 
        return new CActiveDataProvider($this, array(
            'criteria' => $criteria,
        ));
    }
 
    /**
     * Обязательный метод для создания модели
     */
    public static function model($className = __CLASS__)
    {
        return parent::model($className);
    }
}

Вид типичной модели от CFormMode.

class ContactForm extends CFormModel
{
    public $email;
    public $subject;
    public $question;
    public $verifyCode;
 
    /**
     * Declares the validation rules.
     */
    public function rules()
    {
        return array(
            // name, email, subject and body are required
            array('subject, email, question, verifyCode', 'required'),
            // email has to be a valid email address
            array('email', 'email'),
            // verifyCode needs to be entered correctly
            // если авторизован не проверять капчу
            array('verifyCode', 'captcha', 'allowEmpty' => Yii::app()->user->isGuest == false ? true : !CCaptcha::checkRequirements()),
        );
    }
 
    /**
     * Declares customized attribute labels.
     * If not declared here, an attribute would have a label that is
     * the same as its name with the first letter in upper case.
     */
    public function attributeLabels()
    {
        return array(
            'subject' => 'Тема',
            'email' => 'Ваш e-mail для ответа',
            'question' => 'Вопрос',
            'verifyCode' => 'Капча',
        );
    }
}

Пример использования модели в контроллере.

$Posts = new Posts();
$Posts->findAll(...);
// или 
Posts::model()->findAll(...);

Добавлено: 20 Апреля 2018 18:42:09 Добавил: Андрей Ковальчук

Примеры валидации в Yii

В это материале приведены самые популярные примеры валидации в yii.

Примеры валидации в Yii
Метод rules() принадлежит классу CModel ( CActiveRecord наследует CModel ), таким образом, создав модель, мы имеем два метода:

$model->validate()   - пройдена ли валидация (true | false)
$model->getErrors()  - список ошибок (array())

Схема работы валидации.

Схема работы валидации

Стандартные параметры
array(
    'список полей модели',
    'валидатор',
    'on'=>'имя сценария',
    'except'=>'имя сценария',
    'message'=>'сообщение об ошибке',
    …параметры валидации…
);

Использование валидации в виджете CActiveForm
<?=$form->errorSummary($model); ?>
<div class="row">
    <?=$form->labelEx($model,'username'); ?>
    <?=$form->textField($model,'username'); ?>
    <?=$form->error($model,'username'); ?>
</div>

Использование для CHtml
<?=CHtml::errorSummary($model);?>
<?=CHtml::error($model, 'username');?>

Пример использования в контроллере, принимающее post данные из формфы
public function actionCreate() {
    $model = new User('create'); 
    if (isset($_POST['User'])) {
        $model->attributes = $_POST['User'];
         //var_dump($model->validate());
         //var_dump ($model->getErrors());
        // валидация происходит автоматически, чтобы запретить валидацию, нужно передать false ($model->save(false))
       if ($model->save()){          
            $this->redirect(array('view', 'id' => $model->id));
        }      
    }
    $this->render('create', array(
        'model' => $model,
    ));
}

Пометить данные, учавствующие в фильтрах
// @todo Please remove those attributes that should not be searched.
array('user_id, day, month, year', 'safe', 'on' => 'search'),

Пометить данные как безопасные
safe - это такая штука, которая позволяет понять Yii, что данный атрибут модели, пришедший из атрибутов (cmodel::attributes), является разрешенным к присвоению к атрибутам модели без валидации, например $model->attributes = $_POST['Article']. Используйте валидатор safe, когда вам не важно, какое значение может принять валидируемый атрибут при массовом присвоении.

array('day, month, year, position', 'safe')

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

array('day, month, year, position', 'unsafe')

Использование сценария
Правило валидации

array(
  //'on'=>'имя сценария',
  array('username’, 'required', 'on'=>'create,update'),
);

Использование в контроллере

Так:

public function actionCreate() {
	$model = new User('create');
    //...
}

или

public function actionPassword($id) {
    $model = $this->loadModel($id);
    $model->scenario = 'password';
    //...
}

По умолчанию используются два сцинария: insert и update

Исключение валидации для определенных сценариев
array(
  'id, first_name, email',
  'required',
  'except' => array('register'),
),

Своё сообщение об ошибке
return array(
    array('title, content', 'required',
      'message'=>'Введите значение {attribute}.'),
      //другие сообщения
);

Использование фильтра
array('email', 'filter', 'filter' => 'trim'),
array('username','filter', 'filter'=>'strtoupper'),
array('alias', 'filter', 'filter' => array('text', 'getAliasByStr')),
// где text класс хелпера, a getAliasByStr($str) его метод
Дефолтное значение
array('email', default, value => 'goper@tut.by')
Integer Validation
array('status', 'numerical', 'integerOnly'=>true),

Minand Max Length Validation
// min
array('username', 'length', 
    'min'=>20,
    'tooSmall'=>'You must enter minimum 20 characters',
 
),
// max
array('username', 'length', 
    'max'=>45,
    'tooBig'=>'You cannot enter more than 45 characters',
),

Range Validation
array('status', 'in', 
    'range'=>array(1,2,3),
    'allowEmpty'=>false,
    'strict'=>true,// type comparison
),

Preg_match(pattern) Validation
array('username', 'match', 'pattern' => '/^[A-z][\w]+$/')

Required Validation
array('username,password,confirmpassword','required')

Unique Validation
array('username, email','unique',
    'caseSensitive'=>true,
    'allowEmpty'=>true,    
),

URL Validation
array('siteurl', 'url'),

Примеры работы с датой
// правильный формат
array('startDate, endDate', 'date', 'format' => 'MM/dd/yyyy'),
// сравнение двух дат
array(
  'last_date',
  'compare',
  'compareAttribute'=>'start_date',
  'operator'=>'>', 
  'allowEmpty'=>false , 
  'message'=>'{attribute} must be greater than "{compareValue}".'
),  

Свой валидатор
array('file', 'filetype’),
public function filetype($attr, $params){
	if(!true){
		$this->addError('file', 'fuck off');
	}
}

Валидация загрузки файлов
array('my_image, 'file',
    'allowEmpty'  => true,
    'maxFiles'   => 1,
    'maxSize'   => 1024 * 250,
    'minSize'   => 0,
    'types'   => 'jpg',
    'tooLarge' => 'Вы загружаете файл слишком большого размера',
    'tooSmall' => 'Вы загружаете файл слишком маленького размера',
    'tooMany' => 'Вы загружаете слишком большое количество файлов',
    'wrongType' => 'Неправильный тип файла',
    'wrongMimeType' => 'Неправильный MIME-тип файла',
    'on'   => array('user_registration', 'admin_edit'),
)

Добавлено: 20 Апреля 2018 18:38:15 Добавил: Андрей Ковальчук