Passed
Push — master ( ab6f49...c762ff )
by
unknown
16:59 queued 08:07
created

LpXapianIndexer::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 0
c 1
b 0
f 0
nc 1
nop 3
dl 0
loc 5
rs 10
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\CLp;
14
use Doctrine\ORM\EntityManagerInterface;
15
16
/**
17
 * Handles Xapian indexing for learning paths (CLp).
18
 */
19
final class LpXapianIndexer
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 learning path.
30
     *
31
     * @return int|null Xapian document id or null when indexing is skipped
32
     */
33
    public function indexLp(CLp $lp): ?int
34
    {
35
        // Global feature toggle (same style as QuestionXapianIndexer)
36
        $enabled = (string) $this->settingsManager->getSetting('search.search_enabled', true);
37
        if ($enabled !== 'true') {
38
            return null;
39
        }
40
41
        $resourceNode = $lp->getResourceNode();
42
        if (!$resourceNode instanceof ResourceNode) {
43
            // Learning path without a resource node cannot be indexed
44
            return null;
45
        }
46
47
        // Resolve course and session from resource links
48
        [$courseId, $sessionId] = $this->resolveCourseAndSession($resourceNode);
49
50
        $title       = (string) $lp->getTitle();
51
        $description = (string) ($lp->getDescription() ?? '');
52
        $content     = \trim($title.' '.$description);
53
54
        // Keep field names consistent across indexers
55
        $fields = [
56
            'kind'             => 'learnpath',
57
            'tool'             => 'learnpath',
58
            'title'            => $title,
59
            'description'      => $description,
60
            'content'          => $content,
61
            'filetype'         => 'learnpath',
62
            'resource_node_id' => (string) $resourceNode->getId(),
63
            'lp_id'            => $lp->getIid() !== null ? (string) $lp->getIid() : '',
64
            'course_id'        => $courseId !== null ? (string) $courseId : '',
65
            'session_id'       => $sessionId !== null ? (string) $sessionId : '',
66
            'full_path'        => (string) $lp->getPath(),
67
            'xapian_data'      => json_encode([
68
                'type'       => 'learnpath',
69
                'lp_id'      => $lp->getIid(),
70
                'course_id'  => $courseId,
71
                'session_id' => $sessionId,
72
            ]),
73
        ];
74
75
        // Terms for filtering
76
        $terms = ['Tlearnpath', 'Tlp'];
77
        if ($courseId !== null) {
78
            $terms[] = 'C'.$courseId;
79
        }
80
        if ($sessionId !== null) {
81
            $terms[] = 'S'.$sessionId;
82
        }
83
        if ($lp->getIid() !== null) {
84
            $terms[] = 'L'.$lp->getIid();
85
        }
86
87
        // Reuse SearchEngineRef per resource node (same pattern as questions)
88
        /** @var SearchEngineRef|null $existingRef */
89
        $existingRef = $this->em
90
            ->getRepository(SearchEngineRef::class)
91
            ->findOneBy(['resourceNodeId' => $resourceNode->getId()]);
92
93
        $existingDocId = $existingRef?->getSearchDid();
94
95
        if ($existingDocId !== null) {
96
            try {
97
                $this->xapianIndexService->deleteDocument($existingDocId);
98
            } catch (\Throwable) {
99
                // Best-effort delete: ignore errors
100
            }
101
        }
102
103
        try {
104
            $docId = $this->xapianIndexService->indexDocument($fields, $terms);
105
        } catch (\Throwable) {
106
            return null;
107
        }
108
109
        if ($existingRef instanceof SearchEngineRef) {
110
            $existingRef->setSearchDid($docId);
111
        } else {
112
            $existingRef = new SearchEngineRef();
113
            $existingRef->setResourceNodeId((int) $resourceNode->getId());
114
            $existingRef->setSearchDid($docId);
115
            $this->em->persist($existingRef);
116
        }
117
118
        $this->em->flush();
119
120
        return $docId;
121
    }
122
123
    /**
124
     * Delete learning path index (called on entity removal).
125
     */
126
    public function deleteLpIndex(CLp $lp): void
127
    {
128
        $resourceNode = $lp->getResourceNode();
129
        if (!$resourceNode instanceof ResourceNode) {
130
            return;
131
        }
132
133
        /** @var SearchEngineRef|null $ref */
134
        $ref = $this->em
135
            ->getRepository(SearchEngineRef::class)
136
            ->findOneBy(['resourceNodeId' => $resourceNode->getId()]);
137
138
        if (!$ref) {
139
            return;
140
        }
141
142
        try {
143
            $this->xapianIndexService->deleteDocument($ref->getSearchDid());
144
        } catch (\Throwable) {
145
            // Best-effort delete
146
        }
147
148
        $this->em->remove($ref);
149
        $this->em->flush();
150
    }
151
152
    /**
153
     * Resolve course and session ids from resource links.
154
     *
155
     * @return array{0:int|null,1:int|null}
156
     */
157
    private function resolveCourseAndSession(ResourceNode $resourceNode): array
158
    {
159
        $courseId  = null;
160
        $sessionId = null;
161
162
        foreach ($resourceNode->getResourceLinks() as $link) {
163
            if (!$link instanceof ResourceLink) {
164
                continue;
165
            }
166
167
            if ($courseId === null && $link->getCourse()) {
168
                $courseId = $link->getCourse()->getId();
169
            }
170
171
            if ($sessionId === null && $link->getSession()) {
172
                $sessionId = $link->getSession()->getId();
173
            }
174
175
            if ($courseId !== null && $sessionId !== null) {
176
                break;
177
            }
178
        }
179
180
        return [$courseId, $sessionId];
181
    }
182
}
183