PHPackages                             wapplersystems/form - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. wapplersystems/form

ActiveTypo3-cms-framework

wapplersystems/form
===================

WapplerSystems fork of typo3/cms-form — extended backend editor (variants/conditions UI), additional events, cross-field validators (e.g. entropy spam filter).

14.3.x-dev(1mo ago)20[4 PRs](https://github.com/WapplerSystems/form/pulls)GPL-2.0-or-laterPHPCI passing

Since May 27Pushed todayCompare

[ Source](https://github.com/WapplerSystems/form)[ Packagist](https://packagist.org/packages/wapplersystems/form)[ Docs](https://typo3.community/)[ Fund](https://typo3.org/membership)[ RSS](/packages/wapplersystems-form/feed)WikiDiscussions release/v14 Synced today

READMEChangelogDependencies (4)Versions (8)Used By (0)

wapplersystems/form
===================

[](#wapplersystemsform)

A hard fork of the TYPO3 system extension `form` — the read-only subtree split at , itself derived from `typo3/sysext/form/` in .

It is installed via Composer as `wapplersystems/form` and **transparently replaces `typo3/cms-form`** through Composer's `replace` mechanism. The Composer package name and the TYPO3 extension key on disk are deliberately different (`wapplersystems/form` vs. `form`) so the extension stays a drop-in for `EXT:form` — everything that references `\TYPO3\CMS\Form\…`, the YAML mixins, the form editor JavaScript, the Fluid template paths and the FAL config keeps working without changes.

Every addition is backwards compatible: existing forms keep working and features degrade gracefully (e.g. without JavaScript, or for forms that don't use them).

Why fork?
---------

[](#why-fork)

The TYPO3 core `form` sysext is intentionally minimal in places where editor workflows benefit from more depth:

1. **More events** — make finisher pipelines, variant evaluation and form rendering pluggable from outside via PSR-14 events instead of class extension.
2. **Backend editor parity** — features that exist today only via YAML hand-editing (variants/conditions, complex validators, finisher options, translations) become editable in the form editor.
3. **Cross-field validators** — validators that need more than a single field's value (entropy/spam filtering across all submitted text, conditional `required`, sums of numeric fields, etc.).
4. **Visual variants/conditions editor** — so integrators express conditional behavior without writing YAML.
5. **Consolidate `wapplersystems/form_extended`** — the patches and additions that live in `form_extended` (multi-upload, sender-address config in site settings, country/date/time fields, custom finishers) migrate into this fork step by step; `form_extended` is then deprecated.

---

New features
------------

[](#new-features)

### Screenshots

[](#screenshots)

The fork's backend editor, shown here with the German interface language (labels are shipped in `Resources/Private/Language/de.Database.xlf`):

[![Inspector: variants & conditions and in-editor translation](Documentation/Images/01-inspector-variants-translations-de.png)](Documentation/Images/01-inspector-variants-translations-de.png)[![Whole-form translation overview](Documentation/Images/02-whole-form-translation-de.png)](Documentation/Images/02-whole-form-translation-de.png)Variants &amp; conditions plus per-element and whole-form translation, right in the inspector.The whole-form translation matrix (every translatable string × every site language).[![E-mail content editor – HTML body](Documentation/Images/03-email-content-html-de.png)](Documentation/Images/03-email-content-html-de.png)[![E-mail content editor – plain text](Documentation/Images/04-email-content-plaintext-de.png)](Documentation/Images/04-email-content-plaintext-de.png)Rich-text **HTML** body of an e-mail finisher, with field-marker insertion.Separate **plain-text** body (left empty it is derived from the HTML automatically).[![E-mail content editor – preview](Documentation/Images/05-email-content-preview-de.png)](Documentation/Images/05-email-content-preview-de.png)[![Visual condition builder](Documentation/Images/06-condition-builder-de.png)](Documentation/Images/06-condition-builder-de.png)Server-rendered **preview** (real Fluid e-mail layout, filled with sample values) and a test-send action.The visual **condition builder** with a live expression preview.### Backend editor

[](#backend-editor)

- **Variant / condition editor** — Variants (conditions) are editable directly in the form editor for **every** renderable (elements, pages, the form) **and every finisher**, instead of being YAML-only. Per variant: condition expression, visibility (`renderingOptions.enabled`) and conditional *required* (`NotEmpty`). Round-trip-safe on save. Previously, opening + saving a form in the editor silently dropped hand-written variants.
- **Visual condition builder** — A *Build…* button next to any condition field opens a modal to click conditions together: rule rows (field + operator + value) with nested AND/OR groups, live expression preview, and parsing of existing expressions back into the tree (raw-text fallback for anything non-parseable).
- **Comfortable e-mail content editor** — Each e-mail finisher gets an *Edit email content* button opening a large modal with: a **template chooser**, a **rich-text HTML body** and a **separate plain-text body**, a **server-rendered preview** (the real Fluid e-mail layout, filled with type-appropriate sample values), and a **“Send test email”** action. HTML and plain text are now independent finisher options.
- **Field-marker inserter** — In the e-mail content editor (HTML and plain panes), an *Insert field marker…* dropdown drops `{fieldIdentifier}` / `{formValues}` placeholders at the cursor, so editors don't type the placeholder syntax by hand.

### Localization (in-editor, per site language)

[](#localization-in-editor-per-site-language)

No XLF authoring required — translations are stored inside the form definition and work for database-stored forms too.

- **Per-element translation** — A *Translate…* button per element edits **label, placeholder and option labels** for every configured site language, with a completeness badge.
- **Form-wide translation overview** — A *Translate whole form…* button on the form opens a matrix of every translatable string × every language in one place (including finisher options), with an overall completeness indicator.
- **Translatable validation messages** — Custom validator error messages (`properties.validationErrorMessages`) are translatable per language. Built-in validator messages remain localized via TYPO3's shipped XLF.
- **Translatable finisher options** — The text options of e-mail / confirmation finishers (`subject`, `message`, `plainMessage`) are translatable per language, both from a per-finisher *Translate…* button and from the form-wide overview.

### Frontend

[](#frontend)

- **Live conditions (same page)** — Variants/conditions that reference fields on the **same page** react live in the browser (show/hide, toggle *required*) while the user types, without a server round-trip. The server stays authoritative on submit.

### Runtime

[](#runtime)

- **Variant-capable finishers** — Any finisher can carry a `variants` list and be enabled/disabled (or otherwise overridden) by a condition on the submitted values (e.g. “send a copy to the sender only when the checkbox is ticked”). The dedicated `CopyToSenderEmail` finisher was removed in favour of this general mechanism.
- **Additional PSR-14 events** — Extra extension points around the whole form lifecycle (see the reference table below).

### Other additions carried by the fork

[](#other-additions-carried-by-the-fork)

- Cross-field / form-level validators (e.g. an entropy-based spam filter).
- Extra form elements (`Time`) and finishers (`RedirectToUri`, `FeUser`, `AttachUploadsToObject`) and view helpers.
- Opt-in site-sender feature and opt-in validation-failure logging.
- Password-policy JSON endpoint.
- A self-contained build pipeline for the editor TypeScript sources under `Build/`.

---

Installation
------------

[](#installation)

Replaces `typo3/cms-form`; install via Composer. The `replace` clause in this package's `composer.json` makes Composer treat `typo3/cms-form` as already satisfied, so no second copy is downloaded.

### Local development inside the dev14 monorepo

[](#local-development-inside-the-dev14-monorepo)

The `dev14` Composer project loads this directory via the `packages/*/*` path repository. To require it, replace the line

```
"typo3/cms-form": "14.3.*@dev",
```

in the project's `composer.json` with

```
"wapplersystems/form": "dev-release/v14 as 14.3",
```

### Building the editor JavaScript

[](#building-the-editor-javascript)

The editor's TypeScript sources live under `Build/`. The pipeline compiles, rewrites import specifiers, and deploys to `Resources/Public/JavaScript/`:

```
cd Build
npm install
npm run build      # tsc → rewrite-imports → deploy
```

> Note: the TYPO3 backend serves the editor as ES modules and caches them aggressively — after a rebuild, reload the editor with the browser cache bypassed (hard reload), or the stale module keeps running.

---

Reference
---------

[](#reference)

### Fork-added PSR-14 events

[](#fork-added-psr-14-events)

All fork-added events live in `TYPO3\CMS\Form\Event\`, alongside the events shipped by upstream EXT:form, and are dispatched from patched upstream call-sites. Their class names are distinct from the upstream ones, so the two sets never collide.

EventFired fromCarriesUse-case`BeforeFormPageProcessedEvent``FormRuntime::processSubmittedFormValues()``Page`, `FormRuntime`, `RequestInterface`Preprocess submitted request args, snapshot for analytics, early-exit hooks`BeforeFormIsValidatedEvent``FormRuntime::mapAndValidatePage()` (start)`Page`, `FormRuntime`, `RequestInterface`Setup before cross-field validators run (precompute shared values)`AfterFormIsValidatedEvent``FormRuntime::mapAndValidatePage()` (end)`Page`, `FormRuntime`, `RequestInterface`, **mutable** `Result`Cross-field validators add errors via `$event->result->forProperty(...)->addError(...)`. Also the hook for validation-failure logging.`AfterVariantAppliedEvent``FormRuntime::processVariants()``VariableRenderableInterface`, `RenderableVariantInterface`, `FormRuntime`React to dynamic form structure changes (cache invalidation, condition-match analytics)`BeforeFinisherExecutedEvent``AbstractFinisher::execute()``FinisherInterface`, `FinisherContext`Inject runtime values, log finisher invocations, call `$context->cancel()` to skip the rest of the chain`AfterFinisherExecutedEvent``AbstractFinisher::execute()``FinisherInterface`, `FinisherContext`, `mixed` (executeInternal result)Post-finisher logging, output transformation, follow-up actions. Does **not** fire on FinisherException.`AfterYamlConfigurationLoadedEvent``ConfigurationManager::getYamlConfiguration()`mutable `array $yamlConfiguration`Inject runtime-computed values into the form-editor configuration (site languages, file mounts, dynamic option lists). Fires on every load, not cache-gated — listeners must be cheap.`MailBeforeSendingEvent``EmailFinisher::executeInternal()``FluidEmail` (mutable), `FinisherContext`, `EmailFinisher`Mutate the email immediately before transport — extra recipients, custom headers, conditional attachments, audit logging. Does **not** fire if EmailFinisher throws before reaching the transport step.`AfterMailSentEvent``EmailFinisher::executeInternal()``FluidEmail`, `FinisherContext`, `EmailFinisher`Fires **only after a successful** `MailerInterface::send()` — the reliable "delivered" hook for audit logging / post-delivery follow-ups (unlike `MailBeforeSendingEvent`, which can't tell success from a later transport failure).`AfterFormStateInitializedEvent``FormRuntime::triggerAfterFormStateInitialized()``FormRuntime`PSR-14 replacement for the legacy `SC_OPTIONS['ext/form']['afterFormStateInitialized']` hook (still fired alongside). Canonical point to **prefill** form values from fe\_user / GET-POST / session via the FormRuntime ArrayAccess API (`$event->formRuntime['email'] = …`). Fires every request, so guard first-display-only prefills.`AfterFormSubmittedEvent``FormRuntime::invokeFinishers()` (after the chain)`FormRuntime`, `array $formValues`, `string $renderedOutput`, `bool $wasCancelled`Fires **exactly once** per submission after the whole finisher chain (Before/AfterFinisherExecuted fire per finisher). The hook for "submission complete" actions: conversion / analytics tracking, CRM sync, a single follow-up. `wasCancelled` reflects a finisher having called `FinisherContext::cancel()`.`BeforeFinishersInvokedEvent``FormRuntime::invokeFinishers()` (before the chain)`FormRuntime`, **mutable** `FinisherInterface[] $finishers`, `FinisherContext`Fires once before the chain. Listeners may reorder / filter / inject finishers (FormRuntime iterates the modified `$finishers`), cancel the whole chain via `$finisherContext->cancel()`, or seed the shared FinisherVariableProvider. Counterpart to `AfterFormSubmittedEvent`.`AfterDatabaseRecordPersistedEvent``SaveToDatabaseFinisher::saveToDatabase()` (also covers `FeUserFinisher`)`string $table`, `int $uid`, `array $data`, `'insert'|'update' $mode`, `FinisherInterface`, `FinisherContext`Fires after a row was inserted/updated by a form. Hook for "record persisted" follow-ups (workflow on new fe\_user, CRM push) with the inserted `uid` in hand. For `update` there is no single uid → `$uid` is `0`; use `$mode`/`$data`.`AfterFileUploadedEvent``UploadedFileReferenceConverter::importUploadedResource()``File $file`, `array $uploadInfo`Fires right after an uploaded file is stored in FAL (before the FileReference is built). Use for virus scanning, EXIF/metadata stripping, content policy. A listener that **throws** aborts property mapping and thereby rejects the upload.`AfterRenderableIsValidatedEvent``FormRuntime::mapAndValidatePage()` (per field)`RenderableInterface`, `mixed $value`, `FormRuntime`, `RequestInterface`, `Result $validationResult`Per-renderable companion to upstream `BeforeRenderableIsValidatedEvent`; fires after a field's processing rule ran. Inspect or add field-scoped errors via `$event->validationResult->addError(...)`. Fires only for renderables that have a processing rule. For the page aggregate use `AfterFormIsValidatedEvent`.`AfterFormRenderedEvent``FormRuntime::render()``FormRuntime`, **mutable** `string $renderedContent`Fires after the renderer produced the form markup (page-render path only, not finisher output). Listeners may rewrite/wrap the markup — tracking pixel, JSON island for client-side logic, CSP nonces. FormRuntime returns the modified `$renderedContent`.The Before/After finisher events fire generically for every finisher inheriting `AbstractFinisher` — Email, Redirect, Confirmation, SaveToDatabase, FlashMessage, DeleteUploads, Closure and any custom finishers. No need to subclass per finisher type.

### Frontend live-conditions (same-page variants)

[](#frontend-live-conditions-same-page-variants)

Variants whose condition references a field on the **same page** are applied live in the browser (show/hide via `renderingOptions.enabled`, required via a `NotEmpty` validator) — not just on the server at page/step transitions. Pieces:

- `InjectFrontendConditions` (listener on `AfterFormRenderedEvent`) emits a JSON island `` inside the `` carrying each element's `{condition, enabled?, required?}` rules, and loads `Resources/Public/JavaScript/frontend/form-conditions.js` via `AssetCollector`. Forms without such variants get neither.
- `FormRuntime::render()` re-enables (`reEnableClientConditionElements()`) elements that a *client-evaluable* variant disabled, so they are present in the DOM for the client to toggle. A condition is client-evaluable when it uses `traverse(formValues, …)` and no server-only context (`stepType`, `finisherIdentifier`, …). The server re-evaluates authoritatively on submit; without JS the fields stay visible and validation still holds.
- `frontend/form-conditions.js` evaluates a subset of the ExpressionLanguage (`traverse(formValues,"id")`, `== != <  >= in "not in" && || ()`) against the live form values and toggles the field container (`[data-form-element]`), `disabled` (so hidden fields are not submitted) and `required`. Unparseable conditions are skipped.
- `RenderableVariant::getCondition()` / `getOptions()` expose the raw condition + override options for this.

### In-editor localization (per-site-language translations)

[](#in-editor-localization-per-site-language-translations)

Each element has a **“Translate…”** button opening a modal with one section per non-default site language and inputs for the element's **label, placeholder, options** and any **custom validation messages**. The form (root) element additionally offers a **“Translate whole form…”** matrix covering every element and every finisher's text options (`subject`, `message`, `plainMessage`).

Translations are stored in the form definition under `renderingOptions.translation.overrides.` for elements and `options.translation.overrides.` for finishers (round-trip via the `MultiValuePropertiesExtractor`s, so no XLF files are required — works for DB-stored forms too). They are applied at render time before the XLF chain: `TranslationService::translateFormElementValue()` (label / placeholder / options), `translateFormElementError()` (validation messages, keyed `c` to keep the path segment non-numeric) and `translateFinisherOption()` (finisher options). `InjectTranslationEditorIntoFormElements` injects the editor(s) + the available site languages (`SiteFinder`, `languageId !== 0`); the inspector shows per-language completeness badges.

### Visual condition builder (form editor)

[](#visual-condition-builder-form-editor)

The variants editor's condition field has a **“Build…”** button (`Build/Sources/TypeScript/form/backend/form-editor/condition-builder.ts`) opening a modal to click together rules (field / operator / value) with AND/OR groups and nesting. It serializes the rule tree to an ExpressionLanguage condition and parses existing ones back (raw-textarea fallback when unparseable). Pure editor JS.

### Form elements added on top of upstream

[](#form-elements-added-on-top-of-upstream)

ElementClassNotes`Time``TYPO3\CMS\Form\Domain\Model\FormElements\Time`HTML5 ``. Backed by `\DateTimeImmutable` parsed with format `H:i`; the date portion is "today" — only the time portion is meaningful. Fills a real gap (core ships `Date` but no `Time`).### Finishers added on top of upstream

[](#finishers-added-on-top-of-upstream)

IdentifierClassPurpose`RedirectToUri``TYPO3\CMS\Form\Domain\Finishers\RedirectToUriFinisher`Redirect to **any** URI (external too). Core's `Redirect` only handles TYPO3 pages via t3-page IDs. Options: `uri`, `statusCode` (default 303).`FeUser``TYPO3\CMS\Form\Domain\Finishers\FeUserFinisher`Insert/update `fe_users` rows from form values. Built on core's `SaveToDatabase`. Per-element `hashPassword: true` runs the value through `PasswordHashFactory::getDefaultHashInstance('FE')`. Requires `pid` option for the storage page.`AttachUploadsToObject``TYPO3\CMS\Form\Domain\Finishers\AttachUploadsToObjectFinisher`Attaches uploaded files to an arbitrary DB record via new `sys_file_reference` rows. Pair with `SaveToDatabase` and reference the inserted UID via `{SaveToDatabase.insertedUids.}`. Rebuild of the legacy form\_extended finisher: direct ConnectionPool inserts, no fake backend user, no `bypassAccessCheck` hack, supports multiple files per element.> **Conditional finishers via variants** (replaces the removed `CopyToSenderEmail`): any finisher can carry a `variants` list inside its `options`, each entry being `{ condition: , ...overrides }`. Before a finisher runs, `FormRuntime::processFinisherVariants()` merges every matching variant into the finisher options (formValues / stepType / finisherIdentifier are in scope). A "send me a copy" email is just a second `EmailToSender` with `renderingOptions.enabled: false` and a variant `{ condition: 'traverse(formValues, "sendCopy") == 1', renderingOptions: { enabled: true } }`.

### View helpers added on top of upstream

[](#view-helpers-added-on-top-of-upstream)

HelperClassUse case```TYPO3\CMS\Form\ViewHelpers\RemoteAddressViewHelper`Renders client IP via `GeneralUtility::getIndpEnv('REMOTE_ADDR')` (respects trusted-proxy config). Useful for audit-trailing email finishers / confirmation pages.```TYPO3\CMS\Form\ViewHelpers\TranslateViewHelper`Form-aware translation wrapper that hits `TYPO3\CMS\Form\Service\TranslationService` (with its form-element overlay logic) instead of `LocalizationUtility`. Use inside form-rendering templates; outside use Fluid's `f:translate`.### Cross-field (form-level) validators

[](#cross-field-form-level-validators)

Validators that need access to more than a single field's value implement `TYPO3\CMS\Form\Validation\FormAwareValidatorInterface` (or extend `AbstractFormAwareValidator`). They are declared on the form root, not on an individual element:

```
type: Form
identifier: contact
renderingOptions:
  formLevelValidators:
    -
      identifier: EntropySpam
      options:
        minimumEntropy: 2.0
        maximumEntropy: 5.5
        textFieldIdentifiers: ['message', 'subject']
        minimumLength: 30
```

The validator identifier must be registered in the prototype's `validatorsDefinition` (the standard prototype already registers `EntropySpam`). The internal listener `RunFormLevelValidators` consumes `AfterFormIsValidatedEvent` and invokes each declared validator after per-element validation has finished; errors merge into the form's aggregate `Result`.

`EntropySpamValidator` ships as the first concrete cross-field validator and uses a Shannon-entropy band to reject submissions that look either repetitive (`aaaaaaa`, `hahaha`) or uniform-random (bot brute-force). Human-written text in most languages falls between roughly 3.5 and 5.0 bits/character; the default band 1.8-5.8 is intentionally wide to avoid false positives.

### Password policy JSON endpoint

[](#password-policy-json-endpoint)

A frontend middleware at `/_form/password-policy/` exposes TYPO3's configured FE password policy (`$GLOBALS['TYPO3_CONF_VARS']['FE']['passwordPolicy']`) as a structured JSON document. Client-side JavaScript can fetch it once and render a live "is your password strong enough yet?" indicator next to a form's password field, in lockstep with the same `CorePasswordValidator` that will validate the submission server-side.

Response shape:

```
{
  "policy": "default",
  "rules": [
    {"id": "minimumLength",            "label": "…", "value": 8},
    {"id": "upperCaseCharacterRequired","label": "…"},
    {"id": "lowerCaseCharacterRequired","label": "…"},
    {"id": "digitCharacterRequired",    "label": "…"},
    {"id": "specialCharacterRequired",  "label": "…"}
  ]
}
```

Only rules the configured `CorePasswordValidator` actually enforces are emitted; a policy that disables `specialCharacterRequired` simply won't return that rule, so the UI stays consistent with the validator. Labels are translated against the active site default language. The middleware is registered in `Configuration/RequestMiddlewares.php` after `cms-frontend/site` (so the site context is available) and before `cms-frontend/page-resolver` (so the JSON URL never enters page-not-found lookup).

### Site-sender feature (opt-in via extension flag)

[](#site-sender-feature-opt-in-via-extension-flag)

Lets site administrators maintain a list of email sender addresses in the BE Site Configuration module; the form plugin's FlexForm then offers a dropdown to pick one per content element. The actual sender on outgoing emails is resolved at runtime from the selection.

Enable in `Admin Tools → Settings → Extension Configuration → form`:

```
form.featureSiteEmail = 1

```

After flushing caches and updating the schema, a new "Form senders" group appears in each site's BE configuration with `email` and `name` fields per entry.

Architecture (classes follow the standard `TYPO3\CMS\Form\…` layout):

- `Configuration/SiteConfiguration/site_sender.php` — TCA for the sub-site-entity with `email` and `name` columns.
- `Configuration/SiteConfiguration/Overrides/site.php` — adds an inline `senders`collection to the `site` configuration. Only active when `featureSiteEmail` is on.
- `Form/FormDataProvider/SiteTcaInline` + `SiteDatabaseEditRow` — Symfony-DI decorators of the core providers; they add `site_sender` to the allowed inline tables. Registered in `Configuration/Services.yaml`.
- `EventListener/InjectSenderDropdownIntoFormPluginFlexForm` — inserts a `settings.sender`Select into the form-plugin FlexForm whose items are populated by `Hooks/SiteSenderItemsProcFunc`.
- `EventListener/HideStaticSenderFieldsInFormPluginFlexForm` — hides the EmailToReceiver finisher's `senderAddress` / `senderName` fields in the form-plugin FlexForm so editors aren't asked to fill in values that the site-sender will override anyway.
- `EventListener/ApplySiteSenderToEmailFinisher` — consumes the upstream `BeforeEmailFinisherInitializedEvent`, reads the selected sender from the plugin's FlexForm, and rewrites the finisher's `senderAddress` / `senderName` options.

When the feature flag is OFF, all listeners early-return and the decorators behave like the plain core data providers — zero runtime cost.

### Validation-failure logging (opt-in per form)

[](#validation-failure-logging-opt-in-per-form)

Enable per form to track which fields fail validation most often — useful for drop-off analysis without storing any user-submitted values:

```
type: Form
identifier: contact
renderingOptions:
  recordValidationFailures: true
```

The `RecordValidationFailures` listener (consumes `AfterFormIsValidatedEvent`) writes one row to `tx_form_validation_log` per validation error with:

- `form_identifier`, `element_identifier`, `property_path`
- `error_code`, `error_message` (already translated, safe to store)
- `page_uid`, `language_uid`, `page_index`
- `session_hash` — SHA-256 of the `FormSession` identifier so multi-attempt patterns from one visitor can be aggregated without identifying them
- `crdate`

What is **NOT** stored: submitted field values, raw inputs, IPs, user agents. The table is engineered to be GDPR-defensible by default.

Sample analytics query:

```
SELECT element_identifier, error_code, COUNT(*) AS hits
FROM tx_form_validation_log
WHERE form_identifier = 'contact' AND crdate > UNIX_TIMESTAMP() - 86400*30
GROUP BY element_identifier, error_code
ORDER BY hits DESC;
```

**Periodic cleanup.** A native TYPO3 v14 scheduler task ships with the fork: `TYPO3\CMS\Form\Task\CleanupValidationLogTask`. Configure it in `Administration → Scheduler → Create task` and select **Form: clean up validation log**. The `tx_form_retention_days` TCA field controls how old rows must be before deletion (default 90 days, range 1–3650). Schedule it daily for production sites with active validation logging — without it the table grows indefinitely. Manual run from CLI: `ddev typo3 scheduler:execute --task=` after the task instance is created.

---

Fork maintenance
----------------

[](#fork-maintenance)

### Branch layout

[](#branch-layout)

BranchPurpose`release/v14`Active dev branch tracking TYPO3 14.x. **Default branch.**`release/v15`Will be created when TYPO3 v15 ships.`main`Mirror of upstream `main` — never patched, sync-only.`14.3`, …Mirrors of upstream major branches — sync-only.### Upstream sync workflow

[](#upstream-sync-workflow)

Upstream is registered as the `upstream` remote (`https://github.com/TYPO3-CMS/form.git`). The flow for picking up new upstream work is:

```
# Update upstream branches
git fetch upstream --prune --tags

# Mirror upstream/14.3 onto our read-only mirror branch
git push origin "refs/remotes/upstream/14.3:refs/heads/14.3" --force-with-lease
git push origin "refs/remotes/upstream/main:refs/heads/main"   --force-with-lease
git push origin --tags

# Cherry-pick relevant commits onto release/v14
git checkout release/v14
git log --oneline release/v14..upstream/14.3      # what's new upstream
git cherry-pick                               # pick what we want
```

**Never merge `upstream/14.3` directly into `release/v14`.** Use cherry-pick so the fork history stays linear and grep-able; we want to see *our* changes without upstream noise mixed in. When a new TYPO3 minor (e.g. 14.4) lands upstream, cherry-pick the relevant commits up to that tag and adjust the `branch-alias` in `composer.json`.

### Conventions for additions

[](#conventions-for-additions)

- Code added on top of upstream lives in the **standard `\TYPO3\CMS\Form\…` layout**, mirroring upstream's own directory structure (`Event/`, `Validation/`, `Domain/Finishers/`, …). Fork-added classes use **distinct class names** so they never collide with upstream files, and an eventual switch to an official package stays painless. The trade-off vs. a separate subnamespace: upstream cherry-picks can land in the same directories, so watch for conflicts when syncing.
- Every public API surface we add gets a PSR-14 event so downstream extensions (including `wapplersystems/form_extended` during the migration period) can consume it.
- New editor UI is implemented through the form editor's existing extension points (TypeScript under `Build/Sources/TypeScript/form/`, partials registered via `formEditorPartials`) rather than by patching upstream templates.
- Commits that touch upstream files must explain *why* the upstream file needed to change — almost always preferable to add a hook/event upstream separately and contribute it back instead.

###  Health Score

35

—

LowBetter than 77% of packages

Maintenance95

Actively maintained with recent releases

Popularity3

Limited adoption so far

Community19

Small or concentrated contributor base

Maturity24

Early-stage or recently created project

 Bus Factor6

6 contributors hold 50%+ of commits

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~4 days

Total

2

Last Release

52d ago

Major Versions

13.4.x-dev → 14.3.x-dev2026-06-01

### Community

Maintainers

![](https://www.gravatar.com/avatar/a944bb90af783b13d74049f2d8adcff598f4e7cb0aad9d7040a6af0bb8c23984?d=identicon)[svewap](/maintainers/svewap)

---

Top Contributors

[![bmack](https://avatars.githubusercontent.com/u/165630?v=4)](https://github.com/bmack "bmack (195 commits)")[![waldhacker1](https://avatars.githubusercontent.com/u/4954582?v=4)](https://github.com/waldhacker1 "waldhacker1 (192 commits)")[![lolli42](https://avatars.githubusercontent.com/u/2178068?v=4)](https://github.com/lolli42 "lolli42 (168 commits)")[![ohader](https://avatars.githubusercontent.com/u/402145?v=4)](https://github.com/ohader "ohader (103 commits)")[![wouter90](https://avatars.githubusercontent.com/u/1637330?v=4)](https://github.com/wouter90 "wouter90 (89 commits)")[![brosua](https://avatars.githubusercontent.com/u/34569593?v=4)](https://github.com/brosua "brosua (83 commits)")[![o-ba](https://avatars.githubusercontent.com/u/8812114?v=4)](https://github.com/o-ba "o-ba (66 commits)")[![andreaskienast](https://avatars.githubusercontent.com/u/1787983?v=4)](https://github.com/andreaskienast "andreaskienast (61 commits)")[![bnf](https://avatars.githubusercontent.com/u/473155?v=4)](https://github.com/bnf "bnf (56 commits)")[![benjaminkott](https://avatars.githubusercontent.com/u/3243119?v=4)](https://github.com/benjaminkott "benjaminkott (53 commits)")[![PyGuy2](https://avatars.githubusercontent.com/u/11974189?v=4)](https://github.com/PyGuy2 "PyGuy2 (45 commits)")[![alexanderschnitzler](https://avatars.githubusercontent.com/u/711459?v=4)](https://github.com/alexanderschnitzler "alexanderschnitzler (41 commits)")[![helhum](https://avatars.githubusercontent.com/u/904370?v=4)](https://github.com/helhum "helhum (35 commits)")[![svewap](https://avatars.githubusercontent.com/u/1734738?v=4)](https://github.com/svewap "svewap (34 commits)")[![mbrodala](https://avatars.githubusercontent.com/u/5037116?v=4)](https://github.com/mbrodala "mbrodala (32 commits)")[![eliashaeussler](https://avatars.githubusercontent.com/u/16313625?v=4)](https://github.com/eliashaeussler "eliashaeussler (31 commits)")[![georgringer](https://avatars.githubusercontent.com/u/1905663?v=4)](https://github.com/georgringer "georgringer (28 commits)")[![derhansen](https://avatars.githubusercontent.com/u/2629896?v=4)](https://github.com/derhansen "derhansen (25 commits)")[![nhovratov](https://avatars.githubusercontent.com/u/19343425?v=4)](https://github.com/nhovratov "nhovratov (22 commits)")[![maddy2101](https://avatars.githubusercontent.com/u/1925287?v=4)](https://github.com/maddy2101 "maddy2101 (20 commits)")

### Embed Badge

![Health badge](/badges/wapplersystems-form/health.svg)

```
[![Health](https://phpackages.com/badges/wapplersystems-form/health.svg)](https://phpackages.com/packages/wapplersystems-form)
```

###  Alternatives

[typo3/cms-form

TYPO3 CMS Form - Flexible TYPO3 frontend form framework that comes with a backend editor interface.

147.6M269](/packages/typo3-cms-form)[eliashaeussler/typo3-warming

Warming - Warms up Frontend caches based on an XML sitemap. Cache warmup can be triggered via TYPO3 backend or using a console command. Supports multiple languages and custom crawler implementations.

22260.2k](/packages/eliashaeussler-typo3-warming)[friendsoftypo3/content-blocks

TYPO3 CMS Content Blocks - Content Types API | Define reusable components via YAML

103519.9k57](/packages/friendsoftypo3-content-blocks)[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k5.9M754](/packages/sylius-sylius)[eliashaeussler/typo3-form-consent

Extension for TYPO3 CMS that adds double opt-in functionality to EXT:form

1595.5k](/packages/eliashaeussler-typo3-form-consent)[pagemachine/typo3-formlog

Form log for TYPO3

23238.6k8](/packages/pagemachine-typo3-formlog)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
