BulkController::remapCategories()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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