app = $app; # Поддиректория $subDir = $this->app->request()->hostSubdir(); if (!empty($subDir)) { $this->segments['subdir'] = [ 'value' => $subDir, 'required' => true, 'position' => 0, ]; } # Локализация $this->segments['locale'] = [ 'value' => function ($key, $params, $settings) { if (!isset($settings[$key])) { $settings[$key] = $this->app->locale()->current(); } return trim($this->app->locale()->getLanguageUrlPrefix($settings[$key], false), '/'); }, 'position' => 1, ]; $this->segments = $this->app->filter('url.segments', $this->segments); $this->adminPath = $this->app->adminPanel(true); } /** * Добавление отрезка * @param string $key уникальный ключ отрезка * @param Closure $callback функция возвращающая параметры отрезка: [ * string|Closure 'value' значение отрезка (текст или функция его возвращающая) * bool 'required' обязательность отрезка * int 'position' порядок отрезка * ] * @return void */ public function segmentAdd(string $key, Closure $callback) { $this->segments[$key] = $callback; } /** * Удаление отрезка * @param string $key ключ отрезка * @return void */ public function segmentRemove(string $key) { unset($this->segments[$key]); } /** * Настройки отрезков * @param array $settings настройки: * string 'before' получить отрезки до указанного * bool 'regexp' значения следует подготовить для подстановки в регулярное выражение * bool 'raw' получить все данные о настройках отрезков (default=false) * @return array */ public function segments(array $settings = []): array { # Инициализация + сортировка $segments = $this->segments; $sort = []; foreach ($segments as $k => &$v) { if ($v instanceof Closure) { $v = call_user_func($v, $settings); if (empty($v)) { unset($segments[$k]); continue; } } $sort[$k] = $v['position']; } unset($v); array_multisort($sort, SORT_ASC, $segments); # Ограничитель 'before' if (!empty($settings['before']) && isset($segments[$settings['before']])) { $remove = false; foreach ($segments as $k => &$v) { if (!$remove && $k === $settings['before']) { $remove = true; } if ($remove) { unset($segments[$k]); } } unset($v); } # Подготавливаем 'value' foreach ($segments as $k => &$v) { if ($v['value'] instanceof Closure) { $v['value'] = call_user_func($v['value'], $k, $v, $settings); } } unset($v); # Только значения if (!isset($settings['raw'])) { $values = []; foreach ($segments as $v) { if ($v['value'] !== '') { $values[] = (!empty($settings['regexp']) ? preg_quote($v['value'], '/') : $v['value']); } } return $values; } return $segments; } /** * Текущий URL запроса * @return string */ public function current(): string { return $this->to($this->app->request()->uri(false)); } /** * Текущий полный URL запроса * @return string */ public function full(): string { return $this->app->request()->url(true); } /** * Предыдущий полный URL запроса * @param string $fallback * @return string */ public function previous(string $fallback = ''): string { $referrer = $this->app->request()->referer(); if (! empty($referrer)) { return $referrer; } if ($fallback) { if ($this->isValid($fallback)) { return $fallback; } return $this->to($fallback); } return $this->to('/'); } /** * Формирование базового URL * @param string $path * @param array $opts доп. параметры: * string|bool|null 'lang' ключ языка, null - текущий, false - без языка * bool 'dynamic' динамическая ссылка * string|null 'scheme' схема - 'http', 'https', null - схема текущего запроса * string|null 'domain' основной домен, null - по-умолчанию * array 'subdomains' поддомены * @return string */ public function to(string $path = '/', array $opts = []) { $query = ''; if (isset($opts['query'])) { $query = $this->query($opts['query'], [], (mb_stripos($path, '?') !== false ? '&' : '?')); } if ($this->isValid($path)) { return $path . $query; } $subdomains = (!empty($opts['subdomains']) ? join('.', $opts['subdomains']) . '.' : '' ); if (! empty($opts['dynamic'])) { return '//' . $subdomains . static::HOST_PLACEHOLDER . $path . $query; } $lang = $opts['lang'] ?? null; if ($lang !== false) { $lang = $this->app->locale()->getLanguageUrlPrefix($lang, false); } else { $lang = ''; } return ($opts['scheme'] ?? $this->app->request()->scheme()) . '://' . $subdomains . ($opts['domain'] ?? SITEHOST) . $lang . $path . $query; } /** * URL роута * @param string $id ключ роута * @param array $params параметры подставляемые в строку роута * @param array $opts доп. параметры для формирования полного URL * @return string */ public function route(string $id, array $params = [], array $opts = []): string { return $this->app->router()->url($id, $params, $opts); } /** * Формирование обвертки для внешней ссылки * @param string $url * @return string */ public function away(string $url): string { $isSecure = mb_stripos($url, 'https://') === 0; $url = str_replace(['http://', 'https://', 'ftp://'], '', $url); if (empty($url) || $url === '/') { return $this->to(); } return $this->route('away', ['url' => $url, 'https' => $isSecure ? 1 : 0], [ 'dynamic' => false, 'module' => 'site', ]); } /** * Формирование URL в админ-панели * @param string|null $path в формате /{controller}/{method}/{action}?{param}= * @param array $query параметры передаваемые в URL * @param bool $escape выполнять квотирование, false - не выполнять, 'html', 'js' * @return string строка вида index.php?s={controller}&ev={method}&act={action}&{param}= */ public function admin(?string $path = '', array $query = [], $escape = false): string { if (is_null($path) || $path === '') { return $this->adminPath; } $path = explode('/', trim(str_replace('?', '&', $path), '/ ')); $pathQuery = function ($key, $path) use (&$query) { if (stripos($path, '&') !== false) { # перенесем '¶m=' из $path в $query list($path, $pathQuery) = explode('&', $path, 2); parse_str($pathQuery, $pathQuery); $query = array_merge($pathQuery, $query); } return $path; }; $controller = []; foreach (['s', 'ev', 'act'] as $k => $v) { if (isset($path[$k])) { $controller[$v] = $pathQuery($v, $path[$k]); } } $query = $controller + $query; if ($escape === false) { return $this->adminPath . $this->query($query); } return HTML::escape($this->adminPath . $this->query($query), $escape); } /** * Формирование URL для прямого вызова {action} в ajax методе контроллера * @param string $controller * @param string $action * @param array $query параметры передаваемые в URL * @param array $opts [method, escape, query-ignore] * @return string */ public function ajax(string $controller, string $action, array $query = [], array $opts = []) { $query['bff'] = 'ajax'; $query['act'] = $action; return $this->direct($controller, $opts['method'] ?? 'ajax', $query, $opts); } /** * Формирование URL для прямого вызова метода контроллера * @param string $controller * @param string $method * @param array $query параметры передаваемые в URL * @param array $opts [escape, query-ignore] * @return string строка вида /index.php?s={controller}&ev={method}&act={action}&{param}= */ public function direct(string $controller, string $method, array $query = [], array $opts = []) { $query['s'] = $controller; $query['ev'] = $method; $url = '/index.php' . $this->query($query, $opts['query-ignore'] ?? []); return !empty($opts['escape']) ? HTML::escape($url, $opts['escape']) : $url; } /** * Is url dynamic * @param string $url * @return bool */ public function isDynamic(string $url) { return (strpos($url, '{site') !== false); } /** * Формирование итоговой ссылки из динамической (c HOST_PLACEHOLDER) * @param string $url ссылка * @param array $query доп. параметры ссылки (?a=1&b=2) * @param array $opts [ * string 'lang' - ключ языка, null - текущий * string 'scheme' - протокол: 'http', 'https', empty - текущий * ] * @return string */ public function dynamic(string $url, array $query = [], array $opts = []) { $opts = $this->defaults($opts, [ 'lang' => $this->app->locale()->current(), ]); if (empty($opts['scheme'])) { $opts['scheme'] = $this->app->request()->scheme(); } $url = strtr($url, [ static::HOST_PLACEHOLDER => SITEHOST . $this->app->locale()->getLanguageUrlPrefix($opts['lang'], false) ]); if (! empty($url) && $url[0] == '/') { $url = $opts['scheme'] . ':' . $url; } return $url . $this->query($query); } /** * Формирование параметров запроса * @param array|mixed $query параметры * @param array $ignore ключи игнорируемых параметров * @param string $glue * @param int $enc способ кодирования параметров * @return string */ public function query($query = [], array $ignore = [], string $glue = '?', int $enc = PHP_QUERY_RFC1738): string { do { if (empty($query)) { break; } if (is_array($query)) { if (! empty($ignore)) { $query = array_diff_key($query, array_flip($ignore)); if (empty($query)) { break; } } return $glue . http_build_query($query, null, ini_get('arg_separator.output'), $enc); } if (is_scalar($query)) { return $glue . strval($query); } } while (false); return ''; } /** * Проверяем является ли указанный путь корректным URL * @param string $path * @return bool */ public function isValid(string $path): bool { if (! preg_match('~^(#|//|https?://)~', $path)) { return filter_var($path, FILTER_VALIDATE_URL) !== false; } return true; } }