6 мин чтенияИнженерия

Как сократить URL в PHP с curl, Guzzle или WordPress

Как сократить URL в PHP с помощью короткого блока curl, того же вызова в Guzzle или Laravel, версии для WordPress с wp_remote_post, а также повторных попыток и пакетного сокращения.

Ana Kowalska
Marketing solutions engineering
Как сократить URL в PHP: POST-запрос curl отправляет URL назначения в API сокращения ссылок и извлекает короткую ссылку из JSON-ответа, рядом показаны варианты с Guzzle и WordPress

Сокращение URL в PHP - это один HTTP POST-запрос. Отправьте длинную ссылку в API сервиса сокращения с помощью расширения curl, передайте API-ключ как Bearer-токен и извлеките короткую ссылку из ответа через json_decode. Пакет Composer не нужен, хотя Guzzle и HTTP-клиент Laravel сокращают тот же вызов до трех строк.

Большинство учебников по PHP на эту тему уже стали музейными экспонатами. В них подключают urlshortener/v1, который Google закрыл после прекращения создания новых ссылок в 2019 году, а неактивные ссылки goo.gl перестали перенаправлять 25 августа 2025 года. Либо в них используется эндпоинт bit.ly v3, исчезнувший несколько лет назад. Код ниже рассчитан на актуальный API.

Это версия на PHP для общего руководства по сокращению URL. Если вы еще не выбрали сервис, в обзоре бесплатных API сокращения URL описаны форма запроса, модель авторизации и ограничения, предполагаемые здесь. Имена полей принадлежат Elido; саму структуру можно перенести.

Самый быстрый способ: один POST-запрос curl

Поместите ключ в переменную окружения, самостоятельно закодируйте тело запроса и проверьте код состояния, прежде чем доверять содержимому ответа:

<?php

$ch = curl_init('https://api.elido.app/v1/links');

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 10,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . getenv('ELIDO_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'destination_url' => 'https://example.com/spring-sale?utm_source=newsletter',
    ]),
]);

$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($body === false || $status >= 400) {
    throw new RuntimeException("shorten failed: HTTP {$status}");
}

$link = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
echo $link['short_url']; // https://s.elido.me/ab12cd

Две строки здесь появились из-за ошибок, с которыми каждый сталкивается хотя бы однажды. По умолчанию CURLOPT_RETURNTRANSFER имеет значение false, поэтому без него curl выводит JSON прямо в вывод и передает вам true. Именно это обычно стоит за вопросами «почему мой ответ пустой». А CURLOPT_POSTFIELDS переключается на кодирование multipart-формы, как только ему передают массив, поэтому именно json_encode превращает запрос в JSON.

CURLINFO_RESPONSE_CODE не менее важен. curl без проблем вернет тело с {"error": "unauthorized"} и никак на это не пожалуется.

PHP POST-запрос curl отправляет URL назначения и Bearer-токен в эндпоинт ссылок сервиса сокращения, API возвращает HTTP 201 с short_url, который читает json_decode, а цикл повторных попыток увеличивает паузу для ответов 429 и 5xx

Тот же вызов в Guzzle или Laravel

Если проект уже подключает Guzzle, шаблонный код становится короче:

use GuzzleHttp\Client;

$client = new Client([
    'base_uri' => 'https://api.elido.app',
    'timeout'  => 10,
    'headers'  => ['Authorization' => 'Bearer ' . getenv('ELIDO_API_KEY')],
]);

$response = $client->post('/v1/links', [
    'json'    => ['destination_url' => $destination],
    'headers' => ['Idempotency-Key' => hash('sha256', $destination)],
]);

$short = json_decode((string) $response->getBody(), true)['short_url'];

По умолчанию Guzzle выбрасывает ClientException для ответов 4xx и ServerException для ответов 5xx. Это противоположно молчанию curl и поведению JavaScript fetch. Перехватывайте эти исключения либо установите http_errors в false и проверяйте статус самостоятельно.

В Laravel HTTP-клиент оборачивает Guzzle и бесплатно добавляет повторные попытки:

$short = Http::withToken(config('services.elido.key'))
    ->timeout(10)
    ->retry(3, 200, throw: false)
    ->post('https://api.elido.app/v1/links', ['destination_url' => $destination])
    ->json('short_url');

retry(3, 200) означает три попытки с паузой 200 мс. Этого достаточно для временного сбоя, но недостаточно для ограничения частоты запросов. Об этом - в следующем разделе.

Сокращение ссылок внутри WordPress

WordPress не использует curl напрямую. В нем есть wp_remote_post, который сам выбирает транспорт и работает на хостингах с отключенным curl. Подключите его к публикации, а затем сохраните результат в метаданных записи, чтобы его могли прочитать шаблоны и ленты:

add_action('publish_post', function (int $post_id): void {
    if (get_post_meta($post_id, '_elido_short_url', true)) {
        return; // already shortened
    }

    $response = wp_remote_post('https://api.elido.app/v1/links', [
        'timeout' => 10,
        'headers' => [
            'Authorization'   => 'Bearer ' . ELIDO_API_KEY,
            'Content-Type'    => 'application/json',
            'Idempotency-Key' => 'post-' . $post_id,
        ],
        'body' => wp_json_encode(['destination_url' => get_permalink($post_id)]),
    ]);

    if (is_wp_error($response)) {
        error_log('shorten failed: ' . $response->get_error_message());
        return;
    }

    $link = json_decode(wp_remote_retrieve_body($response), true);
    update_post_meta($post_id, '_elido_short_url', $link['short_url']);
}, 10, 1);

ELIDO_API_KEY должен находиться в wp-config.php, а не в файле плагина, который окажется в публичном репозитории, и не в строке опций, доступной для чтения каждому администратору. Обратите внимание на ключ идемпотентности: post-123 остается неизменным, поэтому при повторном срабатывании хука во время повторной публикации второй вызов вернет уже существующую ссылку, а не создаст еще одну.

Если вы не хотите поддерживать этот хук, руководство по сокращению ссылок в WordPress сравнивает этот вариант с плагином и решениями без кода.

Хотите запустить эти фрагменты как есть? Создайте ключ на бесплатном плане, экспортируйте его как ELIDO_API_KEY, и приведенный выше блок curl заработает без изменений.

Повторные попытки, ограничения частоты и идемпотентность

PHP-код без участия пользователя - задание cron, обработчик очереди или пакетный импортер - должен переживать ответ 429 и временный ответ 5xx. Он также должен справляться с неприятным случаем, когда POST-запрос дошел до API, но ответ потерялся по пути обратно. Ваш код увидит тайм-аут, повторит запрос и создаст вторую короткую ссылку для того же назначения, если ключ идемпотентности это не остановит.

function shorten(string $destination, int $retries = 3): string
{
    $key = hash('sha256', $destination); // stable across retries and re-runs

    for ($attempt = 0; $attempt < $retries; $attempt++) {
        [$body, $status, $retryAfter] = post_link($destination, $key);

        if ($status === 429) {
            sleep(max(1, (int) $retryAfter));
            continue;
        }
        if ($status >= 500) {
            sleep(2 ** $attempt); // exponential backoff
            continue;
        }
        if ($status >= 400) {
            throw new RuntimeException("shorten failed: HTTP {$status} {$body}");
        }

        return json_decode($body, true, 512, JSON_THROW_ON_ERROR)['short_url'];
    }

    throw new RuntimeException("shorten failed after {$retries} attempts");
}

Ответ 401 или 403 никогда не исправится сам, поэтому код выбросит исключение уже при первой попытке, а не будет трижды ждать. При ответе 429 он ждет столько, сколько указано в заголовке Retry-After. Только для 5xx применяется увеличивающаяся вдвое пауза. В подробном разборе ограничений частоты и идемпотентности приведено полное обоснование, в том числе объяснение, почему хеш назначения обычно лучше случайного UUID, если один и тот же пакет может быть запущен повторно.

Есть одна специфическая для PHP ловушка: sleep() во время веб-запроса удерживает рабочий процесс php-fpm. Повторные попытки должны находиться в CLI-скриптах, заданиях очереди или WP-Cron, а не в запросе, который отображает страницу.

Пакетное сокращение без последовательных ожиданий

Цикл foreach, сокращающий 500 URL, выполняет 500 последовательных обращений туда и обратно. При 120 мс на каждый это минута чистого ожидания. В PHP нет асинхронной среды выполнения, но есть параллельные HTTP-запросы, а Pool из Guzzle поддерживает фиксированное число запросов в полете:

use GuzzleHttp\Pool;
use GuzzleHttp\Psr7\Request;

$requests = static function (array $urls) {
    foreach ($urls as $url) {
        yield new Request('POST', '/v1/links', [
            'Content-Type'    => 'application/json',
            'Idempotency-Key' => hash('sha256', $url),
        ], json_encode(['destination_url' => $url]));
    }
};

$short = [];
$pool  = new Pool($client, $requests($urls), [
    'concurrency' => 8,
    'fulfilled'   => function ($response, $i) use (&$short, $urls) {
        $short[$urls[$i]] = json_decode((string) $response->getBody(), true)['short_url'];
    },
    'rejected'    => function ($reason, $i) use (&$short, $urls) {
        $short[$urls[$i]] = 'ERROR: ' . $reason->getMessage();
    },
]);

$pool->promise()->wait();
Три пути в PHP к одному эндпоинту сервиса сокращения: обычный curl на виртуальном хостинге, Guzzle или Laravel в приложении и wp_remote_post внутри WordPress, при этом все читают API-ключ из окружения

