918 lines
24 KiB
Markdown
918 lines
24 KiB
Markdown
# 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
|