app = $app; # Поддиректория $this->segments['subdir'] = [ 'value' => function ($key, $params, $settings) { if (! array_key_exists($key, $settings)) { $settings[$key] = $this->app->request()->hostSubdir(); } return $settings[$key] ?: ''; }, 'required' => true, 'position' => 0, ]; # Локализация $this->segments['locale'] = [ 'value' => function ($key, $params, $settings) { if (! array_key_exists($key, $settings)) { $settings[$key] = $this->app->locale()->current(); } elseif (empty($settings[$key])) { return ''; } return trim($this->app->locale()->getLanguageUrlPrefix($settings[$key]), '/'); }, 'position' => 1, ]; $this->segments = $this->app->filter('url.segments', $this->segments); } /** * Добавление отрезка * @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; } /** * Strip all known segments in path * @param string $path * @param bool $full * @return string */ public function segmentsCut(string $path, $full = false) { if (empty($path) || $path === '/') { return $path; } $cut = false; $trimmed = ($path[0] !== '/'); if (! $trimmed) { $path = ltrim($path, '/'); $cut = true; } $segments = $this->segments(); if ($full) { if (! empty($segments)) { $segments = join('/', $segments) . '/'; if (mb_stripos($path, $segments) === 0) { $path = mb_substr($path, mb_strlen($segments)); } } } else { foreach ($segments as $segment) { if (mb_strpos($path, $segment . '/') === 0) { $path = mb_substr($path, mb_strlen($segment . '/')); $cut = true; } } } if ($cut && ! $trimmed) { $path = '/' . $path; } return $path; } /** * Текущий URL запроса * @param array $opts [query, segments, dynamic] * @return string */ public function current(array $opts = []): string { if (! array_key_exists('query', $opts) || $opts['query'] === true) { $opts['query'] = $this->app->request()->query(); } if (array_key_exists('segments', $opts)) { return $this->to($this->app->router()->getUri() ?: '/', $opts); } return $this->to($this->app->request()->uri(false), array_merge($opts, [ 'lang' => false, 'subdir' => false, ])); } /** * Текущий URL запроса с указанием языка * @param string $language * @param bool|array $withQuery * @param array $opts [dynamic] * @return string */ public function currentWithLocale(string $language, $withQuery = true, array $opts = []): string { $request = $this->app->request(); $query = (is_array($withQuery) ? $withQuery : []); if ($withQuery === true) { parse_str($request->query(), $query); if (isset($query['lng'])) { unset($query['lng']); } } if (empty($query) && $this->app->locale()->getDefaultLanguage() === $language && $request->user()) { $query['lng'] = $language; } return $this->current(array_merge($opts, [ 'segments' => $this->segments(['locale' => $language]), 'query' => ($withQuery ? $query : ''), ])); } /** * Текущий полный 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 - по-умолчанию * string[] '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']) . '.' : ''); # segments if (array_key_exists('segments', $opts)) { if ($opts['segments'] === true) { # all registered $opts['segments'] = $this->segments(); } if (empty($opts['segments'])) { # without segments if (! empty($path)) { $path = '/' . ltrim($this->segmentsCut($path), '/'); } } elseif (is_array($opts['segments'])) { # only specified $path = '/' . join('/', $opts['segments']) . (!empty($path) ? '/' . ltrim($this->segmentsCut($path), '/') : ''); } } if (! empty($opts['dynamic'])) { return '//' . $subdomains . static::HOST_PLACEHOLDER . $path . $query; } # lang + subdir if (! array_key_exists('segments', $opts)) { $lang = $opts['lang'] ?? null; if ($lang !== false) { $path = $this->app->locale()->getLanguageUrlPrefix($lang) . $path; } $subDir = $opts['subdir'] ?? $this->app->request()->hostSubdir(); if (! empty($subDir)) { $path = '/' . $subDir . (!empty($path) ? '/' . ltrim($path, '/') : ''); } } $scheme = $opts['scheme'] ?? $this->app->request()->scheme(); if (! empty($scheme)) { $scheme = $scheme . '://'; } return $scheme . $subdomains . ($opts['domain'] ?? SITEHOST) . $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 { $adminPath = $this->app->adminPanel(true); if (is_null($path) || $path === '') { return HTML::escape($this->to($adminPath, ['lang' => false]), $escape); } $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; return HTML::escape($this->to($adminPath, ['query' => $query, 'lang' => false]), $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 - текущий * array|bool 'segments' - настройки отрезков * 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 => $this->to('', array_merge($opts, ['scheme' => 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; } }