Ключом результатов служит исходный URL, поэтому частичный сбой остается видимым и задачу можно запустить повторно. Без Composer curl_multi_init выполняет ту же работу, но требует больше служебного кода: добавьте дескрипторы, запустите цикл с curl_multi_exec, соберите каждый ответ. Начните примерно с восьми параллельных запросов и по заголовкам ответов поймите, нужно ли уменьшить это число.

Какой подход выбрать

Подберите инструмент под хостинг. Виртуальный хостинг без Composer: обычный блок curl. Приложение Symfony или Laravel: Guzzle или HTTP-клиент, в котором повторные попытки один раз настроены на уровне клиента. WordPress: wp_remote_post в хуке публикации. Ночной импорт тысяч ссылок: пул со стабильным ключом идемпотентности, чтобы повторный запуск не создавал новых ссылок.

Какой бы вариант вы ни выбрали, четыре вещи остаются неизменными: ключ берется из окружения, для curl явно задается тайм-аут, код состояния читается до тела ответа, а все, что может выполниться дважды, получает ключ идемпотентности. Командам, которые переросли первый скрипт, обычно нужны API и SDK или другие возможности платформы для разработчиков - клики, теги, срок действия, вебхуки для событий ссылок вместо опроса.

Читайте основную серию

Эта статья входит в инженерный кластер. Начните с руководства по бесплатному API сокращения URL, чтобы разобраться с формой эндпоинта и авторизацией, а затем прочитайте материал об ограничениях частоты и идемпотентности, посвященный корректной работе под нагрузкой. Актуальная справочная информация находится в документации API, а если вам достались ссылки goo.gl, в статье об альтернативе Google URL Shortener описано, куда они теперь ведут.

Другие статьи в блоге

Частые вопросы

Как сократить URL в PHP?

Отправьте длинный URL POST-запросом в API сервиса сокращения, передав API-ключ в заголовке Bearer, а URL назначения - в теле JSON, затем вызовите json_decode и прочитайте поле short_url в ответе. С curl это примерно пятнадцать строк с curl_setopt_array, а с Guzzle или HTTP-клиентом Laravel - три, если проект уже использует один из них.

Как сократить URL в PHP без внешней библиотеки?

Используйте расширение curl, которое поставляется вместе с PHP. curl_init, curl_setopt_array, curl_exec и json_decode покрывают весь процесс без зависимости Composer, что важно на виртуальном хостинге, где нельзя запускать Composer. Установите CURLOPT_RETURNTRANSFER в true, иначе curl_exec выведет ответ вместо его возврата.

Работает ли API Google URL Shortener в PHP?

Нет. Google прекратил принимать новые ссылки через API URL Shortener в 2019 году и закрыл сервис, а неактивные ссылки goo.gl перестали перенаправлять 25 августа 2025 года. Любой учебник по PHP на основе urlshortener/v1 устарел, как и примеры той же эпохи для bit.ly v3.

Можно ли сократить URL внутри WordPress с помощью PHP?

Да. Вызовите wp_remote_post из хука, срабатывающего при публикации, а затем сохраните возвращенную короткую ссылку через update_post_meta, чтобы остальная часть темы могла ее прочитать. Храните API-ключ в wp-config.php, а не в файле плагина или базе данных, и проверяйте is_wp_error для ответа, поскольку WordPress возвращает объект WP_Error, а не выбрасывает исключение.

Как сократить сразу много URL в PHP?

Отправляйте запросы параллельно через ограниченный пул, а не в цикле foreach, который ждет каждый ответ. Pool из Guzzle с параллелизмом около восьми - самый короткий путь, curl_multi делает то же самое без Composer, а Idempotency-Key, полученный из каждого URL, не позволит повторному запуску создать дубликаты ссылок.

Почему мой PHP-запрос curl к сервису сокращения URL возвращает 401 или пустой ответ?

Код 401 означает, что заголовок Authorization отсутствует или составлен неверно: он должен точно выглядеть как 'Authorization: Bearer YOUR_KEY' одной строкой в массиве CURLOPT_HTTPHEADER. Пустое возвращаемое значение обычно означает, что CURLOPT_RETURNTRANSFER не был установлен, поэтому тело сразу ушло в вывод, а curl_exec вместо него вернул true.

Попробуйте Elido

Вставьте URL - получите короткую ссылку

Без регистрации. Ссылка живёт 30 дней. Зарегистрируйтесь, чтобы оставить её навсегда.

Бесплатно, без регистрации · 2 в день

Попробуйте Elido

URL-сокращатель с хостингом в ЕС: собственные домены, глубокая аналитика, открытый API. Бесплатный тариф - без банковской карты.

Теги
how to shorten a url in php
php url shortener
shorten url php curl
url shortener api php
wordpress shorten url php
bulk shorten urls php

Читать дальше