pn_questionnaire 

TYPO3 extension for questionnaires, self-assessments, tests and decision trees.

Extension key pn_questionnaire
TYPO3 compatibility 12.4, 13.4, 14.3
PHP 8.2+
Author ProudNerds
Version 1.1.0
License GPL-2.0-or-later

What this extension does 

pn_questionnaire lets editors build multi-step questionnaires entirely in the TYPO3 backend — no code required. Supported use cases include:

  • Step-by-step decision trees (beslisbomen) that route visitors to the right service
  • Scored self-assessments (health checks, sustainability scans, skills tests)
  • Guided quizzes with a personalised result at the end
  • Multi-path flows where answers determine which questions appear next

Documentation 

Document Audience
Editor Guide Content editors and site administrators — how to build and manage questionnaires in the backend
Admin Guide Administrators, integrators and developers — how to install, configure, theme and extend the extension

Quick start 

  1. Load the TypoScript: on TYPO3 v13 and v14 add the Site Set proudnerds/pn-questionnaire to your site configuration; on v12 include the static template Questionnaire / Test / Decision tree. See Loading the TypoScript in the Admin Guide.
  2. Create a Questionnaire record in the TYPO3 list module (a dedicated sysfolder is recommended)
  3. Add questions, answer options and result pages to the record
  4. Place the Questionnaire / Decision Tree plugin on a page
  5. Select the Questionnaire record in the plugin FlexForm

Admin Guide — pn_questionnaire 

This guide covers installation, TypoScript configuration, theming, and extending the extension.


Table of Contents 

  1. Installation
  2. Loading the TypoScript
  3. TypoScript configuration
  4. Configuring domain record outcomes 4a. Stored results and the result mail
  5. Overriding templates
  6. Styling
  7. JavaScript
  8. Adding a language
  9. Architecture overview
  10. Service layer
  11. Session data format
  12. Extending the extension

1. Installation 

The extension is loaded via Composer as a path repository. It is already registered in the root composer.json:

{
  "repositories": [
    { "type": "path", "url": "packages/*" }
  ],
  "require": {
    "proudnerds/pn-questionnaire": "^1.0"
  }
}
Copied!

Run composer install after adding it. TYPO3 discovers the extension automatically via the PSR-4 autoloader entry in packages/pn_questionnaire/composer.json.

After the first install, run the Database Analyser in the TYPO3 Install Tool to create all seven tables.


2. Loading the TypoScript 

The TypoScript itself lives in Configuration/TypoScript/{setup,constants}.typoscript — a location every supported TYPO3 version understands. It is offered through two routes, both pointing at those same two files:

TYPO3 version Route
14.3 Site Set proudnerds/pn-questionnaire (preferred)
13.4 Site Set proudnerds/pn-questionnaire (preferred)
12.4 Static template Questionnaire / Test / Decision tree

Site Sets were introduced in TYPO3 v13.1 and are ignored by v12, which is why the static template exists. Configuration/Sets/PnQuestionnaire/{setup,constants}.typoscript contain nothing but an @import of the files above.

On TYPO3 v13 and v14 — Site Set 

Add the set to any site that should use the questionnaire plugin by adding it to dependencies in the site's config.yaml:

# config/sites/your-site/config.yaml
dependencies:
  - proudnerds/pn-questionnaire
Copied!

The set name is defined in Configuration/Sets/PnQuestionnaire/config.yaml:

name: proudnerds/pn-questionnaire
label: Questionnaire / Test / Decision tree
Copied!

TYPO3 automatically includes setup.typoscript and constants.typoscript from the set directory when the dependency is active.

Troubleshooting — TypoScript from the set is not loaded

If the set is listed in dependencies but the TypoScript is still not applied, check the root TypoScript template in the TYPO3 backend:

  1. Go to Web → Template, select the root page, open the template record.
  2. On the Options tab, make sure "Clear constants" and "Clear setup" are both unchecked.

When either flag is checked, TYPO3 discards all TypoScript that was loaded by Site Sets before evaluating the template's own content — effectively wiping setup.typoscript entirely. Unchecking both flags lets the Site Set TypoScript survive and be extended by the template.

On TYPO3 v12 — static template 

The extension registers Configuration/TypoScript/ as a selectable static template in Configuration/TCA/Overrides/sys_template.php. Go to Web → Template, open the root template record and add Questionnaire / Test / Decision tree (pn_questionnaire) to Include static (from extensions).

Include it before any site package that overrides the view path constants, otherwise those overrides are undone again.

Do not combine the static template with a site package that already imports EXT:pn_questionnaire/Configuration/TypoScript/setup.typoscript directly — the values are identical so setup stays correct, but a second pass over the constants can override a site package's view path overrides depending on include order.

Backend search on TYPO3 v14 

On v12 and v13 the records of this extension declare their backend search fields explicitly. TYPO3 v14 dropped that mechanism and derives searchability from the field type instead, so on v14 a search in the list module covers every text-like field of these tables rather than a curated subset. The extension follows each version's own convention; nothing needs configuring.


3. TypoScript configuration 

All settings live under plugin.tx_pnquestionnaire_questionnaire.

View path overrides 

Override the default template paths per site package using the constants:

plugin.tx_pnquestionnaire_questionnaire {
  view {
    templateRootPath = EXT:your_sitepackage/Resources/Private/Templates/PnQuestionnaire/
    partialRootPath  = EXT:your_sitepackage/Resources/Private/Partials/PnQuestionnaire/
    layoutRootPath   = EXT:your_sitepackage/Resources/Private/Layouts/PnQuestionnaire/
  }
}
Copied!

