Memakai Validator Di Laravel

Laravel menyediakan sebuah validator sederhana yang diwakili oleh class Illuminate\Validation\Validator. Berbeda dengan Hibernate Validator dimana yang di-validasi adalah sebuah object, Illuminate\Validation\Validator hanya men-validasi array. Aturan validasi diberikan dalam bentuk string. Sebagai contoh, ini adalah kode program untuk melakukan validasi di Laravel:

Route::get('/test', function() {      
    $validator = Validator::make(
        ['email' => 'Solid Snake', 'password' => '12345'],
        ['email' => 'required|email', 'password' => 'required|min:5']);
    if ($validator->passes()) {
        $hasil = 'Data sudah benar.';
    } else {
        $hasil = 'Data salah.';
    }
    return $hasil;
});

Kode program di atas akan memanggil method make() dari Illuminate\Validation\Factory yang akan menghasilkan sebuah object Illuminate\Validation\Validator. Setelah itu, saya dapat memanggil method passes() atau fails() dari Illuminate\Validation\Validator yang akan mengembalikan sebuah nilai boolean.

Aturan validasi seperti email, required, min dan sebagainya didefinisikan sebagai method di Illuminate\Validation\Validator. Sebagai contoh, pada class tersebut, saya dapat menemukan method validateEmail(), validateRequired(), validateMin() dan sebagainya. Daftar aturan validasi yang lengkap beserta dokumentasinya dapat dijumpai di http://laravel.com/docs/validation.

Untuk mendapatkan pesan deskripsi mengenai kesalahan bila terdapat pelanggaran aturan validasi, saya dapat membuat kode program seperti:

Route::get('/test', function() {      
    $validator = Validator::make(
        ['email' => 'test.com', 'password' => '1234'],
        ['email' => 'email', 'password' => 'required|min:5']);
    if ($validator->passes()) {
        $hasil = 'Data sudah benar.';
    } else {
        $hasil = $validator->errors();       
    }
    return $hasil;
});

Kode program di atas akan menghasilkan output seperti:

{"email":["The email must be a valid email address."],"password":["The password must be at least 5 characters."]}

Method errors() dari Illuminate\Validation\Validator akan mengembalikan pesan kesalahan dalam bentuk instance dari class Illuminate\Support\MessageBag. Class tersebut mengimplementasikan interface JsonableInterface dan method __toString() nya telah di-override agar mengembalikan hasil dalam bentuk JSON. Bila tidak ingin memperoleh hasil dalam bentuk JSON, saya juga bisa melakukan enumerasi dengan memanggil method all() seperti pada contoh berikut ini:

$hasil = 'Daftar kesalahan:<br>' . implode($validator->errors()->all(), '<br>');

Untuk membuat pesan kesalahan yang berbeda, misalnya dalam bahasa Indonesia, saya dapat membuat kode program seperti berikut ini:

Route::get('/test', function() {      
    $validator = Validator::make(
        ['email' => 'test.com', 'password' => '1234'],
        ['email' => 'email', 'password' => 'required|min:5'],
        ['email' => 'Email Anda salah.', 
         'password.required' => 'Password harus diisi.', 
         'password.min' => 'Password minimal :min karakter.']);
    if ($validator->passes()) {
        $hasil = 'Data sudah benar.';
    } else {    
        $hasil = 'Daftar kesalahan:<br>' . implode($validator->errors()->all(), '<br>');          
    }
    return $hasil;
});

Kode program di atas akan mengembalikan hasil seperti:

Daftar kesalahan:
Email Anda salah.
Password minimal 5 karakter.

Memberikan pesan kesalahan secara manual setiap kali melakukan validasi adalah hal yang melelahkan. Oleh sebab itu, saya dapat melakukan perubahan secara global dengan mengubah nilai pesan kesalahan di file app/lang/en/validation.php.

Selain itu, saya juga bisa membuat pesan kesalahan yang berbeda sesuai dengan bahasa dengan membuat struktur folder seperti pada gambar berikut ini:

Struktur folder untuk i18n di Laravel

Struktur folder untuk i18n di Laravel

Untuk berpindah ke locale yang berbeda, saya dapat menggunakan perintah App::setLocale() seperti:

App::setLocale('id');

Laravel akan memakai pesan kesalahan di file validation.php pada folder yang memiliki kode yang sesuai dengan locale yang sedang aktif.

Iklan

Apa Manfaat Facade Di Laravel?

