Routing alias enhancer via middleware for TYPO3 CMS. Generates and resolves short URLs for pages and plugin records using configurable patterns.
- PHP ^8.2
- TYPO3 ^13.4
- brannow/typed-pattern-engine ^0.4 - AST-based pattern matching engine
composer require cpsit/shortnrThe extension auto-registers its middleware and cache 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)})"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
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');
}
}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
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 nameWith 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: 1See 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 |
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 |
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;- 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.
- Compiled patterns cached to filesystem
- Encoded/decoded URLs cached with 1 week TTL
- Cache cleared on
cache:allorcache:pagesflush - Cache tags:
all,uri,encode,decode
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)The extension registers:
- PSR-15 middleware (early in stack, before site resolution)
- TYPO3 cache configuration (
cps_shortnr) - DataHandler hook for cache invalidation