Events and CActiveRecord

Hi,

I am trying to fire an event in a Component class (UploadWidget) and handle (listen) it in my Portfolio Model.

This is the flow of events:

[list=1]

[*]Save Form (Portfolio Model) (Working)

[*]onAfterSave is fired (Working)

[*]UploadImages using UploadWidget (Working)

[*]onAfterUpload is fired (Partially working)

[*]Resize Images in Portfolio (Not Working)

[/list]

How do I attach the event handler in Portfolio Model to listen to UploadWidget?

Thanks!!!!

UploadWidget




<?php


class UploadWidget extends CWidget

{

    const PREFIX = 'upl.';              // Prefix for controllers

    const NAME = 'simpleUploadWidget[]';

    const DELETE_ACTION = 'delete';


    public static $tempDir = 'tmp';     // Temporal dir for storing files if necessary

    public static $fileDir = 'files';   // Definitive repository dir for storing files

    public static $thumbnailDirectory = 'files/thumbnails';

    public $model;                      // Model with files attached

    public $inputClassName = 'uploadInput';

    private $_enabled;

    private $_owner;

    private $inputName = 'simpleUploadWidget';


    public function init()

    {

        // Ensure the model is attached

        if ($this->model != null)

            $this->attach($this->model);


        parent::init();

    }


    /** Default view of the widget */

    public function run()

    {

        $this->render('uploadWidget', array(

            'name' => self::NAME,

            'inputClassName' => $this->inputClassName,

            'hide' => ($this->model->getRemainingImageCount() < 1) ? 'hide' : '',

        ));

    }


    public static function actions()

    {

        return array(

            self::DELETE_ACTION => array(

                'class' => 'ext.upload.actions.DeleteAction',

            ),

        );

    }


    public function behaviors()

    {

        return array(

            'onUploadComplete' => array(

                'class' => 'Portfolio')

        );

    }


    /**

     * Events

     */

    public function events()

    {

        return array(

            'onAfterSave' => 'onAfterSaveHandler',

            'onAfterDelete' => 'onAfterDeleteHandler',

        );

    }


    public function onAfterSaveHandler(CEvent $event)

    {

        for ($i = 0; $i < $this->model->getRemainingImageCount(); $i++)

        {

            $filename = UploadUtils::createUniquefilename(

                    $_FILES[$this->inputName]['name'][$i],

                    UploadUtils::getPath(self::$fileDir));


            $path = UploadUtils::getPath(self::$fileDir);


            if (move_uploaded_file(

                    $_FILES[$this->inputName]['tmp_name'][$i],

                    $path . DIRECTORY_SEPARATOR . $filename))

            {

                $this->saveFileToDB($filename, $path);

            }

        }


        $this->uploadComplete();

    }


    public function uploadComplete()

    {

        //if ($this->hasEventHandler('onUploadComplete'))

            $this->onUploadComplete(new CEvent($this));

    }


    public function onUploadComplete($event)

    {

        $this->raiseEvent('onUploadComplete',$event);

    }


    private function saveFileToDB($filename, $path)

    {

        $file = new File;


        $file->filename = $filename;

        $file->entity = get_class($this->model);

        $file->entity_id = $this->model->getPrimaryKey();

        $file->timestamp = time();

        $file->filemime = CFileHelper::getMimeTypeByExtension($file->filename);

        $file->filesize = filesize($path . DIRECTORY_SEPARATOR . $filename);


        // Save the file info to the DB

        $file->save();

    }


    /** Delete all files associated to this model */

    public function onAfterDeleteHandler($event)

    {

        UploadUtils::deleteFiles($this->getOwner(), self::$fileDir);

    }


    /**

     * Attaches the behavior object to the component.

     * The default implementation will set the {@link owner} property

     * and attach event handlers as declared in {@link events}.

     * Make sure you call the parent implementation if you override this method.

     * @param CComponent the component that this behavior is to be attached to.

     */

    public function attach($owner)

    {

        $this->model = $owner;

        $this->_owner = $owner;

        foreach ($this->events() as $event => $handler)

            $owner->attachEventHandler($event, array($this, $handler));


        // Attach relation to the model

        $owner->metadata->relations['files'] = new CHasManyRelation(

                'files', 'File', 'entity_id',

                array(

                    'condition' => "entity='" . get_class($owner) . "'",

            ));

    }


    /**

     * Detaches the behavior object from the component.

     * The default implementation will unset the {@link owner} property

     * and detach event handlers declared in {@link events}.

     * Make sure you call the parent implementation if you override this method.

     * @param CComponent the component that this behavior is to be detached from.

     */

    public function detach($owner)

    {

        foreach ($this->events() as $event => $handler)

            $owner->detachEventHandler($event, array($this, $handler));

        $this->_owner = null;


        // Dettach relation of the model

        unset($owner->metadata->relations['files']);

    }


    /** @return CComponent the owner component that this behavior is attached to. */

    public function getOwner()

    {

        return $this->_owner;

    }


    /** @return boolean whether this behavior is enabled */

    public function getEnabled()

    {

        return $this->_enabled;

    }


    /** @param boolean whether this behavior is enabled */

    public function setEnabled($value)

    {

        $this->_enabled = $value;

    }


    public function getRemainingImagesCount()

    {

        return $this->remainingImagesCount;

    }


}



Portfolio Model




<?php


/**

 * This is the model class for table "{{portfolio}}".

 *

 * The followings are the available columns in table '{{portfolio}}':

 * @property integer $id

 * @property integer $published

 * @property string $description

 * @property string $title

 * @property string $url

 * @property string $create_time

 * @property string $update_time

 * @property integer $create_user_id

 * @property integer $update_user_id

 */

class Portfolio extends CActiveRecord

{


    private $isNew = false;


    const IMAGE_WIDTH = null;

    const IMAGE_HEIGHT = 500;

    const THUMB_WIDTH = null;

    const THUMB_HEIGHT = 190;

    /*

     * The maximum number of images allowed per portfolio.

     */

    const MAX_IMAGE_COUNT = 4;


    public function getRemainingImageCount()

    {

        return self::MAX_IMAGE_COUNT - count($this->images);

    }


    /**

     * Returns the static model of the specified AR class.

     * @return Portfolio the static model class

     */

    public static function model($className=__CLASS__)

    {

        return parent::model($className);

    }


    /**

     * @return string the associated database table name

     */

    public function tableName()

    {

        return '{{portfolio}}';

    }


    /**

     * @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('published, description, title, url, services', 'required'),

            array('published, create_user_id, update_user_id', 'numerical', 'integerOnly' => true),

            array('title, url', 'length', 'max' => 255),

            // The following rule is used by search().

            // Please remove those attributes that should not be searched.

            array('id, published, description, title, url', '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(

            'author' => array(self::BELONGS_TO, 'User', 'create_user_id'),

            'update_author' => array(self::BELONGS_TO, 'User', 'update_user_id'),

            'images' => array(self::HAS_MANY, 'File', 'entity_id'),

            'services' => array(self::MANY_MANY, 'Service', 'tbl_portfolio_services_performed(portfolio_id, services_perfomed_id)'),

        );

    }


    protected function beforeValidate()

    {

        if ($this->isNewRecord)

        {

            $this->create_time = $this->update_time = new CDbExpression('NOW()');

            $this->create_user_id = Yii::app()->user->id;

        }

        else

        {

            $this->update_time = new CDbExpression('NOW()');

            $this->update_user_id = Yii::app()->user->id;

        }


        return parent::beforeValidate();

    }


    /**

     * Appends the behaviour to deal with files in the model lifecycle and the files

     *   relation (thx to jonah <img src='http://www.yiiframework.com/forum/public/style_emoticons/default/tongue.gif' class='bbc_emoticon' alt=':P' />)

     */

    public function behaviors()

    {

        return array(

            'UploadBehaviour' => array(

                'class' => 'ext.upload.UploadWidget'),

            'CAdvancedArBehavior' => array(

                'class' => 'ext.CAdvancedArBehavior'),

        );

    }


    /**

     * Events

     */

    public function events()

    {

        return array(

            'onUploadComplete' => 'onUploadCompleteHandler',

        );

    }


    public function onUploadCompleteHandler(CEvent $event)

    {

        echo 'xxxxxxxxxxxxxxxxxxxxxxx';

        foreach ($this->images as $portfolioImage)

        {

            $image = Yii::app()->image->load($portfolioImage->getFullImagePath());

            $image->master_dim = Image::HEIGHT;

            $image->resize(self::IMAGE_WIDTH, self::IMAGE_HEIGHT);

            $image->save();


            $image->makeThumbnail(self::THUMB_WIDTH, self::THUMB_HEIGHT, $portfolioImage->getFullImagePath());

        }

    }


