Passed
Push — master ( 411e7a...439431 )
by Marcel
05:13 queued 15s
created

DatasourceController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 24
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 10
c 1
b 0
f 0
nc 1
nop 11
dl 0
loc 24
rs 9.9332

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
/**
3
 * Analytics
4
 *
5
 * This file is licensed under the Affero General Public License version 3 or
6
 * later. See the LICENSE.md file.
7
 *
8
 * @author Marcel Scherello <[email protected]>
9
 * @copyright 2019-2022 Marcel Scherello
10
 */
11
12
namespace OCA\Analytics\Controller;
13
14
use OCA\Analytics\Datasource\DatasourceEvent;
15
use OCA\Analytics\Datasource\Excel;
16
use OCA\Analytics\Datasource\ExternalFile;
17
use OCA\Analytics\Datasource\File;
18
use OCA\Analytics\Datasource\Github;
19
use OCA\Analytics\Datasource\Json;
20
use OCA\Analytics\Datasource\Regex;
21
use OCP\AppFramework\Controller;
0 ignored issues
show
Bug introduced by
The type OCP\AppFramework\Controller was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
22
use OCP\EventDispatcher\IEventDispatcher;
0 ignored issues
show
Bug introduced by
The type OCP\EventDispatcher\IEventDispatcher was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
23
use OCP\Files\NotFoundException;
0 ignored issues
show
Bug introduced by
The type OCP\Files\NotFoundException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
24
use OCP\IL10N;
0 ignored issues
show
Bug introduced by
The type OCP\IL10N was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
25
use OCP\IRequest;
0 ignored issues
show
Bug introduced by
The type OCP\IRequest was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
26
use Psr\Log\LoggerInterface;
0 ignored issues
show
Bug introduced by
The type Psr\Log\LoggerInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
27
28
class DatasourceController extends Controller
29
{
30
    private $logger;
31
    private $GithubService;
32
    private $FileService;
33
    private $ExternalFileService;
34
    private $RegexService;
35
    private $JsonService;
36
    private $ExcelService;
37
    /** @var IEventDispatcher */
38
    private $dispatcher;
39
    private $l10n;
40
41
    const DATASET_TYPE_GROUP = 0;
42
    const DATASET_TYPE_FILE = 1;
43
    const DATASET_TYPE_INTERNAL_DB = 2;
44
    const DATASET_TYPE_GIT = 3;
45
    const DATASET_TYPE_EXTERNAL_FILE = 4;
46
    const DATASET_TYPE_REGEX = 5;
47
    const DATASET_TYPE_JSON = 6;
48
    const DATASET_TYPE_EXCEL = 7;
49
50
    public function __construct(
51
        string           $appName,
52
        IRequest         $request,
53
        LoggerInterface  $logger,
54
        Github           $GithubService,
55
        File             $FileService,
56
        Regex            $RegexService,
57
        Json             $JsonService,
58
        ExternalFile     $ExternalFileService,
59
        Excel            $ExcelService,
60
        IL10N            $l10n,
61
        IEventDispatcher $dispatcher
62
    )
63
    {
64
        parent::__construct($appName, $request);
65
        $this->logger = $logger;
66
        $this->ExternalFileService = $ExternalFileService;
67
        $this->GithubService = $GithubService;
68
        $this->RegexService = $RegexService;
69
        $this->FileService = $FileService;
70
        $this->JsonService = $JsonService;
71
        $this->ExcelService = $ExcelService;
72
        $this->dispatcher = $dispatcher;
73
        $this->l10n = $l10n;
74
    }
75
76
    /**
77
     * get all data source ids + names
78
     *
79
     * @NoAdminRequired
80
     */
81
    public function index()
82
    {
83
        $datasources = array();
84
        $result = array();
85
        foreach ($this->getDatasources() as $key => $class) {
86
            $datasources[$key] = $class->getName();
87
        }
88
        $result['datasources'] = $datasources;
89
90
        $options = array();
91
        foreach ($this->getDatasources() as $key => $class) {
92
            $options[$key] = $class->getTemplate();
93
        }
94
        $result['options'] = $options;
95
96
        return $result;
97
    }
98
99
    /**
100
     * get all data source templates
101
     *
102
     * @NoAdminRequired
103
     * @return array
104
     */
105
    public function getTemplates()
106
    {
107
        $result = array();
108
        foreach ($this->getDatasources() as $key => $class) {
109
            $result[$key] = $class->getTemplate();
110
        }
111
        return $result;
112
    }
113
114
    /**
115
     * Get the data from a datasource;
116
     *
117
     * @NoAdminRequired
118
     * @param int $datasourceId
119
     * @param $datasetMetadata
120
     * @return array|NotFoundException
121
     */
122
    public function read(int $datasourceId, $datasetMetadata)
123
    {
124
        if (!$this->getDatasources()[$datasourceId]) {
125
            $result['error'] = $this->l10n->t('Data source not available anymore');
0 ignored issues
show
Comprehensibility Best Practice introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = array(); before regardless.
Loading history...
126
            return $result;
127
        }
128
129
        $option = array();
130
        // before 3.1.0, the options were in another format. as of 3.1.0 the standard option array is used
131
        if ($datasetMetadata['link'][0] !== '{') {
132
            $option['link'] = $datasetMetadata['link'];
133
        } else {
134
            $option = json_decode($datasetMetadata['link'], true);
135
        }
136
        $option['user_id'] = $datasetMetadata['user_id'];
137
138
        try {
139
            // read the data from the source
140
            $result = $this->getDatasources()[$datasourceId]->readData($option);
141
142
            // if data source should be timestamped/snapshoted
143
            if (isset($option['timestamp']) and $option['timestamp'] === 'true') {
144
                date_default_timezone_set('UTC');
145
                $result['data'] = array_map(function ($tag) {
146
                    $columns = count($tag);
147
                    if ($columns > 1) {
148
                        // shift values by one dimension and stores date in second column
149
                        return array($tag[$columns - 2], date("Y-m-d H:i:s") . 'Z', $tag[$columns - 1]);
150
                    } else {
151
                        // just return 2 columns if the original data only has one column
152
                        return array(date("Y-m-d H:i:s") . 'Z', $tag[$columns - 1]);
153
                    }}, $result['data']);
154
            }
155
156
            if (isset($datasetMetadata['filteroptions']) && strlen($datasetMetadata['filteroptions']) >> 2) {
157
                // filter data
158
                $result = $this->filterData($result, $datasetMetadata['filteroptions']);
159
                // remove columns and aggregate data
160
                $result = $this->aggregateData($result, $datasetMetadata['filteroptions']);
161
            }
162
163
164
        } catch (\Error $e) {
165
            $result['error'] = $e->getMessage();
166
        }
167
168
        if (empty($result['data'])) {
169
            $result['status'] = 'nodata';
170
        }
171
        return $result;
172
    }
173
174
    /**
175
     * combine internal and registered datasources
176
     * @return array
177
     */
178
    private function getDatasources()
179
    {
180
        return $this->getOwnDatasources() + $this->getRegisteredDatasources();
181
    }
182
183
    /**
184
     * map all internal data sources to their IDs
185
     * @return array
186
     */
187
    private function getOwnDatasources()
188
    {
189
        $dataSources = [];
190
        $dataSources[self::DATASET_TYPE_FILE] = $this->FileService;
191
        $dataSources[self::DATASET_TYPE_EXCEL] = $this->ExcelService;
192
        $dataSources[self::DATASET_TYPE_GIT] = $this->GithubService;
193
        $dataSources[self::DATASET_TYPE_EXTERNAL_FILE] = $this->ExternalFileService;
194
        $dataSources[self::DATASET_TYPE_REGEX] = $this->RegexService;
195
        $dataSources[self::DATASET_TYPE_JSON] = $this->JsonService;
196
        return $dataSources;
197
    }
198
199
    /**
200
     * map all registered data sources to their IDs
201
     * @return array
202
     */
203
    private function getRegisteredDatasources()
204
    {
205
        $dataSources = [];
206
        $event = new DatasourceEvent();
207
        $this->dispatcher->dispatchTyped($event);
208
209
        foreach ($event->getDataSources() as $class) {
210
            try {
211
                $uniqueId = '99' . \OC::$server->get($class)->getId();
212
213
                if (isset($dataSources[$uniqueId])) {
214
                    $this->logger->error(new \InvalidArgumentException('Data source with the same ID already registered: ' . \OC::$server->get($class)->getName()));
215
                    continue;
216
                }
217
                $dataSources[$uniqueId] = \OC::$server->get($class);
218
            } catch (\Error $e) {
219
                $this->logger->error('Can not initialize data source: ' . json_encode($class));
220
                $this->logger->error($e->getMessage());
221
            }
222
        }
223
        return $dataSources;
224
    }
225
226
    /**
227
     * apply the fiven filters to the hole result set
228
     *
229
     * @NoAdminRequired
230
     * @param $data
231
     * @param $filter
232
     * @return array
233
     */
234
    private function filterData($data, $filter)
235
    {
236
        $options = json_decode($filter, true);
237
        if (isset($options['filter'])) {
238
            foreach ($options['filter'] as $key => $value) {
239
                $filterValue = $value['value'];
240
                $filterOption = $value['option'];
241
                $filtered = array();
242
243
                foreach ($data['data'] as $record) {
244
                    if (
245
                        ($filterOption === 'EQ' && $record[$key] === $filterValue)
246
                        || ($filterOption === 'GT' && $record[$key] > $filterValue)
247
                        || ($filterOption === 'LT' && $record[$key] < $filterValue)
248
                        || ($filterOption === 'LIKE' && strpos($record[$key], $filterValue) !== FALSE)
249
                    ) {
250
                        array_push($filtered, $record);
251
                    } else if ($filterOption === 'IN') {
252
                        $filterValues = explode(',', $filterValue);
253
                        if (in_array($record[$key], $filterValues)) {
254
                            array_push($filtered, $record);
255
                        }
256
                    }
257
                }
258
                $data['data'] = $filtered;
259
            }
260
        }
261
        return $data;
262
    }
263
264
    private function aggregateData($data, $filter)
265
    {
266
        $options = json_decode($filter, true);
267
        if (isset($options['drilldown'])) {
268
            // Sort the indices in descending order
269
            $sortedIndices = array_keys($options['drilldown']);
270
            rsort($sortedIndices);
271
272
            foreach ($sortedIndices as $removeIndex) {
273
                $aggregatedData = [];
274
275
                // remove the header of the column which is not needed
276
                unset($data['header'][$removeIndex]);
277
                $data['header'] = array_values($data['header']);
278
279
                // remove the column of the data
280
                foreach ($data['data'] as $row) {
281
                    // Remove the desired column by its index
282
                    unset($row[$removeIndex]);
283
284
                    // The last column is assumed to always be the value
285
                    $value = array_pop($row);
286
287
                    // If there are no columns left except the value column, insert a dummy
288
                    if (empty($row)) {
289
                        $key = 'xxsingle_valuexx';
290
                    } else {
291
                        // Use remaining columns as key
292
                        $key = implode("|", $row);
293
                    }
294
295
                    if (!isset($aggregatedData[$key])) {
296
                        $aggregatedData[$key] = 0;
297
                    }
298
                    $aggregatedData[$key] += $value;
299
                }
300
301
                // Convert the associative array to the desired format
302
                $result = [];
303
                foreach ($aggregatedData as $aKey => $aValue) {
304
                    // If only the value column remains, append its total value
305
                    if ($aKey === 'xxsingle_valuexx') {
306
                        $aKey = $this->l10n->t('Total');
307
                        // Add an empty column to the header because of the "total" row description
308
                        array_unshift($data['header'], '');
309
                    }
310
                    $result[] = [$aKey, $aValue];
311
                }
312
                $data['data'] = $result;
313
            }
314
        }
315
        return $data;
316
    }
317
}