mremi/url-shortener
A framework-agnostic PHP library for shortening URLs through pluggable providers. It wraps popular URL-shortening services behind a simple API, supports multiple backends, and is easy to extend or integrate into existing PHP applications.
The package is a framework-agnostic PHP library, not a Laravel package — there is no service provider, facade, or config file shipped with it. That means your first step is wiring it into the container yourself.
Install it:
composer require mremi/url-shortener
It pulls in an HTTP client (Symfony HttpClient in recent versions). Check composer.lock to confirm which one, since constructor signatures differ between the 1.x (Guzzle/username+key) and 2.x (Symfony HttpClient/OAuth token) lines.
Read the source, not just the README. The README is short and the API is small. Open src/Provider/ — that's where the real surface area lives. You'll see a ProviderInterface with shorten(Link $link) / expand(Link $link) and concrete providers like BitlyProvider and GoogleProvider, plus a Link model in src/Model/.
Get one credential and one provider working first. Bitly is the only provider worth using in production today; Google's goo.gl was shut down for new links. Put BITLY_ACCESS_TOKEN in .env and never in code.
Smoke-test in Tinker before touching app code:
$client = new \Symfony\Component\HttpClient\HttpClient();
$provider = new \Mremi\UrlShortener\Provider\Bitly\BitlyProvider($client, config('services.bitly.token'));
$link = new \Mremi\UrlShortener\Model\Link();
$link->setLongUrl('https://example.com/some/very/long/path');
$provider->shorten($link);
dump($link->getShortUrl());
Your first real use case will almost always be: shorten a URL for SMS/social sharing and persist the short form alongside the original record.
Bind the provider once, resolve by interface. Register a singleton in a dedicated service provider and let the rest of the app depend on ProviderInterface, not the concrete class:
// app/Providers/UrlShortenerServiceProvider.php
public function register(): void
{
$this->app->singleton(ProviderInterface::class, function ($app) {
return new BitlyProvider(
HttpClient::create(),
config('services.bitly.token')
);
});
}
Add services.bitly.token to config/services.php so config:cache works. Do not call env() from inside the binding closure.
Persist the short URL, don't recompute it. Add short_url (and optionally shortener_provider) columns to the model. Shortening is an external HTTP call; treating it as a pure function you call on every page render is a latency and rate-limit trap.
Shorten asynchronously when possible. For bulk imports or user-triggered creation, dispatch a queued job:
class ShortenUrlJob implements ShouldQueue
{
public int $tries = 3;
public function __construct(public int $linkId) {}
public function handle(ProviderInterface $shortener): void
{
$model = Link::findOrFail($this->linkId);
$link = (new ShortenerLink())->setLongUrl($model->url);
$shortener->shorten($link);
$model->update(['short_url' => $link->getShortUrl()]);
}
}
Wrap the library in your own thin service. A UrlShortener application class that takes a ProviderInterface, logs, caches, and catches transport exceptions gives you one place to change when you swap providers:
class UrlShortener
{
public function __construct(private ProviderInterface $provider) {}
public function shorten(string $url): string
{
return Cache::remember("short:".md5($url), now()->addMonth(), function () use ($url) {
$link = (new ShortenerLink())->setLongUrl($url);
$this->provider->shorten($link);
return $link->getShortUrl();
});
}
}
The cache is important: Bitly returns the same short link for the same long URL, and it also means duplicate submissions cost you nothing.
Implement ProviderInterface for anything the package doesn't ship (Short.io, Rebrandly, Dub). The interface is small — you only need shorten and expand. This is the intended extension point and keeps your app decoupled from a library that hasn't shipped since 2021.
Test against the interface, not the network. Bind a fake ProviderInterface in tests:
$this->app->instance(ProviderInterface::class, new class implements ProviderInterface {
// return a canned Link with https://bit.ly/fake
});
Never let integration tests hit Bitly; reserve one optional live test gated on BITLY_ACCESS_TOKEN being present.
Form requests for the input side. Validate the URL with url + a scheme allowlist before it ever reaches the provider, and render the short link back to the user immediately (from cache) while the queued job does the actual write if you choose the async path.
GoogleProvider is dead. goo.gl shutdown means this code path will 4xx. Only use it as a reference implementation for building your own provider.(ClientInterface, $username, $apiKey); newer ones take (HttpClientInterface, $accessToken, array $options). Read the constructor of whatever version composer resolved rather than copying a snippet from a blog post.BindingResolutionException rather than a package-specific error — don't waste time debugging the library.shorten()/expand() in try/catch for the underlying HTTP client's exceptions, log the response body on failure (Bitly returns useful error JSON), and add a job-level retryUntil rather than blindly retrying in a foreach.Bus::batch with ->chunk(...)) and consider RateLimited middleware with Limit::perMinute().Link model is mutable and stateful. You construct it, call shorten(), then read getShortUrl() off the same object. expand() populates getLongUrl(). If you accidentally reuse a Link instance across calls you'll read stale values — always create a fresh one per operation.BitlyProvider into controllers. You lose the ability to fake it in tests and to swap providers, and you expose Bitly-specific assumptions (e.g. the 2,048-char URL cap) to your domain code.composer why-not symfony/http-client before assuming the library is broken..env and add it to your secrets manager for deployed environments.How can I help you explore Laravel packages today?