Passed
Pull Request — master (#7027)
by
unknown
12:49 queued 03:07
created

QuizMetaExport   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 156
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 69
c 1
b 0
f 1
dl 0
loc 156
rs 10
wmc 28

5 Methods

Rating   Name   Duplication   Size   Complexity  
A ensureDir() 0 4 3
A toArray() 0 10 3
D export() 0 102 17
A writeJson() 0 8 2
A unwrap() 0 7 3
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CourseBundle\Component\CourseCopy\Moodle\Activities;
8
9
/**
10
 * Dumps raw quiz data (quiz, questions, answers) as JSON sidecar files under chamilo/quiz/.
11
 * No mapping, no transformation: we persist exactly what the builder already prepared
12
 * inside $this->course->resources for the selected quiz.
13
 */
14
class QuizMetaExport extends ActivityExport
15
{
16
    /**
17
     * Export JSON files for the given quiz:
18
     *   chamilo/quiz/quiz_{moduleId}/quiz.json
19
     *   chamilo/quiz/quiz_{moduleId}/questions.json
20
     *   chamilo/quiz/quiz_{moduleId}/answers.json
21
     */
22
    public function export(int $activityId, string $exportDir, int $moduleId, int $sectionId): void
23
    {
24
        // Build destination folder
25
        $baseDir = rtrim($exportDir, '/').'/chamilo/quiz/quiz_'.$moduleId;
26
        $this->ensureDir($baseDir);
27
28
        // Resolve quiz bag (accept constant or string key)
29
        $quizBag =
30
            $this->course->resources[\defined('RESOURCE_QUIZ') ? RESOURCE_QUIZ : 'quiz'] ??
31
            $this->course->resources['quiz'] ?? [];
32
33
        if (empty($quizBag[$activityId])) {
34
            @error_log('[QuizMetaExport] WARN quiz not found in resources: id='.$activityId);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for error_log(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

34
            /** @scrutinizer ignore-unhandled */ @error_log('[QuizMetaExport] WARN quiz not found in resources: id='.$activityId);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
35
            return;
36
        }
37
38
        // Unwrap quiz wrapper → raw payload already prepared by build_quizzes()
39
        $quizWrap = $quizBag[$activityId];
40
        $quizObj  = $this->unwrap($quizWrap);          // stdClass payload
41
        $quizArr  = $this->toArray($quizObj);          // array payload for JSON
42
43
        // Keep minimal context references (section/module) for traceability
44
        $quizArr['_context'] = [
45
            'module_id'  => (int) $moduleId,
46
            'section_id' => (int) $sectionId,
47
        ];
48
49
        // Persist quiz.json
50
        $this->writeJson($baseDir.'/quiz.json', ['quiz' => $quizArr]);
51
52
        // Collect questions for this quiz (preserve order if available)
53
        $questionIds = [];
54
        $orders      = [];
55
56
        if (isset($quizArr['question_ids']) && \is_array($quizArr['question_ids'])) {
57
            $questionIds = array_map('intval', $quizArr['question_ids']);
58
        }
59
        if (isset($quizArr['question_orders']) && \is_array($quizArr['question_orders'])) {
60
            $orders = array_map('intval', $quizArr['question_orders']);
61
        }
62
63
        $qBag =
64
            $this->course->resources[\defined('RESOURCE_QUIZQUESTION') ? RESOURCE_QUIZQUESTION : 'Exercise_Question']
65
            ?? $this->course->resources['Exercise_Question']
66
            ?? [];
67
68
        // Build ordered questions array (raw, with their nested answers)
69
        $questions = [];
70
        $answersFlat = [];
71
        $orderMap = [];
72
73
        // If we have question_orders aligned with question_ids, build an order map
74
        if (!empty($questionIds) && !empty($orders) && \count($questionIds) === \count($orders)) {
75
            foreach ($questionIds as $idx => $qid) {
76
                $orderMap[(int) $qid] = (int) $orders[$idx];
77
            }
78
        }
79
80
        foreach ($questionIds as $qid) {
81
            if (!isset($qBag[$qid])) {
82
                continue;
83
            }
84
            $qWrap = $qBag[$qid];
85
            $qObj  = $this->unwrap($qWrap);     // stdClass payload from build_quiz_questions()
86
            $qArr  = $this->toArray($qObj);
87
88
            // Attach quiz reference
89
            $qArr['_links']['quiz_id'] = (int) $activityId;
90
91
            // Optional: attach explicit question_id for clarity
92
            $qArr['id'] = $qArr['id'] ?? (int) ($qWrap->source_id ?? $qid);
93
94
            // Flatten answers to a standalone list (still keep them nested in question)
95
            $answers = [];
96
            if (isset($qArr['answers']) && \is_array($qArr['answers'])) {
97
                foreach ($qArr['answers'] as $ans) {
98
                    $answers[] = $ans;
99
100
                    $answersFlat[] = [
101
                        'quiz_id'     => (int) $activityId,
102
                        'question_id' => (int) $qArr['id'],
103
                        // Persist raw answer data verbatim
104
                        'data'        => $ans,
105
                    ];
106
                }
107
            }
108
109
            // Preserve original order if available; fallback to question "position"
110
            $qArr['_order'] = $orderMap[$qid] ?? (int) ($qArr['position'] ?? 0);
111
            $questions[] = $qArr;
112
        }
113
114
        // Sort questions by _order asc (stable)
115
        usort($questions, static function (array $a, array $b): int {
116
            return ($a['_order'] ?? 0) <=> ($b['_order'] ?? 0);
117
        });
118
119
        // Persist questions.json (full raw)
120
        $this->writeJson($baseDir.'/questions.json', ['questions' => $questions]);
121
122
        // Persist answers.json (flat list)
123
        $this->writeJson($baseDir.'/answers.json', ['answers' => $answersFlat]);
124
    }
125
126
    /** Ensure directory exists (recursive). */
127
    private function ensureDir(string $dir): void
128
    {
129
        if (!is_dir($dir) && !@mkdir($dir, api_get_permissions_for_new_directories(), true)) {
130
            @error_log('[QuizMetaExport] ERROR mkdir failed: '.$dir);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for error_log(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

130
            /** @scrutinizer ignore-unhandled */ @error_log('[QuizMetaExport] ERROR mkdir failed: '.$dir);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
131
        }
132
    }
133
134
    /** Write pretty JSON with utf8/slashes preserved. */
135
    private function writeJson(string $file, array $data): void
136
    {
137
        $json = json_encode(
138
            $data,
139
            JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
140
        );
141
        if (false === @file_put_contents($file, (string) $json)) {
142
            @error_log('[QuizMetaExport] ERROR writing file: '.$file);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for error_log(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

142
            /** @scrutinizer ignore-unhandled */ @error_log('[QuizMetaExport] ERROR writing file: '.$file);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
143
        }
144
    }
145
146
    /**
147
     * Unwraps a legacy wrapper produced by mkLegacyItem() into its raw stdClass payload.
148
     * We prefer ->obj if present; otherwise return the object itself.
149
     */
150
    private function unwrap(object $wrap): object
151
    {
152
        // Some wrappers keep original payload at ->obj
153
        if (isset($wrap->obj) && \is_object($wrap->obj)) {
154
            return $wrap->obj;
155
        }
156
        return $wrap;
157
    }
158
159
    /** Deep convert stdClass/objects to arrays. */
160
    private function toArray($value)
161
    {
162
        if (\is_array($value)) {
163
            return array_map([$this, 'toArray'], $value);
164
        }
165
        if (\is_object($value)) {
166
            // Convert to array and recurse
167
            return array_map([$this, 'toArray'], get_object_vars($value));
168
        }
169
        return $value;
170
    }
171
}
172