TYPO3 merges these with the extension defaults using indexed path arrays (index 1 overrides index 0). You only need to place the files you want to override.

Default FlexForm values 

These defaults apply when a new plugin instance is inserted. Editors can override them per placement.

plugin.tx_pnquestionnaire_questionnaire.settings {
  show_score          = 0   # 0 = hidden, 1 = visible
  show_answer_summary = 0
  introduction_screen = 1
}
Copied!

4. Configuring domain record outcomes 

Note: "Domain record" here refers to a domain model record in the Extbase/DDD sense (e.g. a news article, an activity, an ad) — not a domain name or HTTP address.

The Domain record outcome type redirects the visitor to the detail view of a record from another extension. One target can be configured per site via TypoScript and a TCA override.

Step 1 — TypoScript 

plugin.tx_pnquestionnaire_questionnaire.settings {
  domain_record_target {
    pageUid    = 42    # UID of the page that hosts the detail plugin
    extension  = News  # Extension name (CamelCase, no vendor prefix)
    controller = News
    action     = detail
    argument   = news  # Argument name the action expects
    plugin     = Pi1   # Plugin name as registered with ExtensionUtility
  }
}
Copied!

Step 2 — TCA override 

The extension ships record_uid as a plain number, because it cannot know which table your site links to. The site package names that table, and turns the field into a record browser while it is at it. Create Configuration/TCA/Overrides/tx_pnquestionnaire_result_page.php in your site package:

$config = &$GLOBALS['TCA']['tx_pnquestionnaire_result_page']['columns']['record_uid']['config'];
$config['type'] = 'group';
$config['allowed'] = 'tx_news_domain_model_news';
$config['foreign_table'] = 'tx_news_domain_model_news';
$config['size'] = 1;
Copied!

type and allowed give you the record browser; foreign_table is what Extbase needs to resolve the record. Both table names must match the TypoScript target.

Do not leave allowed empty on a group field. TYPO3 throws RuntimeException 1482250512 while compiling the form, which takes down the entire result page editing form — not just this one field — even for result pages that use a different outcome type. That is why the shipped default is a number and not an unconfigured group.

The DomainRecordResolverService reads the foreign_table value from $GLOBALS['TCA'] at runtime to derive the table name — so a single source of truth is maintained between TypoScript and TCA.


4a. Stored results and the result mail 

Both are off by default and are switched on per plugin instance. See the Editor Guide for what an editor sees; this section covers the technical side.

The table 

tx_pnquestionnaire_saved_result holds one row per completed run.

Column Purpose
token 32 hex characters, 128 bits of entropy, from random_bytes(). Unique index. This is the only key to the row
result_url The generated retrieval URL, cached so the mail and the result page do not have to rebuild it
questionnaire The questionnaire the run belongs to
answers The given answers, as JSON
score The calculated score
expires Unix timestamp after which the row is due for removal
crdate, tstamp, deleted Standard TYPO3 fields; the table uses soft delete

No identifying data is stored — no name, no IP address, no e-mail address, not even hashed. The crdate timestamp is the only field that could in theory be correlated with an access log, which is a reason not to extend the retention period beyond what is needed.

The retrieval route 

Configuration/Routing/SavedResult.yaml provides a route enhancer producing /<plugin-page>/saved-result/<token>. An extension cannot register route enhancers itself, so the site configuration has to import it:

# config/sites/your-site/config.yaml
imports:
  - resource: 'EXT:pn_questionnaire/Configuration/Routing/SavedResult.yaml'
Copied!

Without that import the retrieval link still works, but as a query-string URL. The first segment follows the site language (saved-result, bewaarde-uitslag, gespeichertes-ergebnis); add a locale to the localeMap for another language, or override the whole PnQuestionnaireSavedResult key after the import to change the wording. The token requirement is pinned to [0-9a-f]{32} so the enhancer never claims an unrelated URL on a page without the plugin.

After a domain change or a change to this route, the stored result_url values are stale. They can be regenerated from the tokens — the URL is derived data, not a source.

Retention and purging 

The retention period is db_save_result_lifetime_days in TypoScript, default 365, overridable per plugin instance through the FlexForm. It is applied when a result is stored, by writing expires.

Expired rows are removed by a console command:

vendor/bin/typo3 pnquestionnaire:purgesavedresults
vendor/bin/typo3 pnquestionnaire:purgesavedresults --dry-run
Copied!

Register it as a scheduler task to keep the table clean. Unlike a backend deletion, the command removes rows for real rather than flagging them deleted — a stored result that has expired should not linger in the recycler.

The result mail 

The mail is sent as HTML and plain text, using the installation's own SystemEmail layout, so it inherits the styling already configured for TYPO3 mails.

Rich text inside the mail runs through lib.parseFunc_pnQuestionnaireMail, a copy of lib.parseFunc with forceAbsoluteUrl on. A relative href is a dead link in a mail client, and no Fluid ViewHelper can reach inside stored rich text — f:format.html hands the HTML straight to parseFunc. The nested parseFunc references for lists, preformatted text and table cells are repointed one by one; without that a link inside a list would still come out relative.

The sender falls back through three levels: the plugin's FlexForm, then TypoScript (mail_from_address / mail_from_name), then the installation's default sender.

The send limit 

