# PHP routing system

I made a routing system for PHP inspired by Symfony's router composed of a few classes.

First I am using Symfony's HTTP Foundations component. Then, I am emulating the classes in the routing component but with almost completely my own implementation (I did copy few lines).

The whole system is not simple so I won't be making it the focus of the question, though, this is the GitHub link, and I would be grateful for a full review (rip me apart).

I will provide the class that matches and parses the routes and I would like to know what I can improve.

There is a parser class

<?php

namespace Routing;

/**
* Takes an instance of Routing\Route object
* Extracts the variables from wildcards
* Calculates a regex that can be used to match routes with urls
*/

class RouteParser
{
/**
* Asks for a route, extracts info that can be used later
*
* @param Route               Routing/Route
*
* @return array              Array with parsed values
*/
public function parseRoute(Route $route) {$variables = array();

$parsed = self::parse($route->getPath());

return ['variables' => $parsed['variables'], 'matcherReg' =>$parsed['regex']];
}

/**
* Takes a string pattern, matches it with regexes
*
* @param string              The pattern
*
* @return array              Array with parsed values
*/
private static function parse($pattern) {$matches = '';
$variables = array();$pos = 0;
$reg = '#'; //It seems that regex must start and end with a delimiter$nextText = '';

if($pattern == '/') {$reg = '#^[\/]+$#'; return ['variables' => '', 'regex' =>$reg];
}

//Check if generated regexes are stored, if so it skips the whole process
if(apc_exists($pattern)) {$cacheI = apc_fetch($pattern); return$cacheI;
}

//Extracts the variables enclosed in {}
preg_match_all('#\{\w+\}#', $pattern,$matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);

//Puts each variable in array
//Uses the text before and after to create a regex for the rest of the pattern - $precedingText,$nextText
//If no wildcard is detected in the path it splits it into segments and compiles are regex
foreach ($matches as$match)
{
$varName = substr($match[0][0], 1, -1);
$precedingText = substr($pattern, $pos,$match[0][1] - $pos);$pos = $match[0][1] + strlen($match[0][0]);
$nxt =$pos - strlen($pattern); if($nxt == 0) $nxt = strlen($pattern);

$nextText = substr($pattern, $nxt);$precSegments = explode('/', $precedingText);$precSegments = array_splice($precSegments, 1); //Pulls a regex from the preeceding segment, each variable segment is replaced with '\/[a-zA-Z0-9]+' if(strlen($precedingText) > 1)
{
foreach($precSegments as$key => $value) {$reg .= '\/';
$reg .=$value;
}

$reg .= '[a-zA-Z0-9]+'; } else {$reg .= '\/[a-zA-Z0-9]+';
}

$nextText = str_replace('/', '\/',$nextText);

if(is_numeric($varName)) { throw new \Exception('Argument cannot be a number'); } if (in_array($varName, $variables)) { throw new \Exception(sprintf('More then one occurrence of variable name "%s".',$varName));
}

$variables[] =$varName;
}

//If no variable names, wildcards are found in pattern : /hello/static/path it will replace it with \/hello\/static\/path
if(count($matches) < 1 &&$pattern != '/')
{
$reg .= str_replace('/', '\/',$pattern);
}

$reg =$reg . $nextText;$reg .= '#';

apc_store($pattern, ['variables' =>$variables, 'regex' => $reg]); return ['variables' =>$variables, 'regex' => $reg]; } }  and a matcher class <?php namespace Routing; use Symfony\Component\HttpFoundation\Request; class Matcher { /** * @var RouteCollection */ private$routes;

/**
* @var Symfony\Component\HttpFoundation\Request
*/
private $request; /** * Constructor. * * @param RouteCollection$routes                   A RouteCollection instance
* @param Symfony\Component\HttpFoundation\Request  A Symfony Request
*/
public function __construct(Request $request, RouteCollection$collection)
{
$this->routes =$collection;
$this->request =$request;
}

public function matchRequest()
{
$return = array(); foreach ($this->routes->all() as $name =>$route)
{
if(preg_match($route['parsed']['matcherReg'],$this->request->getPathInfo()))
{
if(!in_array($this->request->getMethod(),$route['route']->getHttpMethods()))
{
throw new \Exception(sprintf('Method "%s" not allowed', $this->request->getMethod()), 1); } //var_dump($this->request);
return [
'_vars' => $route['parsed']['variables'], '_controller' =>$route['route']->getController(),
'_varValues' => self::realVariables($route['route']->getPath(),$this->request->getPathInfo())
];
}
}

throw new \Exception(sprintf('Route for "%s" not found', $this->request->getPathInfo()), 1); } private static function realVariables($routePath, $pathInfo) {$i = 0;
$poss = [];$vars = [];

$routeSegs = explode('/',$routePath);
$segs = explode('/',$pathInfo);

foreach ($routeSegs as$key => $value) { if(preg_match('#\{\w+\}#',$value))
{
$poss[] =$i;

}
$i++; }$segs = array_splice($segs, 1); foreach ($poss as $key =>$index) {
$vars[] =$segs[$index]; } return$vars;
}
}


They are used in an index.php, as in this excerpt:

<?php

$request = Request::createFromGlobals();$response = new Response();

$routes->add('name', new Route(['GET'], 'hi/{p1}/cicki/{p2}', function($p1, $p2) use ($response) {
$response->setContent($p1 . ' - ' . $p2); })); try {$urlMatcher = new Matcher($request,$routes);
$rez =$urlMatcher->matchRequest();
}
catch(Exception $e) {echo$e;}

call_user_func_array($rez['_controller'],$rez['_varValues']);
\$response->send();


So it basically takes a path pattern, with static strings or parameters enclosed in {}, extracts the parameters and generates a regex to be compared to the URL, if it matches it returns the parameters, their values, the controller (a PHP closure) and it doesn't yet support optional parameters.

Edit: You may notice that I am caching my regexes with apc, so I would like to underline that after a route removal I am not invalidating the cache(A feature I still have to work on).