Memakai Reflection API Di PHP


Reflection adalah kode program yang membaca struktur sebuah class dan memanipulasinya bila perlu. Pembuat framework atau library biasanya berusaha membuat kode program yang dapat dipakai se-fleksibel mungkin. Oleh sebab itu, mereka sering kali perlu menjawab pertanyaan seperti: apa nama class ini? ada property atau method yang ada di class ini? Reflection API dirancang untuk menjawab pertanyaan tersebut. Pada Java, saya dapat memakai reflection dengan menggunakan sebuah instance class bernama Class yang diperoleh dengan method getClass(). PHP juga menyediakan reflection API. Informasi mengenai reflection di PHP dapat dibaca di http://www.php.net/manual/en/intro.reflection.php.

Sebagai latihan, kali ini saya akan menerapkan reflection API untuk membuat kode program di artikel Memahami Overloading ala PHP. Saat ini, kode program di artikel tersebut hanya dapat diterapkan di domain class Pelanggan saja. Saya akan membuatnya menjadi sesuatu yang lebih fleksibel yang dapat diterapkan pada seluruh domain class yang ada.

Saya akan mulai dari method save(). Agar method save() menjadi fleksibel dan dapat dipakai di domain class lain, saya mengubahnya menjadi seperti berikut ini:

...
public function save() {
   $class = new \ReflectionClass(get_class());
   $namaTabel = $class->getShortName();
   $props = $class->getProperties(\ReflectionProperty::IS_PUBLIC);

   $daftarKolom = implode(',', array_map(function($prop) { 
      return $prop->name; 
   }, $props));

   $daftarParameter = implode(',', array_fill(0, sizeof($props), '?'));

   $daftarValue = array_map(function($prop) { 
      return $prop->getValue($this); 
   }, $props); 

   $db = new \PDO('mysql:host=localhost;dbname=exercises', 'snake', 'password');
   $st = $db->prepare("INSERT INTO $namaTabel($daftarKolom) VALUES ($daftarParameter)");     
   $st->execute($daftarValue);                           
}
...

Pada kode program di atas, saya membuat sebuah instance ReflectionClass yang merupakan pusat operasi reflection API. Sebagai contoh, saya bisa memperoleh nama class (tanpa namespace) dengan memanggil method getShortName() miliknya. Saya juga bisa memperoleh seluruh property untuk class dengan memanggil method getProperties() yang mengembalikan array yang berisi instance dari class ReflectionProperty. Bukan hanya itu, saya dapat memperoleh nilai sebuah property dengan memanggil method getValue() dari ReflectionProperty.

Berikutnya, saya juga mengubah method __callStatic() agar memakai reflection API, seperti yang terlihat pada kode program berikut ini:

...
public static function __callStatic($name, $args) {
   if (preg_match('/findBy([A-Z]\w*)/', $name, $matches)==1) {

      $class = new \ReflectionClass(get_class());
      $namaTabel = $class->getShortName();

      // Melakukan parsing nama method
      $expr = $matches[1];
      $fields = preg_split("/(And|Or)/", $expr, NULL, PREG_SPLIT_DELIM_CAPTURE);
      $sql = "SELECT * FROM $namaTabel WHERE ";
      for ($i=0; $i<sizeof($fields); $i++) {                
         $sql .= ' ' . $fields[$i] . ' = ? ';
         if (++$i < sizeof($fields) - 1) {
            $sql .= ' ' . $fields[$i];
         }  
      }

      // Melakukan query ke database
      $db = new \PDO('mysql:host=localhost;dbname=exercises', 'snake', 'password');
      $st = $db->prepare($sql);
      $st->execute($args);
      return $st->fetchAll(\PDO::FETCH_CLASS, $class->getName());                    
   }

   trigger_error("Method $name tidak ditemukan!", E_USER_ERROR);  
}
...

Agar lebih rapi, saya akan memindahkan kode program yang berkaitan dengan database ke sebuah class tersendiri. Untuk itu, saya membuat sebuah file baru dengan nama Database.php di folder util yang isinya seperti berikut ini:

<?php

namespace util;

class Database {

   private static $instance;

   public static function getInstance() {
      if (is_null(self::$instance)) {
         self::$instance = new Database();
         self::$instance->pdo = new \PDO('mysql:host=localhost;dbname=exercises', 'snake', 'password');
      }
      return self::$instance;
   }

   /**
    * Instance dari PDO untuk melakukan operasi database.
    * 
    * @var \PDO
    */
   public $pdo;

