public function set($key, $value, $ttl = null)
    {
        if ($this->getCompress()) {
            $value = self::compress($value);
        }

        $keyP = $this->getPatternKey().$key;

        if (is_null($ttl)) {
            $func = function ($redis) use ($keyP, $value) {
                $start = microtime(true);
                $redis->set($keyP, $value);
                $this->notifyEvent('set', array($keyP, $value), microtime(true) - $start);

                return $redis;
            };
        } else {
            $func = function ($redis) use ($keyP, $value, $ttl) {
                $start = microtime(true);
                $redis->setex($keyP, $ttl, $value);
                $this->notifyEvent('setex', array($keyP, $ttl, $value), microtime(true) - $start);

                return $redis;
            };
        }

        if (true === $this->multi) {
            $this->addToExecList($key, $func);
        } else {
            $func($this->getRedis($key)); // set "normal"
        }

        return $this;
    }

public
function
set
(
$
key
,
$
value
,
$
ttl
=
)
{
if
(
$
this
->
getCompress
(
)
)
{
$
value
=
self
::
compress
(
$
value
)
;
}
$
keyP
=
$
this
->
getPatternKey
(
)
.
$
key
;
if
(
is_null
(
$
ttl
)
)
{
$
func
=
function
(
$
redis
)
use
(
$
keyP
,
$
value
)
{
$
start
=
microtime
(
true
)
;
$
redis
->
set
(
$
keyP
,
$
value
)
;
$
this
->
notifyEvent
(
'set'
,
array
(
$
keyP
,
$
value
)
,
microtime
(
true
)
$
start
)
;
return
$
redis
;
}
;
}
else
{
$
func
=
function
(
$
redis
)
use
(
$
keyP
,
$
value
,
$
ttl
)
{
$
start
=
microtime
(
true
)
;
$
redis
->
setex
(
$
keyP
,
$
ttl
,
$
value
)
;
$
this
->
notifyEvent
(
'setex'
,
array
(
$
keyP
,
$
ttl
,
$
value
)
,
microtime
(
true
)
$
start
)
;
return
$
redis
;
}
;
}
if
(
true
===
$
this
->
multi
)
{
$
this
->
addToExecList
(
$
key
,
$
func
)
;
}
else
{
$
func
(
$
this
->
getRedis
(
$
key
)
)
;
// set "normal"
}
return
$
this
;
}

set a value

@param string  $key   la clé
@param string  $value valeur
@param integer $ttl   time to live

@return Cache


public function exists($key)
    {
        $start = microtime(true);
        $ret = $this->getRedis($key)->exists($this->getPatternKey().$key);
        $this->notifyEvent('exists', array($this->getPatternKey().$key), microtime(true) - $start);

        return (boolean) $ret;
    }

public function exists ( $ key ) { $ start = microtime ( true ) ; $ ret = $ this -> getRedis ( $ key ) -> exists ( $ this -> getPatternKey ( ) . $ key ) ; $ this -> notifyEvent ( 'exists' , array ( $ this -> getPatternKey ( ) . $ key ) , microtime ( true ) - $ start ) ; return ( boolean ) $ ret ; }

check if a key exist

@param string $key clé

@return boolean


public function type($key)
    {
        $start = microtime(true);
        $ret = (string)$this->getRedis($key)->type($this->getPatternKey().$key);
        $this->notifyEvent('type', array($this->getPatternKey().$key), microtime(true) - $start);

        return $ret;
    }

public function type ( $ key ) { $ start = microtime ( true ) ; $ ret = ( string ) $ this -> getRedis ( $ key ) -> type ( $ this -> getPatternKey ( ) . $ key ) ; $ this -> notifyEvent ( 'type' , array ( $ this -> getPatternKey ( ) . $ key ) , microtime ( true ) - $ start ) ; return $ ret ; }

return the type of a key

@param string $key clé

@return mixed


public function expire($key, $ttl)
    {
        if ($ttl <= 0) {
            throw new Exception('ttl arg cant be negative');
        }

        $keyP = $this->getPatternKey().$key;
        $func = function ($redis) use ($keyP, $ttl) {
            try {
                $start = microtime(true);
                $ret = $redis->expire($keyP, $ttl);
                $this->notifyEvent('expire', array($keyP, $ttl), microtime(true) - $start);

                return $ret;
            } catch (Predis\ServerException $e) {
                return null;
            }
        };

        // gestion du multi
        if (true === $this->multi) {
            $this->addToExecList($key, $func);

            return $this;
        } else {
            $ret = $func($this->getRedis($key));

            return $ret;
        }
    }

public
function
expire
(
$
key
,
$
ttl
)
{
if
(
$
ttl
<=
0
)
{
throw
new
Exception
(
'ttl arg cant be negative'
)
;
}
$
keyP
=
$
this
->
getPatternKey
(
)
.
$
key
;
$
func
=
function
(
$
redis
)
use
(
$
keyP
,
$
ttl
)
{
try
{
$
start
=
microtime
(
true
)
;
$
ret
=
$
redis
->
expire
(
$
keyP
,
$
ttl
)
;
$
this
->
notifyEvent
(
'expire'
,
array
(
$
keyP
,
$
ttl
)
,
microtime
(
true
)
$
start
)
;
return
$
ret
;
}
catch
(
Predis
\
ServerException
$
e
)
{
return
;
}
}
;
// gestion du multi
if
(
true
===
$
this
->
multi
)
{
$
this
->
addToExecList
(
$
key
,
$
func
)
;
return
$
this
;
}
else
{
$
ret
=
$
func
(
$
this
->
getRedis
(
$
key
)
)
;
return
$
ret
;
}
}

set the key ttl

@param string  $key la clé
@param integer $ttl ttl en seconde

@throws Exception
@return int


public function flush()
    {
        $pattern = $this->getPatternKey();
        $arrReturn = $this->runOnAllRedisServer(
            function ($redis) use ($pattern) {
                $wasDeleted = 0;
                $allKeys = $redis->keys($pattern.'*'); // all keys started with the namespace
                if (count($allKeys)) {
                    $wasDeleted = $redis->del($allKeys);
                }

                return array($wasDeleted);
            }
        );

        return array_sum($arrReturn);
    }

public
function
flush
(
)
{
$
pattern
=
$
this
->
getPatternKey
(
)
;
$
arrReturn
=
$
this
->
runOnAllRedisServer
(
function
(
$
redis
)
use
(
$
pattern
)
{
$
wasDeleted
=
0
;
$
allKeys
=
$
redis
->
keys
(
$
pattern
.
'*'
)
;
// all keys started with the namespace
if
(
count
(
$
allKeys
)
)
{
$
wasDeleted
=
$
redis
->
del
(
$
allKeys
)
;
}
return
array
(
$
wasDeleted
)
;
}
)
;
return
array_sum
(
$
arrReturn
)
;
}

use for debugging !

flush all keys in the namespace
@return integer number of deleted keys


public function exec()
    {
        if (true === $this->multi) {
            $ret         = array();
            $this->multi = false;
            // exec
            foreach ($this->execList as $todos) {
                $redis = $todos[0]['redis']; // ^^ not so secure ?
                $this->notifyEvent('multi', array());
                $redis->multi();
                foreach ($todos as $todo) {
                    if ($todo['function'] instanceof \Closure) {
                        $ret[] = $todo['function']($redis);
                    } else {
                        throw new Exception("Ce n'est pas une Closure !");
                    }
                }
                $this->notifyEvent('exec', array());
                $redis->exec();
            }
            $this->execList = array(); // purge the execList

            return $ret;
        } else {
            throw new Exception("you have to call multi before exec");
        }
    }

public
function
exec
(
)
{
if
(
true
===
$
this
->
multi
)
{
$
ret
=
array
(
)
;
$
this
->
multi
=
false
;
// exec
foreach
(
$
this
->
execList
as
$
todos
)
{
$
redis
=
$
todos
[
0
]
[
'redis'
]
;
// ^^ not so secure ?
$
this
->
notifyEvent
(
'multi'
,
array
(
)
)
;
$
redis
->
multi
(
)
;
foreach
(
$
todos
as
$
todo
)
{
if
(
$
todo
[
'function'
]
instanceof
\
Closure
)
{
$
ret
[
]
=
$
todo
[
'function'
]
(
$
redis
)
;
}
else
{
throw
new
Exception
(
"Ce n'est pas une Closure !"
)
;
}
}
$
this
->
notifyEvent
(
'exec'
,
array
(
)
)
;
$
redis
->
exec
(
)
;
}
$
this
->
execList
=
array
(
)
;
// purge the execList
return
$
ret
;
}
else
{
throw
new
Exception
(
"you have to call multi before exec"
)
;
}
}

execute multiple commands

@throws Exception
@return array outputs of the commands


public function dbSize($idServer = null)
    {
        if (is_null($idServer)) {
            $servers = $this->getServerConfig();

            $dbsize = array();
            foreach (array_keys($servers) as $idServer) {
                $redis = $this->getRedisFromServerConfig($idServer);

                $dbsize[$idServer] = $redis->dbsize();
            }
        } else {
            $redis = $this->getRedisFromServerConfig($idServer);

            $dbsize = $redis->dbsize();
        }

        return $dbsize;
    }

public function dbSize ( $ idServer = null ) { if ( is_null ( $ idServer ) ) { $ servers = $ this -> getServerConfig ( ) ; $ dbsize = array ( ) ; foreach ( array_keys ( $ servers ) as $ idServer ) { $ redis = $ this -> getRedisFromServerConfig ( $ idServer ) ; $ dbsize [ $ idServer ] = $ redis -> dbsize ( ) ; } } else { $ redis = $ this -> getRedisFromServerConfig ( $ idServer ) ; $ dbsize = $ redis -> dbsize ( ) ; } return $ dbsize ; }

return an array or server with the number of keys stored on each

@param string $idServer Id du server

@return array / integer


protected function addToExecList($key, \Closure $func)
    {
        $redis = $this->getRedis($key);
        $this->execList[md5(serialize($redis))][] = array(
            'key'      => $key,
            'function' => $func,
            'redis'    => $redis
        );
    }

protected function addToExecList ( $ key , \ Closure $ func ) { $ redis = $ this -> getRedis ( $ key ) ; $ this -> execList [ md5 ( serialize ( $ redis ) ) ] [ ] = array ( 'key' => $ key , 'function' => $ func , 'redis' => $ redis ) ; }

add a task to the execution list

@param string   $key  clé
@param callable $func closure à ajouter

@return void


protected function dispatch($eventName, BaseConsoleEvent $e)
    {
        if (!is_null($this->eventDispatcher)) {
            $class = str_replace(
                'Symfony\Component\Console\Event',
                'M6Web\Bundle\StatsdBundle\Event',
                get_class($e)
            );

            $finaleEvent = $class::createFromConsoleEvent(
                $e,
                $this->startTime,
                !is_null($this->startTime) ? microtime(true) - $this->startTime : null
            );

            return $this->eventDispatcher->dispatch($eventName, $finaleEvent);
        } else {
            return false;
        }
    }

protected function dispatch ( $ eventName , BaseConsoleEvent $ e ) { if ( ! is_null ( $ this -> eventDispatcher ) ) { $ class = str_replace ( 'Symfony\Component\Console\Event' , 'M6Web\Bundle\StatsdBundle\Event' , get_class ( $ e ) ) ; $ finaleEvent = $ class :: createFromConsoleEvent ( $ e , $ this -> startTime , ! is_null ( $ this -> startTime ) ? microtime ( true ) - $ this -> startTime : null ) ; return $ this -> eventDispatcher -> dispatch ( $ eventName , $ finaleEvent ) ; } else { return false ; } }

Dispatch custom event

@param string           $eventName
@param BaseConsoleEvent $e

@return boolean


private function addTiming($event, $timingMethod, $node, $tags = [])
    {
        $timing = $this->getEventValue($event, $timingMethod);
        if ($timing > 0) {
            $this->timing($node, $timing, 1, $tags);
        }
    }

private function addTiming ( $ event , $ timingMethod , $ node , $ tags = [ ] ) { $ timing = $ this -> getEventValue ( $ event , $ timingMethod ) ; if ( $ timing > 0 ) { $ this -> timing ( $ node , $ timing , 1 , $ tags ) ; } }

Factorisation of the timing method
find the value timed

