Skip to content

Instantly share code, notes, and snippets.

@shadowhand
Last active May 16, 2017 13:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shadowhand/543e4e507bf90feffd6bc63427c7e53b to your computer and use it in GitHub Desktop.
Save shadowhand/543e4e507bf90feffd6bc63427c7e53b to your computer and use it in GitHub Desktop.
Routing interfaces for standards discussion
<?php
interface RouteInterface
{
/**
* Get the HTTP method this route handles.
*
* @return string
*/
public function getMethod();
/**
* Get the URI expression this route matches.
*
* @return string
*/
public function getExpression();
/**
* Get the target handled by this route.
*
* @return callable
*/
public function getTarget();
}
<?php
interface RouteMatchInterface
{
/**
* Get the matched route.
*
* @return Route
*/
public function getRoute();
/**
* Get the matched URI parameters.
*
* @return array
*/
public function getParams();
}
<?php
interface RouterInterface
{
/**
* Find a route match for the given URI.
*
* @param RequestInterface $request
*
* @throws \CannotMatchUriException
* If no route matches the given URI.
*
* @return RouteMatch
*/
public function match(RequestInterface $request);
/**
* Add a route to the current stack.
*
* @param Route $route
*
* @return void
*/
public function add(Route $route);
/**
* Create and add a GET route.
*
* @param string $expression
* @param callable $target
*
* @return static
*/
public function get($expression, callable $target);
/**
* Create and add a POST route.
*
* @param string $expression
* @param callable $target
*
* @return static
*/
public function post($expression, callable $target);
/**
* Create and add a PUT route.
*
* @param string $expression
* @param callable $target
*
* @return static
*/
public function put($expression, callable $target);
/**
* Create and add a PATCH route.
*
* @param string $expression
* @param callable $target
*
* @return static
*/
public function patch($expression, callable $target);
/**
* Create and add a DELETE route.
*
* @param string $expression
* @param callable $target
*
* @return static
*/
public function delete($expression, callable $target);
/**
* Create and add an OPTIONS route.
*
* @param string $expression
* @param callable $target
*
* @return static
*/
public function options($expression, callable $target);
}
@shadowhand
Copy link
Author

Using these interfaces would be something like:

$router = (new Router())
    ->get('/', function () {
        echo 'Hello, world!';
    })
    ->post('/hello/:user', function($user) {
        echo "Hello, $user!";
    });

$match = $router->match($request);

call_user_func_array(
    $match->getRoute()->getTarget(),
    $match->getParams()
);

@atanvarno69
Copy link

I think a standard should follow the PSR-11 model and focus only on how a
router is consumed and not be opinionated on how it is configured.

While I appricate that RouteInterface does not necessairly dictate a router's
internal implementation and is strictly only necessary for interperating
results, I think that is how it would end up being used.

Also, I think a cue should be taken from FastRoute, which allows any PHP value
to be returned as a 'target' (or 'handler' in FastRoute terms), rather than
dictate that a callable must be returned (and thus must be passed as part of
configuration). This allows the return result to be (potentially lazy loaded)
controller (if that PSR gets off the ground), middleware or a middleware
delegate... or any other value the implementor wants to allow.

Rather than concern itself with eventual handling/dispatch, a standard should
limit itself to matching only.

Thus RouterInterface becomes MatcherInterface (or UriMatcherInterface):

use Psr\Http\Message\UriInterface;

interface MatcherInterface
{
    /**
     * Finds a route for the given URI.
     *
     * @return MatchResultInterface
     */
    public function match(Uri $uri): MatcherResultInterface;
}

And RouteMatchInterface combines with RouteInterface to be a value object
for the result of a matching operation. I'm calling this MatcherResultInterface
(naming things is hard):

interface MatcherResultInterface
{
    /**
     * Returns whether a match was found.
     *
     * @return bool `true` for a sucessful match, `false` otherwise.
     */
    public function isFound(): bool;
    
    /**
     * Gets a list of permitted HTTP methods.
     *
     * If `isFound()` returns `false`, this method MUST return an empty array.
     *
     * @return string[]
     */
    public function getMethods(): array;
    
    /**
     * Gets the matched URI parameters.
     *
     * If `isFound()` returns `false`, this method MUST return an empty array.
     *
     * @return string[]
     */
    public function getParameters(): array;
    
    /**
     * Gets the user-defined target value
     *
     * If `isFound()` returns `false`, this method MUST return `null`.
     *
     * @return mixed User-defined target value.
     */
    public function getTarget();
}

This removes the use of an exception for something that I don't think is
exceptional for a matching operation -- not finding a match -- but would
perhaps be for a dispatching operation (though I'm not convinced, it seems like
relying on an exception for application flow control).

How adding/registering routes is handled is left undefined.

There is clearly room for improvement here. I'm not particularly happy that
HTTP method matching is left undefined which dictates that method verfication
must happen after URI matching.

Perhaps:

use Psr\Http\Message\RequestInterface;

interface MatcherInterface
{
    public function match(RequestInterface $request): MatcherResultInterface;
}

interface MatcherResultInterface
{
    public function isAllowed(): bool;
    
    public function isFound(): bool;
    
    public function getMethods(): array;
    
    public function getParameters(): array;
    
    public function getTarget();
}

Is a better starting point?

All thoughts/comments welcome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment