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
:
Router
dapat 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
Router
HTTP request method. Kita akan buat fungsi/method untuk registrasi route, fungsiget
danpost
untuk meng-regitrasi route request GET dan POST.Router
dapat meng-retrieve kembali handler yang terdaftar sesuai dengan HTTP request method dan URL. Kita akan membuat sebuah fungsigetHandlerFor
untuk 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