Working with HABTM Form Data in CakePHP

Posted by Mr PHP on

I would like to document several speedy ways I have of working with HABTM data.

The Tables

CREATE TABLE `posts` (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(255) NOT NULL,
  `body` text NOT NULL,
  PRIMARY KEY  (`id`)
);
CREATE TABLE `tags` (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(255) NOT NULL,
  PRIMARY KEY  (`id`)
);
CREATE TABLE `posts_to_tags` (
  `post_id` int(11) NOT NULL,
  `tag_id` int(11) NOT NULL,
  PRIMARY KEY  (`post_id`,`tag_id`)
);

The Models

Before we get started, lets setup the models.

models/post.php

<?php
class Post extends AppModel {
	var $name = 'Post';
	var $hasAndBelongsToMany = array(
		'Tag' => array(
			'className' => 'Tag',
			'joinTable' => 'posts_to_tags',
			'foreignKey' => 'post_id',
			'associationForeignKey' => 'tag_id',
			'with' => 'PostToTag',
		),
	);	
}

models/tag.php

<?php
class Tag extends AppModel {
	var $name = 'Tag';
}

HABTM Select

This is the default CakePHP way of handling HABTM forms.

It will allow you to add or remove HABTM data using a multiple select box (by holding CTRL).

habtm-select

views/posts/form.ctp

<?php echo $form->create('Post',array('url'=>array('action'=>'form')));?>
	<fieldset>
		<legend><?php __('Post Details');?></legend>
		<?php
		echo $form->input('id');
		echo $form->input('name');
		echo $form->input('body');
		echo $form->input('Tag.Tag');
		?>
	</fieldset>
<?php echo $form->end('Submit');?>

controllers/posts_controller.php

<?php 
class PostsController extends AppController {
	var $name = 'Posts';
	
	function form($id = null) {
		if (!empty($this->data)) {
			// form sends data as:
			// $this->data['Post']['Tag']['Tag'][1] = 1;
			// $this->data['Post']['Tag']['Tag'][3] = 3;
			
			// save the data (auto-handles habtm save)
			$this->Post->create();
			if ($this->Post->save($this->data)) {
				$this->Session->setFlash(__('The Post has been saved.',true));
				$this->redirect(array('action'=>'form',$this->Post->id));
			}
			else {
				$this->Session->setFlash(__('The Post could not be saved. Please, try again.'),true);
			}
		}
		if (empty($this->data)) {
			$this->data = $this->Post->read(null, $id);
		}
		$tags = $this->Post->Tag->find('list',array('fields'=>array('id','name')));
		$this->set(compact('tags'));
	}
}

HABTM Checkbox

If you have too many options to list in a select box, or if the labels in the select options do not suit your needs then you may want to try HABTM Checkbox.

It will allow you to add or remove HABTM data using a checkbox for each HABTM item.

habtm-checkbox

views/posts/form.ctp

<?php echo $form->create('Post',array('url'=>array('action'=>'form')));?>
	<fieldset>
		<legend><?php __('Post Details');?></legend>
		<?php
		echo $form->input('id');
		echo $form->input('name');
		echo $form->input('body');
		?>
	</fieldset>
	<fieldset>
		<legend><?php __('Tags');?></legend>
		<?php

		// output all the checkboxes at once
		echo $form->input('Tag',array(
			'label' => __('Tags',true),
			'type' => 'select',
			'multiple' => 'checkbox',
			'options' => $tags,
			'selected' => $html->value('Tag.Tag'),
		)); 

		/*
		// output all the checkboxes individually
		$checked = $form->value('Tag.Tag');
		echo $form->label(__('Tags',true));
		foreach ($tags as $id=>$label) {
			echo $form->input("Tag.checkbox.$id", array(
				'label'=>$label,
				'type'=>'checkbox',
				'checked'=>(isset($checked[$id])?'checked':false),
			));
		}
		*/
		?>
	</fieldset>
<?php echo $form->end('Submit');?>

controllers/posts_controller.php

<?php
class PostsController extends AppController {
	var $name = 'Posts';

	function form($id = null) {
		if (!empty($this->data)) {

			/*
			// get the tags from the single checkbox data
			$this->data['Tag']['Tag'] = array();
			foreach($this->data['Tag']['checkbox'] as $k=>$v) {
				if ($v) $this->data['Tag']['Tag'][] = $k;
			}
			*/
			
			// save the data
			$this->Post->create();
			if ($this->Post->save($this->data)) {
				$this->Session->setFlash(__('The Post has been saved.', true));
				$this->redirect(array('action'=>'form',$this->Post->id));
			}
			else {
				$this->Session->setFlash(__('The Post could not be saved. Please, try again.', true));
			}
		}
		if (empty($this->data)) {
			$this->data = $this->Post->read(null, $id);
		}
		$tags = $this->Post->Tag->find('list',array('fields'=>array('id','name')));
		$this->set(compact('tags'));
	}
}

HABTM Text Add

If you want to allow users to enter tags that are added to the current tags.

It will allow you to add but not remove HABTM data using a text input with comma seperated values.

habtm-textadd

views/posts/form.ctp

