Yii 1.1: advancedrelationsbehavior

This behavior helps me to update and delete models with HAS_ONE, HAS_MANY and MANY_MANY relations.
19 followers

This behavior helps me to update and delete models with HAS_ONE, HAS_MANY and MANY_MANY relations.

Just give it a try)

Requirements

Yii 1.0.2 or above

Usage

To use this extension, just copy behavior class file to your /components/ directory, add this behavior to each model you would like to extend with new possibilities:

/**
 * This is the model class for table "blog_category".
 *
 * The followings are the available columns in table 'blog_category':
 * @property integer $id
 * @property string $parent_id
 * @property integer $sort
 */
class BlogCategory extends CActiveRecord
{
    ...
    public function behaviors()
    {
        return array(
            'AdvancedRelationsBehavior' => array(
                'class' => 'AdvancedRelationsBehavior',
                'relations' => array(
                    // HAS_MANY relations
                    'categories',
                    // MANY_MANY relations
                    'posts',
                ),
            ),
        );
    }
    ...
}
 
/**
 * This is the model class for table "blog_post_to_category".
 *
 * The followings are the available columns in table 'blog_post_to_category':
 * @property integer $post_id
 * @property integer $category_id
 * @property integer $sort
 */
class BlogPostToCategory extends CActiveRecord
{
    ...
}
 
/**
 * This is the model class for table "blog_post".
 *
 * The followings are the available columns in table 'blog_post':
 * @property integer $id
 */
class BlogPost extends CActiveRecord
{
    ...
    public function behaviors()
    {
        return array(
            'AdvancedRelationsBehavior' => array(
                'class' => 'AdvancedRelationsBehavior',
                'autoUpdate' => false,  // disable automatic update of related records
                'relations' => array(
                    // MANY_MANY relations
                    'categories',
                    'tags', // same structure as `blog_post_to_category`
                ),
            ),
        );
    }
    ...
}

After you've attached this behavior to models, you can use it. For example, to attach category model with PK 3 to categories with PK 1 and 2 now you can use code like this:

$model = BlogCategory::model()->findByPk(3);
$model->categories = array(1, 2);
$model->save();

Or to move all posts to category with PK 1:

$model = BlogCategory::model()->findByPk(1);
$model->posts = BlogPost::model()->findAll();
$model->save();

Also you can use manual update of relations. In the BlogPost model we disable auto-update, so we must call update method updateRelated() to update relations. For example, let's create BlogPost and attach it to all categories:

$model = new BlogPost;
$model->categories = BlogCategory::model()->findAll();
// update only `categories` relation, `tags` relation is ignored
if($model->save())
    $model->updateRelations('categories');

If sort order attribute was specified in the configuration, behavior will update it before save model. To disable this feature, just assign NULL as attribute name.

Advanced usage example

Note: saveWithRelated() and deleteWithRelated() methods added in version 1.0.5

/**
 * Task: build this categories tree, add "Welcome" post to each "Posts" sub-category
 *  - News
 *      - Business
 *          - Posts
 *      - Sci/Tech
 *          - Posts
 *      - Entertainment
 *          - Posts
 *      - Sports
 *          - Posts
 *      - Health
 *          - Posts
 */
 
// the root category model
$rootCategory = new BlogCategory;
$rootCategory->title = "News";
 
// we cannot make direct modifications to the CActiveRecord relations,
// so we need to make a temporary buffer for this
$categories = array();
 
foreach(array(
    'Business',
    'Sci/Tech',
    'Entertainment',
    'Sports',
    'Health'
    ) as $categoryTitle)
{
    $category = new BlogCategory;
    $category->title = $categoryTitle;
 
    // add sub-category
    $postsCategory = new BlogCategory;
    $postsCategory->title = "Posts";
 
    // add "Welcome" post to the "Posts"
    $post = new BlogPost;
    $post->title = "Welcome";
    $postsCategory->posts = $post;
 
    // save "Posts" category with "Welcome" post
    $postsCategory->saveWithRelated('posts');
 
    $category->categories = $postsCategory;
 
    $categories[] = $category;
}
 
$rootCategory->categories = $categories;
 
// save the categories tree
$rootCategory->saveWithRelated('categories');

Update

Version 1.0.9 saveWithRelated was fixed (update related models even if no changes in the owner model attributes)

Version 1.0.8 update doxygen comments for getRelations and updateRelated methods

Version 1.0.7 restore HAS_ONE relation data format after saving

Version 1.0.6 added support for both types of HAS_ONE and HAS_MANY relations

Version 1.0.5 added saveWithRelated() and deleteWithRelated() methods

Version 1.0.4 algorithm of updating HAS_MANY relations was fixed

Version 1.0.3 {{table}} parser RegExp fixed

Version 1.0.2 {{table}} parser RegExp updated

Version 1.0.1 HAS_MANY related records will be removed before saving.

Total 15 comments

#16180 report it
Alex Jay at 2014/01/26 07:35pm
re: doxygen

Thanks, Leffe! I've uploaded a fixed file.

Erasing all relations and adding them back on every update is not good for database load, so I decided that it is better to update only what really needs to be updated. I'm always trying minimize the possible load from my code.

#16179 report it
Leffe at 2014/01/26 06:23pm
doxygen

Thanks for the quick response on my feedback.

In your update, I noticed that you forgot a '*' on line 63 to connect the doxygen block. I don't know if this is still valid doxygen format or not may at least give a warning if anyone runs the doxygen program on your code.

I must add that I like your extension and the fact that it is possible to turn off automatic saving of relations. This allow me to make my code a bit more explicit. Also I like that you don't just erase all relations and add them back as the older similar extension does.

