CategoriesComponent   A
last analyzed

Complexity

Total Complexity 23

Size/Duplication

Total Lines 237
Duplicated Lines 0 %

Importance

Changes 2
Bugs 2 Features 0
Metric Value
eloc 86
dl 0
loc 237
rs 10
c 2
b 2
f 0
wmc 23

11 Methods

Rating   Name   Duplication   Size   Complexity  
A names() 0 13 2
A getAvailableRoots() 0 8 2
A fillRoots() 0 10 2
A tree() 0 24 4
A map() 0 3 1
A index() 0 11 2
A delete() 0 10 2
A hasChanged() 0 10 1
A invalidateSchemaCache() 0 4 1
A save() 0 28 3
A getAllAvailableRoots() 0 16 3
1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2022 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\Component;
14
15
use App\Utility\CacheTools;
16
use BEdita\WebTools\ApiClientProvider;
17
use Cake\Cache\Cache;
18
use Cake\Controller\Component;
19
use Cake\Utility\Hash;
20
21
/**
22
 * Categories component
23
 */
24
class CategoriesComponent extends Component
25
{
26
    /**
27
     * Default page size
28
     *
29
     * @var int
30
     */
31
    public const DEFAULT_PAGE_SIZE = 100;
32
33
    /**
34
     * Fetch categories list.
35
     *
36
     * @param string|null $objectType The object type filter for categories.
37
     * @param array|null $options Query options.
38
     * @return array The BEdita API response for the categories list.
39
     */
40
    public function index(?string $objectType = null, ?array $options = []): array
41
    {
42
        $options = array_filter($options);
43
        $options = array_merge(['page_size' => self::DEFAULT_PAGE_SIZE], $options);
44
        if (!empty($objectType)) {
45
            $options['filter'] = $options['filter'] ?? [];
46
            $options['filter']['type'] = $objectType;
47
        }
48
        $options = array_filter($options);
49
50
        return (array)ApiClientProvider::getApiClient()->get('/model/categories', $options);
51
    }
52
53
    /**
54
     * Fetch categories names
55
     *
56
     * @param string|null $objectType The object type
57
     * @return array
58
     */
59
    public function names(?string $objectType = null): array
60
    {
61
        $apiClient = ApiClientProvider::getApiClient();
62
        $query = [
63
            'fields' => 'name',
64
            'page_size' => 500, // BE4 API MAX_LIMIT
65
        ];
66
        if (!empty($objectType)) {
67
            $query['filter']['type'] = $objectType;
68
        }
69
        $response = $apiClient->get('/model/categories', $query);
70
71
        return (array)Hash::extract($response, 'data.{n}.attributes.name');
72
    }
73
74
    /**
75
     * Create a key/value map of categories from the BEdita categories list response.
76
     *
77
     * @param array $response The BEdita API response for the categories list.
78
     * @return array A map with the category ids as keys and the category attributes as values.
79
     */
80
    public function map(?array $response): array
81
    {
82
        return Hash::combine((array)Hash::get($response, 'data'), '{n}.id', '{n}');
83
    }
84
85
    /**
86
     * Create an id-based categories tree.
87
     * Sort children by label or name.
88
     *
89
     * @param array $map The categories map returned by the map function.
90
     * @return array The categories tree.
91
     */
92
    public function tree(?array $map): array
93
    {
94
        $tree = [
95
            '_' => [],
96
        ];
97
        foreach ($map as $category) {
98
            if (empty($category['attributes']['parent_id'])) {
99
                $tree['_'][] = $category;
100
            } else {
101
                $tree[$category['attributes']['parent_id']][] = $category;
102
            }
103
        }
104
        // sort by label or name
105
        foreach ($tree as $key => $children) {
106
            usort($children, function ($a, $b) {
107
                $tmpA = Hash::get($a, 'attributes.label') ?? Hash::get($a, 'attributes.name');
108
                $tmpB = Hash::get($b, 'attributes.label') ?? Hash::get($b, 'attributes.name');
109
110
                return strcasecmp($tmpA, $tmpB);
111
            });
112
            $tree[$key] = Hash::extract($children, '{n}.id');
113
        }
114
115
        return $tree;
116
    }
117
118
    /**
119
     * Get an id/label map of available category roots.
120
     *
121
     * @param array $map The categories map returned by the map function.
122
     * @return array The list of available roots.
123
     */
124
    public function getAvailableRoots(?array $map): array
125
    {
126
        $roots = ['' => ['id' => 0, 'label' => '-', 'name' => '', 'object_type_name' => '']];
127
        foreach ($map as $category) {
128
            $this->fillRoots($roots, $category);
129
        }
130
131
        return $roots;
132
    }
133
134
    /**
135
     * Fill roots array with categories that have parent_id null.
136
     *
137
     * @param array $roots The roots array to fill.
138
     * @param array $category The category data.
139
     * @return void
140
     */
141
    protected function fillRoots(array &$roots, $category): void
142
    {
143
        if (!empty(Hash::get($category, 'attributes.parent_id'))) {
144
            return;
145
        }
146
        $roots[Hash::get($category, 'id')] = [
147
            'id' => Hash::get($category, 'id'),
148
            'label' => Hash::get($category, 'attributes.label') ?? Hash::get($category, 'attributes.name'),
149
            'name' => Hash::get($category, 'attributes.name'),
150
            'object_type_name' => Hash::get($category, 'attributes.object_type_name'),
151
        ];
152
    }
153
154
    /**
155
     * Get all categories roots.
156
     *
157
     * @return array The list of available roots.
158
     */
159
    public function getAllAvailableRoots(): array
160
    {
161
        $roots = ['' => ['id' => 0, 'label' => '-', 'name' => '', 'object_type_name' => '']];
162
        $options = ['page_size' => self::DEFAULT_PAGE_SIZE, 'filter' => ['roots' => 1]];
163
        $pageCount = $page = 1;
164
        $endpoint = '/model/categories';
165
        while ($page <= $pageCount) {
166
            $response = ApiClientProvider::getApiClient()->get($endpoint, $options + compact('page'));
167
            foreach ($response['data'] as $category) {
168
                $this->fillRoots($roots, $category);
169
            }
170
            $pageCount = (int)Hash::get($response, 'meta.pagination.page_count');
171
            $page++;
172
        }
173
174
        return $roots;
175
    }
176
177
    /**
178
     * Save a category using the `/model/` API.
179
     *
180
     * @param array $data Data to save.
181
     * @return array|null The BEdita API response for the saved category.
182
     */
183
    public function save(array $data): ?array
184
    {
185
        $id = Hash::get($data, 'id');
186
        $type = Hash::get($data, 'object_type_name');
187
        unset($data['id']);
188
        $body = [
189
            'data' => [
190
                'type' => 'categories',
191
                'attributes' => $data,
192
            ],
193
        ];
194
195
        $apiClient = ApiClientProvider::getApiClient();
196
        $endpoint = '/model/categories';
197
        $response = null;
198
        if (empty($id)) {
199
            $response = $apiClient->post($endpoint, json_encode($body));
200
        } else {
201
            $body['data']['id'] = $id;
202
203
            $response = $apiClient->patch(sprintf('%s/%s', $endpoint, $id), json_encode($body));
204
        }
205
206
        if (!empty($type)) {
207
            $this->invalidateSchemaCache($type);
208
        }
209
210
        return $response;
211
    }
212
213
    /**
214
     * Delete a category using the `/model/` API.
215
     *
216
     * @param string $id The category id to delete.
217
     * @param string $type The object type name of the category.
218
     * @return array|null The BEdita API response for the deleted category.
219
     */
220
    public function delete(string $id, $type = null): ?array
221
    {
222
        $apiClient = ApiClientProvider::getApiClient();
223
224
        $response = $apiClient->delete(sprintf('/model/%s/%s', 'categories', $id));
225
        if (!empty($type)) {
226
            $this->invalidateSchemaCache($type);
227
        }
228
229
        return $response;
230
    }
231
232
    /**
233
     * Check if categories or tags values has changed.
234
     *
235
     * @param array $oldValue The old value.
236
     * @param array $newValue The new value.
237
     * @return bool True if it has changed, false otherwise.
238
     */
239
    public function hasChanged(array $oldValue, array $newValue): bool
240
    {
241
        $old = (array)Hash::extract($oldValue, '{n}.name');
242
        sort($old);
243
        $old = implode(',', $old);
244
        $new = (array)Hash::extract($newValue, '{n}.name');
245
        sort($new);
246
        $new = implode(',', $new);
247
248
        return $old !== $new;
249
    }
250
251
    /**
252
     * Invalidate schema cache for forms.
253
     *
254
     * @param string $type The object type name of the category.
255
     * @return void
256
     */
257
    private function invalidateSchemaCache(string $type): void
258
    {
259
        $key = CacheTools::cacheKey($type);
260
        Cache::delete($key, SchemaComponent::CACHE_CONFIG);
261
    }
262
}
263