gollumsf/url-tokenizer-bundle
A Symfony bundle for URL tokenization that integrates tokenizer functionality into your app, helping parse and split URLs into usable tokens for analysis, routing, or custom processing. Lightweight and easy to wire into existing Symfony projects.
This is a Symfony bundle, not a Laravel package. There is no UrlTokenizerServiceProvider, no facade, and no extra.laravel auto-discovery block. Plan on reading ~3 files and wiring it yourself.
1. Read the two things that matter first
README.md — the intended use case and the config keys.src/ — the tokenizer class itself plus the DependencyInjection/Configuration.php + *Extension.php pair. That pair tells you exactly which config keys exist (almost certainly a secret / key and possibly a cipher or base64 flag) and which service ID to grab. The bundle is small enough that the source is the documentation — expect to confirm the method names (encode/decode vs. tokenize/untokenize) yourself rather than trusting a blog post.composer.json — read the symfony/* constraint range, not just the version number. v3.1.4 adds Symfony 8.1 to the tested matrix, and the supported range determines which tag Composer will actually resolve for your Laravel version (see Gotchas).2. Install and bypass the bundle
composer require gollumsf/url-tokenizer-bundle
Symfony bundles need a kernel to register their DI extension. In Laravel you skip that: instantiate the underlying tokenizer class directly in a service provider and pass it the secret from your own config. You are using the class, not the bundle.
// app/Providers/UrlTokenizerServiceProvider.php
public function register(): void
{
$this->app->singleton(UrlTokenizer::class, fn () => new UrlTokenizer(
config('services.url_tokenizer.secret'),
// ...whatever constructor args Configuration.php exposes
));
}
3. First use case: a redirector/link-masking endpoint
The day-one value is turning an arbitrary URL into an opaque path segment so you can put it in a route, an email, or an <img src> without exposing (or letting users tamper with) the destination:
Route::get('/r/{token}', function (string $token) {
$url = app(UrlTokenizer::class)->decode($token);
abort_unless($url && str_starts_with($url, 'https://trusted.example/'), 404);
return redirect()->away($url);
})->where('token', '.*');
That's the whole package in one endpoint. If you only need Laravel-internal links, URL::signedRoute() already does this and you should stop here.
Config file, translated from Symfony YAML
The bundle's extension expects YAML like gollum_sf_url_tokenizer: { secret: '%env(APP_SECRET)%' }. Mirror it as a plain Laravel config so config:cache works normally:
// config/services.php
'url_tokenizer' => [
'secret' => env('URL_TOKENIZER_SECRET'), // NOT APP_SECRET — see Gotchas
],
Token generation at the edge of your app
Generate tokens where the URL is known, store nothing, and let the token be self-contained:
// Marketing mail builder
$trackingUrl = route('redirect', ['token' => $tokenizer->encode($campaign->landing_url)]);
Good fits: email click tracking, image/CDN proxying, webhook fan-out, "return to" parameters, signed download links to third-party storage, affiliate redirects. All of these are stateless — no table, no cleanup job, no cache invalidation, which is the real selling point over a "short links" table.
Wrap it behind your own interface
Never type-hint the vendor class in twenty controllers. One interface, one adapter, one place to swap in Crypt::encryptString() when the package goes stale:
interface UrlToken { public function make(string $url): string; public function parse(string $token): ?string; }
This also makes tests trivial: bind a fake that returns 'tok-'.$url and assert on the redirect target.
Pair it with a policy layer
A tokenizer obfuscates; it does not authorize. Every decode should pass through an allowlist before any outbound request or redirect()->away():
$host = parse_url($url, PHP_URL_HOST);
abort_unless(in_array($host, config('services.url_tokenizer.allowed_hosts'), true), 403);
Do this once in a small SafeRedirect action class and call it from every consumer — the redirector, the image proxy, the webhook dispatcher.
Route binding
Put the token in the path, not the query string, so it survives proxies and reads cleanly. Constrain it loosely (->where('token', '.*')) because base64 tokens can contain / and +. If you control the encoding, prefer base64url (-/_, no padding) and constrain to [A-Za-z0-9_-]+.
Testing
Unit-test the round trip once (encode → decode → same string) to prove the secret is actually wired, then never test it again. Test the policy instead. For feature tests, bind a deterministic fake tokenizer so assertions are readable:
$this->app->instance(UrlToken::class, new class implements UrlToken { /* identity mapping */ });
GPL-3.0 is the headline risk. For a hosted SaaS where users never receive the binary, GPL is usually tolerable; for anything you ship to customers (a Laravel app sold as a product, a Docker image handed over, an on-prem delivery) it is a serious copyleft question. Get a decision from whoever owns licensing before it's in 40 files. The tokenizer is a few dozen lines of base64/openssl — reimplementing it as a clean-room Crypt-based class may be cheaper than the legal review.
Low adoption, but not abandoned (updated). The repo is still tiny (1 star, no dependents), but v3.1.4 shows it is being kept alive against current Symfony releases rather than left to rot. Treat it as maintained-but-niche: pin the exact version in composer.json, keep the interface wrapper so a fork or replacement is a one-file change, and don't read a fresh release as a stability guarantee for your use case — the CI work in this release is about the Symfony matrix, not the tokenizer's behavior.
v3.1.4 is a test-and-CI-only release — safe to bump. The three changes are: Symfony 8.1 added to the CI matrix, SYMFONY_REQUIRE pinning fixed so the symfony_* jobs stop installing whatever components are latest, and a fix to a PHPUnit Request double (Request::$attributes is a hooked property in Symfony 8.1 and can no longer be assigned on a mock). None of that touches runtime tokenizer code. Upgrade freely; a smoke test of one encode/decode round trip is sufficient regression coverage.
Your installable tag depends on your Laravel's Symfony components. Symfony 8.1 implies a PHP 8.4 floor, and Laravel versions ship pinned to specific Symfony majors. If Composer silently resolves an older bundle tag than the one you read about, that's why — check composer show gollumsf/url-tokenizer-bundle and the constraint range rather than assuming you're on the newest. This release doesn't change the class API, so the older tag is fine; just be deliberate about it.
No auto-discovery means a confusing first error. Target [GollumSF\...\UrlTokenizer] is not instantiable or Class not found usually just means the container was never told how to build it. Register the singleton before you debug anything else.
config:cache silently breaks env(). If you read the secret inside the service provider via env() rather than config(), it works locally and returns null in production after caching. Symptom: tokens encode fine but decode to garbage. Read from config() only.
Don't reuse APP_SECRET. Rotating the app key to invalidate sessions/cookies would simultaneously invalidate every outstanding tracking link. Use a dedicated URL_TOKENIZER_SECRET, and remember that rotating that one breaks all previously issued tokens with no graceful path — the format carries no key ID.
No expiry, no nonce. The token is a pure function of the URL. That means it's replayable forever and, if the cipher is deterministic, guessable by comparison — two campaigns pointing at the same URL produce the same token, which leaks that they're the same target. If you need TTL or one-time semantics, prepend expires_at|url and encode the whole thing, then check the timestamp on decode.
Slashes in path segments. Base64 output containing / gets split by Apache/nginx or swallowed by Laravel's router. Always use the base64url variant, or urlencode the token explicitly. + is the other trap — it decodes to a space in query strings.
Trailing = padding breaks copy-paste in emails and chat clients. Strip it and re-pad on decode.
Decode can return false/null/throw on tampering depending on how the underlying library handles bad input — check the actual behavior in src/ and normalize it in your adapter. Every consumer needs the same failure mode, or you'll get 500s on malformed tokens from crawlers and security scanners.
An open redirect by another name. The moment this ships, scanners will hit /r/{token} looking for //evil.com. redirect()->away() with any decoded string is an instant finding. Allowlist hosts, reject protocol-relative URLs, and reject non-http(s) schemes before redirecting.
Symfony DI features don't come along. No tagged services, no compiler passes, no autowiring by type-hint across the bundle's namespace, no Symfony event dispatcher integration. Anything the bundle's extension did at container build time has to be done by hand in your provider. The new SYMFONY_REQUIRE pinning in this release is purely a CI concern — it changes nothing about how you wire the class.
Debugging tip: when a token misbehaves, log both sides — ['token' => $token, 'decoded' => $decoded, 'secret_len' => strlen($secret)]. Nine times out of ten the decoded value is false and the cause is a stale config cache or a secret that picked up a trailing newline from a .env file.
Extension points are minimal, which is fine at this size: subclass the tokenizer if the method is protected, otherwise decorate by composition. Your own interface is the real extension point — treat the vendor class as an implementation detail from day one.
How can I help you explore Laravel packages today?