*/ public function types(): array { return ['single_choice', 'multiple_choice', 'true_false', 'matching', 'sorting', 'drag_drop', 'hotspot', 'scenario', 'branching_scenario']; } /** @param array $configuration @return array */ public function validate(string $type, array $configuration): array { Validator::make(['type' => $type], ['type' => ['required', Rule::in($this->types())]])->validate(); $rules = match ($type) { 'single_choice', 'multiple_choice' => [ 'options' => ['required', 'array', 'min:2', 'max:12'], 'options.*.id' => ['required', 'string', 'distinct', 'max:80'], 'options.*.text' => ['required', 'string', 'max:1000'], 'options.*.correct' => ['required', 'boolean'], 'options.*.feedback' => ['nullable', 'string', 'max:2000'], ], 'true_false' => ['answer' => ['required', 'boolean'], 'feedback' => ['nullable', 'string', 'max:2000']], 'matching' => [ 'pairs' => ['required', 'array', 'min:2', 'max:12'], 'pairs.*.left' => ['required', 'string', 'max:500'], 'pairs.*.right' => ['required', 'string', 'max:500'], ], 'sorting' => ['items' => ['required', 'array', 'min:2', 'max:12'], 'items.*' => ['required', 'string', 'distinct', 'max:500']], 'drag_drop' => [ 'items' => ['required', 'array', 'min:2', 'max:16'], 'items.*.text' => ['required', 'string', 'max:500'], 'items.*.target' => ['required', 'string', 'max:160'], ], 'hotspot' => [ 'imageAssetId' => ['required', 'string'], 'hotspots' => ['required', 'array', 'min:1', 'max:12'], 'hotspots.*.x' => ['required', 'numeric', 'between:0,100'], 'hotspots.*.y' => ['required', 'numeric', 'between:0,100'], 'hotspots.*.radius' => ['required', 'numeric', 'between:1,40'], 'hotspots.*.label' => ['required', 'string', 'max:160'], 'hotspots.*.correct' => ['required', 'boolean'], ], 'scenario' => [ 'context' => ['required', 'string', 'max:5000'], 'choices' => ['required', 'array', 'min:2', 'max:8'], 'choices.*.text' => ['required', 'string', 'max:1000'], 'choices.*.score' => ['required', 'numeric', 'between:0,1'], 'choices.*.feedback' => ['required', 'string', 'max:3000'], ], 'branching_scenario' => [ 'startNodeId' => ['required', 'string'], 'nodes' => ['required', 'array', 'min:2', 'max:80'], 'nodes.*.id' => ['required', 'string', 'distinct', 'max:80'], 'nodes.*.type' => ['required', Rule::in(['scene', 'question', 'result'])], 'nodes.*.title' => ['required', 'string', 'max:240'], 'nodes.*.body' => ['nullable', 'string', 'max:5000'], 'nodes.*.choices' => ['nullable', 'array', 'max:12'], 'nodes.*.choices.*.text' => ['required_with:nodes.*.choices', 'string', 'max:1000'], 'nodes.*.choices.*.targetNodeId' => ['required_with:nodes.*.choices', 'string'], ], }; $validated = Validator::make($configuration, $rules)->validate(); if (in_array($type, ['single_choice', 'multiple_choice'], true)) { $correct = collect($validated['options'])->where('correct', true)->count(); if (($type === 'single_choice' && $correct !== 1) || ($type === 'multiple_choice' && $correct < 1)) { throw ValidationException::withMessages(['configuration.options' => [$type === 'single_choice' ? 'دقیقاً یک گزینه باید صحیح باشد.' : 'حداقل یک گزینه باید صحیح باشد.']]); } } if ($type === 'branching_scenario') { $this->validateGraph($validated); } return $validated; } /** @param array $graph */ private function validateGraph(array $graph): void { $nodes = collect($graph['nodes'])->keyBy('id'); if (! $nodes->has($graph['startNodeId'])) { throw ValidationException::withMessages(['configuration.startNodeId' => ['گره شروع وجود ندارد.']]); } foreach ($nodes as $node) { foreach ($node['choices'] ?? [] as $choice) { if (! $nodes->has($choice['targetNodeId'])) { throw ValidationException::withMessages(['configuration.nodes' => ["مسیر به گره ناموجود {$choice['targetNodeId']} اشاره می‌کند."]]); } } } $reachable = []; $queue = [$graph['startNodeId']]; while ($queue !== []) { $id = array_shift($queue); if (isset($reachable[$id])) { continue; } $reachable[$id] = true; foreach (($nodes[$id]['choices'] ?? []) as $choice) { $queue[] = $choice['targetNodeId']; } } $unreachable = $nodes->keys()->reject(fn (string $id): bool => isset($reachable[$id]))->values(); if ($unreachable->isNotEmpty()) { throw ValidationException::withMessages(['configuration.nodes' => ['گره‌های غیرقابل‌دسترسی: '.$unreachable->implode('، ')]]); } if (! $nodes->contains(fn (array $node): bool => $node['type'] === 'result')) { throw ValidationException::withMessages(['configuration.nodes' => ['سناریو باید حداقل یک گره نتیجه داشته باشد.']]); } } }