Files
gh-netresearch-claude-code-…/references/best-practices.md
2025-11-30 08:43:22 +08:00

24 KiB

TYPO3 Extension Best Practices

Source: TYPO3 Best Practices (Tea Extension) and Core API Standards Purpose: Real-world patterns and organizational best practices for TYPO3 extensions

Project Structure

Complete Extension Layout

my_extension/
├── .ddev/                          # DDEV configuration
│   └── config.yaml
├── .github/                        # GitHub Actions CI/CD
│   └── workflows/
│       └── tests.yml
├── Build/                          # Build tools and configs
│   ├── phpunit/
│   │   ├── UnitTests.xml
│   │   └── FunctionalTests.xml
│   └── Scripts/
│       └── runTests.sh
├── Classes/                        # PHP source code
│   ├── Controller/
│   ├── Domain/
│   │   ├── Model/
│   │   └── Repository/
│   ├── Service/
│   ├── Utility/
│   ├── EventListener/
│   └── ViewHelper/
├── Configuration/                  # TYPO3 configuration
│   ├── Backend/
│   │   └── Modules.php
│   ├── Services.yaml
│   ├── TCA/
│   ├── TypoScript/
│   │   ├── setup.typoscript
│   │   └── constants.typoscript
│   └── Sets/                       # TYPO3 v13+
│       └── MySet/
│           └── config.yaml
├── Documentation/                  # RST documentation
│   ├── Index.rst
│   ├── Settings.cfg
│   ├── Introduction/
│   ├── Installation/
│   ├── Configuration/
│   ├── Developer/
│   └── Editor/
├── Resources/
│   ├── Private/
│   │   ├── Language/
│   │   │   ├── locallang.xlf
│   │   │   └── de.locallang.xlf
│   │   ├── Layouts/
│   │   ├── Partials/
│   │   └── Templates/
│   └── Public/
│       ├── Css/
│       ├── Icons/
│       ├── Images/
│       └── JavaScript/
├── Tests/
│   ├── Unit/
│   ├── Functional/
│   │   └── Fixtures/
│   └── Acceptance/
│       ├── Support/
│       └── codeception.yml
├── .editorconfig                   # Editor configuration
├── .gitattributes                  # Git attributes
├── .gitignore                      # Git ignore rules
├── .php-cs-fixer.dist.php          # PHP CS Fixer config
├── composer.json                   # Composer configuration
├── composer.lock                   # Locked dependencies
├── ext_emconf.php                  # Extension metadata
├── ext_localconf.php               # Global configuration
├── LICENSE                         # License file
├── phpstan.neon                    # PHPStan configuration
└── README.md                       # Project README

Best Practices by Category

1. Dependency Management

composer.json Best Practices:

{
    "name": "vendor/my-extension",
    "type": "typo3-cms-extension",
    "description": "Clear, concise extension description",
    "license": "GPL-2.0-or-later",
    "authors": [
        {
            "name": "Author Name",
            "email": "author@example.com",
            "role": "Developer"
        }
    ],
    "require": {
        "php": "^8.1",
        "typo3/cms-core": "^12.4 || ^13.0",
        "typo3/cms-backend": "^12.4 || ^13.0",
        "typo3/cms-extbase": "^12.4 || ^13.0",
        "typo3/cms-fluid": "^12.4 || ^13.0"
    },
    "require-dev": {
        "typo3/coding-standards": "^0.7",
        "typo3/testing-framework": "^8.0",
        "phpunit/phpunit": "^10.5",
        "phpstan/phpstan": "^1.10",
        "friendsofphp/php-cs-fixer": "^3.0"
    },
    "autoload": {
        "psr-4": {
            "Vendor\\MyExtension\\": "Classes/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Vendor\\MyExtension\\Tests\\": "Tests/"
        }
    },
    "config": {
        "vendor-dir": ".Build/vendor",
        "bin-dir": ".Build/bin",
        "sort-packages": true,
        "allow-plugins": {
            "typo3/class-alias-loader": true,
            "typo3/cms-composer-installers": true
        }
    },
    "extra": {
        "typo3/cms": {
            "extension-key": "my_extension",
            "web-dir": ".Build/Web"
        }
    }
}

2. Code Quality Tools

.php-cs-fixer.dist.php:

<?php
declare(strict_types=1);

$config = \TYPO3\CodingStandards\CsFixerConfig::create();
$config->getFinder()
    ->in(__DIR__ . '/Classes')
    ->in(__DIR__ . '/Configuration')
    ->in(__DIR__ . '/Tests');

return $config;

phpstan.neon:

includes:
    - .Build/vendor/phpstan/phpstan/conf/bleedingEdge.neon

parameters:
    level: 9
    paths:
        - Classes
        - Configuration
        - Tests
    excludePaths:
        - .Build
        - vendor

PHPStan Level 10 Best Practices for TYPO3

Handling $GLOBALS['TCA'] in Tests:

PHPStan cannot infer types for runtime-configured $GLOBALS arrays. Use ignore annotations:

// ✅ Right: Suppress offsetAccess warnings for $GLOBALS['TCA']
/** @var array<string, mixed> $tcaConfig */
$tcaConfig = [
    'type'           => 'text',
    'enableRichtext' => true,
];
// @phpstan-ignore-next-line offsetAccess.nonOffsetAccessible
$GLOBALS['TCA']['tt_content']['columns']['bodytext']['config'] = $tcaConfig;

// ❌ Wrong: No type annotation or suppression
$GLOBALS['TCA']['tt_content']['columns']['bodytext']['config'] = [
    'type' => 'text',
]; // PHPStan error: offsetAccess.nonOffsetAccessible

Factory Methods vs Property Initialization:

Avoid uninitialized property errors in test classes:

// ❌ Wrong: PHPStan warns about uninitialized property
final class MyServiceTest extends UnitTestCase
{
    private MyService $subject; // Uninitialized property

    protected function setUp(): void
    {
        parent::setUp();
        $this->subject = new MyService();
    }
}

// ✅ Right: Use factory method
final class MyServiceTest extends UnitTestCase
{
    private function createSubject(): MyService
    {
        return new MyService();
    }

    #[Test]
    public function testSomething(): void
    {
        $subject = $this->createSubject();
        // Use $subject
    }
}

Type Assertions for Dynamic Arrays:

When testing arrays modified by reference:

// ❌ Wrong: PHPStan cannot verify type after modification
public function testFieldProcessing(): void
{
    $fieldArray = ['bodytext' => '<p>Test</p>'];
    $this->subject->processFields($fieldArray);

    // PHPStan error: Cannot access offset on mixed
    self::assertStringContainsString('Test', $fieldArray['bodytext']);
}

// ✅ Right: Add type assertions
public function testFieldProcessing(): void
{
    $fieldArray = ['bodytext' => '<p>Test</p>'];
    $this->subject->processFields($fieldArray);

    self::assertArrayHasKey('bodytext', $fieldArray);
    self::assertIsString($fieldArray['bodytext']);
    self::assertStringContainsString('Test', $fieldArray['bodytext']);
}

Intersection Types for Mocks:

Use intersection types for proper PHPStan analysis of mocks:

// ✅ Right: Intersection type for mock
/** @var ResourceFactory&MockObject $resourceFactoryMock */
$resourceFactoryMock = $this->createMock(ResourceFactory::class);

// Alternative: @phpstan-var annotation
$resourceFactoryMock = $this->createMock(ResourceFactory::class);
/** @phpstan-var ResourceFactory&MockObject $resourceFactoryMock */

Common PHPStan Suppressions for TYPO3:

// Suppress $GLOBALS['TCA'] access
// @phpstan-ignore-next-line offsetAccess.nonOffsetAccessible
$GLOBALS['TCA']['table']['columns']['field'] = $config;

// Suppress $GLOBALS['TYPO3_CONF_VARS'] access
// @phpstan-ignore-next-line offsetAccess.nonOffsetAccessible
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['key'] = MyClass::class;

// Suppress mixed type from legacy code
// @phpstan-ignore-next-line argument.type
$this->view->assign('data', $legacyArray);

Type Hints for Service Container Retrieval:

// ✅ Right: Type hint service retrieval
/** @var DataHandler $dataHandler */
$dataHandler = $this->get(DataHandler::class);

/** @var ResourceFactory $resourceFactory */
$resourceFactory = $this->get(ResourceFactory::class);

3. Service Configuration

Configuration/Services.yaml:

services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false

  # Auto-register all classes
  Vendor\MyExtension\:
    resource: '../Classes/*'

  # Exclude specific directories
  Vendor\MyExtension\Domain\Model\:
    resource: '../Classes/Domain/Model/*'
    autoconfigure: false

  # Explicit service configuration example
  Vendor\MyExtension\Service\EmailService:
    arguments:
      $fromEmail: '%env(DEFAULT_FROM_EMAIL)%'
      $fromName: 'TYPO3 Extension'

  # Tag configuration example
  Vendor\MyExtension\Command\ImportCommand:
    tags:
      - name: 'console.command'
        command: 'myext:import'
        description: 'Import data from external source'

4. Backend Module Configuration

Configuration/Backend/Modules.php:

<?php
return [
    'web_myext' => [
        'parent' => 'web',
        'position' => ['after' => 'web_info'],
        'access' => 'user',
        'workspaces' => 'live',
        'path' => '/module/web/myext',
        'labels' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang_mod.xlf',
        'extensionName' => 'MyExtension',
        'controllerActions' => [
            \Vendor\MyExtension\Controller\BackendController::class => [
                'list',
                'show',
                'edit',
                'update',
            ],
        ],
    ],
];

5. Testing Infrastructure

Build/Scripts/runTests.sh:

#!/usr/bin/env bash

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"

# Run unit tests
if [ "$1" = "unit" ]; then
    php vendor/bin/phpunit -c Build/phpunit/UnitTests.xml
fi

# Run functional tests
if [ "$1" = "functional" ]; then
    typo3DatabaseDriver=pdo_sqlite \
    php vendor/bin/phpunit -c Build/phpunit/FunctionalTests.xml
fi

# Run all tests
if [ "$1" = "all" ]; then
    php vendor/bin/phpunit -c Build/phpunit/UnitTests.xml
    typo3DatabaseDriver=pdo_sqlite \
    php vendor/bin/phpunit -c Build/phpunit/FunctionalTests.xml
fi

6. CI/CD Configuration

.github/workflows/tests.yml:

name: Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  tests:
    name: Tests (PHP ${{ matrix.php }}, TYPO3 ${{ matrix.typo3 }})
    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        php: ['8.1', '8.2', '8.3']
        typo3: ['12.4', '13.0']

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: mbstring, xml, json, zip, curl
          coverage: none

      - name: Get Composer Cache Directory
        id: composer-cache
        run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

      - name: Cache Composer dependencies
        uses: actions/cache@v3
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: ${{ runner.os }}-composer-

      - name: Install dependencies
        run: composer install --prefer-dist --no-progress

      - name: Lint PHP
        run: find . -name \*.php ! -path "./vendor/*" ! -path "./.Build/*" -exec php -l {} \;

      - name: PHP CS Fixer
        run: .Build/bin/php-cs-fixer fix --dry-run --diff

      - name: PHPStan
        run: .Build/bin/phpstan analyze

      - name: Unit Tests
        run: .Build/bin/phpunit -c Build/phpunit/UnitTests.xml

      - name: Functional Tests
        run: |
          typo3DatabaseDriver=pdo_sqlite \
          .Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml

7. Documentation Standards

Documentation/Index.rst:

.. include:: /Includes.rst.txt

==============
My Extension
==============

:Extension key:
   my_extension

:Package name:
   vendor/my-extension

:Version:
   |release|

:Language:
   en

:Author:
   Author Name

:License:
   This document is published under the
   `Creative Commons BY 4.0 <https://creativecommons.org/licenses/by/4.0/>`__
   license.

:Rendered:
   |today|

----

Clear and concise extension description explaining the purpose and main features.

----

**Table of Contents:**

.. toctree::
   :maxdepth: 2
   :titlesonly:

   Introduction/Index
   Installation/Index
   Configuration/Index
   Editor/Index
   Developer/Index
   Sitemap

Page Size Guidelines:

Follow TYPO3 documentation best practices for page organization and sizing:

Index.rst (Landing Page):

  • Target: 80-150 lines
  • Maximum: 200 lines
  • Purpose: Entry point with metadata, brief description, and navigation only
  • Contains: Extension metadata, brief description, card-grid (optional), toctree, license
  • Anti-pattern: Embedding all content (introduction, requirements, contributing, credits, etc.)

Content Pages:

  • Target: 100-300 lines per file
  • Optimal: 150-200 lines
  • Maximum: 400 lines (split if larger)
  • Structure: Focused on single topic or logically related concepts
  • Split Strategy: Create subdirectories for complex topics with multiple aspects

Red Flags:

  • Index.rst >200 lines → Extract content to Introduction/, Contributing/, etc.
  • Single file >400 lines → Split into multiple focused pages
  • All content in Index.rst → Create proper section directories
  • Navigation by scrolling → Use card-grid + toctree structure