   private function __construct() {}

   public function execute($sql, $args = NULL) {
      $st = $this->pdo->prepare($sql);    
      $st->execute($args);    
   }

   public function executeFetchAll($sql, $class, $args = NULL) {
      $st = $this->pdo->prepare($sql);
      $st->execute($args);                   
      return $st->fetchAll(\PDO::FETCH_CLASS, $class);

   }

}

?>

Dengan demikian, saya bisa menyederhanakan kode program di Pelanggan.php menjadi seperti berikut ini:

<?php

namespace domain;

use util\Database;

class Pelanggan {

   ...

   public function save() {
      $class = new \ReflectionClass(get_class());
      $namaTabel = $class->getShortName();
      $props = $class->getProperties(\ReflectionProperty::IS_PUBLIC);

      $daftarKolom = implode(',', array_map(function($prop) { 
         return $prop->name; 
      }, $props));

      $daftarParameter = implode(',', array_fill(0, sizeof($props), '?'));

      $daftarValue = array_map(function($prop) { 
         return $prop->getValue($this); 
      }, $props); 

      Database::getInstance()->execute(
         "INSERT INTO $namaTabel($daftarKolom) VALUES ($daftarParameter)", 
         $daftarValue);    
   }

   public static function __callStatic($name, $args) {
      if (preg_match('/findBy([A-Z]\w*)/', $name, $matches)==1) {

         $class = new \ReflectionClass(get_class());
         $namaTabel = $class->getShortName();

         // Melakukan parsing nama method
         $expr = $matches[1];
         $fields = preg_split("/(And|Or)/", $expr, NULL, PREG_SPLIT_DELIM_CAPTURE);
         $sql = "SELECT * FROM $namaTabel WHERE ";
         for ($i=0; $i<sizeof($fields); $i++) {                
            $sql .= ' ' . $fields[$i] . ' = ? ';
            if (++$i < sizeof($fields) - 1) {
               $sql .= ' ' . $fields[$i];
            }  
         }

         // Melakukan query ke database
         return Database::getInstance()->executeFetchAll($sql, 
            $class->getName(), $args);
      }

      trigger_error("Method $name tidak ditemukan!", E_USER_ERROR);  
   }  

}

?>

Pertanyaan berikutnya: kode program sudah memakai reflection sehingga bersifat fleksibel, tapi bukankah setiap kali menambah domain class baru tetap harus men-copy paste kode program save() dan __callStatic() ke class baru tersebut? Untuk mengatasi masalah ini, saya dapat menggunakan salah satu fitur OOP yaitu inheritance. Sebuah class yang diturunkan dari class lain secara otomatis akan memiliki seluruh fitur dari class parent-nya. Dengan demikian, saya hanya perlu memastikan seluruh domain class diturunkan dari class DomainObject (pada file DomainObject.php) yang isinya seperti berikut ini:

<?php

namespace domain;

use util\Database;

abstract class DomainObject {

   public function save() {
      $class = new \ReflectionClass(get_class($this));
      $namaTabel = $class->getShortName();
      $props = $class->getProperties(\ReflectionProperty::IS_PUBLIC);

      $daftarKolom = implode(',', array_map(function($prop) {
         return $prop->name;
      }, $props));

      $daftarParameter = implode(',', array_fill(0, sizeof($props), '?'));

      $daftarValue = array_map(function($prop) {
         return $prop->getValue($this);
      }, $props);

      Database::getInstance()->execute(
         "INSERT INTO $namaTabel($daftarKolom) VALUES ($daftarParameter)",
         $daftarValue);
   }

   public static function __callStatic($name, $args) {
      if (preg_match('/findBy([A-Z]\w*)/', $name, $matches)==1) {

         $class = new \ReflectionClass(get_called_class());
         $namaTabel = $class->getShortName();

         // Melakukan parsing nama method
         $expr = $matches[1];
         $fields = preg_split("/(And|Or)/", $expr, NULL, PREG_SPLIT_DELIM_CAPTURE);
         $sql = "SELECT * FROM $namaTabel WHERE ";
         for ($i=0; $i<sizeof($fields); $i++) {
            $sql .= ' ' . $fields[$i] . ' = ? ';
            if (++$i < sizeof($fields) - 1) {
               $sql .= ' ' . $fields[$i];
            }
         }

         // Melakukan query ke database
         return Database::getInstance()->executeFetchAll($sql,
               $class->getName(), $args);
      }

      trigger_error("Method $name tidak ditemukan!", E_USER_ERROR);
   }

}

