mremi/url-shortener-bundle
Symfony bundle that adds URL shortening to your application. It integrates the mremi/url-shortener library with Doctrine-backed storage, configurable short-link generation, and redirect handling, making it easy to create and serve compact URLs from your Symfony project.
Do not install the bundle. composer require mremi/url-shortener-bundle pulls in Symfony's DI extension, a YAML config tree, and Doctrine-oriented service definitions — none of which Laravel's container can compile or resolve. The reusable asset is the underlying library:
composer require mremi/url-shortener
That package is framework-agnostic PHP: provider clients (Bitly, Google, TinyURL), a local/storage-backed shortener strategy, and a ShortenerInterface plus a storage interface. Pin an exact version in composer.json — with 19 stars and zero dependents, you are the QA.
Where to look first, in this order:
vendor/mremi/url-shortener/src/ShortenerInterface.php — the one contract you actually care about (shorten() returns a ShortenedUrl value object).src/Provider/* — the third-party provider clients. Check auth, endpoints, and whether they still match the live APIs before trusting them.src/Storage/* and the Doctrine implementations — read them to model your own Eloquent storage, not to copy them verbatim.Resources/config/services.yaml — it is documentation for how the library was intended to be wired. Read it, don't ship it.First use case, half-day spike. Prove a round trip in php artisan tinker or a throwaway route:
$shortener = app(\Mremi\UrlShortener\Provider\Bitly\BitlyProvider::class);
$short = $shortener->shorten('https://example.com/a/very/long/campaign/url');
dump((string) $short);
Then confirm the read path: you can resolve a stored code back to its target. If both work, you have validated the only two operations you need. Everything after this is glue.
The use case to start with: internal, regenerable links — transactional email, referral codes, "share this dashboard" links. Do not start with customer-facing permanent URLs on packaging; the maintenance risk profile of a single-maintainer package does not support that.
Wrap it behind a first-party interface. This is the whole design decision.
interface ShortLinkService
{
public function create(string $targetUrl, ?string $ownerId = null, ?Carbon $expiresAt = null): ShortLink;
public function resolve(string $code): ?ShortLink;
}
Bind it in a UrlShortenerServiceProvider. Your controllers, mailables, and jobs type-hint ShortLinkService only — never the library's classes. When upstream goes quiet (assume it will), the swap is one binding, not a refactor.
Make the package a codec, not the system of record. Ship your own short_links migration: code (unique index), target_url, owner_id, expires_at, hit_count, timestamps. Store the link yourself; use the library only to generate codes. This side-steps the bundle's Doctrine storage entirely and means an abandoned dependency costs you the encoder, not the data.
Eloquent-backed storage if you use the library's storage interface. Implement the library's storage contract as a thin adapter over an Eloquent model, then register it in the service provider. Do not attempt to port the Doctrine mappings — that is where the Symfony coupling actually lives.
Read path first, and cache it. Short links are read-skewed: one create, thousands of redirects. A redirect controller resolved from Redis keyed by code with a short TTL (minutes) is the right default. The database stays out of the hot path, and cache is strictly derived from it.
Route::get('/s/{code}', RedirectController::class)
->withoutMiddleware(['web']) // no CSRF/session on redirects
->where('code', '[A-Za-z0-9]{4,10}');
Exclude the redirect route from session, CSRF, and any localization/trailing-slash middleware that could mutate the path. Default to 302, not 301 — 301 caches in browsers and makes retargeting an existing code impossible.
Provider interaction belongs on the write path only. Never call Bitly (or any external API) during a redirect. This is the single biggest architectural guardrail: a provider outage should break new link creation with a clear error, while every existing link keeps resolving locally.
Wire config through Laravel, not Symfony YAML. Publish config/url-shortener.php with the driver (internal, bitly, tinyurl), credentials from env(), and the default domain. Map this into the service provider's bindings; do not hand-transcribe the bundle's config tree.
Collision retry with a bounded loop. On the write path, insert with a unique index on code, catch the constraint violation, regenerate, retry — capped at three attempts, then a 500 with an alert. Six characters of base62 is ~56 billion combinations; you will not exhaust it, but you will lose the race occasionally under concurrency.
Optional: an artisan command for bulk/administrative creation. php artisan shortlink:create {url} --owner= --expires= is the lowest-friction write path for ops and saves you building a UI on day one.
mremi/url-shortener and writing the wiring yourself — budget the 1–3 days honestly.composer.json PHP constraint before anything else. A stale "php": "^7.x" constraint is the most likely hard blocker on Laravel 10/11 / PHP 8.2+, and it is invisible until composer refuses to install.short_links table is yours to design — unique index on code is non-negotiable.vendor/mremi/url-shortener/src and adding Log::debug() around the provider call. Factor that into on-call expectations.ShortenerInterface and the storage interface. Everything else — the Symfony config tree, the compiler passes, the service aliases — is bundle-specific and irrelevant to you. Design your adapter so those two interfaces are the only contact surface.app/ rather than depend on it. With zero dependents, upstream is unlikely to be responsive to your issues.How can I help you explore Laravel packages today?