Budujemy klasę App, która będzie wykorzystywać wcześniej napisany router. Do dzieła.

Nasz projekt dotychczas:

class TransactionController {
    public function makeTransaction($params){
        echo "Making transaction {$params['transaction']} <br>";
        echo "Recepit: {$params['receipt']} <br>";
        echo "DONE";
    }
}

class Router {

    private array $routes = [];
    private array $middlewares = [];

    public function normalizePath($path){

        $path = trim($path, '/');
        $path = "/{$path}/";
        $path = preg_replace('#[/]{2,}#', '/', $path);

        return $path;
    }

    public function add(string $method, string $path, array $controller)
  {
    $path = $this->normalizePath($path);

    $regexPath = preg_replace('#{[^/]+}#', '([^/]+)', $path);

    $this->routes[] = [
      'path' => $path,
      'method' => strtoupper($method),
      'controller' => $controller,
      'middlewares' => [],
      'regexPath' => $regexPath
    ];
  }

  public function dispatch(string $path, string $method, $container = null)
  {
    $path = $this->normalizePath($path);
    $method = strtoupper($_POST['_METHOD'] ?? $method);
    
    foreach ($this->routes as $route) {
        if (
          !preg_match("#^{$route['regexPath']}$#", $path, $paramValues) ||
          $route['method'] !== $method
        ) {
          continue;
        }
  
        array_shift($paramValues);
  
        preg_match_all('#{([^/]+)}#', $route['path'], $paramKeys);
  
        $paramKeys = $paramKeys[1];
  
        $params = array_combine($paramKeys, $paramValues);
  
        [$class, $function] = $route['controller'];
  
        $controllerInstance = $container ?
          $container->resolve($class) :
          new $class;
  
        $action = fn () => $controllerInstance->{$function}($params);
        $allMiddleware = [...$route['middlewares'], ...$this->middlewares];

        foreach ($allMiddleware as $middleware) {
            $middlewareInstance = $container ?
              $container->resolve($middleware) :
              new $middleware;
            $action = fn () => $middlewareInstance->process($action);
          }

        $action();

      return;
    }
  }

  public function addMiddleware(string $middleware)
  {
    $this->middlewares[] = $middleware;
  }

  public function addRouteMiddleware(string $middleware)
  {
    $lastRouteKey = array_key_last($this->routes);
    $this->routes[$lastRouteKey]['middlewares'][] = $middleware;
  }

}


interface MiddlewareInterface
{
  public function process(callable $next);
}

class StupidMiddleware implements MiddlewareInterface
{
  public function process(callable $next)
  {
    echo "This is stupid middleware speaking on all routes <br>";
    $next();
  }
}

class StupidRouteMiddleware implements MiddlewareInterface
{
  public function process(callable $next)
  {
    echo "This is stupid route middleware speakin on this route! <br>";
    $next();
  }
}

Router i kontener (tego jeszcze nie mamy) dodamy do App przy pomocy kompozycji:

class App
{
  private Router $router;
  private $container;

  public function __construct(string $containerDefinitionsPath = null)
  {
    $this->router = new Router();
    $this->container = null;

    if ($containerDefinitionsPath) {
      echo "not implemented yet";
    }
  }

}

Metoda run będzie pobierać dane do dispatch z URI (na razie jeszcze musimy je zastąpić „fejkowymi”):

class App
{
  private Router $router;
  private $container;

 //(...)

  public function run()
  {
    
    //$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
    //$method = $_SERVER['REQUEST_METHOD'];

    $path  ='/transaction/123/receipt/helloworld/';
    $method = 'GET';

    $this->router->dispatch($path, $method, $this->container);
  }

}

Metody get, post i delete będą fasadami dla metody add routera zawierającymi dodatkowo fluent interface, czyli „return this”:

class App
{
  private Router $router;
  private $container;

  //(...)

  public function get(string $path, array $controller): App
  {
    $this->router->add('GET', $path, $controller);

    return $this;
  }

  public function post(string $path, array $controller): App
  {
    $this->router->add('POST', $path, $controller);

    return $this;
  }

  public function delete(string $path, array $controller): App
  {
    $this->router->add('DELETE', $path, $controller);

    return $this;
  }

}

Wszystko po to, aby można je było chainować (później dodamy sobie warstwę abstrakcji Route…).

Ok, teraz metody-fasady dla metod routera dodających globalne middleware i „ścieżkowe” middleware:

class App
{
  private Router $router;
  private $container;

  //(...)

  public function addMiddleware(string $middleware)
  {
    $this->router->addMiddleware($middleware);
  }

  public function add(string $middleware)
  {
    $this->router->addRouteMiddleware($middleware);
  }

}

Teraz przykład, że wszystko działa jak należy, nawet fluent interface:

$app = new App();
$app->addMiddleware(StupidMiddleware::class);
$app->get('/transaction/{transaction}/receipt/{receipt}/', [TransactionController::class, 'makeTransaction'])
->add(StupidRouteMiddleware::class);
$app->run();

// This is stupid middleware speaking on all routes
// This is stupid route middleware speakin on this route!
// Making transaction 123
// Recepit: helloworld
// DONE

Projekt MVC będziemy rozbudowywać, docelowo tworząc klon Laravela. Mając middleware mamy naprawdę potężne narzędzie w ręku.