?>

Perhatikan bahwa saya mengubah get_class() menjadi get_class($this) agar bisa memperoleh reflection dari subclass (class turunan dari DomainObject). Karena method __callStatic() bersifat static, maka saya tidak dapat menggunakan get_class(). Sebagai alternatifnya, saya menggunakan get_called_class() yang diperkenalkan sejak PHP 5.3.

Selain itu, saya juga memberikan keyword abstract pada class DomainObject karena class ini tidak boleh diciptakan (dibuat instance-nya). Ia wajib harus dipakai melalui turunannya, seperti Pelanggan, Produk, Order, dan sebagainya.

Sekarang, saya dapat mengubah class Pelanggan agar diturunkan dari DomainObject dengan memakai keyword extends seperti pada kode program berikut ini:

<?php

namespace domain;

class Pelanggan extends DomainObject {

   public $nama;

   public $alamat;

   public $usia;     

   function __construct($nama = NULL, $alamat = NULL, $usia = NULL) {
      if ($nama) $this->nama = $nama;
      if ($alamat) $this->alamat = $alamat;
      if ($usia) $this->usia = $usia;
   }

}

?>

Bila saya menjalankan aplikasi PHP ini, method save() dan finders tetap bekerja dengan baik. Sekarang, saya juga dapat menambah domain model baru dengan mudah. Sebagai contoh, saya akan membuat sebuah class baru bernama Produk di file \domain\Produk.php yang isinya seperti berikut ini:

<?php

namespace domain;

class Produk extends DomainObject {

   public $kode;

   public $nama;

   public $harga;

   function __construct($kode = NULL, $nama = NULL, $harga = NULL) {
      if ($kode) $this->kode= $kode;
      if ($nama) $this->nama = $nama;
      if ($harga) $this->harga = $harga;
   }

}

?>

Setelah itu, saya membuat sebuah tabel dengan SQL seperti berikut ini:

CREATE TABLE produk (
   kode CHAR(7) PRIMARY KEY,
   nama VARCHAR(100) NOT NULL,
   harga INT NOT NULL
);

Berkat inheritance, class Produk secara otomatis memiliki method save() dan finders. Saya dapat membuktikannya dengan kode program seperti berikut ini:

<?php

   spl_autoload_extensions('.php');
   spl_autoload_register();

   use domain\Pelanggan;
   use domain\Produk;

   $snake = new Pelanggan("Solid Snake", "Alaska", 35);
   $snake->save();
   $liquid = new Pelanggan("Liquid Snake", "New York", 35);
   $liquid->save();
   $boss = new Pelanggan("The Boss", "Alaska", 40);
   $boss->save();

   $produk1 = new Produk("PR-0001", "Produk #1", 100000);
   $produk1->save();
   $produk2 = new Produk("PR-0002", "Produk #2", 150000);
   $produk2->save();

?>

<pre>
<?php

   print "Daftar pelanggan dengan nama Solid Snake di Alaska:\n";
   print_r(Pelanggan::findByNamaAndAlamat("Solid Snake", "Alaska"));

   print "\nDaftar pelanggan dengan usia 35 tahun:\n";
   print_r(Pelanggan::findByUsia(35));

   print "\nDaftar pelanggan yang tinggal di Alaska:\n";
   print_r(Pelanggan::findByAlamat("Alaska"));

   print "\nDaftar Produk Dengan Kode PR-0001:\n";
   print_r(Produk::findByKode("PR-0001"));

   print "\nDaftar Produk Dengan Nama 'Produk #2':\n";
   print_r(Produk::findByNama("Produk #2"));

?> 
</pre>   

Kode program di atas akan menyisipkan beberapa record ke tabel Pelanggan dan Produk, serta melakukan query ke tabel tersebut. Reflection API berperan penting membantu saya agar tidak perlu men-hard code operasi SQL ke dalam masing-masing class. Reflection API memungkinkan saya untuk menghasilkan kode program yang fleksibel dan reusable.

Perihal Solid Snake
I'm nothing...

3 Responses to Memakai Reflection API Di PHP

  1. Ping-balik: Memakai Traits Di PHP 5.4 | The Solid Snake

  2. Faisal Burhanudin mengatakan:

    pusing pak ,hahahaha

  3. 陳大衛 mengatakan:

    nice pak…😀 framework java seperti spring dan weld full pake reflection api juga

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: