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
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.

Service layer 

All business logic lives in Classes/Service/. The services are autowired through Configuration/Services.yaml.

Service Responsibility
SessionService Reads and writes visitor answers through FrontendUserAuthentication. Key tx_pnquestionnaire, namespaced per questionnaire uid
ConditionEvaluatorService Iterates all questions, evaluates each condition set, returns the visible questions in sort order
ProgressService calculate(), getNextQuestionUid(), getPreviousQuestionUid(), all operating on the visible questions
ScoringService Traverses visible questions and their answer options, sums the scores. Returns 0.0 when no scores are set
ResultResolverService Iterates result pages top to bottom, returns the first match. Also provides filterAdviceBlocks()
DomainRecordResolverService Builds the redirect URL for domain_record outcomes with UriBuilder, reading the TypoScript target and foreign_table from $GLOBALS['TCA']
ResultStorageService Writes and retrieves a stored result, and generates its token
ResultMailService Composes and sends the result mail, HTML and plain text
MailRateLimitService Wraps the Symfony rate limiter for the mail form
StatisticsService 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)
Copied!

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
Copied!

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
Copied!
Trigger Match condition
catch_all Always
score_range scoreMin <= totalScore <= scoreMax
specific_answer A specific answer option uid appears in the session answers
combination Both score_range and specific_answer match
scale_answer The numeric answer for the trigger question is within [triggerScaleMin, triggerScaleMax]

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.

Extending the extension 

Adding a question type 

  1. Add the new 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 to $answerTypePartialMap in QuestionnaireController::questionAction() .

No other changes are required.

Adding an outcome type 

  1. Add the new 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 it in QuestionnaireController::handleRedirectOutcome() or resultAction() .

Replacing a service 

To replace or extend the result matching, create a service that extends or wraps ResultResolverService and reconfigure it in Configuration/Services.yaml. The same applies to scoring: your service only needs the same method signature.

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

For purely visual changes no PHP is needed at all — copy the Fluid template into your site package, see Templates, styling and JavaScript.