    /**

     * @return array customized attribute labels (name=>label)

     */

    public function attributeLabels()

    {

        return array(

            'id' => 'ID',

            'published' => 'Published',

            'description' => 'Description',

            'title' => 'Title',

            'url' => 'Url',

            'create_time' => 'Created',

            'update_time' => 'Updated',

            'create_user_id' => 'Created By',

            'update_user_id' => 'Updated By',

        );

    }


    /**

     * Retrieves a list of models based on the current search/filter conditions.

     * @return CActiveDataProvider the data provider that can return the models based on the search/filter conditions.

     */

    public function search()

    {

        $criteria = new CDbCriteria;


        $criteria->compare('id', $this->id);

        $criteria->compare('published', $this->published);

        $criteria->compare('description', $this->description, true);

        $criteria->compare('title', $this->title, true);

        $criteria->compare('url', $this->url, true);


        return new CActiveDataProvider(get_class($this), array(

            'criteria' => $criteria,

        ));

    }


    public function getIsNew()

    {

        $today = strtotime(date("Y-m-d"));

        $created = strtotime($this->create_time);


        if ($created >= ($today - 7))

        {

            $this->isNew = true;

        }

        else

        {

            $this->isNew = false;

        }


        return $this->isNew;

    }


    public function listServices()

    {

        $values = array();


        foreach ($this->services as $service)

        {

            $values[] = $service->name;

        }


        return implode(', ', $values);

    }


    

}



I’m also a big fan of using events to automate things. But i have some problems understanding your workflow, still. Can you explain in more detail, e.g. which component fires? You also seem to connect your UploadWidget as behavior. Is this a typo or how’s that supposed to work? Your upload widget would have to implement IBehavior therefore, which it doesn’t.

Hi,

I would like to use events exclusively; I.e no behaviors.

What is happening now is: when I save the Portfolio Form the onAfterSave event is fired. The UploadWidget is subscribing/listening to that event and uploads the files from the $_FILES global. This is working ok. AFAIK I have set this up by including a behavior function in my Portfolio Model. See code snippet. Does this link the Portfolio and UploadWidget? If I remove this function, the event isn’t handled anymore. Can you suggest a more succinct approach - without using the behaviors() method?




/**

     * Appends the behaviour to deal with files in the model lifecycle and the files

     *   relation (thx to jonah <img src='http://www.yiiframework.com/forum/public/style_emoticons/default/tongue.gif' class='bbc_emoticon' alt=':P' />)

     */

    public function behaviors()

    {

        return array(

            'UploadBehaviour' => array(

                'class' => 'ext.upload.UploadWidget'),

            'CAdvancedArBehavior' => array(

                'class' => 'ext.CAdvancedArBehavior'),

        );

    }



The next piece that I’d like to get working is, after the upload has completed, fire and event “onUploadComplete” and have the Portfolio Model subscribe/listen to that event. I will do some image resizing once I know the images have been uploaded and linked to the model.

Please, any advice on events (syntax, attaching, using the events() method, removing the behavior() method) etc would make my day.

Cheers.

Before i dive deeper into your code: Somehow your design doesn’t really feel right to me, under OOP perspective. I don’t really get, why you put so much logic into UploadWidget. A widget should mainly be used for presentational purpose. What you do here is rather business logic - so i would tend to move this code somewhere into the model section. Wouldn’t you agree?

Because often things straighten out, when you use the right place for some code. E.g. how about using UploadImage class, which represents an image and handles logic to save/retrieve images?

Hi Mike,

Thanks for the tip. I rewrote the code and merged it into an UploadModule. My only issue is whether to write it as a module, component or a hybrid.

For example, it is purely a module and I can instantiate the Upload class by:




$upload = new Upload($model, 5);

$upload->setDimensions(200, 500, 20, 50);

$upload->upload();

$upload->save();

$upload->resize();

$upload->thumbnail();



This way, I have code hints (using Netbeans). If I wrote the Upload class as a component there isn’t any code hints. In my case, users can instantiate several objects throughout the application and will need different config options. Ex: image dimensions, thumbnails dimensions etc.

Is there a benefit to a module or a component?

Thanks,

Matt