Passed
Pull Request — master (#1275)
by Dante
01:55
created

TreeController::loadAll()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 0
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2024 Atlas Srl, Chialab Srl
5
 *
6
 * This file is part of BEdita: you can redistribute it and/or modify
7
 * it under the terms of the GNU Lesser General Public License as published
8
 * by the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
12
 */
13
namespace App\Controller;
14
15
use App\Event\TreeCacheEventHandler;
16
use App\Utility\CacheTools;
17
use BEdita\SDK\BEditaClientException;
18
use BEdita\WebTools\ApiClientProvider;
19
use Cake\Cache\Cache;
20
use Cake\Http\Response;
21
use Cake\Utility\Hash;
22
use Psr\Log\LogLevel;
23
24
/**
25
 * Tree Controller: get tree data using cache
26
 */
27
class TreeController extends AppController
28
{
29
    /**
30
     * @inheritDoc
31
     */
32
    public function initialize(): void
33
    {
34
        parent::initialize();
35
36
        $this->Security->setConfig('unlockedActions', ['slug']);
37
    }
38
39
    /**
40
     * Get tree data.
41
     * Use this for /tree?filter[roots]&... and /tree?filter[parent]=x&...
42
     * Use cache to store data.
43
     *
44
     * @return void
45
     */
46
    public function get(): void
47
    {
48
        $this->getRequest()->allowMethod(['get']);
49
        $this->viewBuilder()->setClassName('Json');
50
        $query = $this->getRequest()->getQueryParams();
51
        $tree = $this->treeData($query);
52
        $this->set('tree', $tree);
53
        $this->setSerialize(['tree']);
54
    }
55
56
    /**
57
     * Get all tree data.
58
     * Use cache to store data.
59
     *
60
     * @return void
61
     */
62
    public function loadAll(): void
63
    {
64
        $this->getRequest()->allowMethod(['get']);
65
        $this->viewBuilder()->setClassName('Json');
66
        $data = $this->compactTreeData();
67
        $this->set('data', $data);
68
        $this->setSerialize(['data']);
69
    }
70
71
    public function children(string $id): void
72
    {
73
        $this->getRequest()->allowMethod(['get']);
74
        $this->viewBuilder()->setClassName('Json');
75
        $data = $meta = [];
76
        try {
77
            $query = $this->getRequest()->getQueryParams();
78
            $response = $this->apiClient->get(sprintf('/folders/%s/children', $id), $query);
79
            $data = (array)Hash::get($response, 'data');
80
            foreach ($data as &$item) {
81
                $item = $this->minimalDataWithMeta((array)$item);
82
            }
83
            $meta = (array)Hash::get($response, 'meta');
84
        } catch (BEditaClientException $e) {
85
            $this->log($e->getMessage(), LogLevel::ERROR);
86
        }
87
        $this->set('data', $data);
88
        $this->set('meta', $meta);
89
        $this->setSerialize(['data', 'meta']);
90
    }
91
92
    /**
93
     * Get node by ID.
94
     * Use cache to store data.
95
     *
96
     * @param string $id The ID.
97
     * @return void
98
     */
99
    public function node(string $id): void
100
    {
101
        $this->getRequest()->allowMethod(['get']);
102
        $this->viewBuilder()->setClassName('Json');
103
        $node = $this->fetchNodeData($id);
104
        $this->set('node', $node);
105
        $this->setSerialize(['node']);
106
    }
107
108
    /**
109
     * Get parent by ID.
110
     * Use cache to store data.
111
     *
112
     * @param string $id The ID.
113
     * @return void
114
     */
115
    public function parent(string $id): void
116
    {
117
        $this->getRequest()->allowMethod(['get']);
118
        $this->viewBuilder()->setClassName('Json');
119
        $parent = $this->fetchParentData($id);
120
        $this->set('parent', $parent);
121
        $this->setSerialize(['parent']);
122
    }
123
124
    /**
125
     * Get parents by ID and type.
126
     * Use cache to store data.
127
     *
128
     * @param string $type The type.
129
     * @param string $id The ID.
130
     * @return void
131
     */
132
    public function parents(string $type, string $id): void
133
    {
134
        $this->getRequest()->allowMethod(['get']);
135
        $this->viewBuilder()->setClassName('Json');
136
        $parents = $this->fetchParentsData($id, $type);
137
        $this->set('parents', $parents);
138
        $this->setSerialize(['parents']);
139
    }
140
141
    /**
142
     * Saves the current slug
143
     *
144
     * @return \Cake\Http\Response|null
145
     */
146
    public function slug(): ?Response
147
    {
148
        $this->getRequest()->allowMethod(['post']);
149
        $this->viewBuilder()->setClassName('Json');
150
        $response = $error = null;
151
        try {
152
            $data = (array)$this->getRequest()->getData();
153
            $body = [
154
                'data' => [
155
                    [
156
                        'id' => (string)Hash::get($data, 'id'),
157
                        'type' => (string)Hash::get($data, 'type'),
158
                        'meta' => [
159
                            'relation' => [
160
                                'slug' => (string)Hash::get($data, 'slug'),
161
                            ],
162
                        ],
163
                    ],
164
                ],
165
            ];
166
            $response = $this->apiClient->post(
167
                sprintf('/folders/%s/relationships/children', (string)Hash::get($data, 'parent')),
168
                json_encode($body)
169
            );
170
            // Clearing cache after successful save
171
            Cache::clearGroup('tree', TreeCacheEventHandler::CACHE_CONFIG);
172
        } catch (BEditaClientException $err) {
173
            $error = $err->getMessage();
174
            $this->log($error, 'error');
175
            $this->set('error', $error);
176
        }
177
        $this->set('response', $response);
178
        $this->set('error', $error);
179
        $this->setSerialize(['response', 'error']);
180
181
        return null;
182
    }
183
184
    public function compactTreeData(): array
185
    {
186
        $objectType = $this->getRequest()->getParam('object_type');
187
        $key = CacheTools::cacheKey(sprintf('compact-tree-%s', $objectType));
188
        $noCache = (bool)$this->getRequest()->getQuery('no_cache');
189
        if ($noCache === true) {
190
            Cache::clearGroup('tree', TreeCacheEventHandler::CACHE_CONFIG);
191
        }
192
        $data = [];
193
        try {
194
            $data = Cache::remember(
195
                $key,
196
                function () {
197
                    return $this->fetchCompactTreeData();
198
                },
199
                TreeCacheEventHandler::CACHE_CONFIG
200
            );
201
        } catch (BEditaClientException $e) {
202
            $this->log($e->getMessage(), LogLevel::ERROR);
203
        }
204
205
        return $data;
206
    }
207
208
    /**
209
     * Get tree data by query params.
210
     * Use cache to store data.
211
     *
212
     * @param array $query Query params.
213
     * @return array
214
     */
215
    public function treeData(array $query): array
216
    {
217
        $filter = Hash::get($query, 'filter', []);
218
        $subkey = !empty($filter['parent']) ? sprintf('parent-%s', $filter['parent']) : 'roots';
219
        $tmp = array_filter(
220
            $query,
221
            function ($key) {
222
                return $key !== 'filter';
223
            },
224
            ARRAY_FILTER_USE_KEY
225
        );
226
        $key = CacheTools::cacheKey(sprintf('tree-%s-%s', $subkey, md5(serialize($tmp))));
227
        $data = [];
228
        try {
229
            $data = Cache::remember(
230
                $key,
231
                function () use ($query) {
232
                    return $this->fetchTreeData($query);
233
                },
234
                TreeCacheEventHandler::CACHE_CONFIG
235
            );
236
        } catch (BEditaClientException $e) {
237
            // Something bad happened
238
            $this->log($e->getMessage(), LogLevel::ERROR);
239
240
            return [];
241
        }
242
243
        return $data;
244
    }
245
246
    /**
247
     * Get node from ID.
248
     * It uses cache to store data.
249
     *
250
     * @param string $id The ID.
251
     * @return array|null
252
     */
253
    public function fetchNodeData(string $id): ?array
254
    {
255
        $key = CacheTools::cacheKey(sprintf('tree-node-%s', $id));
256
        $data = [];
257
        try {
258
            $data = Cache::remember(
259
                $key,
260
                function () use ($id) {
261
                    $response = ApiClientProvider::getApiClient()->get(sprintf('/folders/%s', $id));
262
                    $data = (array)Hash::get($response, 'data');
263
264
                    return $this->minimalData($data);
265
                },
266
                TreeCacheEventHandler::CACHE_CONFIG
267
            );
268
        } catch (BEditaClientException $e) {
269
            // Something bad happened
270
            $this->log($e->getMessage(), LogLevel::ERROR);
271
272
            return [];
273
        }
274
275
        return $data;
276
    }
277
278
    /**
279
     * Get parent from ID.
280
     * It uses cache to store data.
281
     *
282
     * @param string $id The ID.
283
     * @return array|null
284
     */
285
    public function fetchParentData(string $id): ?array
286
    {
287
        $key = CacheTools::cacheKey(sprintf('tree-parent-%s', $id));
288
        $data = [];
289
        try {
290
            $data = Cache::remember(
291
                $key,
292
                function () use ($id) {
293
                    $response = ApiClientProvider::getApiClient()->get(sprintf('/folders/%s/parent', $id));
294
                    $data = (array)Hash::get($response, 'data');
295
296
                    return $this->minimalDataWithMeta($data);
297
                },
298
                TreeCacheEventHandler::CACHE_CONFIG
299
            );
300
        } catch (BEditaClientException $e) {
301
            // Something bad happened
302
            $this->log($e->getMessage(), LogLevel::ERROR);
303
304
            return [];
305
        }
306
307
        return $data;
308
    }
309
310
    /**
311
     * Get parent from ID.
312
     * It uses cache to store data.
313
     *
314
     * @param string $id The ID.
315
     * @param string $type The type.
316
     * @return array
317
     */
318
    public function fetchParentsData(string $id, string $type): array
319
    {
320
        $key = CacheTools::cacheKey(sprintf('tree-parents-%s-%s', $id, $type));
321
        $data = [];
322
        try {
323
            $data = Cache::remember(
324
                $key,
325
                function () use ($id, $type) {
326
                    $response = ApiClientProvider::getApiClient()->get(sprintf('/%s/%s?include=parents', $type, $id));
327
                    $included = (array)Hash::get($response, 'included');
328
                    foreach ($included as &$item) {
329
                        $item = $this->minimalDataWithMeta((array)$item);
330
                    }
331
332
                    return $included;
333
                },
334
                TreeCacheEventHandler::CACHE_CONFIG
335
            );
336
        } catch (BEditaClientException $e) {
337
            // Something bad happened
338
            $this->log($e->getMessage(), LogLevel::ERROR);
339
340
            return [];
341
        }
342
343
        return $data;
344
    }
345
346
    protected function fetchCompactTreeData(): array
347
    {
348
        $done = false;
349
        $page = 1;
350
        $pageSize = 100;
351
        $folders = [];
352
        $paths = [];
353
        while (!$done) {
354
            $response = ApiClientProvider::getApiClient()->get('/folders', [
355
                'page_size' => $pageSize,
356
                'page' => $page,
357
            ]);
358
            $data = (array)Hash::get($response, 'data');
359
            foreach ($data as $item) {
360
                $folders[$item['id']] = $this->minimalDataWithMeta((array)$item);
361
                $path = (string)Hash::get($item, 'meta.path');
362
                $paths[$path] = $item['id'];
363
            }
364
            $page++;
365
            $meta = (array)Hash::get($response, 'meta');
366
            if ($page > (int)Hash::get($meta, 'pagination.page_count')) {
367
                $done = true;
368
            }
369
        }
370
        // organize the tree as roots and children
371
        $tree = [];
372
        foreach ($paths as $path => $id) {
373
            $countSlash = substr_count($path, '/');
374
            if ($countSlash === 1) {
375
                $tree[$id] = compact('id');
376
                continue;
377
            }
378
379
            $parentPath = substr($path, 0, strrpos($path, '/'));
380
            $parentId = $paths[$parentPath];
381
            if (empty($parentId)) {
382
                continue;
383
            }
384
            $this->pushIntoTree($tree, $parentId, $id, 'subfolders');
385
        }
386
387
        return compact('tree', 'folders');
388
    }
389
390
    /**
391
     * Push child into tree, searching parent inside the tree structure.
392
     *
393
     * @param array $tree The tree.
394
     * @param string $searchParentId The parent ID.
395
     * @param string $childId The child ID.
396
     * @param string $subtreeKey The subtree key.
397
     * @return bool
398
     */
399
    public function pushIntoTree(array &$tree, string $searchParentId, string $childId, string $subtreeKey): bool
400
    {
401
        if (Hash::check($tree, $searchParentId)) {
402
            $tree[$searchParentId][$subtreeKey][$childId] = ['id' => $childId];
403
404
            return true;
405
        }
406
        foreach ($tree as &$node) {
407
            $subtree = (array)Hash::get($node, $subtreeKey);
408
            if (empty($subtree)) {
409
                continue;
410
            }
411
            if ($this->pushIntoTree($subtree, $searchParentId, $childId, $subtreeKey)) {
412
                $node[$subtreeKey] = $subtree;
413
414
                return true;
415
            }
416
        }
417
418
        return false;
419
    }
420
421
    /**
422
     * Fetch tree data from API.
423
     * Retrieve minimal data for folders: id, status, title.
424
     * Return data and meta (no links, no included).
425
     *
426
     * @param array $query Query params.
427
     * @return array
428
     */
429
    protected function fetchTreeData(array $query): array
430
    {
431
        $fields = 'id,status,title,perms,relation,slug_path';
432
        $response = ApiClientProvider::getApiClient()->get('/folders', compact('fields') + $query);
433
        $data = (array)Hash::get($response, 'data');
434
        $meta = (array)Hash::get($response, 'meta');
435
        foreach ($data as &$item) {
436
            $item = $this->minimalData((array)$item);
437
        }
438
439
        return compact('data', 'meta');
440
    }
441
442
    /**
443
     * Get minimal data for object.
444
     *
445
     * @param array $fullData Full data.
446
     * @return array
447
     */
448
    protected function minimalData(array $fullData): array
449
    {
450
        if (empty($fullData)) {
451
            return [];
452
        }
453
        $meta = (array)Hash::get($fullData, 'meta');
454
        $meta['slug_path_compact'] = $this->slugPathCompact((array)Hash::get($meta, 'slug_path'));
455
456
        return [
457
            'id' => (string)Hash::get($fullData, 'id'),
458
            'type' => (string)Hash::get($fullData, 'type'),
459
            'attributes' => [
460
                'title' => (string)Hash::get($fullData, 'attributes.title'),
461
                'status' => (string)Hash::get($fullData, 'attributes.status'),
462
            ],
463
            'meta' => $meta,
464
        ];
465
    }
466
467
    /**
468
     * Get minimal data for object with meta.
469
     *
470
     * @param array $fullData Full data.
471
     * @return array|null
472
     */
473
    protected function minimalDataWithMeta(array $fullData): ?array
474
    {
475
        if (empty($fullData)) {
476
            return null;
477
        }
478
479
        return [
480
            'id' => (string)Hash::get($fullData, 'id'),
481
            'type' => (string)Hash::get($fullData, 'type'),
482
            'attributes' => [
483
                'title' => (string)Hash::get($fullData, 'attributes.title'),
484
                'uname' => (string)Hash::get($fullData, 'attributes.uname'),
485
                'lang' => (string)Hash::get($fullData, 'attributes.lang'),
486
                'status' => (string)Hash::get($fullData, 'attributes.status'),
487
            ],
488
            'meta' => [
489
                'modified' => (string)Hash::get($fullData, 'meta.modified'),
490
                'path' => (string)Hash::get($fullData, 'meta.path'),
491
                'slug_path' => (array)Hash::get($fullData, 'meta.slug_path'),
492
                'slug_path_compact' => $this->slugPathCompact((array)Hash::get($fullData, 'meta.slug_path')),
493
                'relation' => [
494
                    'canonical' => (string)Hash::get($fullData, 'meta.relation.canonical'),
495
                    'depth_level' => (string)Hash::get($fullData, 'meta.relation.depth_level'),
496
                    'menu' => (string)Hash::get($fullData, 'meta.relation.menu'),
497
                    'slug' => (string)Hash::get($fullData, 'meta.relation.slug'),
498
                ],
499
            ],
500
        ];
501
    }
502
503
    /**
504
     * Get compact slug path.
505
     *
506
     * @param array $slugPath Slug path.
507
     * @return string
508
     */
509
    protected function slugPathCompact(array $slugPath): string
510
    {
511
        $slugPathCompact = '';
512
        foreach ($slugPath as $item) {
513
            $slugPathCompact = sprintf('%s/%s', $slugPathCompact, (string)Hash::get($item, 'slug'));
514
        }
515
516
        return $slugPathCompact;
517
    }
518
}
519