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\$