Skip to content

Instantly share code, notes, and snippets.

@jm42
Last active August 10, 2016 18:59
Show Gist options
  • Save jm42/a38a4f68d28d20016b5b1f88bf64cfda to your computer and use it in GitHub Desktop.
Save jm42/a38a4f68d28d20016b5b1f88bf64cfda to your computer and use it in GitHub Desktop.
Playing around with a match function
  • Sería un responder para Ochenta\emit,

    function __invoke(ServerRequest $req, callable $open) {
        try {
            $action = $this->getRouter()->match($req);
        } catch (RouteNotFoundException $ex) {
            return not_found($req, $open);
        } catch (MethodNotAllowedException $ex) {
            return method_not_allowed($req, $open);
        }
    
        $payload = $action();
        $responder = $this->getResponderOf($payload);
    
        return $responder($req, $open);
    };
  • Las rutas pueden estar encapsuladas en objectos,

    class Hello {
        function __invoke(): array {
            return [
                AddBlog::class => [method('GET'), path('/hello[/<slug:user>]')],
            ];
        }
    
        function addBlog(string $user=null): string {
            if ($user) {
                return "/hello/$user";
            }
            return '/hello';
        }
    }
  • En vez de algo específico para el método y el path, se puede hacer un estilo de middleware pero con funciones que devuelvan FALSE si no matchea la parte del request o un array con valores matcheados.

    function method($method) {
        $normalized = strtoupper($method);
        return function(ServerRequest $req) use($normalized) {
            return $req->getMethod() === $normalized ? [] : FALSE;
        };
    }
  • Mala implementacion de match,

    // FIXME don't use exceptions to flow control
    function match(ServerRequest $req, array $routes) {
        $resolver = function($matches, $route) use($req) {
            if (is_array($match = $route($req))) {
                return $match + $matches;
            } elseif ($match === FALSE) {
                return new \RuntimeException('Route not found');
            }
            return $matches;
        };
    
        foreach ($routes as $handler => $route) {
            try {
                if (is_int($handler) && is_array($route)) {
                    return match($req, $route);
                } elseif (is_int($handler) && is_callable($route)) {
                    return match($req, $route());
                } elseif (is_string($handler) && is_array($route)) {
                    return [$handler, array_reduce($route, $resolver, [])];
                } elseif (is_string($handler) && is_callable($route)) {
                    return [$handler, $resolver([], $route)];
                } else {
                    throw new \InvalidArgumentException('Invalid route');
                }
            } catch (\RuntimeException $ex) {
                continue;
            }
        }
    
        throw new \RuntimeException('Route not found');
    }
  • Uso de match,

    match(new ServerRequest, [Homepage::class => path('/')]);
    match(new ServerRequest, [Homepage::class => [method('GET'), path('/')]]);
    match(new ServerRequest, [function() { return [Homepage::class => [path('/')]]; }]);
    match(new ServerRequest, [new Hello]);
  • Otra idea de $routes,

    // https://gist.github.com/jm42/b4a2a6a9daa23d042edf6d3eb25ea27b
    $routes = [
        '/' => Homepage::class,
        '/admin' => function(callable $r): array {
            return [
                $r('GET', path('/users')) => ListUsers::class,
                $r('GET', path('/user/{int:id}')) => ShowUser::class,
    
                '/api' => function(callable $r): array {
                    return [
                        $r('PATCH', path('/user/{int:id}')) => ApiPatchUser::class,
                    ];
                },
            ];
        },
    ];
  • Primera evolución,

    function match(ServerRequest $req, array $routes) {
        $path = $req->getUri()['path'];
        $left = array_reverse($routes);
        $matchers = [];
    
        while (end($left)) {
            $key = key($left);
            $route = array_pop($left);
    
            if (isset($matchers[$key]) && ($match = $matchers[$key]($req)) !== FALSE) {
                return ['error' => MATCH_ERR_OK, 'match' => $route, 'vars' => iterator_to_array($match)];
            }
    
            if ($key === $path) {
                return ['error' => MATCH_ERR_OK, 'match' => $route];
            }
    
            if (stripos($path, $key) === 0 && is_callable($route)) {
                $left = array_merge($left, array_reverse($route(function(...$all) use($path, &$matchers) {
                    $matchers[$uniqid = uniqid()] = function(ServerRequest $req) use($path, $all) {
                        if (stripos($req->getUri()['path'], $path) !== 0) {
                            return FALSE;
                        }
    
                        foreach ($all as $matcher) {
                            if (($match = $matcher($req)) !== FALSE) {
                                yield from $match;
                            }
                        }
    
                        return FALSE;
                    };
                    
                    return $uniqid;
                })));
    
                $path = substr($path, strlen($key));
            }
        }
    
        return ['error' => MATCH_ERR_NOT_FOUND];
    }
  • Basico,

    function match(array $routes): callable {
        $routes = array_reverse($routes);
    
        return function(ServerRequest $req, array $args=[]) use($routes) {
            $args['path'] = $args['path'] ?? $req->getUri()['path'];
            while (end($routes)) {
                $key = key($routes);
                $route = array_pop($routes);
    
                if ($key === $args['path']) {
                    return ['error' => MATCH_ERR_OK, 'match' => $route];
                }
            }
    
            return ['error' => MATCH_ERR_NOT_FOUND];
        };
    }
  • Extendido,

    function match(array $routes, array $matchers=[]): callable {
        $match = function(callable $route, string $prefix='') use(&$matchers): array {
            $pre = [];
            $new = $route(function(string $method, string $path) use(&$pre, &$matchers, $prefix) {
                $id = $id = uniqid();
                $pre[$id] = NULL;
    
                $matchers[strtoupper($method)][$id] = function(ServerRequest $req) use($prefix, $path) {
                    return $prefix . $path === $req->getUri()['path'];
                };
    
                return $id;
            });
    
            foreach ($new as $nkey => $nroute) {
                if (array_key_exists($nkey, $pre)) {
                    $pre[$nkey] = $nroute;
                } else {
                    $pre[$prefix . $nkey] = $nroute;
                }
            }
    
            return $pre;
        };
    
        return function(ServerRequest $req) use($routes, &$matchers, $match) {
            $method = $req->getMethod();
            $path = $req->getUri()['path'];
    
            while (reset($routes)) {
                $key = key($routes);
                $route = array_shift($routes);
    
                if (isset($matchers[$method][$key]) && !empty($ret = $matchers[$method][$key]($req))) {
                    return ['error' => MATCH_ERR_OK, 'match' => $route, 'return' => $ret];
                }
    
                if ($key === $path) {
                    return ['error' => MATCH_ERR_OK, 'match' => $route];
                }
    
                if (is_callable($route)) {
                    $routes = array_merge($routes, $match($route, $key));
                }
            }
    
            return ['error' => MATCH_ERR_NOT_FOUND];
        };
    }
  • No se puede hacer asi,

    function match(callable $handler) {
        return function(Request $req) use($handler) {
            $path = strstr($req->getTarget(), '?', true) ?: $req->getTarget();
            $method = $req->getMethod();
            $routes = $handler(function(...$matchers) {
                // TODO
            });
    
            while (reset($routes)) {
                $key = key($routes);
                $route = array_shift($routes);
    
                if ($key === $path) {
                    return ['error' => MATCH_ERR_OK, 'match' => $route];
                }
    
                if (is_callable($route) && stripos($path, $key) === 0) {
                    $ret = match($route)(new Request(
                        $req->getMethod(), substr($path, strlen($key)),
                        $req->getHeaders(), $req->getBody()));
                    if ($ret['error'] !== MATCH_ERR_NOT_FOUND) {
                        return $ret;
                    }
                }
            }
    
            return ['error' => MATCH_ERR_NOT_FOUND];
        };
    }
  • Implementacion larga,

    define('MATCH_ERR_OK', 0);
    define('MATCH_ERR_NOT_FOUND', 1);
    define('MATCH_ERR_METHOD_NOT_ALLOWED', 2);
    
    function match(array $routes): callable {
        return function(ServerRequest $req) use($routes): array {
            $matchers = ['GET' => [], 'POST' => []];
            $method = $req->getMethod();
            $path = $req->getUri()['path'];
    
            while (reset($routes)) {
                $key = key($routes);
                $route = array_shift($routes);
    
                if (isset($matchers[$method][$key])) {
                    $args = $matchers[$method][$key]($req);
                    if ($args !== FALSE) {
                        return ['error' => MATCH_ERR_OK, 'match' => $route, 'args' => $args];
                    }
                }
    
                if ($path === $key) {
                    return ['error' => MATCH_ERR_OK, 'match' => $route];
                }
    
                if (is_callable($route) && stripos($path, $key) === 0) {
                    $pre = [];
                    $new = $route(function(string $method, string $pattern, ...$extra) use(&$matchers, &$pre, $key) {
                        $method = strtoupper($method);
                        $pattern = $key . $pattern;
    
                        if (!isset($matchers[$method])) {
                            throw new \InvalidArgumentException('Invalid method');
                        }
    
                        $id = uniqid();
                        $pre[$id] = NULL;
                        $matchers[$method][$id] = function(ServerRequest $req) use($pattern, $extra) {
                            $path = $req->getUri()['path'];
    
                            if ($path === $pattern) {
                                $args = [];
                            } elseif (preg_match($pattern, $path, $matches)) {
                                $args = $matches;
                            } else {
                                return FALSE;
                            }
    
                            foreach ($extra as $matcher) {
                                if (($match = $matcher($req)) === FALSE) {
                                    return FALSE;
                                }
                                $args = $match + $args;
                            }
    
                            return $args;
                        };
    
                        return $id;
                    });
    
                    foreach ($new as $nkey => $nroute) {
                        if (array_key_exists($nkey, $pre)) {
                            $pre[$nkey] = $nroute;
                        } else {
                            $pre[$key.$nkey] = $nroute;
                        }
                    }
    
                    $routes = $pre + $routes;
                }
            }
    
            return ['error' => MATCH_ERR_NOT_FOUND];
        };
    }
  • Sus tests,

    use function Ochenta\match;
    
    describe('match', function() {
        it('returns array with error', function() {
            $matcher = match([]);
            $match = $matcher(new ServerRequest);
    
            expect($match)->toBeA('array')->toContainKey('error');
        });
    
        it('returns matched route by checking exact path from routes key', function() {
            $matcher = match(['/' => 'Homepage']);
            $match = $matcher(new ServerRequest(['REQUEST_URI' => '/']));
    
            expect($match)->toContainKey('match');
            expect($match['match'])->toBe('Homepage');
        });
    
        it('returns matched route by executing the function and check the exact path in the returned array key', function() {
            $matcher = match(['/api' => function() {
                return ['/users' => 'UserController::listAction'];
            }]);
            $match = $matcher(new ServerRequest(['REQUEST_URI' => '/api/users']));
    
            expect($match)->toContainKey('match');
            expect($match['match'])->toBe('UserController::listAction');
        });
    
        it('returns matched route by using the callable injected to the function as keys', function() {
            $matcher = match(['/api' => function(callable $r) {
                return [$r('GET', '/users') => 'UserController::listAction'];
            }]);
            $match = $matcher(new ServerRequest(['REQUEST_URI' => '/api/users']));
    
            expect($match)->toContainKey('match');
            expect($match['match'])->toBe('UserController::listAction');
        });
    });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment