На днях делал админку одного проекта, замутил на 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 :).


















Комментариев нет:
Отправить комментарий
Комментарий будет опубликован после модерации