One question that comes up time and again on the Google Group is “How do I make a single form for more than one record?”, which I call multi-record forms. Multi-record forms have a repeated section in the form for multiple entries of the same data. The example that I am going to use is an Author with many Books.
In CakePHP this technique will be most commonly used with hasMany associations. Note that this technique is not specific to CakePHP but the code and conventions used are.
Building the Form
Our example form consists of an Authors name, date of birth and up to 5 book records with title, year of publication and publisher.
Here is the view.
<form action="<?= $html->url('/authors/add'); ?>" method="post">
<fieldset>
<legend>Author Details</legend>
<?= $form->input('Author.name'); ?>
<?= $form->input('Author.dob', array('type' => 'date')); ?>
</fieldset>
<fieldset>
<legend>Books Published</legend>
<?php for ($i=0; $i<5; $i++) : ?>
<fieldset>
<legend>Book<?= $i; ?></legend>
<label for="BookTitle<?= $i; ?>">Title</label> <input type="text" name="data[Book][<?= $i; ?>][title]" id="BookTitle<?= $i; ?>" />
<?= $form->error('Book.'.$i.'.title'); ?><br />
<label for="BookPublication<?= $i; ?>">Publication Year</label> <input type="text" name="data[Book][<?= $i; ?>][publication]" id="BookPublication<?= $i; ?>" />
<?= $form->error('Book.'.$i.'.publication'); ?><br />
<label for="BookPublisher<?= $i; ?>">Publisher</label> <input type="text" name="data[Book][<?= $i; ?>][publisher]" id="BookPublisher<?= $i; ?>" />
<?= $form->error('Book.'.$i.'.publisher'); ?>
</fieldset>
<?php endfor; ?>
</fieldset>
</form>
The first fieldset is the Author details and is a constructed like a regular form. The Books Published fieldset contains 5 sub-fieldsets that are created by a loop.
Each sub-fieldset contains inputs with names in the format:
name="data[ModelName][index][fieldName]"
N.B. Notice that there are no quotes surrounding the text in the square brackets.
Using existing Data
For an edit page the majority of the view would be the same. The loop would change to:
<?php for ($i=0; $i<count($this->data['Book']); $i++) : ?>
<fieldset>
<legend>Book<?= $i; ?></legend>
<input type="hidden" name="data[Book][<?= $i; ?>][id]" id="BookId<?= $i; ?>" value="<?= $this->data['Book'][$i]['id']; ?>" />
<label for="BookTitle<?= $i; ?>">Title</label> <input type="text" name="data[Book][<?= $i; ?>][title]" id="BookTitle<?= $i; ?>" value="<?= $this->data['Book'][$i]['title']; ?>" />
<?= $form->error('Book.'.$i.'.title'); ?><br />
<label for="BookPublication<?= $i; ?>">Publication Year</label> <input type="text" name="data[Book][<?= $i; ?>][publication]" id="BookPublication<?= $i; ?>" value="<?= $this->data['Book'][$i]['publication']; ?>" />
<?= $form->error('Book.'.$i.'.publication'); ?><br />
<label for="BookPublisher<?= $i; ?>">Publisher</label> <input type="text" name="data[Book][<?= $i; ?>][publisher]" id="BookPublisher<?= $i; ?>" value="<?= $this->data['Book'][$i]['publisher']; ?>" />
<?= $form->error('Book.'.$i.'.publisher'); ?>
</fieldset>
<?php endfor; ?>
We simply changed the loop to terminate when all the books have been looped over, added a value attribute to the inputs and added an hidden id field so that our records get updated correctly.
Saving the Submitted Form
To save the submitted form we loop over each index in the Books array and save it to the database with the authors id.
It is important that Model::create() gets called during each iteration otherwise you will end up inserting on the first iteration and then updating each subsequent iteration.
By using Model::set() we can take advantage of validation rules in the model. However, any validation error messages wont be associated with the correct Book fields. To get around this we manually invoke Model::validates() and if this fails we reassign the invalid fields to a temporary array. Then if there are any errors we feed this temporary array back to the model so that the view has the correct data.
function add(){
if (!empty($this->data)) {
if ($this->Author->save()) {
// grab the id of the Author we just saved
$author_id = $this->Author-id;
$invalidBookFields = array();
foreach($this->data['Book'] as $index => $book) {
$book = array('Book' => $book);
$book['Book']['author_id'] = $author_id;
// clear any previous Book data
$this->Author->Book->create();
// We set the Book data this way so that validation is processed correctly
$this->Author->Book->set($book);
if (!$this->Author->Book->validates()) {
// save the validationErrors and reset for the next iteration
$invalidBookFields[$index] = $this->Author->Book->validationErrors;
unset($this->Author->Book->validationErrors);
} else {
$this->Author->Book->save();
}
}
if (empty($invalidBookFields)) {
// success - set message or redirect
} else {
// put all the errors back in the model so they make it back to the view
$this->Author->Book->validationErrors = $invalidBookFields;
}
}
}
}
Enhancements
If you need to make sure that all Books are valid before saving any of them you will need to loop the Books array twice. The first loop should validate each Book, then if there are no errors the second loop saves each Book.
If you do not know before hand how many items you need on your form you can use javascript or AJAX to dynamically add form elements as you need them.
Recent Comments