Search code examples
phproutesmezziopsr-7

Matching Param on URL in Custom Router PHP


I'm adding a funcionality to this custom Router and custom Request Class in order to be able to serve pages and json responses.

I'm stuck on the router part, where the Route has a parameter in the url like:

example.com/apply/{variable}

Those are the classes:

Router class:

<?php

class Router
{
    private $request;

    private $supportedHttpMethods = array("GET", "POST");

    function __construct(RequestInterface $request)
    {
        $this->request = $request;
    }

    function __call($name, $args)
    {
        list($route, $method) = $args;
        if (!in_array(strtoupper($name), $this->supportedHttpMethods)) {
            $this->invalidMethodHandler();
        }
        $this->{strtolower($name)}[$this->formatRoute($route)] = $method;
    }

    /**
     * Removes trailing forward slashes from the right of the route.
     *
     * @param route (string)
     */
    private function formatRoute($route)
    {
        $result = rtrim($route, '/');
        if ($result === '') {
            return '/';
        }

        return $result;
    }

    private function invalidMethodHandler()
    {
        header("{$this->request->serverProtocol} 405 Method Not Allowed");
    }

    private function defaultRequestHandler()
    {
        header("{$this->request->serverProtocol} 404 Not Found");
    }

    /**
     * Resolves a route
     */
    function resolve()
    {
        $methodDictionary = $this->{strtolower($this->request->requestMethod)};
        $formatedRoute = $this->formatRoute($this->request->requestUri);
        $method = $methodDictionary[$formatedRoute];
        if (is_null($method)) {
            $this->defaultRequestHandler();

            return;
        }
        echo call_user_func_array($method, array(
            $this->request
        ));
    }

    function __destruct()
    {
        $this->resolve();
    }
} 

Request Class:

<?php

include_once 'RequestInterface.php';

class Request implements RequestInterface
{
    private $params = [];

    public function __construct()
    {
        $this->bootstrapSelf();
    }

    private function bootstrapSelf()
    {
        foreach ($_SERVER as $key => $value) {
            $this->{$this->toCamelCase($key)} = $value;
        }
    }

    private function toCamelCase($string)
    {
        $result = strtolower($string);

        preg_match_all('/_[a-z]/', $result, $matches);
        foreach ($matches[0] as $match) {
            $c = str_replace('_', '', strtoupper($match));
            $result = str_replace($match, $c, $result);
        }

        return $result;
    }

    public function isPost()
    {
        return $this->requestMethod === "POST";
    }

    /**
     * Implemented method
     */
    public function getParams()
    {
        if ($this->requestMethod === "GET") {
            $params = [];
            foreach ($_GET as $key => $value) {
                $params[$key] = filter_input(INPUT_POST, $key, FILTER_SANITIZE_SPECIAL_CHARS);
            }
            $this->params = array_merge($this->params, $params);
        }
        if ($this->requestMethod == "POST") {
            $params = [];
            foreach ($_POST as $key => $value) {
                $params[$key] = filter_input(INPUT_POST, $key, FILTER_SANITIZE_SPECIAL_CHARS);
            }
            $this->params = array_merge($this->params, $params);
        }

        return $this->params;
    }
}

This is how I would call the Router:

$router->get('/apply/{code}', function($request) use($myClass) {});

Which approach would be the better? I don't know how to resolve that.


Solution

  • I would strongly recommend having a look into existing http factory implementations before reinventing the wheel from scratch. Even custom implementations may look like they provide some flexibility and benefits in short term, you can easily shoot your own foot in mid/long term by building an application based on such approach.

    Both the language itself and PHP ecosystem is evolved a lot, we are an in 2019, we have dozens of well-written, re-usable libraries around. Just pick your weapons and focus on your real goal. Any code without tests, involving magic, lacks from composer, a proper autoloading mechanism, a well-written router or a fast template engine; most of the time will cause more pain than the value it provides. We should stop repeating ourselves.

    As far as I understand, your goal is serving JSON content on a specific URI path but you are trying to invent a Router. If your goal is writing a proper router, there is nothing to do with Request/Response interfaces which mentioned in question. I would recommend to have a look implementations of some reusable, framework-independent routers such as FastRoute, Zend Router, Aura Router etc to have an idea first. Implementing a proper router is of course not rocket science but its not simple as you may realized. Still, trying to write that component can be educational as well and if your goal is this, go for it.

    Here is a couple of tips (and new problems to think about):

    • There is a PSR-15 Request Handler standard. Sending headers in private methods named requestHandler may not be a good idea.
    • Request handlers and routers are different components and requires different workflows. You are mixing them in your code and this is a warning sign.
    • Any code involving __magic is setting up a trap for yourself and for the potential future developers.
    • I am not sure the result of include_once 'RequestInterface' line but we have HTTP Message interfaces. I would consider having a use Psr\Http\Message\ServerRequestInterface import in any type of custom implementation when dealing with requests.
    • Echoing in __destruct is also interesting. What you need here is an emitter. A few examples: Http Emitter, Zend Http Runner
    • And here is a high level answer for your actual question: you need to implement a mechanism (probably using regex) to catch patterns in URI parts and parse and detect optional or required named parts in "paths".

    Personally, I would recommend to have a look into Zend Expressive. It helps developers a lot when writing lightweight, middleware-driven applications. Best feature of Expressive is you can pick any weapon according to your needs. It is not a full-blown MVC framework, provides a new way to write web applications and it's damn fast. You can freely choose any component you want, for example; Twig for rendering needs, Symfony Console for CLI, Zend Service Manager as dependency injection container, Aura Router for routing etc..

    You can give a try it using only a few commands (assuming you have globally installed composer):

    composer create-project zendframework/zend-expressive-skeleton my-app
    cd my-app
    composer run --timeout=0 serve
    

    And open your browser: http://localhost:8080

    Good luck!