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.