Contoh code dari artikel ini bisa dilihat di todo-project-php.
Sebelumnya kita telah mengimplementasikan foundation untuk struktur project PHP kita. Sekarang kita akan lanjutkan dengan mengimplementasi komponen router yang berfungsi untuk meng-mapping URL terhadap handler yang akan memproses request dari user. Yuk kita mulai!
Router#
Beberapa hal yang perlu kita implementasikan untuk fungsi Router:
Routerdapat menampung mapping URL terhadap handler dikelompokkan sesuai dengan HTTP method, misalnya GET, POST, dll. Kita akan gunakan array associative.Kita dapat meng-registrasikan URL dan handler dikelompokkan berdasarkan
RouterHTTP request method. Kita akan buat fungsi/method untuk registrasi route, fungsigetdanpostuntuk meng-regitrasi route request GET dan POST.Routerdapat meng-retrieve kembali handler yang terdaftar sesuai dengan HTTP request method dan URL. Kita akan membuat sebuah fungsigetHandlerForuntuk mengembalikan handler sesuai dengan HTTP Request.
Untuk fungsi 1 dan 2 kita implementasikan ke dalam class Router. Class ini memiliki
property array associative untuk menampung mapping URL ke handler beserta method
get, post dan getHandlerFor.
src/Router.php
<?php
use Uph22si1Web\Todo;
class Router
{
// base path untuk me-mounting router, mis. /todo. path lain yang didaftarkan
// akan didaftarkan sebagai subpath dari base path.
private string $base;
// array yang nantinya akan menampung daftar URL dan handler
// list ini dipisah menjadi get dan post untuk memudahkan kita ketika melakukan
// matching/pencarian terhadap request GET dan POST
private array $getHandlers;
private array $postHandlers;
function __construct(string $base)
{
$this->base = $base;
$this->getHandlers = [];
$this->postHandlers = [];
}
public function get(string $path, callable $handler)
{
$this->getHandlers[$this->normalizedPath($path)] = $handler;
}
public function post(string $path, callable $handler)
{
$this->postHandlers[$this->normalizedPath($path)] = $handler;
}
// normalisasi path yang akan di register.
// misalnya router dikonfigurasikan dengan base path '/todo'
// ketika user meng-register path '' maka yang diregister ke list path adalah `/todo`
// jika yang diregister adalah '/show' maka yang diregister adalah '/todo/show'
private function normalizedPath(string $path): string {
// ternary operator untuk menentukan apakah path delimiter perlu ditambahkan atau tidak
$pathDelimiter = str_starts_with($path, '/') || str_ends_with($this->base, '/') ? '' : '/';
$fullPath = $this->stripSlashInTheEndOfPath($this->base . $pathDelimiter . $path);
// NOTE: escape / karena akan digunakan pada regex matching
return str_replace('/', '\/', $fullPath);
}
// Hilangkan / diakhir path jika ada
// e.g. /todo/ menjadi /todo
private function stripSlashInTheEndOfPath(string $path): string {
if (str_ends_with($path, '/')) {
return substr($path, 0, strlen($path)-1);
}
return $path;
}
}
Contoh cara menggunakan class Router untuk mendaftarkan route:
$router = new Router('/');
// registrasi route GET /hello
$router->get('/hello', function() { echo "Hello world"; });
// registrasi route POST /bye
$router->post('/bye', function() { echo "Bye world"; });
Berikutnya kita akan mengimplementasikan getHandlerFor pada class Router untuk
mencari dan mengembalikan handler sesuai dengan HTTP request.
src/Router.pphp
class Router
{
...
public function getHandlerFor(Request $request): array|null
{
$path = $this->requestURIPath($request->getUri());
$method = $request->getMethod();
$handlers = ['GET' => $this->getHandlers, 'POST' => $this->postHandlers][$method];
// NOTE: kita hanya handling method GET dan POST sekarang
if (!$handlers) {
return null;
}
// NOTE: cari path sesuai dengan regex pattern
// https://en.wikipedia.org/wiki/Regular_expression
foreach ($handlers as $pattern => $handler) {
$matches = [];
$matched = preg_match("/^{$pattern}$/", $path, $matches);
if ($matched) {
return ['handler' => $handler, 'matches' => array_slice($matches, 1)];
}
}
return null;
}
}
Class Request merupakan abstraksi untuk memudahkan kita mengakses informasi
HTTP request.
src/Request.php
<?php
namespace Uph22si1Web\Todo;
// Request merupakan abstraksi terhadap object request dari user
// object ini akan digunakan oleh router dan controller untuk
// melakukan logic-nya
// contoh router akan menggunakan uri dan method untuk mencari
// handler/controller yang akan dikembalikan
// sedangkan controller akan menerima input berupa object request
// yang menampung seluruh informasi request
class Request {
private string $uri;
private string $method;
private array $allRequestVariables;
private array $queryRequestVariables;
private array $cookieRequestVariables;
function __construct(
string $uri,
string $method,
array $allRequestVariables,
array $queryRequestVariables,
array $cookieRequestVariables
) {
$this->uri = $uri;
$this->method = $method;
$this->allRequestVariables = $allRequestVariables;
$this->queryRequestVariables = $queryRequestVariables;
$this->cookieRequestVariables = $cookieRequestVariables;
}
public function getUri(): string {
return $this->uri;
}
public function getMethod(): string {
return $this->method;
}
public function input(?string $key): mixed {
if (!$key) {
return $this->allRequestVariables;
}
return $this->allRequestVariables[$key] ?? null;
}
public function query(?string $key): mixed {
if (!$key) {
return $this->queryRequestVariables;
}
return $this->queryRequestVariables[$key] ?? null;
}
public function cookie(?string $key): mixed {
if (!$key) {
return $this->cookieRequestVariables;
}
return $this->cookieRequestVariables[$key] ?? null;
}
}
Berikutnya, kita perlu meng-update Server.php untuk meng-inject
Router yang nantinya akan digunakan untuk memanggil handler. Kita juga
perlu menambahkan logic untuk menangani case apabila tidak ada handler
yang terpasang untuk meng-handle request user. Dan default exception handler
untuk mengirimkan response apabila terjadi exception yang tidak di-handle
pada code kita.
src/Server.php
<?php
// deklarasi namespace file harus disimpan di src/Server.php
// karena namespace Uph22si1Web\Todo disimpan pada directory src
// (baca di composer.json)
namespace Uph22si1Web\Todo;
// import menggunakan namespace
use Throwable;
use Uph22si1Web\Todo\Exceptions\NotFoundException;
use Uph22si1Web\Todo\Exceptions\PageExpiredException;
// definisi class
class Server
{
// property private
private Router $router;
// constructor
function __construct(Router $router)
{
$this->router = $router;
// register exception handler untuk menangani exception yang belum dihandle
// tujuannya supaya aplikasi web memiliki default handler apabila ada code
// yang lupa menghandle exception yang terjadi
set_exception_handler(function(Throwable $exception) {
// handle exception apabila resource yang direquest tidak ditemukan
// kita dapat memanfaatkan sistem exception PHP untuk menghandle situasi khusus
// seperti 404 not found, handler/controller cukup meng-throw exception NotFoundException
if ($exception instanceof NotFoundException) {
http_response_code(404);
echo "Not Found";
return;
}
// default handler exception handler
error_log("Unhandled Exception: {$exception->getMessage()}\n{$exception->getTraceAsString()}");
http_response_code(500);
echo "Internal Server Error";
});
}
// serving http request
// method ini membaca data request, dan mengirimkan request ke handler
// yang terdaftar di router
function serve(): void
{
$uri = $_SERVER['REQUEST_URI'];
$method = $_SERVER['REQUEST_METHOD'];
$request = new Request($uri, $method, $_REQUEST, $_GET, $_COOKIE);
// cari handler/controller untuk path yang di-request
$route = $this->router->getHandlerFor($request);
if ($route) {
// NOTE: kita memanggil handler yang ditemukan dengan meng-passing object
// $request beserta $matches dari regex match routing parameter
// operator ... (array unpacking) meng-extract item array menjadi
// variable yang akan dikirimkan secara positional ke handler
$route['handler']($request, ...$route['matches']);
return;
}
// jika handler tidak ditemukan throw exception not found
throw new NotFoundException();
}
}
Kita tambahkan namespace Exceptions untuk menampung object Exception yang berkaitan
dengan server kita. Exception yang sering ditemukan adalah NotFoundException,
terjadi apabila tidak ada route yang sesuai dengan request dari user.
src/Exceptions/NotFoundException.php
<?php
namespace Uph22si1Web\Todo\Exceptions;
class NotFoundException extends \Exception
{
}
Terakhir kita akan meng-update index.php untuk melakukan dependency injection
dan juga meng-registrasi route yang akan dihandle oleh server kita.
index.php
<?php
use Uph22si1Web\Todo\Server;
// buat instance router baru, mount ke path '/todo'
$router = new Router('/todo');
// buat instance server baru untuk menghandle request http
// baca source-nya di src/Server.php
// inject instance $router ke server
$server = new Server($router);
$router->get('/hello', function() { echo "Hello World"; });
$server->serve();
Berikut isi directory project kita:
.
├── .htaccess
├── composer.json
├── index.php
├── src
│ ├── Exceptions
│ │ └── NotFoundException.php
│ ├── Request.php
│ ├── Router.php
│ ├── Server.php
└── vendor
├── autoload.php
└── composer
