1 November 2012
Membuat Form Master Detail dengan CakePHP
Jika kita membuat aplikasi enterprise, pasti bertemu dengan bentuk form master detail. Artikel berikut menjelaskan langkah demi langkah pembuatan modul form master detail dalam bentuk yang sederhana.
Bentuk form-nya seperti ini:
Form bagian atas adalah form master, berisi field-field yang ada pada tabel masters. Dalam contoh ini, hanya 1 (satu) field yang harus diisi oleh user, yaitu field name. Adapun form di bagian bawah adalah form detail. Jumlah record dalam form detail tidak dibatasi, karena itu, penambahan record baru dilakukan on the fly melalui javascript. Tombol [+] jika diklik akan menambahkan baris baru. Jika ingin menghapus baris yang tidak dipakai, tekan tombol [-] pada baris yang hendak dihapus.
Baiklah kita langsung mulai, pertama kita siapkan database terlebih dahulu:
CREATE DATABASE `master_details`; USE `master_details`; CREATE TABLE `masters` ( `id` char(36) NOT NULL, `name` varchar(45) NOT NULL, `created` datetime DEFAULT NULL, `modified` datetime DEFAULT NULL, PRIMARY KEY (`id`) ); CREATE TABLE `details` ( `id` char(36) NOT NULL, `master_id` char(36) NOT NULL, `name` varchar(45) NOT NULL, `qty` int(10) unsigned NOT NULL, PRIMARY KEY (`id`) );
Kemudian buat aplikasi baru dengan perintah bake, misalkan aplikasi tersebut dinamakan 'asal'. Jika Anda kesulitan membuat aplikasi baru, silahkan buka artikel tentang pembuatan aplikasi baru melalui bake.
Jika aplikasi sudah dibuat, kita melanjutkan untuk membuat model melalui perintah bake model all
. Perintah ini akan membuat seluruh model dari tabel yang ada di database secara otomatis, termasuk relasi-relasi antar tabel jika Anda membuat tabel (berikut field-field-nya) sesuai dengan konvensi yang telah disepakati dalam CakePHP.
php -q /var/lib/cakephp2.x/lib/Cake/Console/cake.php -app /home/jackyhtg/Dropbox/www/asal bake model all
Untuk pembuatan controller, kita cukup membuat controller untuk model master, karena controller master ini akan dimodifikasi menjadi form master detail. Perintah yang digunakan tetap perintah bake.
php -q /var/lib/cakephp2.x/lib/Cake/Console/cake.php -app /home/jackyhtg/Dropbox/www/asal bake Welcome to CakePHP v2.2.2 Console --------------------------------------------------------------- App : asal Path: /home/jackyhtg/Dropbox/www/asal/ --------------------------------------------------------------- Interactive Bake Shell --------------------------------------------------------------- [D]atabase Configuration [M]odel [V]iew [C]ontroller [P]roject [F]ixture [T]est case [Q]uit What would you like to Bake? (D/M/V/C/P/F/T/Q) > c --------------------------------------------------------------- Bake Controller Path: /home/jackyhtg/Dropbox/www/asal/Controller/ --------------------------------------------------------------- Possible Controllers based on your current database: --------------------------------------------------------------- 1. Details 2. Masters Enter a number from the list above, type in the name of another controller, or 'q' to exit [q] > 2 --------------------------------------------------------------- Baking MastersController --------------------------------------------------------------- Would you like to build your controller interactively? (y/n) [y] > Would you like to use dynamic scaffolding? (y/n) [n] > Would you like to create some basic class methods (index(), add(), view(), edit())? (y/n) [n] > y Would you like to create the basic class methods for admin routing? (y/n) [n] > Would you like this controller to use other helpers besides HtmlHelper and FormHelper? (y/n) [n] > Would you like this controller to use any components? (y/n) [n] > Would you like to use Session flash messages? (y/n) [y] > --------------------------------------------------------------- The following controller will be created: --------------------------------------------------------------- Controller Name: Masters --------------------------------------------------------------- Look okay? (y/n) [y] > Baking controller class for Masters... Creating file /home/jackyhtg/Dropbox/www/asal/Controller/MastersController.php Wrote `/home/jackyhtg/Dropbox/www/asal/Controller/MastersController.php` PHPUnit is not installed. Do you want to bake unit test files anyway? (y/n) [y] > n
Langkah selanjutnya adalah membuat view melalui perintah bake. Seperti controller, view yang dibuat juga hanya untuk model master, yang akan dimodifikasi pada halaman add dan edit menjadi bentuk form master detail.
Masih di console interactive Bake Shell, buatlah view melalui perintah bake dengan menekan huruf v seperti contoh di bawah ini :
--------------------------------------------------------------- Interactive Bake Shell --------------------------------------------------------------- [D]atabase Configuration [M]odel [V]iew [C]ontroller [P]roject [F]ixture [T]est case [Q]uit What would you like to Bake? (D/M/V/C/P/F/T/Q) > v --------------------------------------------------------------- Bake View Path: /home/jackyhtg/Dropbox/www/asal/View/ --------------------------------------------------------------- Possible Controllers based on your current database: --------------------------------------------------------------- 1. Details 2. Masters Enter a number from the list above, type in the name of another controller, or 'q' to exit [q] > 2 Would you like bake to build your views interactively? Warning: Choosing no will overwrite Masters views if it exist. (y/n) [n] > y Would you like to create some CRUD views (index, add, view, edit) for this controller? NOTE: Before doing so, you'll need to create your controller and model classes (including associated models). (y/n) [y] > y Would you like to create the views for admin routing? (y/n) [n] > Baking `index` view file... Creating file /home/jackyhtg/Dropbox/www/asal/View/Masters/index.ctp Wrote `/home/jackyhtg/Dropbox/www/asal/View/Masters/index.ctp` Baking `view` view file... Creating file /home/jackyhtg/Dropbox/www/asal/View/Masters/view.ctp Wrote `/home/jackyhtg/Dropbox/www/asal/View/Masters/view.ctp` Baking `add` view file... Creating file /home/jackyhtg/Dropbox/www/asal/View/Masters/add.ctp Wrote `/home/jackyhtg/Dropbox/www/asal/View/Masters/add.ctp` Baking `edit` view file... Creating file /home/jackyhtg/Dropbox/www/asal/View/Masters/edit.ctp Wrote `/home/jackyhtg/Dropbox/www/asal/View/Masters/edit.ctp` --------------------------------------------------------------- View Scaffolding Complete.
Setelah semua kode sudah digenerate melalui perintah bake, sekarang tinggal sedikit melakukan modifikasi agar modul master dibuat dalam bentuk form master-detail.
Harus dipahami, bahwa dalam form master detail, satu form akan digunakan untuk operasi CRUD 2 (dua) tabel sekaligus, yaitu tabel masters dan tabel details. Untuk itu kita akan mengedit kode yang dihasilkan proses bake.
Pertama buka file APP/View/Masters/index.ctp, hilangkan link yang tidak diperlukan sehingga tampilan menjadi seperti gambar berikut.
Selanjutnya kita akan mengubah modul add. Tekan tombol New Master. Bagaimana tampilan yang Anda lihat? Kita akan mengubahnya menjadi bentuk master detail menjadi seperti ini :
Hal ini dilakukan dengan mengubah file App/View/Masters/add.ctp menjadi seperti berikut.
<?php echo $this->Html->script('jquery-1.7.2.min');?>
<div class="masters form">
<?php echo $this->Form->create('Master'); ?>
<fieldset>
<legend><?php echo __('Add Master'); ?></legend>
<?php
echo $this->Form->input('name');
?>
<!-- tambahan form untuk tabel detail -->
<table>
<tr>
<th>Name</th>
<th>Qty</th>
<th><a style="cursor:pointer;" onclick="clickAdd();"><span style="background:#aaa;padding:1px 7px;">+</span></a></th>
</tr>
<?php if ($this->request->data) :?>
<?php foreach ($this->request->data['Detail'] as $i=>$v) :?>
<tr id="tr<?php echo $i;?>">
<td><?php echo $this->Form->input('name', array('label'=>false, 'div'=>false, 'id'=>"Detail{$i}Name", 'name'=>"data[Detail][$i][name]", 'value'=>$v['name']));?></td>
<td><?php echo $this->Form->input('qty', array('label'=>false, 'div'=>false, 'id'=>"Detail{$i}Qty", 'name'=>"data[Detail][$i][qty]", 'value'=>$v['qty']));?></td>
<td><a style="cursor:pointer;" onclick="$('#tr<?php echo $i;?>').detach();"><span style="background:#aaa;padding:1px 8px;">-</span></a></td>
</tr>
<?php endforeach;?>
<?php else : ?>
<tr id="tr0">
<td><?php echo $this->Form->input('name', array('label'=>false, 'div'=>false, 'id'=>'Detail0Name', 'name'=>"data[Detail][0][name]"));?></td>
<td><?php echo $this->Form->input('qty', array('label'=>false, 'div'=>false, 'id'=>'Detail0Qty', 'name'=>"data[Detail][0][qty]"));?></td>
<td><a style="cursor:pointer;" onclick="$('#tr0').detach();"><span style="background:#aaa;padding:1px 8px;">-</span></a></td>
</tr>
<?php endif; ?>
<tr id="trHiddenCounterDetail" style="display:none;">
<input id="tr_d_counterDetail" type="hidden" value="<?php echo $this->request->data?(sizeof($this->request->data['Detail'])-1):0;?>">
</tr>
</table>
<!-- end tambahan form untuk tabel detail -->
</fieldset>
<?php echo $this->Form->end(__('Submit')); ?>
</div>
<div class="actions">
<h3><?php echo __('Actions'); ?></h3>
<ul>
<li><?php echo $this->Html->link(__('List Masters'), array('action' => 'index')); ?></li>
</ul>
</div>
<!-- Ini adalah duplikasi elemen row detail, yang akan dipakai dalam penambahan row baru melalui javascript -->
<div style="display:none;" id="htmlDetail">
<xzztr id="trzzz">
<xzztd><?php echo $this->Form->input('name', array('label'=>false, 'div'=>false, 'id'=>'DetailzzzName', 'name'=>"data[Detail][zzz][name]"));?></xzztd>
<xzztd><?php echo $this->Form->input('qty', array('label'=>false, 'div'=>false, 'id'=>'DetailzzzQty', 'name'=>"data[Detail][zzz][qty]"));?></xzztd>
<xzztd><a style="cursor:pointer;" onclick="$('#trzzz').detach();"><span style="background:#aaa;padding:1px 8px;">-</span></a></xzztd>
</xzztr>
</div>
<!-- script yang dipanggil dari form detail -->
<script type="text/javascript">
function clickAdd(){
html=$('#htmlDetail').html().toString();
var nextCounter=Number($('#tr_d_counterDetail').val())+1;
$('#tr_d_counterDetail').val(nextCounter);
while (html != (html = html.replace("zzz", nextCounter)));
while (html != (html = html.replace("xzz", '')));
$('#trHiddenCounterDetail').before(html);
}
</script>
Baris pertama pada script di atas berfungsi untuk meload file core jquery, jadi jangan lupa untuk menambahkan jquery di folder APP/webroot/js/
Kemudian kita sedikit ubah controller di fungsi add, pada perintah if ($this->Master->save($this->request->data))
, kita ganti menjadi if ($this->Master->saveAll($this->request->data)). Perintah saveAll() ini digunakan
agar menyimpan data master beserta seluruh relasinya.
Sekarang kita coba mengisi data ke form add tersebut, seperti pada gambar di bawah ini.
Setelah data diisi, kita tekan tombol submit, tampak data baru berhasil dimasukkan seperti terlihat pada gambar di bawah ini.
Selanjutnya kita akan masuk ke method View, dari halaman index kita tekan tombol view. Tampilan View dari kode yang dihasilkan perintah bake sudah sesuai dengan yang diinginkan. Hanya sedikit perubahan dengan menghilangkan link yang tidak diperlukan sehingga tampilan menjadi seperti ini:
Kemudian kita beranjak ke method edit. Tekan tombol edit. Apa yang Anda lihat? Kita akan mengubah tampilan ini menjadi bentuk form master detail. Method edit ini mirip dengan method add. Perbedaan utamanya adalah jika dalam fungsi add belum ada id (primary key), maka dalam fungsi edit harus diletakkan field id, agar dikenali sebagai proses update, bukan proses insert.
Untuk itu kita akan mengubah file APP/View/Masters/edit.ctp menjadi seperti ini :
<?php echo $this->Html->script('jquery-1.7.2.min');?>
<div class="masters form">
<?php echo $this->Form->create('Master'); ?>
<fieldset>
<legend><?php echo __('Edit Master'); ?></legend>
<?php
echo $this->Form->input('id');
echo $this->Form->input('name');
?>
<!-- tambahan form tabel detail -->
<table>
<tr>
<th>Name</th>
<th>Qty</th>
<th><a style="cursor:pointer;" onclick="clickAdd();"><span style="background:#aaa;padding:1px 7px;">+</span></a></th>
</tr>
<?php if ($this->request->data) :?>
<?php foreach ($this->request->data['Detail'] as $i=>$v) :?>
<tr id="tr<?php echo $i;?>">
<?php if (isset($v['id']) and !empty($v['id'])) :?>
<?php echo $this->Form->input('id', array('label'=>false, 'div'=>false, 'id'=>"Detail{$i}Id", 'name'=>"data[Detail][$i][id]", 'value'=>$v['id']));?>
<?php endif;?>
<td><?php echo $this->Form->input('name', array('label'=>false, 'div'=>false, 'id'=>"Detail{$i}Name", 'name'=>"data[Detail][$i][name]", 'value'=>$v['name']));?></td>
<td><?php echo $this->Form->input('qty', array('label'=>false, 'div'=>false, 'id'=>"Detail{$i}Qty", 'name'=>"data[Detail][$i][qty]", 'value'=>$v['qty']));?></td>
<td><a style="cursor:pointer;" onclick="$('#tr<?php echo $i;?>').detach();"><span style="background:#aaa;padding:1px 8px;">-</span></a></td>
</tr>
<?php endforeach;?>
<?php else : ?>
<tr id="tr0">
<td><?php echo $this->Form->input('name', array('label'=>false, 'div'=>false, 'id'=>'Detail0Name', 'name'=>"data[Detail][0][name]"));?></td>
<td><?php echo $this->Form->input('qty', array('label'=>false, 'div'=>false, 'id'=>'Detail0Qty', 'name'=>"data[Detail][0][qty]"));?></td>
<td><a style="cursor:pointer;" onclick="$('#tr0').detach();"><span style="background:#aaa;padding:1px 8px;">-</span></a></td>
</tr>
<?php endif; ?>
<tr id="trHiddenCounterDetail" style="display:none;">
<input id="tr_d_counterDetail" type="hidden" value="<?php echo $this->request->data?(sizeof($this->request->data['Detail'])-1):0;?>">
</tr>
</table>
<!-- end tambahan form tabel detail -->
</fieldset>
<?php echo $this->Form->end(__('Submit')); ?>
</div>
<div class="actions">
<h3><?php echo __('Actions'); ?></h3>
<ul>
<li><?php echo $this->Form->postLink(__('Delete'), array('action' => 'delete', $this->Form->value('Master.id')), null, __('Are you sure you want to delete # %s?', $this->Form->value('Master.id'))); ?></li>
<li><?php echo $this->Html->link(__('List Masters'), array('action' => 'index')); ?></li>
</ul>
</div>
<!-- Ini adalah duplikasi elemen row detail, yang akan dipakai dalam penambahan row baru melalui javascript -->
<div style="display:none;" id="htmlDetail">
<xzztr id="trzzz">
<xzztd><?php echo $this->Form->input('name', array('label'=>false, 'div'=>false, 'id'=>'DetailzzzName', 'name'=>"data[Detail][zzz][name]"));?></xzztd>
<xzztd><?php echo $this->Form->input('qty', array('label'=>false, 'div'=>false, 'id'=>'DetailzzzQty', 'name'=>"data[Detail][zzz][qty]"));?></xzztd>
<xzztd><a style="cursor:pointer;" onclick="$('#trzzz').detach();"><span style="background:#aaa;padding:1px 8px;">-</span></a></xzztd>
</xzztr>
</div>
<!-- script yang dipanggil dari form detail -->
<script type="text/javascript">
function clickAdd(){
html=$('#htmlDetail').html().toString();
var nextCounter=Number($('#tr_d_counterDetail').val())+1;
$('#tr_d_counterDetail').val(nextCounter);
while (html != (html = html.replace("zzz", nextCounter)));
while (html != (html = html.replace("xzz", '')));
$('#trHiddenCounterDetail').before(html);
}
</script>
Sekarang kita refresh halaman edit, sehingga tampilan form edit berubah menjadi seperti berikut.
Untuk menghandle setelah tombol submit diklik, kita akan merubah fungsi edit di controller. Perubahan yang dilakukan meliputi tiga hal, yaitu ubah method save() menjadi saveAll(), mendeteksi mana record detail yang mengalami operasi penghapusan, dan alihkan redirect ke halaman view setelah proses edit berhasil. Berikut adalah kode fungsi edit setelah mengalami perubahan.
public function edit($id = null) {
$this->Master->id = $id;
if (!$this->Master->exists()) {
throw new NotFoundException(__('Invalid master'));
}
if ($this->request->is('post') || $this->request->is('put')) {
// dapatkan record detail yang lama
$oldDetail=$this->Master->Detail->find('list', array('fields'=>array('Detail.id','Detail.id'), 'conditions'=>"Detail.master_id IN ('$id')"));
// dapatkan record detail yang masih ada
$existRecord=array();
foreach ($this->request->data['Detail'] as $v){
if (isset($v['id']) and !empty($v['id'])) $existRecord[]=$v['id'];
}
// dapatkan record yang dihapus melalui cara perbandingan antara oldDetail dengan existRecord
$deleteRecord=array_diff($oldDetail, $existRecord);
if ($this->Master->saveAll($this->request->data)) {
if (!empty($deleteRecord)) {
if ($this->Master->Detail->deleteAll("Detail.id IN ('".implode("','",$deleteRecord)."')")) {
$this->Session->setFlash(__('The master has been saved'));
}
else $this->Session->setFlash(__('The detail that not used could not be deleted'));
}
else $this->Session->setFlash(__('The master has been saved'));
$this->redirect(array('action' => 'view', $id));
} else {
$this->Session->setFlash(__('The master could not be saved. Please, try again.'));
}
} else {
$this->request->data = $this->Master->read(null, $id);
}
}
Setelah itu kembali ke halaman edit, coba lakukan perubahan data dengan menghapus satu record detail, dan menambahkan satu record detail, seperti pada gambar berikut:
Kemudian tekan tombol submit, tampak halaman akan diredirect ke halaman view dengan data berubah sesuai dengan data yang diedit.
Satu method terakhir yang perlu dilakukan sedikit modifikasi, yaitu method delete. Pada controller tidak perlu dilakukan perubahan apapun, namun agar penghapusan bersifat cascade, yaitu bila master dihapus maka detail juga ikut terhapus, kita perlu memastikan bahwa model master pada relasi hasMany/HABTM, nilai pada variabel dependent di-setting true.
public $hasMany = array( 'Detail' => array( 'className' => 'Detail', 'foreignKey' => 'master_id', 'dependent' => true, 'conditions' => '', 'fields' => '', 'order' => '', 'limit' => '', 'offset' => '', 'exclusive' => '', 'finderQuery' => '', 'counterQuery' => '' ) );
Sekarang coba tekan tombol delete, jika proses delete berhasil halaman akan direcirect ke halaman index.
Pembuatan CRUD modul master-detail telah selesai dilakukan. Bagaimana? Mudah bukan?
Pada contoh di atas, tabel dicreate dengan engine MYISAM yang tidak mendukung transaksi. Pada kenyataan-nya, aplikasi enterprise selalu membutuhkan integritas data, yang hanya bisa dilakukan dengan engine yang mendukung transaksi seperti engine INNODB. Jika Anda ingin menggunakan fitur transaksi, Anda perlu menambahkan transaksi di setiap script controller, seperti begin(), commit(), dan rollback(). Nah sekarang bagian Anda berkreasi menambahkan fitur transaksi. Selamat bekerja.....!
Ada 3 komentar untuk artikel ini
from INDONESIA
on Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:11.0) Gecko/20100101 Firefox/11.0
from INDONESIA
on Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.89 Safari/537.1
Saya lg mentok di form master detail tapi menggunakan date di detailnya
Thanks tutornya
from (Unknown Country?)
on Mozilla/5.0 (Windows NT 6.1; rv:21.0) Gecko/20100101 Firefox/21.0