Yii 1.1: cadvancedarbehavior

CAdvancedArBehavior (Advanced functions for Active Record implementation)
51 followers

The CAdvancedArBehavior extension adds up some functionality to the default possibilites of yii´s ActiveRecord implementation. At the moment it is able to automatically save MANY_MANY relation objects when save()-ing an Object.

Changelog

Version 0.3 added 25. 05. 2011 by thyseus

  • added $ignoreRelations to ignore specified relations. The Behavior will take all found many2many relations by default. Specify exceptions in this array.
  • fixes all the bugs and glitches found in the discussion

Resources

Documentation

Requirements

  • Yii 1.1 or above

Installation

To use this extension, just copy this file to your extensions/ directory, add 'import' => 'application.extensions.CAdvancedArBehavior', [...] to your config/main.php and add this behavior to each model you would like to inherit the new possibilities.

Usage

public function behaviors(){
          return array( 'CAdvancedArBehavior' => array(
            'class' => 'application.extensions.CAdvancedArBehavior'));
          }

Possibilities so far:

Better support of MANY_TO_MANY relations:

When we have defined a MANY_MANY relation in our relations() function, we are now able to add up instances of the foreign Model on the fly while saving our Model to the Database. Let´s assume the following Relation:

Post has: 'categories'=>array(self::MANY_MANY, 'Category', 'tbl_post_category(post_id, category_id)')

Category has: 'posts'=>array(self::MANY_MANY, 'Post', 'tbl_post_category(category_id, post_id)')

Now we can use the attribute 'categories' of our Post model to add up new rows to our MANY_MANY connection Table:

$post = new Post();
$post->categories = Category::model()->findAll();
$post->save();

This will save our new Post in the table Post, and in addition to this it updates our N:M-Table with every Category available in the Database.

We can further limit the Objects given to the attribute, and can also go the other Way around:

 $category = new Category();
 $category->posts = array(5, 6, 7, 10);
 $category->save();

We can pass Object instances like in the first example, or a list of integers that representates the Primary key of the Foreign Table, so that the Posts with the id 5, 6, 7 and 10 get´s added up to our new Category.

5 Queries will be performed here, one for the Category-Model and four for the N:M-Table tbl_post_category. Note that this behavior could be tuned further in the future, so only one query get´s executed for the MANY_MANY Table.

We can also pass a single object or an single integer:

 $category = new Category();
 $category->posts = Post::model()->findByPk(12);
 $category->posts = 12;
 $category->save();

Change Log

January 30, 2010

Version 0.2 Code Cleanup, Bugfixes and added save() support

January 28, 2010

  • Initial release.

Total 20 comments

#16146 report it
Victor Silver at 2014/01/23 04:33am
Handling relations represented in a string

Firstly many thanks for the behavior.

The additions I present here handle data that has been input to a form as string to represent relations .... let's say there is a field that holds the following string "1,2,3,4" to represent the primary keys os a certain relation.

In order to handle this type of data automaticly I have done the following:

//declared as a public variable that can be overriden by the model as needed
public $relationStringGlue = ',';
 
//handle the string representation before the validation
//this way in case there is a validator of the type exists ... it will work
public function beforeValidate($event){
    $this->prepareRelations($event);
 
    return parent::beforeValidate($event);
}
 
/*
 * @param $event CEvent event parameter
 * before validating the model each ralation value is checked
 * in case ralation value is a string in the as eg.: 1,2,3,4
 * the string is explode into an array to comply with 
 * the behaviour intended operation
 */
 protected function prepareRelations($event) {
     foreach ($this->getRelations() as $relation) {
         $relationValue = $event->sender->$relation['key'];
         if (is_string($relationValue)) {
             $hasGlue = strpos($relationValue, $this->relationStringGlue);
             if ($hasGlue !== FALSE) {
                 $relationalArray = explode(',', $relationValue);
                 $event->sender->$relation['key'] = $relationalArray;
             }
        }
    }
}
#15499 report it
elexperimento at 2013/11/15 09:22am
AFTERSAVE event