@param object $event Event
@param string $timingMethod Callable method in the event
@param string $node Node
@param array  $tags Tags key => value for influxDb

@throws Exception


private function replaceConfigPlaceholder($event, $eventName, $string)
    {
        // `event->getName()` is deprecated, we have to replace <name> directly with $eventName
        $string = str_replace('<name>', $eventName, $string);

        if ((preg_match_all('/<([^>]*)>/', $string, $matches) > 0) and ($this->propertyAccessor !== null)) {
            $tokens = $matches[1];
            foreach ($tokens as $token) {
                $value = $this->propertyAccessor->getValue($event, $token);
                $string = str_replace('<'.$token.'>', $value, $string);
            }
        }

        return $string;
    }

Replaces a string with a method name

@param EventInterface $event An event
@param string         $eventName The name of the event
@param string         $string  The node in which the replacing will happen

@return string


private function mergeTags($event, $config)
    {
        $configTags = isset($config['tags']) ? $config['tags'] : [];

        if ($event instanceof MonitorableEventInterface) {
            return array_merge($configTags, $event->getTags());
        }

        return $configTags;
    }

private function mergeTags ( $ event , $ config ) { $ configTags = isset ( $ config [ 'tags' ] ) ? $ config [ 'tags' ] : [ ] ; if ( $ event instanceof MonitorableEventInterface ) { return array_merge ( $ configTags , $ event -> getTags ( ) ) ; } return $ configTags ; }

Merge config tags with tags manually sent with the event

@param mixed $event
@param array $config

@return array of tags


private function dispatchMemory()
    {
        $memory = memory_get_peak_usage(true);
        $memory = ($memory > 1024 ? intval($memory / 1024) : 0);

        $this->eventDispatcher->dispatch(
            'statsd.memory_usage',
            new StatsdEvent($memory)
        );
    }

private function dispatchMemory ( ) { $ memory = memory_get_peak_usage ( true ) ; $ memory = ( $ memory > 1024 ? intval ( $ memory / 1024 ) : 0 ) ; $ this -> eventDispatcher -> dispatch ( 'statsd.memory_usage' , new StatsdEvent ( $ memory ) ) ; }


private function dispatchRequestTime(PostResponseEvent $event)
    {
        $request   = $event->getRequest();
        $startTime = $request->server->get('REQUEST_TIME_FLOAT', $request->server->get('REQUEST_TIME'));
        $time      = microtime(true) - $startTime;
        $time      = round($time * 1000);

        $this->eventDispatcher->dispatch(
            'statsd.time',
            new StatsdEvent($time)
        );
    }

private function dispatchRequestTime ( PostResponseEvent $ event ) { $ request = $ event -> getRequest ( ) ; $ startTime = $ request -> server -> get ( 'REQUEST_TIME_FLOAT' , $ request -> server -> get ( 'REQUEST_TIME' ) ) ; $ time = microtime ( true ) - $ startTime ; $ time = round ( $ time * 1000 ) ; $ this -> eventDispatcher -> dispatch ( 'statsd.time' , new StatsdEvent ( $ time ) ) ; }

dispatchRequestTime dispatch the request time.
This time is a "fake" one, because some actions are performed before the initialization of the request
It is ~100ms smaller than the real kernel time.

@param PostResponseEvent $event

dispatchRequestTime dispatch the request time . This time is a fake one because some actions are performed before the initialization of the request It is ~100ms smaller than the real kernel time .


public static function createFromConsoleEvent(BaseConsoleEvent $e, $startTime = null, $executionTime = null)
    {
        if (static::support($e)) {
            return new static($e, $startTime, $executionTime);
        } else {
            throw \InvalidArgumentException('Invalid event type.');
        }
    }

public
static
function
createFromConsoleEvent
(
BaseConsoleEvent
$
e
,
$
startTime
=
,
$
executionTime
=
)
{
if
(
static
::
support
(
$
e
)
)
{
return
new
static
(
$
e
,
$
startTime
,
$
executionTime
)
;
}
else
{
throw
\
InvalidArgumentException
(
'Invalid event type.'
)
;
}
}

Create new event object

@param BaseConsoleEvent $e
@param float            $startTime
@param float            $executionTime

@return ConsoleEvent

@throws \InvalidArgumentException


protected function srid()
    {
        $this->match(Lexer::T_SRID);
        $this->match(Lexer::T_EQUALS);
        $this->match(Lexer::T_INTEGER);

        $srid = $this->lexer->value();

        $this->match(Lexer::T_SEMICOLON);

        return $srid;
    }

protected function srid ( ) { $ this -> match ( Lexer :: T_SRID ) ; $ this -> match ( Lexer :: T_EQUALS ) ; $ this -> match ( Lexer :: T_INTEGER ) ; $ srid = $ this -> lexer -> value ( ) ; $ this -> match ( Lexer :: T_SEMICOLON ) ; return $ srid ; }


protected function geometry()
    {
        $type       = $this->type();
        $this->type = $type;

        if ($this->lexer->isNextTokenAny(array(Lexer::T_Z, Lexer::T_M, Lexer::T_ZM))) {
            $this->match($this->lexer->lookahead['type']);

            $this->dimension = $this->lexer->value();
        }

        $this->match(Lexer::T_OPEN_PARENTHESIS);

        $value = $this->$type();

        $this->match(Lexer::T_CLOSE_PARENTHESIS);

        return array(
            'type'  => $type,
            'value' => $value
        );
    }

protected function geometry ( ) { $ type = $ this -> type ( ) ; $ this -> type = $ type ; if ( $ this -> lexer -> isNextTokenAny ( array ( Lexer :: T_Z , Lexer :: T_M , Lexer :: T_ZM ) ) ) { $ this -> match ( $ this -> lexer -> lookahead [ 'type' ] ) ; $ this -> dimension = $ this -> lexer -> value ( ) ; } $ this -> match ( Lexer :: T_OPEN_PARENTHESIS ) ; $ value = $ this -> $ type ( ) ; $ this -> match ( Lexer :: T_CLOSE_PARENTHESIS ) ; return array ( 'type' => $ type , 'value' => $ value ) ; }

Match spatial geometry object

@return array


protected function point()
    {
        if (null !== $this->dimension) {
            return $this->coordinates(2 + strlen($this->dimension));
        }

        $values = $this->coordinates(2);

        for ($i = 3; $i <= 4 && $this->lexer->isNextTokenAny(array(Lexer::T_FLOAT, Lexer::T_INTEGER)); $i++) {
            $values[] = $this->coordinate();
        }

        switch (count($values)) {
            case 2:
                $this->dimension = '';
                break;
            case 3:
                $this->dimension = 'Z';
                break;
            case 4:
                $this->dimension = 'ZM';
                break;
        }

        return $values;
    }

protected function point ( ) { if ( null !== $ this -> dimension ) { return $ this -> coordinates ( 2 + strlen ( $ this -> dimension ) ) ; } $ values = $ this -> coordinates ( 2 ) ; for ( $ i = 3 ; $ i <= 4 && $ this -> lexer -> isNextTokenAny ( array ( Lexer :: T_FLOAT , Lexer :: T_INTEGER ) ) ; $ i ++ ) { $ values [ ] = $ this -> coordinate ( ) ; } switch ( count ( $ values ) ) { case 2 : $ this -> dimension = '' ; break ; case 3 : $ this -> dimension = 'Z' ; break ; case 4 : $ this -> dimension = 'ZM' ; break ; } return $ values ; }


protected function coordinate()
    {
        $this->match(($this->lexer->isNextToken(Lexer::T_FLOAT) ? Lexer::T_FLOAT : Lexer::T_INTEGER));

        return $this->lexer->value();
    }

protected function coordinate ( ) { $ this -> match ( ( $ this -> lexer -> isNextToken ( Lexer :: T_FLOAT ) ? Lexer :: T_FLOAT : Lexer :: T_INTEGER ) ) ; return $ this -> lexer -> value ( ) ; }

Match a number and optional exponent

@return int|float


protected function pointList()
    {
        $points = array($this->point());

        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
            $this->match(Lexer::T_COMMA);

            $points[] = $this->point();
        }

        return $points;
    }

protected function pointList ( ) { $ points = array ( $ this -> point ( ) ) ; while ( $ this -> lexer -> isNextToken ( Lexer :: T_COMMA ) ) { $ this -> match ( Lexer :: T_COMMA ) ; $ points [ ] = $ this -> point ( ) ; } return $ points ; }

Match a list of coordinates

@return array[]


protected function pointLists()
    {
        $this->match(Lexer::T_OPEN_PARENTHESIS);

        $pointLists = array($this->pointList());

        $this->match(Lexer::T_CLOSE_PARENTHESIS);

        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
            $this->match(Lexer::T_COMMA);
            $this->match(Lexer::T_OPEN_PARENTHESIS);

            $pointLists[] = $this->pointList();

            $this->match(Lexer::T_CLOSE_PARENTHESIS);
        }

        return $pointLists;
    }

protected function pointLists ( ) { $ this -> match ( Lexer :: T_OPEN_PARENTHESIS ) ; $ pointLists = array ( $ this -> pointList ( ) ) ; $ this -> match ( Lexer :: T_CLOSE_PARENTHESIS ) ; while ( $ this -> lexer -> isNextToken ( Lexer :: T_COMMA ) ) { $ this -> match ( Lexer :: T_COMMA ) ; $ this -> match ( Lexer :: T_OPEN_PARENTHESIS ) ; $ pointLists [ ] = $ this -> pointList ( ) ; $ this -> match ( Lexer :: T_CLOSE_PARENTHESIS ) ; } return $ pointLists ; }

Match nested lists of coordinates

@return array[]


protected function multiPolygon()
    {
        $this->match(Lexer::T_OPEN_PARENTHESIS);

        $polygons = array($this->polygon());

        $this->match(Lexer::T_CLOSE_PARENTHESIS);

        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
            $this->match(Lexer::T_COMMA);
            $this->match(Lexer::T_OPEN_PARENTHESIS);

            $polygons[] = $this->polygon();

            $this->match(Lexer::T_CLOSE_PARENTHESIS);
        }

        return $polygons;
    }

protected function multiPolygon ( ) { $ this -> match ( Lexer :: T_OPEN_PARENTHESIS ) ; $ polygons = array ( $ this -> polygon ( ) ) ; $ this -> match ( Lexer :: T_CLOSE_PARENTHESIS ) ; while ( $ this -> lexer -> isNextToken ( Lexer :: T_COMMA ) ) { $ this -> match ( Lexer :: T_COMMA ) ; $ this -> match ( Lexer :: T_OPEN_PARENTHESIS ) ; $ polygons [ ] = $ this -> polygon ( ) ; $ this -> match ( Lexer :: T_CLOSE_PARENTHESIS ) ; } return $ polygons ; }

Match MULTIPOLYGON value

@return array[]


protected function geometryCollection()
    {
        $collection = array($this->geometry());

        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
            $this->match(Lexer::T_COMMA);

            $collection[] = $this->geometry();
        }

        return $collection;
    }

protected function geometryCollection ( ) { $ collection = array ( $ this -> geometry ( ) ) ; while ( $ this -> lexer -> isNextToken ( Lexer :: T_COMMA ) ) { $ this -> match ( Lexer :: T_COMMA ) ; $ collection [ ] = $ this -> geometry ( ) ; } return $ collection ; }

Match GEOMETRYCOLLECTION value

@return array[]


protected function match($token)
    {
        $lookaheadType = $this->lexer->lookahead['type'];

        if ($lookaheadType !== $token && ($token !== Lexer::T_TYPE || $lookaheadType <= Lexer::T_TYPE)) {
            throw $this->syntaxError($this->lexer->getLiteral($token));
        }

        $this->lexer->moveNext();
    }

protected function match ( $ token ) { $ lookaheadType = $ this -> lexer -> lookahead [ 'type' ] ; if ( $ lookaheadType !== $ token && ( $ token !== Lexer :: T_TYPE || $ lookaheadType <= Lexer :: T_TYPE ) ) { throw $ this -> syntaxError ( $ this -> lexer -> getLiteral ( $ token ) ) ; } $ this -> lexer -> moveNext ( ) ; }

Match token at current position in input

@param $token


