Architecture
How the parts fit together
QuestionnaireController
│
├── QuestionnaireRepository → loads the configured questionnaire
├── SessionService → stores / retrieves visitor answers
├── ConditionEvaluatorService → filters questions to the 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 the advice blocks
└── DomainRecordResolverService → builds the redirect URL for record outcomes
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
All five actions are registered as uncached in ext_.
Service layer
All business logic lives in Classes/. The services are autowired
through Configuration/.
| Service | Responsibility |
|---|---|
Session | Reads and writes visitor answers through
Frontend. Key tx_, namespaced
per questionnaire uid |
Condition | Iterates all questions, evaluates each condition set, returns the visible questions in sort order |
Progress | calculate, get, get,
all operating on the visible questions |
Scoring | Traverses visible questions and their answer options, sums the
scores. Returns 0. when no scores are set |
Result | Iterates result pages top to bottom, returns the first match. Also
provides filter |
Domain | Builds the redirect URL for domain_ outcomes with
Uri, reading the TypoScript target and foreign_ from
$GLOBALS |
Result | Writes and retrieves a stored result, and generates its token |
Result | Composes and sends the result mail, HTML and plain text |
Mail | Wraps the Symfony rate limiter for the mail form |
Statistics | Increments the start and completion counters |
Condition evaluation
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. Multiple conditions are folded together in
list order:
result = evaluate(condition_1)
for each condition_2, condition_3, ...:
if operator == AND → result = result && evaluate(condition)
if operator == OR → result = result || evaluate(condition)
scale_range passes when the numeric answer for the reference question
satisfies the configured operator and threshold:
stored_value [operator] scale_value
e.g. 7 >= 5 → true
Supported operators are >=, <=, >, < and =. When the scale
question has not been answered yet the condition returns false, so 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
| Trigger | Match condition |
|---|---|
catch_ | Always |
score_ | score |
specific_ | A specific answer option uid appears in the session answers |
combination | Both score_ and specific_ match |
scale_ | The numeric answer for the trigger question is within
[trigger |
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)
All values are stored as strings; the
Session methods handle the
conversion.
Extending the extension
Adding a question type
- Add the new value to the
typeselect field inConfiguration/— both theTCA/ tx_ pnquestionnaire_ question. php itemsarray and thetypesarray. - Add the type constant to
Classes/.Domain/ Model/ Question. php - Create
Resources/.Private/ Partials/ Answer Types/ Your Type. html - Add the mapping to
$answerTypePartialMapinQuestionnaire.Controller:: question Action ()
No other changes are required.
Adding an outcome type
- Add the new value to the
outcome_typeselect field in the result page TCA. - Add the outcome constant to
Classes/.Domain/ Model/ Result Page. php - Handle it in
QuestionnaireorController:: handle Redirect Outcome () result.Action ()
Replacing a service
To replace or extend the result matching, create a service that extends or wraps
Result and reconfigure it in
Configuration/. The same applies to scoring: your service
only needs the same method signature.
public function calculateTotal(array $visibleQuestions, array $sessionAnswers): float
For purely visual changes no PHP is needed at all — copy the Fluid template into your site package, see Templates, styling and JavaScript.