Membangun REST API untuk Proyek PHP Lama
Diterbitkan: 2022-03-11Membangun atau merancang REST API bukanlah tugas yang mudah, terutama ketika Anda harus melakukannya untuk proyek PHP lama. Ada banyak perpustakaan pihak ke-3 saat ini yang memudahkan penerapan REST API, tetapi mengintegrasikannya ke dalam basis kode lama yang ada bisa jadi agak menakutkan. Dan, Anda tidak selalu memiliki kemewahan untuk bekerja dengan kerangka kerja modern, seperti Laravel dan Symfony. Dengan proyek PHP lawas, Anda sering kali menemukan diri Anda berada di tengah-tengah kerangka kerja internal yang tidak digunakan lagi, berjalan di atas versi PHP lama.
Dalam artikel ini, kita akan melihat beberapa tantangan umum dalam mencoba mengimplementasikan REST API dari awal, beberapa cara untuk mengatasi masalah tersebut dan strategi keseluruhan untuk membangun server API berbasis PHP khusus untuk proyek lama.
Meskipun artikel ini didasarkan pada PHP 5.3 dan di atasnya, konsep inti berlaku untuk semua versi PHP di luar versi 5.0, dan bahkan dapat diterapkan untuk proyek non-PHP. Di sini, kami tidak akan membahas apa itu REST API secara umum, jadi jika Anda tidak terbiasa, pastikan untuk membacanya terlebih dahulu.
Untuk memudahkan Anda mengikutinya, berikut adalah daftar beberapa istilah yang digunakan di seluruh artikel ini dan artinya:
- Server API: aplikasi REST utama yang melayani API, dalam hal ini, ditulis dalam PHP.
- Titik akhir API: "metode" backend yang digunakan klien untuk berkomunikasi untuk melakukan tindakan dan menghasilkan hasil.
- URL titik akhir API: URL yang melaluinya sistem backend dapat diakses oleh dunia.
- Token API: pengidentifikasi unik yang diteruskan melalui header HTTP atau cookie dari mana pengguna dapat diidentifikasi.
- Aplikasi: aplikasi klien yang akan berkomunikasi dengan aplikasi REST melalui titik akhir API. Pada artikel ini kita akan berasumsi bahwa itu berbasis web (baik desktop atau seluler), dan karena itu ditulis dalam JavaScript.
Langkah Awal
Pola jalan
Salah satu hal pertama yang perlu kita putuskan adalah di jalur URL mana titik akhir API akan tersedia. Ada 2 cara populer:
- Buat subdomain baru, seperti api.example.com.
- Buat jalur, seperti example.com/api.
Sepintas mungkin terlihat varian pertama lebih populer dan atraktif. Namun, pada kenyataannya, jika Anda membuat API khusus proyek, akan lebih tepat untuk memilih varian kedua.
Salah satu alasan terpenting di balik mengambil pendekatan kedua adalah bahwa ini memungkinkan cookie digunakan sebagai sarana untuk mentransfer kredensial. Klien berbasis browser akan secara otomatis mengirim cookie yang sesuai dalam permintaan XHR, menghilangkan kebutuhan header otorisasi tambahan.
Alasan penting lainnya adalah Anda tidak perlu melakukan apa pun terkait konfigurasi subdomain atau masalah manajemen di mana header kustom mungkin dihapus oleh beberapa server proxy. Ini bisa menjadi cobaan yang membosankan dalam proyek-proyek warisan.
Menggunakan cookie dapat dianggap sebagai praktik "tidak tenang" karena permintaan REST harus tanpa kewarganegaraan. Dalam hal ini kami dapat membuat kompromi dan meneruskan nilai token dalam cookie alih-alih meneruskannya melalui header khusus. Secara efektif kami menggunakan cookie hanya sebagai cara untuk meneruskan nilai token alih-alih session_id secara langsung. Pendekatan ini dapat dianggap tanpa kewarganegaraan, tetapi kami dapat menyerahkannya kepada preferensi Anda.
URL titik akhir API juga dapat diversi. Selain itu, mereka dapat menyertakan format respons yang diharapkan sebagai ekstensi dalam nama jalur. Meskipun ini tidak kritis, terutama selama tahap awal pengembangan API, dalam jangka panjang detail ini pasti dapat membuahkan hasil. Terutama ketika Anda perlu menerapkan fitur baru. Dengan memeriksa versi mana yang diharapkan klien dan menyediakan format yang diperlukan untuk kompatibilitas mundur dapat menjadi solusi terbaik.
Struktur URL titik akhir API dapat terlihat sebagai berikut:
example.com/api/${version_code}/${actual_request_path}.${format}
Dan, contoh nyata:
example.com/api/v1.0/records.json
Rute
Setelah memilih URL dasar untuk titik akhir API, hal berikutnya yang perlu kita lakukan adalah memikirkan sistem perutean kita. Itu bisa diintegrasikan ke dalam kerangka kerja yang ada, tetapi jika itu terlalu rumit, solusi potensial adalah membuat folder bernama "api" di root dokumen. Dengan begitu API dapat memiliki logika yang benar-benar terpisah. Anda dapat memperluas pendekatan ini dengan menempatkan logika API ke dalam filenya sendiri, seperti ini:
Anda dapat menganggap "www/api/Apis/Users.php" sebagai "pengontrol" terpisah untuk titik akhir API tertentu. Akan sangat bagus untuk menggunakan kembali implementasi dari basis kode yang ada, misalnya menggunakan kembali model yang sudah diimplementasikan dalam proyek untuk berkomunikasi dengan database.
Terakhir, pastikan untuk mengarahkan semua permintaan yang masuk dari “/api/*” ke “/api/index.php”. Ini dapat dilakukan dengan mengubah konfigurasi server web Anda.
Kelas API
Versi dan Format
Anda harus selalu mendefinisikan dengan jelas versi dan format yang diterima oleh titik akhir API Anda dan apa yang default. Ini akan memungkinkan Anda membangun fitur baru di masa mendatang sambil mempertahankan fungsionalitas lama. Versi API pada dasarnya dapat berupa string tetapi Anda dapat menggunakan nilai numerik untuk pemahaman dan komparabilitas yang lebih baik. Sebaiknya memiliki angka cadangan untuk versi minor karena akan dengan jelas menunjukkan bahwa hanya beberapa hal yang berbeda:
- v1.0 berarti versi pertama.
- v1.1 versi pertama dengan beberapa perubahan kecil.
- v2.0 akan menjadi versi yang benar-benar baru.
Format dapat berupa apa saja yang dibutuhkan klien Anda termasuk namun tidak terbatas pada JSON, XML, dan bahkan CSV. Dengan menyediakannya melalui URL sebagai ekstensi file, url titik akhir API memastikan keterbacaan dan menjadi mudah bagi konsumen API untuk mengetahui format apa yang dapat mereka harapkan:
- "/api/v1.0/records.json" akan mengembalikan array catatan JSON
- "/api/v1.0/records.xml" akan mengembalikan file XML catatan
Perlu ditunjukkan bahwa Anda juga perlu mengirim header Tipe-Konten yang tepat sebagai respons untuk setiap format ini.
Setelah menerima permintaan masuk, salah satu hal pertama yang harus Anda lakukan adalah memeriksa apakah server API mendukung versi dan format yang diminta. Dalam metode utama Anda, yang menangani permintaan masuk, parsing $_SERVER['PATH_INFO'] atau $_SERVER['REQUEST_URI'] untuk menentukan apakah format dan versi yang diminta didukung. Kemudian, lanjutkan atau kembalikan respons 4xx (mis. 406 “Tidak Dapat Diterima”). Bagian terpenting di sini adalah selalu mengembalikan sesuatu yang diharapkan klien. Alternatif untuk ini adalah dengan memeriksa tajuk permintaan "Terima" alih-alih ekstensi jalur URL.
Rute yang Diizinkan
Anda dapat meneruskan semuanya secara transparan ke pengontrol API Anda, tetapi mungkin lebih baik menggunakan kumpulan rute yang diizinkan dalam daftar putih. Ini akan sedikit mengurangi fleksibilitas tetapi akan memberikan wawasan yang sangat jelas tentang seperti apa tampilan URL titik akhir API saat Anda kembali ke kode berikutnya.
private $public_routes = array( 'system' => array( 'regex' => 'system', ), 'records' => array( 'regex' => 'records(?:/?([0-9]+)?)', ), );
Anda juga dapat memindahkan ini ke file terpisah untuk membuat semuanya lebih bersih. Konfigurasi di atas akan digunakan untuk mengaktifkan permintaan ke URL berikut:
/api/v1.0/system.json /api/v1.0/records.json /api/v1.0/records/7.json
Menangani Data PUT
PHP secara otomatis menangani data POST yang masuk dan menempatkannya di bawah $_POST superglobal. Namun, tidak demikian dengan permintaan PUT. Semua data "dikubur" ke dalam php://input . Jangan lupa untuk menguraikannya ke dalam struktur atau larik terpisah sebelum menjalankan metode API yang sebenarnya. Sebuah parse_str sederhana mungkin sudah cukup, tetapi jika klien mengirimkan permintaan multi-bagian, penguraian tambahan mungkin diperlukan untuk menangani batas-batas formulir. Kasus penggunaan umum dari permintaan multi-bagian termasuk unggahan file. Mendeteksi dan menangani permintaan multipart dapat dilakukan sebagai berikut:
self::$input = file_get_contents('php://input'); // For PUT/DELETE there is input data instead of request variables if (!empty(self::$input)) { preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); if (isset($matches[1]) && strpos(self::$input, $matches[1]) !== false) { $this->parse_raw_request(self::$input, self::$input_data); } else { parse_str(self::$input, self::$input_data); } }
Di sini, parse_raw_request dapat diimplementasikan sebagai:
/** * Helper method to parse raw requests */ private function parse_raw_request($input, &$a_data) { // grab multipart boundary from content type header preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); $boundary = $matches[1]; // split content by boundary and get rid of last -- element $a_blocks = preg_split("/-+$boundary/", $input); array_pop($a_blocks); // loop data blocks foreach ($a_blocks as $id => $block) { if (empty($block)) { continue; } // parse uploaded files if (strpos($block, 'application/octet-stream') !== false) { // match "name", then everything after "stream" (optional) except for prepending newlines preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches); // parse all other fields } else { // match "name" and optional value in between newline sequences preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches); } $a_data[$matches[1]] = $matches[2]; } }
Dengan ini, kita dapat memiliki muatan permintaan yang diperlukan di Api::$input sebagai input mentah dan Api::$input_data sebagai array asosiatif.

Memalsukan PUT/DELETE
Terkadang Anda dapat melihat diri Anda dalam situasi di mana server tidak mendukung apa pun selain metode HTTP GET/POST standar. Solusi umum untuk masalah ini adalah "memalsukan" PUT/DELETE atau metode permintaan khusus lainnya. Untuk itu Anda bisa menggunakan parameter “magic”, seperti “_method”. Jika Anda melihatnya di array $_REQUEST Anda, anggap saja permintaan itu dari jenis yang ditentukan. Kerangka kerja modern seperti Laravel memiliki fungsionalitas seperti itu di dalamnya. Ini memberikan kompatibilitas yang luar biasa jika server atau klien Anda memiliki keterbatasan (misalnya seseorang menggunakan jaringan Wi-Fi pekerjaannya di belakang proxy perusahaan yang tidak mengizinkan permintaan PUT.)
Meneruskan ke API Tertentu
Jika Anda tidak memiliki kemewahan untuk menggunakan kembali autoloader proyek yang ada, Anda dapat membuatnya sendiri dengan bantuan fungsi spl_autoload_register . Tentukan di halaman "api/index.php" Anda dan panggil kelas API Anda yang terletak di "api/Api.php". Kelas API bertindak sebagai middleware dan memanggil metode yang sebenarnya. Misalnya, permintaan ke "/api/v1.0/records/7.json" harus berakhir dengan memanggil metode GET "Apis/Records.php" dengan parameter 7. Ini akan memastikan pemisahan masalah dan menyediakan cara untuk menjaga pembersih logika. Tentu saja, jika memungkinkan untuk mengintegrasikan ini lebih dalam ke dalam kerangka kerja yang Anda gunakan dan menggunakan kembali pengontrol atau rute spesifiknya, Anda harus mempertimbangkan kemungkinan itu juga.
Contoh "api/index.php" dengan autoloader primitif:
<?php // Let's define very primitive autoloader spl_autoload_register(function($classname){ $classname = str_replace('Api_', 'Apis/', $classname); if (file_exists(__DIR__.'/'.$classname.'.php')) { require __DIR__.'/'.$classname.'.php'; } }); // Our main method to handle request Api::serve();
Ini akan memuat kelas Api kami dan mulai menyajikannya secara independen dari proyek utama.
OPSI Permintaan
Saat klien menggunakan tajuk khusus untuk meneruskan token uniknya, browser pertama-tama perlu memeriksa kapan pun server mendukung tajuk itu. Di situlah permintaan OPTIONS masuk. Tujuannya adalah untuk memastikan bahwa semuanya baik-baik saja dan aman untuk klien dan server API. Jadi permintaan OPTIONS dapat diaktifkan setiap kali klien mencoba melakukan sesuatu. Namun, ketika klien menggunakan cookie untuk kredensial, ini akan menyelamatkan browser dari keharusan mengirim permintaan OPSI tambahan ini.
Jika klien meminta POST /users/8.json dengan cookie, permintaannya akan cukup standar:
- Aplikasi melakukan permintaan POST ke /users/8.json.
- Browser melakukan permintaan dan menerima tanggapan.
Tetapi dengan otorisasi khusus atau tajuk token:
- Aplikasi melakukan permintaan POST ke /users/8.json.
- Browser berhenti memproses permintaan dan memulai permintaan OPTIONS sebagai gantinya.
- Permintaan OPTIONS dikirim ke /users/8.json.
- Browser menerima respons dengan daftar semua metode dan header yang tersedia, seperti yang ditentukan oleh API.
- Browser melanjutkan dengan permintaan POST asli hanya jika header kustom ada dalam daftar header yang tersedia.
Namun, perlu diingat bahwa meskipun menggunakan cookie, dengan PUT/DELETE Anda mungkin masih menerima permintaan OPTIONS tambahan tersebut. Jadi bersiaplah untuk menanggapinya.
Catatan API
Struktur dasar
Contoh Records API kami cukup mudah. Ini akan berisi semua metode permintaan dan mengembalikan output ke kelas API utama yang sama. Sebagai contoh:
<?php class Api_Records { public function __construct() { // In here you could initialize some shared logic between this API and rest of the project } /** * Get individual record or records list */ public function get($id = null) { if ($id) { return $this->getRecord(intval($id)); } else { return $this->getRecords(); } } /** * Update record */ public function put($record_id = null) { // In real world there would be call to model with validation and probably token checking // Use Api::$input_data to update return Api::responseOk('OK', array()); } // ...
Jadi mendefinisikan setiap metode HTTP akan memungkinkan kita membangun API dalam gaya REST dengan lebih mudah.
Memformat Keluaran
Menanggapi secara naif dengan segala sesuatu yang diterima dari database kembali ke klien dapat memiliki konsekuensi bencana. Untuk menghindari paparan data yang tidak disengaja, buat metode format khusus yang hanya akan mengembalikan kunci yang masuk daftar putih.
Manfaat lain dari kunci yang masuk daftar putih adalah Anda dapat menulis dokumentasi berdasarkan ini dan melakukan semua pemeriksaan tipe untuk memastikan, misalnya, user_id akan selalu berupa bilangan bulat, flag is_banned akan selalu boolean benar atau salah, dan waktu tanggal akan memiliki satu standar format tanggapan.
Keluaran Hasil
header
Metode terpisah untuk keluaran header akan memastikan bahwa semua yang dikirim ke browser sudah benar. Metode ini dapat menggunakan manfaat membuat API dapat diakses melalui domain yang sama sambil tetap mempertahankan kemungkinan untuk menerima header otorisasi khusus. Pilihan antara domain pihak yang sama atau ke-3 dapat terjadi dengan bantuan header server HTTP_ORIGIN dan HTTP_REFERER. Jika aplikasi mendeteksi bahwa klien menggunakan otorisasi x (atau tajuk khusus lainnya), itu harus mengizinkan akses dari semua asal, izinkan tajuk khusus. Jadi bisa terlihat seperti ini:
header('Access-Control-Allow-Origin: *'); header('Access-Control-Expose-Headers: x-authorization'); header('Access-Control-Allow-Headers: origin, content-type, accept, x-authorization'); header('X-Authorization: '.YOUR_TOKEN_HERE);
Namun jika klien menggunakan kredensial berbasis cookie, header mungkin sedikit berbeda, hanya mengizinkan header terkait host dan cookie yang diminta untuk kredensial:
header('Access-Control-Allow-Origin: '.$origin); header('Access-Control-Expose-Headers: set-cookie, cookie'); header('Access-Control-Allow-Headers: origin, content-type, accept, set-cookie, cookie'); // Allow cookie credentials because we're on the same domain header('Access-Control-Allow-Credentials: true'); if (strtolower($_SERVER['REQUEST_METHOD']) != 'options') { setcookie(TOKEN_COOKIE_NAME, YOUR_TOKEN_HERE, time()+86400*30, '/', '.'.$_SERVER['HTTP_HOST']); }
Ingatlah bahwa permintaan OPSI tidak mendukung cookie sehingga aplikasi tidak akan mengirimkannya bersamanya. Dan, akhirnya ini memungkinkan semua metode HTTP yang kami inginkan memiliki kontrol akses kedaluwarsa:
header('Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE'); header('Access-Control-Max-Age: 86400');
Tubuh
Tubuh itu sendiri harus berisi respons dalam format yang diminta oleh klien Anda dengan status HTTP 2xx saat berhasil, status 4xx saat gagal karena klien dan status 5xx saat gagal karena server. Struktur tanggapan dapat bervariasi, meskipun menentukan bidang "status" dan "tanggapan" juga dapat bermanfaat. Misalnya, jika klien mencoba mendaftarkan pengguna baru dan nama pengguna sudah diambil, Anda dapat mengirim respons dengan status HTTP 200 tetapi JSON di badan yang terlihat seperti:
{“status”: “ERROR”, “response”: ”username already taken”}
… alih-alih kesalahan HTTP 4xx secara langsung.
Kesimpulan
Tidak ada dua proyek yang persis sama. Strategi yang diuraikan dalam artikel ini mungkin cocok atau tidak cocok untuk kasus Anda, tetapi konsep intinya tetap harus serupa. Perlu dicatat bahwa tidak setiap halaman dapat memiliki kerangka trending atau up-to-date terbaru di belakangnya, dan terkadang kemarahan tentang "mengapa bundel Symfony REST saya tidak berfungsi di sini" dapat diubah menjadi motivasi untuk membangun sesuatu yang bermanfaat, sesuatu yang bekerja. Hasil akhirnya mungkin tidak mengkilap, karena akan selalu ada beberapa implementasi khusus dan proyek tertentu, tetapi pada akhirnya solusinya akan menjadi sesuatu yang benar-benar berfungsi; dan dalam skenario seperti ini yang seharusnya menjadi tujuan setiap pengembang API.
Contoh implementasi dari konsep yang dibahas di sini telah diunggah ke repositori GitHub untuk kenyamanan. Anda mungkin tidak ingin menggunakan kode contoh ini secara langsung dalam produksi sebagaimana adanya, tetapi ini dapat dengan mudah berfungsi sebagai titik awal untuk proyek integrasi PHP API warisan Anda berikutnya.
Harus menerapkan server REST API untuk beberapa proyek lama baru-baru ini? Bagikan pengalaman Anda dengan kami di bagian komentar di bawah.