Proper Structure Example:

Documentation/
├── Index.rst              # Landing page (80-150 lines)
├── Introduction/          # Getting started
│   └── Index.rst         # Features, requirements, quick start
├── Installation/          # Setup instructions
│   └── Index.rst
├── Configuration/         # Configuration guides
│   ├── Index.rst
│   ├── Basic.rst
│   └── Advanced.rst
├── Contributing/          # Contribution guidelines
│   └── Index.rst         # Code, translations, credits, resources
├── Examples/              # Usage examples
├── Troubleshooting/       # Problem solving
└── API/                   # Developer reference

Benefits:

  • Better user experience (focused, scannable pages)
  • Easier maintenance (smaller, manageable files)
  • Improved search results (specific pages rank better)
  • Clear information architecture
  • Follows TYPO3 documentation standards
  • Mobile-friendly navigation

Reference: TYPO3 tea extension - exemplary documentation structure

8. Version Control Best Practices

Default Branch Naming

Use main as the default branch instead of master

Rationale:

  • Industry Standard: GitHub, GitLab, and Bitbucket all default to main for new repositories
  • Modern Convention: Aligns with current version control ecosystem standards
  • Inclusive Language: Part of broader industry shift toward inclusive terminology
  • Consistency: Matches TYPO3 Core and most modern TYPO3 extensions

Migration from master to main:

If your extension currently uses master, migrate to main:

# 1. Create main branch from master
git checkout master
git pull origin master
git checkout -b main
git push -u origin main

# 2. Change default branch on GitHub
gh repo edit --default-branch main

# 3. Update all branch references in codebase
# - CI/CD workflows (.github/workflows/*.yml)
# - Documentation (guides.xml, *.rst files)
# - URLs in CONTRIBUTING.md, README.md

# 4. Delete old master branch
git branch -d master
git push origin --delete master

Example CI/CD workflow update:

# .github/workflows/tests.yml
on:
  push:
    branches: [main, develop]  # Changed from: master
  pull_request:
    branches: [main]           # Changed from: master

Example documentation update:

<!-- Documentation/guides.xml -->
<extension edit-on-github-branch="main" />  <!-- Changed from: master -->

Branch Protection Enforcement

Prevent accidental master branch recreation and protect main branch using GitHub Repository Rulesets.

Block master branch - prevents creation and pushes:

Create ruleset-block-master.json:

{
  "name": "Block master branch",
  "target": "branch",
  "enforcement": "active",
  "conditions": {
    "ref_name": {
      "include": ["refs/heads/master"],
      "exclude": []
    }
  },
  "rules": [
    {
      "type": "creation"
    },
    {
      "type": "update"
    },
    {
      "type": "deletion"
    }
  ],
  "bypass_actors": []
}

Apply the ruleset:

gh api -X POST repos/OWNER/REPO/rulesets \
  --input ruleset-block-master.json

Protect main branch - requires CI and prevents force pushes:

Create ruleset-protect-main.json:

{
  "name": "Protect main branch",
  "target": "branch",
  "enforcement": "active",
  "conditions": {
    "ref_name": {
      "include": ["refs/heads/main"],
      "exclude": []
    }
  },
  "rules": [
    {
      "type": "required_status_checks",
      "parameters": {
        "required_status_checks": [
          {
            "context": "build"
          }
        ],
        "strict_required_status_checks_policy": false
      }
    },
    {
      "type": "non_fast_forward"
    }
  ],
  "bypass_actors": [
    {
      "actor_id": 5,
      "actor_type": "RepositoryRole",
      "bypass_mode": "always"
    }
  ]
}

Apply the ruleset:

gh api -X POST repos/OWNER/REPO/rulesets \
  --input ruleset-protect-main.json

Verify rulesets are active:

# List all rulesets
gh api repos/OWNER/REPO/rulesets

# Test master branch is blocked (should fail)
git push origin test-branch:master
# Expected: remote: error: GH013: Repository rule violations found

Benefits of Repository Rulesets:

  • Prevents accidental master branch recreation
  • Enforces CI status checks before merging to main
  • Prevents force pushes to protected branches
  • Allows admin bypass for emergency situations
  • More flexible than legacy branch protection rules
  • Supports complex conditions and multiple rule types

9. Language File Organization

Resources/Private/Language/locallang.xlf:

