Passed
Push — main ( 22aa22...c7f4b0 )
by Stefano
01:06
created

CollectionHandler::uploadDocumentJob()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 10
nc 1
nop 2
dl 0
loc 14
rs 9.9332
c 0
b 0
f 0
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->getTable()->saveOrFail($entity, ['_skipAfterSave' => true]);
172
    }
173
174
    /**
175
     * Upload file to index
176
     *
177
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
178
     * @param \BEdita\Core\Model\Entity\ObjectEntity $entity Document entity
179
     * @return void
180
     */
181
    public function uploadDocument(ObjectEntity $collection, ObjectEntity $entity): void
182
    {
183
        $form = new FormData();
184
        if (empty($entity->get('streams'))) {
185
            $entity->getTable()->loadInto($entity, ['Streams']);
186
        }
187
        /** @var \BEdita\Core\Model\Entity\Stream|null $stream */
188
        $stream = Hash::get($entity, 'streams.0');
189
        if (empty($stream)) {
190
            return;
191
        }
192
        $resource = FilesystemRegistry::getMountManager()->readStream($stream->uri);
193
        $file = new UploadedFile(
194
            $resource,
195
            $stream->file_size,
196
            UPLOAD_ERR_OK,
197
            $stream->file_name,
198
            $stream->mime_type,
199
        );
200
        $form->addFile('file', $file);
201
        $form->addMany([
202
            'collection_id' => $collection->get('collection_uuid'),
203
            'document_id' => $entity->get('id'),
204
            'metadata' => json_encode([
205
                'type' => $entity->get('type'),
206
                'file' => $stream->file_name,
207
            ]),
208
        ]);
209
        $this->chatlas->postMultipart(
210
            '/index/upload',
211
            $form
212
        );
213
        $entity->set('index_updated', date('c'));
214
        $entity->set('index_status', 'done');
215
        $entity->getTable()->saveOrFail($entity, ['_skipAfterSave' => true]);
216
    }
217
218
    /**
219
     * Create async job to upload file to index
220
     *
221
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
222
     * @param \BEdita\Core\Model\Entity\ObjectEntity $entity File entity
223
     * @return void
224
     */
225
    protected function uploadDocumentJob(ObjectEntity $collection, ObjectEntity $entity): void
226
    {
227
        $asyncJob = new AsyncJob([
228
            'service' => 'BEdita/Chatlas.IndexFile',
229
            'max_attempts' => 3,
230
            'priority' => 5,
231
            'payload' => [
232
                'collection_id' => $collection->id,
233
                'file_id' => $entity->id,
234
            ],
235
        ]);
236
        $this->fetchTable('AsyncJobs')->saveOrFail($asyncJob);
237
        $entity->set('index_status', 'processing');
238
        $entity->getTable()->saveOrFail($entity, ['_skipAfterSave' => true]);
239
    }
240
241
    /**
242
     * Update collection index for a document
243
     *
244
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
245
     * @param \BEdita\Core\Model\Entity\ObjectEntity $entity Document entity
246
     * @param bool $forceAdd Force add document action
247
     * @return void
248
     */
249
    public function updateDocument(ObjectEntity $collection, ObjectEntity $entity, bool $forceAdd = false): void
250
    {
251
        if (
252
            $entity->isNew() ||
253
            ($entity->isDirty('deleted') && !$entity->get('deleted')) ||
254
            ($entity->isDirty('status') && $entity->get('status') === 'on') ||
255
            $forceAdd
256
        ) {
257
            $this->log($this->logMessage('Add', $collection, $entity), 'info');
258
            $this->addDocument($collection, $entity);
259
260
            return;
261
        }
262
        if (
263
            ($entity->isDirty('deleted') && $entity->get('deleted')) ||
264
            ($entity->isDirty('status') && in_array($entity->get('status'), ['draft', 'off']))
265
        ) {
266
            $this->removeDocument($collection, $entity);
267
268
            return;
269
        }
270
        // see if some object properties have changed (no effect on `files` objects)
271
        if ($entity->get('type') === 'files') {
272
            return;
273
        }
274
275
        foreach (static::DOCUMENT_PROPERTIES as $field) {
276
            if ($entity->isDirty($field)) {
277
                $this->log($this->logMessage('Update', $collection, $entity), 'info');
278
                $this->addDocument($collection, $entity);
279
280
                return;
281
            }
282
        }
283
    }
284
285
    /**
286
     * Log message on index action
287
     *
288
     * @param string $action Action to log
289
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
290
     * @param \BEdita\Core\Model\Entity\ObjectEntity $entity Document entity
291
     * @return string
292
     */
293
    protected function logMessage(string $action, ObjectEntity $collection, ObjectEntity $entity): string
294
    {
295
        return sprintf('%s document "%s"', $action, $entity->get('title')) .
296
            sprintf(' [collection "%s"]', $collection->get('title'));
297
    }
298
299
    /**
300
     * Remove document from collection index
301
     *
302
     * @param \BEdita\Core\Model\Entity\ObjectEntity $collection Collection entity
303
     * @param \BEdita\Core\Model\Entity\ObjectEntity $entity Document entity
304
     * @return void
305
     */
306
    public function removeDocument(ObjectEntity $collection, ObjectEntity $entity): void
307
    {
308
        $this->log($this->logMessage('Remove', $collection, $entity), 'info');
309
        $path = sprintf('/index/%s/%s', $collection->get('collection_uuid'), $entity->get('id'));
310
        $this->chatlas->delete($path);
311
        $entity->set('index_updated', null);
312
        $entity->getTable()->saveOrFail($entity, ['_skipAfterSave' => true]);
313
    }
314
}
315