pharaonic/laravel-short-url
pharaonic/laravel-short-url is a Laravel package for generating and managing short URLs. It creates compact links with redirect handling and a simple, configurable API, making it easy to add URL shortening to any Laravel application.
1. Install and pin.
composer require pharaonic/laravel-short-url:2.0.2
Pin the exact version — this is a single-maintainer package with an April 2022 last release. No caret range.
2. Publish config and migrations, then migrate. The 2.0.2 patch exists specifically because the migrations publish path was wrong in 2.0.0/2.0.1, so this is the step to slow down on:
php artisan vendor:publish --provider="Pharaonic\Laravel\ShortURL\ShortURLServiceProvider"
php artisan migrate
After publishing, open database/migrations/ and confirm the file actually landed there with a sane timestamp. Then run php artisan vendor:publish list output (php artisan vendor:publish with no args) to see the exact tags available — config and migrations may be separate tags, and publishing one does not publish the other.
3. Where to look first. The package is tiny; read it before you write against it, in this order:
src/ShortURLServiceProvider.php — what publishes, what routes register, whether migrations auto-load via loadMigrationsFrom (which affects whether publishing is mandatory), and whether a facade alias is registered.config/short-url.php — the code length and URL/route prefix keys. The assessment brief calls these "configurable code length and URL prefix"; confirm the literal key names.ShortURL model — the entire API is a handful of methods. Confirm the exact names (generate / shorten / make, and a resolver for code → URL) rather than trusting any tutorial, including this one.4. First use case. Create one link and follow it:
use Pharaonic\Laravel\ShortURL\ShortURL;
$link = ShortURL::generate('https://example.com/some/long/path?a=1');
$link->code; // the slug
$link->url; // the full shareable short URL
Then hit ${APP_URL}/${prefix}/${code} in a browser and confirm the 302 lands on the original URL. That single round trip validates install, migration, routing, config, and redirect handling in about two minutes — do it before building anything on top.
Important framing: this package is self-hosted. Short links live in your database and redirect through your Laravel app. It is not a wrapper around Bitly or a third-party shortening API. That distinction drives most of what follows: you own link permanence, uptime, and abuse controls.
Wrap the facade immediately. The package is a commodity with an uncertain maintenance horizon. One hour of wrapping is what makes it replaceable later:
namespace App\Services;
interface ShortLinkService
{
public function shorten(string $url, ?Model $owner = null): string;
public function resolve(string $code): ?string;
}
final class PackageShortLinkService implements ShortLinkService
{
public function shorten(string $url, ?Model $owner = null): string
{
$link = ShortURL::generate($url);
// attribution lives in your table, not the package's
ShortLinkMeta::create([
'code' => $link->code,
'owner_id' => $owner?->getKey(),
'target' => $url,
]);
return $link->url;
}
}
Bind the interface in a service provider. Every call site then depends on your contract, not on Pharaonic\Laravel\ShortURL.
Do not fight the flat schema — extend alongside it. The package gives you a code and a destination. Everything product-shaped (owner, tenant, campaign, UTM set, expiry, deactivation, click count) belongs in your own table keyed by code. This keeps upgrades of the package a non-event and keeps your reporting queries out of vendor columns.
Redirect deliberately. If you keep the package's redirect route, that's fine for internal tools. If you need control — analytics, expiry, 410 for deactivated links, bot filtering — register your own route before the package's, or disable the package route and resolve the code yourself:
Route::get('/s/{code}', function (string $code) {
$link = Cache::remember("link:{$code}", 3600, fn () => ShortLinkMeta::find($code));
abort_if(! $link || $link->trashed(), 404);
abort_if($link->expires_at?->isPast(), 410);
event(new ShortLinkVisited($link));
return redirect()->away($link->target, 302);
})->middleware('throttle:120,1');
Caching is the whole performance story. A database-backed redirect on every hit is a hot path doing a primary-key lookup plus a PHP boot. Cache code → target with a long TTL, invalidate on write, and front the route with your normal HTTP cache. Without this, a moderately popular link is a self-inflicted load test.
Bulk generation belongs in a job. If you're minting links for a campaign or a CSV import, queue it, catch exceptions per item, and return a report. A synchronous loop over 5,000 URLs in a request will time out on the first slow insert.
Testing. Use RefreshDatabase, generate a link, assert the code is unique and the redirect resolves. Write the tests upstream won't: collision retry, trailing whitespace in input, deactivated link returns 404/410, and non-http schemes are rejected.
The 2.0.2 install bug has a second act. If anyone already published migrations on 2.0.0/2.0.1, they may exist at the wrong path and in the migrations table. Before re-publishing and re-running in any shared environment, query the migrations table: a file moved to the right directory with a row already recorded will be skipped, and a re-publish can create a duplicate filename that runs twice. Clean up deliberately.
Stale package, newer Laravel. Last release April 2022 means the composer.json constraint likely caps below your framework version. Test on your actual Laravel/PHP pairing in a throwaway app before proposing it. If the constraint is the only blocker, composer require ... --ignore-platform-reqs for the spike is acceptable — in composer.json, prefer widening with replace/extra tricks only if you're prepared to own the fork.
Collisions and reserved words. The package generates codes, but the guarantee is a unique index, not a retry loop. Wrap creation in a bounded retry on unique-constraint violation, and maintain a small blocklist of reserved paths that would shadow real routes (admin, api, login). Also decide the policy for user-supplied vanity slugs — that's your validation, not the package's.
Case sensitivity bites. Default MySQL collations are case-insensitive, so aB3 and Ab3 collide in the index while looking distinct to users. Either force lowercase on write or set a binary collation on the code column.
Open redirect is your problem. This package will happily shorten javascript: or a URL pointing at an internal host, and your domain will lend its reputation to a phishing link. Validate with filter_var($url, FILTER_VALIDATE_URL) and an in_array(parse_url($url, PHP_URL_SCHEME), ['http','https']) check, block private/reserved IPs, and consider an allowlist if the surface is public-facing.
Enumeration. If codes are derived from auto-increment IDs, they're trivially walkable. Use random generation with sufficient length for your volume, and rate-limit the redirect route.
301 vs 302. A 301 gets cached by browsers and intermediaries permanently — deactivating a link later won't take effect for users who already hit it. Default to 302 unless a link is genuinely immutable.
Config and route caching. After changing the prefix or code length, run php artisan config:clear and php artisan route:clear. On production, route:cache will freeze the package's route registration — verify redirects still work post-deploy, and remember changing the prefix requires a re-cache.
URL generation behind a proxy. Short URLs generated with ShortURL::generate() will use the request host. Behind a load balancer without TrustProxies configured you'll emit http:// and internal hostnames. Set URL::forceScheme('https') in a service provider.
Column width. If you're storing the target yourself, use TEXT, not VARCHAR(255). Long tracking URLs are routinely 300+ characters and will silently truncate on some MySQL configurations.
Click data does not exist. There is no analytics here — redirects are fire-and-forget and you get no server-side signal unless you add it. If click counts are on the roadmap, add the ShortLinkVisited event and a link_clicks table on day one; retrofitting analytics onto a link already in circulation gets you nothing for the past.
No provider to blame. Because this is self-hosted, every "the short link is broken" ticket is yours: bad input, dead target, expired row, cached 404, or the app being down. Budget for that in support expectations — there is no upstream status page to point at, and no external dependency to wait on.
How can I help you explore Laravel packages today?