|
|
<?php
|
|
|
|
|
|
namespace App\Services\Survey;
|
|
|
|
|
|
use InvalidArgumentException;
|
|
|
|
|
|
/** Shared rules for server-owned question definitions, typed answers and legacy replies. */
|
|
|
class SurveySchema
|
|
|
{
|
|
|
public static function arrayValue($value): array
|
|
|
{
|
|
|
if (is_string($value)) {
|
|
|
$value = json_decode($value, true);
|
|
|
}
|
|
|
return is_array($value) ? $value : [];
|
|
|
}
|
|
|
|
|
|
public static function blank($value): bool
|
|
|
{
|
|
|
return $value === null || $value === [] || (is_string($value) && trim($value) === '');
|
|
|
}
|
|
|
|
|
|
public static function required(array $field): bool
|
|
|
{
|
|
|
return !empty($field['need_fill']) || in_array('required', explode(',', $field['rule'] ?? ''), true);
|
|
|
}
|
|
|
|
|
|
public static function question(array $q): array
|
|
|
{
|
|
|
$types = ['section', 'radio', 'checkbox', 'text', 'textarea', 'number', 'date', 'datetime', 'multi_dimension', 'file'];
|
|
|
if (!in_array($q['edit_input'] ?? '', $types, true)) {
|
|
|
throw new InvalidArgumentException('不支持的题型');
|
|
|
}
|
|
|
if (self::blank($q['name'] ?? null) || mb_strlen($q['name']) > 255) {
|
|
|
throw new InvalidArgumentException('请填写标题,且不超过255字');
|
|
|
}
|
|
|
if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $q['field'] ?? '')) {
|
|
|
throw new InvalidArgumentException('字段标识须以字母开头,仅包含字母、数字和下划线');
|
|
|
}
|
|
|
$q['config'] = self::arrayValue($q['config'] ?? []);
|
|
|
$userField = $q['config']['user_field'] ?? '';
|
|
|
if ($userField !== '' && (!in_array($userField, ['name', 'mobile', 'no', 'sex', 'birthday', 'email', 'company_name', 'company_position', 'idcard', 'plate'], true)
|
|
|
|| !in_array($q['edit_input'], ['text', 'textarea', 'date'], true)
|
|
|
|| ($q['edit_input'] === 'date' && $userField !== 'birthday'))) {
|
|
|
throw new InvalidArgumentException('学员字段仅支持个人信息字段,日期题只能关联出生日期');
|
|
|
}
|
|
|
|
|
|
if ($q['edit_input'] === 'section') {
|
|
|
$q['rule'] = '';
|
|
|
$q['need_fill'] = false;
|
|
|
$q['allow_input'] = false;
|
|
|
$q['select_item'] = [];
|
|
|
$q['dimensions'] = [];
|
|
|
$q['config'] = [];
|
|
|
return $q;
|
|
|
}
|
|
|
if ($q['edit_input'] === 'multi_dimension') {
|
|
|
$q['dimensions'] = self::arrayValue($q['dimensions'] ?? []);
|
|
|
if (!$q['dimensions']) {
|
|
|
throw new InvalidArgumentException('请至少添加一个维度');
|
|
|
}
|
|
|
$fields = [];
|
|
|
foreach ($q['dimensions'] as &$dim) {
|
|
|
$dim['edit_input'] = $dim['edit_input'] ?? 'radio';
|
|
|
if (!in_array($dim['edit_input'], ['radio', 'number'], true)) {
|
|
|
throw new InvalidArgumentException('维度仅支持单选或数字输入');
|
|
|
}
|
|
|
if (self::blank($dim['name'] ?? null) || !preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $dim['field'] ?? '')) {
|
|
|
throw new InvalidArgumentException('请填写维度名称和合法的字段标识');
|
|
|
}
|
|
|
if (in_array($dim['field'], $fields, true)) {
|
|
|
throw new InvalidArgumentException('维度字段标识不能重复');
|
|
|
}
|
|
|
$fields[] = $dim['field'];
|
|
|
$dim['need_fill'] = self::required($q);
|
|
|
$dim['rule'] = $dim['need_fill'] ? 'required' : '';
|
|
|
$dim['config'] = self::arrayValue($dim['config'] ?? []);
|
|
|
$dim = self::field($dim);
|
|
|
}
|
|
|
unset($dim);
|
|
|
$q['select_item'] = [];
|
|
|
return $q;
|
|
|
}
|
|
|
return self::field($q);
|
|
|
}
|
|
|
|
|
|
public const FILE_EXTENSIONS = ['zip', 'rar', 'ppt', 'pptx', 'xls', 'xlsx', 'doc', 'docx', 'png', 'gif', 'jpg', 'jpeg', 'pdf', 'mp4'];
|
|
|
|
|
|
public static function fileExtensions(array $config): array
|
|
|
{
|
|
|
$extensions = $config['allowed_extensions'] ?? self::FILE_EXTENSIONS;
|
|
|
if (!is_array($extensions) || !$extensions) throw new InvalidArgumentException('请至少选择一种允许上传的文件格式');
|
|
|
foreach ($extensions as $extension) {
|
|
|
if (!is_string($extension) || !in_array(strtolower($extension), self::FILE_EXTENSIONS, true)) throw new InvalidArgumentException('不支持的上传文件格式');
|
|
|
}
|
|
|
return array_values(array_unique(array_map('strtolower', $extensions)));
|
|
|
}
|
|
|
|
|
|
private static function field(array $field): array
|
|
|
{
|
|
|
$config = $field['config'] ?? [];
|
|
|
if ($field['edit_input'] === 'file') {
|
|
|
$limit = $config['max_files'] ?? 1;
|
|
|
if (filter_var($limit, FILTER_VALIDATE_INT) === false || $limit < 1 || $limit > 10) throw new InvalidArgumentException('上传数量须为1至10');
|
|
|
$field['config']['max_files'] = (int) $limit;
|
|
|
$field['config']['allowed_extensions'] = self::fileExtensions($config);
|
|
|
}
|
|
|
if (in_array($field['edit_input'], ['radio', 'checkbox'], true)) {
|
|
|
$options = self::arrayValue($field['select_item'] ?? []);
|
|
|
if (!$options) {
|
|
|
throw new InvalidArgumentException('选择题至少需要一个选项');
|
|
|
}
|
|
|
$keys = [];
|
|
|
$labels = [];
|
|
|
foreach ($options as &$option) {
|
|
|
if (self::blank($option['key'] ?? null) || self::blank($option['value'] ?? null)) {
|
|
|
throw new InvalidArgumentException('请填写选项标识和文字');
|
|
|
}
|
|
|
$option['key'] = (string) $option['key'];
|
|
|
$option['value'] = (string) $option['value'];
|
|
|
if (in_array($option['key'], $keys, true) || in_array($option['value'], $labels, true)) {
|
|
|
throw new InvalidArgumentException('同一题的选项标识和文字不能重复');
|
|
|
}
|
|
|
$keys[] = $option['key'];
|
|
|
$labels[] = $option['value'];
|
|
|
if (!empty($config['is_score'])) {
|
|
|
if ($field['edit_input'] !== 'radio' || !isset($option['score']) || !is_numeric($option['score']) || !is_finite((float) $option['score'])) {
|
|
|
throw new InvalidArgumentException('评分仅支持单选,每个选项须填写有效分值');
|
|
|
}
|
|
|
$option['score'] = (float) $option['score'];
|
|
|
} else {
|
|
|
unset($option['score']);
|
|
|
}
|
|
|
}
|
|
|
unset($option);
|
|
|
if (!self::blank($config['other_key'] ?? null) && !in_array((string) $config['other_key'], $keys, true)) {
|
|
|
throw new InvalidArgumentException('其他选项必须属于本题选项');
|
|
|
}
|
|
|
if ($field['edit_input'] === 'checkbox' && isset($config['max_choices'])) {
|
|
|
$max = $config['max_choices'];
|
|
|
if (filter_var($max, FILTER_VALIDATE_INT) === false || $max < 0 || $max > count($options)) throw new InvalidArgumentException('最多选择项数须为0至选项数量之间的整数');
|
|
|
}
|
|
|
$notes = $config['option_notes'] ?? [];
|
|
|
if (!is_array($notes)) throw new InvalidArgumentException('选项附加输入框配置不正确');
|
|
|
foreach ($notes as $key => $note) {
|
|
|
if (!in_array((string) $key, $keys, true) || !is_array($note)) throw new InvalidArgumentException('附加输入框必须属于本题选项');
|
|
|
if ((string) $key === (string) ($config['other_key'] ?? '')) throw new InvalidArgumentException('同一选项不能重复设置补充输入框');
|
|
|
if (!is_string($note['placeholder'] ?? '') || mb_strlen($note['placeholder'] ?? '') > 255) throw new InvalidArgumentException('输入框提示文字不能超过255字');
|
|
|
$field['config']['option_notes'][$key]['required'] = !empty($note['required']);
|
|
|
}
|
|
|
if (!is_string($config['other_placeholder'] ?? '') || mb_strlen($config['other_placeholder'] ?? '') > 255) throw new InvalidArgumentException('补充输入框提示文字不能超过255字');
|
|
|
$field['select_item'] = $options;
|
|
|
}
|
|
|
if ($field['edit_input'] === 'number') {
|
|
|
foreach (['min', 'max'] as $key) {
|
|
|
if (isset($config[$key]) && $config[$key] !== '' && (!is_numeric($config[$key]) || !is_finite((float) $config[$key]))) {
|
|
|
throw new InvalidArgumentException('数字范围必须为有效数字');
|
|
|
}
|
|
|
}
|
|
|
if (isset($config['min'], $config['max']) && $config['min'] !== '' && $config['max'] !== '' && $config['min'] > $config['max']) {
|
|
|
throw new InvalidArgumentException('最小值不能大于最大值');
|
|
|
}
|
|
|
}
|
|
|
return $field;
|
|
|
}
|
|
|
|
|
|
public static function hash(array $questions): string
|
|
|
{
|
|
|
$fields = ['id', 'name', 'field', 'edit_input', 'sort', 'help', 'rule', 'need_fill', 'allow_input', 'select_item', 'dimensions', 'config', 'course_content_id'];
|
|
|
return hash('sha256', json_encode(array_map(function ($q) use ($fields) {
|
|
|
return array_intersect_key($q, array_flip($fields));
|
|
|
}, $questions), JSON_UNESCAPED_UNICODE));
|
|
|
}
|
|
|
|
|
|
/** Legacy data is a list of question snapshots or an id -> value map. */
|
|
|
public static function answerMap($data): array
|
|
|
{
|
|
|
$map = [];
|
|
|
foreach (self::arrayValue($data) as $key => $raw) {
|
|
|
if (is_array($raw) && isset($raw['id'])) {
|
|
|
$map[(string) $raw['id']] = $raw;
|
|
|
} else {
|
|
|
$map[(string) $key] = ['value' => $raw];
|
|
|
}
|
|
|
}
|
|
|
return $map;
|
|
|
}
|
|
|
|
|
|
// Input must be in the same sort/id order used for questionnaire rendering.
|
|
|
public static function validateConditions(array $questions): void
|
|
|
{
|
|
|
$preceding = [];
|
|
|
foreach ($questions as $q) {
|
|
|
$condition = $q['config']['visible_when'] ?? null;
|
|
|
if ($condition !== null) {
|
|
|
if (!is_array($condition) || !is_string($condition['field'] ?? null) || !is_scalar($condition['value'] ?? null)) throw new InvalidArgumentException('显示条件格式不正确');
|
|
|
$parent = $preceding[$condition['field']] ?? null;
|
|
|
if ($q['edit_input'] === 'section' || !$parent || !in_array($parent['edit_input'], ['radio', 'checkbox'], true)) throw new InvalidArgumentException($q['name'].':显示条件只能引用本题之前的单选或多选题,请先调整条件');
|
|
|
$keys = array_map(fn ($o) => (string) $o['key'], self::arrayValue($parent['select_item'] ?? []));
|
|
|
if (!in_array((string) $condition['value'], $keys, true)) throw new InvalidArgumentException($q['name'].':显示条件引用的选项不存在,请先调整条件');
|
|
|
}
|
|
|
$preceding[$q['field']] = $q;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public static function visible(array $q, array $questions, array $map, bool $keyed = true, array $visited = []): bool
|
|
|
{
|
|
|
$condition = $q['config']['visible_when'] ?? null;
|
|
|
if (!$condition) return true;
|
|
|
$field = $q['field'] ?? (string) $q['id'];
|
|
|
if (in_array($field, $visited, true)) return false;
|
|
|
$visited[] = $field;
|
|
|
foreach ($questions as $parent) {
|
|
|
if (($parent['field'] ?? '') !== ($condition['field'] ?? '')) continue;
|
|
|
if (!self::visible($parent, $questions, $map, $keyed, $visited)) return false;
|
|
|
$value = $map[(string) $parent['id']]['value'] ?? null;
|
|
|
$keys = self::selectedKeys($parent, $value, $keyed);
|
|
|
return in_array((string) ($condition['value'] ?? ''), $keys, true);
|
|
|
}
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
public static function normalize(array $questions, $data, bool $keyed = false): array
|
|
|
{
|
|
|
$map = self::answerMap($data);
|
|
|
$result = [];
|
|
|
foreach ($questions as $q) {
|
|
|
$raw = $map[(string) $q['id']] ?? [];
|
|
|
$q['other_text'] = '';
|
|
|
$q['option_texts'] = [];
|
|
|
if (!self::visible($q, $questions, $map, $keyed)) {
|
|
|
$q['value'] = null;
|
|
|
$q['hidden'] = true;
|
|
|
$result[] = $q;
|
|
|
continue;
|
|
|
}
|
|
|
$q['hidden'] = false;
|
|
|
if ($q['edit_input'] === 'section') {
|
|
|
$q['value'] = null;
|
|
|
} elseif ($q['edit_input'] === 'multi_dimension') {
|
|
|
$values = self::arrayValue($raw['value'] ?? []);
|
|
|
$q['value'] = [];
|
|
|
foreach (self::arrayValue($q['dimensions'] ?? []) as $dim) {
|
|
|
$dim['edit_input'] = $dim['edit_input'] ?? 'radio';
|
|
|
$dim['need_fill'] = self::required($q);
|
|
|
$dim['rule'] = $dim['need_fill'] ? 'required' : '';
|
|
|
$q['value'][$dim['field']] = self::answer($dim, $values[$dim['field']] ?? null, $keyed, $q['name'] . ' / ' . $dim['name']);
|
|
|
}
|
|
|
} else {
|
|
|
$q['value'] = self::answer($q, $raw['value'] ?? null, $keyed, $q['name']);
|
|
|
$config = self::arrayValue($q['config'] ?? []);
|
|
|
foreach ($config['option_notes'] ?? [] as $key => $settings) {
|
|
|
if (!in_array((string) $key, array_map('strval', (array) $q['value']), true)) continue;
|
|
|
$text = $raw['option_texts'][$key] ?? '';
|
|
|
if (!is_scalar($text) || mb_strlen((string) $text) > 10000) throw new InvalidArgumentException($q['name'].':补充文字格式不正确或过长');
|
|
|
$text = trim((string) $text);
|
|
|
if (!empty($settings['required']) && $text === '') throw new InvalidArgumentException($q['name'].':请填写补充说明');
|
|
|
$q['option_texts'][$key] = $text;
|
|
|
}
|
|
|
$otherKey = $config['other_key'] ?? null;
|
|
|
$selectedOther = !self::blank($otherKey) && in_array((string) $otherKey, array_map('strval', (array) $q['value']), true);
|
|
|
// Existing questionnaires allowed a separate free-text response without an "other" option.
|
|
|
$legacyOther = !empty($q['allow_input']) && self::blank($otherKey);
|
|
|
if ($selectedOther || $legacyOther) {
|
|
|
$text = $raw['other_text'] ?? $raw['allow_input_text'] ?? '';
|
|
|
if (!is_scalar($text) || mb_strlen((string) $text) > 10000) {
|
|
|
throw new InvalidArgumentException($q['name'] . ':补充文字格式不正确或过长');
|
|
|
}
|
|
|
$q['other_text'] = trim((string) $text);
|
|
|
if ($selectedOther && !empty($config['other_required']) && $q['other_text'] === '') {
|
|
|
throw new InvalidArgumentException($q['name'] . ':请填写其他内容');
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
// Preserve the server definition; never store a client-supplied label, score or dimension.
|
|
|
$result[] = $q;
|
|
|
}
|
|
|
return $result;
|
|
|
}
|
|
|
|
|
|
private static function answer(array $field, $value, bool $keyed, string $label)
|
|
|
{
|
|
|
$type = $field['edit_input'];
|
|
|
if (self::blank($value)) {
|
|
|
if (self::required($field)) {
|
|
|
throw new InvalidArgumentException($label . ':请填写此项');
|
|
|
}
|
|
|
return $type === 'checkbox' ? [] : null;
|
|
|
}
|
|
|
$config = self::arrayValue($field['config'] ?? []);
|
|
|
if (in_array($type, ['radio', 'checkbox'], true)) {
|
|
|
$values = $type === 'checkbox' ? (is_array($value) ? $value : explode(',', (string) $value)) : [$value];
|
|
|
$keys = [];
|
|
|
foreach ($values as $v) {
|
|
|
$match = null;
|
|
|
foreach (self::arrayValue($field['select_item'] ?? []) as $option) {
|
|
|
if (is_scalar($v) && (string) $v === (string) ($keyed ? $option['key'] : $option['value'])) {
|
|
|
$match = (string) $option['key'];
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
if ($match === null) {
|
|
|
throw new InvalidArgumentException($label . ':选项已变更,请刷新问卷');
|
|
|
}
|
|
|
$keys[] = $match;
|
|
|
}
|
|
|
$keys = array_values(array_unique($keys));
|
|
|
if ($type === 'checkbox' && !empty($config['max_choices']) && count($keys) > (int) $config['max_choices']) throw new InvalidArgumentException($label.':最多选择'.$config['max_choices'].'项');
|
|
|
return $type === 'checkbox' ? $keys : $keys[0];
|
|
|
}
|
|
|
if ($type === 'file') {
|
|
|
if (!is_array($value) || count($value) > ($config['max_files'] ?? 1)) throw new InvalidArgumentException($label.':上传文件数量不正确');
|
|
|
$files = [];
|
|
|
foreach ($value as $item) {
|
|
|
$id = is_array($item) ? ($item['id'] ?? null) : $item;
|
|
|
if (!is_scalar($id) || !ctype_digit((string) $id)) throw new InvalidArgumentException($label.':文件无效,请重新上传');
|
|
|
$file = \App\Models\Upload::whereKey($id)->where('creator_id', auth('mobile')->id())->first();
|
|
|
if (!$file || $file->size >= 50 * 1024 * 1024 || !in_array(strtolower($file->extension), ['zip','rar','ppt','pptx','xls','xlsx','doc','docx','png','gif','jpg','jpeg','pdf','mp4'], true)) throw new InvalidArgumentException($label.':文件无效或不属于当前用户,请重新上传');
|
|
|
if (!in_array(strtolower($file->extension), self::fileExtensions($config), true)) throw new InvalidArgumentException($label.':只允许上传 '.implode('、', self::fileExtensions($config)).' 格式');
|
|
|
$files[(string) $file->id] = ['id' => $file->id, 'original_name' => $file->original_name, 'url' => $file->url, 'extension' => $file->extension, 'size' => $file->size];
|
|
|
}
|
|
|
return array_values($files);
|
|
|
}
|
|
|
if ($type === 'number') {
|
|
|
if (!is_scalar($value) || !is_numeric($value) || !is_finite((float) $value)) {
|
|
|
throw new InvalidArgumentException($label . ':请输入有效数字');
|
|
|
}
|
|
|
$number = (float) $value;
|
|
|
if (!empty($config['integer']) && floor($number) !== $number) {
|
|
|
throw new InvalidArgumentException($label . ':请输入整数');
|
|
|
}
|
|
|
foreach (['min', 'max'] as $bound) {
|
|
|
if (isset($config[$bound]) && $config[$bound] !== '' && ($bound === 'min' ? $number < $config[$bound] : $number > $config[$bound])) {
|
|
|
throw new InvalidArgumentException($label . ':数字超出允许范围');
|
|
|
}
|
|
|
}
|
|
|
return $number;
|
|
|
}
|
|
|
if (!is_scalar($value) || mb_strlen((string) $value) > 10000) {
|
|
|
throw new InvalidArgumentException($label . ':答案格式不正确或超过10000字');
|
|
|
}
|
|
|
$value = trim((string) $value);
|
|
|
if (in_array($type, ['date', 'datetime'], true)) {
|
|
|
$format = $type === 'date' ? 'Y-m-d' : 'Y-m-d H:i:s';
|
|
|
$date = \DateTime::createFromFormat('!' . $format, $value);
|
|
|
if (!$date || $date->format($format) !== $value) {
|
|
|
throw new InvalidArgumentException($label . ':日期格式不正确');
|
|
|
}
|
|
|
}
|
|
|
foreach (explode(',', $field['rule'] ?? '') as $rule) {
|
|
|
if (($rule === 'integer' && !preg_match('/^-?\d+$/', $value)) ||
|
|
|
($rule === 'numeric' && !is_numeric($value)) ||
|
|
|
($rule === 'email' && !filter_var($value, FILTER_VALIDATE_EMAIL)) ||
|
|
|
($rule === 'mobile' && !preg_match('/^1[3-9]\d{9}$/', $value)) ||
|
|
|
($rule === 'idcard' && !preg_match('/^(\d{15}|\d{17}[\dXx])$/', $value))) {
|
|
|
throw new InvalidArgumentException($label . ':格式不符合校验规则');
|
|
|
}
|
|
|
}
|
|
|
return $value;
|
|
|
}
|
|
|
|
|
|
public static function display(array $field, $value, bool $keyed): string
|
|
|
{
|
|
|
if (self::blank($value)) {
|
|
|
return '未填写';
|
|
|
}
|
|
|
$type = $field['edit_input'] ?? 'radio';
|
|
|
if ($type === 'file') return implode('、', array_column(is_array($value) ? $value : [], 'original_name'));
|
|
|
if (in_array($type, ['radio', 'checkbox'], true)) {
|
|
|
$values = is_array($value) ? $value : ($type === 'checkbox' ? explode(',', (string) $value) : [$value]);
|
|
|
$labels = [];
|
|
|
foreach ($values as $v) {
|
|
|
$label = is_scalar($v) ? (string) $v : '';
|
|
|
foreach (self::arrayValue($field['select_item'] ?? []) as $option) {
|
|
|
if ($label === (string) ($keyed ? $option['key'] : $option['value'])) {
|
|
|
$label = (string) $option['value'];
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
$labels[] = $label;
|
|
|
}
|
|
|
return implode('、', $labels);
|
|
|
}
|
|
|
return is_scalar($value) ? (string) $value : '';
|
|
|
}
|
|
|
|
|
|
private static function selectedKeys(array $field, $value, bool $keyed): array
|
|
|
{
|
|
|
$type = $field['edit_input'] ?? 'radio';
|
|
|
if (!in_array($type, ['radio', 'checkbox'], true) || self::blank($value)) return [];
|
|
|
$values = is_array($value) ? $value : ($type === 'checkbox' ? explode(',', (string) $value) : [$value]);
|
|
|
$keys = [];
|
|
|
foreach (self::arrayValue($field['select_item'] ?? []) as $option) {
|
|
|
foreach ($values as $selected) {
|
|
|
if (is_scalar($selected) && (string) $selected === (string) ($keyed ? $option['key'] : $option['value'])) {
|
|
|
$keys[] = (string) $option['key'];
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return $keys;
|
|
|
}
|
|
|
|
|
|
public static function replyQuestions(array $current, $data, $snapshot): array
|
|
|
{
|
|
|
$snapshot = self::arrayValue($snapshot);
|
|
|
$map = self::answerMap($data);
|
|
|
$questions = $snapshot['questions'] ?? $current;
|
|
|
$keyed = ($snapshot['version'] ?? 0) >= 2;
|
|
|
$result = [];
|
|
|
foreach ($questions as $q) {
|
|
|
$raw = $map[(string) $q['id']] ?? [];
|
|
|
if (!$keyed && isset($raw['edit_input'], $raw['name'])) {
|
|
|
$q = array_replace($q, $raw); // legacy submissions already carried their definition
|
|
|
}
|
|
|
$q['value'] = $raw['value'] ?? null;
|
|
|
$q['hidden'] = !self::visible($q, $questions, $map, $keyed);
|
|
|
$q['option_texts'] = self::arrayValue($raw['option_texts'] ?? []);
|
|
|
$q['other_text'] = $raw['other_text'] ?? $raw['allow_input_text'] ?? '';
|
|
|
$q['display_value'] = self::display($q, $q['value'], $keyed);
|
|
|
$q['selected_keys'] = self::selectedKeys($q, $q['value'], $keyed);
|
|
|
if ($q['edit_input'] === 'multi_dimension') {
|
|
|
$q['value'] = self::arrayValue($q['value']);
|
|
|
$q['dimensions'] = self::arrayValue($q['dimensions'] ?? []);
|
|
|
foreach ($q['dimensions'] as &$dim) {
|
|
|
$dim['edit_input'] = $dim['edit_input'] ?? 'radio';
|
|
|
$dim['display_value'] = self::display($dim, $q['value'][$dim['field']] ?? null, $keyed);
|
|
|
$dim['selected_keys'] = self::selectedKeys($dim, $q['value'][$dim['field']] ?? null, $keyed);
|
|
|
}
|
|
|
unset($dim);
|
|
|
}
|
|
|
$result[] = $q;
|
|
|
}
|
|
|
// Preserve deleted questions in older answers that carried full definitions.
|
|
|
if (!$keyed) {
|
|
|
$seen = array_column($result, 'id');
|
|
|
foreach ($map as $raw) {
|
|
|
if (isset($raw['id'], $raw['name'], $raw['edit_input']) && !in_array($raw['id'], $seen)) {
|
|
|
$result = array_merge($result, self::replyQuestions([$raw], [$raw], ['questions' => [$raw]]));
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return $result;
|
|
|
}
|
|
|
}
|