New_Micro_Learning/backend/app/Modules/Assessments/Application/QuestionSchema.php

118 خطوط
6.0 KiB
PHP

<?php
namespace App\Modules\Assessments\Application;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
final class QuestionSchema
{
/** @return list<string> */
public function types(): array
{
return ['single_choice', 'multiple_choice', 'true_false', 'matching', 'sorting', 'drag_drop', 'hotspot', 'scenario', 'branching_scenario'];
}
/** @param array<string, mixed> $configuration @return array<string, mixed> */
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<string, mixed> $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' => ['سناریو باید حداقل یک گره نتیجه داشته باشد.']]);
}
}
}