5
\$\begingroup\$

I made a simple routing class that I like to get reviews for, mostly because I do not know how to make it SOLID, since I even made this class separate from the HTTP response/request for the sake of SRP.

All this class does is route-specific, which seems like solid enough to me.

The actual library can be found at fastpress/framework for more reference.

Here is an example of how to use it separately from the framework:

require 'path/to/router.php'
use Somenamespace\Router as route; 
$route = new Route; 

// get request
$route->get('/', function(){ 
   echo 'Hello'; 
});

// get with regex named params 
$route->get('/user/{:name}/{:id}', function($name, $id){
   echo "name: $name id: $id";
});

// get with vanilla regex
$route->get('/user/[a-z]+', function(){}); 

// Rest-like API
$route->put('/foo', function(){});
$route->delete('/foo', function(){});
$route->post('/foo', function(){});

// MVC example. 
$route->get('/mvc', 'UserController@index'); 

NOTE: In the case of that last MVC example, where the handler is a string with "controller@index" and since for some reason, it did not seem right to me that a routing class should .. in this case:

  • check if controller file / class exists,
  • check if methods exists in controller class
  • instantiate the controller and inject the argument in the method

All inside a routing class, I just made the router return an array containing all these details for other class to take over and instantiate and load anything MVC-specific.

Here is the line in the router that does that:

if(is_string($routerPath) && strpos($routerPath, '@')){
    list($ctrl, $method) = explode('@', $routerPath); 
    return ['controller' => $ctrl, 'method' => $method, 'args' => $args];
}

Aside from that, the actual router class looks like this.

<?php

class Router{

protected $routes = [
    'GET'    => [],
    'POST'   => [],
    'ANY'    => [],
    'PUT'    => [],
    'DELETE' => [],   
];

public $patterns = [
    ':any'  => '.*',
    ':id'   => '[0-9]+',
    ':slug' => '[a-z\-]+',
    ':name' => '[a-zA-Z]+',
];

const REGVAL = '/({:.+?})/';    

public function any($path, $handler){
    $this->addRoute('ANY', $path, $handler);
}

public function get($path, $handler){
    $this->addRoute('GET', $path, $handler);
}

public function post($path, $handler){
    $this->addRoute('POST', $path, $handler);
}

public function put($path, $handler){
    $this->addRoute('PUT', $path, $handler);
}

public function delete($path, $handler){
    $this->addRoute('DELETE', $path, $handler);
}

protected function addRoute($method, $path, $handler){
    array_push($this->routes[$method], [$path => $handler]);
}

public function match(array $server = [], array $post){
    $requestMethod = $server['REQUEST_METHOD'];
    $requestUri    = $server['REQUEST_URI'];

    $restMethod = $this->getRestfullMethod($post); 

    #@TODO: Implement REST method. 

    if (!$restMethod && !in_array($requestMethod, array_keys($this->routes))) {
        return FALSE;
    }

    $method = $restMethod ?: $requestMethod;

    foreach ($this->routes[$method]  as $resource) {

        $args    = []; 
        $route   = key($resource); 
        $handler = reset($resource);

        if(preg_match(self::REGVAL, $route)){
            list($args, $uri, $route) = $this->parseRegexRoute($requestUri, $route);  
        }

        if(!preg_match("#^$route$#", $requestUri)){
            unset($this->routes[$method]);
            continue ;
        }

        if(is_string($handler) && strpos($handler, '@')){
            list($ctrl, $method) = explode('@', $handler); 
            return ['controller' => $ctrl, 'method' => $method, 'args' => $args];
        }

        if(empty($args)){
            return $handler(); 
        }

        #TODO: pass app by func array_push($args, $this);
         return call_user_func_array($handler, $args);

      }

      header('HTTP/1.1 404');
 }

protected function getRestfullMethod($postVar){
    if(array_key_exists('_method', $postVar)){
        if(in_array($method, array_keys($this->routes))){
            return $method;
        }
    }
} 

protected function parseRegexRoute($requestUri, $resource){
    $route = preg_replace_callback(self::REGVAL, function($matches) {
        $patterns = $this->patterns; 
        $matches[0] = str_replace(['{', '}'], '', $matches[0]);

        if(in_array($matches[0], array_keys($patterns))){                       
            return  $patterns[$matches[0]];
        }

    }, $resource);


    $regUri = explode('/', $resource); 

    $args = array_diff(
                array_replace($regUri, 
                explode('/', $requestUri)
            ), $regUri
        );  

    return [array_values($args), $resource, $route]; 
}
}

I would like to know how to make this solid, and if you were to remove some code to do so, please state your reason, not because symfony/laravel/xyz does so. Explain (if you can) why that code belongs separately from the class.

It also goes without saying that if you see any improvement, namely on speed or performance, let me know.

\$\endgroup\$
  • \$\begingroup\$ Are you invoking it with something like this?.. $route->match($_SERVER, $_POST); \$\endgroup\$ – Nigel Alderton Apr 5 '16 at 16:43
  • \$\begingroup\$ @NigelAlderton Nope. That is just to check if the current url from $_SERVER matches the regex pattern. The $_POST is being passed to match() so I can check if the request is RESTFul, or there are PUT, DELETE, UPDATE methods in post \$\endgroup\$ – samayo Apr 8 '16 at 12:55
  • \$\begingroup\$ Good question why no answer... \$\endgroup\$ – Timothy Coetzee Mar 27 '17 at 4:31
  • \$\begingroup\$ >[Composer\Downloader\TransportException] The "api.github.com/repos/fastpress/framework/zipball/… ed75c0" file could not be downloaded (HTTP/1.1 302 Found) \$\endgroup\$ – Anonymous Nov 16 '17 at 19:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.