1 November 2012

Membuat Form Master Detail dengan CakePHP

follow article
 

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:

contoh form master detail

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.

List Master Detail

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 :

form add master detail

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.

mengisi form add master detail

Setelah data diisi, kita tekan tombol submit, tampak data baru berhasil dimasukkan seperti terlihat pada gambar di bawah ini.

List Master detail, record baru telah ditambahkan

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:

view master detail

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.

form edit master detail

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:

Perubahan data pada form edit master detail

Kemudian tekan tombol submit, tampak halaman akan diredirect ke halaman view dengan data berubah sesuai dengan data yang diedit.

halaman view master detail setelah dilakukan perubahan data

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.

list master detail setelah penghapusan data dilakukan

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.....!

follow article


Ada 3 komentar untuk artikel ini

mantap... salam opensource bro
 
isul
from INDONESIA
on Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:11.0) Gecko/20100101 Firefox/11.0
Isoul, makasih udah mampir ya.
 
Rijal Asep Nugroho
from INDONESIA
on Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.89 Safari/537.1
Pak Rijal, kok gak ada update tutorial lagi?
Saya lg mentok di form master detail tapi menggunakan date di detailnya
Thanks tutornya
 
Imam
from (Unknown Country?)
on Mozilla/5.0 (Windows NT 6.1; rv:21.0) Gecko/20100101 Firefox/21.0
 

Leave Comment

<i>, <u>, <b> and <a>

Captcha


Reload image