prepare(" SELECT questionID, defaultText, type, orderIndex, configJson FROM question WHERE questionnaireID = :qn ORDER BY orderIndex "); $stmt->execute([':qn' => $questionnaireId]); $path = []; foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { $parts = explode('__', $row['questionID']); $shortId = end($parts); $config = json_decode($row['configJson'] ?? '{}', true) ?: []; $options = []; $ao = $pdo->prepare(" SELECT defaultText, nextQuestionId FROM answer_option WHERE questionID = :qid ORDER BY orderIndex "); $ao->execute([':qid' => $row['questionID']]); foreach ($ao->fetchAll(PDO::FETCH_ASSOC) as $opt) { $options[] = [ 'text' => $opt['defaultText'], 'nextQuestionId' => $opt['nextQuestionId'] ?? '', ]; } $symptoms = []; if (!empty($config['symptoms']) && is_array($config['symptoms'])) { $symptoms = $config['symptoms']; } $path[] = [ 'questionID' => $row['questionID'], 'shortId' => $shortId, 'type' => $row['type'] ?? '', 'storageKey' => $row['defaultText'] !== '' ? $row['defaultText'] : $shortId, 'options' => $options, 'symptoms' => $symptoms, 'isLastPage' => ($row['type'] ?? '') === 'last_page', ]; } return $path; } /** * @param array $answerMap keyed by full questionID * @return array reachable full questionIDs */ function qrb_reachable_question_ids(PDO $pdo, string $questionnaireId, array $answerMap, array $optionTextMap): array { $path = qrb_load_path_questions($pdo, $questionnaireId); $reachable = []; $shortToIndex = []; foreach ($path as $idx => $q) { $shortToIndex[$q['shortId']] = $idx; } $i = 0; $count = count($path); while ($i < $count) { $q = $path[$i]; if ($q['isLastPage']) { $i++; continue; } $reachable[$q['questionID']] = true; $nextTarget = qrb_resolve_next_target($q, $answerMap, $optionTextMap); if ($nextTarget !== null) { foreach ($path as $pq) { if ($pq['isLastPage'] && $pq['shortId'] === $nextTarget) { break 2; } } if (isset($shortToIndex[$nextTarget])) { $i = $shortToIndex[$nextTarget]; } else { $i++; } } elseif (qrb_has_branching_options($q) && !qrb_has_branch_answer($q, $answerMap, $optionTextMap)) { break; } else { $i++; } } return $reachable; } function qrb_resolve_next_target(array $q, array $answerMap, array $optionTextMap): ?string { $answer = $answerMap[$q['questionID']] ?? null; if ($answer === null) { return null; } $selectedText = qrb_answer_display_value($answer, $optionTextMap); if ($selectedText === null || $selectedText === '') { return null; } foreach ($q['options'] as $opt) { if ($opt['text'] === $selectedText && $opt['nextQuestionId'] !== '') { return $opt['nextQuestionId']; } } return null; } function qrb_has_branching_options(array $q): bool { foreach ($q['options'] as $opt) { if ($opt['nextQuestionId'] !== '') { return true; } } return false; } function qrb_has_branch_answer(array $q, array $answerMap, array $optionTextMap): bool { $answer = $answerMap[$q['questionID']] ?? null; if ($answer === null) { return false; } $v = qrb_answer_display_value($answer, $optionTextMap); return $v !== null && $v !== ''; } function qrb_answer_display_value(array $answer, array $optionTextMap): ?string { if (!empty($answer['answerOptionID']) && isset($optionTextMap[$answer['answerOptionID']])) { return $optionTextMap[$answer['answerOptionID']]; } if (isset($answer['freeTextValue']) && $answer['freeTextValue'] !== '') { return (string)$answer['freeTextValue']; } if (isset($answer['numericValue']) && $answer['numericValue'] !== null && $answer['numericValue'] !== '') { return (string)$answer['numericValue']; } return null; } function qrb_format_cell(array $answer, array $optionTextMap): string { $v = qrb_answer_display_value($answer, $optionTextMap); return $v ?? ''; }