Mini router http con php
Tags: routing, php
Published: 2022-12-05 04:09:00

---
title: Mini router http con php
published: true
description: ho realizzato un componente 
tags: routing, php
series: tdd
---

## 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.

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

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

    /** @test */
    public function doSomething()
    {
    }
}
```

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
   ┴
```

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

```php
class Router
{
}
```

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
   ┴
```

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.

```php
/** @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);
}
```

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.

```php
class Request
{
    public function getMethod()
    {
    }

    public function getPath()
    {
    }
}
```

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.

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

class Router
{
    public function match(Request $request): bool
    {
        return false;
    }
}
```

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

```php
    /** @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);
    }
```

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.

```php
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]);
    }
}
```

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

```php
/** @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());
}
```

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.

```php
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;
}
```

```php
public function controller()
{
    return $this->controller;
}
```

```php
public function action()
{
    return $this->action;
}
```

## 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.

```php
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';
}
```

## 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