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.
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.
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
very nice!!
very good job,
I change the lines 109 and 114 ... to delete the relationships when the user deselects all checkbox
109 - else //if (is_array($this->owner->$key)&& $this->owner->$key != array())
114 - foreach((array)$this->owner->$key as $foreignobject)
nice but...
the php file is corrupt.. all i see is garbage characters.. plz save in UTF 8 :P
Unable to save relations
I did the following but am not able to save() many-many relationships.
//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)'),
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>
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
checkBoxList empty after validation fails
Found a clean-ish solution to the "checkBoxList forgetting posted values" problem. See this post for details.
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 ?
@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));
checkboxList will be fixed in 1.1.8
to fix it now refer to http://code.google.com/p/yii/issues/detail?id=2412
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.
Error saving MANY_MANY Objects where the $foreignobject primary key name != m2mForeignField
It currently fails when I have the following table setup:
Item1
PrimaryKey:id
Item2
PrimaryKey:id
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.
Bug report
I have 2 small bug reports:
a typo: line 142 in v0.3 should read
primaryKey
instead ofPrimaryKey
(notice the capitalization)line 165 in v0.3 should look like this:
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 haspost_id
andcomment_id
fieldsAwesome
Nice extension @thyseus, really helpful. Thanks also to @tetele for the tips, you should consider to update it. Cheers, Pablo.
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
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} ); } }
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
Great work!
Couldn't live without this extension - at least for Yii 1.x Thanks!
GREAT EXTENSION
Hi, I just want to thank you for this great extension:
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
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
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; } } } }
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.