Passed
Pull Request — main (#7)
by Stefano
01:24
created

CollectionHandler::saveObject()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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