private function syntaxError($expected)
    {
        $expected = sprintf('Expected %s, got', $expected);
        $token    = $this->lexer->lookahead;
        $found    = null === $this->lexer->lookahead ? 'end of string.' : sprintf('"%s"', $token['value']);
        $message  = sprintf(
            '[Syntax Error] line 0, col %d: Error: %s %s in value "%s"',
            isset($token['position']) ? $token['position'] : '-1',
            $expected,
            $found,
            $this->input
        );

        return new UnexpectedValueException($message);
    }

Create exception with descriptive error message

@param string $expected

@return UnexpectedValueException


protected function createResponseParts($responseParts) {
    if ($responseParts === null) {
      return array();
    }
    $responses = array();
    foreach ($responseParts as $responsePart) {
      $responses[] = new Response($responsePart);
    }
    return $responses;
  }

protected function createResponseParts ( $ responseParts ) { if ( $ responseParts === null ) { return array ( ) ; } $ responses = array ( ) ; foreach ( $ responseParts as $ responsePart ) { $ responses [ ] = new Response ( $ responsePart ) ; } return $ responses ; }

Creates an array of Response objects from a given response array
@param $responseParts
@return array


public function getTime() {
    if (isset($this->data['time'])) {
      $requestTime = new \DateTime();
      //TODO: fix the microseconds to miliseconds
      $requestTime->createFromFormat("Y-m-dTH:i:s.uZ", $this->data["time"]);
      return $requestTime;
    }
    return null;
  }

Returns if exists the request time
@return \DateTime


protected function createApiClient() {
    // Provide a BC switch between guzzle 5 and guzzle 6.
    if (class_exists('GuzzleHttp\Psr7\Response')) {
      $this->apiClient = new Client(array("base_uri" => $this->getPhantomJSHost()));
    }
    else {
      $this->apiClient = new Client(array("base_url" => $this->getPhantomJSHost()));
    }
  }

Creates an http client to consume the phantomjs API


public function command() {
    try {
      $args = func_get_args();
      $commandName = $args[0];
      array_shift($args);
      $messageToSend = json_encode(array('name' => $commandName, 'args' => $args));
      /** @var $commandResponse \GuzzleHttp\Psr7\Response|\GuzzleHttp\Message\Response */
      $commandResponse = $this->getApiClient()->post("/api", array("body" => $messageToSend));
      $jsonResponse = json_decode($commandResponse->getBody(), TRUE);
    } catch (ServerException $e) {
      $jsonResponse = json_decode($e->getResponse()->getBody()->getContents(), true);
    } catch (ConnectException $e) {
      throw new DeadClient($e->getMessage(), $e->getCode(), $e);
    } catch (\Exception $e) {
      throw $e;
    }

    if (isset($jsonResponse['error'])) {
      throw $this->getErrorClass($jsonResponse);
    }

    return $jsonResponse['response'];
  }

Sends a command to the browser
@throws BrowserError
@throws \Exception
@return mixed


public function getRedirectUrl() {
    if (isset($this->data['redirectUrl']) && !empty($this->data['redirectUrl'])) {
      return $this->data['redirectUrl'];
    }
    return null;
  }

public function getRedirectUrl ( ) { if ( isset ( $ this -> data [ 'redirectUrl' ] ) && ! empty ( $ this -> data [ 'redirectUrl' ] ) ) { return $ this -> data [ 'redirectUrl' ] ; } return null ; }

Get redirect url if response is a redirect
@return string


protected function checkRenderOptions($options) {
    //Default is full and no selection
    if (count($options) === 0) {
      $options["full"] = true;
      $options["selector"] = null;
    }

    if (isset($options["full"]) && isset($options["selector"])) {
      if ($options["full"]) {
        //Whatever it is, full is more powerful than selection
        $options["selector"] = null;
      }
    } else {
      if (!isset($options["full"]) && isset($options["selector"])) {
        $options["full"] = false;
      }
    }
    return $options;
  }

Check and fix render options
@param $options
@return mixed


public function render($path, $options = array()) {
    $fixedOptions = $this->checkRenderOptions($options);
    return $this->command('render', $path, $fixedOptions["full"], $fixedOptions["selector"]);
  }

public function render ( $ path , $ options = array ( ) ) { $ fixedOptions = $ this -> checkRenderOptions ( $ options ) ; return $ this -> command ( 'render' , $ path , $ fixedOptions [ "full" ] , $ fixedOptions [ "selector" ] ) ; }

Renders a page or selection to a file given by path
@param string $path
@param array  $options
@return mixed


public function renderBase64($imageFormat, $options = array()) {
    $fixedOptions = $this->checkRenderOptions($options);
    return $this->command('render_base64', $imageFormat, $fixedOptions["full"], $fixedOptions["selector"]);
  }

public function renderBase64 ( $ imageFormat , $ options = array ( ) ) { $ fixedOptions = $ this -> checkRenderOptions ( $ options ) ; return $ this -> command ( 'render_base64' , $ imageFormat , $ fixedOptions [ "full" ] , $ fixedOptions [ "selector" ] ) ; }

Renders base64 a page or selection to a file given by path
@param string $imageFormat (PNG, GIF, JPEG)
@param array  $options
@return mixed


public function find($method, $selector) {
    $result = $this->command('find', $method, $selector);
    $found["page_id"] = $result["page_id"];
    foreach ($result["ids"] as $id) {
      $found["ids"][] = $id;
    }
    return $found;
  }

public function find ( $ method , $ selector ) { $ result = $ this -> command ( 'find' , $ method , $ selector ) ; $ found [ "page_id" ] = $ result [ "page_id" ] ; foreach ( $ result [ "ids" ] as $ id ) { $ found [ "ids" ] [ ] = $ id ; } return $ found ; }

Find elements given a method and a selector
@param $method
@param $selector
@return array


public function findWithin($pageId, $elementId, $method, $selector) {
    return $this->command('find_within', $pageId, $elementId, $method, $selector);
  }

public function findWithin ( $ pageId , $ elementId , $ method , $ selector ) { return $ this -> command ( 'find_within' , $ pageId , $ elementId , $ method , $ selector ) ; }

Find elements within a page, method and selector
@param $pageId
@param $elementId
@param $method
@param $selector
@return mixed

Find elements within a page method and selector


public function setAttribute($pageId, $elementId, $name, $value) {
    return $this->command('set_attribute', $pageId, $elementId, $name, $value);
  }

public function setAttribute ( $ pageId , $ elementId , $ name , $ value ) { return $ this -> command ( 'set_attribute' , $ pageId , $ elementId , $ name , $ value ) ; }

Set an attribute to the given element in the given page
@param $pageId
@param $elementId
@param $name
@param $value
@return mixed
@throws \Zumba\GastonJS\Exception\BrowserError
@throws \Exception


public function keyEvent($pageId, $elementId, $keyEvent, $key, $modifier) {
    return $this->command("key_event", $pageId, $elementId, $keyEvent, $key, $modifier);
  }

public function keyEvent ( $ pageId , $ elementId , $ keyEvent , $ key , $ modifier ) { return $ this -> command ( "key_event" , $ pageId , $ elementId , $ keyEvent , $ key , $ modifier ) ; }

Sends the order to execute a key event on a given element
@param $pageId
@param $elementId
@param $keyEvent
@param $key
@param $modifier
@return mixed


public function selectOption($pageId, $elementId, $value, $multiple = false) {
    return $this->command("select_option", $pageId, $elementId, $value, $multiple);
  }

public function selectOption ( $ pageId , $ elementId , $ value , $ multiple = false ) { return $ this -> command ( "select_option" , $ pageId , $ elementId , $ value , $ multiple ) ; }

Sends the command to select and option given a value
@param      $pageId
@param      $elementId
@param      $value
@param bool $multiple
@return mixed


public function debug($enable = false) {
    $this->debug = $enable;
    return $this->command('set_debug', $this->debug);
  }

public function debug ( $ enable = false ) { $ this -> debug = $ enable ; return $ this -> command ( 'set_debug' , $ this -> debug ) ; }

Set the debug mode on the browser
@param bool $enable
@return bool


public function setProxy($proxy)
  {
    $args = array('set_proxy');
    if ($proxy !== false)
    {
      if (preg_match('~^(http|socks5)://(?:([^:@/]*?):([^:@/]*?)@)?([^:@/]+):(\d+)$~', $proxy, $components))
      {
        array_push($args, $components[4], intval($components[5], 10), $components[1]);
        if (strlen($components[2]) || strlen($components[3]))
        {
          array_push($args, urldecode($components[2]), urldecode($components[3]));
        }
      }
      else
      {
        throw new \UnexpectedValueException('Invalid proxy url ' . $proxy);
      }
    }
    return call_user_func_array(array($this, 'command'), $args);
  }

Sets or unsets web proxy.

@param string|false $proxy proxy url formatted as '(http|socks5)://[username:password@]host:port', or false to unset
@return bool
@throws \UnexpectedValueException when the proxy url is invalid


public function networkTraffic() {
    $networkTraffic = $this->command('network_traffic');
    $requestTraffic = array();

    if (count($networkTraffic) === 0) {
      return null;
    }

    foreach ($networkTraffic as $traffic) {
      $requestTraffic[] = new Request($traffic["request"], $traffic["responseParts"]);
    }

    return $requestTraffic;
  }

public function networkTraffic ( ) { $ networkTraffic = $ this -> command ( 'network_traffic' ) ; $ requestTraffic = array ( ) ; if ( count ( $ networkTraffic ) === 0 ) { return null ; } foreach ( $ networkTraffic as $ traffic ) { $ requestTraffic [ ] = new Request ( $ traffic [ "request" ] , $ traffic [ "responseParts" ] ) ; } return $ requestTraffic ; }

Get all the network traffic that the page have created
@return array


public function cookies() {
    $cookies = $this->command('cookies');
    $objCookies = array();
    foreach ($cookies as $cookie) {
      $objCookies[$cookie["name"]] = new Cookie($cookie);
    }
    return $objCookies;
  }

public function cookies ( ) { $ cookies = $ this -> command ( 'cookies' ) ; $ objCookies = array ( ) ; foreach ( $ cookies as $ cookie ) { $ objCookies [ $ cookie [ "name" ] ] = new Cookie ( $ cookie ) ; } return $ objCookies ; }

Gets the cookies on the browser

@return Cookie[]


public function setCookie($cookie) {
    //TODO: add error control when the cookie array is not valid
    if (isset($cookie["expires"])) {
      $cookie["expires"] = intval($cookie["expires"]) * 1000;
    }
    $cookie['value'] = urlencode($cookie['value']);
    return $this->command('set_cookie', $cookie);
  }

Sets a cookie on the browser, expires times is set in seconds
@param $cookie
@return mixed

Sets a cookie on the browser expires times is set in seconds


public function javascriptErrors() {
    $jsErrors = array();
    $errors = $this->response["error"]["args"][0];
    foreach ($errors as $error) {
      $jsErrors[] = new JSErrorItem($error["message"], $error["stack"]);
    }
    return $jsErrors;
  }

public function javascriptErrors ( ) { $ jsErrors = array ( ) ; $ errors = $ this -> response [ "error" ] [ "args" ] [ 0 ] ; foreach ( $ errors as $ error ) { $ jsErrors [ ] = new JSErrorItem ( $ error [ "message" ] , $ error [ "stack" ] ) ; } return $ jsErrors ; }

Get the javascript errors found during the use of the phantomjs
@return array


public function message() {
    $error = "One or more errors were raised in the Javascript code on the page.
            If you don't care about these errors, you can ignore them by
            setting js_errors: false in your Poltergeist configuration (see documentation for details).";
    //TODO: add javascript errors
    $jsErrors = $this->javascriptErrors();
    foreach($jsErrors as $jsError){
      $error = "$error\n$jsError";
    }
    return $error;
  }

Returns the javascript errors found
@return string


