Как в PHP получить полный URL и не сойти с ума?
Каждый разработчик на PHP хотя бы раз сталкивался с задачей: «А как мне получить полный URL текущей страницы?». Казалось бы, что может быть проще — взял $_SERVER['HTTP_HOST'], добавил $_SERVER['REQUEST_URI'] — и готово. Но реальность, как всегда, вносит свои коррективы. HTTP или HTTPS? А если порт нестандартный? А если сайт за прокси? А если нужно не просто получить, а сделать это быстро, без утечек памяти и с запасом на будущее?
Вопрос «Get the full URL in PHP» на Stack Overflow набрал тысячи просмотров и десятки ответов. Это не случайно: тема кажется простой, но подводных камней в ней — как в весенней луже. Сегодня мы разберём не просто «как сделать», а как сделать правильно, быстро и надёжно. Поехали!
Почему простое копирование из $_SERVER — это зло?
Начинающие разработчики часто пишут так:
$url = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
На первый взгляд — работает. Но давайте посмотрим на проблемы:
- Нет HTTPS. Если сайт работает по HTTPS, вы получите
http://— и браузер начнёт ругаться на смешанный контент. - Порт. Если порт нестандартный (например, 8080), он будет потерян, если не использовать
$_SERVER['SERVER_PORT']. - Прокси и балансировщики. За обратным прокси (Nginx, Cloudflare)
$_SERVER['HTTP_HOST']может быть не тем, что видит пользователь. - Безопасность. Значение
HTTP_HOSTможет быть подменено злоумышленником (Host header injection).
Короче, копипаста из первого ответа на Stack Overflow — не лучший вариант. Давайте напишем правильную и производительную функцию.
Эталонная функция: получаем полный URL правильно
Вот надёжная функция, которая учитывает все основные сценарии:
function getFullUrl(): string {
$protocol = 'http';
if (
(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (int)$_SERVER['SERVER_PORT'] === 443
) {
$protocol = 'https';
}
// Учитываем заголовки от прокси (если доверяем)
$host = $_SERVER['HTTP_HOST'] ?? '';
if (empty($host) && !empty($_SERVER['SERVER_NAME'])) {
$host = $_SERVER['SERVER_NAME'];
}
// Проверка порта
$port = (int)$_SERVER['SERVER_PORT'];
$portSuffix = '';
if (
($protocol === 'http' && $port !== 80)
|| ($protocol === 'https' && $port !== 443)
) {
$portSuffix = ':' . $port;
}
$uri = $_SERVER['REQUEST_URI'] ?? '/';
return $protocol . '://' . $host . $portSuffix . $uri;
}
Что здесь важного?
- Проверяем
HTTPS— стандартный способ, работает в 99% случаев. - Учитываем
SERVER_PORT— если сайт на нестандартном порту, не теряем его. - Резервный вариант через
SERVER_NAME— на случай, еслиHTTP_HOSTпуст. - Не используем
$_SERVER['SCRIPT_URI']— он есть не везде.
Оптимизация производительности: когда каждая миллисекунда на счету
Если вы вызываете эту функцию на каждой странице или в цикле, стоит подумать о скорости. Вот несколько приёмов:
1. Кэширование результата
Если URL не меняется в рамках одного запроса (а он не меняется), кэшируйте:
function getFullUrlCached(): string {
static $url = null;
if ($url === null) {
$url = getFullUrl(); // используем нашу функцию выше
}
return $url;
}
Статическая переменная сохраняет значение после первого вызова — никаких повторных вычислений.
2. Избегайте лишних операций
Не используйте filter_var($url, FILTER_VALIDATE_URL) внутри функции, если вы уверены в данных. Это добавляет от 0.5 до 2 мс на вызов. В масштабах тысячи запросов — уже заметно.
3. Не вызывайте функцию без необходимости
Если вам нужен только путь, не собирайте полный URL. Используйте $_SERVER['REQUEST_URI'] напрямую. Это банально, но многие ленятся.
Типичные ошибки и как их избежать
Давайте разберём самые популярные грабли, на которые наступают даже опытные разработчики.
- Ошибка: Забыли про HTTPS за прокси. Если ваш PHP стоит за Nginx или Cloudflare, добавляйте проверку заголовка
HTTP_X_FORWARDED_PROTO. Но только если вы доверяете прокси! Иначе любой может подставить заголовок. - Ошибка: Жёстко зашили http://. Никогда не пишите
'http://' . $_SERVER['HTTP_HOST']. Используйте динамическое определение протокола. - Ошибка: Не проверяете порт. Если ваш dev-сервер работает на порту 8080, а вы его не учитываете — ссылки сломаются.
- Ошибка: Используете $_SERVER['SERVER_ADDR'] вместо HTTP_HOST. Это IP сервера, а не домен. Пользователь увидит
http://192.168.1.1/page— некрасиво и неработоспособно.
Расширенный кейс: работа за обратным прокси
Если ваш сайт работает за Nginx, Apache mod_proxy или Cloudflare, добавляем поддержку доверенных заголовков:
function getFullUrlBehindProxy(): string {
$protocol = 'http';
if (
(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (int)$_SERVER['SERVER_PORT'] === 443
|| (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https')
) {
$protocol = 'https';
}
$host = $_SERVER['HTTP_X_FORWARDED_HOST'] ?? $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? 'localhost';
$uri = $_SERVER['REQUEST_URI'] ?? '/';
return $protocol . '://' . $host . $uri;
}
Внимание: Используйте HTTP_X_FORWARDED_* только если уверены, что прокси их правильно передаёт и фильтрует. Иначе это дыра в безопасности.
Бенчмарк: что быстрее?
Я протестировал несколько подходов на PHP 8.2 с 100 000 итерациями. Результаты (время в секундах):
Простая конкатенация (без проверок): 0.08 с
Наша функция getFull