Salah satu pertanyaan yang paling sering ditanyakan oleh mereka yang baru mencoba Laravel adalah apa gunanya facade? Seperti yang diketahui, facade adalah sebuah fitur dari Laravel yang mengimplementasikan facade pattern (http://en.wikipedia.org/wiki/Facade_pattern). Pada facade pattern, terdapat sebuah class yang disebut facade class yang akan mengakses class lain. Class pengguna (client) tidak akan mengakses class yang di-‘facade’ secara langsung melainkan mengaksesnya melalui facade class. Tujuan dari pattern ini adalah untuk menyembunyikan kerumitan.

Pada Laravel, facade diimplementasikan dalam bentuk sebuah class dengan method static yang bila dipanggil akan memanggil method di object lain yang di-‘facade’ olehnya. Sebagai contoh facade Log::debug() akan memanggil method debug() dari sebuah objek Monolog\Logger di IoC Container. Dalam hal ini, facade dapat dianggap sebagai cara singkat dalam mengakses sebuah object di container.

Tapi facade memiliki peran penting lainnya yang berkaitan dengan unit testing (atau tepatnya integration testing atau functional testing). Untuk menunjukkannya, saya akan membuat sebuah functional test yang isinya seperti berikut ini:

<?php

class PelangganControllerTest extends TestCase {

    public function testTambahSukses() {
        // Mengirim semua data yang dibutuhkan untuk disimpan
        $response = $this->call('GET', 'pelanggan/tambah',
            ['email'=>'solid@snake.com', 'nama'=>'Solid Snake']);

        $this->assertJson($response->getContent());
        $this->assertEquals('solid@snake.com', $response->getData()->email);
    }

    public function testTambahGagal() {
        // Hanya mengirim 'nama' tanpa 'email'
        $response = $this->call('GET', 'pelanggan/tambah',
            ['nama'=>'Solid Snake']);        

        $this->assertJson($response->getContent());
        $this->assertEquals('Email tidak boleh kosong', $response->getData()->kesalahan);
    }

}

?>

Seperti biasa, bila saya menjalankan unit test di atas, saya akan memperoleh pesan kesalahan karena saya belum membuat controller yang bersangkutan. Untuk itu, saya segera membuat controller dengan nama PelangganController.php di direktori app/controllers yang isinya seperti berikut ini:

<?php

class PelangganController extends BaseController {

    public function getTambah()
    {       
        $email = Input::get('email');
        $nama = Input::get('nama');
        if (empty($email)) {
            $hasil['kesalahan'] = 'Email tidak boleh kosong';
        } else {
            mail($email, 'Registrasi', "Selamat, $email.  Anda sudah terdaftar!", "From:me");
            $hasil['email'] = $email;
        }
        return Response::json($hasil);
    }

}

?>

Pada kode program di atas, bila pengguna mengisi alamat email, maka kode tersebut akan mengirim pesan ke alamat email yang bersangkutan melalui function mail() dari PHP. Agar controller di atas dapat dipakai, saya akan mendaftarkannya di routes.php dengan menambahkan kode program berikut ini:

Route::controller('pelanggan', 'PelangganController');

Setelah ini, bila saya menjalankan unit test, saya akan menemukan sebuah kegagalan seperti yang terlihat pada gambar berikut ini:

Test yang gagal

Test yang gagal

Hal ini terjadi karena saya memanggil mail() dari PHP dan saya tidak men-setup mail server di komputer lokal. Walaupun seandainya saya sudah men-setup mail server, hal ini tetap jadi masalah karena setiap kali saya menjalankan unit test, ia akan berusaha mengirim email yang sesungguhnya.

Untuk mengatasi masalah seperti ini, saya dapat mengubah kode program di atas agar tidak memanggil mail() secara langsung, melainkan memanggil facade yang telah disediakan oleh Laravel, yaitu Mail. Oleh sebab itu, saya mengubah kode program PelangganController.php menjadi seperti berikut ini:

<?php

class PelangganController extends BaseController {

    public function getTambah()
    {       
        $email = Input::get('email');
        $nama = Input::get('nama');
        if (empty($email)) {
            $hasil['kesalahan'] = 'Email tidak boleh kosong';
        } else {
            Mail::send('emails.registrasi', ['email'=>$email], function($message) use ($email) {
                $message->from('me@snake.com', 'Solid Snake');
                $message->to($email)->subject('Registrasi!');
            });         
            $hasil['email'] = $email;
        }
        return Response::json($hasil);
    }

}

?>

Class Mail adalah facade ke sebuah objek Illuminate\Mail\Mailer. Perhatikan bahwa saya memakai class secara static tapi sebenarnya ada sebuah object yang saya akses yaitu instance dari Illuminate\Mail\Mailer. Object tersebut dibuat oleh Illuminate\Mail\MailServiceProvider dengan perintah seperti new Mailer().

Bila saya menjalankan pengujian, saya tetap akan memperoleh pesan kesalahan. Lalu, apa bedanya? Seluruh facade di Laravel dilengkapi dengan kemampuan mocking dengan menggunakan Mockery, misalnya melalui method shouldReceive() dan sebagainya. Tapi karena saya tidak memakai Mockery, saya bisa memakai fasilitas mocking dari PHPUnit. Sebagai contoh, saya mengubah kode program pengujian menjadi seperti berikut ini:

<?php

class PelangganControllerTest extends TestCase {

    public function testTambahSukses() {
        // Mocking dari Mailer
        $mailer = $this->getMock('Mailer', array('send'));
        $mailer->expects($this->once())->method('send');

        // Mendaftarkan hasil mock ke facade
        $mailer = Mail::swap($mailer);

        // Mengirim semua data yang dibutuhkan untuk disimpan
        $response = $this->call('GET', 'pelanggan/tambah',
            ['email'=>'solid@snake.com', 'nama'=>'Solid Snake']);

        // Mengembalikan facade seperti semula
        $mailer = Mail::swap($mailer);          

        // Pengujian
        $this->assertJson($response->getContent());
        $this->assertEquals('solid@snake.com', $response->getData()->email);     
    }

    public function testTambahGagal() {
        // Hanya mengirim 'nama' tanpa 'email'
        $response = $this->call('GET', 'pelanggan/tambah',
            ['nama'=>'Solid Snake']);        

        $this->assertJson($response->getContent());
        $this->assertEquals('Email tidak boleh kosong', $response->getData()->kesalahan);
    }

}

?>

Mock adalah sebuah class ‘palsu’ yang menyerupai aslinya. Dengan class ‘palsu’ ini, saya bisa melakukan assertions seperti memastikan ada method tertentu yang dipanggil tanpa harus benar-benar memanggil method bersangkutan. Sebagai contoh, saya membuat mock dari Illuminate\Mail\Mailer dan memastikan bahwa method send() akan dipanggil sekali. Karena ini hanya class ‘palsu’, tidak ada proses pengiriman email yang akan terjadi, hanya saja saya menyakinkan diri bahwa method send() akan dipanggil.

Sekarang, bila saya menjalankan pengujian, saya akan memperoleh hasil sukses seperti pada gambar berikut ini:

Hasil pengujian yang sukses

Hasil pengujian yang sukses

Terlihat bahwa facade mempermudah pengujian bila dibandingkan dengan saya memanggil fungsi yang diharapkan langsung pada kode program.

Sebagai informasi tambahan, khusus untuk Mail, sebenarnya Laravel sudah memiliki cara yang lebih singkat dan lebih mudah untuk tidak benar-benar mengirim email. Saya bisa mengubah file konfigurasi di app/config/testing/mail.php (atau men-copy-nya bila belum ada) dimana nilai 'pretend' di-set menjadi true, seperti pada contoh berikut ini:

<?php

return array(

    ...

    'pretend' => true,

);

Melakukan Unit Test Di Laravel

Satu hal yang membuat saya sulit beradaptasi dengan Eloquent (data mapper milik Laravel) adalah relasi seperti one-to-many di-hard code langsung dalam bentuk pemanggilan function. Pada JPA di dunia Java, relasi one-to-many tetap sebuah Collection biasa yang memiliki getter dan setter. Implementasi JPA akan menggunakan proxy (method yang dipanggil secara transparan tanpa sepengetahuan pengguna) atau bytecode generation untuk melakukan fetching. Dengan demikian, model di JPA (tepatnya entity) akan terlihat seperti sebuah class normal yang dapat diuji secara biasa. Tapi, pada Eloquent, pengujian model yang memiliki relasi dengan model lainnya (seperti one-to-many) akan melibatkan database.

Sebagai contoh, berikut ini adalah contoh definisi model yang diwakili class Faktur:

<?php

class Faktur extends Eloquent {

    protected $dates = array('tanggal');

}

?>

Class Pelanggan akan memiliki relasi one-to-many dengan Faktur dimana faktur yang belum dilunasi oleh pelanggan bisa diakses melalui Pelanggan. Oleh sebab itu, saya membuat class Pelanggan yang isinya seperti berikut ini:

<?php

class Pelanggan extends Eloquent {

    public function fakturBelumLunas() {
        return $this->hasMany('Faktur');
    }

    public function sisaPiutang() {
        $sisa = 0;
        foreach($this->fakturBelumLunas as $faktur) {
            $sisa += $faktur->total;
        }
        return $sisa;
    }

}

?>

Berikut ini adalah contoh test case yang valid untuk melakukan pengujian yang harus terhubung ke database:

<?php

use CarbonCarbon;

class PelangganTest extends TestCase {

    public function testSisaPiutang() {
        $pelanggan = new Pelanggan();
        $pelanggan->nama = 'Test';
        $pelanggan->save();

        $faktur1 = new Faktur();
        $faktur1->nomor = 'FA-01';
        $faktur1->tanggal = Carbon::createFromDate(2014,05,01);
        $faktur1->total = 100000;
        $pelanggan->fakturBelumLunas()->save($faktur1);

        $faktur2 = new Faktur();
        $faktur2->nomor = 'FA-02';
        $faktur2->tanggal = Carbon::createFromDate(2014,05,02);      
        $faktur2->total = 200000;
        $pelanggan->fakturBelumLunas()->save($faktur2);

        $this->assertEquals(300000, $pelanggan->sisaPiutang());       

    }

}

?>

Masalah timbul karena fakturBelumLunas() mengembalikan Illuminate\Database\Eloquent\Relations\HasMany yang mewakili sebuah query. Domain class sudah tercemari oleh database! Itu adalah resiko memakai data mapper yang menerapkan active record pattern seperti Eloquent, Grails, Active Record (Ruby on Rails), dan sejenisnya. Pola active record, walaupun mudah dipakai, secara tidak langsung sudah mencemari domain class dengan method infrastruktur yang bukan bagian dari business logic (termasuk save(), delete(), dan sebagainya). Saya lebih senang memakai pendekatan seperti yang saya lakukan pada simple-jpa dimana saya menambahkan ‘method ajaib’ hanya pada repository bukan pada domain class sehingga domain class benar-benar ‘polos’.

Untuk membuktikan bahwa pengujian ini harus terhubung ke database, saya akan menjalankan test case setelah mematikan database. Saya akan menjumpai kesalahan seperti yang terlihat pada gambar berikut ini:

Pengujian gagal bila tidak terkoneksi ke database

Pengujian gagal bila tidak terkoneksi ke database

Salah satu masalah dalam pengujian yang saya lakukan adalah isi tabel di database akan selalu bertambah akibat pemanggilan save(). Isi database bisa tercemar. Untuk mengatasi hal tersebut, saya perlu memanggil perintah Artisan migrate:refresh. Agar lebih otomatis, saya dapat memanggil perintah Artisan melalui Artisan::call(). Pada PHPUnit, saya dapat meletakkan kode program yang akan selalu dikerjakan sebelum test dimulai pada method setUp() sehingga kode program saya terlihat seperti:

<?php

use CarbonCarbon;

class PelangganTest extends TestCase {

    public function setUp() {
        parent::setUp();
        Artisan::call('migrate:refresh');
    }

    public function testSisaPiutang() {                 
        $pelanggan = new Pelanggan();
        $pelanggan->nama = 'Test';
        $pelanggan->save();

        $faktur1 = new Faktur();
        $faktur1->nomor = 'FA-01';
        $faktur1->tanggal = Carbon::createFromDate(2014,05,01);
        $faktur1->total = 100000;
        $pelanggan->fakturBelumLunas()->save($faktur1);

        $faktur2 = new Faktur();
        $faktur2->nomor = 'FA-02';
        $faktur2->tanggal = Carbon::createFromDate(2014,05,02);
        $faktur2->total = 200000;
        $pelanggan->fakturBelumLunas()->save($faktur2);

        $this->assertEquals(300000, $pelanggan->sisaPiutang());       
    }


}

?>

Sekarang, setiap kali menjalankan pengujian, isi database akan selalu dikosongkan terlebih dahulu sehingga pengujian akan dilakukan pada kondisi database yang konsisten.

Dalam banyak kasus, test case sebaiknya tidak menyentuh database production secara langsung, melainkan database lokal atau database in-memory. Salah satu alasan untuk memakai database yang sama di lokal (misalnya sama-sama MySQL) adalah bisa dipastikan bahwa environment-nya sama persis. Dengan demikian, tidak mungkin ada masalah SQL yang jalan di lokal tapi tidak jalan di production. Kelemahannya adalah penggunaan database seperti MySQL membuat pengujian menjadi lambat. Padahal pengujian adalah sesuatu yang sangat sering dilakukan. Untuk mengatasi hal tersebut, saya dapat menggunakan database in-memory. Sebagai contoh, database SQLite dapat dipakai sebagai database in-memory.

Untuk memakai database SQLite sebagai database in-memory hanya pada pengujian, saya perlu men-copy file database.php pada lokasi app/config/testing seperti yang terlihat pada gambar berikut ini:

Konfigurasi untuk enviroment testing

Konfigurasi untuk enviroment testing

Sama seperti di Griffon dan framework populer lainnya, Laravel juga memiliki konsep environment pada konfigurasinya. Masing-masing subdirektori pada direktori config akan mewakili sebuah environment. Laravel akan membaca file konfigurasi sesuai dengan environment yang sedang aktif. Salah satu environment bawaan Laravel yang hanya akan aktif pada saat pengujian adalah testing. Dengan demikian, saat melakukan koneksi database di pengujian, file konfigurasi yang dibaca bukan app/config/database.php melainkan app/config/testing/database.php. Saya akan mengubah isi file tersebut menjadi:

<?php

return array(

    'fetch' => PDO::FETCH_CLASS,

    'default' => 'sqlite',

    'connections' => array(

        'sqlite' => array(
            'driver'   => 'sqlite',
            'database' => ':memory:',
            'prefix'   => '',
        ),

    ),

);

?>

Selain itu, saya juga mengubah Artisan::call('migrate:refresh') menjadi Artisan::call('migrate') di PelangganTest.php. Saya tidak perlu menghapus tabel secara manual karena pada database in-memory, setelah selesai dikerjakan, semuanya akan hilang.

Sampai disini, bila saya menjalankan pengujian, maka database yang dipakai adalah database SQLite sebagai database in-memory. Tapi sebenarnya, untuk kasus saya, karena pada kode program di atas saya memakai dynamic properties dalam bentuk Illuminate\Database\Eloquent\Collection, saya bisa menguji tanpa harus terhubung ke database. Cara ini tidak bisa diterapkan bila saya memanggil method secara langsung seperti $this->fakturBelumLunas()->getResults() di kode program diuji. Sebagai contoh, saya bisa mengubah test case menjadi seperti berikut ini:

<?php

use IlluminateDatabaseEloquentCollection;

class PelangganTest extends TestCase {

    public function testSisaPiutang() {     

        $faktur1 = new Faktur();
        $faktur1->nomor = 'FA-01';     
        $faktur1->total = 100000;        

        $faktur2 = new Faktur();
        $faktur2->nomor = 'FA-02';         
        $faktur2->total = 200000;

        $fakturs = new Collection([$faktur1, $faktur2]);    
        $pelanggan = new Pelanggan();   
        $pelanggan->fakturBelumLunas = $fakturs;                         

        $this->assertEquals(300000, $pelanggan->sisaPiutang());               

    }

}

?>

Bila saya menjalankan pengujian di atas, saya akan memperoleh hasil sukses tanpa harus memakai database sama sekali seperti yang terlihat pada gambar berikut ini:

Pengujian sukses tanpa harus terhubung ke database

Pengujian sukses tanpa harus terhubung ke database

Memakai Server-Sent Events API Di HTML5

Pada artikel Memakai Reverse Ajax Dengan Spring Web MVC 3.2, saya mengimplementasikan Reverse Ajax dengan menggunakan teknik long polling. Pada artikel tersebut, di sisi client, saya secara recursive terus memanggil jQuery.getJSON() untuk melakukan polling. Pada artikel ini, saya akan memakai cara yang lebih sederhana dengan menggunakan Server-Sent Events (SSE) API. Walaupun membuat semuanya menjadi sederhana, SSE hanya membolehkan komunikasi satu arah (yaitu dari server ke client). Selain itu, data tidak boleh dalam bentuk sembarangan melainkan harus mengikuti format text/event-stream. Setidaknya ini masih lebih mudah diterapkan bila dibandingkan menggunakan WebSocket yang mengharuskan server khusus.

Sebagai latihan, saya akan mulai dengan membuat sebuah proyek Laravel baru dengan memberikan perintah:

composer create-project laravel/laravel latihan_sse --prefer-dist

Setelah itu, saya akan membuat sebuah view sederhana yang melakukan proses login dengan nama login.php di folder view yang isinya seperti berikut ini:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
    <form method="post" action="<?php echo action('LoginController@login'); ?>">
        <div>
            <label for="nama">Nama</label>
            <input type="text" name="nama" maxlength="50" required />
        </div>
        <div>
            <label for="password">Password</label>
            <input type="password" name="password" required />
        </div>
        <div>
            <input type="submit" value="Login" />
        </div>
    </form>
</body>
</html>

View di atas akan memanggil LoginController.login() bila tombol submit di-klik.

Berikutnya, saya membuat sebuah view lagi dengan nama Logout.php di folder view yang isinya seperti berikut ini:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
    Selamat datang, <strong><?php echo $nama; ?></strong>.
    <?php 
        echo link_to_action('LoginController@logout', 
            'Klik disini untuk logout', array('nama'=>$nama));
    ?>
</body>
</html>

View di atas akan memanggil LoginController.logout() dengan menyertakan parametr nama user bila link untuk logout di-klik oleh pengguna.

Setelah view selesai dibuat, saya akan lanjut dengan membuat sebuah controller sederhana dengan nama LoginController.php di folder controller yang isinya seperti berikut ini:

<?php

class LoginController extends BaseController {

    public function login() {       
        if (Request::has('nama') && Request::has('password')) {
            $nama = Request::get('nama');
            $events = Cache::get('events', function() { return []; });
            $events[] = ['nama'=>$nama, 'aksi'=>'login'];
            Cache::forever('events', $events);            
            return Response::view('logout', array('nama' => $nama));
        } else {
            return Redirect::to('login');
        }
    }

    public function logout() {
        $nama = Request::get('nama');
        $events = Cache::get('events', function() { return []; });
        $events[] = ['nama'=>$nama, 'aksi'=>'logout'];
        Cache::forever('events', $events);        
        return Redirect::to('login');
    }

}

?>

Kode program sederhana di atas akan menyimpan log aktifitas login dan logout ke dalam sebuah array. Karena PHP tidak memungkinkan sebuah penyimpanan yang permanen untuk session berbeda, tidak seperti variabel static di Java EE yang dapat diakses kapan saja, maka saya akan menggunakan Cache dari Laravel. Secara default, Laravel akan menulis isi Cache ke sebuah file sehingga saya dapat memperoleh isi Cache yang sama kapan saja pada session apapun.

Langkah terakhir untuk membuat proses login dan logout bekerja adalah menambahkan kode program PHP berikut ini di routes.php:

Route::get('login', function() {
    return View::make('login');
});

Route::post('login', 'LoginController@login');

Route::get('logout', 'LoginController@logout');

Sekarang, bila saya membuka URL untuk /login, saya dapat login dengan nama pengguna apa saja, kemudian memilih logout. Di balik layar, PHP akan menyimpan aktifitas (event) login atau logout tersebut pada Cache milik Laravel.

Sekarang, saya akan mengimplementasikan bagian yang membaca event login atau logout dari Cache. Halaman tersebut akan memakai SSE: sebuah halaman yang menampilkan status user, apakah sedang online atau offline. Dengan Reverse Ajax, server akan menghubungi client bila ada user yang login atau logoff sehingga client hanya perlu bekerja memperbaharui tampilan jika ada event yang perlu diproses.

Saya akan mulai dengan membuat sebuah view bernama User.php di folder view yang isinya terlihat seperti berikut ini:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
    <style type="text/css">
        table {
            color: #666; font-size: 20px; text-shadow: 1px 1px 0px #fff;
            background: #eaebec; margin: 20px; border: #ccc 1px solid;
            border-radius: 3px; box-shadow: 0 1px 2px #d1d1d1;          
        }
        table td {
            padding-left: 20px; border-top: 1px solid #fff;
            border-bottom: 1px solid #e0e0e0;

        }
        .offline { color: red;  }       
        .online { color: green; }
    </style>  
</head>
<body>
    <table id='tabel'>
        <tbody>
            <tr id='solid snake'>
                <td>solid snake</td>
                <td class='offline'>OFFLINE</td>
            </tr>
            <tr id='liquid snake'>
                <td>liquid snake</td>
                <td class='offline'>OFFLINE</td>
            </tr>
            <tr id='big boss'>
                <td>big boss</td>
                <td class='offline'>OFFLINE</td>
            </tr>
            <tr id='mei ling'>
                <td>mei ling</td>
                <td class='offline'>OFFLINE</td>
            </tr>
        </tbody>
    </table>
    <script type="text/javascript">     
        var source = new EventSource("<?php echo action("EventController@proses"); ?>");      
        source.addEventListener('login', function(e) {
            var td = document.querySelector("table tr[id='" + e.data + "'] td:nth-child(2)");
            td.className = 'online';
            td.textContent = 'ONLINE';    
        }, false);
        source.addEventListener('logout', function(e) {
            var td = document.querySelector("table tr[id='" + e.data + "'] td:nth-child(2)");
            td.className = 'offline';
            td.textContent = 'OFFLINE';
        }, false);      
    </script>
</body>
</html>

Pada view di atas, penggunaan SSE membuat kode program JavaScript terlihat sederhana. Saya hanya perlu membuat sebuah object EventSource. Setelah itu, saya menggunakan addEventListener untuk menentukan apa yang akan dilakukan bila server mengirim event tertentu. Pada contoh di atas, bila event login (custom event) terjadi, maka JavaScript akan mengisi kolom kedua untuk user bersangkutan dengan ‘ONLINE’. Sebaliknya, bila event logout (custom event) dikirim oleh server, maka JavaScript akan mengisi kolom kedua untuk user bersangkutan menjadi ‘OFFLINE’.

Bagian yang lebih sulit adalah membuat kode program di sisi server-nya. Saya wajib mengembalikan respon dalam bentuk seperti:

event: login
data: solid snake

event: login
data: liquid snake

event: logout
data: liquid snake

event: logout
data: solid snake

...

Untuk itu, saya membuat sebuah controller baru dengan nama EventController.php di folder controllers yang isinya seperti berikut ini:

<?php

class EventController extends BaseController {

    public function proses() {
        header('Content-Type: text/event-stream');
        header('Cache-Control: no-cache');
        set_time_limit(0);
        while(1) {
            $events = Cache::get('events', function() { return []; });                    
            if (count($events) > 0) {
                $event = array_shift($events);
                echo "event: {$event['aksi']}n";
                echo "data: {$event['nama']}n";
                echo "n";
                ob_flush();
                flush();
            }           
            Cache::forever('events', $events);            
            sleep(1);           
        }
    }

}

?>

Pada kode program di atas, saya memberikan header Content-Type secara manual. Untuk mengembalikan respon untuk SSE, nilai Content-Type wajib berisi text/event-stream. Setelah itu, saya akan memeriksa isi Cache di Laravel untuk menentukan apakah event yang perlu dikirim ke browser. Perhatikan bahwa saya melakukan ini terus menerus dan function proses() tidak akan pernah selesai. Tidak semua server bisa bertahan bila dipaksa untuk seperti ini! Untuk keperluan produksi saat aplikasi diakses banyak orang secara bersamaan, sebaiknya gunakan server yang mendukung seperti Node.js atau server JEE yang mendukung Servlet 3.0 (asynchronous servlet).

Langkah terakhir adalah mendaftarkan routes di routes.php dengan menambahkan kode program berikut ini:

Route::get('user', function() {
    return View::make('user');
});

Route::get('event', 'EventController@proses');

Sekarang, saya bisa mencoba menjalankan aplikasi web sederhana ini dengan membuka URL http://localhost/latihan_sse/public/user. Semua user pada awalnya akan memiliki status offline. Setelah itu, pada tab browser yang berbeda, saya dapat membuka satu atau lebih URL http://localhost/latihan_sse/public/login untuk melakukan login sesuai dengan nama user yang diinginkan. Perhatikan bahwa status di halaman user akan langsung diperbaharui, seperti yang terlihat pada gambar berikut ini:

Animasi yang menunjukkan isi tabel diperbaharui secara otomatis

Animasi yang menunjukkan isi tabel diperbaharui secara otomatis

Memakai MVC dan Model Binding Di Laravel

MVC pada awalnya berasal dari dunia pemograman desktop. Komponen pada MVC terdiri atas model yang berisi data, view yang mempresentasikan data di model, dan controller yang memanipulasi model. Pada kebanyakan framework MVC desktop modern seperti Griffon, WPF dan JavaFX, terdapat binding antara model dan view (melalui observer pattern) sehingga perubahan yang dilakukan oleh controller pada model akan menyebabkan perubahan pada view, dan sebaliknya, perubahan pada view (misalnya akibat input dari pengguna) akan menyebabkan perubahan pada model. Pada MVC desktop, model murni dipakai untuk mewakili data pada view dan bukan sebuah domain model yang harus bisa berdiri sendiri tanpa terikat pada layer apapun termasuk presentation layer (MVC). Yup! Data pada GUI berbeda dengan yang ada di domain model (permasalahan bisnis), misalnya, pada GUI bisa saja ada data berupa total harga, total diskon, informasi kesalahan, nomor sementara dan sebagainya yang harus ditampilkan.

Pada pemograman web, MVC memiliki implementasi yang berbeda karena view ‘setengah’ berada di client sementara sisanya ada di server. Dengan demikian, tidak ada binding secara langsung antara model dan view. Perubahan pada model tidak akan langsung mempengaruhi view (tampilan di browser) dan juga sebaliknya! Salah satu bentuk implementasi pada Spring Web MVC adalah memakai model yang diwakili oleh Map. Model dalam bentuk Map tersebut dapat diterjemahkan menjadi sebuah class dengan menggunakan @ModelAttribute. Dalam banyak kasus, @ModelAttribute dipakai untuk menerjemahkan setiap elemen dalam Map langsung menjadi domain model. Ada juga aliran puritan yang ‘menentang’ dan tetap ingin mempertahankan view model pada Web MVC. Kecenderungan menganggap domain model sebagai view model kerap menghasilkan domain model yang hanya berisi data tanpa banyak operasi dan berdiri sendiri tanpa banyak kolaborasi (dengan kata lain: kurang OOP!).

Pada kesempatan ini, saya akan mencoba memakai Web MVC pada framework Laravel. Saya akan mulai dengan membuat model bernama Mahasiswa.php pada lokasi app/models. Pada Laravel, sama seperti Web MVC pada umumnya, model pada MVC langsung merujuk pada domain model. Laravel memiliki ORM (atau tepatnya sebuah implementasi active record pattern) yang diberi nama Eloquent ORM untuk menyimpan dan membaca domain model dari database. Ini memang subjektif, tapi bagi saya, simple-jpa tetap lebih praktis untuk dunia nyata yang memang kompleks πŸ™‚

Sebagai contoh, saya membuat isi Mahasiswa.php seperti berikut ini:

<?php

use CarbonCarbon;

class Mahasiswa extends Eloquent {

    protected $table = 'mahasiswa';

    protected $fillable = array('nama', 'alamat', 'tanggalLahir');

    protected $dates = array('tanggalLahir');

    public function getUsia() {     
        return isset($this->tanggalLahir)? $this->tanggalLahir->age: 0;
    }
}

?>

Saya menurunkan Mahasiswa dari Eloquent sehingga ia bisa disimpan dan dibaca dari database dengan active record pattern dari Eloquent. Pada PHP yang bersifat dinamis, saya tidak perlu mendefinisikan seluruh atribut yang ada pada Mahasiswa karena mereka bisa muncul sendiri saat ditambahkan. Ini bisa jadi sesuatu yang buruk. Oleh sebab itu, saya mendeklarasikan apa saja atribut yang ada dan dapat di-isi pada $fillable.

Pada simple-jpa, saya memakai Joda-Time karena penanganan tipe data tanggal yang sangat buruk di Java 7 (dan hal ini sudah diperbaiki di Java 8). Sama seperti yang saya lakukan, Laravel memakai library Carbon untuk meningkatkan kemampuan class DateTime bawaan PHP. Bila saya ingin memakai Carbon pada atribut yang mewakili tanggal seperti tanggalLahir, saya perlu mendaftarkannya di $dates. Bila ini tidak dilakukan, tanggalLahir akan dianggap seperti tipe data tanggal native di PHP yang berupa angka dan non-object.

Tidak seperti pada JPA yang memungkinkan pendekatan object-first dimana pengguna merancang domain model baru kemudian menghasilkan database berdasarkan hasil rancangan, pada Laravel, saya harus membuat tabel secara manual. Saya akan mulai dari mengubah informasi koneksi database di database.php. Laravel menyediakan fasilitas migration (sejenis Flyway di Java) untuk mempermudah memindahkan tabel dari development ke server produksi nanti. Untuk itu, saya akan memberikan perintah berikut ini:

php artisan migrate:make tabelAwal

Perintah di atas akan membuat file baru di app/database/migrations yang diawali dengan timestamp seperti 2014_05_03_09139_tabelAwal.php. Saya akan memakai fasilitas schema builder dari Laravel untuk membuat tabel Mahasiswa sehingga kode program akan terlihat seperti:

<?php

use IlluminateDatabaseSchemaBlueprint;
use IlluminateDatabaseMigrationsMigration;
use IlluminateSupportFacadesSchema;

class TabelAwal extends Migration {

    public function up()
    {
        Schema::create('mahasiswa', function($table) {
            $table->increments('id');
            $table->string('nama', 50);
            $table->string('alamat', 100);
            $table->date('tanggalLahir');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::drop('mahasiswa');
    }

}

?>

Setelah ini, untuk mengerjakan kode program di atas, saya memberikan perintah:

php artisan migrate

Pada kasus yang kompleks dimana ada banyak domain model, saya akan sangat merindukan perintah griffon generate-schema dari simple-jpa yang dapat menghasilkan file SQL secara otomatis πŸ™‚

Berikutnya, saya dapat menguji class Mahasiswa dengan membuat unit test sederhana seperti berikut ini:

<?php

use CarbonCarbon;

class MahasiswaTest extends TestCase {

    public function testUsia() {
        $mahasiswa = new Mahasiswa();
        $mahasiswa->id = 1;
        $mahasiswa->nama = 'Solid Snake';
        $mahasiswa->tanggalLahir = Carbon::createFromDate(1985,12,25);
        $mahasiswa->save();

        $mahasiswa = Mahasiswa::find(1);                
        $this->assertEquals(28, $mahasiswa->getUsia());

        $mahasiswa->tanggalLahir = null;
        $this->assertEquals(0, $mahasiswa->getUsia());

        $mahasiswa->delete();                
    }

}

?>

Setelah memastikan bahwa model sudah siap, saya akan lanjut dengan membuat view. Laravel memiliki fasilitas templating untuk view yang disebut sebagai Blade. Untuk memakai Blade, saya hanya perlu mengakhiri nama file dengan .blade.php. Sebagai contoh, saya akan membuat view dengan nama tambahMahasiswa.blade.php di app\views yang isinya seperti berikut ini:

<html>
    <body>
        <h1>Entry Data Mahasiswa</h1>

        {{ Form::model($mahasiswa, array('action' => array('MahasiswaController@simpan', $mahasiswa->id))) }}

            <div>
            {{ Form::label('nama', 'Nama') }}
            {{ Form::text('nama') }}
            </div>

            <div>
            {{ Form::label('alamat', 'Alamat') }}
            {{ Form::text('alamat') }}            
            </div>

            <div>
            {{ Form::label('tanggalLahir', 'Tanggal Lahir') }}
            {{ Form::selectRange('tanggal', 1, 31, isset($mahasiswa->tanggalLahir)? $mahasiswa->tanggalLahir->day: 1)}}
            {{ Form::selectMonth('bulan', isset($mahasiswa->tanggalLahir)? $mahasiswa->tanggalLahir->month: 0) }}
            {{ Form::selectYear('tahun', 1940, 2013, isset($mahasiswa->tanggalLahir)? $mahasiswa->tanggalLahir->year: 1940) }}
            </div>

            <div>
            {{ Form::submit('Simpan') }}
            </div>

        {{ Form::close() }}

        <div>
            {{ $pesan }}
        </div>

    </body>
</html>

Pada view di atas, saya memakai form builder dari Laravel untuk menghasil HTML Form secara otomatis. Agar view di atas dapat ditampilkan, saya perlu menambahkan baris berikut ini pada file routes.php sehingga isinya menjadi seperti berikut ini:

Route::get('/tambah', function()
{
    return View::make('tambahMahasiswa', array('mahasiswa' => new Mahasiswa(), 'pesan' => NULL));   
});

Sekarang, saya bisa mengakses view tersebut dengan membuka URL seperti http://localhost/latihan/public/tambah di browser.

Komunikasi dari view ke controller terjadi bila tombol Simpan di-klik oleh pengguna. Pada contoh ini, method bernama simpan() di MahasiswaController akan dikerjakan. Sebelum method tersebut dapat dipanggil dari view, saya perlu menambahkan isi routes.php berupa:

Route::model('mahasiswa', 'Mahasiswa');

Route::post('/tambah/proses/{mahasiswa?}', 'MahasiswaController@simpan');

Pada Spring Web MVC, saya mendeklarasikan route secara langsung di controller melalui annotation @RequestMapping pada class tersebut atau masing-masing method miliknya. PHP tidak memiliki fasilitas annotation. Walaupun ada beberapa framework yang memakai ‘sejenis’ annotation, mereka hanya dianggap komentar (misalnya tidak ada fitur bahasa PHP untuk mencari daftar annotation pada sebuah class). Oleh sebab itu, pemetaan antara controller dan URL biasanya didefinisikan pada sebuah file terpisah. Pada Laravel, file tersebut adalah routes.php. Saya menggunakan {mahasiswa?} untuk menunjukkan bahwa parameter tersebut tidak wajib ada (untuk membedakan antara operasi tambah dan operasi edit).

Dengan menggunakan Route::model('mahasiswa', 'Mahasiswa'), Laravel akan menerjemahkan id di semua parameter bernama mahasiswa menjadi model Mahasiswa. Laravel akan men-query ke database dan memberikan object Mahasiswa sebagai parameter di controller nanti. Ini adalah sesuatu yang mirip seperti @ModelAttribute di Spring Web MVC. Bedanya, @ModelAttribute hanya menerjemahkan setiap parameter terpisah (dalam bentuk Map) yang di-isi oleh pengguna ke dalam sebuah class model. Route::model() hanya menerima sebuah id dan melakukan query ke database untuk mengembalikan object berdasarkan id tersebut.

Sekarang, saya siap untuk membuat implementasi dari controller tersebut dalam bentuk sebuah file PHP bernama MahasiswaController.php di folder app/controllers yang isinya seperti berikut ini:

<?php

use IlluminateSupportFacadesInput;
use CarbonCarbon;

class MahasiswaController extends BaseController {

    public function simpan($mahasiswa = NULL) {
        if (is_null($mahasiswa)) {
            $mahasiswa = new Mahasiswa();           
            $pesan = 'Data mahasiswa ini berhasil disimpan!';
        } else {
            $pesan = 'Data mahasiswa berhasil diupdate!';
        }
        $mahasiswa->nama = Input::get('nama');
        $mahasiswa->alamat = Input::get('alamat');
        $mahasiswa->tanggalLahir = Carbon::createFromDate(Input::get('tahun'), Input::get('bulan'), Input::get('tanggal'));
        if ($mahasiswa->getUsia() > 20) {
            $pesan = 'Penyimpanan gagal karena kamu terlalu tua!';
        } else {        
            $mahasiswa->save();
        }                       
        return View::make('tambahMahasiswa', array('mahasiswa' => $mahasiswa, 'pesan' => $pesan));
    }

}

?>

Sekarang, bila saya membuka URL seperti http://localhost/latihan/public/tambah, saya dapat mengisi form dan men-klik tombol Simpan untuk menyimpannya ke database. Setelah itu, view yang sama akan kembali ditampilkan lengkap dengan data seperti pada saat di-isi. Bila kali ini tombol Simpan di-klik, maka data yang sudah ada akan di-update.

Pola komunikasi web MVC yang terjadi pada contoh di atas adalah seperti berikut ini:

  1. Komunikasi controller -> view: Controller melewatkan model ke view sehingga nilai pada form akan terisi sesuai dengan nilai di model. Untuk mencapai hal seperti ini, saya memakai Form::model() di view.
  2. Komunikasi controller <- view: Pada saat view memanggil controller, method pada controller akan memperoleh sebuah model yang dibuat secara otomatis berdasarkan id-nya. Selain itu, nilai di view juga dapat diakses secara manual dengan menggunakan Input::get().

Composer Dan Permasalahan Autoloader Di PHP

Salah satu kecenderungan dalam OOP memecah permasalahan yang dihadapi menjadi class-class kecil yang saling berkolaborasi. Pada Java, setiap class dapat dikelompokkan ke dalam package (sehingga class boleh memiliki nama yang sama asalkan berada di package yang berbeda). Untuk memakai class dalam package yang berbeda, programmer Java akan menggunakan import untuk menentukan lokasi class yang akan dipakai. Kumpulan class yang saling berkaitan (misalnya sebuah komponen) didistribusikan dalam bentuk file JAR. Programmer dapat me-load class secara dinamis dengan menggunakan ClassLoader, tapi biasanya dependensi dideklarasikan secara statis dengan memberitahu lokasi file JAR yang dibutuhkan melalui classpath.

PHP memiliki penanganan package dan class yang tidak sesederhana di Java. Hal ini karena bahasa tersebut awalnya adalah bahasa scripting dengan asumsi bahwa satu halaman PHP adalah sebuah halaman web. Seiring waktu, PHP menjadi semakin kompleks dan banyak library OOP pun bermunculan. Untuk memakai class dalam library eksternal, programmer perlu memberikan perintah include atau require agar file yang berisi deklarasi class dibaca. Hal ini karena biasanya programmer membuat sebuah class dalam sebuah file PHP tunggal. Selain itu, class umumnya dideklarasikan dalam namespace berbeda sehingga programmer perlu memberikan perintah use untuk memakai namespace yang bersangkutan. Jadi ribet, bukan?

Untuk mengatasi permasalahan ini, PHP 5 memperkenalkan fungsi spl_autoload_register() yang akan mengerjakan sebuah function bila sebuah class tidak dikenal dipanggil. Tapi kembali muncul sebuah masalah baru: spl_autoload_register() hanya menyediakan cara untuk mencari class, tapi tidak ada standar yang pasti mengenai lokasi file PHP yang berisi class tersebut. Hal ini berbeda dengan Java dimana setiap package diwakili oleh sebuah folder/direktori sehingga lokasi file class untuk sebuah package dapat ditelusuri secara mudah. PHP tidak membatasi sebuah namespace harus memiliki struktur folder serupa. Programmer bisa bebas meletakkan file PHP berisi class di lokasi mana saja. Lalu, bagaimana cara universal untuk mencari lokasi file PHP berdasarkan informasi namespace?

Untuk itu, PHP-FIG (PHP Framework Interop Group), http://www.php-fig.org/faq/, menciptakan standar seperti PSR-0 (Autoloading Standar) dan PSR-4 (Improved Autoloading). Pada standar PSR-0, namespace seperti \Doctrine\Common\IsolatedClassLoader akan dipetakan pada folder /path/to/project/lib/vendor/Doctrine/Common/IsolatedClassLoader.php. Hal ini mirip seperti pada pemetaan folder untuk package di Java. Standar PSR-4 memperkenalkan sesuatu yang berbeda dimana namespace terdiri atas namespace prefix yang dapat dipetakan ke lokasi apa saja dan sisanya merupakan subfolder dari prefix tersebut. Walaupun lebih fleksibel, PSR-4 membuat lebih sulit menebak lokasi file PHP. Sebagai contoh, anggap saja terdapat class: Foo\Bar\Bat\Baz. Si pembuat class kemudian memilih Foo\Bar sebagai namespace prefix. Namespace prefix Foo\Bar dapat dipetakan pada satu atau lebih folder (misalnya /path/to/packages/foo-bar/src dan /path/to/packages/foo-bar/tests). Jadi, untuk mencari file PHP yang mengandung Foo\Bar\Bat\Baz, autoloader harus memeriksa apakah ada file /path/to/packages/foo-bar/src/Bat/Baz.php atau /path/to/packages/foo-bar/tests/Bat/Baz.php.

Btw, PHP-FIG dan standar PSR (PHP Standards Recommendation) dari mereka bukanlah bagian resmi dari PHP. Sebagai contoh, RFC untuk memasukkan PSR-0 menjadi bagian dari PHP telah ditolak seperti yang dapat dilihat di https://wiki.php.net/rfc/splclassloader.

Permasalahan berikutnya adalah siapa yang harus membuat isi function yang didaftarkan dengan memanggil spl_autoload_register()? Function ini harus mencari lokasi file PHP yang berisi deklarasi class yang berada dalam namespace berbeda. Mendaftarkan setiap lokasi folder untuk namespace prefix di PSR-4 bisa jadi sesuatu yang membosankan. Beruntungnya, Composer memiliki fasilitas untuk membuat implementasi function yang didaftarkan melalui spl_autoload_register() secara otomatis. Yang perlu saya lakukan tinggal memanggil (dengan keyword include atau require) file vendor/autoload.php.

Untuk melakukan konfigurasi autoloader di Composer, saya dapat mengubah bagian dari composer.json yang terlihat seperti berikut ini:

"autoload": {
    "classmap": [
        "app/commands",
        "app/controllers",
        "app/models",
        "app/database/migrations",
        "app/database/seeds",
        "app/tests/TestCase.php"
    ]
},

Composer mendukung PSR-0 autoloading, PSR-4 autoloading, classmap generation dan files includes.

Pada contoh di atas, yang merupakan bawaan dari proyek Laravel, konfigurasi autoloader dilakukan dengan menggunakan classmap. Pada konfigurasi ini, Composer akan memeriksa isi direktori yang diberikan dan membuat sebuah pemetaan dari nama class ke nama file PHP yang dapat dijumpai di vendor/composer/autoload_classmap.php. Karena sudah pemetaan yang tinggal dicari, metode ini merupakan metode yang paling cepat dan tidak membutuhkan algoritma tambahan seperti pada PSR-0/4. Kelemahannya adalah setiap kali ada file baru yang dibuat pada direktori, saya harus memberikan perintah untuk memperbaharui isi vendor/composer/autoload_classmap.php seperti berikut ini:

composer dump-autoload

Tunggu dulu, pengguna tidak harus mengerjakan perintah di atas bila menambah class tertentu di proyek Laravel mereka! Mengapa demikian? Hal ini karena selain memakai autoloader dari Composer, Laravel juga mendaftarkan autoloader miliknya sendiri di Illuminate\Support\ClassLoader. Autoloader tersebut akan mencari file PHP di lokasi folder yang terdaftar di app/start/global.php seperti pada cuplikan berikut ini:

/*
|--------------------------------------------------------------------------
| Register The Laravel Class Loader
|--------------------------------------------------------------------------
|
| In addition to using Composer, you may use the Laravel class loader to
| load your controllers and models. This is useful for keeping all of
| your classes in the "global" namespace without Composer updating.
|
*/

ClassLoader::addDirectories(array(

    app_path().'/commands',
    app_path().'/controllers',
    app_path().'/models',
    app_path().'/database/seeds',

));

Walaupun framework seperti Symfony dan Laravel memakai nama ClassLoader untuk autoloader mereka, perlu diingat bahwa autoloader PHP berbeda dengan ClassLoader di Java. Pada Java, beberapa ClassLoader berbeda dapat membaca class yang sama tapi dengan versi berbeda tanpa mengalami bentrokan asalkan mereka dipakai pada context ClassLoader masing-masing. Pada PHP, autoloader biasanya hanya mencari dan men-include file PHP berdasarkan urutan registrasinya. Sebagai contoh, saya memakai PHPUnit bawaan Zend Studio untuk menguji sebuah proyek Laravel. PHPUnit ini memakai Composer versi lama yang belum mendukung PSR-4. Sementara itu, proyek Laravel memakai Composer versi baru yang sudah mendukung PSR-4. Pada saat saya menjalankan pengujian melalui PHPUnit tersebut, saya akan memperoleh kesalahan seperti:

Debug Error: /proyek/vendor/composer/autoload_real.php line 40 - Call to undefined method Composer\Autoload\ClassLoader::setPsr4()

Mengapa demikian? Karena yang ditemukan terlebih dahulu oleh autoloader adalah Composer\Autoload\ClassLoader versi lama milik PHPUnit bawaan Zend Studio, sementara itu Composer\Autoload\ClassLoader milik Composer yang ter-install diproyek saat ini akan diabaikan (padahal ini yang dibutuhkan!). Padahal, permasalahan seperti ini adalah salah satu alasan memakai custom ClassLoader di Java.

Memahami IoC Container Dan Facade Di Laravel

Sebagai seorang yang datang dari dunia Java, bicara soal IoC Container (Inversion of Control Container), yang muncul dalam bayangan saya adalah Spring Container. Secara sederhana, IoC Container adalah sesuatu yang menyiapkan instance dari class yang ada dan memberikannya pada yang dibutuhkan. Jadi, developer tidak perlu report dengan perintah new. Ia juga tidak perlu ragu lagi apakah object yang sudah ada sebelumnya dapat dipakai ulang. IoC Container akan memberikan object pada class yang dibutuhkan melalui beberapa cara, misalnya service locator (pengguna meminta object tersebut secara manual) atau dependency injection (object disuntik secara otomatis pada penggunanya).

Untuk menunjukkan penggunaan IoC Container di Laravel 4, saya akan membuat sebuah class service sederhana yang mengembalikan nomor faktur berurut seperti ‘FA-2014/05-00001’:

<?php

namespace services;

use DB, Log;

class NomorService {

    private $nomorTerakhir;

    function __construct() {
        Log::debug('Melakukan query ke database untuk mencari nomor terakhir...');
        $hasil = DB::select('SELECT nomorFaktur FROM faktur WHERE ' . 
            'SUBSTRING(nomorFaktur, 4, 4) = YEAR(CURDATE()) AND '.    
            'SUBSTRING(nomorFaktur, 9, 2) = MONTH(CURDATE()) ' .
            'ORDER BY nomorFaktur DESC LIMIT 1');
        if (empty($hasil)) {
            $this->nomorTerakhir = 0;
        } else {            
            $this->nomorTerakhir = substr($hasil[0]->nomorFaktur, 11);            
        }
    }

    public function getCalonNomor() {       
        return sprintf('FA-%s/%s-%05d', date('Y'), date('m'), $this->nomorTerakhir + 1);
    }
}

?>

Tanpa IoC Container, setiap kali saya akan menggunakan class di atas, saya perlu membuat instance baru darinya seperti pada contoh berikut ini:

<?php

use services\NomorService;

class LatihanController extends BaseController {

    public function getNomorFaktur() {
        $nomorService = new NomorService();
        return $nomorService->getCalonNomor();
    }

}

?>

Cara ini dianggap kurang rapi karena setiap kali membutuhkan NomorService, kode program saya (di controller) memiliki tanggung jawab untuk membuat instance baru dari class tersebut. Hal ini dapat dihindari bila memakai IoC Controller, misalnya dengan constructor injection seperti berikut ini:

<?php

use services\NomorService;

class LatihanController extends BaseController {

    public function __construct(NomorService $nomorService) {
        $this->nomorService = $nomorService;
    }

    public function getNomorFaktur() {      
        return $this->nomorService->getCalonNomor();
    }

}

?>

Pada versi yang memakai constructor injection, IoC Container dari Laravel akan membuat instance baru dari NomorService secara otomatis dan melewatkannya pada constructor. Cara ini jauh lebih baik karena controller tidak punya tanggung jawab untuk membuat services.

Sedikit permasalahan pada kode di atas adalah instance NomorService selalu dibuat setiap kali LatihanController dipanggil. Hal ini biasanya tidak jadi masalah karena pada PHP, setiap kali terdapat request baru, seluruh object selalu dibuat ulang dari awal sehingga static (singleton) tidak begitu banyak berguna. Walaupun demikian, Laravel memungkinkan untuk selalu menyuntikkan instance yang sama (tanpa membuat baru). Ini adalah perilaku default untuk Spring Container karena pada Java, object yang masih dipakai (terutama yang static) tetap dipertahankan di memori walaupun sudah berganti request sehingga terlalu sering membuat objek baru akan boros memori. Agar IoC Container dari Laravel selalu menyuntikkan instance yang sama, saya dapat menambahkan kode program seperti berikut ini di global.php:

App::singleton('services\NomorService', function() { return new \services\NomorService; });
App::make('services\NomorService');

Alternatif lain yang lebih rapi adalah dengan membuat sebuah service provider yang bertugas khusus untuk mendaftarkan NomorService. Sebagai contoh, saya dapat membuat service provider seperti berikut ini:

<?php

namespace services;

use Illuminate\Support\ServiceProvider;

class NomorServiceProvider extends ServiceProvider {

    public function register() {        
        $this->app->singleton('services\NomorService', function() {
            return new NomorService();
        });     
    }   

}

?>

Kemudian, saya perlu mendaftarkan service provider di atas dengan mengubah file config\app.php dengan menambahkan baris seperti berikut ini:

'providers' => array(

    ...
    'services\NomorServiceProvider'

),

Fasilitas lain dari Laravel yang berhubungan dengan IoC adalah facade. Sebuah facade adalah cara singkat untuk memakai object yang ada dalam bentuk class yang berisi method static. Laravel memiliki banyak facade bawaan, misalnya App untuk mewakili Illuminate\Foundation\Application, DB untuk mewakili Illuminate\Database\DatabaseManager, Log untuk mewakili Illuminate\Log\Writer, dan sebagainya.

Sebagai latihan, saya akan membuat facade sehingga obyek NomorService dapat dipakai secara lebih mudah. Untuk itu, saya perlu membuat sebuah class yang diturunkan dari Facade seperti berikut ini:

<?php

namespace services;

use Illuminate\Support\Facades\Facade;

class Nomor extends Facade {

    protected static function getFacadeAccessor() {
        return 'services\NomorService';
    }

}

?>

Saya juga bisa memberikan alias pada facade ini dengan menambah nilai aliases di app/config/app.php seperti berikut ini:

'aliases' => array(
    ...
    'Nomor'           => 'services\Nomor'
),

Setelah ini, saya dapat memanggil NomorService kapan saja dan dimana saja dengan kode program seperti berikut ini:

print Nomor::getCalonNomor();

Logging Dengan MonoLog Di PHP dan Laravel

Salah satu library yang hampir selalu dipakai dalam setiap proyek Java adalah log4j. Library ini dipakai untuk mencatat pesan ke sebuah file atau output lainnya. Setiap pesan dalam memiliki level seperti fatal, error, warn, info, debug atau trace. Pembuat aplikasi dapat menentukan level yang akan ditampilkan dalam hasi log. Sebagai contoh, pada saat pengembangan aplikasi, saya sering mengatur level logging hingga ke debug sehingga supaya lebih mudah menemukan sumber kesalahan. Pada saat produksi, saya hanya memakai level error supaya aplikasi tidak terlalu sibuk mencatat log dan ukuran file log tidak terlalu besar.

Sebuah alternatif sejenis log4j untuk PHP adalah Monolog. Library ini tampaknya cukup terkenal karena framework populer di PHP seperti Symfony 2 dan Laravel 4 sudah dilengkapi dengan Monolog. Berdasarkan informasi yang ada di repository-nya, Monolog mendukung level berupa debug, info, notice, warning, error, critical, alert dan emergency. Sama seperti pada log4j, Mono juga terdiri atas beberapa class ringan seperti Logger untuk menulis log (dipanggil oleh aplikasi), beberapa implementasi handler seperti StreamHandler atau FirePHPHandler() untuk menentukan tujuan log, dan formatter seperti LineFormatter untuk menentukan format log yang dihasilkan.

Framework Laravel 4 melakukan enkapsulasi class Logger milik Monolog ke dalam class \Illuminate\Log\Writer sehingga penggunanya tidak memanggil Logger secara langsung. Class Writer tersebut menyediakan method seperti useFiles() dan useDailyFiles() yang pada dasarnya adalah shorcut (cara singkat) dari memanggil Logger->pushHandler() milik Monolog dan menyertakan StreamHandler (untuk useFiles()) atau RotatingFileHandler (untuk useDailyFiles()).

Untuk mengakses Logger milik Monolog secara langsung dari \Illuminate\Log\Writer, saya dapat memakai kode program seperti:

$monolog = Log::getMonolog();
print $monolog->isHandling($monolog::DEBUG); 

Untuk menghasilkan log di aplikasi, saya dapat langsung memanggil \Illuminate\Log\Writer melalui fascade Log, seperti pada contoh berikut ini (di app\routes.php):

Route::get('/latihan', function()
{
    Log::debug('IP ' . Request::getClientIP() . ' membuka /latihan!');
    return 'Selamat datang!';
});

akan menghasilkan baris log seperti berikut ini:

[2014-05-01 10:30:13] production.DEBUG: IP 127.0.0.1 membuka /latihan! [] []

Secara default, saya dapat menemukan hasil log di folder app/storage/logs dalam bentuk sebuah file bernama laravel.log. Lokasi ini dapat diganti dengan mengubah isi file app/start/global.php pada baris berikut ini:

/*
|--------------------------------------------------------------------------
| Application Error Logger
|--------------------------------------------------------------------------
|
| Here we will configure the error logger setup for the application which
| is built on top of the wonderful Monolog library. By default we will
| build a basic log file setup which creates a single file for logs.
|
*/

Log::useFiles(storage_path().'/logs/laravel.log')

Bagaimana bila saya ingin memiliki format yang berbeda di hasil log? Karena useFiles() tidak menyediakan pilihan tersebut, maka saya dapat mengakses Monolog secara langsung, seperti yang terlihat pada contoh kode program berikut ini:

Log::useFiles(storage_path().'/logs/laravel.log');
$handler = Log::getMonolog()->popHandler();
$formatBaru = new MonologFormatterLineFormatter(
    "%datetime% [%level_name%] %message%n",
    "d-m-Y H:i:s"
);
$handler->setFormatter($formatBaru);
Log::getMonolog()->pushHandler($handler);

Kode program di atas akan menghasilkan log seperti:

01-05-2014 10:44:25 [DEBUG] IP 127.0.0.1 membuka URL /latihan!

Atau, bila saya ingin menghasilkan log dalam output berupa HTML, saya bisa mengubah kode program di atas menjadi berikut ini:

Log::useFiles(storage_path().'/logs/laravel.log');
$handler = Log::getMonolog()->popHandler();
$formatBaru = new MonologFormatterHTMLFormatter("d-m-Y H:i:s");
$handler->setFormatter($formatBaru);
Log::getMonolog()->pushHandler($handler)

Kode program di atas akan menghasilkan log seperti:

<h1 style="background: #cccccc;color: #ffffff;padding: 5px;">DEBUG</h1><table cellspacing="1" width="100%"><tr style="padding: 4px;spacing: 0;text-align: left;">
<th style="background: #cccccc" width="100px">Message:</th>
<td style="padding: 4px;spacing: 0;text-align: left;background: #eeeeee"><pre>IP 127.0.0.1 membuka URL /latihan!</pre></td>
</tr><tr style="padding: 4px;spacing: 0;text-align: left;">
<th style="background: #cccccc" width="100px">Time:</th>
<td style="padding: 4px;spacing: 0;text-align: left;background: #eeeeee"><pre>2014-05-01T10:45:57.733039+0000</pre></td>
</tr><tr style="padding: 4px;spacing: 0;text-align: left;">
<th style="background: #cccccc" width="100px">Channel:</th>
<td style="padding: 4px;spacing: 0;text-align: left;background: #eeeeee"><pre>production</pre></td>
</tr></table>

Memakai Composer Untuk Mengelola Ketergantungan Proyek Di PHP

Pada sebuah sesi interview, saya sebagai the Java guy ditanya mengenai kesediaan untuk terlibat dalam proyek web yang dikembangkan dengan memakai PHP dan framework Laravel. Setelah interview selesai, karena rasa penasaran, saya mencoba mencari tahu seperti apa teknologi yang berkaitan dengan PHP mengingat saya jarang memakai ‘pemanis’ di PHP. Sebuah fakta menarik yang saya jumpai adalah banyak framework dan library PHP dapat disertakan ke dalam proyek melalui Composer.

Saya melihat Composer sebagai sebuah dependency manager sama halnya seperti (atau lebih terbatas) Maven atau Ivy di Java. Pengguna menentukan apa saja library yang dibutuhkan dalam proyek mereka dan Composer akan men-download seluruh library yang dibutuhkan secara otomatis (beserta ketergantungannya bila ada). Pada Java, sebuah unit yang dapat disertakan ke dalam proyek lain adalah sebuah file JAR. Pada Composer, unit tersebut, disebut sebagai package, berada dalam bentuk kumpulan file PHP dalam sebuah folder yang sudah memiliki metadata. Agar dapat di-download oleh berbagai pengguna, setiap pembuat package harus meletakkan hasil karya mereka dalam sebuah repository. Repository resmi untuk Composer (main repository) adalah https://packagist.org. Hal ini sama seperti Maven Central Repository yang berisi banyak (hampir semua) library open source Java di http://search.maven.org. Bedanya, Composer dapat memakai VCS seperti GitHub sebagai repository selama mereka memiliki file composer.json.

Bila pada proyek Java yang memakai Maven, saya mendefinisikan ketergantungan proyek pada file pom.xml, maka pada Composer, saya mendefinisikan ketergantungan proyek pada file yang disebut composer.json. Sebagai contoh, bila saya ingin memakai Twig pada proyek, maka saya dapat membuat file composer.json dengan isi seperti berikut ini:

{
  "require": {
     "twig/twig": "1.15.1"
  }
}

Setiap package Composer memiliki nama pengenal yang terdiri atas nama vendor dan nama proyek seperti twig/twig, symfony/console, laravel/framework dan sebagainya. Setiap package dapat memiliki satu atau lebih versi. Composer juga mendukung wildcard pada versi seperti 1.*.

Setelah membuat file composer.json pada direktori proyek, saya perlu memberikan perintah composer install untuk men-download file yang dibutuhkan, seperti yang terlihat pada gambar berikut ini:

Men-download package yang dibutuhkan secara otomatis

Men-download package yang dibutuhkan secara otomatis

Struktur folder yang dihasilkan akan terlihat seperti berikut ini:

Latihan
\---vendor
    \---composer
    \---twig
    +---autoload.php
+---composer.json
+---composer.lock

Composer menghasilkan sebuah file composer.lock dan sebuah folder bernama vendor yang berisi package yang dibutuhkan oleh proyek. Selain itu, pada folder vendor, terdapat file autoload.php yang dapat dipakai untuk men-include packages yang ada secara mudah.

Bila saya melakukan perubahan dependency pada file composer.json, maka saya perlu memberikan perintah composer update untuk memperbaharui isi folder vendor.

Bagaimana bila saya ingin memakai Composer secara offline, misalnya saat tidak terhubung ke internet? Composer sebenarnya menyimpan file yang pernah di-download ke dalam sebuah cache sehingga ia masih bisa bekerja secara offline. Untuk mengetahui lokasi cache, saya dapat memberikan perintah composer config --list dan melihat isi cache-dir seperti berikut ini:

C:\>composer config --list
...
[cache-dir] C:/Users/Me/AppData/Local/Composer
[cache-files-dir] {$cache-dir}/files (C:/Users/Me/AppData/Local/Composer/files)
[cache-repo-dir] {$cache-dir}/repo (C:/Users/Me/AppData/Local/Composer/repo)
[cache-vcs-dir] {$cache-dir}/vcs (C:/Users/Me/AppData/Local/Composer/vcs)
...
[home] C:/Users/Me/AppData/Roaming/Composer

Composer juga memiliki perintah create-project yang pada dasarnya akan men-clone source dari package yang diberikan. Hal ini tampak aneh karena jarang saya temui di Java. Perintah create-project tidak banyak berguna untuk library seperti Twig, tapi ia sangat berguna bagi package yang berperan sebagai skeleton. Sebagai contoh, package laravel/laravel (bisa dilihat di https://github.com/laravel/laravel) mengandung folder seperti app/controllers, app/models, app/views, public dan sebagainya yang tinggal diubah atau ditambah oleh pengguna. Hal ini berbeda dengan package laravel/framework yang berisi library atau kode program framework Laravel itu sendiri.

Sebagai contoh, untuk membuat sebuah proyek baru yang memakai framework Laravel, saya dapat memberikan perintah seperti pada gambar berikut ini:

Memakai create-package di Composer

Memakai create-package di Composer

Melakukan Profiling Dengan Zend Studio

Profiling adalah sebuah aktifitas untuk mengukur kode program, seperti waktu eksekusi sebuah function atau jumlah pemanggilan sebuah function. Dengan melakukan profiling, seorang programmer akan mengetahui bagian mana dari kode programnya yang lambat atau bagian mana yang lebih sering dipanggil. Zend Studio dilengkapi dengan profiler untuk situs PHP. Syarat untuk menggunakan profiler tersebut adalah server yang men-host aplikasi PHP harus memiliki extension Zend Debugger. Karena saya memakai Zend Server CE (versi gratis), maka Zend Debugger sudah ter-install dan aktif.

Untuk melakukan profiling dari Zend Studio, saya dapat men-klik kanan sebuah halaman PHP, lalu memilih Profile As, PHP Web Application. Sebagai alternatif lainnya, saya dapat memilih menu Run, Profile URL dan memasukkan URL yang merujuk ke halaman PHP yang akan diperiksa. Selain itu, bila saya men-install Zend Studio Toolbar di browser, maka saya dapat langsung men-klik tombol Profile saat mengunjungi sebuah halaman PHP seperti yang terlihat pada gambar berikut ini:

Melakukan profiling dari browser

Melakukan profiling dari browser

Setelah itu, Zend Studio akan menampilkan konfirmasi untuk beralih ke PHH Profile perspective. Bagi yang belum terbiasa dengan Eclipse, sebuah perspective adalah kumpulan beberapa window dengan lokasi tertentu yang biasanya dipakai untuk keperluan tertentu. Perspective yang biasa dipakai untuk mengembangkan kode program adalah PHP perspective. Untuk melakukan debugging, terdapat Debug perspective. Pengguna dapat berganti perspective dengan men-klik icon perspective yang diinginkan di sisi kanan atas Eclipse, atau memilih menu Window, Open Perspective. Bagi yang terbiasa dengan produk Adobe, perspective di Eclipse mirip seperti workspace di Adobe. Sebagai contoh, pada Adobe Photoshop CS5, saya dapat memilih workspace seperti Essentials, Design, Painting, 3D dan sebagainya. Jadi, jangan lupa bila ingin kembali coding, ganti perspective dari PHP Profile persepective menjadi PHP perspective.

Pada Profile Information, saya dapat menemukan grafis pie yang menunjukkan waktu eksekusi kode program, seperti yang terlihat pada gambar berikut ini:

Diagram pie untuk waktu ekseksusi

Diagram pie untuk waktu ekseksusi

Kode program yang saya profile adalah sebuah halaman PHP sederhana yang mengambil data dari database dan menampilkannya pada view dengan menggunakan Twig. Grafis di atas memperlihatkan bahwa Autoloader.php (untuk loading class yang dibutuhkan Twig) dan Database.php (melakukan query database) membutuhkan waktu paling lama.

Saya dapat menemukan informasi lebih detail di Execution Statistics seperti yang terlihat pada gambar berikut ini:

Tabel untuk execution statistics

Tabel untuk execution statistics

Sebagai contoh, gambar di atas memperlihatkan bahwa function autoload() di Autoloader.php dikerjakan sebanyak 70 kali. Saya dapat melihat informasi lebih detail lagi dengan men-klik kanan function tersebut dan memilih Open function invocation statistics. Zend Studio profiler akan menampilkan informasi seperti yang terlihat pada gambar berikut ini:

Tabel untuk invocation statistics

Tabel untuk invocation statistics

Bagian Selected function is invoked by: pada tab Function Invocation Statistics akan memperlihatkan siapa saja yang mengerjakan function autoload(). Sebagai informasi, ini adalah fungsi yang akan dikerjakan setiap kali terdapat penggunaan sebuah class baru yang belum dikenali. autoload() bertugas men-include file yang mewakili deklarasi class tersebut. Saya dapat memperoleh informasi lebih lanjut class apa saja yang dibuat dengan memeriksa bagian Selected function invokes: seperti yang terlihat pada gambar berikut ini:

Tabel untuk invoke statistics

Tabel untuk invoke statistics

Pada tab Execution Flow, saya dapat melihat statistik function dimana setiap function diurutkan berdasarkan urutan eksekusi, seperti yang terlihat pada gambar berikut ini:

Tabel untuk execution flow

Tabel untuk execution flow