For critical things like this, it's really important to write a test with a test Framework such as PHPUnit.
Install it as described here (you need a pear): https://github.com/sebastianbergmann/phpunit/
I also use a virtual file system, so your test folder will not get caught: https://github.com/mikey179/vfsStream/wiki/Install
I just dumped your Route function into a file called Route.php . In the same directory, I created a test.php file with the following contents:
<?php require_once 'Route.php'; class RouteTest extends PHPUnit_Framework_TestCase { }
To check if everything works, open a command prompt and do the following:
$ cd path/to/directory $ phpunit test.php PHPUnit 3.7.13 by Sebastian Bergmann. F Time: 0 seconds, Memory: 1.50Mb There was 1 failure: 1) Warning No tests found in class "RouteTest". FAILURES! Tests: 1, Assertions: 0, Failures: 1.
If it looks, PHPUnit is installed correctly, and you are ready to write tests.
To make the route function more reliable and less attached to the server, and the file system, I changed it a little:
// new parameter $request instead of relying on server variables function Route($root, $request_uri, $request_method) { // vfsStream doesn't support realpath(). This will do. $root .= '/'; // replaced server variable with $request_uri $segments = array_filter(explode('/', $request_uri), 'strlen'); if ((count($segments) == 0) || (is_dir($root) === false)) { return true; // serve index } $controller = null; $all_segments = array_values($segments); $segments = $all_segments; while ((is_null($segment = array_shift($segments)) !== true) && (is_dir($root . $controller . $segment . '/'))) { $controller .= $segment . '/'; } if (is_file($controller = $root . $controller . $segment . '.php')) { $class = basename($controller . '.php'); // replaced server variable with $request_method $method = array_shift($segments) ?: $request_method; require($controller); if (method_exists($class = new $class(), $method)) { return call_user_func_array(array($class, $method), $segments); } } // $all_segments variable instead of a call to self:: throw new Exception('/' . implode('/', $all_segments), 404); // serve 404 }
Let's add a test to check if the function returns true if a pointer route is requested:
public function testIndexRoute() { $this->assertTrue(Route('.', '', 'get')); $this->assertTrue(Route('.', '/', 'get')); }
Since your test class extends PHPUnit_Framework_TestCase , you can now use methods such as $this->assertTrue to verify that a particular statement evaluates to true. Restart it again:
$ phpunit test.php PHPUnit 3.7.13 by Sebastian Bergmann. . Time: 0 seconds, Memory: 1.75Mb OK (1 test, 2 assertions)
Passed this test! array_filter if array_filter correctly removes empty segments:
public function testEmptySegments() { $this->assertTrue(Route('.', '//', 'get')); $this->assertTrue(Route('.', '//////////', 'get')); }
It also allows you to check if a pointer route is requested if the $root directory for routes does not exist.
public function testInexistentRoot() { $this->assertTrue(Route('./inexistent', '/', 'get')); $this->assertTrue(Route('./does-not-exist', '/some/random/route', 'get')); }
To test more than this, we need files containing classes with methods. Therefore, let us use our virtual file system to configure the directory structure of files before starting each test.
require_once 'Route.php'; require_once 'vfsStream/vfsStream.php'; class RouteTest extends PHPUnit_Framework_TestCase { public function setUp() {
PHPUnit has special methods for this kind of thing. The setUp method setUp run before each test method in this test class. And the tearDown method after executing the test method.
Now I create the Structure directory using vfsStream. (If you are looking for a tutorial for this: https://github.com/mikey179/vfsStream/wiki is a pretty good resource)
public function setUp() { $edit_php = <<<EDIT_PHP <?php class edit { public function get() { return __METHOD__ . "()"; } public function post() { return __METHOD__ . "()"; } } EDIT_PHP; $company_php = <<<COMPANY_PHP <?php class company { public function get(\$id = null) { return __METHOD__ . "(\$id)"; } } COMPANY_PHP; $this->root = vfsStream::setup('controllers', null, Array( 'admin' => Array( 'company' => Array( 'edit.php' => $edit_php ), 'company.php' => $company_php ) )); } public function tearDown() { unset($this->root); }
vfsStream::setup() now creates a virtual directory with the specified file structure and given file contents. And, as you can see, I will allow my controllers to return the method name and parameters as a string.
Now we can add some more tests to our test suite:
public function testSimpleDirectMethodAccess() { $this->assertEquals("edit::get()", Route(vfsStream::url('controllers'), '/controllers/admin/company/edit/get', 'get')); }
But this time the test fails:
$ phpunit test.php PHPUnit 3.7.13 by Sebastian Bergmann. ... Fatal error: Class 'edit.php.php' not found in C:\xampp\htdocs\r\Route.php on line 27
So, something is wrong with the $class variable. If we now check the next line in the route function with a debugger (or some echo s).
$class = basename($controller . '.php');
We can see that the $controller variable contains the correct file name, but why does the .php application exist? These seams should be a common mistake. I think it should be:
$class = basename($controller, '.php');
Because it removes the .php extension. And we get the correct class name edit .
Now let's test if an exception occurs if we request a random path that does not exist in our directory structure.
public function testForInexistentRoute() { Route(vfsStream::url('controllers'), '/random-route-to-the/void', 'get'); }
PHPUnit automatically reads these comments and checks if an exception of type Exception occurs when this method is executed, and if the exception message was /random-route-to-the/void
These are the seams for the job. Checks if the $request_method parameter works correctly.
public function testMethodAccessByHTTPMethod() { $this->assertEquals("edit::get()", Route(vfsStream::url('controllers'), '/admin/company/edit', 'get')); $this->assertEquals("edit::post()", Route(vfsStream::url('controllers'), '/admin/company/edit', 'post')); }
If we run this test, we will face another problem:
$ phpunit test.php PHPUnit 3.7.13 by Sebastian Bergmann. .... Fatal error: Cannot redeclare class edit in vfs:
It looks like we are using include / require several times for the same file.
require($controller);
Lets change it to
require_once($controller);
Now consider your problem and write a test to verify that the company directory and company.php do not interfere with each other.
$this->assertEquals("company::get()", Route(vfsStream::url('controllers'), '/admin/company', 'get')); $this->assertEquals("company::get()", Route(vfsStream::url('controllers'), '/admin/company/get', 'get'));
And here we get exception 404, as you stated in your question:
$ phpunit test.php PHPUnit 3.7.13 by Sebastian Bergmann. .....E. Time: 0 seconds, Memory: 2.00Mb There was 1 error: 1) RouteTest::testControllerWithSubControllers Exception: /admin/company C:\xampp\htdocs\r\Route.php:32 C:\xampp\htdocs\r\test.php:69 FAILURES! Tests: 7, Assertions: 10, Errors: 1.
The problem here is that we do not know when to enter the subdirectory and when to use the controller in the .php file. Therefore, we need to indicate exactly what you want. And I assume the following, because it makes sense.
- Enter the subdirectory only if the controller does not contain the requested method.
- If neither the controller nor the subdirectory contains the requested method, run 404
So instead of looking for directories like this here:
while ((is_null($segment = array_shift($segments)) !== true) && (is_dir($root . $controller . $segment . '/'))) { $controller .= $segment . '/'; }
We need to search for files. And if we find a file that does not contain the requested method, we look for a directory.
function Route($root, $request_uri, $request_method) { $segments = array_filter(explode('/', $request_uri), 'strlen'); if ((count($segments) == 0) || (is_dir($root) === false)) { return true; // serve index } $all_segments = array_values($segments); $segments = $all_segments; $directory = $root . '/'; do { $segment = array_shift($segments); if(is_file($controller = $directory . $segment . ".php")) { $class = basename($controller, '.php'); $method = isset($segments[0]) ? $segments[0] : $request_method; require_once($controller); if (method_exists($class = new $class(), $method)) { return call_user_func_array(array($class, $method), array_slice($segments, 1)); } } $directory .= $segment . '/'; } while(is_dir($directory)); throw new Exception('/' . implode('/', $all_segments), 404); // serve 404 }
This method works now as expected.
Now we can add a lot more test cases, but I don't want to stretch this anymore. As you can see, it is very useful to run a set of automated tests to ensure that some things in your function work. It is also very useful for debugging, because you will find out exactly where the error occurred. I just wanted you to start how to make TDD and how to use PHPUnit so that you can debug your code yourself.
"Give a man a fish, and you feed him for a day. Teach a man to fish, and you feed him for life."
Of course, you must write tests before writing code.
Here are some more links that might be interesting: