<?php<liu21st@gmail.com>declare (strict_types = 1);
namespace think;
use Closure;
use think\exception\RouteNotFoundException;
use think\route\Dispatch;
use think\route\dispatch\Callback;
use think\route\dispatch\Url as UrlDispatch;
use think\route\Domain;
use think\route\Resource;
use think\route\Rule;
use think\route\RuleGroup;
use think\route\RuleItem;
use think\route\RuleName;
use think\route\Url as UrlBuild;
class Route
{
protected $rest = [
'index' => ['get', '', 'index'],
'create' => ['get', '/create', 'create'],
'edit' => ['get', '/<id>/edit', 'edit'],
'read' => ['get', '/<id>', 'read'],
'save' => ['post', '', 'save'],
'update' => ['put', '/<id>', 'update'],
'delete' => ['delete', '/<id>', 'delete'],
];
protected $config = [
'pathinfo_depr' => '/',
'url_lazy_route' => false,
'url_route_must' => false,
'route_rule_merge' => false,
'route_complete_match' => false,
'remove_slash' => false,
'route_annotation' => false,
'default_route_pattern' => '[\w\.]+',
'url_html_suffix' => 'html',
'controller_layer' => 'controller',
'empty_controller' => 'Error',
'controller_suffix' => false,
'default_controller' => 'Index',
'default_action' => 'index',
'action_suffix' => '',
'url_common_param' => true,
];
protected $app;
protected $request;
protected $ruleName;
protected $host;
protected $group;
protected $bind = [];
protected $domains = [];
protected $cross;
protected $lazy = false;
protected $isTest = false;
protected $mergeRuleRegex = false;
protected $removeSlash = false;
public function __construct(App $app)
{
$this->app = $app;
$this->ruleName = new RuleName();
$this->setDefaultDomain();
if (is_file($this->app->getRuntimePath() . 'route.php')) {
$this->import(include $this->app->getRuntimePath() . 'route.php');
}
}
protected function init()
{
$this->config = array_merge($this->config, $this->app->config->get('route'));
if (!empty($this->config['middleware'])) {
$this->app->middleware->import($this->config['middleware'], 'route');
}
$this->lazy($this->config['url_lazy_route']);
$this->mergeRuleRegex = $this->config['route_rule_merge'];
$this->removeSlash = $this->config['remove_slash'];
$this->group->removeSlash($this->removeSlash);
}
public function config(string $name = null)
{
if (is_null($name)) {
return $this->config;
}
return $this->config[$name] ?? null;
}
public function lazy(bool $lazy = true)
{
$this->lazy = $lazy;
return $this;
}
public function setTestMode(bool $test): void
{
$this->isTest = $test;
}
public function isTest(): bool
{
return $this->isTest;
}
public function mergeRuleRegex(bool $merge = true)
{
$this->mergeRuleRegex = $merge;
$this->group->mergeRuleRegex($merge);
return $this;
}
protected function setDefaultDomain(): void
{
$domain = new Domain($this);
$this->domains['-'] = $domain;
$this->group = $domain;
}
public function setGroup(RuleGroup $group): void
{
$this->group = $group;
}
public function getGroup(string $name = null)
{
return $name ? $this->ruleName->getGroup($name) : $this->group;
}
public function pattern(array $pattern)
{
$this->group->pattern($pattern);
return $this;
}
public function option(array $option)
{
$this->group->option($option);
return $this;
}
public function domain($name, $rule = null): Domain
{
$domainName = is_array($name) ? array_shift($name) : $name;
if (!isset($this->domains[$domainName])) {
$domain = (new Domain($this, $domainName, $rule))
->lazy($this->lazy)
->removeSlash($this->removeSlash)
->mergeRuleRegex($this->mergeRuleRegex);
$this->domains[$domainName] = $domain;
} else {
$domain = $this->domains[$domainName];
$domain->parseGroupRule($rule);
}
if (is_array($name) && !empty($name)) {
foreach ($name as $item) {
$this->domains[$item] = $domainName;
}
}
return $domain;
}
public function getDomains(): array
{
return $this->domains;
}
public function getRuleName(): RuleName
{
return $this->ruleName;
}
public function bind(string $bind, string $domain = null)
{
$domain = is_null($domain) ? '-' : $domain;
$this->bind[$domain] = $bind;
return $this;
}
public function getBind(): array
{
return $this->bind;
}
public function getDomainBind(string $domain = null)
{
if (is_null($domain)) {
$domain = $this->host;
} elseif (false === strpos($domain, '.') && $this->request) {
$domain .= '.' . $this->request->rootDomain();
}
if ($this->request) {
$subDomain = $this->request->subDomain();
if (strpos($subDomain, '.')) {
$name = '*' . strstr($subDomain, '.');
}
}
if (isset($this->bind[$domain])) {
$result = $this->bind[$domain];
} elseif (isset($name) && isset($this->bind[$name])) {
$result = $this->bind[$name];
} elseif (!empty($subDomain) && isset($this->bind['*'])) {
$result = $this->bind['*'];
} else {
$result = null;
}
return $result;
}
public function getName(string $name = null, string $domain = null, string $method = '*'): array
{
return $this->ruleName->getName($name, $domain, $method);
}
public function import(array $name): void
{
$this->ruleName->import($name);
}
public function setName(string $name, RuleItem $ruleItem, bool $first = false): void
{
$this->ruleName->setName($name, $ruleItem, $first);
}
public function setRule(string $rule, RuleItem $ruleItem = null): void
{
$this->ruleName->setRule($rule, $ruleItem);
}
public function getRule(string $rule): array
{
return $this->ruleName->getRule($rule);
}
public function getRuleList(): array
{
return $this->ruleName->getRuleList();
}
public function clear(): void
{
$this->ruleName->clear();
if ($this->group) {
$this->group->clear();
}
}
public function rule(string $rule, $route = null, string $method = '*'): RuleItem
{
if ($route instanceof Response) {
$route = function () use ($route) {
return $route;
};
}
return $this->group->addRule($rule, $route, $method);
}
public function setCrossDomainRule(Rule $rule, string $method = '*')
{
if (!isset($this->cross)) {
$this->cross = (new RuleGroup($this))->mergeRuleRegex($this->mergeRuleRegex);
}
$this->cross->addRuleItem($rule, $method);
return $this;
}
public function group($name, $route = null): RuleGroup
{
if ($name instanceof Closure) {
$route = $name;
$name = '';
}
return (new RuleGroup($this, $this->group, $name, $route))
->lazy($this->lazy)
->removeSlash($this->removeSlash)
->mergeRuleRegex($this->mergeRuleRegex);
}
public function any(string $rule, $route): RuleItem
{
return $this->rule($rule, $route, '*');
}
public function get(string $rule, $route): RuleItem
{
return $this->rule($rule, $route, 'GET');
}
public function post(string $rule, $route): RuleItem
{
return $this->rule($rule, $route, 'POST');
}
public function put(string $rule, $route): RuleItem
{
return $this->rule($rule, $route, 'PUT');
}
public function delete(string $rule, $route): RuleItem
{
return $this->rule($rule, $route, 'DELETE');
}
public function patch(string $rule, $route): RuleItem
{
return $this->rule($rule, $route, 'PATCH');
}
public function options(string $rule, $route): RuleItem
{
return $this->rule($rule, $route, 'OPTIONS');
}
public function resource(string $rule, string $route): Resource
{
return (new Resource($this, $this->group, $rule, $route, $this->rest))
->lazy($this->lazy);
}
public function view(string $rule, string $template = '', array $vars = []): RuleItem
{
return $this->rule($rule, function () use ($vars, $template) {
return Response::create($template, 'view')->assign($vars);
}, 'GET');
}
public function redirect(string $rule, string $route = '', int $status = 301): RuleItem
{
return $this->rule($rule, function () use ($status, $route) {
return Response::create($route, 'redirect')->code($status);
}, '*');
}
public function rest($name, $resource = [])
{
if (is_array($name)) {
$this->rest = $resource ? $name : array_merge($this->rest, $name);
} else {
$this->rest[$name] = $resource;
}
return $this;
}
public function getRest(string $name = null)
{
if (is_null($name)) {
return $this->rest;
}
return $this->rest[$name] ?? null;
}
public function miss($route, string $method = '*'): RuleItem
{
return $this->group->miss($route, $method);
}
public function dispatch(Request $request, $withRoute = true)
{
$this->request = $request;
$this->host = $this->request->host(true);
$this->init();
if ($withRoute) {
if ($withRoute instanceof Closure) {
$withRoute();
}
$dispatch = $this->check();
} else {
$dispatch = $this->url($this->path());
}
$dispatch->init($this->app);
return $this->app->middleware->pipeline('route')
->send($request)
->then(function () use ($dispatch) {
return $dispatch->run();
});
}
public function check()
{
$url = str_replace($this->config['pathinfo_depr'], '|', $this->path());
$completeMatch = $this->config['route_complete_match'];
$result = $this->checkDomain()->check($this->request, $url, $completeMatch);
if (false === $result && !empty($this->cross)) {
$result = $this->cross->check($this->request, $url, $completeMatch);
}
if (false !== $result) {
return $result;
} elseif ($this->config['url_route_must']) {
throw new RouteNotFoundException();
}
return $this->url($url);
}
protected function path(): string
{
$suffix = $this->config['url_html_suffix'];
$pathinfo = $this->request->pathinfo();
if (false === $suffix) {
$path = $pathinfo;
} elseif ($suffix) {
$path = preg_replace('/\.(' . ltrim($suffix, '.') . ')$/i', '', $pathinfo);
} else {
$path = preg_replace('/\.' . $this->request->ext() . '$/i', '', $pathinfo);
}
return $path;
}
public function url(string $url): Dispatch
{
if ($this->request->method() == 'OPTIONS') {
return new Callback($this->request, $this->group, function () {
return Response::create('', 'html', 204)->header(['Allow' => 'GET, POST, PUT, DELETE']);
});
}
return new UrlDispatch($this->request, $this->group, $url);
}
protected function checkDomain(): Domain
{
$item = false;
if (count($this->domains) > 1) {
$subDomain = $this->request->subDomain();
$domain = $subDomain ? explode('.', $subDomain) : [];
$domain2 = $domain ? array_pop($domain) : '';
if ($domain) {
$domain3 = array_pop($domain);
}
if (isset($this->domains[$this->host])) {
$item = $this->domains[$this->host];
} elseif (isset($this->domains[$subDomain])) {
$item = $this->domains[$subDomain];
} elseif (isset($this->domains['*.' . $domain2]) && !empty($domain3)) {
$item = $this->domains['*.' . $domain2];
$panDomain = $domain3;
} elseif (isset($this->domains['*']) && !empty($domain2)) {
if ('www' != $domain2) {
$item = $this->domains['*'];
$panDomain = $domain2;
}
}
if (isset($panDomain)) {
$this->request->setPanDomain($panDomain);
}
}
if (false === $item) {
$item = $this->domains['-'];
}
if (is_string($item)) {
$item = $this->domains[$item];
}
return $item;
}
public function buildUrl(string $url = '', array $vars = []): UrlBuild
{
return $this->app->make(UrlBuild::class, [$this, $this->app, $url, $vars], true);
}
public function __call($method, $args)
{
return call_user_func_array([$this->group, $method], $args);
}
}