Configuration/Services.yaml defines a rate limiter for the mail form: three sends per hour, counted separately per recipient address and per client address. It uses the Symfony rate limiter that ships with the core, so there is no custom counting.

Two things to be aware of. The per-client limit is only as reliable as the proxy configuration — if the site runs behind a reverse proxy, reverseProxyIP has to be configured or every visitor looks like the same client. And the FlexForm has a Switch off the send limit checkbox intended for testing an installation; it should be off on a live site, because the limit is what prevents the form from being used to send mail to other people's addresses in bulk.

The form's protection against cross-site submission relies on FE.cookieSameSite being lax or stricter. That is a requirement, not a detail.

Usage counters 

With Count usage on, the questionnaire record keeps two tallies: starts, incremented when a visitor begins, and completions, incremented on a completed run. They are plain counters on the questionnaire record — no per-visitor data is involved.

Inverted conditions 

negate_condition on tx_pnquestionnaire_advice_block inverts a specific_answer condition. It is applied in ResultResolverService, at the point where the block's trigger answer is matched against the given answers. An absent answer counts as "not given", so an inverted block also appears for a question the visitor never answered — which is why the Editor Guide recommends making such questions required.


5. Overriding templates 

The extension uses standard TYPO3 Fluid template path merging. Copy any file from the extension's Resources/Private/ tree into your site package at the configured override path and TYPO3 will use your version instead.

Default paths (from extension) 

Resources/Private/
├── Layouts/
│   └── Default.html
├── Templates/
│   └── Questionnaire/
│       ├── Intro.html
│       ├── Question.html
│       └── Result.html
└── Partials/
    ├── AnswerTypes/
    │   ├── SingleChoice.html
    │   ├── MultipleChoice.html
    │   ├── YesNo.html
    │   ├── Scale.html
    │   └── Informational.html
    └── AdviceBlock.html
Copied!

Custom ViewHelper namespace 

The extension provides one custom ViewHelper. Declare its namespace in any template that uses it:

<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
      xmlns:pnq="http://typo3.org/ns/ProudNerds/PnQuestionnaire/ViewHelpers"
      data-namespace-typo3-fluid="true">
Copied!

Available ViewHelpers:

ViewHelper Description
pnq:inArray(haystack: array, needle: value) Returns true when needle exists in haystack (string-cast comparison). Used in answer type partials to restore the checked state of previously given answers.

Key template variables 

Question.html 

Variable Type Description
questionnaire Questionnaire The questionnaire record
question Question The current question
progress array{current: int, total: int} Step X of Y
progressPercentage int 0–100, for the progress bar fill width
prevQuestionUid int|null UID of the previous question, or null if first
currentAnswer string[] Previously stored answer values for this question
hasAnswers bool true when at least one answer is stored in the session — used to conditionally show the Reset button
answerTypePartial string Partial path, e.g. AnswerTypes/SingleChoice
scaleRange int[] Array from scaleMin to scaleMax (radio scale only)
scaleDisplay string 'radio' or 'range' — from the question's Scale display field
scaleMiddle int Midpoint of the scale range: round((scaleMin + scaleMax) / 2) — used as the default slider position

Result.html 

Variable Type Description
questionnaire Questionnaire The questionnaire record
resultPage ResultPage|null The matched result page (null = no catch-all configured)
adviceBlocks AdviceBlock[] Pre-filtered visible advice blocks
totalScore float Calculated score (0.0 when no scores are set)
showScore bool From FlexForm setting
showAnswerSummary bool From FlexForm setting
answerSummary array[] Pre-built list of {question, answers[]} for the summary

Form field naming 

Answer inputs must use this exact name attribute for Extbase to map them correctly to processAction(array $answers):

name="tx_pnquestionnaire_questionnaire[answers][]"
Copied!

The hidden question UID field:

name="tx_pnquestionnaire_questionnaire[questionUid]"
Copied!

Heading levels, and why the RTE needs Heading 4 and Heading 5 

The result page builds a heading hierarchy from three sources, only two of which the templates control:

Level Rendered by Content
h2 Result.html Result page headline
h3 AdviceBlock.html A group heading (condition_type = group_header), or a block title that is not in a group
h4 AdviceBlock.html The title of a block that points at a group heading through group_header
h5 The editor, inside body_text A sub-heading within a block

That last row is the catch: the deepest level comes out of a rich text field, so it is the RTE configuration — not the extension — that decides whether an editor can produce a valid document. An installation therefore needs both Heading 4 and Heading 5 available in the RTE preset used for tx_pnquestionnaire_advice_block.body_text:

  • Heading 5 for a sub-heading inside a grouped block, whose title is already an h4.
  • Heading 4 for a sub-heading inside an ungrouped block, whose title is an h3.

Which one an editor needs depends on whether the block sits in a group, and that can change after the fact — moving a block into a group shifts its title from h3 to h4, so any sub-heading in its body has to move down a level too. Offer both and the editor can always pick the level one step below the block title.

If the preset stops at Heading 3, every sub-heading an editor creates lands at the same level as — or above — the title of the block it belongs to, which breaks the document outline (WCAG 1.3.1). The extension deliberately does not set richtextConfiguration on the field to force this: that would override whatever preset the site assigns through page TSconfig, for every installation.

Add the missing levels to the heading.options list of the preset your site already uses:

