Saving HasAndBelongsToMany (HABTM) data in CakePHP

Saving data from a HasAndBelongsToMany (HABTM) relationship with CakePHP is not the easiest part of using this framework. Just have a look at all the related questions in Stack Overflow or dedicated forums... The main difficulty is that the format of HABTM data is not the same wether you want to associate existing records together (only update entries in the join table), or create new records and also associate them (create new records in the models table and in the join table).

In addition, they are both different from the data format returned by a find() operation which, for all others associations, can be directly used as such with the save() method.

So you have to be extra careful to choose the right data format depending of which use you are considering.

1. Associating existing records

In this case, you usually only need a select input with the multiple attribute. When you bake your View files with the standard CakePHP template, you get a simple input with the alias of your HABTM relation and CakePHP automagically constructs the multiple select with the right format. Neat !

This format works with a simple save() in your main model, no need to use saveAll().

Imagine you have the relation: Foo hasAndBelongsToMany Bar. An automagic multiple select will send this data to the server when associating an existing record of Foo with several existing records of Bar: 

array(
	'Foo' => array(
		'id' => '...',
		...
	),
	'Bar' => array(
		'Bar' => array(
			[0] => 'id1', // id of an existing Bar
			[1] => 'id2', // id of another existing Bar
			...
		)
	)
)

2. Associating new records

The CookBook teaches that in order to associate existing records of Foo with new records of Bar, we have to use another format, which kinda depends on the fact that you use a save() or a saveAll(). With a save() your data array will have two keys (one for each model), with a saveAll() your data array will have a key for each relation you want to save. But that's not overly complicated.

Reminder: saveAll() is just a wrapper around the saveMany() and saveAssociated() methods.

If we have the relation: Foo hasAndBelongsToMany Bar, and we want to associate multiple new records of Bar with existing records of Foo, we must format the data that way:

array(
	[0] => array(
		[Foo] => array(
			[id] => ...
		),
		[Bar] => array(
			[name] => ...
		)
        ),
	[1] => array(
		[Foo] => array(
			[id] => ...
		),
		[Bar] => array(
			[name] => ...
                )
        )
)

3. Add a little snippet of code and always format the data in the same way

When you wander out of CakePHP standards and automatically baked views and start saving custom data (using JavaScript for example), then you have to manually construct the data you send to your controllers and you have complete control over its format. You thus might want to be sure that your records and association will be saved in the right way wether you create new records or not. The simplest way is to settle for a format and always construct your data in the same fashion and let models handle the data (that is their job afterall).

The data format returned by a find() is the simplest to use: you easily have access to it in your views (DebugKit FTW!), and it is quite logical to use the same data format for find() and save(). You have an index for the main model of the find() and then an index for each associated Model. Multiple associated models like hasMany or hasAndBelongsToMany have subsequent subarrays for each associated entry. Building this type of array with existing data (we have an id) or new data (there is no id yet) is very simple:

array(
	'Foo' => array(
		'id' => '...',
		...
	),
	'Bar' => array(
		(int) 0 => array(
			'id' => '...', // Existing Bar, we have its id
			'name' => '...' // The name may have been modified
		),
		(int) 1 => array(
			'name' => '...' // This Bar will be created
		)
	)
)

Then with a simple snippet of code, you can overload the saveAssociated() function to save each instance of Bar: create new ones (those without an id) and update existing ones. Then call the parent saveAssociated() function from the library (you rarely want to entirely overload functions from the library) with a nicely formated association array (the one from our first case) to update the entries in the join table. Copy the following code in your AppModel.php and the trick is done! 

public function saveAssociated($data = null, $options = array()) {
	foreach ($data as $alias => $modelData) {
		if (!empty($this->hasAndBelongsToMany[$alias])) {
			$habtm = array();
			$Model = ClassRegistry::init($this->hasAndBelongsToMany[$alias]['className']);
			foreach ($modelData as $modelDatum) {
				if (empty($modelDatum['id'])) {
					$Model->create();
				}
				$Model->save($modelDatum);
				$habtm[] = empty($modelDatum['id']) ? $Model->getInsertID() : $modelDatum['id'];					
			}
			$data[$alias] = array($alias => $habtm);
		}
	}
	return parent::saveAssociated($data, $options);
}
This page belongs to the following categories: news , CakePHP , Code.

Comments

tierrarara 18/10/2013 18:04:05

this example not work for me

i have tables
account - accounts_role - role
i try to associate the accounts with roles
$data = array (
array ( 'Account' => array ( 'account_id' => 1,
'firstname' => uniqid () ),
'Role' => array (
'Role' => array (
0 => array ('role_id' => 1 ),
1 => array ('role_id' => 2)
)
) )

Chris 23/10/2013 14:45:39

Hey, did you try to use the last code snippet or did you try to save the data with the CakePHP default saveAssociated() ?

Moreover your tables should be named accounts, accounts_roles and roles to follow the CakePHP conventions.

Skatch 17/06/2014 13:46:12

This is excellent. The best tutorial for HABTM relations and I use it every time I need something. Thanks!

Add a comment

3122
Petits fours baked