router = $router; $this->id = $id; $this->uri = $uri; $this->setAction($action); $this->methods = $methods; } /** * Determine if the route matches a given uri * @param string $uri * @param string|null $method * @return bool */ public function matches(string $uri, ?string $method = null) { $this->parseUri(); if (! is_null($method) && ! in_array($method, $this->methods)) { return false; } $this->params = []; if (preg_match($this->regex, $uri, $matches)) { unset($matches[0]); if ($this->placeholders) { $params = []; foreach ($this->placeholders as $key => $placeholder) { if (isset($matches[$placeholder['index']]) && $key !== 'any') { $params[$key] = Input::clean( $matches[$placeholder['index']], $placeholder['type'] ); } } } else { $params = array_values($matches); } $this->params = $params; $this->parseActionOnMatch($uri); return true; } if (is_string($this->action)) { $callback = preg_replace($this->regex, $this->action, $uri, 1, $found); if ($found && $callback !== '') { $this->parseActionOnMatch($uri); return true; } } return false; } /** * Run route * @param \bff\http\Request $request * @return \bff\http\Response|mixed */ public function run(Request $request) { bff::hook( 'routing.route.run.before.' . $this->getId(), $this, $request ); return bff::filter( 'routing.route.run.after.' . $this->getId(), $this->runAction(), $this, $request ); } /** * Run route action * @return mixed * @throws \Exception */ public function runAction() { $params = $this->getParams(); if ($this->controller instanceof Closure) { return call_user_func_array($this->controller, array_values($params)); } return bff()->callController( $this->getControllerName(), $this->getControllerMethod(), $params, [ 'inject' => true, 'direct' => $this->isDirect(), ] ); } /** * Parse action to get controller * @return void */ protected function parseAction() { $callable = $this->action; if (is_string($callable)) { if (strpos($callable, '/') !== false) { $callable = explode('/', trim($callable, '/'), 3); } elseif (strpos($callable, '::') !== false) { $callable = explode('::', $callable); } elseif (strpos($callable, '@') !== false) { $callable = explode('@', $callable); } else { $callable = [$callable, '__invoke']; } } if (is_array($callable)) { $this->controllerName = $callable[0] ?? ''; $this->controllerMethod = $callable[1] ?? ''; } # Array or Closure $this->controller = $callable; } /** * Extract params from action string * @param string $uri * @param string|mixed $action * @return void */ protected function parseActionOnMatch(string $uri, $action = null) { $action = $action ?? $this->action; if (!is_string($action)) { return; } $action = trim($action, '/'); if (mb_stripos($action, '/') === false) { return; } $action = preg_replace($this->regex, $action, $uri); $action = explode('/', $action, 3); # Controller Name from Uri if (isset($action[0]) && mb_stripos($this->getControllerName(), '$') !== false) { $this->controllerName = $action[0]; } # Controller Method from Uri if (isset($action[1]) && mb_stripos($this->getControllerMethod(), '$') !== false) { $this->controllerMethod = $action[1]; } # Extra params if (! empty($action[2])) { parse_str($action[2], $params); if (! empty($params) && is_array($params)) { $this->params = array_merge($this->params, $params); } } } /** * Set new action * @param mixed $action */ public function setAction($action) { $this->action = $action; $this->parseAction(); } /** * Get controller object * @return mixed */ public function getController() { return $this->controller; } /** * Get controller name * @return string */ public function getControllerName() { $name = $this->controllerName ?? ''; if (is_object($name)) { if ($name instanceof Plugin || $name instanceof Theme) { $name = $name->getName(); } elseif ($name instanceof Module) { $name = $name->module_name; } else { $name = get_class($name); } } return $name; } /** * Get controller method name * @deprecated use getControllerMethod() * @return string */ public function getControllerAction() { return $this->getControllerMethod(); } /** * Get controller method name * @return string */ public function getControllerMethod() { return $this->controllerMethod ?? ''; } /** * Get route id * @return string|null */ public function getId() { return $this->id; } /** * Compare current route * @param string|array $id * @return bool */ public function is($id) { return Str::is($id, $this->id); } /** * Is direct route * @return bool */ public function isDirect() { return $this->is(Router::DIRECT_ROUTE); } /** * Get pages views responsable for view * @return array */ public function getPages() { return $this->pages; } /** * Guess page view by controller */ public function guessPageByController() { if (is_string($this->controllerName)) { if (mb_stripos($this->controllerName, '\views\\') !== false) { $this->pages[] = $this->controllerName; } } } /** * Get the HTTP methods the route responds to * @return array */ public function getMethods() { return $this->methods; } /** * Build route url * @param array $params * @param array $options * @return string */ public function getUrl(array $params = [], array $options = []) { $this->parseUri(); # Prepare params if ($this->before) { $modifiedParams = call_user_func($this->before, $params, $options, $this); if (is_array($modifiedParams)) { $params = $modifiedParams; } } # Alias if ($this->alias instanceof static) { return $this->alias->getUrl($params, $options); } elseif (is_string($this->alias) && ! empty($this->alias)) { return $this->router->url($this->alias, $params, $options); } $url = '/'; # Set params $ignore = []; if (! empty($this->uri)) { $replace = []; foreach ($this->placeholders as $key => $v) { if (array_key_exists($key, $params) && $params[$key] !== '') { $replace[$v['search']] = str_replace('{v}', $params[$key], $v['replace']); $ignore[] = $key; } else { if ($v['optional']) { $replace[$v['search']] = str_replace('{v}', '', rtrim($v['replace'], '/')); $ignore[] = $key; } } } $url .= strtr($this->uri, $replace); } $url .= Url::query($params, $ignore); # // => / $url = preg_replace('#/+#i', '/', $url); # Relative URL if (! empty($options['relative'])) { return $url; } # Full URL $options['domain'] = $options['domain'] ?? $this->domain; $options['lang'] = $options['lang'] ?? Lang::current(); if ($this->urlBuilder) { $options['uri'] = $url; $url = call_user_func($this->urlBuilder, $params, $options, $this); } else { $url = Url::to($url, $options); } # Hooks::moduleUrl if (! empty($options['module'])) { $url = bff::filter($options['module'] . '.url', $url, [ 'key' => $this->id, 'opts' => $params, 'dynamic' => !empty($options['dynamic']), 'base' => Url::to('', $options), ]); } return $url; } /** * Get route placeholders * @return array */ public function getPlaceholders() { $this->parseUri(); return $this->placeholders; } /** * Get route parameters * @return array */ public function getParams() { $params = ($this->params ?? []); foreach ($this->defaults as $key => $value) { $params[$key] = $params[$key] ?? $value; } return $params; } /** * Get route parameter by key * @param string $key * @param mixed|null $default * @param mixed $validate * @return mixed */ public function param(string $key, $default = null, $validate = TYPE_NOCLEAN) { $value = $this->getParams()[$key] ?? $default; return Input::clean($value, $validate); } /** * Parse route uri * @return void */ protected function parseUri() { if (!is_null($this->placeholders)) { return; } list('regex' => $this->regex, 'placeholders' => $this->placeholders) = $this->router->parseUri( $this->uri, $this->wheres ); } /** * Get route placeholders specific rules * @return array */ public function getWheres() { return $this->wheres; } /** * Get route alias * @return string|static|null */ public function getAlias() { return $this->alias; } /** * Is route an alias * @return bool */ public function isAlias() { return ! is_null($this->alias); } /** * Get route before callback * @return callable|null */ public function getBefore() { return $this->before; } /** * Get route middleware * @return array */ public function getMiddleware() { return $this->middleware; } /** * Get route middleware to exclude * @return array */ public function getMiddlewareExcluded() { return $this->middlewareExcluded; } /** * Specify that the route should not allow concurrent requests from the same session * @param int|null $lockSeconds * @param int|null $waitSeconds * @return static */ public function block($lockSeconds = 10, $waitSeconds = 10) { $this->lockSeconds = $lockSeconds; $this->waitSeconds = $waitSeconds; return $this; } /** * Specify that the route should allow concurrent requests from the same session * @return static */ public function withoutBlocking() { return $this->block(null, null); } /** * Get the maximum number of seconds the route's session lock should be held for * @return int|null */ public function locksFor() { return $this->lockSeconds; } /** * Get the maximum number of seconds to wait while attempting to acquire a session lock * @return int|null */ public function waitsFor() { return $this->waitSeconds; } /** * Set page view responsable for view * @param string|array $page * @return static */ public function page($page) { $this->pages = is_array($page) ? $page : func_get_args(); $this->guessPageByController(); return $this; } /** * Set the HTTP methods the route responds to * @param string|array $method * @return static */ public function method($method) { $this->methods = is_array($method) ? $method : func_get_args(); return $this; } /** * Set callback to call before run * @param callable $callback { * @callback-param array $params * @callback-param array $options * @callback-param \bff\base\Route $route * @callback-return array changed $params * } * @return static */ public function before(callable $callback) { $this->before = $callback; return $this; } /** * Set route alias * @param string|static $route * @return static */ public function alias($route) { $this->alias = $route; if (! $this->before) { $this->before(function ($params) { return $params; }); } return $this; } /** * Set url builder callback * @param callable $callback { * @callback-param array $params * @callback-param array $options * @callback-param \bff\base\Route $route * @callback-return string * } * @return static */ public function url(callable $callback) { $this->urlBuilder = $callback; return $this; } /** * Set url domain * @param string $domain * @return static */ public function domain(string $domain) { $this->domain = $domain; return $this; } /** * Define route placeholder * @param string $name * @param string $regex * @param string|null $regexOptional * @param int $type * @return $this */ public function where(string $name, string $regex, ?string $regexOptional = null, $type = TYPE_STR) { $this->wheres[$name] = [ 'r' => $regex, 'ro' => $regexOptional ?? $regex, 't' => $type, ]; $this->placeholders = null; # force reparse uri return $this; } /** * Set default params values * @param string|array $key * @param mixed $value * @return static */ public function defaults($key, $value = null) { if (! is_array($key)) { $key = [$key => $value]; } foreach ($key as $k => $v) { $this->defaults[$k] = $v; } return $this; } /** * Set default params values * @param array $defaults * @return static */ public function setDefaults(array $defaults) { return $this->defaults($defaults); } /** * Set route priority * @param int $priority * @return static */ public function priority(int $priority) { $this->router->setRoutePriority($this->getId(), $priority); return $this; } /** * Set route middleware * @param string|array|\Closure $middleware * @return static */ public function middleware($middleware) { $this->middleware = array_merge($this->middleware, func_get_args()); return $this; } /** * Set route middleware that should be excluded from the final stack * @param string|array|\Closure $middleware * @return static */ public function withoutMiddleware($middleware) { $this->middlewareExcluded = array_merge($this->middlewareExcluded, func_get_args()); return $this; } }