Passed
Pull Request — master (#1275)
by Dante
01:42
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
    /**
72
     * Get children of a folder by ID.
73
     * Use cache to store data.
74
     *
75
     * @param string $id The ID.
76
     * @return void
77
     */
78
    public function children(string $id): void
79
    {
80
        $this->getRequest()->allowMethod(['get']);
81
        $this->viewBuilder()->setClassName('Json');
82
        $data = $meta = [];
83
        try {
84
            $query = $this->getRequest()->getQueryParams();
85
            $response = $this->apiClient->get(sprintf('/folders/%s/children', $id), $query);
86
            $data = (array)Hash::get($response, 'data');
87
            foreach ($data as &$item) {
88
                $item = $this->minimalDataWithMeta((array)$item);
89
            }
90
            $meta = (array)Hash::get($response, 'meta');
91
        } catch (BEditaClientException $e) {
92
            $this->log($e->getMessage(), LogLevel::ERROR);
93
        }
94
        $this->set('data', $data);
95
        $this->set('meta', $meta);
96
        $this->setSerialize(['data', 'meta']);
97
    }
98
99
    /**
100
     * Get node by ID.
101
     * Use cache to store data.
102
     *
103
     * @param string $id The ID.
104
     * @return void
105
     */
106
    public function node(string $id): void
107
    {
108
        $this->getRequest()->allowMethod(['get']);
109
        $this->viewBuilder()->setClassName('Json');
110
        $node = $this->fetchNodeData($id);
111
        $this->set('node', $node);
112
        $this->setSerialize(['node']);
113
    }
114
115
    /**
116
     * Get parent by ID.
117
     * Use cache to store data.
118
     *
119
     * @param string $id The ID.
120
     * @return void
121
     */
122
    public function parent(string $id): void
123
    {
124
        $this->getRequest()->allowMethod(['get']);
125
        $this->viewBuilder()->setClassName('Json');
126
        $parent = $this->fetchParentData($id);
127
        $this->set('parent', $parent);
128
        $this->setSerialize(['parent']);
129
    }
130
131
    /**
132
     * Get parents by ID and type.
133
     * Use cache to store data.
134
     *
135
     * @param string $type The type.
136
     * @param string $id The ID.
137
     * @return void
138
     */
139
    public function parents(string $type, string $id): void
140
    {
141
        $this->getRequest()->allowMethod(['get']);
142
        $this->viewBuilder()->setClassName('Json');
143
        $parents = $this->fetchParentsData($id, $type);
144
        $this->set('parents', $parents);
145
        $this->setSerialize(['parents']);
146
    }
147
148
    /**
149
     * Saves the current slug
150
     *
151
     * @return \Cake\Http\Response|null
152
     */
153
    public function slug(): ?Response
154
    {
155
        $this->getRequest()->allowMethod(['post']);
156
        $this->viewBuilder()->setClassName('Json');
157
        $response = $error = null;
158
        try {
159
            $data = (array)$this->getRequest()->getData();
160
            $body = [
161
                'data' => [
162
                    [
163
                        'id' => (string)Hash::get($data, 'id'),
164
                        'type' => (string)Hash::get($data, 'type'),
165
                        'meta' => [
166
                            'relation' => [
167
                                'slug' => (string)Hash::get($data, 'slug'),
168
                            ],
169
                        ],
170
                    ],
171
                ],
172
            ];
173
            $response = $this->apiClient->post(
174
                sprintf('/folders/%s/relationships/children', (string)Hash::get($data, 'parent')),
175
                json_encode($body)
176
            );
177
            // Clearing cache after successful save
178
            Cache::clearGroup('tree', TreeCacheEventHandler::CACHE_CONFIG);
179
        } catch (BEditaClientException $err) {
180
            $error = $err->getMessage();
181
            $this->log($error, 'error');
182
            $this->set('error', $error);
183
        }
184
        $this->set('response', $response);
185
        $this->set('error', $error);
186
        $this->setSerialize(['response', 'error']);
187
188
        return null;
189
    }
190
191
    /**
192
     * Get compact tree data.
193
     * Use cache to store data.
194
     *
195
     * @return array
196
     */
197
    public function compactTreeData(): array
198
    {
199
        $objectType = $this->getRequest()->getParam('object_type');
200
        $key = CacheTools::cacheKey(sprintf('compact-tree-%s', $objectType));
201
        $noCache = (bool)$this->getRequest()->getQuery('no_cache');
202
        if ($noCache === true) {
203
            Cache::clearGroup('tree', TreeCacheEventHandler::CACHE_CONFIG);
204
        }
205
        $data = [];
206
        try {
207
            $data = Cache::remember(
208
                $key,
209
                function () {
210
                    return $this->fetchCompactTreeData();
211
                },
212
                TreeCacheEventHandler::CACHE_CONFIG
213
            );
214
        } catch (BEditaClientException $e) {
215
            $this->log($e->getMessage(), LogLevel::ERROR);
216
        }
217
218
        return $data;
219
    }
220
221
    /**
222
     * Get tree data by query params.
223
     * Use cache to store data.
224
     *
225
     * @param array $query Query params.
226
     * @return array
227
     */
228
    public function treeData(array $query): array
229
    {
230
        $filter = Hash::get($query, 'filter', []);
231
        $subkey = !empty($filter['parent']) ? sprintf('parent-%s', $filter['parent']) : 'roots';
232
        $tmp = array_filter(
233
            $query,
234
            function ($key) {
235
                return $key !== 'filter';
236
            },
237
            ARRAY_FILTER_USE_KEY
238
        );
239
        $key = CacheTools::cacheKey(sprintf('tree-%s-%s', $subkey, md5(serialize($tmp))));
240
        $data = [];
241
        try {
242
            $data = Cache::remember(
243
                $key,
244
                function () use ($query) {
245
                    return $this->fetchTreeData($query);
246
                },
247
                TreeCacheEventHandler::CACHE_CONFIG
248
            );
249
        } catch (BEditaClientException $e) {
250
            // Something bad happened
251
            $this->log($e->getMessage(), LogLevel::ERROR);
252
253
            return [];
254
        }
255
256
        return $data;
257
    }
258
259
    /**
260
     * Get node from ID.
261
     * It uses cache to store data.
262
     *
263
     * @param string $id The ID.
264
     * @return array|null
265
     */
266
    public function fetchNodeData(string $id): ?array
267
    {
268
        $key = CacheTools::cacheKey(sprintf('tree-node-%s', $id));
269
        $data = [];
270
        try {
271
            $data = Cache::remember(
272
                $key,
273
                function () use ($id) {
274
                    $response = ApiClientProvider::getApiClient()->get(sprintf('/folders/%s', $id));
275
                    $data = (array)Hash::get($response, 'data');
276
277
                    return $this->minimalData($data);
278
                },
279
                TreeCacheEventHandler::CACHE_CONFIG
280
            );
281
        } catch (BEditaClientException $e) {
282
            // Something bad happened
283
            $this->log($e->getMessage(), LogLevel::ERROR);
284
285
            return [];
286
        }
287
288
        return $data;
289
    }
290
291
    /**
292
     * Get parent from ID.
293
     * It uses cache to store data.
294
     *
295
     * @param string $id The ID.
296
     * @return array|null
297
     */
298
    public function fetchParentData(string $id): ?array
299
    {
300
        $key = CacheTools::cacheKey(sprintf('tree-parent-%s', $id));
301
        $data = [];
302
        try {
303
            $data = Cache::remember(
304
                $key,
305
                function () use ($id) {
306
                    $response = ApiClientProvider::getApiClient()->get(sprintf('/folders/%s/parent', $id));
307
                    $data = (array)Hash::get($response, 'data');
308
309
                    return $this->minimalDataWithMeta($data);
310
                },
311
                TreeCacheEventHandler::CACHE_CONFIG
312
            );
313
        } catch (BEditaClientException $e) {
314
            // Something bad happened
315
            $this->log($e->getMessage(), LogLevel::ERROR);
316
317
            return [];
318
        }
319
320
        return $data;
321
    }
322
323
    /**
324
     * Get parent from ID.
325
     * It uses cache to store data.
326
     *
327
     * @param string $id The ID.
328
     * @param string $type The type.
329
     * @return array
330
     */
331
    public function fetchParentsData(string $id, string $type): array
332
    {
333
        $key = CacheTools::cacheKey(sprintf('tree-parents-%s-%s', $id, $type));
334
        $data = [];
335
        try {
336
            $data = Cache::remember(
337
                $key,
338
                function () use ($id, $type) {
339
                    $response = ApiClientProvider::getApiClient()->get(sprintf('/%s/%s?include=parents', $type, $id));
340
                    $included = (array)Hash::get($response, 'included');
341
                    foreach ($included as &$item) {
342
                        $item = $this->minimalDataWithMeta((array)$item);
343
                    }
344
345
                    return $included;
346
                },
347
                TreeCacheEventHandler::CACHE_CONFIG
348
            );
349
        } catch (BEditaClientException $e) {
350
            // Something bad happened
351
            $this->log($e->getMessage(), LogLevel::ERROR);
352
353
            return [];
354
        }
355
356
        return $data;
357
    }
358
359
    /**
360
     * Fetch compact tree data from API.
361
     * Retrieve minimal data for folders: id, status, title, meta.
362
     * Return tree and folders.
363
     * Return an array with 'tree' and 'folders' keys.
364
     *
365
     * @return array
366
     */
367
    protected function fetchCompactTreeData(): array
368
    {
369
        $done = false;
370
        $page = 1;
371
        $pageSize = 100;
372
        $folders = [];
373
        $paths = [];
374
        while (!$done) {
375
            $response = ApiClientProvider::getApiClient()->get('/folders', [
376
                'page_size' => $pageSize,
377
                'page' => $page,
378
            ]);
379
            $data = (array)Hash::get($response, 'data');
380
            foreach ($data as $item) {
381
                $folders[$item['id']] = $this->minimalDataWithMeta((array)$item);
382
                $path = (string)Hash::get($item, 'meta.path');
383
                $paths[$path] = $item['id'];
384
            }
385
            $page++;
386
            $meta = (array)Hash::get($response, 'meta');
387
            if ($page > (int)Hash::get($meta, 'pagination.page_count')) {
388
                $done = true;
389
            }
390
        }
391
        // organize the tree as roots and children
392
        $tree = [];
393
        foreach ($paths as $path => $id) {
394
            $countSlash = substr_count($path, '/');
395
            if ($countSlash === 1) {
396
                $tree[$id] = compact('id');
397
                continue;
398
            }
399
400
            $parentPath = substr($path, 0, strrpos($path, '/'));
401
            $parentId = $paths[$parentPath];
402
            if (!empty($parentId)) {
403
                $this->pushIntoTree($tree, $parentId, $id, 'subfolders');
404
            }
405
        }
406
407
        return compact('tree', 'folders');
408
    }
409
410
    /**
411
     * Push child into tree, searching parent inside the tree structure.
412
     *
413
     * @param array $tree The tree.
414
     * @param string $searchParentId The parent ID.
415
     * @param string $childId The child ID.
416
     * @param string $subtreeKey The subtree key.
417
     * @return bool
418
     */
419
    public function pushIntoTree(array &$tree, string $searchParentId, string $childId, string $subtreeKey): bool
420
    {
421
        if (Hash::check($tree, $searchParentId)) {
422
            $tree[$searchParentId][$subtreeKey][$childId] = ['id' => $childId];
423
424
            return true;
425
        }
426
        foreach ($tree as &$node) {
427
            $subtree = (array)Hash::get($node, $subtreeKey);
428
            if (empty($subtree)) {
429
                continue;
430
            }
431
            if ($this->pushIntoTree($subtree, $searchParentId, $childId, $subtreeKey)) {
432
                $node[$subtreeKey] = $subtree;
433
434
                return true;
435
            }
436
        }
437
438
        return false;
439
    }
440
441
    /**
442
     * Fetch tree data from API.
443
     * Retrieve minimal data for folders: id, status, title.
444
     * Return data and meta (no links, no included).
445
     *
446
     * @param array $query Query params.
447
     * @return array
448
     */
449
    protected function fetchTreeData(array $query): array
450
    {
451
        $fields = 'id,status,title,perms,relation,slug_path';
452
        $response = ApiClientProvider::getApiClient()->get('/folders', compact('fields') + $query);
453
        $data = (array)Hash::get($response, 'data');
454
        $meta = (array)Hash::get($response, 'meta');
455
        foreach ($data as &$item) {
456
            $item = $this->minimalData((array)$item);
457
        }
458
459
        return compact('data', 'meta');
460
    }
461
462
    /**
463
     * Get minimal data for object.
464
     *
465
     * @param array $fullData Full data.
466
     * @return array
467
     */
468
    protected function minimalData(array $fullData): array
469
    {
470
        if (empty($fullData)) {
471
            return [];
472
        }
473
        $meta = (array)Hash::get($fullData, 'meta');
474
        $meta['slug_path_compact'] = $this->slugPathCompact((array)Hash::get($meta, 'slug_path'));
475
476
        return [
477
            'id' => (string)Hash::get($fullData, 'id'),
478
            'type' => (string)Hash::get($fullData, 'type'),
479
            'attributes' => [
480
                'title' => (string)Hash::get($fullData, 'attributes.title'),
481
                'status' => (string)Hash::get($fullData, 'attributes.status'),
482
            ],
483
            'meta' => $meta,
484
        ];
485
    }
486
487
    /**
488
     * Get minimal data for object with meta.
489
     *
490
     * @param array $fullData Full data.
491
     * @return array|null
492
     */
493
    protected function minimalDataWithMeta(array $fullData): ?array
494
    {
495
        if (empty($fullData)) {
496
            return null;
497
        }
498
499
        return [
500
            'id' => (string)Hash::get($fullData, 'id'),
501
            'type' => (string)Hash::get($fullData, 'type'),
502
            'attributes' => [
503
                'title' => (string)Hash::get($fullData, 'attributes.title'),
504
                'uname' => (string)Hash::get($fullData, 'attributes.uname'),
505
                'lang' => (string)Hash::get($fullData, 'attributes.lang'),
506
                'status' => (string)Hash::get($fullData, 'attributes.status'),
507
            ],
508
            'meta' => [
509
                'modified' => (string)Hash::get($fullData, 'meta.modified'),
510
                'path' => (string)Hash::get($fullData, 'meta.path'),
511
                'slug_path' => (array)Hash::get($fullData, 'meta.slug_path'),
512
                'slug_path_compact' => $this->slugPathCompact((array)Hash::get($fullData, 'meta.slug_path')),
513
                'relation' => [
514
                    'canonical' => (string)Hash::get($fullData, 'meta.relation.canonical'),
515
                    'depth_level' => (string)Hash::get($fullData, 'meta.relation.depth_level'),
516
                    'menu' => (string)Hash::get($fullData, 'meta.relation.menu'),
517
                    'slug' => (string)Hash::get($fullData, 'meta.relation.slug'),
518
                ],
519
            ],
520
        ];
521
    }
522
523
    /**
524
     * Get compact slug path.
525
     *
526
     * @param array $slugPath Slug path.
527
     * @return string
528
     */
529
    protected function slugPathCompact(array $slugPath): string
530
    {
531
        $slugPathCompact = '';
532
        foreach ($slugPath as $item) {
533
            $slugPathCompact = sprintf('%s/%s', $slugPathCompact, (string)Hash::get($item, 'slug'));
534
        }
535
536
        return $slugPathCompact;
537
    }
538
}
539