#16178 report it
Alex Jay at 2014/01/26 05:41pm
doxygen comments

Hi, Leffe! Thanks for your review! I've updated doxygen comments for these methods.

#16177 report it
Leffe at 2014/01/26 03:08pm
Minor points from reading your code

Reviewing the code, I found these minor points:

  • The doxygen comment for getRelations indicate that it will return HAS_MANY and MANY_MANY relations but the code also includes HAS_ONE.

  • An undocumented side effect of updateRelated on a MANY_MANY-relation is that if the attribute value is a non-array it will be converted to an array. Also HAS_ONE is forced to not be an array at the end of updateRelated even if it was an array when updateRelated was called.

  • For MANY_MANY, updateRelated will call save() on relations to new foreign models (models where $model->isNewRecord == true). This is good as often PK is an incrementing number assigned by the database, but it may cause trouble to some if they don't know about this. So perhaps add a note about this in the doxygen comment.

#5741 report it
Alex Jay at 2011/11/08 02:38pm
about deleteRelated method

Hi, nlac! Nice to see that AdvancedRelationsBehavior is used not only by me!))) Thanks for your idea, I've also thought about this before and found that if we add this behavior to models from relations this will be done automaticaly.

This will be done here:

if(!$model->delete())
    $success = false;

When we delete a particular model, all models from relations with behavior AdvancedRelationsBehavior will be handled automaticaly.

/*
Example data structure:
 
- category PK#1
  - category PK#2
    - blog posts
*/
BlogCategory::model()->findByPk(1)->delete();

So if we delete category PK#1, will be also deleted category PK#2 with all related blog posts.

#5656 report it
nlac at 2011/10/29 02:46am
recursive deleting

Hi me again:) i have an another idea for your extension: extending the deleteRelated() with a parameter "$recursive" so this function would be able to delete all records along the HAS_MANY relationship tree, not just the immediate childs. It is simple and cool, the code is:

line 172: public function deleteRelated($relations = null, $bRecursive = false)
 
right after line 203:
if ($bRecursive)
  $success &= $model->deleteRelated(null,true);
 
...

The function deleteWithRelated() can also be extend with that parameter as well.

#4047 report it
Alex Jay at 2011/05/31 07:26pm
fix and enhancement

I've fix an algorithm of updating HAS_MANY relations and added saveWithRelated() and deleteWithRelated() methods for better usage

#4031 report it
nlac at 2011/05/30 07:25pm
important addition to the previous line

sorry, i forgot to write to my previous line: when i tested, the "autoDelete" AdvancedRelationsBehavior property was set for the related models, the current updateRelated() method produces the data losing when that is set.

#4030 report it
nlac at 2011/05/30 07:16pm
data losing in updateRelated()

Hi alexjay, i tested your extension again (actually i decided to use it in my module, i like it). I'm not sure that the proper action is done by updateRelated() method in case of HAS_MANY relations.

If i'm correct, for that case the method does:
1. gathers the records by the $owner->$relationname data, makes an array of clones and sets the $owner->$relationname property to this clone array
2. deletes all records belonging the owner record from the db (the REAL related records, belonging to the owner by foreign keys)
3. saves the clone array to the db as new records

The problem with this approach is, the method does NOT clone the RELATED data when making the clones in step 1 - i mean the relation data of the cloned records! All the relation info will be lost in step 2, the clones will be saved in step 3 without ANY relation info, except the setting the proper PKs through which they relates to the owner.

I suggest not to make any clones in step 1, delete only the necessary related records in step 2 and just set the proper FKs in step 3.

Another small thing: in line 80, there's a condition if(is_integer($model)) ... That will fail if the given variable is a string eg. "2", better to use something like this if (!@$model->attributes) ...

#3956 report it
Alex Jay at 2011/05/23 02:39pm
RegExp fixed ( I want to believe ;) )

Thanks, nlac. Shame on me)

Current RegExp is:

if(preg_match('/^\s*[\{]*([^\}]+?)[\}]*\s*\(\s*([^,\s]+)\s*,\s*([^\)]+?)\s*\)\s*$/s', $relation[2], $m))
#3928 report it
nlac at 2011/05/22 07:56pm
regexp still not ok

Hi alexjay, the regexp is buggy (the first group actually - it includes the two closing }} ), i think the following will be correct:

if(preg_match('/^\s*[\{]*([^}]+)[\}]*\((.+)\s*,\s*(.+)\)\s*$/s', $relation[2], $m))

Nice extension!

#3897 report it
Alex Jay at 2011/05/18 01:26pm
RegExp fixed

Thanks for your notice, RegExp fixed.

New expression:

if(preg_match('/^\s*[\{]*(.+)[\}]*\((.+)\s*,\s*(.+)\)\s*$/s', $relation[2], $m))
#3890 report it
boydzethuong at 2011/05/18 03:31am
active records with prefixed table names

when we use prefix (e.g. 'tbl_'), our relations will look like {{table_name}}(primary_key, foreign_key) and the expression below will go wrong.

preg_match('/^(.+)\((.+)\s*,\s*(.+)\)$/s', $relation[2], $m)
#3127 report it
Alex Jay at 2011/03/18 01:50pm
fixed

Thanks for you notice, fixed

#3123 report it
Dudie Rirkx at 2011/03/17 06:53pm
inconsistencies

There are a few numeric inconsistencies in the article. Please check the ID numbers for clarity. An example should be perfect and errorless.

Leave a comment

Please to leave your comment.

Create extension