nowo-tech / short-link-bundle
Short links for Symfony: per-tenant short codes behind a public stateless redirect, a target guard (OutboundUrlGuard + denylist), a privacy-preserving click log with daily rollups and retention, a PHP API for generated links and a UiKit + FormKit management UI.
Package info
github.com/nowo-tech/ShortLinkBundle
Type:symfony-bundle
pkg:composer/nowo-tech/short-link-bundle
Fund package maintenance!
Requires
- php: >=8.2 <8.6
- doctrine/dbal: ^3.8.2 || ^4.0
- doctrine/doctrine-bundle: ^2.8 || ^3.0
- doctrine/orm: ^3.7
- doctrine/persistence: ^3.4 || ^4.0
- nowo-tech/form-kit-bundle: ^2.22
- nowo-tech/outbound-url-guard-bundle: ^1.5
- nowo-tech/ui-kit-bundle: ^2.49
- psr/clock: ^1.0
- psr/log: ^2.0 || ^3.0
- symfony/clock: ^7.4 || ^8.0
- symfony/config: ^7.4 || ^8.0
- symfony/console: ^7.4 || ^8.0
- symfony/dependency-injection: ^7.4 || ^8.0
- symfony/event-dispatcher: ^7.4 || ^8.0
- symfony/form: ^7.4 || ^8.0
- symfony/framework-bundle: ^7.4 || ^8.0
- symfony/http-kernel: ^7.4 || ^8.0
- symfony/polyfill-intl-idn: ^1.31
- symfony/rate-limiter: ^7.4 || ^8.0
- symfony/routing: ^7.4 || ^8.0
- symfony/security-bundle: ^7.4 || ^8.0
- symfony/translation: ^7.4 || ^8.0
- symfony/twig-bundle: ^7.4 || ^8.0
- symfony/uid: ^7.4 || ^8.0
- symfony/validator: ^7.4 || ^8.0
- twig/extra-bundle: ^3.12
- twig/string-extra: ^3.12
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- igor-php/igor-php: ^0.10.1
- nowo-tech/phpstan-frankenphp: ^1.3
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^11.5
- rector/rector: ^2.0
- symfony/browser-kit: ^7.4 || ^8.0
- symfony/css-selector: ^7.4 || ^8.0
- symfony/dom-crawler: ^7.4 || ^8.0
- symfony/filesystem: ^7.4 || ^8.0
- symfony/lock: ^7.4 || ^8.0
- symfony/messenger: ^7.4 || ^8.0
- symfony/phpunit-bridge: ^7.4 || ^8.0
- symfony/process: ^7.4 || ^8.0
- symfony/yaml: ^7.4 || ^8.0
Suggests
- symfony/flex: For automatic recipe installation
- symfony/lock: Exact plan limits under concurrent creates (quota)
- symfony/messenger: Click write mode `messenger` (click_log.write)
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-12 22:18:41 UTC
README
⭐ Found this useful? Install it from Packagist and give it a star on GitHub: it helps us maintain and improve the project.
Short links for Symfony: per-tenant short codes behind a public stateless redirect (/s/{code}), a target guard
(OutboundUrlGuard + denylist), a privacy-preserving click log with daily rollups and retention, a PHP API for generated
links (e-mail sends) and a UiKit + FormKit management UI.
Standard Symfony by default, DDD / hexagonal opt-in: zero configuration in a standard project; public ports are interfaces and value objects with no Symfony, Doctrine or HTTP types, the bundle-owned tables are mapped in XML (with an attribute twin) and referenced by id, and the bundle never flushes your EntityManager.
🚧 Status: built slice by slice from its design contract (docs/CONTRACT.md, v1.0 for spec 230 Block A): S1 (domain + persistence) and S2 (redirect + click log) are in review; statistics and the UI follow (docs/ROADMAP.md). First tag: 0.1.0, after its security audit.
📋 Compatible with Symfony 7.4, 8.0 and 8.1 · PHP 8.2+ (Symfony 8.x requires PHP 8.4+)
This bundle is FrankenPHP worker mode friendly.
Features
Slices S1–S4 (contract v1.0): the PHP API, the redirect, statistics and the management UI; 1.0.0 after the audit.
- FrankenPHP worker mode, kernel kept alive and not reset: readonly services, no memo of codes, links, tenants or IPs; a worker test (one kernel, thousands of requests over two tenants, flat memory) (docs/FRANKENPHP-WORKER-AUDIT.md).
- Codes: random base62 (7 characters, case-sensitive, offensive-substring blocklist) or custom
[a-z0-9-]{3,32}, unique per instance, reserved words, deleted codes quarantined for 365 days. - Redirect
GET|HEAD /s/{code}: stateless (no session, no cookie), one indexed lookup, neutral identical 404 for every failure, per-IP miss / hit limiters,no-store+no-referrerheaders, p95 < 20 ms budget. - Target guard: http(s) only, no userinfo, no private / link-local / metadata targets (DNS check at save through OutboundUrlGuard, literal checks on every redirect), host denylist port, self-chain refusal, daily recheck command.
- Click log: truncated IP (/24, /48), browser family, referrer host, optional country, machine flag for scanners and link previews, Global Privacy Control honoured, raw rows kept 90 days, daily rollups kept.
- Multi-tenant: every call takes a tenant key; host ports for tenant context, view / manage access, plan quota
(exact with
symfony/lock), denylist and country. - Generated links:
createMany()for e-mail sends (kindemail, send ref + label + position, deduplicated), clicks per send throughbySource(), GDPR export / erasure per send, tenant and actor. - UI on UiKit + FormKit: list, create / edit, disable / enable / delete, stats with a chart and breakdowns, CSV.
Installation
composer require nowo-tech/short-link-bundle
See docs/INSTALLATION.md.
Configuration
# config/packages/nowo_short_link.yaml nowo_short_link: public_base_url: '%env(DEFAULT_URI)%' ui: enabled: false
Every key of the contract is already accepted (strict tree): docs/CONFIGURATION.md.
Requirements
- PHP >= 8.2, < 8.6 (Symfony 8.x needs PHP 8.4+)
- Symfony 7.4, 8.0 or 8.1
- Doctrine DBAL 3.8+ / 4 and ORM 3.7+ with DoctrineBundle
nowo-tech/ui-kit-bundle^2.49,nowo-tech/form-kit-bundle^2.22,nowo-tech/outbound-url-guard-bundle^1.5
Demo
demo/symfony7 (Symfony 7.4, http://localhost:8230) and demo/symfony8 (Symfony 8.1, http://localhost:8231):
FrankenPHP worker mode, MySQL, UiKit layout and FormKit forms, Web Profiler, Twig Inspector and Hot Reload
(make -C demo up-symfony8), management UI under /short-links. Playwright e2e: make test-e2e; screenshots:
make demo-screenshots. See docs/DEMO-FRANKENPHP.md.
Documentation
- Installation
- Configuration
- Usage
- Contributing
- Code of Conduct
- Changelog
- Upgrading
- Release
- Security
- Engram
- Spec-driven development
- GitHub Spec Kit
- Roadmap
Additional documentation
- Contract v1.0 (spec 230 Block A)
- Architecture
- FrankenPHP worker audit
- Performance
- Privacy
- Demo with FrankenPHP
- PSR compliance
- GitHub CI
Tests and coverage
make test # PHPUnit in Docker make test-coverage-100 # fails below 100 % of src/ make test-with-db # Database suite on MySQL 8.4 make release-check # every gate, demos included
- PHP: 100 % of
src/(gate in CI). Database suite on SQLite, MySQL 8.4, MariaDB 10.11 and PostgreSQL 17 in CI.
License
MIT. See LICENSE.
Author
Created by Héctor Franco Aceituno at Nowo.tech.