private function compileNode(
        bool $isCompilingHostTrie,
        TrieNode $currTrieNode,
        AstNode $ast,
        Route $route,
        ?TrieNode $hostTrie
    ): void {
        $astChildren = $ast->children;
        $numAstChildren = \count($astChildren);
        $isEndpoint = false;
        $segmentContainsVariable = false;
        $segmentBuffer = [];

        foreach ($isCompilingHostTrie ? \array_reverse($astChildren) : $astChildren as $i => $childAstNode) {
            /**
             * This isn't an endpoint if we're compiling a path trie which has a host trie
             * This is an endpoint if it's the last node or the last non-optional node
             */
            $isEndpoint = $isEndpoint
                || (
                    ($isCompilingHostTrie || $hostTrie === null)
                    && (
                        $i === $numAstChildren - 1
                        || $childAstNode->type === AstNodeTypes::OPTIONAL_ROUTE_PART
                        || ($astChildren[$i + 1]->type === AstNodeTypes::OPTIONAL_ROUTE_PART)
                    )
                );

            switch ($childAstNode->type) {
                case AstNodeTypes::SEGMENT_DELIMITER:
                    // Checking if this is an endpoint handles the case of a route at the root path
                    if ($isEndpoint || \count($segmentBuffer) > 0) {
                        $newTrieNode = self::createTrieNode(
                            $segmentBuffer,
                            $segmentContainsVariable,
                            $isEndpoint,
                            $route,
                            $hostTrie
                        );
                        $currTrieNode->addChild($newTrieNode);
                        $currTrieNode = $newTrieNode;
                    }

                    break;
                case AstNodeTypes::OPTIONAL_ROUTE_PART:
                    // Handles flushing 'foo' in the case of 'foo[/bar]'
                    if (\count($segmentBuffer) > 0) {
                        $newTrieNode = self::createTrieNode(
                            $segmentBuffer,
                            $segmentContainsVariable,
                            $isEndpoint,
                            $route,
                            $hostTrie
                        );
                        $currTrieNode->addChild($newTrieNode);
                        $currTrieNode = $newTrieNode;
                    }

                    $this->compileNode($isCompilingHostTrie, $currTrieNode, $childAstNode, $route, $hostTrie);
                    break;
                case AstNodeTypes::TEXT:
                    $segmentBuffer[] = $childAstNode->value;
                    break;
                case AstNodeTypes::VARIABLE:
                    $segmentContainsVariable = true;
                    $segmentBuffer[] = $this->compileVariableNode($childAstNode);
                    break;
                default:
                    throw new InvalidArgumentException("Unexpected node type {$childAstNode->type}");
            }
        }

        // Check if we need to flush the buffer
        if (\count($segmentBuffer) > 0) {
            $currTrieNode->addChild(
                self::createTrieNode($segmentBuffer, $segmentContainsVariable, $isEndpoint, $route, $hostTrie)
            );
        }
    }

Creates a new node

@param bool $isCompilingHostTrie Whether or not we're compiling a host trie
@param TrieNode $currTrieNode The current node
@param AstNode $ast The current AST node
@param Route $route The current route
@param TrieNode|null $hostTrie The host trie if one should be included, otherwise null


private function compileVariableNode(AstNode $astNode): RouteVariable
    {
        $rules = [];

        foreach ($astNode->children as $childAstNode) {
            if ($childAstNode->type !== AstNodeTypes::VARIABLE_RULE) {
                throw new InvalidArgumentException("Unexpected node type {$childAstNode->type}");
            }

            $ruleParams = $childAstNode->hasChildren() ? $childAstNode->children[0]->value : [];
            $rules[] = $this->ruleFactory->createRule($childAstNode->value, $ruleParams);
        }

        return new RouteVariable($astNode->value, $rules);
    }

Compiles a variable node

@param AstNode $astNode The variable node to compile
@return RouteVariable The created route variable


private static function createTrieNode(
        array &$segmentBuffer,
        bool &$segmentContainsVariable,
        bool $isEndpoint,
        Route $route,
        ?TrieNode $hostTrie
    ): TrieNode {
        $routes = $isEndpoint ? $route : [];

        if ($segmentContainsVariable) {
            $node = new VariableTrieNode($segmentBuffer, [], $routes, $hostTrie);
        } else {
            $node = new LiteralTrieNode(\implode('', $segmentBuffer), [], $routes, $hostTrie);
        }

        // Clear the buffer data
        $segmentBuffer = [];
        $segmentContainsVariable = false;

        return $node;
    }

private
static
function
createTrieNode
(
array
&
$
segmentBuffer
,
bool
&
$
segmentContainsVariable
,
bool
$
isEndpoint
,
Route
$
route
,
TrieNode
$
hostTrie
)
:
TrieNode
{
$
routes
=
$
isEndpoint
$
route
:
[
]
;
if
(
$
segmentContainsVariable
)
{
$
node
=
new
VariableTrieNode
(
$
segmentBuffer
,
[
]
,
$
routes
,
$
hostTrie
)
;
}
else
{
$
node
=
new
LiteralTrieNode
(
\
implode
(
''
,
$
segmentBuffer
)
,
[
]
,
$
routes
,
$
hostTrie
)
;
}
// Clear the buffer data
$
segmentBuffer
=
[
]
;
$
segmentContainsVariable
=
false
;
return
$
node
;
}

Creates a trie node

@param string[]|RouteVariable[] $segmentBuffer The current buffer of parts (eg text or RouteVariables)
@param bool $segmentContainsVariable Whether or not the segment contains a variable
@param bool $isEndpoint Whether or not this node is an endpoint
@param Route $route The current route
@param TrieNode|null $hostTrie The host trie
@return TrieNode The created node


private static function flushTextBuffer(string &$textBuffer, array &$tokens): void
    {
        if ($textBuffer !== '') {
            $tokens[] = new Token(TokenTypes::T_TEXT, $textBuffer);
            $textBuffer = '';
        }
    }

private static function flushTextBuffer ( string & $ textBuffer , array & $ tokens ) : void { if ( $ textBuffer !== '' ) { $ tokens [ ] = new Token ( TokenTypes :: T_TEXT , $ textBuffer ) ; $ textBuffer = '' ; } }

Flushes any text from the buffer

@param string $textBuffer The current text buffer
@param Token[] $tokens The list of tokens to add to


private static function lexNumber(string $number, array &$tokens, int &$cursor): void
    {
        $floatVal = (float)$number;
        $intVal = (int)$number;

        // Determine if this was a float or not
        if ($floatVal && $intVal != $floatVal) {
            $tokens[] = new Token(TokenTypes::T_NUMBER, $floatVal);
        } else {
            $tokens[] = new Token(TokenTypes::T_NUMBER, $intVal);
        }

        $cursor += \mb_strlen($number);
    }

private
static
function
lexNumber
(
string
$
number
,
array
&
$
tokens
,
int
&
$
cursor
)
:
void
{
$
floatVal
=
(
float
)
$
number
;
$
intVal
=
(
int
)
$
number
;
// Determine if this was a float or not
if
(
$
floatVal
&&
$
intVal
!=
$
floatVal
)
{
$
tokens
[
]
=
new
Token
(
TokenTypes
::
T_NUMBER
,
$
floatVal
)
;
}
else
{
$
tokens
[
]
=
new
Token
(
TokenTypes
::
T_NUMBER
,
$
intVal
)
;
}
$
cursor
+=
\
mb_strlen
(
$
number
)
;
}

Lexes a number lexeme

@param string $number The lexeme to add
@param Token[] $tokens The list of tokens to add to
@param int $cursor The current cursor


private static function lexPunctuation(string $punctuation, array &$tokens, int &$cursor): void
    {
        $tokens[] = new Token(TokenTypes::T_PUNCTUATION, $punctuation);
        $cursor++;
    }

private static function lexPunctuation ( string $ punctuation , array & $ tokens , int & $ cursor ) : void { $ tokens [ ] = new Token ( TokenTypes :: T_PUNCTUATION , $ punctuation ) ; $ cursor ++ ; }

Lexes a punctuation lexeme

@param string $punctuation The lexeme to add
@param Token[] $tokens The list of tokens to add to
@param int $cursor The current cursor


private static function lexQuotedString(string $quotedString, array &$tokens, int &$cursor): void
    {
        $tokens[] = new Token(TokenTypes::T_QUOTED_STRING, \stripcslashes(\substr(\trim($quotedString), 1, -1)));
        $cursor += \mb_strlen($quotedString);
    }

private static function lexQuotedString ( string $ quotedString , array & $ tokens , int & $ cursor ) : void { $ tokens [ ] = new Token ( TokenTypes :: T_QUOTED_STRING , \ stripcslashes ( \ substr ( \ trim ( $ quotedString ) , 1 , - 1 ) ) ) ; $ cursor += \ mb_strlen ( $ quotedString ) ; }

Lexes a quoted string lexeme

@param string $quotedString The lexeme to add
@param Token[] $tokens The list of tokens to add to
@param int $cursor The current cursor


private static function lexTextChar(string $char, string &$textBuffer, int &$cursor): void
    {
        $textBuffer .= $char;
        $cursor++;
    }

private static function lexTextChar ( string $ char , string & $ textBuffer , int & $ cursor ) : void { $ textBuffer .= $ char ; $ cursor ++ ; }

Lexes a text character lexeme

@param string $char The lexeme to add
@param string $textBuffer The text buffer to add to
@param int $cursor The current cursor


private static function lexVariableName(string $variableName, array &$tokens, int &$cursor): void
    {
        // Remove the colon before the variable name
        $trimmedVariableName = \substr($variableName, 1);

        if (\mb_strlen($trimmedVariableName) > self::VARIABLE_NAME_MAX_LENGTH) {
            throw new InvalidArgumentException("Variable name \"$trimmedVariableName\" exceeds the max length limit");
        }

        $tokens[] = new Token(TokenTypes::T_VARIABLE, $trimmedVariableName);
        // We have to advance the cursor the length of the untrimmed variable name
        $cursor += \mb_strlen($variableName);
    }

Lexes a variable name lexeme

@param string $variableName The lexeme to add
@param Token[] $tokens The list of tokens to add to
@param int $cursor The current cursor


public function add(Route $route): void
    {
        $this->routes[] = $route;

        if ($route->name !== null) {
            $this->namedRoutes[$route->name] =& $route;
        }
    }

public function add ( Route $ route ) : void { $ this -> routes [ ] = $ route ; if ( $ route -> name !== null ) { $ this -> namedRoutes [ $ route -> name ] = & $ route ; } }

Adds a route to the collection

@param Route $route The route to add


public function getNamedRoute(string $name): ?Route
    {
        if (!isset($this->namedRoutes[$name])) {
            return null;
        }

        return $this->namedRoutes[$name];
    }

public function getNamedRoute ( string $ name ) : ? Route { if ( ! isset ( $ this -> namedRoutes [ $ name ] ) ) { return null ; } return $ this -> namedRoutes [ $ name ] ; }

Gets the route with the input name

@param string $name The name to search for
@return Route|null The route with the input name if one existed, otherwise null


public function createTrie(): TrieNode
    {
        if ($this->trieCache !== null && ($trie = $this->trieCache->get()) !== null) {
            return $trie;
        }

        // Need to generate the trie
        $trie = new RootTrieNode();

        foreach ($this->routeFactory->createRoutes()->getAll() as $route) {
            foreach ($this->trieCompiler->compile($route)->getAllChildren() as $childNode) {
                $trie->addChild($childNode);
            }
        }

        // Save this to cache for next time
        if ($this->trieCache !== null) {
            $this->trieCache->set($trie);
        }

        return $trie;
    }

public
function
createTrie
(
)
:
TrieNode
{
if
(
$
this
->
trieCache
!==
&&
(
$
trie
=
$
this
->
trieCache
->
get
(
)
)
!==
)
{
return
$
trie
;
}
// Need to generate the trie
$
trie
=
new
RootTrieNode
(
)
;
foreach
(
$
this
->
routeFactory
->
createRoutes
(
)
->
getAll
(
)
as
$
route
)
{
foreach
(
$
this
->
trieCompiler
->
compile
(
$
route
)
->
getAllChildren
(
)
as
$
childNode
)
{
$
trie
->
addChild
(
$
childNode
)
;
}
}
// Save this to cache for next time
if
(
$
this
->
trieCache
!==
)
{
$
this
->
trieCache
->
set
(
$
trie
)
;
}
return
$
trie
;
}

Creates a trie

@return TrieNode The trie


private static function normalizeName($name): string
    {
        $dashedName = \str_replace('_', '-', $name);

        if (\stripos($dashedName, 'HTTP-') === 0) {
            $dashedName = \substr($dashedName, 5);
        }

        return $dashedName;
    }

private static function normalizeName ( $ name ) : string { $ dashedName = \ str_replace ( '_' , '-' , $ name ) ; if ( \ stripos ( $ dashedName , 'HTTP-' ) === 0 ) { $ dashedName = \ substr ( $ dashedName , 5 ) ; } return $ dashedName ; }

