Loading...
Loading...
Expert guidance on TYPO3 Powermail 13+ form extension. Creating forms, custom finishers, validators, spam protection, ViewHelpers, PSR-14 events, TypoScript configuration, email templates, backend modules, and extension development. Use when working with powermail forms, mail handling, form validation, or extending powermail functionality.
npx skill4agent add dirnbauer/webconsulting-skills typo3-powermailCompatibility: TYPO3 v13.x and v14.x with Powermail 13.x All code examples target PHP 8.2+ and TYPO3 v13/v14.
TYPO3 API First: Always use TYPO3's built-in APIs, core features, and established conventions before creating custom implementations. Do not reinvent what TYPO3 already provides. Always verify that the APIs and methods you use exist and are not deprecated in your target TYPO3 version (v13 or v14) by checking the official TYPO3 documentation.
Supplements:
- SKILL-CONDITIONS.md - Conditional field/page visibility (powermail_cond)
- SKILL-PHP84.md - PHP 8.4 patterns (property hooks, asymmetric visibility, new array functions)
- SKILL-EXAMPLES.md - Multi-step shop form with Austrian legal types, DDEV SQL + DataHandler CLI
Form (tx_powermail_domain_model_form)
└── Page (tx_powermail_domain_model_page)
└── Field (tx_powermail_domain_model_field)
Mail (tx_powermail_domain_model_mail)
└── Answer (tx_powermail_domain_model_answer)
└── references FieldformcreateconfirmationoptinConfirmdisclaimermarketingcomposer require in2code/powermail| Type | Key | Value Type | Notes |
|---|---|---|---|
| Text | | TEXT (0) | Standard input |
| Textarea | | TEXT (0) | Multi-line |
| Select | | TEXT/ARRAY (0/1) | Multiselect possible |
| Checkbox | | ARRAY (1) | Multiple values |
| Radio | | TEXT (0) | Single selection |
| Submit | | — | Form submit button |
| Captcha | | TEXT (0) | Built-in CAPTCHA |
| Reset | | — | Form reset button |
| Static text | | — | Display only |
| Content element | | — | CE reference |
| HTML | | TEXT (0) | Raw HTML |
| Password | | PASSWORD (4) | Hashed storage |
| File upload | | UPLOAD (3) | File attachments |
| Hidden | | TEXT (0) | Hidden input |
| Date | | DATE (2) | Datepicker |
| Country | | TEXT (0) | Country selector |
| Location | | TEXT (0) | Geolocation |
| TypoScript | | TEXT (0) | TS-generated content |
Answer::VALUE_TYPE_TEXT = 0; // String values
Answer::VALUE_TYPE_ARRAY = 1; // JSON-encoded arrays (checkboxes, multiselect)
Answer::VALUE_TYPE_DATE = 2; // Timestamps
Answer::VALUE_TYPE_UPLOAD = 3; // File references
Answer::VALUE_TYPE_PASSWORD = 4; // Hashed passwordsplugin.tx_powermail {
settings {
setup {
# Form settings
main {
pid = {$plugin.tx_powermail.settings.main.pid}
form = {$plugin.tx_powermail.settings.main.form}
confirmation = 0
optin = 0
morestep = 0
}
# Receiver mail
receiver {
enable = 1
subject = Mail from {firstname} {lastname}
body = A new mail from your website
senderNameField = firstname
senderEmailField = email
# Override receiver: receiver.overwrite.email = admin@example.com
# Attach uploads: receiver.attachment = 1
# Add CC: receiver.overwrite.cc = copy@example.com
}
# Sender confirmation mail
sender {
enable = 1
subject = Thank you for your message
body = We received your submission
senderName = Website
senderEmail = noreply@example.com
}
# Double Opt-In
optin {
subject = Please confirm your submission
senderName = Website
senderEmail = noreply@example.com
}
# Thank you page
thx {
redirect = # Page UID for redirect after submit
}
# Spam protection
spamshield {
_enable = 1
indicator {
honeypod = 5
link = 3
name = 3
session = 1
unique = 2
blacklistString = 7
blacklistIp = 7
rateLimit = 10
}
# Factor threshold (0-100): reject if >=
factor = 75
}
# Validation
misc {
htmlForLabels = 1
showOnlyFilledValues = 1
ajaxSubmit = 0
file {
folder = uploads/tx_powermail/
size = 25000000
extension = jpg,jpeg,gif,png,tif,txt,doc,docx,xls,xlsx,ppt,pptx,pdf,zip,csv,svg
}
}
}
}
}plugin.tx_powermail.settings.setup.prefill {
# By field marker
email = TEXT
email.data = TSFE:fe_user|user|email
firstname = TEXT
firstname.data = TSFE:fe_user|user|first_name
# Prefill from GET/POST
subject = TEXT
subject.data = GP:subject
}plugin.tx_powermail.settings.setup.marketing {
enable = 1
# Tracked: refererDomain, referer, country, mobileDevice, frontendLanguage, browserLanguage, pageFunnel
}plugin.tx_powermail.settings.setup.finishers {
# Lower number = runs first
0.class = In2code\Powermail\Finisher\RateLimitFinisher
10.class = In2code\Powermail\Finisher\SaveToAnyTableFinisher
20.class = In2code\Powermail\Finisher\SendParametersFinisher
100.class = In2code\Powermail\Finisher\RedirectFinisher
# Custom finisher
50.class = Vendor\MyExt\Finisher\CrmFinisher
50.config {
apiUrl = https://crm.example.com/api
apiKey = secret123
}
}<?php
declare(strict_types=1);
namespace Vendor\MyExt\Finisher;
use In2code\Powermail\Finisher\AbstractFinisher;
use In2code\Powermail\Finisher\FinisherInterface;
use In2code\Powermail\Domain\Model\Mail;
final class CrmFinisher extends AbstractFinisher implements FinisherInterface
{
/**
* Method name MUST end with "Finisher"
* Can have initialize*Finisher() called before
*/
public function myCustomFinisher(): void
{
/** @var Mail $mail */
$mail = $this->getMail();
$settings = $this->getSettings();
$configuration = $this->getConfiguration(); // TS config.*
// Access form answers
foreach ($mail->getAnswers() as $answer) {
$fieldMarker = $answer->getField()->getMarker();
$value = $answer->getValue();
// Process...
}
// Access by marker
$answers = $mail->getAnswersByFieldMarker();
$email = $answers['email'] ?? null;
// Check if form was actually submitted (not just displayed)
if (!$this->isFormSubmitted()) {
return;
}
}
}| Class | Key | Purpose |
|---|---|---|
| 0 | Consumes rate limiter tokens |
| 10 | Save answers to custom DB tables |
| 20 | POST form data to external URL |
| 100 | Redirect after submission |
plugin.tx_powermail.settings.setup.dbEntry {
1 {
_enable = TEXT
_enable.value = 1
_table = fe_users
_ifUnique.email = update # update|skip|none
username.value = {email}
email.value = {email}
first_name.value = {firstname}
last_name.value = {lastname}
pid.value = 123
}
}<?php
declare(strict_types=1);
namespace Vendor\MyExt\EventListener;
use In2code\Powermail\Events\CustomValidatorEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
#[AsEventListener('vendor-myext/custom-validator')]
final class CustomValidatorListener
{
public function __invoke(CustomValidatorEvent $event): void
{
$mail = $event->getMail();
$field = $event->getField();
// Validate specific field by marker
if ($field->getMarker() === 'company_vat') {
$answer = null;
foreach ($mail->getAnswers() as $a) {
if ($a->getField()?->getUid() === $field->getUid()) {
$answer = $a;
break;
}
}
if ($answer !== null && !$this->isValidVat((string)$answer->getValue())) {
$event->setIsValid(false);
$event->setValidationMessage('Invalid VAT number');
}
}
}
private function isValidVat(string $vat): bool
{
return (bool)preg_match('/^[A-Z]{2}\d{8,12}$/', $vat);
}
}| Validator | Purpose |
|---|---|
| Email, URL, phone, number, letters, min/max length, regex |
| File size, extension whitelist |
| Password match and strength |
| Built-in CAPTCHA |
| Multi-method spam detection |
| Unique field values |
| Validate against foreign table |
| TypoScript-based custom rules |
| Method | Weight | Description |
|---|---|---|
| 5 | Hidden honeypot field |
| 3 | Excessive links detection |
| 3 | Suspicious name patterns |
| 1 | Session token validation |
| 2 | Duplicate submission check |
| 7 | Blacklisted content |
| 7 | Blacklisted IP addresses |
| 10 | Request rate limiting |
// Before form is rendered
FormControllerFormActionEvent
// Before confirmation page
FormControllerConfirmationActionEvent
// After mail is saved to database
FormControllerCreateActionAfterMailDbSavedEvent
// After submit view is built
FormControllerCreateActionAfterSubmitViewEvent
// Before final view is rendered
FormControllerCreateActionBeforeRenderViewEvent
// Controller initialization
FormControllerInitializeObjectEvent// Modify receiver email addresses
ReceiverMailReceiverPropertiesServiceSetReceiverEmailsEvent
// Modify receiver name
ReceiverMailReceiverPropertiesServiceGetReceiverNameEvent
// Modify sender email (receiver mail)
ReceiverMailSenderPropertiesGetSenderEmailEvent
// Modify sender name (receiver mail)
ReceiverMailSenderPropertiesGetSenderNameEvent
// Modify sender email (confirmation mail)
SenderMailPropertiesGetSenderEmailEvent
// Modify sender name (confirmation mail)
SenderMailPropertiesGetSenderNameEvent
// Modify email body before sending
SendMailServiceCreateEmailBodyEvent
// Before email is sent (last chance to modify)
SendMailServicePrepareAndSendEvent// Control if mail should be saved to DB
CheckIfMailIsAllowedToSaveEvent
// Custom validation logic
CustomValidatorEvent
// Prefill field values
PrefillFieldViewHelperEvent
PrefillMultiFieldViewHelperEvent
// File upload processing
UploadServicePreflightEvent
UploadServiceGetFilesEvent
GetNewPathAndFilenameEvent
// Before password is hashed
MailFactoryBeforePasswordIsHashedEvent
// Modify mail variables/markers
MailRepositoryGetVariablesWithMarkersFromMailEvent
// Validation data attributes
ValidationDataAttributeViewHelperEvent
// Double opt-in confirmation
FormControllerOptinConfirmActionAfterPersistEvent
FormControllerOptinConfirmActionBeforeRenderViewEvent
// Disclaimer/unsubscribe
FormControllerDisclaimerActionBeforeRenderViewEvent<?php
declare(strict_types=1);
namespace Vendor\MyExt\EventListener;
use In2code\Powermail\Events\ReceiverMailReceiverPropertiesServiceSetReceiverEmailsEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
#[AsEventListener('vendor-myext/dynamic-receiver')]
final class DynamicReceiverListener
{
public function __invoke(
ReceiverMailReceiverPropertiesServiceSetReceiverEmailsEvent $event
): void {
$mail = $event->getMail();
$answers = $mail->getAnswersByFieldMarker();
// Route to department based on form field
$department = $answers['department'] ?? null;
if ($department !== null) {
$value = (string)$department->getValue();
$emails = match ($value) {
'sales' => ['sales@example.com'],
'support' => ['support@example.com'],
default => $event->getReceiverEmails(),
};
$event->setReceiverEmails($emails);
}
}
}<?php
declare(strict_types=1);
namespace Vendor\MyExt\EventListener;
use In2code\Powermail\Events\CheckIfMailIsAllowedToSaveEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
#[AsEventListener('vendor-myext/skip-db-save')]
final class SkipDbSaveListener
{
public function __invoke(CheckIfMailIsAllowedToSaveEvent $event): void
{
// Skip DB save for specific forms
$form = $event->getMail()->getForm();
if ($form !== null && $form->getTitle() === 'Contact (no storage)') {
$event->setSavingOfMailAllowed(false);
}
}
}plugin.tx_powermail {
view {
templateRootPaths {
0 = EXT:powermail/Resources/Private/Templates/
10 = EXT:my_ext/Resources/Private/Templates/Powermail/
}
partialRootPaths {
0 = EXT:powermail/Resources/Private/Partials/
10 = EXT:my_ext/Resources/Private/Partials/Powermail/
}
layoutRootPaths {
0 = EXT:powermail/Resources/Private/Layouts/
10 = EXT:my_ext/Resources/Private/Layouts/Powermail/
}
}
}| Template | Purpose |
|---|---|
| Main form rendering |
| Confirmation page |
| Thank you page |
| Admin notification email |
| User confirmation email |
| Double opt-in email |
| All-fields summary |
Partials/Form/Field/Input.html
Partials/Form/Field/Textarea.html
Partials/Form/Field/Select.html
Partials/Form/Field/Check.html
Partials/Form/Field/Radio.html
Partials/Form/Field/File.html
Partials/Form/Field/Date.html
Partials/Form/Field/Captcha.html
Partials/Form/Field/Hidden.html
Partials/Form/Field/Password.html
Partials/Form/Field/Country.html
Partials/Form/Field/Location.html
Partials/Form/Field/Html.html
Partials/Form/Field/Content.html
Partials/Form/Field/Typoscript.html
Partials/Form/Field/Submit.html
Partials/Form/Field/Reset.html<!-- In ReceiverMail.html / SenderMail.html -->
{mail} <!-- Mail domain object -->
{mail.senderName} <!-- Sender name -->
{mail.senderMail} <!-- Sender email -->
{mail.form.title} <!-- Form title -->
{mail.answers} <!-- All answers (ObjectStorage) -->
<!-- Iterate answers -->
<f:for each="{mail.answers}" as="answer">
{answer.field.title}: {answer.value}
</f:for>
<!-- PowermailAll marker (all fields formatted) -->
{powermail_all}<!-- Enable JS validation and/or AJAX submit -->
<vh:validation.enableJavascriptValidationAndAjax
form="{form}"
additionalAttributes="{...}" />
<!-- Validation data attributes on fields -->
<vh:validation.validationDataAttribute field="{field}" />
<!-- Error CSS class -->
<vh:validation.errorClass field="{field}" class="error" />
<!-- Upload attributes (accept, multiple) -->
<vh:validation.uploadAttributes field="{field}" /><!-- Country selector -->
<vh:form.countries
settings="{settings}"
field="{field}"
mail="{mail}" />
<!-- Advanced select with optgroups -->
<vh:form.advancedSelect
field="{field}"
mail="{mail}" />
<!-- Multi-upload -->
<vh:form.multiUpload field="{field}" /><!-- Prefill single-value field -->
<vh:misc.prefillField field="{field}" mail="{mail}" />
<!-- Prefill multi-value field (select, check, radio) -->
<vh:misc.prefillMultiField field="{field}" mail="{mail}" cycle="{cycle}" /><!-- Check if field is not empty -->
<vh:condition.isNotEmpty val="{value}">
<f:then>Has value</f:then>
</vh:condition.isNotEmpty>
<!-- Check if array -->
<vh:condition.isArray val="{value}">
<f:then>Is array</f:then>
</vh:condition.isArray>
<!-- Check file exists -->
<vh:condition.fileExists file="{path}">
<f:then>File available</f:then>
</vh:condition.fileExists><!-- Edit link in backend module -->
<vh:be.editLink table="tx_powermail_domain_model_mail" uid="{mail.uid}">
Edit
</vh:be.editLink>plugin.tx_powermail.settings.setup.misc.ajaxSubmit = 1plugin.tx_powermail.settings.setup.main.optin = 1
plugin.tx_powermail.settings.setup.optin {
subject = Please confirm your submission
senderName = My Website
senderEmail = noreply@example.com
}hidden=1optinConfirmAction#mail:searchterm#form:searchtermservices:
Vendor\MyExt\EventListener\CrmSyncListener:
tags:
- name: event.listener
identifier: 'vendor-myext/crm-sync'#[AsEventListener]// By field marker (most common)
$answers = $mail->getAnswersByFieldMarker();
$email = $answers['email']?->getValue();
// By field UID
$answers = $mail->getAnswersByFieldUid();
// Filter by value type
$uploads = $mail->getAnswersByValueType(Answer::VALUE_TYPE_UPLOAD);// Add custom data (available in all finishers/events)
$mail->addAdditionalData('crm_id', $crmResponse['id']);
// Retrieve in another finisher/event
$crmId = $mail->getAdditionalData()['crm_id'] ?? null;ext_conf_template.txtTableGarbageCollectionTaskrouteEnhancers:
PowermailOptIn:
type: Plugin
routePath: '/optin/{mail}/{hash}'
namespace: 'tx_powermail_pi1'
requirements:
mail: '\d+'
hash: '[a-zA-Z0-9]+'ReceiverMailReceiverPropertiesServiceSetReceiverEmailsEvent<?php
declare(strict_types=1);
namespace Vendor\MyExt\SpamShield;
use In2code\Powermail\Domain\Validator\SpamShield\AbstractMethod;
final class ApiCheckMethod extends AbstractMethod
{
public function spamCheck(): bool
{
$mail = $this->getMail();
// Return true if spam detected
return $this->callExternalApi($mail);
}
}plugin.tx_powermail.settings.setup.spamshield.methods {
100 {
class = Vendor\MyExt\SpamShield\ApiCheckMethod
_enable = 1
configuration {
apiUrl = https://spam-api.example.com
}
}
}plugin.tx_powermail.settings.setup.manipulateVariablesInPowermailAllMarker {
timestamp = TEXT
timestamp.data = date:U
timestamp.strftime = %Y-%m-%d %H:%M:%S
}Conditions tables: See SKILL-CONDITIONS.md Section 12 fortables.tx_powermailcond_*
| Column | Type | Purpose |
|---|---|---|
| int AUTO_INCREMENT | Primary key |
| int | Storage page UID |
| int | Last modification timestamp |
| int | Creation timestamp |
| tinyint | Soft-delete flag |
| tinyint | Visibility flag |
| int | Language UID (0 = default, -1 = all) |
| int | UID of the default language record |
| mediumblob | Diff source for translation |
| int | Publish start (Unix timestamp) |
| int | Publish end (Unix timestamp) |
| Column | Type | Description |
|---|---|---|
| varchar(255) | Form title |
| tinyint | Backend note renderer (internal) |
| varchar(255) | CSS class for form wrapper |
| varchar(255) | IRRE children count or element browser list |
| varchar(3) | Autocomplete on/off/empty |
| tinyint | Test record flag |
language (l10n_parent, sys_language_uid)| Column | Type | Description |
|---|---|---|
| int | Parent form UID |
| varchar(255) | Page/step title |
| varchar(255) | CSS class for fieldset |
| int | IRRE children count |
| int | Sort order within form |
parent_form (form)language (l10n_parent, sys_language_uid)| Column | Type | Description |
|---|---|---|
| int | Parent page UID |
| varchar(255) | Field label |
| varchar(255) | Field type key (input, select, check, ...) |
| text | Options for select/radio/check (one per line) |
| varchar(255) | File path reference |
| int | CE reference for type=content |
| text | Static text for type=text |
| text | Default/prefill value |
| text | Placeholder text |
| text | Placeholder for repeat field (password) |
| text | TypoScript for type=typoscript |
| int | Validation type (0=none, 1=email, ...) |
| varchar(255) | Regex or config for validation |
| varchar(255) | CSS class for field wrapper |
| varchar(255) | Help text / description |
| tinyint | Allow multi-select |
| varchar(255) | Datepicker format |
| varchar(255) | Prefill from fe_user property |
| tinyint | This field is the sender email |
| tinyint | This field is the sender name |
| tinyint | Required field |
| tinyint | Custom marker enabled |
| varchar(255) | Field marker (variable name) |
| varchar(255) | Custom mandatory error text |
| varchar(20) | Autocomplete attribute |
| varchar(100) | Autocomplete section |
| varchar(8) | Autocomplete type |
| varchar(8) | Autocomplete purpose |
| int | Sort order within page |
parent_page (page)language (l10n_parent, sys_language_uid)| Column | Type | Description |
|---|---|---|
| varchar(255) | Submitter name |
| varchar(255) | Submitter email |
| varchar(255) | Mail subject |
| varchar(1024) | Receiver email(s) |
| text | Mail body (RTE) |
| int | Frontend user UID (if logged in) |
| tinytext | Submitter IP address |
| text | Browser user agent |
| int | Submission timestamp |
| int | Source form UID |
| int | IRRE children count |
| varchar(255) | Spam score |
| text | HTTP referer domain |
| text | Full HTTP referer |
| text | Visitor country |
| tinyint | Mobile device flag |
| int | Frontend language UID |
| text | Browser Accept-Language |
| text | Pages visited before submit |
form (form)feuser (feuser)| Column | Type | Description |
|---|---|---|
| int | Parent mail UID |
| text | Answer value (JSON for arrays) |
| int | 0=text, 1=array, 2=date, 3=upload, 4=password |
| int | Source field UID |
mail (mail)deleted (deleted)hidden (hidden)language (l10n_parent, sys_language_uid)tx_powermail_domain_model_form
│ 1
├──── * tx_powermail_domain_model_page (IRRE via form)
│ │ 1
│ └──── * tx_powermail_domain_model_field (IRRE via page)
│ │
│ │ referenced by
│ ▼
│ tx_powermail_domain_model_answer.field
│
└──── * tx_powermail_domain_model_mail (via form)
│ 1
└──── * tx_powermail_domain_model_answer (IRRE via mail)t3ver_wsidt3ver_oidt3ver_statet3ver_staget3ver_wsid > 0t3ver_*t3ver_wsid = <ws_id>t3ver_state = 1t3ver_stage = 1t3ver_wsid = 0Detailed SQL and DataHandler examples: See SKILL-EXAMPLES.md for complete workspace-aware queries, publishing workflows, and CLI options.
| Level | What gets translated | Key columns |
|---|---|---|
| Form | Title | |
| Page | Title (step heading) | |
| Field | Title, settings, placeholder, mandatory_text, description | |
Automatically stored with | | |
| Answer | Stored with language of submission | |
sys_language_uid = 0sys_language_uid = 1l10n_parentmarkertypesettings-- Form
INSERT INTO tx_powermail_domain_model_form (pid, title, sys_language_uid, l10n_parent)
VALUES (1, 'Contact Form', 0, 0);
-- Assume UID = 10
-- Page
INSERT INTO tx_powermail_domain_model_page (pid, form, title, sorting, sys_language_uid, l10n_parent)
VALUES (1, 10, 'Your Details', 1, 0, 0);
-- Assume UID = 20
-- Fields
INSERT INTO tx_powermail_domain_model_field
(pid, page, title, type, marker, mandatory, sender_name, sorting, sys_language_uid, l10n_parent)
VALUES
(1, 20, 'First Name', 'input', 'firstname', 1, 1, 1, 0, 0), -- UID 30
(1, 20, 'Last Name', 'input', 'lastname', 1, 0, 2, 0, 0), -- UID 31
(1, 20, 'Email', 'input', 'email', 1, 0, 3, 0, 0), -- UID 32
(1, 20, 'Message', 'textarea', 'message', 0, 0, 4, 0, 0), -- UID 33
(1, 20, 'Subject', 'select', 'subject', 1, 0, 5, 0, 0), -- UID 34
(1, 20, 'Send', 'submit', 'submit', 0, 0, 6, 0, 0); -- UID 35
-- Select options for subject (English)
UPDATE tx_powermail_domain_model_field
SET settings = 'General Inquiry\nSupport Request\nPartnership\nOther'
WHERE uid = 34;
-- Mark email field as sender_email
UPDATE tx_powermail_domain_model_field SET sender_email = 1 WHERE uid = 32;-- Form translation (l10n_parent = 10, the English form)
INSERT INTO tx_powermail_domain_model_form (pid, title, sys_language_uid, l10n_parent)
VALUES (1, 'Kontaktformular', 1, 10);
-- Page translation (l10n_parent = 20)
INSERT INTO tx_powermail_domain_model_page
(pid, form, title, sorting, sys_language_uid, l10n_parent)
VALUES (1, 10, 'Ihre Daten', 1, 1, 20);
-- Field translations (l10n_parent points to English field UID)
INSERT INTO tx_powermail_domain_model_field
(pid, page, title, type, marker, mandatory, sender_name, sorting, sys_language_uid, l10n_parent)
VALUES
(1, 20, 'Vorname', 'input', 'firstname', 1, 1, 1, 1, 30),
(1, 20, 'Nachname', 'input', 'lastname', 1, 0, 2, 1, 31),
(1, 20, 'E-Mail-Adresse', 'input', 'email', 1, 0, 3, 1, 32),
(1, 20, 'Nachricht', 'textarea', 'message', 0, 0, 4, 1, 33),
(1, 20, 'Betreff', 'select', 'subject', 1, 0, 5, 1, 34),
(1, 20, 'Absenden', 'submit', 'submit', 0, 0, 6, 1, 35);
-- German select options for subject
UPDATE tx_powermail_domain_model_field
SET settings = 'Allgemeine Anfrage\nSupportanfrage\nPartnerschaft\nSonstiges'
WHERE sys_language_uid = 1 AND l10n_parent = 34;
-- Mark email field as sender_email (must be set on translation too)
UPDATE tx_powermail_domain_model_field
SET sender_email = 1
WHERE sys_language_uid = 1 AND l10n_parent = 32;<?php
declare(strict_types=1);
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
$dataHandler->start([], []);
// Localize form (UID 10) to German (sys_language_uid=1)
$cmdMap = [
'tx_powermail_domain_model_form' => [
10 => [
'localize' => 1, // target language UID
],
],
];
$dataHandler->start([], $cmdMap);
$dataHandler->process_cmdmap();
// DataHandler auto-creates translations of all IRRE children (pages + fields)
// Then update the translated titles:
$translatedFormUid = $dataHandler->copyMappingArray_merged['tx_powermail_domain_model_form'][10] ?? null;
if ($translatedFormUid) {
$data = [
'tx_powermail_domain_model_form' => [
$translatedFormUid => [
'title' => 'Kontaktformular',
],
],
];
$dataHandler->start($data, []);
$dataHandler->process_datamap();
}emailemaillocalizesettingssys_language_uidFor a comprehensive multi-step mini-shop example with Austrian legal types (Gesellschaftsformen), conditional fields per legal type, GDPR compliance, and two implementation approaches (DDEV SQL + DataHandler CLI command), see SKILL-EXAMPLES.md.