Files
gh-netresearch-claude-code-…/skills/typo3-conformance/references/backend-module-v13.md
2025-11-30 08:43:13 +08:00

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 of EXT: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 URL
  • makeShortcutButton() - Bookmark module state
  • makeHelpButton() - Context-sensitive help
  • makeInputButton() - Form submission
  • makeFullyRenderedButton() - 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 message
  • Notification.error(title, message, duration) - Red error message
  • Notification.warning(title, message, duration) - Yellow warning
  • Notification.info(title, message, duration) - Blue information
  • Notification.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 tables
  • role="row" - On table rows
  • role="columnheader" - On table headers
  • aria-label="..." - On interactive elements without visible text
  • aria-labelledby="..." - Reference to label element
  • aria-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.js as ES6 module
  • Import @typo3/backend/modal.js and @typo3/backend/notification.js
  • Implement class-based structure with proper initialization
  • Replace all alert() with Notification API
  • Replace all confirm() with Modal.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.html with TYPO3 v13 structure
  • Add xmlns:core namespace to Module.html
  • Include <f:flashMessages /> in Module.html
  • Update ALL templates to use <f:layout name="Module" />
  • Add xmlns:core namespace to all templates
  • Remove any Default.html layout 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 IconFactory into 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() from setupModuleTemplate()

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 Modal and Notification in ES6 module
  • Replace confirm() with Modal.confirm() with severity levels
  • Replace alert() success with Notification.success()
  • Replace alert() errors with Notification.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
  • Add role="grid" to data tables
  • Add role="row" to table rows
  • Add role="columnheader" to table headers
  • Add aria-label to checkboxes without visible labels
  • Add aria-label to 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.php if missing
  • Migrate icon registration from ext_localconf.php
  • Use proper return array structure
  • Set correct provider class (SvgIconProvider, BitmapIconProvider, etc.)
  • Verify icon source paths 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

  1. Classes/Controller/Backend/TemporalCacheController.php - Added IconFactory, DocHeader, JS module loading
  2. Resources/Private/Layouts/Module.html - Created new layout
  3. Resources/Private/Templates/Backend/TemporalCache/*.html - Fixed keys, removed inline JS, added ARIA
  4. Resources/Public/JavaScript/BackendModule.js - Created 246-line ES6 module
  5. Configuration/Icons.php - Created icon registration
  6. ext_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

Created: 2025-11-21 based on real-world nr_temporal_cache modernization