<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file source-language="en" datatype="plaintext"
          original="EXT:my_extension/Resources/Private/Language/locallang.xlf"
          date="2024-01-01T12:00:00Z"
          product-name="my_extension">
        <header/>
        <body>
            <trans-unit id="plugin.title" resname="plugin.title">
                <source>My Extension Plugin</source>
            </trans-unit>
            <trans-unit id="plugin.description" resname="plugin.description">
                <source>Displays product list with filters</source>
            </trans-unit>
        </body>
    </file>
</xliff>

10. TCA Best Practices

Configuration/TCA/tx_myext_domain_model_product.php:

<?php
return [
    'ctrl' => [
        'title' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang_db.xlf:tx_myext_domain_model_product',
        'label' => 'title',
        'tstamp' => 'tstamp',
        'crdate' => 'crdate',
        'delete' => 'deleted',
        'sortby' => 'sorting',
        'versioningWS' => true,
        'origUid' => 't3_origuid',
        'languageField' => 'sys_language_uid',
        'transOrigPointerField' => 'l10n_parent',
        'transOrigDiffSourceField' => 'l10n_diffsource',
        'translationSource' => 'l10n_source',
        'enablecolumns' => [
            'disabled' => 'hidden',
            'starttime' => 'starttime',
            'endtime' => 'endtime',
        ],
        'searchFields' => 'title,description',
        'iconfile' => 'EXT:my_extension/Resources/Public/Icons/product.svg',
    ],
    'types' => [
        '1' => [
            'showitem' => '
                --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,
                    title, description,
                --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,
                    hidden, starttime, endtime
            ',
        ],
    ],
    'columns' => [
        'title' => [
            'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang_db.xlf:tx_myext_domain_model_product.title',
            'config' => [
                'type' => 'input',
                'size' => 30,
                'eval' => 'trim,required',
                'max' => 255,
            ],
        ],
        'description' => [
            'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang_db.xlf:tx_myext_domain_model_product.description',
            'config' => [
                'type' => 'text',
                'enableRichtext' => true,
                'richtextConfiguration' => 'default',
            ],
        ],
    ],
];

11. Security Best Practices

Input Validation:

use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;

// Validate integer input
if (!MathUtility::canBeInterpretedAsInteger($input)) {
    throw new \InvalidArgumentException('Invalid integer value');
}

// Sanitize email
$email = GeneralUtility::validEmail($input) ? $input : '';

// Escape output in templates
{product.title -> f:format.htmlspecialchars()}

SQL Injection Prevention:

// Use QueryBuilder with bound parameters
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
    ->getQueryBuilderForTable('tx_myext_domain_model_product');

$products = $queryBuilder
    ->select('*')
    ->from('tx_myext_domain_model_product')
    ->where(
        $queryBuilder->expr()->eq(
            'uid',
            $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)
        )
    )
    ->executeQuery()
    ->fetchAllAssociative();

CSRF Protection:

<!-- Always include form protection token -->
<f:form.hidden property="__trustedProperties" value="{formProtection}" />

Common Anti-Patterns to Avoid

Don't: Use GeneralUtility::makeInstance() for Services

// Old way (deprecated)
$repository = GeneralUtility::makeInstance(ProductRepository::class);

Do: Use Dependency Injection

// Modern way
public function __construct(
    private readonly ProductRepository $repository
) {}

Don't: Access $GLOBALS directly

// Avoid global state
$user = $GLOBALS['BE_USER'];
$tsfe = $GLOBALS['TSFE'];

Do: Inject Context and Services

public function __construct(
    private readonly Context $context,
    private readonly TypoScriptService $typoScriptService
) {}

Don't: Use ext_tables.php for configuration

// ext_tables.php (deprecated for most uses)

Do: Use dedicated configuration files

// Configuration/Backend/Modules.php
// Configuration/TCA/
// Configuration/Services.yaml

Conformance Checklist

  • Complete directory structure following best practices
  • composer.json with proper PSR-4 autoloading
  • Quality tools configured (php-cs-fixer, phpstan)
  • CI/CD pipeline (GitHub Actions or GitLab CI)
  • Comprehensive test coverage (unit, functional, acceptance)
  • Complete documentation in RST format
  • Service configuration in Services.yaml
  • Backend modules in Configuration/Backend/
  • TCA files in Configuration/TCA/
  • Language files in XLIFF format
  • Dependency injection throughout
  • No global state access
  • Security best practices followed
  • .editorconfig for consistent formatting
  • README.md with clear instructions
  • LICENSE file present