Passed
Push — master ( 4789f3...606c62 )
by Dante
01:56
created

TreeController::slug()   A

Complexity

Conditions 2
Paths 5

Size

Total Lines 36
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

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