cpsit / cps-shortnr
routing alias enhancer over middleware
Requires
- php: ^8.2
- brannow/typed-pattern-engine: ^0.4
- typo3/minimal: ^13.4
Requires (Dev)
- phpunit/phpunit: ^12.3
- typo3/testing-framework: ^9.2
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-03 19:29:26 UTC
README
Routing alias enhancer via middleware for TYPO3 CMS. Generates and resolves short URLs for pages and plugin records using configurable patterns.
Requirements
- PHP ^8.2
- TYPO3 ^13.4
Dependencies
- brannow/typed-pattern-engine ^0.4 - AST-based pattern matching engine
Installation
composer require cpsit/shortnr
The extension auto-registers its middleware and cache configuration.
Configuration
Default Configuration
The extension ships with a minimal default configuration in Configuration/config.yaml:
shortNr: _default: notFound: "/" languageParentField: "l10n_parent" languageField: "sys_language_uid" identifierField: "uid" pages: type: page table: pages pattern: "p{uid:int(min=1)}(-{sys_language_uid:int(min=0,default=0)})"
Pattern Syntax
Patterns use the typed-pattern-engine syntax:
| Syntax | Description |
|---|---|
{field:int(min=1)} |
Integer with minimum value |
{field:int(min=0,default=0)} |
Integer with default |
(-{field}) |
Optional segment (parentheses) |
PREFIX{field} |
Static prefix |
Example: NEWS{uid:int(min=1)}(-{sys_language_uid:int(min=0,default=0)}) generates NEWS123 or NEWS123-1
Adding Custom Configuration
Create an event listener to register your configuration file:
<?php namespace Vendor\SitePackage\EventListener\ShortNr; use CPSIT\ShortNr\Event\ShortNrConfigPathEvent; use TYPO3\CMS\Core\Attribute\AsEventListener; #[AsEventListener( identifier: 'site-package/shortNrConfigListener', event: ShortNrConfigPathEvent::class, method: 'loadConfig' )] class ConfigLoadingEventListener { public function loadConfig(ShortNrConfigPathEvent $event): void { $event->addConfigPath('EXT:site_package/Configuration/cps_shortnr.yaml'); } }
Config Merging Behavior
Configs are merged via array_replace_recursive - higher priority configs (default: 10) override lower ones (extension default: -1000).
- Every field can be overwritten by later configs
- Arrays merge recursively (add new keys, override existing)
- Set a field to
nullto remove it - the priority can be altered at the ShortNrConfigPathEvent::addConfigPath call
Configuration Examples
Page type:
shortNr: pages: type: page table: pages pattern: "PAGE{uid:int(min=1)}(-{sys_language_uid:int(min=0,default=0)})"
Plugin type (e.g., News):
shortNr: news: type: plugin table: tx_news_domain_model_news pattern: "NEWS{uid:int(min=1)}(-{sys_language_uid:int(min=0,default=0)})" plugin: extension: News plugin: Pi1 pid: 43 # Page UID containing the plugin action: detail controller: News objectName: news # Request parameter name
With conditions:
shortNr: event: type: plugin table: tx_news_domain_model_news pattern: "EVENT{uid:int(min=1)}(-{sys_language_uid:int(min=0,default=0)})" plugin: extension: News plugin: Pi1 pid: 474 action: detail controller: News objectName: news condition: is_event: 1
Available Condition Operators
See Configuration/config.example.full.yaml for detailed examples.
| Operator | Example | Description |
|---|---|---|
| implicit | field: value |
Equals |
eq |
field: {eq: value} |
Equals (explicit) |
in |
field: [a, b, c] |
Value in array |
contains |
field: {contains: "text"} |
Substring match |
starts |
field: {starts: "prefix"} |
Starts with |
ends |
field: {ends: "suffix"} |
Ends with |
gt, gte |
field: {gte: 50} |
Greater than (or equal) |
lt, lte |
field: {lt: 100} |
Less than (or equal) |
between |
field: {between: [10, 50]} |
Range |
isset |
field: {isset: true} |
Field exists |
not |
field: {not: {eq: value}} |
Negation |
Usage
ViewHelper
Generate short URLs in Fluid templates:
<html xmlns:sn="http://typo3.org/ns/CPSIT/ShortNr/ViewHelpers" data-namespace-typo3-fluid="true"> <!-- By config name and UID --> <sn:shortUrl name="news" uid="123" output="result" absolute="1"> <f:if condition="{result.uri}"> <a href="{result.uri}">{result.uri}</a> </f:if> </sn:shortUrl> <!-- From current page context (no name/uid = environment demand) --> <sn:shortUrl output="shortNr" absolute="1"> {shortNr.uri} </sn:shortUrl> <!-- From domain object --> <sn:shortUrl object="{newsItem}" output="result" absolute="1"> {result.uri} </sn:shortUrl>
ViewHelper Arguments:
| Argument | Type | Description |
|---|---|---|
output |
string | Variable name for result (required) |
languageUid |
int | Target language |
absolute |
bool | Generate absolute URL |
The ViewHelper supports three demand modes (mutually exclusive):
| Mode | Arguments | Description |
|---|---|---|
| Config + UID | name + uid |
Explicit config name and record UID |
| Object | object |
Domain object (extracts config + UID automatically) |
| Environment | (none) | Uses current page/plugin context |
Service (PHP)
Encoding (generate short URL):
use CPSIT\ShortNr\Service\EncoderService; use CPSIT\ShortNr\Service\Url\Demand\Encode\ConfigNameEncoderDemand; use CPSIT\ShortNr\Service\Url\Demand\Encode\ObjectEncoderDemand; use CPSIT\ShortNr\Service\Url\Demand\Encode\EnvironmentEncoderDemand; // Inject EncoderService via constructor public function __construct(private readonly EncoderService $encoderService) {} // 1. Config + UID: Explicit config name and record UID $demand = (new ConfigNameEncoderDemand('news', 123)) ->setRequest($request) // Required for site context ->setLanguageId(0) // Target language (null = from request) ->setAbsolute(true); // Full URL vs relative path $uri = $this->encoderService->encode($demand); // 2. Object: From domain object (AbstractEntity) $demand = (new ObjectEncoderDemand($newsEntity)) ->setRequest($request) ->setAbsolute(true); $uri = $this->encoderService->encode($demand); // 3. Environment: From current request context $demand = (new EnvironmentEncoderDemand( $request->getQueryParams(), $request->getAttribute('frontend.page.information')?->getPageRecord() ?? [], $request->getAttribute('routing'), $request->getAttribute('extbase') )) ->setRequest($request) ->setAbsolute(true); $uri = $this->encoderService->encode($demand);
Decoding (resolve short URL):
use CPSIT\ShortNr\Service\DecoderService; use CPSIT\ShortNr\Service\Url\Demand\Decode\DecoderDemand; // Inject DecoderService via constructor public function __construct(private readonly DecoderService $decoderService) {} // Direct: From short URL string $demand = new DecoderDemand('NEWS123'); $fullUri = $this->decoderService->decode($demand); // From request: Extracts short URL from request path $demand = $this->decoderService->getDecoderDemandFromRequest($request); $fullUri = $demand ? $this->decoderService->decode($demand) : null;
How It Works
- Middleware intercepts GET requests and checks for short URL patterns
- DecoderService matches request path against configured patterns
- On match: 301 redirect to resolved full URL
- On no match: request continues to TYPO3 routing
Encoding compiles record data into short URL using configured pattern. Decoding extracts UID from short URL, queries database with conditions, generates full URL.
Which record a short number serves
Decoding runs before backend authentication, so it answers for an anonymous visitor:
- Only visible records count: not deleted, not hidden, starttime reached, endtime not passed
(TCA
enablecolumns), live workspace only. Encoding ignores visibility, so editors can copy the short link of a record before publishing it. - The short number names a visible translation in the requested language: exactly that one. Otherwise, several visible translations in one language (left-over drafts): the lowest uid wins.
- The base record (the row without language parent, usually the default language) is not visible:
not found, also for its translations. Records that exist only in another language (free mode,
l10n_parent = 0) are their own base record. - Pages: sysfolders and spacers are never a target.
- The requested language has no visible translation:
languageFallbackdecides (fallback = the base record in its own language).
shortNr: _default: languageFallback: true # default: serve the base record in its own language press: languageFallback: false # this item only: missing translation = not found
Caching
- Decoded and encoded URLs are cached for at most one week in the TYPO3 cache
cps_shortnr. - A decode entry expires early at the next starttime/endtime of the record or one of its translations (embargoed records become reachable on time). A notFound answer lives at most one hour (safety net for writes no invalidation sees).
- The decode cache key is built from the parsed values:
p0200andp200share one entry. - Entries are tagged per table they read and dropped when such a record changes:
- DataHandler saves (backend, any command): automatic
- Extbase persistence (importers, forms): automatic, once per table at the end of the request
- plain SQL writers must call
GeneralUtility::makeInstance(ShortNrCacheInvalidator::class)->invalidateTable('my_table')Only tables used by a config item are flushed.
- Cache tags:
all,uri,encode,decode,tree,shortnr_table_<table>.cache:flushand "flush frontend caches" clear everything. - The default backend is
FileBackend: one cache per web node, and every tag flush reads every cache file. With several web nodes or many short numbers, configure a shared backend:$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['cps_shortnr']['backend'] = \TYPO3\CMS\Core\Cache\Backend\RedisBackend::class; // or Typo3DatabaseBackend
- Compiled config is cached in
var/cache/code/cps_shortnrand rebuilt when a YAML file changes. A broken YAML is logged once (error), short numbers pass through to TYPO3 until it is fixed; the failure is retried every 5 minutes.
NotFound Handling
Configure fallback behavior when a short URL cannot be resolved:
shortNr: _default: notFound: 2 # Page UID # or notFound: "/404" # URI string # or notFound: "" # Disable (continue to TYPO3 routing, TYPO3 answers 404)
A not found is logged at debug level only (bots probing number ranges are normal); query errors and broken processor configuration are logged as errors.
Extension Setup
The extension registers:
- PSR-15 middleware (early in stack, before site resolution)
- TYPO3 cache configuration (
cps_shortnr) - DataHandler hook for cache invalidation