Passed
Pull Request — master (#1222)
by
unknown
01:35
created

TreeController::fetchParentsData()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 26
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 16
nc 2
nop 2
dl 0
loc 26
rs 9.7333
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 node by ID.
58
     * Use cache to store data.
59
     *
60
     * @param string $id The ID.
61
     * @return void
62
     */
63
    public function node(string $id): void
64
    {
65
        $this->getRequest()->allowMethod(['get']);
66
        $this->viewBuilder()->setClassName('Json');
67
        $node = $this->fetchNodeData($id);
68
        $this->set('node', $node);
69
        $this->setSerialize(['node']);
70
    }
71
72
    /**
73
     * Get parent by ID.
74
     * Use cache to store data.
75
     *
76
     * @param string $id The ID.
77
     * @return void
78
     */
79
    public function parent(string $id): void
80
    {
81
        $this->getRequest()->allowMethod(['get']);
82
        $this->viewBuilder()->setClassName('Json');
83
        $parent = $this->fetchParentData($id);
84
        $this->set('parent', $parent);
85
        $this->setSerialize(['parent']);
86
    }
87
88
    /**
89
     * Get parents by ID and type.
90
     * Use cache to store data.
91
     *
92
     * @param string $type The type.
93
     * @param string $id The ID.
94
     * @return void
95
     */
96
    public function parents(string $type, string $id): void
97
    {
98
        $this->getRequest()->allowMethod(['get']);
99
        $this->viewBuilder()->setClassName('Json');
100
        $parents = $this->fetchParentsData($id, $type);
101
        $this->set('parents', $parents);
102
        $this->setSerialize(['parents']);
103
    }
104
105
    /**
106
     * Saves the current slug
107
     */
108
    public function slug(): ?Response
109
        {
110
        $this->getRequest()->allowMethod(['post']);
111
        $this->viewBuilder()->setClassName('Json');
112
        $response = $error = null;
113
        try {
114
            $data = (array)$this->getRequest()->getData();
115
            $parentId = $data['parent'];
116
            $newSlug = $data['slug'];
117
            $type = $data['type'];
118
            $id = $data['id'];
119
            $body = [
120
                'data' => [
121
                    [
122
                        'type' => $type,
123
                        'id' => $id,
124
                        'meta' => [
125
                            'relation' => [
126
                                'slug' => $newSlug,
127
                            ],
128
                        ],
129
                    ],
130
                ],
131
            ];
132
            $response = $this->apiClient->post(
133
                sprintf('/folders/%s/relationships/children', $parentId),
134
                json_encode($body)
135
            );
136
            // Clearing cache after successful save
137
            Cache::clearGroup('tree', TreeCacheEventHandler::CACHE_CONFIG);
138
        } catch (BEditaClientException $err) {
139
            $error = $err->getMessage();
140
            $this->log($error, 'error');
141
            $this->set('error', $error);
142
        }
143
        $this->set('response', $response);
144
        $this->set('error', $error);
145
        $this->setSerialize(['response', 'error']);
146
147
        return null;        
148
    }
149
150
    /**
151
     * Get tree data by query params.
152
     * Use cache to store data.
153
     *
154
     * @param array $query Query params.
155
     * @return array
156
     */
157
    public function treeData(array $query): array
158
    {
159
        $filter = Hash::get($query, 'filter', []);
160
        $subkey = !empty($filter['parent']) ? sprintf('parent-%s', $filter['parent']) : 'roots';
161
        $tmp = array_filter(
162
            $query,
163
            function ($key) {
164
                return $key !== 'filter';
165
            },
166
            ARRAY_FILTER_USE_KEY
167
        );
168
        $key = CacheTools::cacheKey(sprintf('tree-%s-%s', $subkey, md5(serialize($tmp))));
169
        $data = [];
170
        try {
171
            $data = Cache::remember(
172
                $key,
173
                function () use ($query) {
174
                    return $this->fetchTreeData($query);
175
                },
176
                TreeCacheEventHandler::CACHE_CONFIG
177
            );
178
        } catch (BEditaClientException $e) {
179
            // Something bad happened
180
            $this->log($e->getMessage(), LogLevel::ERROR);
181
182
            return [];
183
        }
184
185
        return $data;
186
    }
187
188
    /**
189
     * Get node from ID.
190
     * It uses cache to store data.
191
     *
192
     * @param string $id The ID.
193
     * @return array|null
194
     */
195
    public function fetchNodeData(string $id): ?array
196
    {
197
        $key = CacheTools::cacheKey(sprintf('tree-node-%s', $id));
198
        $data = [];
199
        try {
200
            $data = Cache::remember(
201
                $key,
202
                function () use ($id) {
203
                    $response = ApiClientProvider::getApiClient()->get(sprintf('/folders/%s', $id));
204
                    $data = (array)Hash::get($response, 'data');
205
206
                    return $this->minimalData($data);
207
                },
208
                TreeCacheEventHandler::CACHE_CONFIG
209
            );
210
        } catch (BEditaClientException $e) {
211
            // Something bad happened
212
            $this->log($e->getMessage(), LogLevel::ERROR);
213
214
            return [];
215
        }
216
217
        return $data;
218
    }
219
220
    /**
221
     * Get parent from ID.
222
     * It uses cache to store data.
223
     *
224
     * @param string $id The ID.
225
     * @return array|null
226
     */
227
    public function fetchParentData(string $id): ?array
228
    {
229
        $key = CacheTools::cacheKey(sprintf('tree-parent-%s', $id));
230
        $data = [];
231
        try {
232
            $data = Cache::remember(
233
                $key,
234
                function () use ($id) {
235
                    $response = ApiClientProvider::getApiClient()->get(sprintf('/folders/%s/parent', $id));
236
                    $data = (array)Hash::get($response, 'data');
237
238
                    return $this->minimalDataWithMeta($data);
239
                },
240
                TreeCacheEventHandler::CACHE_CONFIG
241
            );
242
        } catch (BEditaClientException $e) {
243
            // Something bad happened
244
            $this->log($e->getMessage(), LogLevel::ERROR);
245
246
            return [];
247
        }
248
249
        return $data;
250
    }
251
252
    /**
253
     * Get parent from ID.
254
     * It uses cache to store data.
255
     *
256
     * @param string $id The ID.
257
     * @param string $type The type.
258
     * @return array
259
     */
260
    public function fetchParentsData(string $id, string $type): array
261
    {
262
        $key = CacheTools::cacheKey(sprintf('tree-parents-%s-%s', $id, $type));
263
        $data = [];
264
        try {
265
            $data = Cache::remember(
266
                $key,
267
                function () use ($id, $type) {
268
                    $response = ApiClientProvider::getApiClient()->get(sprintf('/%s/%s?include=parents', $type, $id));
269
                    $included = (array)Hash::get($response, 'included');
270
                    foreach ($included as &$item) {
271
                        $item = $this->minimalDataWithMeta((array)$item);
272
                    }
273
274
                    return $included;
275
                },
276
                TreeCacheEventHandler::CACHE_CONFIG
277
            );
278
        } catch (BEditaClientException $e) {
279
            // Something bad happened
280
            $this->log($e->getMessage(), LogLevel::ERROR);
281
282
            return [];
283
        }
284
285
        return $data;
286
    }
287
288
    /**
289
     * Fetch tree data from API.
290
     * Retrieve minimal data for folders: id, status, title.
291
     * Return data and meta (no links, no included).
292
     *
293
     * @param array $query Query params.
294
     * @return array
295
     */
296
    protected function fetchTreeData(array $query): array
297
    {
298
        $fields = 'id,status,title,perms,relation,slug_path';
299
        $response = ApiClientProvider::getApiClient()->get('/folders', compact('fields') + $query);
300
        $data = (array)Hash::get($response, 'data');
301
        $meta = (array)Hash::get($response, 'meta');
302
        foreach ($data as &$item) {
303
            $item = $this->minimalData((array)$item);
304
        }
305
306
        return compact('data', 'meta');
307
    }
308
309
    /**
310
     * Get minimal data for object.
311
     *
312
     * @param array $fullData Full data.
313
     * @return array
314
     */
315
    protected function minimalData(array $fullData): array
316
    {
317
        if (empty($fullData)) {
318
            return [];
319
        }
320
        $meta = (array)Hash::get($fullData, 'meta');
321
        $meta['slug_path_compact'] = $this->slugPathCompact((array)Hash::get($meta, 'slug_path'));
322
323
        return [
324
            'id' => (string)Hash::get($fullData, 'id'),
325
            'type' => (string)Hash::get($fullData, 'type'),
326
            'attributes' => [
327
                'title' => (string)Hash::get($fullData, 'attributes.title'),
328
                'status' => (string)Hash::get($fullData, 'attributes.status'),
329
            ],
330
            'meta' => $meta,
331
        ];
332
    }
333
334
    /**
335
     * Get minimal data for object with meta.
336
     *
337
     * @param array $fullData Full data.
338
     * @return array|null
339
     */
340
    protected function minimalDataWithMeta(array $fullData): ?array
341
    {
342
        if (empty($fullData)) {
343
            return null;
344
        }
345
346
        return [
347
            'id' => (string)Hash::get($fullData, 'id'),
348
            'type' => (string)Hash::get($fullData, 'type'),
349
            'attributes' => [
350
                'title' => (string)Hash::get($fullData, 'attributes.title'),
351
                'status' => (string)Hash::get($fullData, 'attributes.status'),
352
            ],
353
            'meta' => [
354
                'path' => (string)Hash::get($fullData, 'meta.path'),
355
                'slug_path' => (array)Hash::get($fullData, 'meta.slug_path'),
356
                'slug_path_compact' => $this->slugPathCompact((array)Hash::get($fullData, 'meta.slug_path')),
357
                'relation' => [
358
                    'canonical' => (string)Hash::get($fullData, 'meta.relation.canonical'),
359
                    'depth_level' => (string)Hash::get($fullData, 'meta.relation.depth_level'),
360
                    'menu' => (string)Hash::get($fullData, 'meta.relation.menu'),
361
                    'slug' => (string)Hash::get($fullData, 'meta.relation.slug'),
362
                ],
363
            ],
364
        ];
365
    }
366
367
    /**
368
     * Get compact slug path.
369
     *
370
     * @param array $slugPath Slug path.
371
     * @return string
372
     */
373
    protected function slugPathCompact(array $slugPath): string
374
    {
375
        $slugPathCompact = '';
376
        foreach ($slugPath as $item) {
377
            $slugPathCompact = sprintf('%s/%s', $slugPathCompact, (string)Hash::get($item, 'slug'));
378
        }
379
380
        return $slugPathCompact;
381
    }
382
}
383