I've been using this component in many projects, and it didn't work today. Everything was done correctly. BUT it didn't save the registries in the relation table. WHAT DID I DO? I went to the source code of the file, and identified this: public function afterSave($event) {

... } So... it is USING THE AFTER SAVE event. And I was using this very same event in a customized ActiveRecord class from which all the models are inherited. So, So, the execution wasn't even running the "afterSave" event in this class. So, folks, if you are doing everything right, AND IT STOPS WORKING, this debugging step might work: beware on how you are using your "afterSave" in your modules.

Regards,

David López. Investigación y Programación SAS

#15428 report it
elexperimento at 2013/11/08 10:29am
GREAT EXTENSION

Hi, I just want to thank you for this great extension: 1. It's simple to install 2. It's simple to implement 3. It's perfect for the lack of many-many relations that Yii should have, but this component solves easily.

Thanks, best regards from Colombia, South America. In exchange for this great component, I might share one of my developments, for example a lightbox library in javascript, or a conceptual scheme quite better than the usual methods for administering roles in databases.

David López Contact me at Investigación y Programación, http://investigacionyprogramacion.com

#13411 report it
waterloomatt at 2013/05/28 02:26am
Great work!

Couldn't live without this extension - at least for Yii 1.x Thanks!

#9565 report it
rhomb at 2012/08/23 07:44am
MSSQl

Thanks for the Behavior.. I've been using it for a couple of projects already, and will keep doing so in the future.

One thing to note: If using MSSQL you have to delete the "ignore" keyword from 193 so it looks like this:

