# TYPO3 PHP Architecture Standards **Source:** TYPO3 Core API Reference - PHP Architecture **Purpose:** Dependency injection, services, events, Extbase, middleware patterns ## Dependency Injection TYPO3 uses **Symfony's Dependency Injection Container** for service management. ### Constructor Injection (Preferred) ```php // ✅ Right: Constructor injection with readonly properties userRepository->findAll(); $this->view->assign('users', $users); return $this->htmlResponse(); } } ``` ### Method Injection (inject* Methods) ```php // ✅ Right: Method injection for abstract classes userRepository = $userRepository; } } ``` **When to Use Method Injection:** - Extending abstract core classes (ActionController, AbstractValidator) - Avoiding breaking changes when base class constructor changes - Optional dependencies **When to Use Constructor Injection:** - All new code (preferred) - Required dependencies - Better testability ### Interface Injection ```php // ✅ Right: Depend on interfaces, not implementations username; } public function getEmail(): string { return $this->email; } public function getAdditionalData(): array { return $this->additionalData; } public function setAdditionalData(array $additionalData): void { $this->additionalData = $additionalData; } } ``` ### Dispatching Events ```php // ✅ Right: Inject and dispatch events eventDispatcher->dispatch($event); // Use potentially modified data from event $finalUsername = $event->getUsername(); $finalEmail = $event->getEmail(); // Create user with final data } } ``` ### Event Listeners ```php // ✅ Right: Event listener with AsEventListener attribute getEmail(), FILTER_VALIDATE_EMAIL)) { throw new \InvalidArgumentException('Invalid email format'); } // Add custom data $event->setAdditionalData([ 'validated_at' => time(), 'validator' => 'ValidateUserCreationListener', ]); } } ``` ### Event Listener Registration (Services.yaml) ```yaml # Alternative: Register event listeners in Services.yaml services: Vendor\ExtensionKey\EventListener\ValidateUserCreationListener: tags: - name: event.listener identifier: 'vendor/extension-key/validate-user-creation' event: Vendor\ExtensionKey\Event\BeforeUserCreatedEvent method: '__invoke' ``` ### PSR-14 Event Class Standards (TYPO3 13+) Modern event classes should follow these quality standards: ```php // ✅ Right: Modern event class with final keyword and readonly properties newsController; } public function getAssignedValues(): array { return $this->assignedValues; } public function setAssignedValues(array $assignedValues): void { $this->assignedValues = $assignedValues; } public function getRequest(): ServerRequestInterface { return $this->request; // Read-only, no setter } } ``` **Event Class Quality Checklist:** - [ ] Use `final` keyword (prevents inheritance, ensures immutability) - [ ] Use `readonly` for properties that should never change after construction - [ ] Provide getters for all properties - [ ] Provide setters ONLY for properties that should be modifiable - [ ] Type hint all properties and methods - [ ] Document the purpose and usage of the event **Why `final` for Events?** - Events are data carriers, not meant to be extended - Prevents unexpected behavior from inheritance - Makes event behavior predictable and testable - Follows modern PHP best practices **Why `readonly` for Properties?** - Some event data should never change (e.g., original request, user context) - Explicit immutability prevents accidental modifications - Clearly communicates intent to event listeners - Available in PHP 8.1+ (TYPO3 13 minimum is PHP 8.1) ## TYPO3 13 Site Sets **Purpose:** Modern configuration distribution system replacing static TypoScript includes ### Site Sets Structure ``` Configuration/Sets/ ├── MyExtension/ # Base configuration set │ ├── config.yaml # Set metadata and dependencies │ ├── setup.typoscript # Frontend TypoScript │ ├── constants.typoscript │ └── settings.definitions.yaml # Setting definitions for extension configuration ├── RecordLinks/ # Optional feature set │ ├── config.yaml │ └── setup.typoscript └── Bootstrap5/ # Frontend framework preset ├── config.yaml ├── setup.typoscript └── settings.yaml ``` ### config.yaml Structure ```yaml # ✅ Right: Proper Site Set configuration name: vendor/extension-key label: Extension Name Base Configuration # Dependencies on other sets dependencies: - typo3/fluid-styled-content - vendor/extension-key-styles # Load order priority (optional) priority: 50 # Settings that can be overridden settings: mySetting: value: 'default value' type: string label: 'My Setting Label' description: 'Description of what this setting does' ``` ### settings.definitions.yaml ```yaml # ✅ Right: Define extension settings with validation settings: # Text input mySetting: type: string default: 'default value' label: 'LLL:EXT:extension_key/Resources/Private/Language/locallang.xlf:settings.mySetting' description: 'LLL:EXT:extension_key/Resources/Private/Language/locallang.xlf:settings.mySetting.description' # Boolean checkbox enableFeature: type: bool default: false label: 'Enable Feature' # Integer input itemsPerPage: type: int default: 10 label: 'Items per page' validators: - name: NumberRange options: minimum: 1 maximum: 100 # Select dropdown layout: type: string default: 'default' label: 'Layout' enum: default: 'Default' compact: 'Compact' detailed: 'Detailed' ``` ### Benefits of Site Sets 1. **Modular Configuration**: Split configuration into focused, reusable sets 2. **Dependency Management**: Declare dependencies on other sets 3. **Override Capability**: Sites can override set settings without editing files 4. **Type Safety**: Settings are validated with defined types 5. **Better UX**: Settings UI auto-generated from definitions 6. **Version Control**: Configuration changes tracked properly ### Migration from Static TypoScript ```php // ❌ Old: Static TypoScript includes (TYPO3 12 and earlier) Configuration/TCA/Overrides/sys_template.php: getQueryParams()['status'] ?? null) === 'check') { $response = $this->responseFactory->createResponse(200, 'OK'); $response->getBody()->write(json_encode([ 'status' => 'ok', 'message' => 'System is healthy' ])); return $response->withHeader('Content-Type', 'application/json'); } // Pass to next middleware return $handler->handle($request); } } ``` ### Middleware Registration ```php // Configuration/RequestMiddlewares.php [ 'vendor/extension-key/status-check' => [ 'target' => \Vendor\ExtensionKey\Middleware\StatusCheckMiddleware::class, 'before' => [ 'typo3/cms-frontend/page-resolver', ], 'after' => [ 'typo3/cms-core/normalized-params-attribute', ], ], ], ]; ``` ## Extbase Architecture ### Domain Models ```php // ✅ Right: Extbase domain model title; } public function setTitle(string $title): void { $this->title = $title; } public function getPrice(): float { return $this->price; } public function setPrice(float $price): void { $this->price = $price; } public function isAvailable(): bool { return $this->available; } public function setAvailable(bool $available): void { $this->available = $available; } } ``` ### Repositories ```php // ✅ Right: Extbase repository with dependency injection */ public function findByPriceRange(float $minPrice, float $maxPrice): array { $query = $this->createQuery(); $query->matching( $query->logicalAnd( $query->greaterThanOrEqual('price', $minPrice), $query->lessThanOrEqual('price', $maxPrice) ) ); return $query->execute()->toArray(); } } ``` ### Controllers ```php // ✅ Right: Extbase controller with dependency injection productRepository->findAll(); $this->view->assign('products', $products); return $this->htmlResponse(); } public function showAction(int $productId): ResponseInterface { $product = $this->productRepository->findByUid($productId); $this->view->assign('product', $product); return $this->htmlResponse(); } } ``` ### Validators ```php // ✅ Right: Extbase validator with dependency injection addError('Value must be a string', 1234567890); return; } $existingProduct = $this->productRepository->findOneByTitle($value); if ($existingProduct !== null) { $this->addError( 'Product with title "%s" already exists', 1234567891, [$value] ); } } } ``` ## Common Patterns ### Factory Pattern ```php // ✅ Right: Factory for Connection objects services: Vendor\ExtensionKey\Domain\Repository\MyRepository: factory: ['@TYPO3\CMS\Core\Database\ConnectionPool', 'getConnectionForTable'] arguments: - 'my_table' ``` ### Singleton Services ```php // ✅ Right: Use DI container, not Singleton pattern // Services are automatically singleton by default // ❌ Wrong: Don't use GeneralUtility::makeInstance() for new code use TYPO3\CMS\Core\Utility\GeneralUtility; $service = GeneralUtility::makeInstance(MyService::class); // Deprecated ``` ### PSR Interfaces ```php // ✅ Right: Use PSR interfaces use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Client\ClientInterface; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Psr\Clock\ClockInterface; // Inject PSR-compliant services public function __construct( private readonly LoggerInterface $logger, private readonly ClockInterface $clock ) {} ``` ## Anti-Patterns to Avoid ### ❌ Wrong: Direct instantiation ```php $repository = new ProductRepository(); // Missing dependencies ``` ### ❌ Wrong: Using GeneralUtility::makeInstance() ```php use TYPO3\CMS\Core\Utility\GeneralUtility; $repository = GeneralUtility::makeInstance(ProductRepository::class); ``` ### ❌ Wrong: Global state access ```php $user = $GLOBALS['BE_USER']; // Avoid global state $typoScript = $GLOBALS['TSFE']->tmpl->setup; ``` ### ✅ Right: Dependency injection ```php public function __construct( private readonly ProductRepository $repository, private readonly Context $context ) {} ``` ## Conformance Checklist ### Basic Dependency Injection - [ ] Constructor injection used for all dependencies - [ ] Services registered in Configuration/Services.yaml - [ ] No direct class instantiation (new MyClass()) - [ ] No GeneralUtility::makeInstance() for new services - [ ] PSR interfaces used (ResponseInterface, LoggerInterface, etc.) - [ ] No global state access ($GLOBALS) ### PSR-14 Events (Mandatory) - [ ] PSR-14 events used instead of hooks - [ ] Event classes are immutable with proper getters/setters - [ ] Event listeners use #[AsEventListener] attribute or Services.yaml tags - [ ] Event classes use `final` keyword (TYPO3 13+) - [ ] Event classes use `readonly` for immutable properties (TYPO3 13+) ### TYPO3 13 Site Sets (Mandatory for TYPO3 13) - [ ] Configuration/Sets/ directory exists - [ ] Base set has config.yaml with proper metadata - [ ] settings.definitions.yaml defines extension settings with types - [ ] Set names follow vendor/package convention - [ ] Dependencies declared in config.yaml ### Advanced Services.yaml (Mandatory) - [ ] Event listeners registered with proper tags - [ ] Console commands tagged with schedulable flag - [ ] Data processors registered with unique identifiers - [ ] Cache services use factory pattern - [ ] Service tags include all required attributes ### PSR-15 Middleware - [ ] PSR-15 middlewares registered in RequestMiddlewares.php - [ ] Middleware ordering defined with before/after ### Extbase Architecture - [ ] Extbase models extend AbstractEntity - [ ] Repositories extend Repository base class - [ ] Controllers use constructor injection - [ ] Validators extend AbstractValidator ### Factory Pattern - [ ] Factory pattern for complex object creation (e.g., Connection objects)