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()
{
}
}
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.
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.
/** @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.
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.
$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.
/** @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.
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.:
/** @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.
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;
}
public function controller()
{
return $this->controller;
}
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.
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
Top comments (0)