Passed
Pull Request — master (#7159)
by
unknown
09:00
created

QuizXapianIndexer::indexQuiz()   C

Complexity

Conditions 12
Paths 194

Size

Total Lines 102
Code Lines 60

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 60
c 1
b 0
f 0
nc 194
nop 1
dl 0
loc 102
rs 5.8193

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Search\Xapian;
8
9
use Chamilo\CoreBundle\Entity\ResourceLink;
10
use Chamilo\CoreBundle\Entity\ResourceNode;
11
use Chamilo\CoreBundle\Entity\SearchEngineRef;
12
use Chamilo\CoreBundle\Settings\SettingsManager;
13
use Chamilo\CourseBundle\Entity\CQuiz;
14
use Doctrine\ORM\EntityManagerInterface;
15
16
/**
17
 * Handles Xapian indexing for quizzes (exercises).
18
 */
19
final class QuizXapianIndexer
20
{
21
    public function __construct(
22
        private readonly XapianIndexService $xapianIndexService,
23
        private readonly EntityManagerInterface $em,
24
        private readonly SettingsManager $settingsManager,
25
    ) {
26
    }
27
28
    /**
29
     * Index or reindex a quiz.
30
     *
31
     * @return int|null Xapian document id or null when indexing is skipped
32
     */
33
    public function indexQuiz(CQuiz $quiz): ?int
34
    {
35
        $resourceNode = $quiz->getResourceNode();
36
37
        error_log('[Xapian] indexQuiz: start for quiz id='.(string) $quiz->getIid()
38
            .', resource_node_id='.($resourceNode ? $resourceNode->getId() : 'null')
39
        );
40
41
        // Check global setting
42
        $enabled = (string) $this->settingsManager->getSetting('search.search_enabled', true);
43
        if ($enabled !== 'true') {
44
            error_log('[Xapian] indexQuiz: search is disabled, skipping');
45
            return null;
46
        }
47
48
        if (!$resourceNode instanceof ResourceNode) {
49
            error_log('[Xapian] indexQuiz: missing ResourceNode, skipping');
50
            return null;
51
        }
52
53
        // Resolve course and session from resource links
54
        [$courseId, $sessionId] = $this->resolveCourseAndSession($resourceNode);
55
56
        $title       = (string) $quiz->getTitle();
57
        $description = (string) ($quiz->getDescription() ?? '');
58
59
        // For now: description only. If later you implement "specific fields"
60
        // for quizzes in C2, you can concatenate them here like in v1.
61
        $content = \trim($description);
62
63
        // Data that will appear in search results under "data"
64
        $fields = [
65
            'kind'             => 'quiz',
66
            'tool'             => 'quiz',
67
            'title'            => $title,
68
            'description'      => $description,
69
            'content'          => $content,
70
            'resource_node_id' => (string) $resourceNode->getId(),
71
            'quiz_id'          => (string) $quiz->getIid(),
72
            'course_id'        => $courseId !== null ? (string) $courseId : '',
73
            'session_id'       => $sessionId !== null ? (string) $sessionId : '',
74
            // Legacy-like metadata, if you want them later:
75
            'xapian_data'      => json_encode([
76
                'type'        => 'exercise',
77
                'exercise_id' => (int) $quiz->getIid(),
78
                'course_id'   => $courseId,
79
            ]),
80
        ];
81
82
        // Terms: allow filtering by kind, course, session
83
        $terms = ['Tquiz'];
84
        if ($courseId !== null) {
85
            $terms[] = 'C'.$courseId;
86
        }
87
        if ($sessionId !== null) {
88
            $terms[] = 'S'.$sessionId;
89
        }
90
91
        // Look for existing SearchEngineRef for this resource node
92
        /** @var SearchEngineRef|null $existingRef */
93
        $existingRef = $this->em
94
            ->getRepository(SearchEngineRef::class)
95
            ->findOneBy(['resourceNodeId' => $resourceNode->getId()]);
96
97
        $existingDocId = $existingRef?->getSearchDid();
98
99
        if ($existingDocId !== null) {
100
            try {
101
                $this->xapianIndexService->deleteDocument($existingDocId);
102
                error_log('[Xapian] indexQuiz: deleted previous docId='.(string) $existingDocId);
103
            } catch (\Throwable $e) {
104
                error_log('[Xapian] indexQuiz: failed to delete previous docId='
105
                    .(string) $existingDocId.' error='.$e->getMessage()
106
                );
107
            }
108
        }
109
110
        // Index in Xapian
111
        try {
112
            $docId = $this->xapianIndexService->indexDocument($fields, $terms);
113
        } catch (\Throwable $e) {
114
            error_log('[Xapian] indexQuiz: indexDocument() failed: '.$e->getMessage());
115
            return null;
116
        }
117
118
        // Update mapping
119
        if ($existingRef instanceof SearchEngineRef) {
120
            $existingRef->setSearchDid($docId);
121
        } else {
122
            $existingRef = new SearchEngineRef();
123
            $existingRef->setResourceNodeId((int) $resourceNode->getId());
124
            $existingRef->setSearchDid($docId);
125
            $this->em->persist($existingRef);
126
        }
127
128
        $this->em->flush();
129
130
        error_log('[Xapian] indexQuiz: completed with docId='.(string) $docId
131
            .', search_engine_ref_id='.$existingRef->getId()
132
        );
133
134
        return $docId;
135
    }
136
137
    /**
138
     * Delete quiz index (called on entity removal).
139
     */
140
    public function deleteQuizIndex(CQuiz $quiz): void
141
    {
142
        $resourceNode = $quiz->getResourceNode();
143
        if (!$resourceNode instanceof ResourceNode) {
144
            return;
145
        }
146
147
        /** @var SearchEngineRef|null $ref */
148
        $ref = $this->em
149
            ->getRepository(SearchEngineRef::class)
150
            ->findOneBy(['resourceNodeId' => $resourceNode->getId()]);
151
152
        if (!$ref) {
153
            return;
154
        }
155
156
        try {
157
            $this->xapianIndexService->deleteDocument($ref->getSearchDid());
158
        } catch (\Throwable $e) {
159
            error_log('[Xapian] deleteQuizIndex: deleteDocument failed: '.$e->getMessage());
160
        }
161
162
        $this->em->remove($ref);
163
        $this->em->flush();
164
    }
165
166
    /**
167
     * Resolve course and session ids from resource links.
168
     *
169
     * @return array{0: int|null, 1: int|null}
170
     */
171
    private function resolveCourseAndSession(ResourceNode $resourceNode): array
172
    {
173
        $courseId  = null;
174
        $sessionId = null;
175
176
        foreach ($resourceNode->getResourceLinks() as $link) {
177
            if (!$link instanceof ResourceLink) {
178
                continue;
179
            }
180
181
            if ($courseId === null && $link->getCourse()) {
182
                $courseId = $link->getCourse()->getId();
183
            }
184
185
            if ($sessionId === null && $link->getSession()) {
186
                $sessionId = $link->getSession()->getId();
187
            }
188
189
            if ($courseId !== null && $sessionId !== null) {
190
                break;
191
            }
192
        }
193
194
        return [$courseId, $sessionId];
195
    }
196
}
197