Normalizes a name

@param string $name The name to normalize
@return string The normalized name


public function expect(string $type, $value = null, string $message = null): void
    {
        if ($this->test($type, $value)) {
            return;
        }

        $currentToken = $this->getCurrent();

        if ($message === null) {
            // Let's create a default message
            $formattedMessage = \sprintf(
                'Expected token type %s%s',
                $type,
                $value === null ? '' : " with value \"$value\""
            );

            if ($currentToken === null) {
                $formattedMessage .= ', got end of stream';
            } else {
                $formattedMessage .= \sprintf(
                    ', got %s with value \"%s\"',
                    $currentToken->type,
                    $currentToken->value
                );
            }
        } else {
            $formattedMessage = \sprintf(
                $message,
                $currentToken === null ? 'T_EOF' : $currentToken->type,
                $currentToken === null ? 'end of stream' : $currentToken->value
            );
        }

        throw new InvalidArgumentException($formattedMessage);
    }

public
function
expect
(
string
$
type
,
$
value
=
,
string
$
message
=
)
:
void
{
if
(
$
this
->
test
(
$
type
,
$
value
)
)
{
return
;
}
$
currentToken
=
$
this
->
getCurrent
(
)
;
if
(
$
message
===
)
{
// Let's create a default message
$
formattedMessage
=
\
sprintf
(
'Expected token type %s%s'
,
$
type
,
$
value
===
''
:
" with value \"$value\""
)
;
if
(
$
currentToken
===
)
{
$
formattedMessage
.=
', got end of stream'
;
}
else
{
$
formattedMessage
.=
\
sprintf
(
', got %s with value \"%s\"'
,
$
currentToken
->
type
,
$
currentToken
->
value
)
;
}
}
else
{
$
formattedMessage
=
\
sprintf
(
$
message
,
$
currentToken
===
'T_EOF'
:
$
currentToken
->
type
,
$
currentToken
===
'end of stream'
:
$
currentToken
->
value
)
;
}
throw
new
InvalidArgumentException
(
$
formattedMessage
)
;
}

Tests the current token to see if it matches the input type, and optionally the input value, and throws an
exception if the token did not match

@param string $type The type to check for
@param mixed $value The optional value to match against
@param string|null $message The exception message to use, otherwise a default one is generated
Any '%s' in the message is first populated with the expected type, and then with the expected value
@throws InvalidArgumentException Thrown if the current token didn't match the expected type and value

Tests the current token to see if it matches the input type and optionally the input value and throws an exception if the token did not match


public function getCurrent(): ?Token
    {
        return \count($this->tokens) > $this->cursor ? $this->tokens[$this->cursor] : null;
    }

public function getCurrent ( ) : ? Token { return \ count ( $ this -> tokens ) > $ this -> cursor ? $ this -> tokens [ $ this -> cursor ] : null ; }

Gets the current token

@return Token|null The current token


public function next(): ?Token
    {
        return \count($this->tokens) > ++$this->cursor ? $this->tokens[$this->cursor] : null;
    }

public function next ( ) : ? Token { return \ count ( $ this -> tokens ) > ++ $ this -> cursor ? $ this -> tokens [ $ this -> cursor ] : null ; }

Gets the next token, if there is one

@return Token|null The next token, if there is one, otherwise false


public function nextIfType(string $type, $value = null): bool
    {
        $currentToken = $this->getCurrent();
        $typeMatches = $currentToken !== null && $currentToken->type === $type;

        if ($typeMatches && ($value === null || $currentToken->value === $value)) {
            $this->next();

            return true;
        }

        return false;
    }

public function nextIfType ( string $ type , $ value = null ) : bool { $ currentToken = $ this -> getCurrent ( ) ; $ typeMatches = $ currentToken !== null && $ currentToken -> type === $ type ; if ( $ typeMatches && ( $ value === null || $ currentToken -> value === $ value ) ) { $ this -> next ( ) ; return true ; } return false ; }

Gets the next token if the current one matches the input type, and optionally performs a value check

@param string $type The type to check for
@param mixed $value The optional value to match against
@return bool True if the current token is of the input type, otherwise false

Gets the next token if the current one matches the input type and optionally performs a value check


public function peek(int $lookahead = 1): ?Token
    {
        if ($this->cursor + $lookahead >= \count($this->tokens)) {
            return null;
        }

        return $this->tokens[$this->cursor + $lookahead];
    }

public function peek ( int $ lookahead = 1 ) : ? Token { if ( $ this -> cursor + $ lookahead >= \ count ( $ this -> tokens ) ) { return null ; } return $ this -> tokens [ $ this -> cursor + $ lookahead ] ; }

Peeks ahead at the next token

@param int $lookahead The number of tokens to look ahead
@return Token|null The token


public function addChild(AstNode $node): AstNode
    {
        $node->parent = $this;
        $this->children[] = $node;

        return $this;
    }

public function addChild ( AstNode $ node ) : AstNode { $ node -> parent = $ this ; $ this -> children [ ] = $ node ; return $ this ; }

Adds a child to this node

@param AstNode $node The child to add
@return AstNode Returns this for chaining


public function addChild(TrieNode $childNode): self
    {
        if ($childNode instanceof LiteralTrieNode) {
            $this->addLiteralChildNode($childNode);
        } elseif ($childNode instanceof VariableTrieNode) {
            $this->addVariableChildNode($childNode);
        } else {
            throw new InvalidArgumentException('Unexpected trie node type ' . \get_class($childNode));
        }

        return $this;
    }

public
function
addChild
(
TrieNode
$
childNode
)
:
self
{
if
(
$
childNode
instanceof
LiteralTrieNode
)
{
$
this
->
addLiteralChildNode
(
$
childNode
)
;
}
elseif
(
$
childNode
instanceof
VariableTrieNode
)
{
$
this
->
addVariableChildNode
(
$
childNode
)
;
}
else
{
throw
new
InvalidArgumentException
(
'Unexpected trie node type '
.
\
get_class
(
$
childNode
)
)
;
}
return
$
this
;
}

Adds a child node and recursively merges all its children, too

@param $childNode $node The node to add
@return self For chaining
@throws InvalidArgumentException Thrown if the node was an invalid type

Adds a child node and recursively merges all its children too


public function getAllChildren(): array
    {
        $children = [];

        foreach ($this->literalChildrenByValue as $childNode) {
            $children[] = $childNode;
        }

        foreach ($this->variableChildren as $childNode) {
            $children[] = $childNode;
        }

        return $children;
    }

public function getAllChildren ( ) : array { $ children = [ ] ; foreach ( $ this -> literalChildrenByValue as $ childNode ) { $ children [ ] = $ childNode ; } foreach ( $ this -> variableChildren as $ childNode ) { $ children [ ] = $ childNode ; } return $ children ; }

Gets all the child nodes

@return TrieNode[] The child nodes


private function addLiteralChildNode(LiteralTrieNode $childNode): void
    {
        // Stringify the value in case it's a number and we don't want PHP getting confused
        $valueAsString = \strtolower((string)$childNode->value);

        if (isset($this->literalChildrenByValue[$valueAsString])) {
            // A literal child already exists with this value, so merge the routes and add all its children
            $matchingChildNode = $this->literalChildrenByValue[$valueAsString];
            $matchingChildNode->routes = \array_merge(
                $matchingChildNode->routes,
                $childNode->routes
            );

            foreach ($childNode->getAllChildren() as $grandChildNode) {
                $matchingChildNode->addChild($grandChildNode);
            }
        } else {
            $this->literalChildrenByValue[$valueAsString] = $childNode;
        }
    }

Adds a literal child node

@param LiteralTrieNode $childNode The child node to add


private function addVariableChildNode(VariableTrieNode $childNode): void
    {
        // Try to find a variable child whose parts match the input child's parts
        // If we find one, then we merge its routes and add all its children
        $matchingChildNode = null;

        foreach ($this->variableChildren as $variableChildNode) {
            // Purposely doing a loose check here because we don't care about reference equality
            if ($variableChildNode->parts == $childNode->parts) {
                $matchingChildNode = $variableChildNode;
                $variableChildNode->routes = \array_merge($variableChildNode->routes, $childNode->routes);
                break;
            }
        }

        if ($matchingChildNode === null) {
            $this->variableChildren[] = $childNode;
        } else {
            foreach ($childNode->getAllChildren() as $grandChildNode) {
                $matchingChildNode->addChild($grandChildNode);
            }
        }
    }

Adds a variable child node

@param VariableTrieNode $childNode The child node to add


public function buildAll(): array
    {
        $builtRoutes = [];

        foreach ($this->routeBuilders as $routeBuilder) {
            $builtRoutes[] = $routeBuilder->build();
        }

        return $builtRoutes;
    }

public function buildAll ( ) : array { $ builtRoutes = [ ] ; foreach ( $ this -> routeBuilders as $ routeBuilder ) { $ builtRoutes [ ] = $ routeBuilder -> build ( ) ; } return $ builtRoutes ; }

Builds all the route builders in the registry

@return Route[] The list of routes built by this registry


public function group(RouteGroupOptions $groupOptions, Closure $callback): void
    {
        $this->groupOptionsStack[] = $groupOptions;
        $callback($this);
        \array_pop($this->groupOptionsStack);
    }

public function group ( RouteGroupOptions $ groupOptions , Closure $ callback ) : void { $ this -> groupOptionsStack [ ] = $ groupOptions ; $ callback ( $ this ) ; \ array_pop ( $ this -> groupOptionsStack ) ; }

Creates a group of routes that share similar options

@param RouteGroupOptions $groupOptions The list of options shared by all routes in the group
@param Closure $callback The callback that accepts an instance of this class


public function map(
        $httpMethods,
        string $pathTemplate,
        string $hostTemplate = null,
        bool $isHttpsOnly = false
    ): RouteBuilder {
        $this->applyGroupRouteTemplates($pathTemplate, $hostTemplate, $isHttpsOnly);
        $routeBuilder = new RouteBuilder(
            (array)$httpMethods,
            new UriTemplate($pathTemplate, $hostTemplate, $isHttpsOnly)
        );
        $this->applyGroupConstraints($routeBuilder);
        $this->applyGroupMiddleware($routeBuilder);
        $this->applyGroupAttributes($routeBuilder);
        $this->routeBuilders[] = $routeBuilder;

        return $routeBuilder;
    }

public function map ( $ httpMethods , string $ pathTemplate , string $ hostTemplate = null , bool $ isHttpsOnly = false ) : RouteBuilder { $ this -> applyGroupRouteTemplates ( $ pathTemplate , $ hostTemplate , $ isHttpsOnly ) ; $ routeBuilder = new RouteBuilder ( ( array ) $ httpMethods , new UriTemplate ( $ pathTemplate , $ hostTemplate , $ isHttpsOnly ) ) ; $ this -> applyGroupConstraints ( $ routeBuilder ) ; $ this -> applyGroupMiddleware ( $ routeBuilder ) ; $ this -> applyGroupAttributes ( $ routeBuilder ) ; $ this -> routeBuilders [ ] = $ routeBuilder ; return $ routeBuilder ; }

Creates a route builder with some values already set

@param array|string $httpMethods The HTTP method or list of methods the route uses
@param string $pathTemplate The path template
@param string|null $hostTemplate The host template
@param bool $isHttpsOnly Whether or not the route is HTTPS-only
@return RouteBuilder The configured route builder


private function applyGroupAttributes(RouteBuilder $routeBuilder): void
    {
        $groupAttributes = [];

        foreach ($this->groupOptionsStack as $groupOptions) {
            $groupAttributes = \array_merge($groupAttributes, $groupOptions->attributes);
        }

        $routeBuilder->withManyAttributes($groupAttributes);
    }

private function applyGroupAttributes ( RouteBuilder $ routeBuilder ) : void { $ groupAttributes = [ ] ; foreach ( $ this -> groupOptionsStack as $ groupOptions ) { $ groupAttributes = \ array_merge ( $ groupAttributes , $ groupOptions -> attributes ) ; } $ routeBuilder -> withManyAttributes ( $ groupAttributes ) ; }

Applies a group's attributes to the input route builder

@param RouteBuilder $routeBuilder The route builder to bind attributes to

Applies a group s attributes to the input route builder


