DEV Community

Simone Gentili
Simone Gentili

Posted on

Mini router http con php

Il routing

Alcune applicazioni web senza una vera e propria architettura mostrano contenuti diversi quando diverse pagine vengono richieste dall’utente. Quindi potremmo avere la pagina contatti.php, homepage.php, index.php, … e cosi via.

In altri casi, che preferisco, possiamo trovare un front controller. Il front controller pattern è un modello di progettazione per software. Questo modello si applica alla progettazione di pagine web. Il suo scopo è quello di fornire un punto di ingresso centralizzato per la gestione delle richieste.

Il front controller non fa altro che instradare una certa richiesta ad un corrispondente handler.

Funzioni

isset() - verifica se un certo item di un certo array è presente oppure no.

strtolower() - trasforma tutti i caratteri di una stringa in lowercase.

Svolgimento

Per prima cosa andiamo a creare la classe di test RoutingTest. In questa classe creiamo subito il metodo setUp(): void che viene richiamato da phpunit prima di eseguire qualsiasi test. Poi andiamo ad eseguire il nostro primo test. Il primo test può essere anche vuoto. Tanto ci basta per eseguire la riga di codice del setUp(): void ed avere un primo test rosso.

class RouterTest extends PHPUnit\Framework\TestCase
{
    private Router $router;

    public function setUp(): void
    {
        $this->router = new Router();
    }

    /** @test */
    public function doSomething()
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

Abbiamo comunque un test rosso. Infatti la classe Router non è mai stata creata:

Router
 ✘ Do something  73 ms
   ┐
   ├ Error: Class "Router" not found
   │
   ╵ /var/www/html/tests/RouterTest.php:9
   ┴
Enter fullscreen mode Exit fullscreen mode

Quindi andiamo a crearla e scriviamo la minor quantità di codice possibile per far diventare verde il test.

class Router
{
}
Enter fullscreen mode Exit fullscreen mode

A questo punto se rilanciamo i test otterremo un risultato diverso ma che comunque ci dice che il nostro test non è completo perché non c’è ancora nessun tipo di assertion.

Router
 ☢ Do something  255 ms
   ┐
   ├ This test did not perform any assertions
   ├
   ├ /var/www/html/tests/RouterTest.php:13
   ┴
Enter fullscreen mode Exit fullscreen mode

Creiamo quindi il primo vero test, eliminando quello precedente. Vogliamo che esista un certo metodo match e che questo prenda in ingresso un oggetto Request. Lanciando il test, PHPUnit ci dice che getPath e getMethod non esistono. In questo caso possiamo creare un’inerfaccia Request con i metodi che ci servono o anche semplicemente una classe Request.

/** @test */
public function shouldNeverLookForMethodAndPathWheneverIsEmpty()
{
    $this->request = $this
        ->getMockBuilder(Request::class)
        ->getMock();
    $this->request->expects($this->never())
        ->method('getPath');
    $this->request->expects($this->never())
        ->method('getMethod');

    $this->router->match($this->request);
}
Enter fullscreen mode Exit fullscreen mode

Scelgo la seconda opzione e creo la nostra classe Request che non toccheremo più fino a quando non verrà creato il client e faremo delle vere chiamate con il browser.

class Request
{
    public function getMethod()
    {
    }

    public function getPath()
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

Il metodo match non esiste quindi andrà creato anche quello. Dobbiamo ricordarci che deve prendere in ingresso un oggetto di tipo Request. Vogliamo anche che ci restituisca true o false se c’è un riscotro tra le rotte inserite e la request ricevuta. Di seguito una piccola modifica al nostro test e la classe Router.

$result = $this->router->match($this->request);
$this->assertSame(false, $result);

class Router
{
    public function match(Request $request): bool
    {
        return false;
    }
}
Enter fullscreen mode Exit fullscreen mode

La prossima mossa è quella di aggiungere una rotta nel router e far si che lo stesso match questa volta restituisca true.

