CollectionHandler::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 1
eloc 1
c 2
b 1
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * Chatlas BEdita plugin
6
 *
7
 * Copyright 2023 Atlas Srl
8
 */
9
namespace BEdita\Chatlas\Index;
10
11
use BEdita\Chatlas\Client\ChatlasClient;
12
use BEdita\Core\Filesystem\FilesystemRegistry;
13
use BEdita\Core\Model\Entity\AsyncJob;
14
use BEdita\Core\Model\Entity\ObjectEntity;
15
use Cake\Http\Client\FormData;
16
use Cake\Log\LogTrait;
17
use Cake\ORM\Locator\LocatorAwareTrait;
18
use Cake\Utility\Hash;
19
use Laminas\Diactoros\UploadedFile;
20
21
/**
22
 * Handle Chatlas collection via API.
23
 */
24
class CollectionHandler
25
{
26
    use LocatorAwareTrait;
27
    use LogTrait;
28
29
    /**
30
     * Chatlas API client
31
     *
32
     * @var \BEdita\Chatlas\Client\ChatlasClient
33
     */
34
    protected ChatlasClient $chatlas;
35
36
    /**
37
     * List of properties to exclude when saving Chatlas collection metadata
38
     *
39
     * @var array
40
     */
41
    public const COLLECTION_FIELDS_EXCLUDED = [
42
        'uname',
43
        'type',
44
        'created',
45
        'modified',
46
        'locked',
47
        'published',
48
        'created_by',
49
        'modified_by',
50
        'collection_uuid',
51
        'collection_updated',
52
        '_meta',
53
    ];
54
55
    /**
56
     * List of properties to check when updating a Chatlas collection index
57
     *
58
     * @var array
59
     */
60
    public const DOCUMENT_PROPERTIES = [
61
        'title',
62
        'description',
63
        'body',
64
    ];
65
66
    /**
67
     * Handler constructor
68
     */
69
    public function __construct()
70
    {
71
        $this->chatlas = new ChatlasClient();
72
    }
73
74
    /**
75
     * Create Chatlas collection
76
     *
77
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
78
     * @return void
79
     */
80
    public function createCollection(ObjectEntity $collection): void
81
    {
82
        $msg = sprintf('Creating collection "%s"', $collection->get('title'));
83
        $this->log($msg, 'info');
84
85
        $response = $this->chatlas->post('/collections', $this->chatlasCollection($collection));
86
        $collection->set('collection_uuid', Hash::get($response->getJson(), 'uuid'));
87
        $collection->set('collection_updated', date('c'));
88
        $collection->getTable()->saveOrFail($collection, ['_skipAfterSave' => true]);
89
    }
90
91
    /**
92
     * Update Chatlas collection
93
     *
94
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
95
     * @return void
96
     */
97
    public function updateCollection(ObjectEntity $collection): void
98
    {
99
        $msg = sprintf('Updating collection "%s"', $collection->get('title'));
100
        $this->log($msg, 'info');
101
        $path = sprintf('/collections/%s', $collection->get('collection_uuid'));
102
        $this->chatlas->patch($path, $this->chatlasCollection($collection));
103
        $collection->set('collection_updated', date('c'));
104
        $collection->getTable()->saveOrFail($collection, ['_skipAfterSave' => true]);
105
    }
106
107
    /**
108
     * Fetch Chatlas collection fields
109
     *
110
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
111
     * @return array
112
     */
113
    protected function chatlasCollection(ObjectEntity $collection): array
114
    {
115
        $fields = array_diff_key(
116
            $collection->toArray(),
117
            array_flip(static::COLLECTION_FIELDS_EXCLUDED)
118
        ) + ['deleted' => $collection->get('deleted')];
119
120
        return [
121
            'name' => $collection->get('uname'),
122
            'cmetadata' => array_filter($fields),
123
        ];
124
    }
125
126
    /**
127
     * Remove Chatlas collection
128
     *
129
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
130
     * @return void
131
     */
132
    public function removeCollection(ObjectEntity $collection): void
133
    {
134
        $msg = sprintf('Removing collection "%s"', $collection->get('title'));
135
        $this->log($msg, 'info');
136
        $path = sprintf('/collections/%s', $collection->get('collection_uuid'));
137
        $this->chatlas->delete($path);
138
        $collection->set('collection_uuid', null);
139
    }
140
141
    /**
142
     * Add document to collection index
143
     *
144
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
145
     * @param \BEdita\Core\Model\Entity\ObjectEntity $entity Document entity
146
     * @return void
147
     */
148
    protected function addDocument(ObjectEntity $collection, ObjectEntity $entity): void
149
    {
150
        if ($entity->get('status') !== 'on' || $entity->get('deleted')) {
151
            $msg = sprintf('Skipping doc "%s" - ', $entity->get('title')) .
152
                sprintf('status "%s" - deleted %b', $entity->get('status'), (bool)$entity->get('deleted'));
153
            $this->log($msg, 'info');
154
155
            return;
156
        }
157
        if ($entity->get('type') === 'files') {
158
            $this->uploadDocumentJob($collection, $entity);
159
160
            return;
161
        }
162
        $content = sprintf("%s\n%s", (string)$entity->get('title'), strip_tags((string)$entity->get('body')));
163
        $body = [
164
            'content' => $content,
165
            'collection_id' => $collection->get('collection_uuid'),
166
            'document_id' => $entity->get('id'),
167
            'metadata' => ['type' => $entity->get('type')],
168
        ];
169
        $this->chatlas->post('/index', $body);
170
        $entity->set('index_updated', date('c'));
171
        $entity->set('index_status', 'done');
172
        $entity->getTable()->saveOrFail($entity, ['_skipAfterSave' => true]);
173
    }
174
175
    /**
176
     * Upload file to index
177
     *
178
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
179
     * @param \BEdita\Core\Model\Entity\ObjectEntity $entity Document entity
180
     * @return void
181
     */
182
    public function uploadDocument(ObjectEntity $collection, ObjectEntity $entity): void
183
    {
184
        $form = new FormData();
185
        if (empty($entity->get('streams'))) {
186
            $entity->getTable()->loadInto($entity, ['Streams']);
187
        }
188
        /** @var \BEdita\Core\Model\Entity\Stream|null $stream */
189
        $stream = Hash::get($entity, 'streams.0');
190
        if (empty($stream)) {
191
            return;
192
        }
193
        $resource = FilesystemRegistry::getMountManager()->readStream($stream->uri);
194
        $file = new UploadedFile(
195
            $resource,
196
            $stream->file_size,
197
            UPLOAD_ERR_OK,
198
            $stream->file_name,
199
            $stream->mime_type,
200
        );
201
        $form->addFile('file', $file);
202
        $form->addMany([
203
            'collection_id' => $collection->get('collection_uuid'),
204
            'document_id' => $entity->get('id'),
205
            'metadata' => json_encode([
206
                'type' => $entity->get('type'),
207
                'file' => $stream->file_name,
208
            ]),
209
        ]);
210
        $this->chatlas->postMultipart(
211
            '/index/upload',
212
            $form
213
        );
214
        $entity->set('index_updated', date('c'));
215
        $entity->set('index_status', 'done');
216
        $entity->getTable()->saveOrFail($entity, ['_skipAfterSave' => true]);
217
    }
218
219
    /**
220
     * Create async job to upload file to index
221
     *
222
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
223
     * @param \BEdita\Core\Model\Entity\ObjectEntity $entity File entity
224
     * @return void
225
     */
226
    protected function uploadDocumentJob(ObjectEntity $collection, ObjectEntity $entity): void
227
    {
228
        $asyncJob = new AsyncJob([
229
            'service' => 'BEdita/Chatlas.IndexFile',
230
            'max_attempts' => 3,
231
            'priority' => 5,
232
            'payload' => [
233
                'collection_id' => $collection->id,
234
                'file_id' => $entity->id,
235
            ],
236
        ]);
237
        $this->fetchTable('AsyncJobs')->saveOrFail($asyncJob);
238
        $entity->set('index_status', 'processing');
239
        $entity->getTable()->saveOrFail($entity, ['_skipAfterSave' => true]);
240
    }
241
242
    /**
243
     * Update collection index for a document
244
     *
245
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
246
     * @param \BEdita\Core\Model\Entity\ObjectEntity $entity Document entity
247
     * @param bool $forceAdd Force add document action
248
     * @return void
249
     */
250
    public function updateDocument(ObjectEntity $collection, ObjectEntity $entity, bool $forceAdd = false): void
251
    {
252
        if (
253
            $entity->isNew() ||
254
            ($entity->isDirty('deleted') && !$entity->get('deleted')) ||
255
            ($entity->isDirty('status') && $entity->get('status') === 'on') ||
256
            $forceAdd
257
        ) {
258
            $this->log($this->logMessage('Add', $collection, $entity), 'info');
259
            $this->addDocument($collection, $entity);
260
261
            return;
262
        }
263
        if (
264
            ($entity->isDirty('deleted') && $entity->get('deleted')) ||
265
            ($entity->isDirty('status') && in_array($entity->get('status'), ['draft', 'off']))
266
        ) {
267
            $this->removeDocument($collection, $entity);
268
269
            return;
270
        }
271
        // see if some object properties have changed (no effect on `files` objects)
272
        if ($entity->get('type') === 'files') {
273
            return;
274
        }
275
276
        foreach (static::DOCUMENT_PROPERTIES as $field) {
277
            if ($entity->isDirty($field)) {
278
                $this->log($this->logMessage('Update', $collection, $entity), 'info');
279
                $this->addDocument($collection, $entity);
280
281
                return;
282
            }
283
        }
284
    }
285
286
    /**
287
     * Log message on index action
288
     *
289
     * @param string $action Action to log
290
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
291
     * @param \BEdita\Core\Model\Entity\ObjectEntity $entity Document entity
292
     * @return string
293
     */
294
    protected function logMessage(string $action, ObjectEntity $collection, ObjectEntity $entity): string
295
    {
296
        return sprintf('%s document "%s"', $action, $entity->get('title')) .
297
            sprintf(' [collection "%s"]', $collection->get('title'));
298
    }
299
300
    /**
301
     * Remove document from collection index
302
     *
303
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
304
     * @param \BEdita\Core\Model\Entity\ObjectEntity $entity Document entity
305
     * @return void
306
     */
307
    public function removeDocument(ObjectEntity $collection, ObjectEntity $entity): void
308
    {
309
        $this->log($this->logMessage('Remove', $collection, $entity), 'info');
310
        $path = sprintf('/index/%s/%s', $collection->get('collection_uuid'), $entity->get('id'));
311
        $this->chatlas->delete($path);
312
        $entity->set('index_status', null);
313
        $entity->set('index_updated', null);
314
        $entity->getTable()->saveOrFail($entity, ['_skipAfterSave' => true]);
315
    }
316
}
317