Passed
Pull Request — master (#537)
by Stefano
02:56
created

ExportController::rowFields()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 12
c 1
b 0
f 0
nc 6
nop 2
dl 0
loc 17
rs 9.2222
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
    const DEFAULT_EXPORT_LIMIT = 10000;
32
33
    /**
34
     * Default page size
35
     *
36
     * @var int
37
     */
38
    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
    }
50
51
    /**
52
     * Export data to format specified by user
53
     *
54
     * @return \Cake\Http\Response|null
55
     */
56
    public function export(): ?Response
57
    {
58
        $format = (string)$this->request->getData('format');
59
        if (!$this->Export->checkFormat($format)) {
60
            return null;
61
        }
62
        // check request (allowed methods and required parameters)
63
        $data = $this->checkRequest([
64
            'allowedMethods' => ['post'],
65
            'requiredParameters' => ['objectType'],
66
        ]);
67
        $ids = $this->request->getData('ids');
68
69
        // load data for objects by object type and ids
70
        $rows = $this->rows($data['objectType'], $ids);
71
72
        // create spreadsheet and return as download
73
        $filename = sprintf('%s_%s.%s', $data['objectType'], date('Ymd-His'), $format);
74
        $data = $this->Export->format($format, $rows, $filename);
75
76
        // output
77
        $response = $this->response->withStringBody(Hash::get($data, 'content'));
78
        $response = $response->withType(Hash::get($data, 'contentType'));
79
80
        return $response->withDownload($filename);
81
    }
82
83
    /**
84
     * Obtain csv rows using api get per object type.
85
     * When using parameter ids, get only specified ids,
86
     * otherwise get all by object type.
87
     * First element of data is the attributes/fields array.
88
     *
89
     * @param string $objectType The object type
90
     * @param string $ids Object IDs comma separated string
91
     * @return array
92
     */
93
    protected function rows(string $objectType, string $ids = ''): array
94
    {
95
        if (empty($ids)) {
96
            return $this->rowsAll($objectType);
97
        }
98
99
        $response = $this->apiClient->get($this->apiUrl(), ['filter' => ['id' => $ids]]);
100
        $fields = $this->getFieldNames($response);
101
        $data = [$fields];
102
        $this->fillDataFromResponse($data, $response, $fields);
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type null; however, parameter $response of App\Controller\ExportCon...:fillDataFromResponse() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

102
        $this->fillDataFromResponse($data, /** @scrutinizer ignore-type */ $response, $fields);
Loading history...
103
104
        return $data;
105
    }
106
107
    /**
108
     * Get API Url
109
     *
110
     * @return string
111
     */
112
    protected function apiUrl(): string
113
    {
114
        return sprintf('/%s', $this->request->getData('objectType'));
0 ignored issues
show
Bug introduced by
It seems like $this->request->getData('objectType') can also be of type array; however, parameter $values of sprintf() does only seem to accept double|integer|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

114
        return sprintf('/%s', /** @scrutinizer ignore-type */ $this->request->getData('objectType'));
Loading history...
115
    }
116
117
    /**
118
     * Load all data for a given type using limit and query filters.
119
     *
120
     * @param string $objectType Object type
121
     * @return array
122
     */
123
    protected function rowsAll(string $objectType): array
0 ignored issues
show
Unused Code introduced by
The parameter $objectType is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

123
    protected function rowsAll(/** @scrutinizer ignore-unused */ string $objectType): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
124
    {
125
        $data = $fields = [];
126
        $limit = Configure::read('Export.limit', self::DEFAULT_EXPORT_LIMIT);
127
        $pageCount = $page = 1;
128
        $total = 0;
129
        $query = ['page_size' => self::DEFAULT_PAGE_SIZE] + $this->prepareQuery();
130
        while ($total < $limit && $page <= $pageCount) {
131
            $response = (array)$this->apiClient->get($this->apiUrl(), $query + compact('page'));
132
            $pageCount = (int)Hash::get($response, 'meta.pagination.page_count');
133
            $total += (int)Hash::get($response, 'meta.pagination.page_items');
134
            $page++;
135
136
            if (empty($fields)) {
137
                $fields = $this->getFieldNames($response);
138
                $data = [$fields];
139
            }
140
141
            $this->fillDataFromResponse($data, $response, $fields);
142
        }
143
144
        return $data;
145
    }
146
147
    /**
148
     * Prepare additional API query from POST data
149
     *
150
     * @return array
151
     */
152
    protected function prepareQuery(): array
153
    {
154
        $res = [];
155
        $f = (array)$this->request->getData('filter');
156
        if (!empty($f)) {
157
            $filter = [];
158
            foreach ($f as $v) {
159
                $filter += (array)json_decode($v, true);
160
            }
161
            $res = compact('filter');
162
        }
163
        $q = (string)$this->request->getData('q');
164
        if (!empty($q)) {
165
            $res += compact('q');
166
        }
167
168
        return $res;
169
    }
170
171
    /**
172
     * Fill data array, using response.
173
     * Return the fields representing each data item.
174
     *
175
     * @param array $data The array of data
176
     * @param array $response The response to use as source for data
177
     * @param array $fields Field names array
178
     * @return void
179
     */
180
    protected function fillDataFromResponse(array &$data, array $response, array $fields): void
181
    {
182
        if (empty($response['data'])) {
183
            return;
184
        }
185
186
        // fill row data from response data
187
        foreach ($response['data'] as $val) {
188
            $data[] = $this->rowFields($val, $fields);
189
        }
190
    }
191
192
    /**
193
     * Get field names array using data first element attributes
194
     *
195
     * @param array $response The response from which extract fields
196
     * @return array
197
     */
198
    protected function getFieldNames($response): array
199
    {
200
        $fields = (array)Hash::get($response, 'data.0.attributes');
201
        $meta = (array)Hash::get($response, 'data.0.meta');
202
        unset($meta['extra']);
203
        $fields = array_merge(['id' => ''], $fields, $meta);
204
        $fields = array_merge($fields, (array)Hash::get($response, 'data.0.meta.extra'));
205
206
        return array_keys($fields);
207
    }
208
209
    /**
210
     * Get row data per fields
211
     *
212
     * @param array $data The data
213
     * @param array $fields The fields
214
     * @return array
215
     */
216
    protected function rowFields(array $data, array $fields): array
217
    {
218
        $row = [];
219
        foreach ($fields as $field) {
220
            $row[$field] = '';
221
            if (isset($data[$field])) {
222
                $row[$field] = $this->getValue($data[$field]);
223
            } elseif (isset($data['attributes'][$field])) {
224
                $row[$field] = $this->getValue($data['attributes'][$field]);
225
            } elseif (isset($data['meta'][$field])) {
226
                $row[$field] = $this->getValue($data['meta'][$field]);
227
            } elseif (isset($data['meta']['extra'][$field])) {
228
                $row[$field] = $this->getValue($data['meta']['extra'][$field]);
229
            }
230
        }
231
232
        return $row;
233
    }
234
235
    /**
236
     * Get value from $value.
237
     * If is an array, return json representation.
238
     * Return value otherwise
239
     *
240
     * @param mixed $value The value
241
     * @return mixed
242
     */
243
    protected function getValue($value)
244
    {
245
        if (is_array($value)) {
246
            return json_encode($value);
247
        }
248
249
        return $value;
250
    }
251
}
252