return sprintf("delete from %s where %s = '%s'",

rho

#8783 report it
Maxim Ezhov at 2012/06/26 11:52am
Additional fields in connection table

I fell in trouble, when I had to save several additional fields in connection table. Information scheme is the next:

A -- C(a_id, b_id , field_1, field_2) -- B .

So problem: every time, when I save model A, all connections are reset, and all info in additional fields become lost. Even when any connection was modified.

In code I found that, on every saving of model A, all connections are removing from DB, and new created, according to POST data.

I made several changes in the code. Now connections are modified only in case, when new are added or existing removed (queries are optimized to affect only on modified records). So any additional infromation will not be lost.

Here is the code:

/** 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)
    {
        $ids = array_diff($this->getSetupIds($relation), $this->getExistedIds($relation));
 
        if (count($ids) > 0)
        {
            foreach ($ids as $id)
                $this->execute($this->makeManyManyInsertCommand($relation, $id));
        }
    }
 
    /**
     * Returns related ids which are exists in DB
     * 
     * @param array $relation
     * @return array 
     */
    protected function getExistedIds($relation)
    {
        $query = sprintf("select %s from %s where  %s = '%s'", $relation['m2mForeignField'], $relation['m2mTable'], $relation['m2mThisField'], $this->owner->{$this->owner->tableSchema->primaryKey}
        );
 
 
        $q_res = Yii::app()->db->createCommand($query)->query()->readAll();
        $result = array();
        foreach ($q_res as $row)
            $result[] = $row[$relation['m2mForeignField']];
 
        return $result;
    }
 
    /**
     * Returns related ids which are actualy connected
     * 
     * @param array $relation
     * @return array 
     */
    protected function getSetupIds($relation)
    {
        $IDS = array();
        $key = $relation['key'];
 
        // Only an object or primary key id is given
        if (is_object($this->owner->$key))
        {
            $this->owner->$key = array($this->owner->$key);
        }
 
        // An array of objects is given
        if (isset($this->owner->$key))
            foreach ($this->owner->$key as $foreignobject)
            {
                if (!is_numeric($foreignobject))
                {
                    $foreignobject = $foreignobject->{$foreignobject->$relation['m2mForeignField']};
                }
                $IDS[] = $foreignobject;
            }
 
        return $IDS;
    }
 
    /* 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));
    }
 
    public function execute($query)
    {
        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)
    {
        $ids = ($this->getSetupIds($relation));
        if (count($ids) > 0)
        {
            return sprintf("delete ignore from %s where  %s = '%s' and %s NOT IN (%s)", $relation['m2mTable'], $relation['m2mThisField'], $this->owner->{$this->owner->tableSchema->primaryKey}, $relation['m2mForeignField'], implode(',', $ids)
            );
        }
        else
        {
            return sprintf("delete ignore from %s where %s = '%s'", $relation['m2mTable'], $relation['m2mThisField'], $this->owner->{$this->owner->tableSchema->primaryKey}
            );
        }
    }
#8115 report it
Mukke at 2012/05/11 09:19am
Class name and autoload

You should change the classname to CAdvancedArBehavior instead of CAdvancedArbehavior for the yii convention

also you can just save the file in components and in you models add this:

public function behaviors(){
        return array(
            'CSaveRelationsBehavior' => array(
                'class' => 'application.components.CAdvancedArBehavior'
            )
    );
    }

no need to load it in the main config

#7796 report it
Pablovp at 2012/04/19 04:42am
Awesome

Nice extension @thyseus, really helpful. Thanks also to @tetele for the tips, you should consider to update it. Cheers, Pablo.

#6923 report it
tetele at 2012/02/13 09:22pm
Bug report

I have 2 small bug reports:

  1. a typo: line 142 in v0.3 should read primaryKey instead of PrimaryKey (notice the capitalization)

  2. line 165 in v0.3 should look like this:

$foreignobject = $foreignobject->{$foreignobject->tableSchema->primaryKey};

The reason is that not always the field in the m2m table is the same as the field in the object table. For instance, I name all of my PKs simply id. So I have $post->id and $category->id, but the m2m table has post_id and comment_id fields

#6548 report it
thaddeusmt at 2012/01/17 02:21pm
Error saving MANY_MANY Objects where the $foreignobject primary key name != m2mForeignField

It currently fails when I have the following table setup:

  1. Table: Item1 PrimaryKey: id
  2. Table: Item2 PrimaryKey: id
  3. Relation Table: Item1_Item2 Foreign Keys: item1_id - item2_id

This is the trouble spot in writeRelation():

foreach((array)$this->owner->$key as $foreignobject)
    {
      if(!is_numeric($foreignobject) && is_object($foreignobject))
        $foreignobject = $foreignobject->{$foreignobject->$relation['m2mForeignField']};
      $this->execute($this->makeManyManyInsertCommand($relation, $foreignobject));
    }

It works when I change it to this:

foreach((array)$this->owner->$key as $foreignobject)
    {
      if(!is_numeric($foreignobject) && is_object($foreignobject)) {
        $pk = $foreignobject->tableSchema->primaryKey; // get the primary key name
        $foreignobject = $foreignobject->{$pk}; // NOW get the primary key
      }
      $this->execute($this->makeManyManyInsertCommand($relation, $foreignobject));
    }

This should let you have the foreign key column names in the relation table be different from the primary key in the main tables.

#4177 report it
Pentium10 at 2011/06/13 12:38pm
key must be checked against array

In version 0.4 line 180

if(isset($this->owner->$key)) foreach($this->owner->$key as $foreignobject)

the foreach fails if the key is set and it is empty string.

The documentation of http://www.yiiframework.com/doc/api/1.1/CHtml#activeCheckBoxList-detail says In case no selection is made, the corresponding POST value is an empty string.

Please fix by adding is_array check too. Thank you. And please post the updated 0.4 version here too.

#4161 report it
klod at 2011/06/10 10:09am
checkboxList will be fixed in 1.1.8
#4160 report it
klod at 2011/06/10 09:35am
@waterloomatt

same issue as waterloomatt. I have just added "if(!empty($foreignobject))" on line 166 of CAdvencedArBehavior and it works like a charm.

original code

$this->execute(
    $this->makeManyManyInsertCommand($relation, $foreignobject));

new code

if(!empty($foreignobject))$this->execute(
    $this->makeManyManyInsertCommand($relation, $foreignobject));
#4044 report it
nguyendh at 2011/05/31 11:59am
Saving extra columns in the middle table ?

I have a Mortgage application where A Mortgage can have many applicants and a applicant can have many mortgages. I also want to know who is the primary applicant of the mortgage.

So the middle tbl needs to have mortgage_id , person_id, primary_applicant (a flag).

How do I use this extension to save the primary_applicant flag ?

#3003 report it
waterloomatt at 2011/03/07 11:10am
checkBoxList empty after validation fails

Found a clean-ish solution to the "checkBoxList forgetting posted values" problem. See this post for details.

#2986 report it
thyseus at 2011/03/05 04:41am
Solution

a solution would be to use the Relation Widget, that is tested to work good with CAdvancedArBehavior:

extension: http://www.yiiframework.com/extension/relation/

svn: http://code.google.com/p/yii-user-management/source/browse/trunk/user/components/Relation.php

#2985 report it
waterloomatt at 2011/03/05 02:40am
checkBoxList empty after validation fails

Hi,

Nice extension. The problem: The checkBoxList is empty after validation fails on the parent model. The question: Has anyone found a solution to re-populating the checkBoxList after validation fails?

My view:

<div class="row oneLineLabel">
        <?php echo $form->labelEx($model, 'services'); ?>
        <?php echo $form->checkBoxList($model, 'services',
            CHtml::listData(Service::model()->findAll(), 'id', 'name'),
            array('attributeitem' => 'id', 'checkAll' => 'Check All')); ?>
        <?php echo $form->error($model, 'services'); ?>
    </div>
#2590 report it
Shalabh Vyas at 2011/01/20 04:21am
Unable to save relations

I did the following but am not able to save() many-many relationships.

  1. Imported the CAdvancedArBehavior to extensions folder.
  2. Added 'application.extensions.CAdvancedArBehavior' to 'import' in config/main.php
  3. Added the 'behaviors()' method as specified by you in 'Watchlist' and 'Stock' models in my app.
  4. Tried the following but got an error:
//Initialize newStock
//Initialize new Watchlist
 
//Add stock to the watchlist
$newWatchList->stocks=array($newStock->id);
//Save the watchlist        
$this->assertTrue($newWatchlist->save());
//Access the stock using watchlist object
$this->assertEquals($newWatchlist->stocks[0]->symbol,$newSymbol);

The last statement throws an error saying 'Undefined offset: 0'. The relations are defined as:
Watchlist model:

'stocks' => array(self::MANY_MANY, 'Stock', 'tbl_watchlist_stock(watchlist_id, stock_id)'),

Stock model:

'watchlists' => array(self::MANY_MANY, 'Watchlist', 'tbl_watchlist_stock(stock_id, watchlist_id)'),
#51 report it
pappleton at 2010/10/03 05:56pm
Very nice

Great extension, but can you tell me if it updates unchanged records. I imagine a that a loop to check this before going to the DB would be a great saving. I had a look through the code and could not see such a loop but there is a very good chance I have missed it. Top marks though.

#297 report it
JoeCoT at 2010/07/15 11:44am
Works great!

Seems to work great, only problem I have is that it doesn't seem to work with $model->attributes assignment.

The relation I have is $model->extensions.

If I do this (which is the default) when updating:

$model->attributes=$_POST['Foo'];

$model->extensions isn't updated, even though $_POST['Foo']['extensions'] exists and is an array. I ended up doing this:

if(isset($_POST['Foo']['extensions'])){
        $model->extensions = $_POST['Foo']['extensions'];
        unset($_POST['Foo']['extensions']);
}
else $model->extensions = Array();

Also had to update lines 109 and 114 per rafa.informatica's review, in order to get it to remove all if the array is empty

Leave a comment

Please to leave your comment.

Create extension