editor:
  config:
    heading:
      options:
        - { model: 'paragraph', title: 'Paragraph' }
        - { model: 'heading2', view: 'h2', title: 'Heading 2' }
        - { model: 'heading3', view: 'h3', title: 'Heading 3' }
        - { model: 'heading4', view: 'h4', title: 'Heading 4' }
        - { model: 'heading5', view: 'h5', title: 'Heading 5' }
Copied!

Note that CKEditor replaces this list as a whole rather than merging it, so if you override it in a separate file, repeat the entries you want to keep. Also check that h5 is present in the allowTags list of the processing configuration — otherwise the tag is stripped on save. The default EXT:rte_ckeditor/Configuration/RTE/Processing.yaml allows h1 through h6.


6. Styling 

The extension ships a minimal CSS file (Resources/Public/Css/Questionnaire.css) that only defines the progress bar. Everything else is unstyled — apply all visual design in your site package.

BEM class reference 

Class Element
.pn-questionnaire Root wrapper (also has data-questionnaire attribute)
.pn-questionnaire__intro Intro screen container
.pn-questionnaire__intro-text Introduction text area
.pn-questionnaire__intro-actions Start button wrapper
.pn-questionnaire__intro-footer Text below the start button
.pn-questionnaire__step Question step container (has data-active-step attribute)
.pn-questionnaire__progress Progress indicator wrapper
.pn-questionnaire__progress-bar Grey track of the progress bar
.pn-questionnaire__progress-fill Coloured fill (width set inline via progressPercentage)
.pn-questionnaire__progress-text "Step X of Y" text
.pn-questionnaire__progress-note Dynamic steps note below the bar
.pn-questionnaire__context-content Embedded tt_content element
.pn-questionnaire__question-text Question text area
.pn-questionnaire__help-text Help text below question
.pn-questionnaire__form The answer form
.pn-questionnaire__answers Fieldset wrapping all answer inputs
.pn-questionnaire__answer-option Wrapper for one answer option
.pn-questionnaire__answer-option--radio Modifier for radio inputs
.pn-questionnaire__answer-option--checkbox Modifier for checkbox inputs
.pn-questionnaire__answer-option--yes-no Modifier for yes/no inputs
.pn-questionnaire__radio Radio input
.pn-questionnaire__checkbox Checkbox input
.pn-questionnaire__answer-label Label for an answer option
.pn-questionnaire__scale Scale question wrapper
.pn-questionnaire__scale--radio Modifier: radio button display
.pn-questionnaire__scale--range Modifier: range slider display
.pn-questionnaire__scale-options Row of scale radio buttons (radio display only)
.pn-questionnaire__scale-option One scale value (radio display only)
.pn-questionnaire__scale-label Label for one radio scale value
.pn-questionnaire__scale-range-track Wrapper for the slider input and output (range display only)
.pn-questionnaire__scale-range-label--min Min endpoint label (range display only)
.pn-questionnaire__scale-range-label--max Max endpoint label (range display only)
.pn-questionnaire__range The <input type="range"> element
.pn-questionnaire__range-value The <output> element showing the live selected value
.pn-questionnaire__error Client-side validation error message (hidden by default)
.pn-questionnaire__required-mark Asterisk rendered inline after the question text for required questions
.pn-questionnaire__nav Previous / Next / Reset button wrapper
.pn-questionnaire__btn Base button class
.pn-questionnaire__btn--primary Primary action button
.pn-questionnaire__btn--secondary Secondary action button (Previous)
.pn-questionnaire__btn--reset Reset / Start over button
.pn-questionnaire__result Result container
.pn-questionnaire__result--error Modifier when no result page matched
.pn-questionnaire__result-headline Result headline
.pn-questionnaire__result-score Score display
.pn-questionnaire__result-body Result body text
.pn-questionnaire__result-cta CTA button wrapper
.pn-questionnaire__result-reset Reset button on result page
.pn-questionnaire__advice-blocks Advice blocks container
.pn-questionnaire__advice-block One advice block
.pn-questionnaire__advice-block--always Modifier for always-visible blocks
.pn-questionnaire__advice-block--score_range Modifier for score-range blocks
.pn-questionnaire__advice-block--specific_answer Modifier for specific-answer blocks
.pn-questionnaire__advice-block--scale_range Modifier for scale-range blocks
.pn-questionnaire__advice-block-headline Advice block headline
.pn-questionnaire__advice-block-body Advice block body text
.pn-questionnaire__answer-summary Answer summary container
.pn-questionnaire__summary-item One question/answer pair in the summary
.pn-questionnaire__summary-question Question text in summary
.pn-questionnaire__summary-answers Answer text in summary

Overriding the progress bar colour 

The fill uses currentColor, which inherits from the nearest element with an explicit color value:

/* In your site package */
.pn-questionnaire__progress-fill {
    color: #your-brand-color;
}
Copied!

7. JavaScript 

The extension ships two JavaScript classes.

Questionnaire.js 

Loaded via f:asset.script in Layouts/Default.html and initialises on DOMContentLoaded by looking for [data-questionnaire] in the DOM.

Feature Description
Scroll to container On each page load, scrolls the questionnaire into view when a step is active ([data-active-step] present)
Required validation Checks for [data-required] flag inside the form; validates that at least one non-hidden input is filled/checked before allowing submission
Error message Reads the error text from data-required-error attribute on the form (set from locallang.xlf in the template)
Submit lock Disables the submit button after a valid submission to prevent double-posting

<input type="range"> always has a value, so required validation passes automatically for scale slider questions.

ScaleRange.js 

Loaded conditionally via f:asset.script only when a scale question renders as a range slider (scale_display = range). TYPO3 deduplicates it automatically — safe to use across multiple scale questions on the same page.

Feature Description
Live value display Keeps the <output> element next to the slider in sync with the selected value as the visitor drags
Initial sync Syncs on load in case the browser normalises the server-rendered value attribute (e.g. out-of-range values)

The <output> element is server-rendered with the correct initial value, so the slider is usable without JavaScript.

All behaviour is progressive enhancement — the questionnaire works without JavaScript through standard HTML form submissions.

Replacing the JavaScript 

Point f:asset.script in your layout override to a different file, or load your own script that initialises a class targeting [data-questionnaire].


8. Adding a language 

  1. Copy Resources/Private/Language/locallang.xlf to a new file named {language-code}.locallang.xlf in the same directory
  2. Add target-language="{language-code}" to the <file> element
  3. Add <target> elements with the translated strings for each <trans-unit>

TYPO3 picks up the translation automatically based on the active frontend language.

The existing Dutch translation (nl.locallang.xlf) can be used as a reference.


9. Architecture overview 

QuestionnaireController
    │
    ├── QuestionnaireRepository   → loads the configured questionnaire
    ├── SessionService            → stores / retrieves visitor answers
    ├── ConditionEvaluatorService → filters questions to visible subset
    ├── ProgressService           → calculates step X of Y, prev/next UIDs
    ├── ScoringService            → sums answer option scores
    ├── ResultResolverService     → picks the first matching result page
    │                               and filters advice blocks
    └── DomainRecordResolverService → builds redirect URL for record outcomes
Copied!

Action flow 

GET  /page  →  introAction()      render intro screen
GET  /page  →  questionAction()   render current question
POST /page  →  processAction()    store answer, redirect to next or result
GET  /page  →  resultAction()     resolve result, render or redirect
POST /page  →  resetAction()      clear session, redirect to intro
Copied!

All five actions are registered as uncached in ext_localconf.php.


10. Service layer 

All business logic lives in Classes/Service/. Services are autowired via Configuration/Services.yaml.

Service Responsibility
SessionService Read/write visitor answers via FrontendUserAuthentication. Key: tx_pnquestionnaire, namespaced per questionnaire UID.
ConditionEvaluatorService Iterates all questions, evaluates each condition set, returns visible questions in sort order.
ProgressService calculate(), getNextQuestionUid(), getPreviousQuestionUid(). All operate on the visible question array.
ScoringService Traverses visible questions → answer options, sums scores. Returns 0.0 when no scores are set.
ResultResolverService Iterates result pages top-to-bottom, returns first match. Also provides filterAdviceBlocks().
DomainRecordResolverService Uses UriBuilder to build the redirect URL for domain_record outcomes. Reads TypoScript domain_record_target and foreign_table from $GLOBALS['TCA'].

Condition evaluation logic 

Each condition has a condition_type that determines how it is evaluated:

specific_answer (default) — passes when the visitor selected a specific answer option for the reference question:

result = evaluate(condition_1)
for each condition_2, condition_3, ...:
    if operator == AND → result = result && evaluate(condition)
    if operator == OR  → result = result || evaluate(condition)
Copied!

scale_range — passes when the visitor's numeric answer for the reference question satisfies the configured operator and threshold:

stored_value [operator] scale_value
e.g. 7 >= 5  → true
Copied!

Supported operators: >=, <=, >, <, =

If the scale question has not been answered yet, the condition returns false (the dependent question stays hidden).

A question is shown when the final combined result is true.

Result page selection 

foreach resultPage in questionnaire.resultPages (ordered by sort_order):
    if matches(resultPage, sessionAnswers, totalScore):
        return resultPage  ← first match wins
return null  ← no catch-all configured
Copied!

Trigger type matching:

Trigger Match condition
catch_all Always
score_range scoreMin <= totalScore <= scoreMax
specific_answer A specific answer option UID appears in sessionAnswers
combination Both score_range AND specific_answer match
scale_answer The visitor's numeric answer for triggerQuestion is within [triggerScaleMin, triggerScaleMax]

11. Session data format 

Answers are stored in the TYPO3 frontend session under the root key tx_pnquestionnaire.

tx_pnquestionnaire
└── q_{questionnaireUid}
    └── answers
        ├── "{questionUid}" → ["{answerOptionUid}"]          ← single choice
        ├── "{questionUid}" → ["{uid1}", "{uid2}"]           ← multiple choice
        └── "{questionUid}" → ["{scaleValue}"]               ← scale (raw number)
Copied!

All values are stored as strings. The SessionService methods handle the conversion.


12. Extending the extension 

Adding a new question type 

  1. Add the new type value to the type select field in Configuration/TCA/tx_pnquestionnaire_question.php (both the items array and the types array)
  2. Add the type constant to Classes/Domain/Model/Question.php
  3. Create Resources/Private/Partials/AnswerTypes/YourType.html
  4. Add the mapping in QuestionnaireController::questionAction() in $answerTypePartialMap

No other changes are required.

Adding a new outcome type 

  1. Add the new outcome value to the outcome_type select field in the result page TCA
  2. Add the outcome constant to Classes/Domain/Model/ResultPage.php
  3. Handle the new outcome in QuestionnaireController::handleRedirectOutcome() or resultAction()

Custom result resolver logic 