private function applyGroupConstraints(RouteBuilder $routeBuilder): void
    {
        $groupConstraintBindings = [];

        foreach ($this->groupOptionsStack as $groupOptions) {
            $groupConstraintBindings = \array_merge($groupConstraintBindings, $groupOptions->constraints);
        }

        $routeBuilder->withManyConstraints($groupConstraintBindings);
    }

private function applyGroupConstraints ( RouteBuilder $ routeBuilder ) : void { $ groupConstraintBindings = [ ] ; foreach ( $ this -> groupOptionsStack as $ groupOptions ) { $ groupConstraintBindings = \ array_merge ( $ groupConstraintBindings , $ groupOptions -> constraints ) ; } $ routeBuilder -> withManyConstraints ( $ groupConstraintBindings ) ; }

Applies a group's constraints to the input route builder

@param RouteBuilder $routeBuilder The route builder to bind constraints to

Applies a group s constraints to the input route builder


private function applyGroupMiddleware(RouteBuilder $routeBuilder): void
    {
        $groupMiddlewareBindings = [];

        foreach ($this->groupOptionsStack as $groupOptions) {
            $groupMiddlewareBindings = \array_merge($groupMiddlewareBindings, $groupOptions->middlewareBindings);
        }

        $routeBuilder->withManyMiddleware($groupMiddlewareBindings);
    }

private function applyGroupMiddleware ( RouteBuilder $ routeBuilder ) : void { $ groupMiddlewareBindings = [ ] ; foreach ( $ this -> groupOptionsStack as $ groupOptions ) { $ groupMiddlewareBindings = \ array_merge ( $ groupMiddlewareBindings , $ groupOptions -> middlewareBindings ) ; } $ routeBuilder -> withManyMiddleware ( $ groupMiddlewareBindings ) ; }

Applies a group's middleware to the input route builder

@param RouteBuilder $routeBuilder The route builder to bind middleware to

Applies a group s middleware to the input route builder


private function applyGroupRouteTemplates(
        string &$pathTemplate,
        string &$hostTemplate = null,
        bool &$isHttpsOnly = false
    ): void {
        $groupPathTemplate = '';
        $groupHostTemplate = '';
        $groupIsHttpsOnly = false;

        foreach ($this->groupOptionsStack as $groupOptions) {
            $groupPathTemplate .= $groupOptions->pathTemplate;
            $groupHostTemplate = $groupOptions->hostTemplate . $groupHostTemplate;
            $groupIsHttpsOnly = $groupIsHttpsOnly || $groupOptions->isHttpsOnly;
        }

        $pathTemplate = $groupPathTemplate . $pathTemplate;
        $hostTemplate = ($hostTemplate ?? '') . $groupHostTemplate;
        $isHttpsOnly = $isHttpsOnly || $groupIsHttpsOnly;
    }

private function applyGroupRouteTemplates ( string & $ pathTemplate , string & $ hostTemplate = null , bool & $ isHttpsOnly = false ) : void { $ groupPathTemplate = '' ; $ groupHostTemplate = '' ; $ groupIsHttpsOnly = false ; foreach ( $ this -> groupOptionsStack as $ groupOptions ) { $ groupPathTemplate .= $ groupOptions -> pathTemplate ; $ groupHostTemplate = $ groupOptions -> hostTemplate . $ groupHostTemplate ; $ groupIsHttpsOnly = $ groupIsHttpsOnly || $ groupOptions -> isHttpsOnly ; } $ pathTemplate = $ groupPathTemplate . $ pathTemplate ; $ hostTemplate = ( $ hostTemplate ?? '' ) . $ groupHostTemplate ; $ isHttpsOnly = $ isHttpsOnly || $ groupIsHttpsOnly ; }

Applies all the group options to a route

@param string $pathTemplate The path template to apply settings to
@param string|null $hostTemplate The host template to apply settings to
@param bool $isHttpsOnly Whether or not the group is HTTPS-only


public function registerRuleFactories(IRuleFactory $ruleFactory): IRuleFactory
    {
        $ruleFactory->registerRuleFactory(AlphaRule::getSlug(), function () {
            return new AlphaRule();
        });
        $ruleFactory->registerRuleFactory(AlphanumericRule::getSlug(), function () {
            return new AlphanumericRule();
        });
        $ruleFactory->registerRuleFactory(BetweenRule::getSlug(), function ($min, $max, bool $isInclusive = true) {
            return new BetweenRule($min, $max, $isInclusive);
        });
        $ruleFactory->registerRuleFactory(DateRule::getSlug(), function ($formats) {
            return new DateRule($formats);
        });
        $ruleFactory->registerRuleFactory(InRule::getSlug(), function (array $acceptableValues) {
            return new InRule($acceptableValues);
        });
        $ruleFactory->registerRuleFactory(IntegerRule::getSlug(), function () {
            return new IntegerRule();
        });
        $ruleFactory->registerRuleFactory(NotInRule::getSlug(), function (array $unacceptableValues) {
            return new NotInRule($unacceptableValues);
        });
        $ruleFactory->registerRuleFactory(NumericRule::getSlug(), function () {
            return new NumericRule();
        });
        $ruleFactory->registerRuleFactory(RegexRule::getSlug(), function (string $regex) {
            return new RegexRule($regex);
        });
        $ruleFactory->registerRuleFactory(UuidV4Rule::getSlug(), function () {
            return new UuidV4Rule();
        });

        return $ruleFactory;
    }

public function registerRuleFactories ( IRuleFactory $ ruleFactory ) : IRuleFactory { $ ruleFactory -> registerRuleFactory ( AlphaRule :: getSlug ( ) , function ( ) { return new AlphaRule ( ) ; } ) ; $ ruleFactory -> registerRuleFactory ( AlphanumericRule :: getSlug ( ) , function ( ) { return new AlphanumericRule ( ) ; } ) ; $ ruleFactory -> registerRuleFactory ( BetweenRule :: getSlug ( ) , function ( $ min , $ max , bool $ isInclusive = true ) { return new BetweenRule ( $ min , $ max , $ isInclusive ) ; } ) ; $ ruleFactory -> registerRuleFactory ( DateRule :: getSlug ( ) , function ( $ formats ) { return new DateRule ( $ formats ) ; } ) ; $ ruleFactory -> registerRuleFactory ( InRule :: getSlug ( ) , function ( array $ acceptableValues ) { return new InRule ( $ acceptableValues ) ; } ) ; $ ruleFactory -> registerRuleFactory ( IntegerRule :: getSlug ( ) , function ( ) { return new IntegerRule ( ) ; } ) ; $ ruleFactory -> registerRuleFactory ( NotInRule :: getSlug ( ) , function ( array $ unacceptableValues ) { return new NotInRule ( $ unacceptableValues ) ; } ) ; $ ruleFactory -> registerRuleFactory ( NumericRule :: getSlug ( ) , function ( ) { return new NumericRule ( ) ; } ) ; $ ruleFactory -> registerRuleFactory ( RegexRule :: getSlug ( ) , function ( string $ regex ) { return new RegexRule ( $ regex ) ; } ) ; $ ruleFactory -> registerRuleFactory ( UuidV4Rule :: getSlug ( ) , function ( ) { return new UuidV4Rule ( ) ; } ) ; return $ ruleFactory ; }

Registers the built-in rule factories

@param IRuleFactory The rule factory to register new factories to
@return IRuleFactory The rule factory with all the registered factories (for chaining)


private static function getMatchCandidates(
        TrieNode $node,
        array $segments,
        int $segmentIter,
        array $hostSegments,
        array &$routeVars
    ): iterable {
        // Base case.  We iterate to 1 past the past segments there are n + 1 levels of nodes due to the root node.
        if ($segmentIter === \count($segments)) {
            if ($node->hostTrie === null) {
                foreach ($node->routes as $route) {
                    yield new MatchedRouteCandidate($route, $routeVars);
                }
            } else {
                // We have to traverse the host trie now
                $routeVarsCopy = $routeVars;
                yield from self::getMatchCandidates($node->hostTrie, $hostSegments, 0, $hostSegments, $routeVarsCopy);
            }

            return;
        }

        $segment = $segments[$segmentIter];

        // Check for a literal segment match, and recursively check its descendants
        if (($childNode = ($node->literalChildrenByValue[\strtolower($segment)] ?? null)) !== null) {
            $routeVarsCopy = $routeVars;
            yield from self::getMatchCandidates($childNode, $segments, $segmentIter + 1, $hostSegments, $routeVarsCopy);
        }

        // If a variable child is a match, check its descendants
        foreach ($node->variableChildren as $childNode) {
            $routeVarsCopy = $routeVars;

            if ($childNode->isMatch($segment, $routeVarsCopy)) {
                yield from self::getMatchCandidates(
                    $childNode,
                    $segments,
                    $segmentIter + 1,
                    $hostSegments,
                    $routeVarsCopy
                );
            }
        }
    }

Gets the list of matching route candidates for a particular node

This method uses generators that, given the order of the code, will return literal segments before variables
ones. Callers of this method will only attempt to descend the trie either on the first time or if previous
match candidates didn't meet constraints, hence saving us unnecessary descents down the trie.

@param TrieNode $node The current node
@param array $segments The list of URI segments to match
@param int $segmentIter The current index of segments
@param array $hostSegments The list of URI host segments, which will be traversed if there's a host trie
@param array $routeVars The mapping of route variable names to values
@return iterable|MatchedRouteCandidate[] The list of matched route candidates


public function build(): Route
    {
        if ($this->action === null) {
            throw new LogicException('No controller specified for route');
        }

        return new Route(
            $this->uriTemplate,
            $this->action,
            $this->constraints,
            $this->middlewareBindings,
            $this->name,
            $this->attributes
        );
    }

public
function
build
(
)
:
Route
{
if
(
$
this
->
action
===
)
{
throw
new
LogicException
(
'No controller specified for route'
)
;
}
return
new
Route
(
$
this
->
uriTemplate
,
$
this
->
action
,
$
this
->
constraints
,
$
this
->
middlewareBindings
,
$
this
->
name
,
$
this
->
attributes
)
;
}

Builds a route object from all the settings in this builder

@return Route The built route
@throws LogicException Thrown if no controller was specified


public function toMethod(string $controllerClassName, string $controllerMethodName): self
    {
        $this->action = new MethodRouteAction($controllerClassName, $controllerMethodName);

        return $this;
    }

public function toMethod ( string $ controllerClassName , string $ controllerMethodName ) : self { $ this -> action = new MethodRouteAction ( $ controllerClassName , $ controllerMethodName ) ; return $ this ; }

Binds the controller the route uses to be a method

@param string $controllerClassName The name of the class the route goes to
@param string $controllerMethodName The name of the method the route goes to
@return self For chaining


public function withAttribute(string $name, $value): self
    {
        $this->attributes[$name] = $value;

        return $this;
    }

public function withAttribute ( string $ name , $ value ) : self { $ this -> attributes [ $ name ] = $ value ; return $ this ; }

Binds a custom attribute to the route
This is useful for custom route constraint matching

@param string $name The name of the attribute
@param mixed $value The value of the attribute
@return self For chaining


public function withManyAttributes(array $attributes): self
    {
        $this->attributes = \array_merge($this->attributes, $attributes);

        return $this;
    }

public function withManyAttributes ( array $ attributes ) : self { $ this -> attributes = \ array_merge ( $ this -> attributes , $ attributes ) ; return $ this ; }

Binds many custom attributes to the route
This is useful for custom route constraint matching

@param array $attributes The mapping of custom attribute names => values
@return self For chaining


public function withManyConstraints(array $constraints): self
    {
        $this->constraints = \array_merge($this->constraints, $constraints);

        return $this;
    }

public function withManyConstraints ( array $ constraints ) : self { $ this -> constraints = \ array_merge ( $ this -> constraints , $ constraints ) ; return $ this ; }

Binds constraints to this route

@param IRouteConstraint[] $constraints The constraints to add
@return self For chaining


public function withManyMiddleware(array $middlewareBindings): self
    {
        foreach ($middlewareBindings as $middlewareBinding) {
            if (\is_string($middlewareBinding)) {
                $this->middlewareBindings[] = new MiddlewareBinding($middlewareBinding);
            } elseif ($middlewareBinding instanceof MiddlewareBinding) {
                $this->middlewareBindings[] = $middlewareBinding;
            } else {
                throw new InvalidArgumentException(
                    'Middleware binding must either be a string or an instance of ' . MiddlewareBinding::class
                );
            }
        }

        return $this;
    }

