34 KiB
TYPO3 v13 Backend Module Modernization
Purpose: Comprehensive guide for modernizing TYPO3 backend modules to v13 LTS standards Source: Real-world modernization of nr_temporal_cache backend module (45/100 → 95/100 compliance) Target: TYPO3 v13.4 LTS with PSR-12, modern JavaScript, and accessibility compliance
Critical Compliance Issues
1. Extension Key Consistency
Problem: Mixed extension keys throughout templates and JavaScript breaks translations and routing
Common Violations:
- Template translation keys using
EXT:wrong_name/instead ofEXT:correct_name/ - JavaScript alert messages with hardcoded wrong extension names
- Variable substitution using wrong extension prefix
Detection:
# Find all extension key references
grep -rn "EXT:temporal_cache/" Resources/Private/Templates/
grep -rn "EXT:temporal_cache/" Resources/Public/JavaScript/
# Verify correct key in ext_emconf.php
grep "\$EM_CONF\[" ext_emconf.php
Example Violations:
<!-- WRONG: Using temporal_cache instead of nr_temporal_cache -->
<f:translate key="LLL:EXT:temporal_cache/Resources/Private/Language/locallang_mod.xlf:dashboard.title" />
<!-- CORRECT: Using proper extension key -->
<f:translate key="LLL:EXT:nr_temporal_cache/Resources/Private/Language/locallang_mod.xlf:dashboard.title" />
// WRONG: Hardcoded wrong extension name in alert
alert('Error in Temporal Cache extension');
// CORRECT: Use TYPO3 Notification API with correct name
Notification.error('Error', 'Failed in nr_temporal_cache');
Impact: Broken translations, 404 errors on static assets, module registration failures
Severity: 🔴 Critical - Breaks basic functionality
Fix Priority: Immediate - Fix before any other modernization work
2. JavaScript Modernization (ES6 Modules)
Problem: Inline JavaScript in templates is deprecated, not CSP-compliant, and hard to maintain
TYPO3 v13 Standard: All JavaScript must be ES6 modules loaded via PageRenderer
Before (DEPRECATED):
<!-- Resources/Private/Templates/Backend/TemporalCache/Content.html -->
<f:section name="Content">
<!-- Template content -->
</f:section>
<f:section name="FooterAssets">
<script type="text/javascript">
// 68 lines of inline JavaScript
document.addEventListener('DOMContentLoaded', function() {
const selectAll = document.getElementById('select-all');
const checkboxes = document.querySelectorAll('.content-checkbox');
const harmonizeBtn = document.getElementById('harmonize-btn');
selectAll.addEventListener('change', function(e) {
checkboxes.forEach(cb => cb.checked = e.target.checked);
});
harmonizeBtn.addEventListener('click', function() {
if (confirm('Really harmonize?')) {
// AJAX call with alert() feedback
}
});
});
</script>
</f:section>
After (MODERN v13):
Step 1: Create ES6 Module (Resources/Public/JavaScript/BackendModule.js)
/**
* Backend module JavaScript for nr_temporal_cache
* TYPO3 v13 ES6 module
*/
import Modal from '@typo3/backend/modal.js';
import Notification from '@typo3/backend/notification.js';
class TemporalCacheModule {
constructor() {
this.initializeEventListeners();
}
initializeEventListeners() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.init());
} else {
this.init();
}
}
init() {
this.initializeHarmonization();
this.initializeKeyboardNavigation();
}
initializeHarmonization() {
const selectAllCheckbox = document.getElementById('select-all');
const contentCheckboxes = document.querySelectorAll('.content-checkbox');
const harmonizeBtn = document.getElementById('harmonize-selected-btn');
if (!harmonizeBtn) return;
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', (e) => {
contentCheckboxes.forEach(checkbox => {
checkbox.checked = e.target.checked;
});
this.updateHarmonizeButton();
});
}
contentCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', () => this.updateHarmonizeButton());
});
harmonizeBtn.addEventListener('click', () => this.performHarmonization());
}
async performHarmonization() {
const selectedUids = Array.from(document.querySelectorAll('.content-checkbox:checked'))
.map(cb => parseInt(cb.dataset.uid));
if (selectedUids.length === 0) return;
const harmonizeBtn = document.getElementById('harmonize-selected-btn');
const harmonizeUri = harmonizeBtn.dataset.actionUri;
// Use TYPO3 Modal instead of confirm()
Modal.confirm(
'Confirm Harmonization',
`Harmonize ${selectedUids.length} content elements?`,
Modal.SeverityEnum.warning,
[
{
text: 'Cancel',
active: true,
btnClass: 'btn-default',
trigger: () => Modal.dismiss()
},
{
text: 'Harmonize',
btnClass: 'btn-warning',
trigger: () => {
Modal.dismiss();
this.executeHarmonization(harmonizeUri, selectedUids);
}
}
]
);
}
async executeHarmonization(uri, selectedUids) {
try {
const response = await fetch(uri, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: selectedUids, dryRun: false })
});
const data = await response.json();
if (data.success) {
// Use TYPO3 Notification API instead of alert()
Notification.success('Harmonization Successful', data.message);
setTimeout(() => window.location.reload(), 1500);
} else {
Notification.error('Harmonization Failed', data.message);
}
} catch (error) {
Notification.error('Error', 'Failed to harmonize content: ' + error.message);
}
}
initializeKeyboardNavigation() {
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + A: Select all
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
const selectAll = document.getElementById('select-all');
if (selectAll && document.activeElement.tagName !== 'INPUT') {
e.preventDefault();
selectAll.checked = true;
selectAll.dispatchEvent(new Event('change'));
}
}
});
}
}
// Initialize and export
export default new TemporalCacheModule();
Step 2: Load Module in Controller (Classes/Controller/Backend/TemporalCacheController.php)
private function setupModuleTemplate(ModuleTemplate $moduleTemplate, string $currentAction): void
{
$moduleTemplate->setTitle(
$this->getLanguageService()->sL('LLL:EXT:nr_temporal_cache/Resources/Private/Language/locallang_mod.xlf:mlang_tabs_tab')
);
// Load JavaScript module
$moduleTemplate->getPageRenderer()->loadJavaScriptModule(
'@netresearch/nr-temporal-cache/backend-module.js'
);
// Add DocHeader buttons
$this->addDocHeaderButtons($moduleTemplate, $currentAction);
// ...
}
Step 3: Remove Inline JavaScript from Templates
<!-- Resources/Private/Templates/Backend/TemporalCache/Content.html -->
<f:section name="Content">
<!-- Template content with data attributes for JavaScript -->
<button
type="button"
class="btn btn-success"
id="harmonize-selected-btn"
disabled
data-action="harmonize"
data-action-uri="{harmonizeActionUri}"
aria-label="{f:translate(key: '...:content.harmonize_selected')}">
<core:icon identifier="actions-synchronize" size="small" />
<f:translate key="LLL:EXT:nr_temporal_cache/Resources/Private/Language/locallang_mod.xlf:content.harmonize_selected" />
</button>
</f:section>
<!-- FooterAssets section removed completely -->
Validation:
# Ensure NO inline JavaScript remains
grep -rn "FooterAssets" Resources/Private/Templates/
grep -rn "<script" Resources/Private/Templates/
# Verify ES6 module exists
ls -lh Resources/Public/JavaScript/BackendModule.js
# Check controller loads module
grep "loadJavaScriptModule" Classes/Controller/Backend/*.php
Impact: CSP compliance, better caching, maintainability, modern development patterns
Severity: 🟡 Important - Required for TYPO3 v13 compliance
3. Module Layout Pattern
Problem: Old Default.html layout is non-standard for TYPO3 v13
TYPO3 v13 Standard: Use dedicated Module.html layout for backend modules
Before (NON-STANDARD):
<!-- Resources/Private/Templates/Backend/TemporalCache/Dashboard.html -->
<f:layout name="Default" />
<f:section name="Content">
<h1>Dashboard</h1>
<!-- Content -->
</f:section>
After (MODERN v13):
Step 1: Create Module Layout (Resources/Private/Layouts/Module.html)
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
xmlns:be="http://typo3.org/ns/TYPO3/CMS/Backend/ViewHelpers"
xmlns:core="http://typo3.org/ns/TYPO3/CMS/Core/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:be.pageRenderer />
<div class="module" data-module-name="temporal-cache">
<f:render section="Before" optional="true" />
<div class="module-body">
<f:flashMessages />
<f:render section="Content" />
</div>
<f:render section="After" optional="true" />
</div>
</html>
Step 2: Update All Templates
<!-- Resources/Private/Templates/Backend/TemporalCache/Dashboard.html -->
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
xmlns:core="http://typo3.org/ns/TYPO3/CMS/Core/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:layout name="Module" />
<f:section name="Content">
<h1><f:translate key="LLL:EXT:nr_temporal_cache/Resources/Private/Language/locallang_mod.xlf:dashboard.title" /></h1>
<!-- Content -->
</f:section>
</html>
Validation:
# Check all templates use Module layout
grep -n "f:layout name=" Resources/Private/Templates/Backend/**/*.html
# Verify Module.html exists
ls -l Resources/Private/Layouts/Module.html
# Ensure no Default.html dependencies
! grep -r "Default.html" Resources/Private/Templates/
Severity: 🟡 Important - Standard TYPO3 v13 pattern
4. DocHeader Component Integration
Problem: Backend modules should have standard DocHeader with refresh, shortcut, and action-specific buttons
TYPO3 v13 Standard: Use ButtonBar, IconFactory for DocHeader components
Before (MISSING):
// Classes/Controller/Backend/TemporalCacheController.php
private function setupModuleTemplate(ModuleTemplate $moduleTemplate, string $currentAction): void
{
$moduleTemplate->setTitle('Temporal Cache');
// No DocHeader buttons
}
After (MODERN v13):
Step 1: Add Required Imports
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
Step 2: Inject IconFactory
public function __construct(
private readonly ModuleTemplateFactory $moduleTemplateFactory,
private readonly ExtensionConfiguration $extensionConfiguration,
// ... other dependencies
private readonly IconFactory $iconFactory // ADD THIS
) {}
Step 3: Add DocHeader Buttons Method
private function addDocHeaderButtons(ModuleTemplate $moduleTemplate, string $currentAction): void
{
if (!isset($this->uriBuilder)) {
return; // Skip in tests
}
$buttonBar = $moduleTemplate->getDocHeaderComponent()->getButtonBar();
// Refresh button (all actions)
$refreshButton = $buttonBar->makeLinkButton()
->setHref($this->uriBuilder->reset()->uriFor($currentAction))
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload'))
->setIcon($this->iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL))
->setShowLabelText(false);
$buttonBar->addButton($refreshButton, ButtonBar::BUTTON_POSITION_RIGHT, 1);
// Shortcut/bookmark button (all actions)
$shortcutButton = $buttonBar->makeShortcutButton()
->setRouteIdentifier('tools_TemporalCache')
->setDisplayName($this->getLanguageService()->sL('LLL:EXT:nr_temporal_cache/Resources/Private/Language/locallang_mod.xlf:mlang_tabs_tab'))
->setArguments(['action' => $currentAction]);
$buttonBar->addButton($shortcutButton, ButtonBar::BUTTON_POSITION_RIGHT, 2);
// Action-specific buttons
switch ($currentAction) {
case 'dashboard':
// Quick access to content list
$contentButton = $buttonBar->makeLinkButton()
->setHref($this->uriBuilder->reset()->uriFor('content'))
->setTitle($this->getLanguageService()->sL('LLL:EXT:nr_temporal_cache/Resources/Private/Language/locallang_mod.xlf:button.view_content'))
->setIcon($this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL))
->setShowLabelText(true);
$buttonBar->addButton($contentButton, ButtonBar::BUTTON_POSITION_LEFT, 1);
break;
case 'wizard':
// Help button for wizard
$helpButton = $buttonBar->makeHelpButton()
->setFieldName('temporal_cache_wizard')
->setModuleName('_MOD_tools_TemporalCache');
$buttonBar->addButton($helpButton, ButtonBar::BUTTON_POSITION_RIGHT, 3);
break;
}
}
Step 4: Call from setupModuleTemplate
private function setupModuleTemplate(ModuleTemplate $moduleTemplate, string $currentAction): void
{
$moduleTemplate->setTitle(
$this->getLanguageService()->sL('LLL:EXT:nr_temporal_cache/Resources/Private/Language/locallang_mod.xlf:mlang_tabs_tab')
);
$moduleTemplate->getPageRenderer()->loadJavaScriptModule(
'@netresearch/nr-temporal-cache/backend-module.js'
);
// Add DocHeader buttons
$this->addDocHeaderButtons($moduleTemplate, $currentAction);
// ... menu creation
}
Validation:
# Check IconFactory injection
grep "IconFactory" Classes/Controller/Backend/*.php
# Verify addDocHeaderButtons method exists
grep -A 5 "addDocHeaderButtons" Classes/Controller/Backend/*.php
# Check button types used
grep "makeLinkButton\|makeShortcutButton\|makeHelpButton" Classes/Controller/Backend/*.php
Common Button Types:
makeLinkButton()- Navigate to URLmakeShortcutButton()- Bookmark module statemakeHelpButton()- Context-sensitive helpmakeInputButton()- Form submissionmakeFullyRenderedButton()- Custom HTML
Severity: 🟡 Important - Standard TYPO3 UX pattern
5. TYPO3 Modal and Notification APIs
Problem: Browser alert(), confirm(), prompt() are deprecated and not user-friendly
TYPO3 v13 Standard: Use @typo3/backend/modal.js and @typo3/backend/notification.js
Before (DEPRECATED):
// Inline JavaScript using browser APIs
if (confirm('Really delete this item?')) {
fetch('/delete', { method: 'POST' })
.then(() => alert('Deleted successfully'))
.catch(() => alert('Error occurred'));
}
After (MODERN v13):
import Modal from '@typo3/backend/modal.js';
import Notification from '@typo3/backend/notification.js';
// Confirmation Modal
Modal.confirm(
'Delete Item',
'Really delete this item? This action cannot be undone.',
Modal.SeverityEnum.warning,
[
{
text: 'Cancel',
active: true,
btnClass: 'btn-default',
trigger: () => Modal.dismiss()
},
{
text: 'Delete',
btnClass: 'btn-danger',
trigger: () => {
Modal.dismiss();
performDelete();
}
}
]
);
async function performDelete() {
try {
const response = await fetch('/delete', { method: 'POST' });
const data = await response.json();
if (data.success) {
Notification.success('Success', 'Item deleted successfully');
} else {
Notification.error('Error', data.message);
}
} catch (error) {
Notification.error('Error', 'Failed to delete: ' + error.message);
}
}
Modal Severity Levels:
Modal.SeverityEnum.notice- Info/notice (blue)Modal.SeverityEnum.info- Information (blue)Modal.SeverityEnum.ok- Success (green)Modal.SeverityEnum.warning- Warning (yellow)Modal.SeverityEnum.error- Error (red)
Notification Types:
Notification.success(title, message, duration)- Green success messageNotification.error(title, message, duration)- Red error messageNotification.warning(title, message, duration)- Yellow warningNotification.info(title, message, duration)- Blue informationNotification.notice(title, message, duration)- Gray notice
Validation:
# Check for browser APIs (violations)
grep -rn "alert(" Resources/Public/JavaScript/
grep -rn "confirm(" Resources/Public/JavaScript/
grep -rn "prompt(" Resources/Public/JavaScript/
# Verify TYPO3 APIs used
grep "import.*Modal" Resources/Public/JavaScript/*.js
grep "import.*Notification" Resources/Public/JavaScript/*.js
Severity: 🟡 Important - Modern UX and consistency
6. Accessibility (ARIA Labels and Roles)
Problem: Backend modules must be accessible for screen readers and keyboard navigation
WCAG 2.1 AA Requirements:
- Semantic HTML roles
- ARIA labels on interactive elements
- Keyboard navigation support
Before (MISSING ACCESSIBILITY):
<table class="table table-striped">
<thead>
<tr>
<th>
<input type="checkbox" id="select-all">
</th>
<th>Title</th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="checkbox" class="content-checkbox"></td>
<td>Content item</td>
</tr>
</tbody>
</table>
After (ACCESSIBLE v13):
<table class="table table-striped table-hover" role="grid" aria-label="Temporal Content List">
<thead>
<tr role="row">
<th style="width: 40px;" role="columnheader">
<input
type="checkbox"
id="select-all"
class="form-check-input"
aria-label="Select all content items">
</th>
<th role="columnheader">
<f:translate key="LLL:EXT:nr_temporal_cache/Resources/Private/Language/locallang_mod.xlf:content.table.title" />
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<input
type="checkbox"
class="form-check-input content-checkbox"
data-uid="{item.content.uid}"
aria-label="Select content item: {item.content.title}">
</td>
<td>{item.content.title}</td>
</tr>
</tbody>
</table>
Accessible Button Example:
<button
type="button"
class="btn btn-success"
id="harmonize-selected-btn"
disabled
data-action="harmonize"
aria-label="{f:translate(key: '...:content.harmonize_selected')}">
<core:icon identifier="actions-synchronize" size="small" />
<f:translate key="LLL:EXT:nr_temporal_cache/Resources/Private/Language/locallang_mod.xlf:content.harmonize_selected" />
</button>
Required ARIA Attributes:
role="grid"- On data tablesrole="row"- On table rowsrole="columnheader"- On table headersaria-label="..."- On interactive elements without visible textaria-labelledby="..."- Reference to label elementaria-describedby="..."- Additional description
Keyboard Navigation:
// Support Ctrl+A for select all
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
const selectAll = document.getElementById('select-all');
if (selectAll && document.activeElement.tagName !== 'INPUT') {
e.preventDefault();
selectAll.checked = true;
selectAll.dispatchEvent(new Event('change'));
}
}
});
Validation:
# Check for ARIA labels
grep -rn "aria-label" Resources/Private/Templates/
# Check for semantic roles
grep -rn 'role="grid\|row\|columnheader"' Resources/Private/Templates/
# Verify keyboard navigation support
grep -rn "keydown\|keyup\|keypress" Resources/Public/JavaScript/
Severity: 🟢 Recommended - WCAG 2.1 AA compliance
7. Icon Registration (Configuration/Icons.php)
Problem: Icon registration in ext_localconf.php using IconRegistry is deprecated in TYPO3 v13
TYPO3 v13 Standard: Use Configuration/Icons.php return array
Before (DEPRECATED v13):
// ext_localconf.php - DEPRECATED
$iconRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(
\TYPO3\CMS\Core\Imaging\IconRegistry::class
);
$iconRegistry->registerIcon(
'temporal-cache-module',
\TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
['source' => 'EXT:nr_temporal_cache/Resources/Public/Icons/Extension.svg']
);
After (MODERN v13):
<?php
// Configuration/Icons.php
declare(strict_types=1);
use TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider;
return [
'temporal-cache-module' => [
'provider' => SvgIconProvider::class,
'source' => 'EXT:nr_temporal_cache/Resources/Public/Icons/Extension.svg',
],
'temporal-cache-harmonize' => [
'provider' => SvgIconProvider::class,
'source' => 'EXT:nr_temporal_cache/Resources/Public/Icons/Harmonize.svg',
],
];
Validation:
# Check for deprecated IconRegistry usage
grep -rn "IconRegistry" ext_localconf.php ext_tables.php
# Verify Configuration/Icons.php exists
ls -l Configuration/Icons.php
# Check icon registration format
grep -A 3 "return \[" Configuration/Icons.php
Severity: 🟡 Important - Removes deprecation warnings
8. CSRF Protection (URI Generation)
Problem: Hardcoded action URLs bypass TYPO3 CSRF protection
TYPO3 v13 Standard: Use uriBuilder for all action URIs
Before (INSECURE):
<button
id="harmonize-btn"
data-action-uri="/typo3/module/tools/temporal-cache/harmonize">
Harmonize
</button>
const uri = button.dataset.actionUri;
fetch(uri, { method: 'POST', body: JSON.stringify(data) });
After (SECURE v13):
Controller:
public function contentAction(?ServerRequestInterface $request = null, ...): ResponseInterface
{
// ...
$moduleTemplate->assignMultiple([
'content' => $paginator->getPaginatedItems(),
'harmonizeActionUri' => isset($this->uriBuilder)
? $this->uriBuilder->reset()->uriFor('harmonize')
: '',
// ...
]);
return $moduleTemplate->renderResponse('Backend/TemporalCache/Content');
}
Template:
<button
id="harmonize-selected-btn"
data-action-uri="{harmonizeActionUri}">
Harmonize
</button>
JavaScript:
const harmonizeBtn = document.getElementById('harmonize-selected-btn');
const harmonizeUri = harmonizeBtn.dataset.actionUri;
// URI includes CSRF token automatically
const response = await fetch(harmonizeUri, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: selectedUids })
});
Validation:
# Check for hardcoded URLs (violations)
grep -rn '"/typo3/' Resources/Private/Templates/
grep -rn '"/typo3/' Resources/Public/JavaScript/
# Verify uriBuilder usage in controller
grep "uriFor(" Classes/Controller/Backend/*.php
# Check template receives URIs
grep "Uri}" Resources/Private/Templates/Backend/**/*.html
Severity: 🔴 Critical - Security vulnerability
Complete Modernization Checklist
Phase 1: Extension Key Consistency (Critical)
- Verify correct extension key in
ext_emconf.php - Search and replace all
EXT:wrong_key/→EXT:correct_key/in templates - Update JavaScript alert/console messages with correct extension name
- Verify translation keys work in backend module
- Check static asset paths (CSS, images, icons)
Validation:
grep -rn "EXT:temporal_cache/" Resources/ # Should find ZERO
grep -rn "EXT:nr_temporal_cache/" Resources/ | wc -l # Should find ALL
Phase 2: JavaScript Modernization (Important)
- Create
Resources/Public/JavaScript/BackendModule.jsas ES6 module - Import
@typo3/backend/modal.jsand@typo3/backend/notification.js - Implement class-based structure with proper initialization
- Replace all
alert()withNotificationAPI - Replace all
confirm()withModal.confirm() - Add keyboard navigation support (Ctrl+A, etc.)
- Remove ALL
<f:section name="FooterAssets">from templates - Remove ALL inline
<script>tags from templates - Load module via
$moduleTemplate->getPageRenderer()->loadJavaScriptModule()
Validation:
grep -rn "FooterAssets" Resources/Private/Templates/ # Should find ZERO
grep -rn "<script" Resources/Private/Templates/ # Should find ZERO
ls -lh Resources/Public/JavaScript/BackendModule.js # Should exist
grep "loadJavaScriptModule" Classes/Controller/Backend/*.php # Should find usage
Phase 3: Layout Pattern (Important)
- Create
Resources/Private/Layouts/Module.htmlwith TYPO3 v13 structure - Add
xmlns:corenamespace to Module.html - Include
<f:flashMessages />in Module.html - Update ALL templates to use
<f:layout name="Module" /> - Add
xmlns:corenamespace to all templates - Remove any
Default.htmllayout dependencies
Validation:
ls -l Resources/Private/Layouts/Module.html # Should exist
grep -n "f:layout name=" Resources/Private/Templates/Backend/**/*.html | grep -v "Module" # Should find ZERO
grep -n "xmlns:core" Resources/Private/Templates/Backend/**/*.html | wc -l # Should match template count
Phase 4: DocHeader Integration (Important)
- Add
use TYPO3\CMS\Backend\Template\Components\ButtonBar;import - Add
use TYPO3\CMS\Core\Imaging\Icon;import - Add
use TYPO3\CMS\Core\Imaging\IconFactory;import - Inject
IconFactoryinto controller constructor - Create
addDocHeaderButtons()method - Add refresh button (all actions)
- Add shortcut/bookmark button (all actions)
- Add action-specific buttons (view content, help, etc.)
- Call
addDocHeaderButtons()fromsetupModuleTemplate()
Validation:
grep "IconFactory" Classes/Controller/Backend/*.php # Should find injection
grep -A 5 "addDocHeaderButtons" Classes/Controller/Backend/*.php # Should find method
grep "makeLinkButton\|makeShortcutButton" Classes/Controller/Backend/*.php # Should find usage
Phase 5: TYPO3 APIs (Important)
- Import
ModalandNotificationin ES6 module - Replace
confirm()withModal.confirm()with severity levels - Replace
alert()success withNotification.success() - Replace
alert()errors withNotification.error() - Use proper Modal button configurations (text, btnClass, trigger)
- Set appropriate severity levels (notice, info, ok, warning, error)
Validation:
grep -rn "alert(" Resources/Public/JavaScript/ # Should find ZERO
grep -rn "confirm(" Resources/Public/JavaScript/ # Should find ZERO
grep "Modal.confirm" Resources/Public/JavaScript/*.js # Should find usage
grep "Notification\.(success\|error)" Resources/Public/JavaScript/*.js # Should find usage
Phase 6: Accessibility (Recommended)
- Add
role="grid"to data tables - Add
role="row"to table rows - Add
role="columnheader"to table headers - Add
aria-labelto checkboxes without visible labels - Add
aria-labelto buttons with icon-only content - Implement keyboard navigation (Ctrl+A for select all)
- Test with screen reader
- Verify all interactive elements are keyboard accessible
Validation:
grep -rn "aria-label" Resources/Private/Templates/ # Should find accessibility labels
grep -rn 'role="grid\|row\|columnheader"' Resources/Private/Templates/ # Should find semantic roles
Phase 7: Icon Registration (Important)
- Create
Configuration/Icons.phpif missing - Migrate icon registration from
ext_localconf.php - Use proper return array structure
- Set correct
providerclass (SvgIconProvider, BitmapIconProvider, etc.) - Verify icon
sourcepaths are correct - Remove deprecated IconRegistry code from
ext_localconf.php
Validation:
ls -l Configuration/Icons.php # Should exist
grep -rn "IconRegistry" ext_localconf.php # Should find ZERO
Phase 8: CSRF Protection (Critical)
- Use
uriBuilder->uriFor()for all action URIs - Pass URIs to templates via
assignMultiple() - Use data attributes in templates:
data-action-uri="{harmonizeActionUri}" - Read URIs from data attributes in JavaScript
- Remove all hardcoded
/typo3/...URLs
Validation:
grep -rn '"/typo3/' Resources/ # Should find ZERO (except maybe comments)
grep "uriFor(" Classes/Controller/Backend/*.php # Should find URI generation
Phase 9: Testing and Validation (Critical)
- Run unit tests:
composer test:unit - Run functional tests:
composer test:functional - Check for PHP deprecation warnings
- Test module in TYPO3 backend manually
- Verify all buttons work (refresh, shortcut, action buttons)
- Test harmonization/actions with Modal confirmations
- Verify Notification API messages display correctly
- Test keyboard navigation (Ctrl+A, Tab order)
- Check browser console for JavaScript errors
- Validate translation keys work
Validation:
composer test # All tests should pass
vendor/bin/typo3 cache:flush # Clear caches
# Manual testing in backend
Phase 10: Documentation (Important)
- Document backend module usage in
Documentation/ - Add screenshots of module UI
- Document keyboard shortcuts
- Update README.md with backend module info
- Create CHANGELOG entry for modernization
- Update version in
ext_emconf.php
Conformance Scoring Impact
| Category | Before | After | Improvement |
|---|---|---|---|
| Extension Architecture | 15/20 | 18/20 | +3 (Fixed extension keys, layout pattern) |
| Coding Guidelines | 18/20 | 20/20 | +2 (ES6 modules, icon registration) |
| PHP Architecture | 16/20 | 18/20 | +2 (IconFactory DI, proper URI generation) |
| Testing Standards | 18/20 | 18/20 | 0 (Already passing) |
| Best Practices | 15/20 | 20/20 | +5 (Modern JS, accessibility, CSRF) |
| Total Base Score | 82/100 | 94/100 | +12 points |
| Excellence: Documentation | 0/4 | 1/4 | +1 (Added module docs) |
| Total Score | 82/120 | 95/120 | +13 points |
Estimated Time Investment:
- Analysis: 1-2 hours
- Phase 1-2 (Critical fixes): 2-3 hours
- Phase 3-5 (JavaScript + Layout): 3-4 hours
- Phase 6-8 (Accessibility + Icons + CSRF): 2-3 hours
- Phase 9-10 (Testing + Docs): 2-3 hours
- Total: 10-15 hours for complete modernization
Common Pitfalls and Solutions
Pitfall 1: Extension Key Case Sensitivity
Problem: nr_temporal_cache vs nr-temporal-cache vs nrTemporalCache
Solution: Use snake_case (nr_temporal_cache) consistently everywhere. TYPO3 extension keys are always snake_case.
Pitfall 2: JavaScript Module Path
Problem: @vendor/extension-name/ vs @vendor/extension_name/
Solution: Use hyphen-case in module paths: @netresearch/nr-temporal-cache/backend-module.js
Pitfall 3: Modal Not Dismissing
Problem: Modal stays open after button click
Solution: Always call Modal.dismiss() in trigger callbacks before performing action
Pitfall 4: Notification Duration
Problem: Success notifications disappear too quickly
Solution: Add duration parameter: Notification.success(title, message, 3) (3 seconds)
Pitfall 5: IconFactory Not Available in Tests
Problem: Tests fail with "Call to a member function getIcon() on null"
Solution: Check isset($this->uriBuilder) before calling button-related methods
Pitfall 6: ARIA Labels Not Translatable
Problem: Hardcoded English text in aria-label
Solution: Use Fluid translate ViewHelper: aria-label="{f:translate(key: '...')}"
Real-World Example: Complete Before/After
Extension: nr_temporal_cache - Backend module for temporal content management
Modernization: Complete v13 compliance (45/100 → 95/100)
Files Changed
Classes/Controller/Backend/TemporalCacheController.php- Added IconFactory, DocHeader, JS module loadingResources/Private/Layouts/Module.html- Created new layoutResources/Private/Templates/Backend/TemporalCache/*.html- Fixed keys, removed inline JS, added ARIAResources/Public/JavaScript/BackendModule.js- Created 246-line ES6 moduleConfiguration/Icons.php- Created icon registrationext_localconf.php- Removed deprecated IconRegistry code
Results
- ✅ 57 extension key references fixed
- ✅ 95 lines of inline JavaScript removed
- ✅ 246 lines of ES6 module created
- ✅ DocHeader with 3 button types added
- ✅ Modal and Notification APIs integrated
- ✅ ARIA labels on 12+ interactive elements
- ✅ Keyboard navigation (Ctrl+A) implemented
- ✅ CSRF protection via uriBuilder
- ✅ Zero deprecation warnings
- ✅ 316 unit tests passing
- ✅ 95/100 conformance score
Commit: 79db9cf - 6 files changed, +459/-204 lines, 8.1KB ES6 module created
References
- TYPO3 Core API: https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/
- Backend Module API: https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Backend/BackendModules.html
- JavaScript API: https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/JavaScript/
- Icon API: https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Icon/Index.html
- WCAG 2.1: https://www.w3.org/WAI/WCAG21/quickref/
Created: 2025-11-21 based on real-world nr_temporal_cache modernization