BulkController::saveCategories()   A
last analyzed

Complexity

Conditions 3
Paths 8

Size

Total Lines 12
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 12
rs 9.9332
cc 3
nc 8
nop 0
1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2021 ChannelWeb 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\Core\Bulk\CustomBulkActionInterface;
16
use BEdita\SDK\BEditaClientException;
17
use Cake\Core\App;
18
use Cake\Http\Response;
19
use Cake\Utility\Hash;
20
use Psr\Log\LogLevel;
21
22
/**
23
 * Bulk Controller.
24
 */
25
class BulkController extends AppController
26
{
27
    /**
28
     * Object type currently used
29
     *
30
     * @var string
31
     */
32
    protected $objectType = null;
33
34
    /**
35
     * Object type is abstract
36
     *
37
     * @var bool
38
     */
39
    protected $abstractType = null;
40
41
    /**
42
     * Selected objects IDs
43
     *
44
     * @var array
45
     */
46
    protected $ids = [];
47
48
    /**
49
     * Selected objects (in the format [{id:<id>, type:<type>}, ...])
50
     *
51
     * @var array
52
     */
53
    protected $objects = [];
54
55
    /**
56
     * Selected categories
57
     *
58
     * @var array|string
59
     */
60
    protected $categories = [];
61
62
    /**
63
     * Errors
64
     *
65
     * @var array
66
     */
67
    protected $errors = [];
68
69
    /**
70
     * Saved ids
71
     *
72
     * @var array
73
     */
74
    protected $saved = [];
75
76
    /**
77
     * @inheritDoc
78
     */
79
    public function initialize(): void
80
    {
81
        parent::initialize();
82
83
        $this->objectType = $this->getRequest()->getParam('object_type');
84
        $this->abstractType = in_array($this->objectType, $this->Schema->abstractTypes());
85
    }
86
87
    /**
88
     * Bulk change attribute value for selected ids.
89
     *
90
     * @return \Cake\Http\Response|null
91
     */
92
    public function attribute(): ?Response
93
    {
94
        $requestData = $this->getRequest()->getData();
95
        $this->objects = Hash::get($requestData, 'objects');
96
        $this->objects = is_string($this->objects) ? json_decode($this->objects, true) : $this->objects;
97
        $this->saveAttribute((array)Hash::get($requestData, 'attributes'));
98
        $this->showResult();
99
100
        return $this->modulesListRedirect();
101
    }
102
103
    /**
104
     * Bulk custom action for selected ids.
105
     *
106
     * @return \Cake\Http\Response|null
107
     */
108
    public function custom(): ?Response
109
    {
110
        $requestData = $this->request->getData();
111
        $this->ids = explode(',', (string)Hash::get($requestData, 'ids'));
112
        $this->performCustomAction((string)Hash::get($requestData, 'custom_action'));
113
        $this->showResult();
114
115
        return $this->modulesListRedirect();
116
    }
117
118
    /**
119
     * Perform bulk action via custom class.
120
     *
121
     * @param string $bulkClass Custom action class name.
122
     * @return void
123
     */
124
    protected function performCustomAction(string $bulkClass): void
125
    {
126
        $class = App::className($bulkClass);
127
        if (empty($class)) {
128
            $this->errors[] = __('Custom action class {0} not found', $bulkClass);
129
130
            return;
131
        }
132
        $bulkAction = new $class();
133
        if (!$bulkAction instanceof CustomBulkActionInterface) {
134
            $this->errors[] = __('Custom action class {0} is not valid', $bulkClass);
135
136
            return;
137
        }
138
139
        $this->errors = $bulkAction->bulkAction($this->ids, $this->objectType);
140
    }
141
142
    /**
143
     * Redirect to modules index.
144
     *
145
     * @return \Cake\Http\Response|null
146
     */
147
    protected function modulesListRedirect(): ?Response
148
    {
149
        return $this->redirect([
150
            '_name' => 'modules:list',
151
            'object_type' => $this->objectType,
152
            '?' => $this->request->getQuery(),
153
        ]);
154
    }
155
156
    /**
157
     * Bulk associate categories to selected ids.
158
     *
159
     * @return \Cake\Http\Response|null
160
     */
161
    public function categories(): ?Response
162
    {
163
        $requestData = $this->getRequest()->getData();
164
        $this->ids = explode(',', (string)Hash::get($requestData, 'ids'));
165
        $this->categories = (string)Hash::get($requestData, 'categories');
166
        $this->loadCategories();
167
        $this->saveCategories();
168
        $this->showResult();
169
170
        return $this->modulesListRedirect();
171
    }
172
173
    /**
174
     * Bulk associate position in tree to selected ids.
175
     *
176
     * @return \Cake\Http\Response|null
177
     */
178
    public function position(): ?Response
179
    {
180
        $requestData = $this->getRequest()->getData();
181
        $this->ids = explode(',', (string)Hash::get($requestData, 'ids'));
182
        $folder = (string)Hash::get($requestData, 'folderSelected');
183
        $action = (string)Hash::get($requestData, 'action');
184
        if ($action === 'copy') {
185
            $this->copyToPosition($folder);
186
        } else { // move
187
            $this->moveToPosition($folder);
188
        }
189
        $this->showResult();
190
191
        return $this->modulesListRedirect();
192
    }
193
194
    /**
195
     * Save attribute for selected objects.
196
     *
197
     * @param array $attributes The attributes data
198
     * @return void
199
     */
200
    protected function saveAttribute(array $attributes): void
201
    {
202
        $itemsMap = [];
203
        foreach ($this->objects as $item) {
204
            $itemsMap[$item['type']][] = $item['id'];
205
        }
206
        $result = $this->apiClient->bulkEdit($itemsMap, $attributes);
207
        $this->errors = (array)Hash::get($result, 'data.errors');
208
        $this->saved = (array)Hash::get($result, 'data.saved');
209
    }
210
211
    /**
212
     * Load categories by type and ids.
213
     *
214
     * @return void
215
     */
216
    protected function loadCategories(): void
217
    {
218
        $schema = (array)$this->Schema->getSchema($this->objectType);
219
        $schemaCategories = (array)Hash::extract($schema, 'categories');
220
        $ids = explode(',', (string)$this->categories);
221
        $this->categories = [];
222
        foreach ($schemaCategories as $schemaCategory) {
223
            if (in_array($schemaCategory['id'], $ids)) {
224
                $this->categories[] = $schemaCategory['name'];
225
            }
226
        }
227
    }
228
229
    /**
230
     * Save categories for selected objects.
231
     *
232
     * @return void
233
     */
234
    protected function saveCategories(): void
235
    {
236
        foreach ($this->ids as $id) {
237
            try {
238
                $object = $this->apiClient->getObject($id, $this->objectType, ['fields' => 'categories']);
239
                $objectCategories = (array)Hash::extract($object, 'data.attributes.categories.{n}.name');
240
                $this->categories = array_unique(array_merge((array)$this->categories, $objectCategories));
241
                $payload = compact('id');
242
                $payload['categories'] = $this->remapCategories((array)$this->categories);
243
                $this->apiClient->save($this->objectType, $payload);
244
            } catch (BEditaClientException $e) {
245
                $this->errors[] = ['id' => $id, 'message' => $e->getAttributes()];
246
            }
247
        }
248
    }
249
250
    /**
251
     * Remap categories, returning an array of items 'name':<category>
252
     *
253
     * @param array $input The input categories
254
     * @return array
255
     */
256
    protected function remapCategories(array $input): array
257
    {
258
        return array_map(function ($category) {
259
260
            return ['name' => $category];
261
        }, $input);
262
    }
263
264
    /**
265
     * Copy selected objects to position.
266
     *
267
     * @param string $position The folder ID
268
     * @return void
269
     */
270
    protected function copyToPosition(string $position): void
271
    {
272
        $ids = array_reverse($this->ids);
273
        foreach ($ids as $id) {
274
            try {
275
                $this->apiClient->addRelated($position, 'folders', 'children', [
276
                    [
277
                        'id' => $id,
278
                        'type' => $this->getType($id),
279
                    ],
280
                ]);
281
            } catch (BEditaClientException $e) {
282
                $this->errors[] = ['id' => $id, 'message' => $e->getAttributes()];
283
            }
284
        }
285
    }
286
287
    /**
288
     * Move selected objects to position.
289
     *
290
     * @param string $position The folder ID
291
     * @return void
292
     */
293
    protected function moveToPosition(string $position): void
294
    {
295
        $ids = array_reverse($this->ids);
296
        foreach ($ids as $id) {
297
            try {
298
                $this->apiClient->replaceRelated($id, $this->getType($id), 'parents', [
299
                    'id' => $position,
300
                    'type' => 'folders',
301
                ]);
302
            } catch (BEditaClientException $e) {
303
                $this->errors[] = ['id' => $id, 'message' => $e->getAttributes()];
304
            }
305
        }
306
    }
307
308
    /**
309
     * Display success or error messages.
310
     *
311
     * @return void
312
     */
313
    protected function showResult(): void
314
    {
315
        if (empty($this->errors)) {
316
            $count = count($this->saved) > 0 ? count($this->saved) : count($this->ids);
317
            $this->Flash->success(__('Bulk action performed on {0} objects', $count));
318
319
            return;
320
        }
321
        $this->log('Error: ' . json_encode($this->errors), LogLevel::ERROR);
322
        $this->Flash->error(__('Bulk Action failed on: '), ['params' => $this->errors]);
323
    }
324
325
    /**
326
     * Return errors.
327
     *
328
     * @return array
329
     * @codeCoverageIgnore
330
     */
331
    public function getErrors(): array
332
    {
333
        return $this->errors;
334
    }
335
336
    /**
337
     * Get concrete object type when it is abstract, otherwise just return object type.
338
     *
339
     * @param string $id The object ID
340
     * @return string
341
     */
342
    protected function getType(string $id): string
343
    {
344
        if (!$this->abstractType) {
345
            return $this->objectType;
346
        }
347
        $response = (array)$this->apiClient->get(sprintf('/%s/%s', $this->objectType, $id));
348
349
        return (string)Hash::get($response, 'data.type');
350
    }
351
}
352