Lewati ke konten utama
  1. Posts/

PHP MVC Web - Part 3 Router

·1145 kata·6 menit· loading · loading ·
Code Web Php
Desdulianto
Penulis
Desdulianto
Seorang professional IT, bekerja dari rumah
php-web-mvc - This article is part of a series.
Part 3: This Article

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:

  1. Router dapat menampung mapping URL terhadap handler dikelompokkan sesuai dengan HTTP method, misalnya GET, POST, dll. Kita akan gunakan array associative.

  2. Kita dapat meng-registrasikan URL dan handler dikelompokkan berdasarkan Router HTTP request method. Kita akan buat fungsi/method untuk registrasi route, fungsi get dan post untuk meng-regitrasi route request GET dan POST.

  3. Router dapat meng-retrieve kembali handler yang terdaftar sesuai dengan HTTP request method dan URL. Kita akan membuat sebuah fungsi getHandlerFor 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
php-web-mvc - This article is part of a series.
Part 3: This Article