Completed
Push — master ( b32b81...2b4c6f )
by Stefano
16s queued 11s
created

ExportController::export()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 2
eloc 13
c 2
b 1
f 0
nc 2
nop 0
dl 0
loc 25
rs 9.8333
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
     * {@inheritDoc}
35
     * {@codeCoverageIgnore}
36
     */
37
    public function initialize(): void
38
    {
39
        parent::initialize();
40
41
        $this->loadComponent('Export');
42
    }
43
44
    /**
45
     * Export data to format specified by user
46
     *
47
     * @return \Cake\Http\Response|null
48
     */
49
    public function export(): ?Response
50
    {
51
        $format = (string)$this->request->getData('format');
52
        if (!$this->Export->checkFormat($format)) {
53
            return null;
54
        }
55
        // check request (allowed methods and required parameters)
56
        $data = $this->checkRequest([
57
            'allowedMethods' => ['post'],
58
            'requiredParameters' => ['objectType'],
59
        ]);
60
        $ids = $this->request->getData('ids');
61
62
        // load data for objects by object type and ids
63
        $rows = $this->rows($data['objectType'], $ids);
64
65
        // create spreadsheet and return as download
66
        $filename = sprintf('%s_%s.%s', $data['objectType'], date('Ymd-His'), $format);
67
        $data = $this->Export->format($format, $rows, $filename);
68
69
        // output
70
        $response = $this->response->withStringBody(Hash::get($data, 'content'));
71
        $response = $response->withType(Hash::get($data, 'contentType'));
72
73
        return $response->withDownload($filename);
74
    }
75
76
    /**
77
     * Obtain csv rows using api get per object type.
78
     * When using parameter ids, get only specified ids,
79
     * otherwise get all by object type.
80
     * First element of data is the attributes/fields array.
81
     *
82
     * @param string $objectType The object type
83
     * @param string $ids Object IDs comma separated string
84
     * @return array
85
     */
86
    protected function rows(string $objectType, string $ids = ''): array
87
    {
88
        if (empty($ids)) {
89
            return $this->rowsAll($objectType);
90
        }
91
92
        $data = [];
93
        $response = $this->apiClient->getObjects($objectType, ['filter' => ['id' => $ids]]);
94
        $fields = $this->fillDataFromResponse($data, $response);
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

94
        $fields = $this->fillDataFromResponse($data, /** @scrutinizer ignore-type */ $response);
Loading history...
95
        array_unshift($data, $fields);
96
97
        return $data;
98
    }
99
100
    /**
101
     * Load all data for a given type using limit and query filters.
102
     *
103
     * @param string $objectType Object type
104
     * @return array
105
     */
106
    protected function rowsAll(string $objectType): array
107
    {
108
        $data = [];
109
        $limit = Configure::read('Export.limit', self::DEFAULT_EXPORT_LIMIT);
110
        $pageCount = $page = 1;
111
        $total = 0;
112
        $query = ['page_size' => 100] + $this->prepareQuery();
113
        while ($total < $limit && $page <= $pageCount) {
114
            $response = (array)$this->apiClient->getObjects($objectType, $query + compact('page'));
115
            $pageCount = (int)Hash::get($response, 'meta.pagination.page_count');
116
            $total += (int)Hash::get($response, 'meta.pagination.page_items');
117
            $page++;
118
119
            $fields = $this->fillDataFromResponse($data, $response);
120
        }
121
        array_unshift($data, $fields);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $fields does not seem to be defined for all execution paths leading up to this point.
Loading history...
122
123
        return $data;
124
    }
125
126
    /**
127
     * Prepare additional API query from POST data
128
     *
129
     * @return array
130
     */
131
    protected function prepareQuery(): array
132
    {
133
        $res = [];
134
        $f = (array)$this->request->getData('filter');
135
        if (!empty($f)) {
136
            $filter = [];
137
            foreach ($f as $v) {
138
                $filter += (array)json_decode($v, true);
139
            }
140
            $res = compact('filter');
141
        }
142
        $q = (string)$this->request->getData('q');
143
        if (!empty($q)) {
144
            $res += compact('q');
145
        }
146
147
        return $res;
148
    }
149
150
    /**
151
     * Fill data array, using response.
152
     * Return the fields representing each data item.
153
     *
154
     * @param array $data The array of data
155
     * @param array $response The response to use as source for data
156
     * @return array The fields representing each data item
157
     */
158
    private function fillDataFromResponse(array &$data, array $response): array
159
    {
160
        if (empty($response['data'])) {
161
            return [];
162
        }
163
164
        // get fields for response 'attributes' and 'meta'
165
        $fields = $this->getFields($response);
166
        array_unshift($fields, 'id');
167
        $metaFields = $this->getFields($response, 'meta');
168
169
        // fill row data from response data
170
        foreach ($response['data'] as $key => $val) {
171
            $row = [];
172
173
            // fill row fields
174
            $this->fillRowFields($row, $val, $fields);
175
176
            // fill row data for meta
177
            $this->fillRowFields($row, $val, $metaFields);
178
179
            $data[] = $row;
180
        }
181
        foreach ($metaFields as $f) {
182
            $fields[$f] = $f;
183
        }
184
185
        return $fields;
186
    }
187
188
    /**
189
     * Get fields array using data first element attributes
190
     *
191
     * @param array $response The response from which extract fields
192
     * @param string $key The key
193
     * @return array
194
     */
195
    private function getFields($response, $key = 'attributes'): array
196
    {
197
        $data = (array)Hash::get($response, sprintf('data.0.%s', $key), []);
198
199
        return array_keys($data);
200
    }
201
202
    /**
203
     * Fill row data per fields
204
     *
205
     * @param array $row The row to be filled with data
206
     * @param mixed $data The data
207
     * @param array $fields The fields
208
     * @return void
209
     */
210
    private function fillRowFields(&$row, $data, $fields): void
211
    {
212
        foreach ($fields as $field) {
213
            $row[$field] = '';
214
            if (isset($data[$field])) {
215
                $row[$field] = $this->getValue($data[$field]);
216
            } elseif (isset($data['attributes'][$field])) {
217
                $row[$field] = $this->getValue($data['attributes'][$field]);
218
            } elseif (isset($data['meta'][$field])) {
219
                $row[$field] = $this->getValue($data['meta'][$field]);
220
            }
221
        }
222
    }
223
224
    /**
225
     * Get value from $value.
226
     * If is an array, return json representation.
227
     * Return value otherwise
228
     *
229
     * @param mixed $value The value
230
     * @return mixed
231
     */
232
    private function getValue($value)
233
    {
234
        if (is_array($value)) {
235
            return json_encode($value);
236
        }
237
238
        return $value;
239
    }
240
}
241