<?php echo $form->create('Post',array('url'=>array('action'=>'form')));?>
	<fieldset>
 		<legend><?php __('Post Details');?></legend>
	<?php
		echo $form->input('id');
		echo $form->input('name');
		echo $form->input('body');
	?>
	</fieldset>
	<fieldset>
 		<legend><?php __('Tags');?></legend>
	<?php
		// display current tags
		$links = array();
		if ($post['Tag']) {
			foreach($post['Tag'] as $k=>$row) {
				$links[] = $row['name'];
			}
		}
		echo '<div>';
		echo __('Current tags',true).':<br/>';
		echo implode(', ',$links);
		echo '</div>';
	    echo $form->input('Tag.tags',array(
			'type' => 'text',
			'label' => __('Add Tags',true),
			'after' => __('Seperate each tag with a comma.  Eg: family, sports, icecream',true)
		));
	?>
	</fieldset>
<?php echo $form->end('Submit');?>

controllers/posts_controller.php

<?php
class PostsController extends AppController {
	var $name = 'Posts';

	function form($id = null) {
		if (!empty($this->data)) {

			// get the tags from the text data
			if ($this->data['Tag']['tags']) {
				$this->data['Post']['id'] = $id;
				$tags = explode(',',$this->data['Tag']['tags']);
				foreach($tags as $_tag) {
					$_tag = strtolower(trim($_tag));
					if ($_tag) {
						// check if the tag exists
						$this->Post->Tag->recursive = -1;
						$tag = $this->Post->Tag->findByName($_tag);
						if (!$tag) {
							// create new tag
							$this->Post->Tag->create();
							$tag = $this->Post->Tag->save(array('name'=>$_tag));
							$tag['Tag']['id'] = $this->Post->Tag->id;
							if (!$tag) {
								$this->Session->setFlash(__(sprintf('The Tag %s could not be saved.',$_tag), true));
							}
						}
						if ($tag) {
							// use current tag
							$this->data['Tag']['Tag'][$tag['Tag']['id']] = $tag['Tag']['id'];
						}
					}
				}
			}

			// prevent the current tags from being deleted
			$this->Post->hasAndBelongsToMany['Tag']['unique'] = false;
			
			// save the data
			$this->Post->create();
			if ($this->Post->save($this->data)) {
				$this->Session->setFlash(__('The Post has been saved.', true));
				$this->redirect(array('action'=>'form',$this->Post->id));
			}
			else {
				$this->Session->setFlash(__('The Post could not be saved. Please, try again.', true));
			}
		}
		if (empty($this->data)) {
			$this->data = $this->Post->read(null, $id);
		}
		
		// get the posts current tags
		$post = $id ? $this->Post->find(array('Post.id'=>$id)) : false;
		
		$this->set(compact('post'));
	}
}

HABTM TextArea Edit

Finally, your admin may ask you if they can just edit all of the data as a comma separated list.

It will allow you to add and remove HABTM data using a textarea input with comma separated values.

habtm-textareaedit

views/posts/form.ctp

<?php echo $form->create('Post',array('url'=>array('action'=>'form')));?>
	<fieldset>
 		<legend><?php __('Post Details');?></legend>
		<?php
		echo $form->input('id');
		echo $form->input('name');
		echo $form->input('body');
		?>
	</fieldset>
	<fieldset>
 		<legend><?php __('Tags');?></legend>
	<?php
	    echo $form->input('Post.tags',array(
			'type' => 'textarea',
			'label' => __('Tags',true),
			'after' => __('Seperate each tag with a comma.  Eg: family, sports, icecream',true)
		));
	?>
	</fieldset>
<?php echo $form->end('Submit');?>

controllers/posts_controller.php

<?php
class PostsController extends AppController {
	var $name = 'Posts';

	function form($id = null) {
		if (!empty($this->data)) {

			// get the tags from the textarea data
			$tags = explode(',',$this->data['Post']['tags']);
			foreach($tags as $_tag) {
				$_tag = strtolower(trim($_tag));
				if ($_tag) {
					$this->Post->Tag->recursive = -1;
					$tag = $this->Post->Tag->findByName($_tag);
					if (!$tag) {
						$this->Post->Tag->create();
						$tag = $this->Post->Tag->save(array('name'=>$_tag));
						$tag['Tag']['id'] = $this->Post->Tag->id;
						if (!$tag) {
							$this->_flash(__(sprintf('The Tag %s could not be saved.',$_tag), true),'success');
						}
					}
					if ($tag) {
						$this->data['Tag']['Tag'][$tag['Tag']['id']] = $tag['Tag']['id'];
					}
				}
			}

			// save the data
			$this->Post->create();
			if ($this->Post->save($this->data)) {
				$this->Session->setFlash(__('The Post has been saved.', true));
				$this->redirect(array('action'=>'form',$this->Post->id));
			}
			else {
				$this->Session->setFlash(__('The Post could not be saved. Please, try again.', true));
			}
		}
		if (empty($this->data)) {
			$this->data = $this->Post->read(null, $id);
			// load the habtm data for the textarea
			$tags = array();
			if (isset($this->data['Tag']) && !empty($this->data['Tag'])) {
				foreach($this->data['Tag'] as $tag) {
					$tags[] = $tag['name'];
				}
				$this->data['Post']['tags'] = implode(', ',$tags);
			}
		}
		
		// get the posts current tags
		$post = $id ? $this->Post->find(array('Post.id'=>$id)) : false;
		
		$this->set(compact('post'));
	}
}

Conclusion

There are many great ways to work with HABTM data, these are just a few examples of what can be done. Enjoy your CakePHP.


Tagged with : CakePHP


Comments