Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:43:22 +08:00
commit aab6ef2415
31 changed files with 12720 additions and 0 deletions

View File

@@ -0,0 +1,917 @@
# 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:**
```json
{
"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
<?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:**
```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:
```php
// ✅ 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:
```php
// ❌ 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:
```php
// ❌ 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:
```php
// ✅ 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:**
```php
// 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:**
```php
// ✅ 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:**
```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
<?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:**
```bash
#!/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:**
```yaml
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:**
```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](https://github.com/TYPO3BestPractices/tea) - 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`:
```bash
# 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:**
```yaml
# .github/workflows/tests.yml
on:
push:
branches: [main, develop] # Changed from: master
pull_request:
branches: [main] # Changed from: master
```
**Example documentation update:**
```xml
<!-- 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`:
```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:
```bash
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`:
```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:
```bash
gh api -X POST repos/OWNER/REPO/rulesets \
--input ruleset-protect-main.json
```
**Verify rulesets are active:**
```bash
# 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
<?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
<?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:**
```php
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:**
```php
// 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:**
```html
<!-- Always include form protection token -->
<f:form.hidden property="__trustedProperties" value="{formProtection}" />
```
## Common Anti-Patterns to Avoid
### ❌ Don't: Use GeneralUtility::makeInstance() for Services
```php
// Old way (deprecated)
$repository = GeneralUtility::makeInstance(ProductRepository::class);
```
### ✅ Do: Use Dependency Injection
```php
// Modern way
public function __construct(
private readonly ProductRepository $repository
) {}
```
### ❌ Don't: Access $GLOBALS directly
```php
// Avoid global state
$user = $GLOBALS['BE_USER'];
$tsfe = $GLOBALS['TSFE'];
```
### ✅ Do: Inject Context and Services
```php
public function __construct(
private readonly Context $context,
private readonly TypoScriptService $typoScriptService
) {}
```
### ❌ Don't: Use ext_tables.php for configuration
```php
// ext_tables.php (deprecated for most uses)
```
### ✅ Do: Use dedicated configuration files
```php
// 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