To replace or extend the result matching logic, create a service that extends or wraps ResultResolverService and reconfigure it in Configuration/Services.yaml.

Custom scoring logic 

Replace ScoringService with your own implementation via Configuration/Services.yaml service aliasing. Your service only needs to implement the same method signature:

public function calculateTotal(array $visibleQuestions, array $sessionAnswers): float
Copied!

Template-only customisation 

For purely visual changes — copy any Fluid template from Resources/Private/ into your site package at the configured override path. TYPO3's path merging ensures your version takes precedence. No PHP changes needed.

Editor Guide — pn_questionnaire 

This guide explains how to build and manage questionnaires in the TYPO3 backend. No technical knowledge is required.


Table of Contents 

  1. Concepts
  2. Step 1 — Create a Questionnaire record
  3. Step 2 — Place the plugin on a page
  4. Step 3 — Add questions
  5. Step 4 — Add answer options
  6. Step 5 — Add conditions (branching)
  7. Step 6 — Add result pages
  8. Step 7 — Add advice blocks
  9. Plugin settings (FlexForm)
  10. Question types reference
  11. Result page trigger types
  12. Result page outcome types
  13. Tips and rules of thumb

1. Concepts 

Before building a questionnaire it helps to understand the main building blocks.

Term What it is
Questionnaire The top-level record that holds everything — questions, settings and result pages
Question A single step shown to the visitor (one per screen)
Answer option A selectable choice within a question
Condition A rule that shows or skips a question based on a previous answer
Result page What happens when the visitor finishes — shown inline or as a redirect
Advice block An optional content section within an inline result, conditionally visible
Score A numeric value on an answer option, summed into a total at the end

2. Step 1 — Create a Questionnaire record 

  1. Open the List module and navigate to the folder where questionnaire records should be stored (ask your developer to set up a dedicated sysfolder)
  2. Create a new Questionnaire record
  3. Fill in the Title — this is an internal label, never shown to the visitor
  4. On the Introduction screen tab, optionally fill in:

    • Introduction text — appears before the first question, above the start button
    • Text below the start button — a closing remark, for instance how long the questionnaire takes or what happens with the answers

    Both are only shown when the intro screen is enabled in the plugin settings, and both may be left empty.

  5. Save the record

3. Step 2 — Place the plugin on a page 

  1. Edit the page where the questionnaire should appear
  2. Add a new content element and select Questionnaire / Decision Tree from the plugins tab
  3. In the Configuration tab, select the Questionnaire record you just created in the Questionnaire field
  4. Adjust the remaining FlexForm settings if needed — see §9
  5. Save

One questionnaire, multiple pages The same questionnaire record can be placed on multiple pages. Each placement can have different display settings (show score, show answer summary, etc.) because those settings are stored on the plugin instance, not the questionnaire record itself.


4. Step 3 — Add questions 

Inside the Questionnaire record, open the Questions tab and click Create new Question.

Field Description
Question The question text shown to the visitor (supports rich text)
Help text Optional short instruction below the question
Context content element Optional link to an existing tt_content record — useful for adding images, video or formatted text as context
Type The answer type — see §10
Required Whether the visitor must answer before proceeding
Scale minimum / maximum Lower and upper bound of the numeric scale (Scale / Rating type only)
Scale display How the scale is shown: Radio buttons (individual numbered options) or Range slider (draggable slider, default). Scale / Rating type only

Questions are shown in the order they appear in the list. Drag and drop to reorder.

Informational questions Use the Informational type to insert a screen with text or media but no user input — for example, an intermediate explanation or a section break. The visitor just clicks Next to continue.


5. Step 4 — Add answer options 

For Single choice, Multiple choice and Yes / No questions, open the Answer Options tab of the question and add one option per selectable answer.

Field Description
Label The text shown to the visitor
Value Internal identifier — used in conditions and result triggers. Use something descriptive, e.g. employed, yes, option_a
Score Optional numeric score (positive or negative, decimal allowed) — only relevant when you use score-based results

Drag and drop answer options to reorder them.


6. Step 5 — Add conditions (branching) 

A condition makes a question only visible when a specific previous question was answered in a specific way. This is how you build a decision tree.

There are two condition types:

Condition type: Specific answer 

  1. Open the Visibility Conditions tab of the question that should be conditionally shown
  2. Click Create new Condition
  3. Set Condition type to Specific answer
  4. Select the Reference question — the earlier question whose answer determines visibility
  5. Select the Reference answer — the specific answer option that must have been chosen
  6. Save

Condition type: Scale value 

Use this when the reference question is a Scale / Rating question and you want to show or hide based on the numeric value the visitor selected.

  1. Open the Visibility Conditions tab
  2. Click Create new Condition
  3. Set Condition type to Scale value
  4. Select the Reference question (must be a scale question)
  5. Choose an Operator: >=, <=, >, <, or =
  6. Enter a Value to compare against
  7. Save

Example: Show a follow-up question only if the visitor rated satisfaction >= 8.

Multiple conditions 

You can add more than one condition per question. Each condition has an Operator field:

Operator Meaning
AND This condition must also pass (all AND conditions must be met)
OR Passing this condition is enough on its own

Conditions are evaluated in the order they appear in the list (top to bottom, by sort order).

Rules 

  • Conditions can only reference questions that appear earlier in the questionnaire. It is your responsibility to ensure this.
  • A question with no conditions is always shown.
  • Skipped questions do not count toward progress or score.
  • For Scale value conditions: if the referenced question has not been answered yet, the condition fails and the dependent question stays hidden.

