reformtech / short-url
A framework-agnostic URL shortener core with an optional Laravel integration layer.
Requires
- php: ^8.1
Requires (Dev)
- illuminate/database: ^10.0 || ^11.0
- illuminate/routing: ^10.0 || ^11.0
- illuminate/support: ^10.0 || ^11.0
- orchestra/testbench: ^8.0 || ^9.0
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^10.0
Suggests
- illuminate/database: Required if you want to use the built-in Eloquent storage adapter.
- illuminate/support: Required if you want to use the Laravel integration layer (facade, service provider).
Provides
None
Conflicts
None
Replaces
None
README
A URL shortener for PHP — usable standalone in any PHP project, or with a first-class integration layer for Laravel: single-use links, activation windows, HTTPS enforcement, visit tracking, and events.
Unlike most short-URL packages, the core has zero framework dependencies and no required PHP extensions beyond what ships with PHP itself — the Laravel layer is an optional add-on, not a requirement.
Table of Contents
- Overview
- Installation
- Usage: Plain PHP
- Usage: Laravel
- Configuration Reference
- Testing
- Static Analysis
- Security
- Contributing
- Changelog
- License
Overview
This package solves one problem two ways:
| Plain PHP | Laravel | |
|---|---|---|
| Dependencies | None | illuminate/support, illuminate/database |
| Storage | ArrayStorage, PdoStorage, or your own |
Eloquent, auto-migrated |
| API | UrlShortener::shorten() / resolve() |
Fluent Builder, or the same simple API via a facade |
| Redirect handling | You wire it up | Auto-registered route + controller |
| Advanced features (single-use, tracking, windows) | — | ✅ |
If you're in Symfony, WordPress, a legacy codebase, or a CLI tool, use the core directly. If you're in Laravel, install the same package and get a zero-config, feature-rich experience out of the box.
Installation
Requirements
- PHP 8.1+
- For the Laravel layer: Laravel 10.0+ (
illuminate/support,illuminate/database)
composer require reformtech/short-url
Plain PHP Setup
Run the provided schema once against your database:
mysql -u root -p your_database < vendor/reformtech/short-url/database/schema.sql
(Or adapt it for Postgres/SQLite — it's a single, simple CREATE TABLE statement.)
Laravel Setup
The service provider and facade are auto-discovered — no manual registration needed.
Publish the config and run the migrations:
php artisan vendor:publish --tag=short-url-config php artisan migrate
This creates two tables: short_urls and short_url_visits.
Usage: Plain PHP
Quick Start
use Reformtech\ShortUrl\UrlShortener; use Reformtech\ShortUrl\Storage\PdoStorage; $pdo = new PDO('mysql:host=localhost;dbname=app', 'user', 'pass'); $shortener = new UrlShortener(new PdoStorage($pdo)); $key = $shortener->shorten('https://example.com/some/very/long/url'); // "aZ9kL" $original = $shortener->resolve($key); // "https://example.com/some/very/long/url" $shortener->forget($key);
Custom keys work the same way as in Laravel:
$shortener->shorten('https://example.com/sale', 'summer-sale');
For quick scripts or tests, skip the database entirely:
use Reformtech\ShortUrl\Storage\ArrayStorage; $shortener = new UrlShortener(new ArrayStorage());
Handling Redirects
Resolving a code is only half the job — the package also handles the
"visitor hits the short URL, gets redirected" flow, via RedirectHandler.
No manual $_GET reading, header() calls, or try/catch required:
// index.php use Reformtech\ShortUrl\Http\RedirectHandler; $shortener = /* however you built it above */; (new RedirectHandler($shortener))->handle();
Point your webserver (or php -S localhost:8000) at this script, then visit
index.php?s={code} — it resolves the code and issues a 302 redirect
automatically. Missing code → 400. Unknown code → 404.
By default it reads the code from the s query parameter. Customize it:
(new RedirectHandler($shortener, queryParam: 'code', redirectStatus: 301))->handle();
Custom Storage Backends
Implement Reformtech\ShortUrl\Contracts\StorageInterface (five methods)
to back the shortener with Redis, MongoDB, or any storage shape you like:
use Reformtech\ShortUrl\Contracts\StorageInterface; class RedisStorage implements StorageInterface { public function save(string $code, string $originalUrl, ?\DateTimeInterface $expiresAt = null): void { /* ... */ } public function find(string $code): ?string { /* ... */ } public function exists(string $code): bool { /* ... */ } public function delete(string $code): bool { /* ... */ } public function incrementClicks(string $code): void { /* ... */ } }
Usage: Laravel
Quick Start
use Reformtech\ShortUrl\Laravel\Facades\ShortUrl; $key = ShortUrl::shorten('https://example.com/some/long/url'); // "aZ9kL" $original = ShortUrl::resolve($key); ShortUrl::forget($key);
Visiting the generated short URL (default: /s/{key}) automatically
redirects to the destination — the route is registered for you.
The Fluent Builder
For anything beyond the basics — single-use links, tracking, scheduling — use the fluent builder via the same facade:
use Reformtech\ShortUrl\Laravel\Facades\ShortUrl; $shortUrl = ShortUrl::destinationUrl('https://example.com/launch') ->urlKey('product-launch') ->singleUse() ->trackVisits() ->make(); $shortUrl->short_url; // "https://yourapp.com/s/product-launch"
make() returns the underlying ShortUrl Eloquent model, so you get full
access to its attributes and relationships.
Custom Keys
ShortUrl::destinationUrl('https://example.com/sale') ->urlKey('summer-sale') ->make(); // https://yourapp.com/s/summer-sale
Omit ->urlKey() and a random key is generated, using the length set in
key_length in the config (default: 5).
Single Use Links
Once visited, a single-use link returns a 404 on every subsequent visit:
ShortUrl::destinationUrl('https://example.com/one-time-offer') ->singleUse() ->make();
Enforcing HTTPS
Forces the destination URL to https://, regardless of what was passed in:
ShortUrl::destinationUrl('http://example.com') ->secure() ->make(); // Destination stored as: https://example.com
Redirect Status Code
Defaults to 302 (recommended, so tracking/events reliably fire on every
visit — browsers tend to cache 301s and skip re-visiting the short URL).
ShortUrl::destinationUrl('https://example.com') ->redirectStatusCode(301) ->make();
Activation and Deactivation Windows
Useful for scheduled marketing campaigns — a link that only works during a set window:
ShortUrl::destinationUrl('https://example.com/black-friday') ->activateAt(now()->addDays(3)) ->deactivateAt(now()->addDays(4)) ->make();
Outside the window, visitors get a 404.
Custom Fields
Attach your own columns (e.g. to associate a link with a tenant or user) via
beforeCreate() — just remember to add the column via your own migration
first:
use Reformtech\ShortUrl\Laravel\Models\ShortUrl as ShortUrlModel; ShortUrl::destinationUrl('https://example.com') ->beforeCreate(function (ShortUrlModel $model) { $model->tenant_id = auth()->user()->tenant_id; }) ->make();
Conditionals
The builder supports Laravel's when()/unless() via the Conditionable trait:
ShortUrl::destinationUrl($url) ->when($request->boolean('single_use'), fn ($builder) => $builder->singleUse()) ->make();
Tracking Visitors
Tracking is on by default (configurable). Each visit records IP address,
user agent, a best-effort device type (desktop/mobile/tablet/robot),
and the referer URL — each field individually toggleable in config, or
per-link:
ShortUrl::destinationUrl('https://example.com') ->trackVisits() ->make();
Access recorded visits via the relationship:
$shortUrl = ShortUrlModel::findByKey('summer-sale'); $visits = $shortUrl->visits; // Collection of ShortUrlVisit
Events
Every visit dispatches Reformtech\ShortUrl\Laravel\Events\ShortUrlVisited,
carrying the ShortUrl model and the recorded ShortUrlVisit (or null if
tracking was disabled for that link):
use Reformtech\ShortUrl\Laravel\Events\ShortUrlVisited; class SendSlackNotification { public function handle(ShortUrlVisited $event): void { // $event->shortUrl, $event->visit } }
Note: with a
301redirect, browsers often cache the destination and skip re-visiting the short URL on subsequent clicks — so the event may not fire every time. Use302(the default) if you need reliable per-visit events.
Helper Methods
use Reformtech\ShortUrl\Laravel\Models\ShortUrl; // Find by key $shortUrl = ShortUrl::findByKey('summer-sale'); // Find every link that points to a given destination $shortUrls = ShortUrl::findByDestinationUrl('https://example.com/sale'); // Check state $shortUrl->isActive(); $shortUrl->hasBeenUsed(); $shortUrl->trackingEnabled(); $shortUrl->trackingFields(); // ['ip_address', 'user_agent', ...]
Model Factories
use Reformtech\ShortUrl\Laravel\Models\ShortUrl; $shortUrl = ShortUrl::factory()->create(); $deactivated = ShortUrl::factory()->deactivated()->create(); $notYetActive = ShortUrl::factory()->inactive()->create();
Configuration Reference
After publishing, config/short-url.php contains:
| Key | Default | Description |
|---|---|---|
default_url |
null |
Base domain for short URLs. null falls back to app.url. |
prefix |
s |
Route prefix (/{prefix}/{key}). Set null to remove it. |
disable_default_route |
false |
Set true if you're registering your own route to RedirectController. |
middleware |
[] |
Middleware applied to the package's default route. |
key_length |
5 |
Minimum length of auto-generated keys. |
redirect_status_code |
302 |
Default HTTP status for redirects (overridable per link). |
allowed_url_schemes |
['http://', 'https://'] |
Schemes accepted when shortening a URL. |
tracking.default_enabled |
true |
Whether new links track visits by default. |
tracking.fields.* |
all true |
Individually toggle which visit fields are recorded. |
Testing
composer install vendor/bin/phpunit
Static Analysis
vendor/bin/phpstan analyse
Security
If you discover a security vulnerability, please email
info@reformtech.in instead of opening a public issue.
Contributing
Pull requests are welcome. For significant changes, please open an issue first to discuss what you'd like to change.
Changelog
See CHANGELOG.md for a history of changes.
License
The MIT License (MIT). See LICENSE.md for details.