rukbat/bitly-bundle
Rukbat Bitly Bundle offers a lightweight integration for shortening URLs through the Bitly API in your PHP application. Configure your access token, then generate short links directly from your code with a simple service wrapper and minimal setup.
Read this first: it is not a Laravel package. rukbat/bitly-bundle is a Symfony2 bundle (RukbatBitlyBundle, DependencyInjection/*Extension, Resources/config/services.xml). There is no ServiceProvider, no publishable config, no facade, and no container auto-resolution path Laravel can consume. Do not composer require it into a Laravel app — you will spend more time fighting Guzzle 3/4 constraints and PHP 5.x-era syntax than the integration is worth.
What to do instead (Laravel-first):
Http client hitting Bitly v4 (POST https://api-ssl.bitly.com/v4/shorten, GET /v4/bitlinks/{bitlink}, GET /v4/bitlinks/{bitlink}/clicks/summary).// app/Contracts/Shortener.php
interface Shortener {
public function shorten(string $url): string;
public function expand(string $bitlink): string;
public function clicks(string $bitlink): int;
}
// app/Services/BitlyShortener.php
final class BitlyShortener implements Shortener
{
public function __construct(
private readonly Http $http,
private readonly string $token,
) {}
public function shorten(string $url): string
{
return $this->http->withToken($this->token)
->post('https://api-ssl.bitly.com/v4/shorten', ['long_url' => $url])
->throw()
->json('link');
}
}
Bind it in a service provider ($this->app->bind(Shortener::class, BitlyShortener::class)), pull the token from config('services.bitly.token') sourced from BITLY_TOKEN in .env, and never commit it.
If you genuinely maintain a legacy Symfony 2.8 app (the only stack where the bundle runs natively): require it, register Rukbat\BitlyBundle\RukbatBitlyBundle in app/AppKernel.php, add rukbat_bitly: { access_token: "%bitly_access_token%" } to config.yml, and fetch the service via $this->get('bitly'). Before assuming the service id, open Resources/config/services.xml — the id is not guaranteed to be bitly. Also read DependencyInjection/Configuration.php to confirm whether access_token is required or optional.
First use case to validate wiring: shorten one URL and assert you get back a bit.ly link. That single call exercises auth, HTTP transport, and response parsing — if it works, everything else (expand, clicks) is the same shape.
Never call the shortener inline from controllers or Blade. The whole point of wrapping it is that Bitly is an external dependency with an expiry date. $this->get('bitly')->shorten($url) in a controller is fine for a spike and wrong for production.
Wrap + interface + fake. In Laravel that's the Shortener binding above; in tests, swap in a fake that returns deterministic links. This is the single highest-leverage decision — when Bitly v4 is eventually retired, you replace one class, not every call site.
Dedupe on write. Bitly has no idempotency key on shorten, so a retried request after a timeout creates a second short link and fragments your click analytics. Always check a local table first:
$link = ShortLink::firstOrCreate(
['long_url_hash' => hash('sha256', $url)],
['long_url' => $url, 'bitlink' => $shortener->shorten($url)],
);
Key on a hash of the long URL (some URLs exceed index length limits). This is the biggest day-to-day win and it's absent from the bundle.
Batch shortening is a loop. Bitly v4 has no bulk endpoint — one long URL per call. For lists (campaign assets, product URLs), push a job per URL onto a queue with rate limiting rather than looping synchronously in a request. Laravel's RateLimited job middleware maps well here.
Cache click analytics aggressively. clicks is not real-time on Bitly's side; polling per page render burns quota for stale numbers. Pull stats on a schedule (php artisan schedule:run → console command) into a bitly_clicks table, then read from your own DB. Keep a short TTL cache (5–15 min) even on the scheduled pull.
Branded links / custom back-halves. Pass domain and key in the shorten payload (['long_url' => ..., 'domain' => 'your.co', 'key' => 'abc123']). Check whether the account has a verified branded domain first — this fails with a 4xx, not a silent fallback.
Graceful degradation. Store the destination URL alongside the bitlink so a Bitly outage degrades to serving the raw URL rather than a broken link in an SMS or email. Wrap all calls in a try/catch that logs and returns the long URL:
try {
return $shortener->shorten($url);
} catch (BitlyException $e) {
report($e);
return $url; // never break the user flow over link shortening
}
Laravel integration points: a Blade directive or component (<x-short-link :url="$post->url" />) that reads from your cache is clean; resolving the shortener directly inside Blade is not. In queued mailables, pre-shorten at dispatch time, not inside the mailable's render.
The bundle almost certainly targets Bitly v3, which was retired in 2020. v3 used api.bit.ly/v3/ with access_token/login+apiKey query params; v4 requires an OAuth Bearer token and different paths (api-ssl.bitly.com/v4/). Even if you're on Symfony 2.8, expect the core call path to be dead. Verify the URL constants in the source before anything else.
EOL stack = no security patch path. Symfony 2.0–2.8 has been EOL since 2019. Pin-and-fork it or, better, extract the client class and re-host it. The code surface is small enough that a fork is cheap — but it's a permanent maintenance line item and a recurring audit question.
Guzzle constraint clashes. The bundle pins Guzzle 3.x/4.x. That will not co-exist with Guzzle 6/7 in any current project, and the failure usually surfaces at autoload or first method call rather than at composer install. If you see signature or Class not found errors on an existing install, check composer why guzzlehttp/guzzle before debugging your own code.
PHP 8 reserved words and nullability. Code written for PHP 5.3–5.6 may use names the PHP 8 parser rejects, and implicit-nullable parameter warnings can become TypeErrors. If you copy anything verbatim, audit the signatures.
429s arrive exactly when the feature matters — campaign launches, bulk imports. Add exponential backoff with jitter (Http::retry(3, 200, throw: false)), queue non-interactive shortening, and alert on sustained 429s separately from other errors.
Token expiry fails silently as 403s. Don't rely on exceptions for auth failures in a thin wrapper; explicitly check $response->status() for 401/403 and log + alert. A revoked token should page someone, not degrade quietly into empty links.
Thin wrappers commonly swallow errors and return null. Whatever you build or fork, make the failure explicit — throw a domain exception, log with context (long URL, account), and let the caller decide whether to degrade.
Service-id discovery (Symfony path). Don't guess bitly as the container id — read Resources/config/services.xml. The service methods mirror Bitly endpoint names (shorten, expand, clicks, referrers, countries), but argument shapes (link vs hash, options array vs positional args) vary and aren't documented here. Read the class.
Extension point worth keeping. The bundle's only genuinely useful abstraction is the container-managed client wrapper. Preserve that idea in whatever replaces it — a bound interface — so testing and swapping stay cheap. Everything else (the DI extension, the config tree, the bundle class) is plumbing you can discard.
How can I help you explore Laravel packages today?