    /** @test */
    public function shouldMatchGetRequestIfStoredInTheRouter()
    {
        $this->request = $this
            ->getMockBuilder(Request::class)
            ->getMock();
        $this->request->expects($this->once())
            ->method('getPath')
            ->willReturn('/');
        $this->request->expects($this->once())
            ->method('getMethod')
            ->willReturn('GET');

        $this->router->add('GET', '/', HomeController::class);

        $result = $this->router->match($this->request);
        $this->assertSame(true, $result);
    }
Enter fullscreen mode Exit fullscreen mode

E gia questo fara fare un bel balzo in avanti al nostro Router. Tanto per iniziare introduciamo un altro nuovo metodo: add. Con questo metodo potremo caricare il router di nuove rotte da gestire. Si romperà il primo test e lo terremo a bada restituendo subito false ne caso in cui le rotte siano un array vuoto.

class Router
{
    private array $routes = [];

    public function add(string $method, string $path, string $controller)
    {
        $this->routes[$path][$method] = $controller;
    }

    public function match(Request $request): bool
    {
        if ($this->routes === []) {
            return false;
        }

        $path = $request->getPath();
        $method = $request->getMethod();

        return isset($this->routes[$path][$method]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Ora sappiamo come aggiungere delle rotte al nostro router, ma ci serve recuperare l’handler. Quindi scriviamo il test che ci permetter’ di ottenerlo.:

/** @test */
public function shouldReturnControllerAfterGivenSuccessfulMatch()
{
    $this->request = $this
        ->getMockBuilder(Request::class)
        ->getMock();
    $this->request->expects($this->once())
        ->method('getPath')
        ->willReturn('/');
    $this->request->expects($this->once())
        ->method('getMethod')
        ->willReturn('GET');

    $this->router->add('GET', '/', HomeController::class);

    $this->router->match($this->request);

    $this->assertSame(HomeController::class, $this->router->controller());
    $this->assertSame('get', $this->router->action());
}
Enter fullscreen mode Exit fullscreen mode

Tramite path e metodo posso ottenere il controller e l’action. Quindi li render; disponibili al mondo esterno tramite due metodi. Si potrebbe anche fare in modo diverso. Ad esempio restituendo false se non si trova nulla o un array con controller ed action. Questo ci farebbe risparmiare un paio di metodi ed un paio di variabili. Si potrebbe invece restituire un DTO contenente controller ed action. Anche in questo caso avremmo meno codice da gestire.

public function match(Request $request): bool
{
    if ($this->routes === []) {
        return false;
    }

    $this->request = $request;

    $path = $this->request->getPath();
    $method = $this->request->getMethod();

    $found = isset($this->routes[$path][$method]);

    $this->controller = $this->routes[$path][$method];
    $this->action = strtolower($method);

    return $found;
}
Enter fullscreen mode Exit fullscreen mode
public function controller()
{
    return $this->controller;
}
Enter fullscreen mode Exit fullscreen mode
public function action()
{
    return $this->action;
}
Enter fullscreen mode Exit fullscreen mode

Un client per il browser

Quando abbiamo iniziato a lavorare sul componente Routing abbiamo parlato del front controller. Il front controller lo troviamo nel path public/index.php. Il nostro front controller stapmerà HTT/2 200 se chiamata sarà una get normalissima all’indirizzo localhost:8894. Diversamente stamperà un 404.

class HomeController
{
    public function get()
    {
        echo 'pagina trovata';
    }
}

$router = new Router();
$router->add(GET, '/', HomeController::class);
if ($router->match(new Request())) {
    echo '< HTTP/2 200<br>< ';
    $controller = $router->controller();
    $action = $router->action();
    $page = new $controller();
    $method = strtolower($action);
    $page->$method();
} else {
    echo '< HTTP/2 404<br>< Pagina non trovata';
}
Enter fullscreen mode Exit fullscreen mode

Miglioramenti

Rotte dinamiche

Questo router è molto basico. Consente la gestione delle sole rotte statiche. Si potrebbe andare avanti ad implementare qualche funzionalità in più. Ad esempio proprio questa gestione di rotte non solo statiche.

Sicurezza

Bel problema da affrontare questo. Una rotta dovrebbe poter essere messa in sicurezza. Qui si andrebbe a creare una piccolo applicativo MVC. Molto interessante come problema da affrontare.

Links

Una versione video di questo articolo la puoi trovare a questo link: https://youtu.be/Gsa7yztCNgw

Top comments (0)