05 September 2012

今天开发的过程中对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开头的事件绑定!所以,开发需谨慎!

  • 分享到: