Completed
Push — master ( 7a5132...6f68ab )
by Stefano
21s queued 11s
created

ExportController::related()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 26
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 26
rs 9.8666
c 0
b 0
f 0
cc 2
nc 2
nop 3
1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2018 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 Cake\Core\Configure;
16
use Cake\Http\Response;
17
use Cake\Utility\Hash;
18
19
/**
20
 * Export controller: upload and load using filters
21
 *
22
 * @property \App\Controller\Component\ExportComponent $Export
23
 */
24
class ExportController extends AppController
25
{
26
    /**
27
     * Default max number of exported items
28
     *
29
     * @var int
30
     */
31
    public const DEFAULT_EXPORT_LIMIT = 10000;
32
33
    /**
34
     * Default page size
35
     *
36
     * @var int
37
     */
38
    public const DEFAULT_PAGE_SIZE = 500;
39
40
    /**
41
     * {@inheritDoc}
42
     * {@codeCoverageIgnore}
43
     */
44
    public function initialize(): void
45
    {
46
        parent::initialize();
47
48
        $this->loadComponent('Export');
49
        $this->Security->setConfig('unlockedActions', ['related']);
50
    }
51
52
    /**
53
     * Export data to format specified by user
54
     *
55
     * @return \Cake\Http\Response|null
56
     */
57
    public function export(): ?Response
58
    {
59
        // check request (allowed methods and required parameters)
60
        $data = $this->checkRequest([
61
            'allowedMethods' => ['post'],
62
            'requiredParameters' => ['objectType'],
63
        ]);
64
65
        $format = (string)$this->getRequest()->getData('format');
66
        if (!$this->Export->checkFormat($format)) {
67
            $this->Flash->error(__('Format choosen is not available'));
68
69
            return $this->redirect($this->referer());
70
        }
71
72
        $ids = (string)$this->getRequest()->getData('ids');
73
74
        // load data for objects by object type and ids
75
        $rows = $this->rows($data['objectType'], $ids);
76
77
        // create spreadsheet and return as download
78
        $filename = $this->getFileName($data['objectType'], $format);
79
        $data = $this->Export->format($format, $rows, $filename);
80
81
        // output
82
        $response = $this->getResponse()->withStringBody(Hash::get($data, 'content'));
83
        $response = $response->withType(Hash::get($data, 'contentType'));
84
85
        return $response->withDownload($filename);
86
    }
87
88
    /**
89
     * Export related data to format specified by user
90
     *
91
     * @param string $id The object ID
92
     * @param string $relation The relation name
93
     * @param string $format The file format
94
     * @return \Cake\Http\Response|null
95
     */
96
    public function related(string $id, string $relation, string $format): ?Response
97
    {
98
        // check request (allowed methods and required parameters)
99
        $this->checkRequest([
100
            'allowedMethods' => ['get'],
101
        ]);
102
103
        if (!$this->Export->checkFormat($format)) {
104
            $this->Flash->error(__('Format choosen is not available'));
105
106
            return $this->redirect($this->referer());
107
        }
108
109
        // load related
110
        $objectType = $this->getRequest()->getParam('object_type');
111
        $rows = $this->rowsAllRelated($objectType, $id, $relation);
112
113
        // create spreadsheet and return as download
114
        $filename = sprintf('%s_%s_%s.%s', $objectType, $relation, date('Ymd-His'), $format);
115
        $data = $this->Export->format($format, $rows, $filename);
116
117
        // output
118
        $response = $this->getResponse()->withStringBody(Hash::get($data, 'content'));
119
        $response = $response->withType(Hash::get($data, 'contentType'));
120
121
        return $response->withDownload($filename);
122
    }
123
124
    /**
125
     * Obtain csv rows using api get per object type.
126
     * When using parameter ids, get only specified ids,
127
     * otherwise get all by object type.
128
     * First element of data is the attributes/fields array.
129
     *
130
     * @param string $objectType The object type
131
     * @param string $ids Object IDs comma separated string
132
     * @return array
133
     */
134
    protected function rows(string $objectType, string $ids = ''): array
135
    {
136
        if (empty($ids)) {
137
            return $this->rowsAll($objectType);
138
        }
139
140
        $response = $this->apiClient->get($this->apiPath(), ['filter' => ['id' => $ids]]);
141
        $fields = $this->getFieldNames($response);
142
        $data = [$fields];
143
        $this->fillDataFromResponse($data, $response, $fields);
144
145
        return $data;
146
    }
147
148
    /**
149
     * Get API path.
150
     *
151
     * @return string
152
     */
153
    protected function apiPath(): string
154
    {
155
        return sprintf('/%s', (string)$this->getRequest()->getData('objectType'));
156
    }
157
158
    /**
159
     * Get exported file name.
160
     *
161
     * @param string $type Object or resource type.
162
     * @param string $format The format.
163
     * @return string
164
     */
165
    protected function getFileName(string $type, string $format): string
166
    {
167
        return sprintf('%s_%s.%s', $type, date('Ymd-His'), $format);
168
    }
169
170
    /**
171
     * Load all data for a given type using limit and query filters.
172
     *
173
     * @param string $objectType Object type
174
     * @return array
175
     */
176
    protected function rowsAll(string $objectType): array
177
    {
178
        $data = $fields = [];
179
        $limit = Configure::read('Export.limit', self::DEFAULT_EXPORT_LIMIT);
180
        $pageCount = $page = 1;
181
        $total = 0;
182
        $query = ['page_size' => self::DEFAULT_PAGE_SIZE] + $this->prepareQuery();
183
        while ($total < $limit && $page <= $pageCount) {
184
            $response = (array)$this->apiClient->get($this->apiPath(), $query + compact('page'));
185
            $pageCount = (int)Hash::get($response, 'meta.pagination.page_count');
186
            $total += (int)Hash::get($response, 'meta.pagination.page_items');
187
188
            if ($page === 1) {
189
                $fields = $this->getFieldNames($response);
190
                $data = [$fields];
191
            }
192
193
            $this->fillDataFromResponse($data, $response, $fields);
194
            $page++;
195
        }
196
197
        return $data;
198
    }
199
200
    /**
201
     * Load all related data for a given type and relation using limit and query filters.
202
     *
203
     * @param string $objectType Object type
204
     * @param string $id The object ID
205
     * @param string $relationName The relation name
206
     * @return array
207
     */
208
    protected function rowsAllRelated(string $objectType, string $id, string $relationName): array
209
    {
210
        $data = $fields = [];
211
        $url = sprintf('/%s/%s/%s', $objectType, $id, $relationName);
212
        $limit = Configure::read('Export.limit', self::DEFAULT_EXPORT_LIMIT);
213
        $pageCount = $page = 1;
214
        $total = 0;
215
        $query = ['page_size' => self::DEFAULT_PAGE_SIZE] + $this->prepareQuery();
216
        while ($total < $limit && $page <= $pageCount) {
217
            $response = (array)$this->apiClient->get($url, $query + compact('page'));
218
            $pageCount = (int)Hash::get($response, 'meta.pagination.page_count');
219
            $total += (int)Hash::get($response, 'meta.pagination.page_items');
220
221
            if ($page === 1) {
222
                $fields = $this->getFieldNames($response);
223
                $data = [$fields];
224
            }
225
226
            $this->fillDataFromResponse($data, $response, $fields);
227
            $page++;
228
        }
229
230
        return $data;
231
    }
232
233
    /**
234
     * Prepare additional API query from POST data
235
     *
236
     * @return array
237
     */
238
    protected function prepareQuery(): array
239
    {
240
        $res = [];
241
        $f = (array)$this->getRequest()->getData('filter');
242
        if (!empty($f)) {
243
            $filter = [];
244
            foreach ($f as $v) {
245
                $filter += (array)json_decode($v, true);
246
            }
247
            $res = compact('filter');
248
        }
249
        $q = (string)$this->getRequest()->getData('q');
250
        if (!empty($q)) {
251
            $res += compact('q');
252
        }
253
254
        return $res;
255
    }
256
257
    /**
258
     * Fill data array, using response.
259
     * Return the fields representing each data item.
260
     *
261
     * @param array $data The array of data
262
     * @param array $response The response to use as source for data
263
     * @param array $fields Field names array
264
     * @return void
265
     */
266
    protected function fillDataFromResponse(array &$data, array $response, array $fields): void
267
    {
268
        if (empty($response['data'])) {
269
            return;
270
        }
271
272
        // fill row data from response data
273
        foreach ($response['data'] as $val) {
274
            $data[] = $this->rowFields($val, $fields);
275
        }
276
    }
277
278
    /**
279
     * Get field names array using data first element attributes
280
     *
281
     * @param array $response The response from which extract fields
282
     * @return array
283
     */
284
    protected function getFieldNames($response): array
285
    {
286
        $fields = (array)Hash::get($response, 'data.0.attributes');
287
        $meta = (array)Hash::get($response, 'data.0.meta');
288
        unset($meta['extra']);
289
        $fields = array_merge(['id' => ''], $fields, $meta);
290
        $fields = array_merge($fields, (array)Hash::get($response, 'data.0.meta.extra'));
291
292
        return array_keys($fields);
293
    }
294
295
    /**
296
     * Get row data per fields
297
     *
298
     * @param array $data The data
299
     * @param array $fields The fields
300
     * @return array
301
     */
302
    protected function rowFields(array $data, array $fields): array
303
    {
304
        $row = [];
305
        foreach ($fields as $field) {
306
            $row[$field] = '';
307
            if (isset($data[$field])) {
308
                $row[$field] = $this->getValue($data[$field]);
309
            } elseif (isset($data['attributes'][$field])) {
310
                $row[$field] = $this->getValue($data['attributes'][$field]);
311
            } elseif (isset($data['meta'][$field])) {
312
                $row[$field] = $this->getValue($data['meta'][$field]);
313
            } elseif (isset($data['meta']['extra'][$field])) {
314
                $row[$field] = $this->getValue($data['meta']['extra'][$field]);
315
            }
316
        }
317
318
        return $row;
319
    }
320
321
    /**
322
     * Get value from $value.
323
     * If is an array, return json representation.
324
     * Return value otherwise
325
     *
326
     * @param mixed $value The value
327
     * @return mixed
328
     */
329
    protected function getValue($value)
330
    {
331
        if (is_array($value)) {
332
            return json_encode($value);
333
        }
334
335
        return $value;
336
    }
337
}
338