Example 

Q1: What is your situation?
    → "Employed"           → show Q2a (work questions)
    → "Self-employed"      → show Q2b (entrepreneur questions)
    → "Looking for work"   → show Q2c (job-seeker questions)
Copied!

Q2a has a condition: type = Specific answer, reference question = Q1, reference answer = "Employed". Q2b has a condition: type = Specific answer, reference question = Q1, reference answer = "Self-employed". Q2c has a condition: type = Specific answer, reference question = Q1, reference answer = "Looking for work".


7. Step 6 — Add result pages 

Open the Result Pages tab of the Questionnaire and click Create new Result Page.

Result pages are evaluated top to bottom — the first match wins. Always put specific conditions first and a catch-all result last as a fallback.

Each result page has two sections:

Trigger — when does this result apply? 

See §11 for all trigger types.

Outcome — what happens when it applies? 

See §12 for all outcome types.


8. Step 7 — Add advice blocks 

Advice blocks are conditional content sections within an inline result page. They let you show personalised advice depending on the visitor's score or a specific answer — within the same result page.

Only available when Outcome type is set to Inline.

Field Description
Condition type When to show this block: Always / Score range / Specific answer / Scale answer range / Group heading above other blocks
Score min / max Visible score range (only for Score range condition)
Trigger answer The answer that must have been given (only for Specific answer condition)
Invert condition Shows the block when the chosen answer was not given — see Inverting a condition
Scale question The scale question whose value is checked (only for Scale answer range condition)
Minimum value / Maximum value Inclusive range the scale answer must fall within (only for Scale answer range condition)
Part of group Places this block under a group heading — see Grouping blocks under a heading
Headline Block heading
Body text Block content (rich text)

Advice blocks are shown in the order they appear in the list. Only blocks whose condition is met are rendered.

Use case example Result page for all visitors with score 10–20. Within it: — Advice block A (always shown): general recommendation — Advice block B (specific answer: "I work outdoors"): specific outdoor tip — Advice block C (score range 15–20): extra warning for higher scores — Advice block D (scale answer range, Q3 satisfaction >= 8): positive reinforcement message

Inverting a condition 

Invert condition turns a Specific answer condition around: the block appears when the visitor did not choose that answer.

This is useful for advice that only applies to people who skipped something. Take a question about organ donation with one option "I have already registered my choice". Attach a block to that option, tick Invert condition, and everyone who did not register gets the reminder — while the people who did are spared a message that does not apply to them.

Watch out: an unanswered question counts as "not given". If the question is optional, or the visitor never reached it, the answer is absent — so an inverted block will appear. Whether that is what you want depends on the text. A reminder ("don't forget to arrange this") is fine for someone who skipped the question. A statement that assumes a situation ("since you have not arranged this yet") is not. Make the question required if you need the distinction to be reliable.

Grouping blocks under a heading 

A long result page reads better in sections. Set Condition type to Group heading above other blocks to turn a block into a heading, then set Part of group on every block that belongs underneath it.

What to keep in mind:

  • A group heading has no condition of its own. It appears automatically when at least one block in its group is visible, and disappears when none are. You never have to maintain the heading's visibility by hand.
  • A group heading has no body text — only a headline.
  • The headline of a grouped block moves one heading level deeper than an ungrouped one. That matters for the rich text in the body: sub-headings inside a grouped block belong on Heading 5, not Heading 4. Using the wrong level skips a level in the document outline, which screen readers and search engines both read as an error.
  • Blocks that are not part of any group keep their normal level and can sit before, between or after the groups.

9. Plugin settings (FlexForm) 

These settings live on the plugin instance (the content element on the page), not the questionnaire record. This means the same questionnaire can behave differently on different pages.

They are split over three tabs.

Tab: Settings 

Setting Description Default
Questionnaire Which questionnaire record to display (required)
Show introduction screen Show a dedicated intro screen with a Start button before Q1 On
Show score on result Show the calculated score on an inline result page Off
Show answer summary Show a recap of the visitor's given answers on the result page Off
Count usage Keep a tally on the questionnaire record of how often it was started and completed Off

Tab: Button labels 

The navigation buttons carry a standard text that follows the language of the site. Each of them can be given an own text here, per plugin instance — useful when the questionnaire is a checklist rather than a test, or when the tone of the page asks for something else than "Next".

Setting Standard text
Start button Start
Previous button Previous
Next button Next
Finish button Finish
Start over button Start over
Change answers button Change my answers
Copy link button Copy link
Send mail button Send

The standard text of each field is shown in grey as long as the field is empty, and an empty field keeps that standard text. There is no need to fill in all of them.

The last two only appear once their function is switched on in Storing and mailing the result: the copy button belongs to the retrieval link, the send button to the mail form.

Tab: Storing and mailing the result 

Setting Description Default
Store the result Lets the visitor keep the result and return to it later through a link Off
Folder for the stored results Where the stored results are filed. Required as soon as storing is on (falls back to the current page)
Keep the result for (days) After this many days a stored result is removed 365
Allow mailing the result Shows a form on the result page where the visitor can mail the result to themselves Off
Link to the privacy statement The page explaining what is stored and for how long. Shown next to the mail form (empty)
Sender address of the mail / Sender name Overrides the installation's default sender for this plugin (installation default)
Opening text of the mail / Closing text Replaces the standard opening and closing lines of the mail (standard text)
Switch off the send limit Testing only — see the warning below Off

