今天开发的过程中对Yii的CActiveRecord成员函数setAttribute进行了一次封装,主要是实现将变更的字段、字段旧值、字段新值、操作类型
(新增/编辑)、设计到的变更表和操作人等信息记录到定义的日志池中,之后对成员方法update()、insert()和save()进行封装,在成功操作后绑定记录日志事件并清空日志信息池,最终代码如下:
<?php
Abstract class AmsAbstractList extends CActiveRecord{
/**
* @access public
* @var array $aLogs 日志信息池
*/
public $aLogs = array();
/**
* @access public
* @var int $nLogCount 日志信息数量
*/
public $nLogCount = 0;
public function init() {
//绑定日记记录事件
attachEventHandler('onAddRecordLog', array($this, 'addRecordLog'));
parent::init();
}
/**
* 对Yii的setAttribute进行封装,增加变更字段的日志信息池记录
*
* @access public
* @param string $sName 属性名称
* @param string $sValue 属性值
* @return boolean 基类设置属性值是否成功的标识
*/
public function setAttribute($sName, $sValue) {
$sSrc = $this->$sName;//旧的数据
$sDst = $sValue; //新的数据
$bSetSuccess = parent::setAttribute($sName, $sValue);
//日志信息池记录
if($bSetSuccess && $sSrc != $sDst && $sName != 'id') {
$this->aLogs[] = array(
'object_id' => $this->id,
'table_name' => $this->tableName(),
'field_name' => $sName,
'src' => $sSrc,
'dst' => $sDst,
'date' => time(),
'desc' => '',
'operator' => Yii::app()->user->name,
'operate' => $this->isNewRecord ? '新增' : '编辑',
);
$this->nLogCount++;
}
return $bSetSuccess;
}
/**
* AR类信息保存或更新
*
* @access public
* @param boolean $runValidation
* @param array $attributes 需要保存的属性数组
* @return boolean 操作成功或失败
*/
public function save($runValidation = true, $attributes = null) {
$bSuccess = parent::save($runValidation, $attributes);
$this->flushLogData($bSuccess);
return $bSuccess;
}
/**
* 激活日志记录事件并清空日志信息池
*
* @access public
* @param boolean $bFlush 是否需要激活记录事件
*/
public function flushLogData($bFlush = false) {
if($bFlush && $this->hasEventHandler('onAddRecordLog')) {
$this->onAddRecordLog(new CEvent($this));
}
$this->aLogs = array();
$this->nLogCount = 0;
}
/**
* 增加记录日志事件
*
* @access public
* @param CEvent $objEvent The Event Parameter
*/
public function onAddRecordLog($objEvent) {
$this->raiseEvent('onAddRecordLog', $objEvent);
}
/**
* 记录日志事件回调
* 因为每个资产数据的日志表结构不同
* 需要记录日志的AR类各自实现日志记录逻辑
*
* @access public
* @param CEvent $objEvent The Event Parameter
*/
public function addRecordLog($objEvent) {
//记录日志操作,需要在各个类中各自实现
}
起初代码刚刚写好之后出现了两个问题:
一个是无法通过$ar->onAddRecordLog = array($this, ‘func’);方式去绑定事件回调方法;
二是无法通过$ar->attributes = array(‘attr1′ => ‘val1′, ‘attr2′ => ‘val2′);方式去为类成员批量赋值,系统会将attributes认为是ar的一个成员属性
由于根本没有在ar中定义attributes这个属性,那么断定为问题出现在了__set上,于是去翻阅Yii的源代码去查看问题所在,其中CActiveRecord的源代码中对于__set魔法函数的实现代码如下:
<?php
/**
* PHP setter magic method.
* This method is overridden so that AR attributes can be accessed like properties.
* @param string $name property name
* @param mixed $value property value
*/
public function __set($name,$value)
{
if($this->setAttribute($name,$value)===false)
{
if(isset($this->getMetaData()->relations[$name]))
$this->_related[$name]=$value;
else
parent::__set($name,$value);
}
}
/**
* Sets the named attribute value.
* You may also use $this->AttributeName to set the attribute value.
* @param string $name the attribute name
* @param mixed $value the attribute value.
* @return boolean whether the attribute exists and the assignment is conducted successfully
* @see hasAttribute
*/
public function setAttribute($name,$value)
{
if(property_exists($this,$name))
$this->$name=$value;
else if(isset($this->getMetaData()->columns[$name]))
$this->_attributes[$name]=$value;
else
return false;
return true;
}
大家可以看到这里首先是调用了类中的成员函数setAttribute方法,而setAttribute方法首先验证传入的名称是否是ar类的成员属性,如果是直接进行复制,如果不是继续验证是否为元数据,如果为元数据,那么就存储到ar类的属性集合中,其他情况则返回false,设置成功则返回true
ar的__set方法根据setAttribute的返回值进行验证,若返回值绝对等于false,那么会验证是否为ar的关联数据,若为是那么记录到ar类的关联关系集合中,若不是则调用父类CModel的__set方法,CModel中不存在__set魔术方法,方法继承自父类CComponent,具体代码如下:
<?php
/**
* Sets value of a component property.
* Do not call this method. This is a PHP magic method that we override
* to allow using the following syntax to set a property or attach an event handler
* $this->propertyName=$value;
* $this->eventName=$callback;
*
* @param string $name the property name or the event name
* @param mixed $value the property value or callback
* @throws CException if the property/event is not defined or the property is read only.
* @see __get
*/
public function __set($name,$value)
{
$setter='set'.$name;
if(method_exists($this,$setter))
return $this->$setter($value);
else if(strncasecmp($name,'on',2)===0 && method_exists($this,$name))
{
// duplicating getEventHandlers() here for performance
$name=strtolower($name);
if(!isset($this->_e[$name]))
$this->_e[$name]=new CList;
return $this->_e[$name]->add($value);
}
else if(is_array($this->_m))
{
foreach($this->_m as $object)
{
if($object->getEnabled() && (property_exists($object,$name) || $object->canSetProperty($name)))
return $object->$name=$value;
}
}
if(method_exists($this,'get'.$name))
throw new CException(Yii::t('yii','Property "{class}.{property}" is read only.', array('{class}'=>get_class($this), '{property}'=>$name)));
else
throw new CException(Yii::t('yii','Property "{class}.{property}" is not defined.',
array('{class}'=>get_class($this), '{property}'=>$name)));
}
这个方法会从根本上去做分发工作,若存在setXX方法,那么会进行调用赋值,其次判断是否为on开头,因为on是Yii中事件注册关键字,如果为on,那么就注册到系统的事件集合中,若非以上两种情况,那么验证是否存在该名称的behavior,存在则进行赋值绑定。
说道这里那么回想我当初的问题,无法对on事件进行绑定回调,和无法使用attributes进行赋值,回首自己写的setAttribute方法,发现没有进行对parent::setAttribute的返回值进行返回, 则导致setAttribute() === false 始终不成立,无法进入到父类的__set方法,也就无法使用setAttributes方法和on开头的事件绑定!所以,开发需谨慎!