На днях делал админку одного проекта, замутил на PHP + Yii. Модель данных MySQL содержит связи многие-ко-многим (собственно автор модели тоже я), поэтому редактирование контента реализовал с помощью расширения CAdvancedArBehavior. В процессе реализации обнаружил один баг в расширении, о чем поведу речь ниже. По окончании статьи осиливший путь освоит пару приемов работы с yii framework.
На протяжении изложения я буду использовать MS Windows + denwer.
Стартуем denwer, открываем phpMyAdmin, создаем тестовую базу данных и таблицы:
Поясню: auth - авторы, book - книги, auth_to_book - связь авторов с книгами многие-ко-многим. Стандартный пример.
Создаем хост по имени yii-test, рестартуем denwer, качаем последнюю версию yii framework со страницы загрузки, распакуем каталог framework в корневой каталог хоста.
Запускаем консоль, переходим в каталог распакованного фреймворка. Не забываем добавить в переменную окружения PATH путь к интерпретатору PHP - в моем случае "W:\usr\local\php5\".
Создаем приложение:
- php -f yiic webapp ..\
В браузере идем по адресу созданного хоста:
Редактируем файл конфигурации приложения main.php, который можно найти в каталоге "protected/config/" (у меня w:\home\yii-test\www\protected\config\main.php).
Логинимся с именем admin и паролем admin, открываем страницу генератора кода нашего приложения.
Используем генератор кода по прямому назначению: создадим модель данных таблицы auth - авторы.
Переходим по ссылке Model Generator, выбираем таблицу:
Нажимаем Preview:
Генерируем код:
В результате мы получили файл Auth.php, содержащий код класса модели таблицы auth по имени Auth.
Нажимаем Preview:
Кстати, нажав на ссылку с именем файла можно посмотреть создаваемый код - к примеру контроллера:
Генерируем код:
Перейдем по ссылке try it now, создадим пару авторов - у меня Smith & Wesson.
Похожим образом поступаем с таблицей book - книги. Создадим пару книг - у меня Gang & Bang.
А теперь хотелось бы добавить нашей книге авторов.
Качаем последнюю версию расширения CAdvancedArBehavior, после чего распакуем файл CAdvancedArBehavior.php в каталог приложения "protected/extensions/" (у меня "w:\home\yii-test\www\protected\extensions\").
Для того, чтобы не повторять код в каждом классе модели, содержащей отношения многие-ко-многим, создадим файл myActiveRecord.php, содержащий код класса, который наследует от CActiveRecord.
Изменим наследование классов Auth и Book, добавим правила (public function rules()) для связей books и auth (public function relations()):
- Auth.php:
- ./protected/views/auth/_form.php:
- ./protected/views/book/_form.php:
Редактируем данные автора Smith, добавим в его коллекцию обе книги:
Открываем книгу Gang:
Все в елочку. Добавляем открытой книге Gang еще одного автора:
Без проблем. А теперь откроем, к примеру, автора Wesson и попробуем присвоить ему авторство обоих произведений выбрав их с помощью клавиатурного сочетания Ctrl+A:
Много букаф :). Решение очевидно - нужно добавить проверку на наличие пустого поля в списке выделенных полей. Редактируем код скачанного ранее расширения - файл CAdvancedArBehavior.php (./protected/extensions/CAdvancedArBehavior.php):
Проверяем:
Easy peasy lemon squeezy :).
На протяжении изложения я буду использовать MS Windows + denwer.
Стартуем denwer, открываем phpMyAdmin, создаем тестовую базу данных и таблицы:
create database if not exists test_db character set utf8 collate utf8_general_ci; use test_db; create table if not exists `auth` ( `id` mediumint unsigned not null auto_increment, `name` varchar(20) not null, primary key(`id`) ) engine=innodb; create table if not exists `book` ( `id` mediumint unsigned not null auto_increment, `name` varchar(20) not null, primary key(`id`) ) engine=innodb; create table if not exists `auth_to_book` ( `auth_id` mediumint unsigned not null, `book_id` mediumint unsigned not null, primary key (`auth_id`, `book_id`), constraint `fk_auth` foreign key (`auth_id`) references auth(`id`), constraint `fk_book` foreign key (`book_id`) references book(`id`) ) engine=innodb;
Поясню: auth - авторы, book - книги, auth_to_book - связь авторов с книгами многие-ко-многим. Стандартный пример.
Создаем хост по имени yii-test, рестартуем denwer, качаем последнюю версию yii framework со страницы загрузки, распакуем каталог framework в корневой каталог хоста.
Запускаем консоль, переходим в каталог распакованного фреймворка. Не забываем добавить в переменную окружения PATH путь к интерпретатору PHP - в моем случае "W:\usr\local\php5\".
Создаем приложение:
- php -f yiic webapp ..\
В браузере идем по адресу созданного хоста:
Редактируем файл конфигурации приложения main.php, который можно найти в каталоге "protected/config/" (у меня w:\home\yii-test\www\protected\config\main.php).
<?php // uncomment the following to define a path alias // Yii::setPathOfAlias('local','path/to/local-folder'); // This is the main Web application configuration. Any writable // CWebApplication properties can be configured here. return array( 'basePath'=>dirname(__FILE__).DIRECTORY_SEPARATOR.'..', 'name'=>'My Web Application', // preloading 'log' component 'preload'=>array('log'), // autoloading model and component classes 'import'=>array( 'application.models.*', 'application.components.*', ), 'modules'=>array( // uncomment the following to enable the Gii tool // рескоментируем для использования gii 'gii'=>array( 'class'=>'system.gii.GiiModule', 'password'=>false, // без пароля // If removed, Gii defaults to localhost only. Edit carefully to taste. 'ipFilters'=>array('127.0.0.1','::1'), ), ), // application components 'components'=>array( 'user'=>array( // enable cookie-based authentication 'allowAutoLogin'=>true, ), // uncomment the following to enable URLs in path-format /* 'urlManager'=>array( 'urlFormat'=>'path', 'rules'=>array( '<controller:\w+>/<id:\d+>'=>'<controller>/view', '<controller:\w+>/<action:\w+>/<id:\d+>'=>'<controller>/<action>', '<controller:\w+>/<action:\w+>'=>'<controller>/<action>', ), ), */ // закомментируем строку подключения к sqlite /* 'db'=>array( 'connectionString' => 'sqlite:'.dirname(__FILE__).'/../data/testdrive.db', */ // uncomment the following to use a MySQL database // раскомментируем для использования mysql 'db'=>array( 'connectionString' => 'mysql:host=localhost;dbname=test_db', 'emulatePrepare' => true, 'username' => 'root', 'password' => '', 'charset' => 'utf8', ), 'errorHandler'=>array( // use 'site/error' action to display errors 'errorAction'=>'site/error', ), 'log'=>array( 'class'=>'CLogRouter', 'routes'=>array( array( 'class'=>'CFileLogRoute', 'levels'=>'error, warning', ), // uncomment the following to show log messages on web pages /* array( 'class'=>'CWebLogRoute', ), */ ), ), ), // application-level parameters that can be accessed // using Yii::app()->params['paramName'] 'params'=>array( // this is used in contact page 'adminEmail'=>'webmaster@example.com', ), );
Логинимся с именем admin и паролем admin, открываем страницу генератора кода нашего приложения.
Используем генератор кода по прямому назначению: создадим модель данных таблицы auth - авторы.
Переходим по ссылке Model Generator, выбираем таблицу:
Нажимаем Preview:
Генерируем код:
В результате мы получили файл Auth.php, содержащий код класса модели таблицы auth по имени Auth.
<?php /** * This is the model class for table "auth". * * The followings are the available columns in table 'auth': * @property integer $id * @property string $name * * The followings are the available model relations: * @property Book[] $books */ class Auth extends CActiveRecord { /** * @return string the associated database table name */ public function tableName() { return 'auth'; } /** * @return array validation rules for model attributes. */ public function rules() { // NOTE: you should only define rules for those attributes that // will receive user inputs. return array( array('name', 'required'), array('name', 'length', 'max'=>20), // The following rule is used by search(). // @todo Please remove those attributes that should not be searched. array('id, name', 'safe', 'on'=>'search'), ); } /** * @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( 'books' => array(self::MANY_MANY, 'Book', 'auth_to_book(auth_id, book_id)'), ); } /** * @return array customized attribute labels (name=>label) */ public function attributeLabels() { return array( 'id' => 'ID', 'name' => 'Name', ); } /** * Retrieves a list of models based on the current search/filter conditions. * * Typical usecase: * - Initialize the model fields with values from filter form. * - Execute this method to get CActiveDataProvider instance which will filter * models according to data in model fields. * - Pass data provider to CGridView, CListView or any similar widget. * * @return CActiveDataProvider the data provider that can return the models * based on the search/filter conditions. */ 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); $criteria->compare('name',$this->name,true); return new CActiveDataProvider($this, array( 'criteria'=>$criteria, )); } /** * Returns the static model of the specified AR class. * Please note that you should have this exact method in all your CActiveRecord descendants! * @param string $className active record class name. * @return Auth the static model class */ public static function model($className=__CLASS__) { return parent::model($className); } }Идем по ссылке Crud Generator, создаем контроллер, а также представления CRUD (Create, Read, Update, Delete) операций с созданной выше моделью данных.
Нажимаем Preview:
Кстати, нажав на ссылку с именем файла можно посмотреть создаваемый код - к примеру контроллера:
Генерируем код:
Перейдем по ссылке try it now, создадим пару авторов - у меня Smith & Wesson.
Похожим образом поступаем с таблицей book - книги. Создадим пару книг - у меня Gang & Bang.
А теперь хотелось бы добавить нашей книге авторов.
Качаем последнюю версию расширения CAdvancedArBehavior, после чего распакуем файл CAdvancedArBehavior.php в каталог приложения "protected/extensions/" (у меня "w:\home\yii-test\www\protected\extensions\").
Для того, чтобы не повторять код в каждом классе модели, содержащей отношения многие-ко-многим, создадим файл myActiveRecord.php, содержащий код класса, который наследует от CActiveRecord.
<?php abstract class myActiveRecord extends CActiveRecord { public function behaviors() { return array( 'CAdvancedArBehavior' => array( 'class' => 'application.extensions.CAdvancedArBehavior'), ); } } ?>
Изменим наследование классов Auth и Book, добавим правила (public function rules()) для связей books и auth (public function relations()):
- Auth.php:
<?php /** * This is the model class for table "auth". * * The followings are the available columns in table 'auth': * @property integer $id * @property string $name * * The followings are the available model relations: * @property Book[] $books */ class Auth extends myActiveRecord { /** * @return string the associated database table name */ public function tableName() { return 'auth'; } /** * @return array validation rules for model attributes. */ public function rules() { // NOTE: you should only define rules for those attributes that // will receive user inputs. return array( array('name', 'required'), array('name', 'length', 'max'=>20), // The following rule is used by search(). // @todo Please remove those attributes that should not be searched. array('id, name', 'safe', 'on'=>'search'), array('books', 'safe'), ); } /** * @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( 'books' => array(self::MANY_MANY, 'Book', 'auth_to_book(auth_id, book_id)'), ); } /** * @return array customized attribute labels (name=>label) */ public function attributeLabels() { return array( 'id' => 'ID', 'name' => 'Name', ); } /** * Retrieves a list of models based on the current search/filter conditions. * * Typical usecase: * - Initialize the model fields with values from filter form. * - Execute this method to get CActiveDataProvider instance which will filter * models according to data in model fields. * - Pass data provider to CGridView, CListView or any similar widget. * * @return CActiveDataProvider the data provider that can return the models * based on the search/filter conditions. */ 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); $criteria->compare('name',$this->name,true); return new CActiveDataProvider($this, array( 'criteria'=>$criteria, )); } /** * Returns the static model of the specified AR class. * Please note that you should have this exact method in all your CActiveRecord descendants! * @param string $className active record class name. * @return Auth the static model class */ public static function model($className=__CLASS__) { return parent::model($className); } }- Book.php:
<?php /** * This is the model class for table "book". * * The followings are the available columns in table 'book': * @property integer $id * @property string $name * * The followings are the available model relations: * @property Auth[] $auths */ class Book extends myActiveRecord { /** * @return string the associated database table name */ public function tableName() { return 'book'; } /** * @return array validation rules for model attributes. */ public function rules() { // NOTE: you should only define rules for those attributes that // will receive user inputs. return array( array('name', 'required'), array('name', 'length', 'max'=>20), // The following rule is used by search(). // @todo Please remove those attributes that should not be searched. array('id, name', 'safe', 'on'=>'search'), array('auths', 'safe'), ); } /** * @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( 'auths' => array(self::MANY_MANY, 'Auth', 'auth_to_book(book_id, auth_id)'), ); } /** * @return array customized attribute labels (name=>label) */ public function attributeLabels() { return array( 'id' => 'ID', 'name' => 'Name', ); } /** * Retrieves a list of models based on the current search/filter conditions. * * Typical usecase: * - Initialize the model fields with values from filter form. * - Execute this method to get CActiveDataProvider instance which will filter * models according to data in model fields. * - Pass data provider to CGridView, CListView or any similar widget. * * @return CActiveDataProvider the data provider that can return the models * based on the search/filter conditions. */ 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); $criteria->compare('name',$this->name,true); return new CActiveDataProvider($this, array( 'criteria'=>$criteria, )); } /** * Returns the static model of the specified AR class. * Please note that you should have this exact method in all your CActiveRecord descendants! * @param string $className active record class name. * @return Book the static model class */ public static function model($className=__CLASS__) { return parent::model($className); } }Открываем файлы _form.php представлений, редактируем - добавим элементы для отображения связанных значений:
- ./protected/views/auth/_form.php:
<?php /* @var $this AuthController */ /* @var $model Auth */ /* @var $form CActiveForm */ ?> <div class="form"> <?php $form=$this->beginWidget('CActiveForm', array( 'id'=>'auth-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'=>20,'maxlength'=>20)); ?> <?php echo $form->error($model,'name'); ?> </div> <div class="row"> <?php echo $form->labelEx($model, 'books'); ?> <?php echo $form->dropDownList($model,'books', CHtml::listData(Book::model()->findAll(array('order'=>'id')), 'id', 'name'), array('empty'=>'','multiple'=>'multiple','style'=>'width:300px;','size'=>'3')); ?> <?php echo $form->error($model, 'books'); ?> </div> <div class="row buttons"> <?php echo CHtml::submitButton($model->isNewRecord ? 'Create' : 'Save'); ?> </div> <?php $this->endWidget(); ?> </div><!-- form -->
- ./protected/views/book/_form.php:
<?php /* @var $this BookController */ /* @var $model Book */ /* @var $form CActiveForm */ ?> <div class="form"> <?php $form=$this->beginWidget('CActiveForm', array( 'id'=>'book-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'=>20,'maxlength'=>20)); ?> <?php echo $form->error($model,'name'); ?> </div> <div class="row"> <?php echo $form->labelEx($model, 'auths'); ?> <?php echo $form->dropDownList($model,'auths', CHtml::listData(Auth::model()->findAll(array('order'=>'id')), 'id', 'name'), array('empty'=>'','multiple'=>'multiple','style'=>'width:300px;','size'=>'3')); ?> <?php echo $form->error($model, 'auths'); ?> </div> <div class="row buttons"> <?php echo CHtml::submitButton($model->isNewRecord ? 'Create' : 'Save'); ?> </div> <?php $this->endWidget(); ?> </div><!-- form -->
Редактируем данные автора Smith, добавим в его коллекцию обе книги:
Открываем книгу Gang:
Все в елочку. Добавляем открытой книге Gang еще одного автора:
Без проблем. А теперь откроем, к примеру, автора Wesson и попробуем присвоить ему авторство обоих произведений выбрав их с помощью клавиатурного сочетания Ctrl+A:
Много букаф :). Решение очевидно - нужно добавить проверку на наличие пустого поля в списке выделенных полей. Редактируем код скачанного ранее расширения - файл CAdvancedArBehavior.php (./protected/extensions/CAdvancedArBehavior.php):
<?php class CAdvancedArbehavior extends CActiveRecordBehavior { // Set this to false to disable tracing of changes public $trace = true; // If you want to ignore some relations, set them here. public $ignoreRelations = array(); // After the save process of the model this behavior is attached to // is finished, we begin saving our MANY_MANY related data public function afterSave($event) { if(!is_array($this->ignoreRelations)) throw new CException('ignoreRelations of CAdvancedArBehavior needs to be an array'); $this->writeManyManyTables(); parent::afterSave($event); return true; } protected function writeManyManyTables() { if($this->trace) Yii::trace('writing MANY_MANY data for '.get_class($this->owner), 'system.db.ar.CActiveRecord'); foreach($this->getRelations() as $relation) { $this->cleanRelation($relation); $this->writeRelation($relation); } } /* A relation will have the following format: $relation['m2mTable'] = the tablename of the foreign object $relation['m2mThisField'] = the column in the many2many table that represents the primary Key of the object that this behavior is attached to $relation['m2mForeignField'] = the column in the many2many table that represents the foreign object. Written in Yii relation syntax, it would be like this 'relationname' => array('foreignobject', 'column', 'm2mTable(m2mThisField, m2mForeignField) */ protected function getRelations() { $relations = array(); foreach ($this->owner->relations() as $key => $relation) { if ($relation[0] == CActiveRecord::MANY_MANY && !in_array($key, $this->ignoreRelations) && $this->owner->hasRelated($key) && $this->owner->$key != -1) { $info = array(); $info['key'] = $key; $info['foreignTable'] = $relation[1]; if (preg_match('/^(.+)\((.+)\s*,\s*(.+)\)$/s', $relation[2], $pocks)) { $info['m2mTable'] = $pocks[1]; $info['m2mThisField'] = $pocks[2]; $info['m2mForeignField'] = $pocks[3]; } else { $info['m2mTable'] = $relation[2]; $info['m2mThisField'] = $this->owner->tableSchema->PrimaryKey; $info['m2mForeignField'] = CActiveRecord::model($relation[1])->tableSchema->primaryKey; } $relations[$key] = $info; } } return $relations; } /** writeRelation's job is to check if the user has given an array or an * single Object, and executes the needed query */ protected function writeRelation($relation) { $key = $relation['key']; // Only an object or primary key id is given if(!is_array($this->owner->$key) && $this->owner->$key != array()) $this->owner->$key = array($this->owner->$key); // An array of objects is given foreach((array)$this->owner->$key as $foreignobject) { if(empty($foreignobject)) continue; // если выбрали пустое поле - не пишем if(!is_numeric($foreignobject) && is_object($foreignobject)) $foreignobject = $foreignobject->{$foreignobject->$relation['m2mForeignField']}; $this->execute($this->makeManyManyInsertCommand($relation, $foreignobject)); } } /* before saving our relation data, we need to clean up exsting relations so * they are synchronized */ protected function cleanRelation($relation) { $this->execute($this->makeManyManyDeleteCommand($relation)); } // A wrapper function for execution of SQL queries public function execute($query) { return Yii::app()->db->createCommand($query)->execute(); } public function makeManyManyInsertCommand($relation, $value) { return sprintf("insert into %s (%s, %s) values ('%s', '%s')", $relation['m2mTable'], $relation['m2mThisField'], $relation['m2mForeignField'], $this->owner->{$this->owner->tableSchema->primaryKey}, $value); } public function makeManyManyDeleteCommand($relation) { return sprintf("delete ignore from %s where %s = '%s'", $relation['m2mTable'], $relation['m2mThisField'], $this->owner->{$this->owner->tableSchema->primaryKey} ); } }Тестируем: пытаемся сохранить...ошибок нет.
Проверяем:
Easy peasy lemon squeezy :).
Комментариев нет:
Отправить комментарий
Комментарий будет опубликован после модерации