Sauvegarder des données de relations HasAndBelongsToMany (HABTM) dans CakePHP

Enregistrer les données d'une relation HasAndBelongsToMany (HABTM) dans CakePHP n'est pas la partie la plus facile de l'utilisation de ce framework. Il suffit de regarder le nombre de questions associées dans Stack Overflow ou dans les forums spécialisés. La difficulté principale consiste dans le fait que le format de données HABTM n'est pas le même si vous voulez associer des données existantes (ce qui mettra à jour les entrées de la table jointe), ou alors créer de nouvelles données et les associer au passage (créer des entrées dans les tables des modèles ainsi que dans la table jointe).

De plus, ces deux formats sont différents du format de données renvoyé par un find() qui, pour les autres associations, peut être utilisé tel quel pour enregistrer des données avec un save().

Il faut donc être très attentif de choisir le bon format de données en fonction de l'utilisation que vous prévoyez.

1. Associer des données existantes

Dans ce cas, vous avez souvent juste besoin d'un input de type select avec l'attribut multiple. Quand vous générez vos vues en suivant le standard de CakePHP, vous obtenez un input simple avec l'alias de la relation HABTM et CakePHP crée de manière automagique un select avec le bon format. Pratique !

Pour sauvegarder ce genre de données, un simple save() suffit, pas besoin de saveAll().

Imaginez que vous avez la relation: Foo hasAndBelongsToMany Bar. Un select multiple automagique va renvoyer ce genre de données au serveur lorsque vous associez une instance existante de Foo avec plusieurs instances existantes de Bar :

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

2. Associer des nouvelles données

Le CookBook nous indique que pour associer des instances existantes de Foo avec de nouvelles instances de Bar, il faut utiliser un autre format, qui dépend en plus du fait qu'on veuille enregistrer une association avec un save() ou plusieurs associations avec un saveAll(). Avec un save(), le tableau de données aura deux clés (une pour chaque modèle de l'association, avec un saveAll(), votre tableau de données devra avoir une clé pour chaque association créée. Ce n'est toutefois pas la partie la plus compliquée.

Rappel: saveAll() est seulement un wrapper autour des méthodes saveMany() et saveAssociated()

Avec la même relation Foo hasAndBelongsToMany Bar, si l'on veut associer plusieurs nouvelles instances de Bar avec des nouvelles instances de Foo, il faudra formater les données de la manière suivante : 

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

3. Ajoutez un petit bout de code et utilisez le même format dans toute votre application

Quand vous sortez des standards CakePHP et des vues générées automatiquement et commencez à sauvegarder des données personnalisées (en utilisant JavaScript par exemple), vous devez alors construire à la main les données que vous envoyez aux contrôleurs et vous avez alors le contrôle sur le format des données. Vous devez alors être sur que les données et les associations sont sauvegardées dans tous les cas. Le moyen le plus simple est de choisir un format et de toujours contruire vos données de la même manière en laissant les modèles gérer les données (ce qui est leur travail après tout).

Le format de données renvoyé par un find() est le plus simple à utiliser: vous y avez accès facilement dans vos vues (DebugKit FTW!), et il est plutôt logique d'utiliser le même format de données dans un find() et dans un save(). Vous avez un index pour le modèle principal sur lequel le find() est fait puis des index pour chaque modèle associé. Des modèles ayant plusieurs associations, comme à l'aide de hasMany ou hasAndBelongsToMany on alors des sous-tableaux pour chacune de ces associations. Construire ce genre de tableau pour des relations entre instances existantes (qui ont un id) ou instances nouvelles (qui n'ont pas d'id) est très 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
		)
	)
)

En formattant ainsi les données, à l'aide d'un simple morçeau de code, il est possible de surcharger la fonction saveAssociated() pour d'abord sauvegarder éventuellement chaque nouvelle instance de Bar: créer les nouvelles (celles sans id) et mettre à jour les existantes. Il faut ensuite appeler la fonction saveAssociated() de la librairie (il est souvent déconseillé de surcharger entièrement une fonction de la librairie) avec un tableau formatté comme il faut (celui de notre premier cas qui associe des instances existantes) pour mettre à jour les entrées de la table jointe. Vous n'avez qu'à copier ce code dans votre AppModel.php et le tour est joué !

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);
}
Cette page appartient aux catégories suivantes: actualités , CakePHP , Code.

Commentaires

Ajoutez un commentaire

5103
Petits fours servis