Binds many middleware bindings to the route

@param MiddlewareBinding[]|string[] $middlewareBindings The list of middleware bindings to add, or a single
class name without properties
@return self For chaining
@throws InvalidArgumentException Thrown if the middleware bindings are not the correct type


public function withMiddleware(string $middlewareClassName, array $middlewareProperties = []): self
    {
        $this->middlewareBindings[] = new MiddlewareBinding($middlewareClassName, $middlewareProperties);

        return $this;
    }

public function withMiddleware ( string $ middlewareClassName , array $ middlewareProperties = [ ] ) : self { $ this -> middlewareBindings [ ] = new MiddlewareBinding ( $ middlewareClassName , $ middlewareProperties ) ; return $ this ; }

Binds a single middleware class to the route

@param string $middlewareClassName The name of the middleware class to bind
@param array $middlewareProperties Any properties this method relies on
@return self For chaining


private function parsePunctuation(TokenStream $tokens, AstNode &$currNode, bool $parsingPath): void
    {
        if (($token = $tokens->getCurrent()) === null) {
            return;
        }

        switch ($token->value) {
            case '/':
                if (!$parsingPath) {
                    throw new InvalidArgumentException("Unexpected {$token->type} \"{$token->value}\" in host");
                }

                $currNode->addChild(new AstNode(AstNodeTypes::SEGMENT_DELIMITER, $token->value));
                $tokens->next();
                break;
            case '.':
                // Periods in paths are to be treated as just text
                $nodeType = $parsingPath ? AstNodeTypes::TEXT : AstNodeTypes::SEGMENT_DELIMITER;
                $currNode->addChild(new AstNode($nodeType, $token->value));
                $tokens->next();
                break;
            case '[':
                $parentNodeType = $currNode->type;

                if (
                    $parentNodeType !== AstNodeTypes::HOST
                    && $parentNodeType !== AstNodeTypes::PATH
                    && $parentNodeType !== AstNodeTypes::OPTIONAL_ROUTE_PART
                ) {
                    throw new InvalidArgumentException("Unexpected {$token->type} \"{$token->value}\"");
                }

                $optionalRoutePartNode = new AstNode(AstNodeTypes::OPTIONAL_ROUTE_PART, $token->value);
                $currNode->addChild($optionalRoutePartNode);
                $currNode = $optionalRoutePartNode;
                $tokens->next();

                if ($parsingPath) {
                    $tokens->expect(
                        TokenTypes::T_PUNCTUATION,
                        '/',
                        'Expected optional path part to start with \'/\', got %s'
                    );
                }

                break;
            case ']':
                if ($currNode->type !== AstNodeTypes::OPTIONAL_ROUTE_PART) {
                    // Just treat this as normal text
                    $currNode->addChild(new AstNode(AstNodeTypes::TEXT, $token->value));
                    $tokens->next();
                    break;
                }

                if (!$parsingPath) {
                    /**
                     * Optional parts in hosts must end with '.', eg [foo.[bar.]]example.com
                     * So, make sure that the previous non-optional route part ends with '.'
                     */
                    $isValid = false;

                    for ($i = \count($currNode->children) - 1;$i >= 0;$i--) {
                        $childNode = $currNode->children[$i];

                        if ($childNode->type !== AstNodeTypes::OPTIONAL_ROUTE_PART) {
                            if ($childNode->type === AstNodeTypes::SEGMENT_DELIMITER) {
                                $isValid = true;
                            }

                            break;
                        }
                    }

                    if (!$isValid) {
                        throw new InvalidArgumentException('Expected optional host part to end with \'.\'');
                    }
                }

                // End this optional route part
                $currNode = $currNode->parent;
                $tokens->next();
                break;
            default:
                // Since we handle punctuation inside of variables elsewhere, we'll just treat this as text
                $currNode->addChild(new AstNode(AstNodeTypes::TEXT, $token->value));
                $tokens->next();
                break;
        }
    }

Parses a punctuation token

@param TokenStream $tokens The stream of tokens to parse
@param AstNode $currNode The abstract syntax tree to add nodes to
@param bool $parsingPath Whether or not we're parsing the path (otherwise we're parsing the host)
@throws InvalidArgumentException Thrown if an unexpected punctuation token was found


private function parseText(TokenStream $tokens, AstNode $currNode): void
    {
        if (($token = $tokens->getCurrent()) === null) {
            return;
        }

        $currNode->addChild(new AstNode(AstNodeTypes::TEXT, $token->value));
        $tokens->next();
    }

private function parseText ( TokenStream $ tokens , AstNode $ currNode ) : void { if ( ( $ token = $ tokens -> getCurrent ( ) ) === null ) { return ; } $ currNode -> addChild ( new AstNode ( AstNodeTypes :: TEXT , $ token -> value ) ) ; $ tokens -> next ( ) ; }

Parses a text token

@param TokenStream $tokens The stream of tokens to parse
@param AstNode $currNode The abstract syntax tree to add nodes to


private function parseTokens(TokenStream $tokens, AstNode $ast): void
    {
        $parsingPath = $ast->type === AstNodeTypes::PATH;
        $currNode = $ast;

        while (($token = $tokens->getCurrent()) !== null) {
            switch ($token->type) {
                case TokenTypes::T_TEXT:
                    $this->parseText($tokens, $currNode);
                    break;
                case TokenTypes::T_NUMBER:
                    // Since we handle a number inside of variables elsewhere, we'll just treat this as text
                    $this->parseText($tokens, $currNode);
                    break;
                case TokenTypes::T_VARIABLE:
                    $this->parseVariable($tokens, $currNode);
                    break;
                case TokenTypes::T_PUNCTUATION:
                    $this->parsePunctuation($tokens, $currNode, $parsingPath);
                    break;
                case TokenTypes::T_QUOTED_STRING:
                    // Since we handle a quoted string inside of variables elsewhere, we'll just treat this as text
                    $this->parseText($tokens, $currNode);
                    break;
            }
        }
    }

Parses a stream of tokens and adds it to an abstract syntax tree

@param TokenStream $tokens The tokens to parse
@param AstNode $ast The abstract syntax tree to add to


private function parseVariable(TokenStream $tokens, AstNode &$currNode): void
    {
        if (($token = $tokens->getCurrent()) === null) {
            return;
        }

        $variableNode = new AstNode(AstNodeTypes::VARIABLE, $token->value);
        $currNode->addChild($variableNode);
        $currNode = $variableNode;
        $tokens->next();

        // Check for the beginning of a rule list
        if ($tokens->nextIfType(TokenTypes::T_PUNCTUATION, '(')) {
            // Parse all variable rules
            do {
                $this->parseVariableRule($tokens, $variableNode);
            } while ($tokens->nextIfType(TokenTypes::T_PUNCTUATION, ','));

            $tokens->expect(TokenTypes::T_PUNCTUATION, ')', 'Expected closing parenthesis after rules, got %s');
            $tokens->next();
        }

        if ($tokens->test(TokenTypes::T_VARIABLE)) {
            throw new InvalidArgumentException('Cannot have consecutive variables without a delimiter');
        }

        $currNode = $currNode->parent;
    }

Parses a stream of variable tokens

@param TokenStream $tokens The stream of tokens to parse
@param AstNode $currNode The abstract syntax tree to add nodes to


private function parseVariableRule(TokenStream $tokens, AstNode $currNode): void
    {
        // Expect a rule name
        $tokens->expect(TokenTypes::T_TEXT, null, 'Expected rule name, got %s');

        if (($token = $tokens->getCurrent()) === null) {
            return;
        }

        $variableRuleNode = new AstNode(AstNodeTypes::VARIABLE_RULE, $token->value);
        $tokens->next();

        // Check for a parameter list for this rule
        if ($tokens->nextIfType(TokenTypes::T_PUNCTUATION, '(')) {
            $parameters = [];
            $currentToken = $tokens->getCurrent();

            while ($currentToken !== null && !$tokens->test(TokenTypes::T_PUNCTUATION, ')')) {
                if (!$tokens->test(TokenTypes::T_PUNCTUATION, ',')) {
                    $parameters[] = $currentToken->value;
                }

                $currentToken = $tokens->next();
            }

            $variableRuleNode->addChild(new AstNode(AstNodeTypes::VARIABLE_RULE_PARAMETERS, $parameters));
            $tokens->expect(
                TokenTypes::T_PUNCTUATION,
                ')',
                'Expected closing parenthesis after rule parameters, got %s'
            );
            $tokens->next();
        }

        $currNode->addChild($variableRuleNode);
    }

Parses a single variable rule

@param TokenStream $tokens The stream of tokens to parse
@param AstNode $currNode The variable node to add nodes to


