I had previously asked the question here.
The response was pretty much "wow this is bad".
So I learned everything I could and wrote what I believe to be better using TDD.
The strict requirement is that the url will be parsed as /controller/action/everything/else/is/a/parameter
.
So here is what I wrote:
The router class
<?php declare(strict_types=1); namespace xxxx\xxxx\Router; use xxxx\xxxx\Util\StorageUtils; class Router{ public array $defaultRoute; public array $notFoundRoute; public array $urlSegments; public $controller; public $action; public $parameters; public $controllerName; public $view; public function setDefaultRoute($controller, $action): void{ $this->defaultRoute = [$controller, $action]; } public function setNotFoundRoute($controller, $action): void{ $this->notFoundRoute = [$controller, $action]; } public function execute($url): void{ $this->getUrlSegments($url) ->determineController() ->createController() ->determineParameters() ->determineAction() ->executeAction() ->determineView(); } public function executeView(){ $this->controller->initData(); if($this->controller->data != null){ extract($this->controller->data, EXTR_PREFIX_SAME, 'wddx'); } if($this->view != null){ $viewPath = StorageUtils::getFullPath($this->getViewFile($this->controllerName, $this->view)); if($this->controller->useTemplate){ $templatePath = StorageUtils::getFullPath($this->getViewFile('Template', $this->controller->template)); require_once($templatePath); }else{ require_once($viewPath); } } $this->controller->init(); } public function executeAction(): self{ $action = $this->action; $this->controller->$action($this->parameters); return $this; } public function getUrlSegments($url): self{ $url = explode('/', ltrim($url,'/')); $this->urlSegments = $url; return $this; } public function determineController(): self{ if(count($this->urlSegments) > 0){ $controller = ucfirst($this->urlSegments[0]); if(empty($controller)){ $this->controllerName = $this->defaultRoute[0]; return $this; } if($this->controllerExist($controller)){ $this->controllerName = $controller; return $this; } } $this->controllerName = $this->notFoundRoute[0]; return $this; } public function createController(): self{ $controllerName = $this->controllerName; $this->requireFile($this->getControllerFile($controllerName)); $this->controller = new $controllerName(); return $this; } public function determineParameters(): self{ $this->parameters = array_slice($this->urlSegments, 2); return $this; } public function determineAction(): self{ $action = 'index'; if(count($this->urlSegments) > 1){ if(strlen($this->urlSegments[1]) > 0){ $action = $this->urlSegments[1]; } } if(!$this->actionExist($action)){ $this->controllerName = 'ErrorPages'; $this->createController(); $action = $this->notFoundRoute[1]; } $this->action = $action; return $this; } public function determineView(): self{ $this->view = $this->controller->view; if(!is_string($this->view)){ $this->view = null; } if(!$this->viewExist($this->controllerName, $this->view) && $this->view != null){ $this->controllerName = 'ErrorPages'; $this->createController(); $this->action = $this->notFoundRoute[1]; $this->executeAction(); $this->view = '404'; } return $this; } public function controllerExist($controllerName): bool{ return StorageUtils::fileExist(StorageUtils::getFullPath($this->getControllerFile($controllerName))); } public function actionExist($action): bool{ return method_exists($this->controller, $action); } public function viewExist($controllerName, $view): bool{ return StorageUtils::fileExist(StorageUtils::getFullPath($this->getViewFile($controllerName, $view))); } public function getControllerFile($controllerName): string{ return "/src/Application/$controllerName/Controller/$controllerName.php"; } public function getViewFile($controllerName, $viewName): string{ return "/src/Application/$controllerName/View/$viewName.php"; } private function requireFile($route): void{ require_once(StorageUtils::getFullPath($route)); } } ?>
The corresponding test class
<?php namespace xxxx\xxxx\Test; use PHPUnit\Framework\TestCase; use xxxx\xxxx\Router\Router; class RouterTest extends TestCase{ private $router; public function setUp(): void{ $this->router = new Router(); $this->router->setDefaultRoute('Home', 'index'); $this->router->setNotFoundRoute('ErrorPages', 'notFound'); } /** * @covers getUrlSegments */ public function testGetUrlSegments(){ $returnValue = $this->router->getUrlSegments('/test/index/111/222'); $this->assertNotEmpty($this->router->urlSegments); $this->assertIsArray($this->router->urlSegments); $this->assertEquals('test', $this->router->urlSegments[0]); $this->assertEquals('index', $this->router->urlSegments[1]); $this->assertEquals('111', $this->router->urlSegments[2]); $this->assertEquals('222', $this->router->urlSegments[3]); $this->assertEquals(Router::class, $returnValue::class); } /** * @covers setDefaultRoute */ public function testSetDefaultRoute(){ $this->assertEquals('Home', $this->router->defaultRoute[0]); $this->assertEquals('index', $this->router->defaultRoute[1]); } /** * @covers setNotFoundRoute */ public function testSetNotFoundRoute(){ $this->assertEquals('ErrorPages', $this->router->notFoundRoute[0]); $this->assertEquals('notFound', $this->router->notFoundRoute[1]); } public function routeProvider(){ return [ ['/test', 'Test'], ['/test/sdf24', 'Test'], ['/home', 'Home'], ['/', 'Home'], ['', 'Home'], ['/home/', 'Home'], ['adafeadsdas', 'ErrorPages'], ['ad/asd/efwefd/qed', 'ErrorPages'], ['!@#@!#@', 'ErrorPages'], ['/!@@$2323', 'ErrorPages'], ['/.html', 'ErrorPages'], ['/someNonExistentController', 'ErrorPages'], ['/%', 'ErrorPages'], ['/@@$%$%%5E', 'ErrorPages'] ]; } /** * @covers determineController * @dataProvider routeProvider */ public function testDetermineController($url, $expectedResult){ $this->router->getUrlSegments($url) ->determineController(); $this->assertEquals($expectedResult, $this->router->controllerName); } /** * @covers createController * @dataProvider routeProvider */ public function testCreateController($url, $expected){ $this->router->getUrlSegments($url) ->determineController() ->createController(); $this->assertEquals($expected, $this->router->controller::class); } public function actionDataProvider(){ return [ ['/test', 'index'], ['/test/doesNotExist', 'notFound'], ['/test/test', 'test'], ['/test/!#$@#$ad', 'notFound'], ['/test/', 'index'] ]; } /** * @covers determineAction * @dataProvider actionDataProvider */ public function testDetermineAction($url, $expected){ $this->router->getUrlSegments($url) ->determineController() ->createController() ->determineAction(); $this->assertEquals($expected, $this->router->action); } /** * @covers determineAction */ public function testDetermineActionChangesControllerOnActionNotExist(){ $this->router->getUrlSegments('/test/doesNotExist') ->determineController() ->createController() ->determineAction(); $this->assertEquals($this->router->notFoundRoute[0], $this->router->controllerName); $this->assertEquals($this->router->notFoundRoute[1], $this->router->action); $this->assertEquals(\ErrorPages::class, $this->router->controller::class); } public function parameterDataProvider(){ return [ ['/test', []], ['/test/index', []], ['/test/index/one', ['one']], ['/test/index/one/two/three', ['one', 'two', 'three']], ['/test/index/3,d,/!#$/', ['3,d,', '!#$', '']], ['/text/index/index.html', ['index.html']] ]; } /** * @covers determineParameters * @dataProvider parameterDataProvider */ public function testDetermineParameters($url, $expected){ $this->router->getUrlSegments($url) ->determineParameters(); $this->assertEquals($expected, $this->router->parameters); } public function viewDataProvider(){ return [ ['/test/validView', 'ValidView'], ['/test/invalidView', '404'] ]; } /** * @covers determineView * @dataProvider viewDataProvider */ public function testDetermineView($url, $expected){ $this->router->execute($url); $this->assertEquals($expected, $this->router->view); } public function printedReturnDataProvider(){ return [ ['/test/printedReturnView', null], ['/test/printRReturnView', null] ]; } /** * @covers determineView * @dataProvider printedReturnDataProvider */ public function testDetermineViewWithPrintedReturnData($url, $expected){ $this->router->execute($url); $this->expectOutputString('{"randomData":"dsdsddd"}'); $this->assertEquals($expected, $this->router->view); } /** * @covers determineView */ public function testDetermineViewChangesControllerOnViewNotExist(){ $this->router->execute('/test/invalidView'); $this->assertEquals('ErrorPages', $this->router->controllerName); } }
I realize that there is probably a lot of room to grow here and I'm curious to know how I did this time around and where improvements can be made.
Almost forgot to add what the index.php file looks like now.
use xxxx\xxxx\Util\StorageUtils; use xxxx\xxxx\Router\Router; if(isset($argv)){ $options = getopt('', ['uri::']); $url = $options['uri']; }else{ if($engine == 'nginx'){ $url = $_SERVER['REQUEST_URI']; }else if($engine == 'apache'){ $url = $_SERVER['PATH_INFO']; } } $router = new Router(); $router->setDefaultRoute('Home', 'index'); $router->setNotFoundRoute('ErrorPages', 'notFound'); $router->execute($url); $router->executeView();
new $controllerName()
seems to imply that all your controllers must have parameterless constructors, which disallows to use constructor injection...\$\endgroup\$