Leaving a field empty is meaningful here: the extension then falls back to its own standard text or to the installation's setting. You only fill in what you want to deviate from.

Keeping and sharing the result 

With Store the result on, the result page gains a link the visitor can save or share. Opening it later brings back the same result, without a login and without the visitor's session.

Nothing identifying is stored: no name, no IP address, no e-mail address, not even in hashed form. What is stored is the given answers, the score and a random code that appears in the link. Someone who does not have the link cannot find the result, and the answers cannot be traced back to a person.

Two consequences worth knowing:

  • The result disappears once the retention period expires. Say so in the text around the link if visitors are likely to rely on it.
  • The link is the only key. A visitor who loses it cannot recover the result, and neither can an administrator.

Point Link to the privacy statement at the page where your organisation explains this. The extension only links to it — the text itself is yours to write, and it should mention the retention period and that no personal data is stored.

Mailing the result 

With Allow mailing the result on, the result page shows a form asking for an e-mail address. The visitor receives the full result plus the retrieval link.

The address is used to send that one mail and is not stored — not in the database, not in a log, not on the stored result.

The mail uses the layout of your TYPO3 installation and is sent as both HTML and plain text. Opening text of the mail and Closing text let you replace the standard lines; leave them empty to keep the defaults.

Leave "Switch off the send limit" off on a live site. The limit is what stops the form from being used to send mail to other people's addresses in bulk. Switching it off is meant for testing an installation, where repeated attempts would otherwise be blocked.


10. Question types reference 

Type Input Use for
Single choice Radio buttons — one answer Most questions
Multiple choice Checkboxes — one or more answers "Select all that apply"
Yes / No Two radio buttons (Yes and No — you define the labels) Binary questions
Scale / Rating Numbered scale (min–max, e.g. 1–10) — displayed as radio buttons or a range slider Assessments, NPS, ratings
Informational No input — Next button only Intermediate explanation, section break

Scale display 

Scale questions have a Scale display field that controls how the input is rendered:

Option Description
Range slider (default) A draggable <input type="range"> showing the selected value live. Defaults to the midpoint of the range when no previous answer is stored.
Radio buttons Individual numbered radio inputs, one per step in the range

Scale and informational questions are score-neutral — they never add to the total score.


11. Result page trigger types 

Trigger type When it matches
Catch-all Always — use as the last result page as a fallback
Score range When the visitor's total score is between Score min and Score max (inclusive)
Specific answer When the visitor selected a specific answer option (anywhere in the questionnaire)
Combination When both the score range AND the specific answer match
Scale answer range When the visitor's numeric answer for a specific scale question falls within the configured min–max range (inclusive)

When Scale answer range is selected, three extra fields appear:

Field Description
Scale question The scale question whose answer is checked (only scale questions are listed)
Minimum value Lower bound (inclusive)
Maximum value Upper bound (inclusive)

Order matters. Result pages are evaluated top to bottom. The first match wins. Place the most specific triggers first and catch-all last.


12. Result page outcome types 

Outcome type What happens
Inline Result content (headline, body text, advice blocks, CTA) is shown within the questionnaire on the same page
Internal page Visitor is redirected to a TYPO3 page you select from the page tree
External URL Visitor is redirected to an external URL you enter
Domain record Visitor is sent to the detail view of a specific record from another extension (e.g. news article, event, vacancy, ad or activity). Requires developer configuration in TypoScript and overrides TCA

Tip — redirects and visitor expectations When the outcome is Internal page, External URL or Domain record, the visitor is silently redirected away from the questionnaire page the moment they submit their last answer. They may not understand why they suddenly land on a different page.

Consider adding an Informational question as the very last step before the result. Use it to tell the visitor what will happen next — for example:

"Based on your answers we have found a suitable match for you. Click Next to view your result."

This gives the visitor context and makes the redirect feel intentional rather than unexpected.

Inline outcome fields 

Field Description
Headline Result headline
Body text Main result text (rich text — you can add links here)
CTA label Call-to-action button text (optional)
CTA link Call-to-action button target URL (optional)
Advice blocks Conditional content sections — see §8

Multiple links in a result? The result page supports one CTA button. If you need multiple links, add them as hyperlinks inside the Body text field.


13. Tips and rules of thumb 

Questionnaire structure 

  • Start with a sketch on paper of the question flow and the possible outcomes before building in the backend
  • Use descriptive internal titles on questionnaire and result page records — editors will thank you later
  • Store questionnaire records in a dedicated sysfolder to keep the list module tidy

Conditions 

  • Only reference questions that appear above the current question in the list
  • Test the branching by going through the questionnaire as a visitor — condition mistakes are easy to miss in the backend
  • Use the Start over button during testing to reset the session — it only appears after the first answer has been submitted

Result pages 

  • Always add a catch-all result as the last entry — without it, visitors who don't match any specific trigger will see an error message
  • For score-based questionnaires, make sure your score ranges cover all possible totals with no gaps

Scoring 

  • Scores can be positive or negative (e.g. +2 for a healthy choice, -1 for a risk factor)
  • Score is only displayed to the visitor if Show score on result is enabled in the plugin settings
  • Advice blocks with score-range conditions work regardless of whether the score is displayed

Reuse 

  • The same questionnaire record can be placed on multiple pages — useful for embedding the same tool in different contexts
  • Each placement has its own FlexForm settings, so you can show the score on one page but not another