rezzza/shorty
Shorty is a PHP URL shortener library that creates compact links, stores short codes, and resolves redirects. It provides a straightforward API for adding link shortening and management to your applications.
rezzza/shorty ("underwear for your long urls") is a thin abstraction over third-party URL shorteners (Bitly, Google, etc.). It was built in the Symfony ecosystem around 2015, so on Laravel you should treat it as a library you wire up yourself, not a package that drops in cleanly.
composer require rezzza/shorty
Before committing, confirm the resolved constraint. The last release was December 2015, so on modern PHP (8.x) Composer may resolve an old tag that fails composer install for platform reasons, or silently pull a Guzzle major that has since been replaced. Check composer show rezzza/shorty and, if necessary, add a "config": { "platform": { "php": "7.4" } } override only in a legacy branch — better to fork.
This predates Laravel 5.5 auto-discovery. Add a binding in a service provider:
// app/Providers/AppServiceProvider.php
public function register(): void
{
$this->app->singleton(\Rezzza\Shorty\Shorty::class, function ($app) {
return new \Rezzza\Shorty\Shorty(
config('services.shorty.provider'),
config('services.shorty.credentials')
);
});
}
(Exact constructor signature varies by tag — read vendor/rezzza/shorty/src first, then bind to what's actually there.)
vendor/rezzza/shorty/src/ — the provider classes tell you which services still have live APIs.vendor/rezzza/shorty/README.md — the only usage docs; they target Symfony, so translate the service-container wiring into a Laravel binding.config/services.php, not a vendor-published file, so they are env-driven.Shorten a link in a controller and redirect:
public function share(Post $post)
{
$short = app(\Rezzza\Shorty\Shorty::class)->shorten(
route('posts.show', $post)
);
return response()->json(['url' => $short]);
}
If your chosen provider's API key is valid, that is the whole loop. Getting to this point is 90% of the work with this package; the shortening call itself is trivial.
Because the package is unmaintained, never let Rezzza\Shorty\* types leak into controllers or models. Define a small contract and one adapter:
interface UrlShortener
{
public function shorten(string $url): string;
}
final class ShortyUrlShortener implements UrlShortener
{
public function __construct(private \Rezzza\Shorty\Shorty $shorty) {}
public function shorten(string $url): string
{
return $this->shorty->shorten($url);
}
}
Bind the interface in your provider. When the package finally breaks (or when your provider's API version is retired), you swap one class, not every call site.
For content that always needs a share link, generate it on save rather than on every render:
class PostObserver
{
public function created(Post $post): void
{
dispatch(function () use ($post) {
$post->update([
'short_url' => app(UrlShortener::class)
->shorten($post->permalink()),
]);
})->afterResponse();
}
}
Store the result in a short_url column. Shortening is a network call — never do it inside a Blade template or an accessor that runs per row.
Short URLs are immutable for a given long URL (within a provider). Cache them:
public function shorten(string $url): string
{
return Cache::remember(
'shorty:'.md5($url),
now()->addMonth(),
fn () => $this->shorty->shorten($url),
);
}
This also protects you from rate limits, which is the single most common production failure with these providers.
Importing a few hundred links? Fan out one job per URL with a rate-limited queue:
public function process(Batch $batch)
{
foreach ($batch->links as $link) {
ShortenLink::dispatch($link)->onQueue('shorty');
}
}
Configure the queue worker with a low throughput and a RateLimited middleware (Illuminate\Queue\Middleware\RateLimited::class) so you respect the provider's per-minute quota instead of burning through it in a second.
The library's design is provider-per-class: you configure a provider name and credentials, and the Shorty object delegates. If the bundled provider list is stale (very likely by now), extend it with a small local class implementing the expected interface and register that — for instance a BitlyV4Provider that talks to the current Bitly v4 endpoint. Keep this class in your app, not in a fork of the vendor tree, so composer update doesn't clobber it.
Do not hit the network in tests. Bind a fake:
$this->app->instance(UrlShortener::class, new class implements UrlShortener {
public function shorten(string $url): string
{
return 'https://sho.rt/'.substr(md5($url), 0, 7);
}
});
Then assert on the fake's deterministic output. Add a single integration test (marked @group external) that hits the real provider against a known URL to catch credential/API drift.
Last release December 2015. It predates Laravel auto-discovery, uses older PSR conventions, and pins Guzzle versions that conflict with current Laravel/Symfony HTTP clients. Do a timeboxed spike: install it in a throwaway app and confirm the resolved dependency graph before promising it to anyone. If it fights you for more than a few hours, implementing a 40-line Bitly client yourself is a better investment than a fork.
login + api_key pair no longer authenticates.Expect to write your own provider adapter against the current API regardless of what ships in the box.
If you bind credentials in a closure reading config(), remember php artisan config:cache. In deployments where .env changes without re-caching config, you get a Shorty constructed with null credentials that fails only at the first shorten() call — long after boot. Fail fast: validate credentials in the service provider and throw on missing values.
The library tends to surface provider errors as generic exceptions. Wrap calls so you can distinguish transient failures (429, 5xx, timeouts) from permanent ones (invalid URL, revoked token):
try {
$short = $this->shorty->shorten($url);
} catch (\Throwable $e) {
Log::warning('Shortening failed', ['url' => $url, 'error' => $e->getMessage()]);
throw $e; // let the queue retry with backoff
}
Set an HTTP timeout (a few seconds) and retry transient failures with exponential backoff via the queue's retryUntil/backoff. Never let a hanging shortening call block a user-facing request.
The providers differ on what they accept, but almost all reject non-HTTP(S) schemes. Normalize and validate first:
$url = filter_var($input, FILTER_VALIDATE_URL);
if (! $url || ! Str::startsWith($url, ['http://', 'https://'])) {
throw new InvalidArgumentException('Unsupported URL');
}
This turns a confusing remote 400 into a clear local error.
When shorten() returns nothing useful:
dd(get_class(app(\Rezzza\Shorty\Shorty::class))) — confirm the binding resolved a real instance.curl directly — this isolates "package bug" from "API key problem."vendor/ and read.For a new project, this package is hard to justify. Either:
UrlShortener interface, orThe UrlShortener abstraction above makes both paths and a future swap cheap, which is really the day-to-day value of looking at this package at all.
How can I help you explore Laravel packages today?