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;
}
}