public function isMatch(string $segmentValue, array &$routeVariables): bool
    {
        if ($this->onlyContainsVariable) {
            foreach ($this->parts[0]->rules as $rule) {
                if (!$rule->passes($segmentValue)) {
                    return false;
                }
            }

            $routeVariables[$this->parts[0]->name] = $segmentValue;

            return true;
        }

        $matches = [];

        if (\preg_match($this->regex, $segmentValue, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
            return false;
        }

        // Don't change the actual array until we're sure all the rules pass
        $routeVariablesCopy = $routeVariables;

        foreach ($this->parts as $part) {
            if ($part instanceof RouteVariable) {
                foreach ($part->rules as $rule) {
                    if (!$rule->passes($matches[$part->name])) {
                        return false;
                    }
                }

                $routeVariablesCopy[$part->name] = $matches[$part->name];
            }
        }

        $routeVariables = $routeVariablesCopy;

        return true;
    }

Gets whether or not a segment matches this node

@param string $segmentValue The segment value to match against
@param array $routeVariables The route variables found on a successful match
@return bool True if the input segment value matches this node, otherwise false


public function group(array $attributes, Closure $routes): Router
    {
        // Backup current properties
        $oldName = $this->currentName;
        $oldMiddleware = $this->currentMiddleware;
        $oldNamespace = $this->currentNamespace;
        $oldPrefix = $this->currentPrefix;
        $oldDomain = $this->currentDomain;

        $this->currentName = null;

        // Set middleware for the group
        if (isset($attributes[GroupAttributes::MIDDLEWARE])) {
            if (is_array($attributes[Route::MIDDLEWARE]) == false) {
                $attributes[GroupAttributes::MIDDLEWARE] = [$attributes[GroupAttributes::MIDDLEWARE]];
            }

            $this->currentMiddleware = array_merge($attributes[GroupAttributes::MIDDLEWARE], $this->currentMiddleware);
        }

        // Set namespace for the group
        if (isset($attributes[GroupAttributes::NAMESPACE])) {
            $this->currentNamespace = $attributes[GroupAttributes::NAMESPACE];
        }

        // Set prefix for the group
        if (isset($attributes[GroupAttributes::PREFIX])) {
            $this->currentPrefix = $this->currentPrefix.$attributes[GroupAttributes::PREFIX];
        }

        // Set domain for the group
        if (isset($attributes[GroupAttributes::DOMAIN])) {
            $this->currentDomain = $attributes[GroupAttributes::DOMAIN];
        }

        // Run the group body closure
        call_user_func($routes, $this);

        // Restore properties
        $this->currentName = $oldName;
        $this->currentDomain = $oldDomain;
        $this->currentPrefix = $oldPrefix;
        $this->currentMiddleware = $oldMiddleware;
        $this->currentNamespace = $oldNamespace;

        return $this;
    }

public
function
group
(
array
$
attributes
,
Closure
$
routes
)
:
Router
{
// Backup current properties
$
oldName
=
$
this
->
currentName
;
$
oldMiddleware
=
$
this
->
currentMiddleware
;
$
oldNamespace
=
$
this
->
currentNamespace
;
$
oldPrefix
=
$
this
->
currentPrefix
;
$
oldDomain
=
$
this
->
currentDomain
;
$
this
->
currentName
=
;
// Set middleware for the group
if
(
isset
(
$
attributes
[
GroupAttributes
::
MIDDLEWARE
]
)
)
{
if
(
is_array
(
$
attributes
[
Route
::
MIDDLEWARE
]
)
==
false
)
{
$
attributes
[
GroupAttributes
::
MIDDLEWARE
]
=
[
$
attributes
[
GroupAttributes
::
MIDDLEWARE
]
]
;
}
$
this
->
currentMiddleware
=
array_merge
(
$
attributes
[
GroupAttributes
::
MIDDLEWARE
]
,
$
this
->
currentMiddleware
)
;
}
// Set namespace for the group
if
(
isset
(
$
attributes
[
GroupAttributes
::
NAMESPACE
]
)
)
{
$
this
->
currentNamespace
=
$
attributes
[
GroupAttributes
::
NAMESPACE
]
;
}
// Set prefix for the group
if
(
isset
(
$
attributes
[
GroupAttributes
::
PREFIX
]
)
)
{
$
this
->
currentPrefix
=
$
this
->
currentPrefix
.
$
attributes
[
GroupAttributes
::
PREFIX
]
;
}
// Set domain for the group
if
(
isset
(
$
attributes
[
GroupAttributes
::
DOMAIN
]
)
)
{
$
this
->
currentDomain
=
$
attributes
[
GroupAttributes
::
DOMAIN
]
;
}
// Run the group body closure
call_user_func
(
$
routes
,
$
this
)
;
// Restore properties
$
this
->
currentName
=
$
oldName
;
$
this
->
currentDomain
=
$
oldDomain
;
$
this
->
currentPrefix
=
$
oldPrefix
;
$
this
->
currentMiddleware
=
$
oldMiddleware
;
$
this
->
currentNamespace
=
$
oldNamespace
;
return
$
this
;
}

Group routes with the given attributes

@param array $attributes
@param Closure $routes
@return $this


public function map(
        ?string $method,
        string $route,
        $controller,
        $middleware = [],
        string $domain = null,
        string $name = null
    ): Router {
        $name = $name ?: $this->currentName;
        $uri = $this->currentPrefix.$route;
        $middleware = is_array($middleware) ? $middleware : [$middleware];

        if (is_string($controller) && is_callable($controller) == false) {
            $controller = $this->currentNamespace."\\".$controller;
        }

        $route = new Route(
            $name,
            $uri,
            $method,
            $controller,
            array_merge($this->currentMiddleware, $middleware),
            $domain ?: $this->currentDomain
        );

        $this->routes[] = $route;

        if ($name) {
            $this->names[$name] = $route;
            $this->currentName = null;
        }

        return $this;
    }

public function map ( ? string $ method , string $ route , $ controller , $ middleware = [ ] , string $ domain = null , string $ name = null ) : Router { $ name = $ name ? : $ this -> currentName ; $ uri = $ this -> currentPrefix . $ route ; $ middleware = is_array ( $ middleware ) ? $ middleware : [ $ middleware ] ; if ( is_string ( $ controller ) && is_callable ( $ controller ) == false ) { $ controller = $ this -> currentNamespace . "\\" . $ controller ; } $ route = new Route ( $ name , $ uri , $ method , $ controller , array_merge ( $ this -> currentMiddleware , $ middleware ) , $ domain ? : $ this -> currentDomain ) ; $ this -> routes [ ] = $ route ; if ( $ name ) { $ this -> names [ $ name ] = $ route ; $ this -> currentName = null ; } return $ this ; }

Map a controller to a route and set basic attributes

@param string $method
@param string $route
@param Closure|callable|string $controller
@param Middleware|string|Middleware[]|string[] $middleware
@param string|null $domain
@param string|null $name
@return $this


public function dispatch(): Router
    {
        $this->prepare();

        $method = $this->request->getMethod();
        $scheme = $this->request->getUri()->getScheme();
        $domain = substr($this->request->getUri()->getHost(), strlen($scheme.'://'));
        $uri = $this->request->getUri()->getPath();

        sort($this->routes, SORT_DESC);

        foreach ($this->routes as $route) {
            $parameters = [];

            if (
                $this->compareMethod($route->getMethod(), $method) &&
                $this->compareDomain($route->getDomain(), $domain) &&
                $this->compareUri($route->getUri(), $uri, $parameters)
            ) {
                $this->currentRoute = $route;

                $this->publisher->publish($this->run($route, $parameters));

                return $this;
            }
        }

        throw new RouteNotFoundException();
    }

public function dispatch ( ) : Router { $ this -> prepare ( ) ; $ method = $ this -> request -> getMethod ( ) ; $ scheme = $ this -> request -> getUri ( ) -> getScheme ( ) ; $ domain = substr ( $ this -> request -> getUri ( ) -> getHost ( ) , strlen ( $ scheme . '://' ) ) ; $ uri = $ this -> request -> getUri ( ) -> getPath ( ) ; sort ( $ this -> routes , SORT_DESC ) ; foreach ( $ this -> routes as $ route ) { $ parameters = [ ] ; if ( $ this -> compareMethod ( $ route -> getMethod ( ) , $ method ) && $ this -> compareDomain ( $ route -> getDomain ( ) , $ domain ) && $ this -> compareUri ( $ route -> getUri ( ) , $ uri , $ parameters ) ) { $ this -> currentRoute = $ route ; $ this -> publisher -> publish ( $ this -> run ( $ route , $ parameters ) ) ; return $ this ; } } throw new RouteNotFoundException ( ) ; }

Dispatch routes and run the application

@return $this
@throws RouteNotFoundException
@throws Throwable


private function compareMethod(?string $routeMethod, string $requestMethod): bool
    {
        return $routeMethod == null || $routeMethod == $requestMethod;
    }

private function compareMethod ( ? string $ routeMethod , string $ requestMethod ) : bool { return $ routeMethod == null || $ routeMethod == $ requestMethod ; }

Check if given request method matches given route method

@param string|null $routeMethod
@param string $requestMethod
@return bool


private function compareDomain(?string $routeDomain, string $requestDomain): bool
    {
        return $routeDomain == null || preg_match('@^'.$routeDomain.'$@', $requestDomain);
    }

private function compareDomain ( ? string $ routeDomain , string $ requestDomain ) : bool { return $ routeDomain == null || preg_match ( '@^' . $ routeDomain . '$@' , $ requestDomain ) ; }

Check if given request domain matches given route domain

@param string|null $routeDomain
@param string $requestDomain
@return bool


private function compareUri(string $routeUri, string $requestUri, array &$parameters): bool
    {
        $pattern = '@^'.$this->regexUri($routeUri).'$@';

        return preg_match($pattern, $requestUri, $parameters);
    }

private function compareUri ( string $ routeUri , string $ requestUri , array & $ parameters ) : bool { $ pattern = '@^' . $ this -> regexUri ( $ routeUri ) . '$@' ; return preg_match ( $ pattern , $ requestUri , $ parameters ) ; }

Check if given request uri matches given uri method

@param string $routeUri
@param string $requestUri
@param array $parameters
@return bool


private function run(Route $route, array $parameters)
    {
        $controller = $route->getController();

        if (count($middleware = $route->getMiddleware()) > 0) {
            $controllerRunner = function (ServerRequest $request) use ($controller, $parameters) {
                return $this->runController($controller, $parameters, $request);
            };

            return $this->runControllerThroughMiddleware($middleware, $this->request, $controllerRunner);
        } else {
            return $this->runController($controller, $parameters, $this->request);
        }
    }

private function run ( Route $ route , array $ parameters ) { $ controller = $ route -> getController ( ) ; if ( count ( $ middleware = $ route -> getMiddleware ( ) ) > 0 ) { $ controllerRunner = function ( ServerRequest $ request ) use ( $ controller , $ parameters ) { return $ this -> runController ( $ controller , $ parameters , $ request ) ; } ; return $ this -> runControllerThroughMiddleware ( $ middleware , $ this -> request , $ controllerRunner ) ; } else { return $ this -> runController ( $ controller , $ parameters , $ this -> request ) ; } }

Run the controller of the given route

@param Route $route
@param array $parameters
@return mixed|ResponseInterface
@throws InvalidControllerException
@throws InvalidMiddlewareException
@throws Throwable


private function arrangeMethodParameters(
        string $class,
        string $method,
        array $parameters,
        ServerRequestInterface $request
    ) {
        return $this->arrangeParameters(new ReflectionMethod($class, $method), $parameters, $request);
    }

private function arrangeMethodParameters ( string $ class , string $ method , array $ parameters , ServerRequestInterface $ request ) { return $ this -> arrangeParameters ( new ReflectionMethod ( $ class , $ method ) , $ parameters , $ request ) ; }

Arrange parameters for given method

@param string $class
@param string $method
@param array $parameters
@param ServerRequestInterface $request
@return array
@throws ReflectionException


private function regexUri(string $route): string
    {
        return preg_replace_callback('@{([^}]+)}@', function (array $match) {
            return $this->regexParameter($match[1]);
        }, $route);
    }

private function regexUri ( string $ route ) : string { return preg_replace_callback ( '@{([^}]+)}@' , function ( array $ match ) { return $ this -> regexParameter ( $ match [ 1 ] ) ; } , $ route ) ; }

Convert route to regex

@param string $route
@return string


private function regexParameter(string $name): string
    {
        if ($name[-1] == '?') {
            $name = substr($name, 0, -1);
            $suffix = '?';
        } else {
            $suffix = '';
        }

        $pattern = $this->parameters[$name] ?? '[^/]+';

        return '(?<'.$name.'>'.$pattern.')'.$suffix;
    }

private function regexParameter ( string $ name ) : string { if ( $ name [ - 1 ] == '?' ) { $ name = substr ( $ name , 0 , - 1 ) ; $ suffix = '?' ; } else { $ suffix = '' ; } $ pattern = $ this -> parameters [ $ name ] ?? '[^/]+' ; return '(?<' . $ name . '>' . $ pattern . ')' . $ suffix ; }

Convert route parameter to regex

@param string $name
@return string


public function any(
        string $route,
        $controller,
        $middleware = [],
        string $domain = null,
        string $name = null
    ): Router {
        return $this->map(null, $route, $controller, $middleware, $domain, $name);
    }

public function any ( string $ route , $ controller , $ middleware = [ ] , string $ domain = null , string $ name = null ) : Router { return $ this -> map ( null , $ route , $ controller , $ middleware , $ domain , $ name ) ; }

Map a controller to given route for all the http methods

@param string $route
@param Closure|callable|string $controller
@param Middleware|string|Middleware[]|string[] $middleware
@param string|null $domain
@param string|null $name
@return $this


public function define(string $name, string $pattern): Router
    {
        $this->parameters[$name] = $pattern;

        return $this;
    }

public function define ( string $ name , string $ pattern ) : Router { $ this -> parameters [ $ name ] = $ pattern ; return $ this ; }

Define a parameter pattern

@param string $name
@param string $pattern
@return $this


public function url(string $routeName, array $parameters = []): ?string
    {
        if (isset($this->names[$routeName]) == false) {
            return null;
        }

        $uri = $this->names[$routeName]->getUri();

        foreach ($parameters as $name => $value) {
            $uri = preg_replace('/\??\{'.$name.'\??\}/', $value, $uri);
        }

        $uri = preg_replace('/{[^\}]+\?\}/', '', $uri);
        $uri = str_replace('/?', '', $uri);

        return $uri;
    }

public function url ( string $ routeName , array $ parameters = [ ] ) : ? string { if ( isset ( $ this -> names [ $ routeName ] ) == false ) { return null ; } $ uri = $ this -> names [ $ routeName ] -> getUri ( ) ; foreach ( $ parameters as $ name => $ value ) { $ uri = preg_replace ( '/\??\{' . $ name . '\??\}/' , $ value , $ uri ) ; } $ uri = preg_replace ( '/{[^\}]+\?\}/' , '' , $ uri ) ; $ uri = str_replace ( '/?' , '' , $ uri ) ; return $ uri ; }

Generate URL for given route name
It returns Null if route with given name is not found.

@param string $routeName
@param string[] $parameters
@return string|null

Generate URL for given route name It returns Null if route with given name is not found .


private function prepare(): void
    {
        if ($this->request == null) {
            $this->request = ServerRequestFactory::fromGlobals();
        }

        if ($this->publisher == null) {
            $this->publisher = new Publisher();
        }
    }

private function prepare ( ) : void { if ( $ this -> request == null ) { $ this -> request = ServerRequestFactory :: fromGlobals ( ) ; } if ( $ this -> publisher == null ) { $ this -> publisher = new Publisher ( ) ; } }