GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — develop ( a8a408...275d09 )
by Bob Olde
03:55
created

ExportService::getData()   C

Complexity

Conditions 7
Paths 16

Size

Total Lines 32
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7.0671

Importance

Changes 16
Bugs 3 Features 6
Metric Value
c 16
b 3
f 6
dl 0
loc 32
ccs 16
cts 18
cp 0.8889
rs 6.7272
cc 7
eloc 13
nc 16
nop 1
crap 7.0671
1
<?php
2
3
namespace Craft;
4
5
/**
6
 * Export service.
7
 *
8
 * Handles common export logics.
9
 *
10
 * @author    Bob Olde Hampsink <[email protected]>
11
 * @copyright Copyright (c) 2015, Bob Olde Hampsink
12
 * @license   MIT
13
 *
14
 * @link      http://github.com/boboldehampsink
15
 */
16
class ExportService extends BaseApplicationComponent
17
{
18
    /**
19
     * Contains the working export service's name.
20
     *
21
     * @var IExportElementType
22
     */
23
    private $_service;
24
25
    /**
26
     * Custom <tr> paths.
27
     *
28
     * @var array
29
     */
30
    public $customTableRowPaths = array();
31
32
    /**
33
     * Whether custom table row paths have been loaded.
34
     *
35
     * @var bool
36
     */
37
    private $_loadedTableRowPaths = false;
38
39
    /**
40
     * Saves an export map to the database.
41
     *
42
     * @param array $settings
43
     * @param array $map
44
     */
45 2
    public function saveMap(array $settings, array $map)
46
    {
47
        // Unset non-map settings
48 2
        unset($settings['limit'], $settings['offset']);
49 2
        ksort($settings);
50
51
        // Set criteria
52 2
        $criteria = new \CDbCriteria();
53 2
        $criteria->condition = 'settings = :settings';
54 2
        $criteria->params = array(
55 2
            ':settings' => JsonHelper::encode($settings),
56
        );
57
58
        // Check if we have a map already
59 2
        $mapRecord = $this->findMap($criteria);
60
61 2
        if (!count($mapRecord) || $mapRecord->settings != $settings) {
62
63
            // Save settings and map to database
64 1
            $mapRecord = $this->getNewMap();
65 1
            $mapRecord->settings = $settings;
66 1
        }
67
68
        // Save new map to db
69 2
        $mapRecord->map = $map;
70 2
        $mapRecord->save(false);
71 2
    }
72
73
    /**
74
     * @codeCoverageIgnore
75
     *
76
     * @param \CDbCriteria $criteria
77
     *
78
     * @return Export_MapRecord|array|null
79
     */
80
    public function findMap(\CDbCriteria $criteria)
81
    {
82
        return Export_MapRecord::model()->find($criteria);
83
    }
84
85
    /**
86
     * @codeCoverageIgnore
87
     *
88
     * @return Export_MapRecord
89
     */
90
    protected function getNewMap()
91
    {
92
        return new Export_MapRecord();
93
    }
94
95
    /**
96
     * Download the export csv.
97
     *
98
     * @param array $settings
99
     *
100
     * @return string
101
     *
102
     * @throws Exception
103
     */
104 7
    public function download(array $settings)
105
    {
106
        // Get max power
107 7
        craft()->config->maxPowerCaptain();
108
109
        // Check what service we're gonna need
110 7
        if (!($this->_service = $this->getService($settings['type']))) {
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getService($settings['type']) can also be of type boolean. However, the property $_service is declared as type object<Craft\IExportElementType>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
111 1
            throw new Exception(Craft::t('Unknown Element Type Service called.'));
112
        }
113
114
        // Get delimiter
115 6
        $delimiter = craft()->plugins->callFirst('registerExportCsvDelimiter');
116 6
        $delimiter = is_null($delimiter) ? ',' : $delimiter;
117
118
        // Open output buffer
119 6
        ob_start();
120
121
        // Write to output stream
122 6
        $export = fopen('php://output', 'w');
123
124
        // Get data
125 6
        $data = $this->getData($settings);
126
127
        // If there is data, process
128 6
        if (count($data)) {
129
130
            // Put down columns
131 3
            fputcsv($export, $this->parseColumns($settings), $delimiter);
132
133
            // Loop trough data
134 3
            foreach ($data as $element) {
0 ignored issues
show
Bug introduced by
The expression $data of type array|string is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
135
136
                // Fetch element in case of element id
137 3
                if (is_numeric($element)) {
138 2
                    $element = craft()->elements->getElementById($element, $settings['type']);
139 2
                }
140
141
                // Get fields
142 3
                $fields = $this->parseFields($settings, $element);
143
144
                // Gather row data
145 3
                $rows = array();
146
147
                // Loop trough the fields
148 3
                foreach ($fields as $handle => $data) {
149
150
                    // Parse element data
151 3
                    $data = $this->parseElementData($handle, $data);
152
153
                    // Parse field data
154 3
                    $data = $this->parseFieldData($handle, $data);
155
156
                    // Encode and add to rows
157 3
                    $rows[] = StringHelper::convertToUTF8($data);
158 3
                }
159
160
                // Add rows to export
161 3
                fputcsv($export, $rows, $delimiter);
162 3
            }
163 3
        }
164
165
        // Close buffer and return data
166 6
        fclose($export);
167 6
        $data = ob_get_clean();
168
169
        // Use windows friendly newlines
170 6
        $data = str_replace("\n", "\r\n", $data);
171
172
        // Return the data to controller
173 6
        return $data;
174
    }
175
176
    /**
177
     * Get service to use for exporting.
178
     *
179
     * @param string $elementType
180
     *
181
     * @return object|bool
182
     */
183 4
    public function getService($elementType)
184
    {
185
        // Check if there's a service for this element type elsewhere
186 4
        $service = craft()->plugins->callFirst('registerExportService', array(
187 4
            'elementType' => $elementType,
188 4
        ));
189
190
        // If not, do internal check
191 4
        if ($service == null) {
192
193
            // Get from right elementType
194 4
            $service = 'export_'.strtolower($elementType);
195 4
        }
196
197
        // Check if elementtype can be imported
198 4
        if (isset(craft()->$service) && craft()->$service instanceof IExportElementType) {
199
200
            // Return this service
201 3
            return craft()->$service;
202
        }
203
204 1
        return false;
205
    }
206
207
    /**
208
     * Get path to fieldtype's custom <tr> template.
209
     *
210
     * @param string $fieldHandle
211
     *
212
     * @return string
213
     */
214 2
    public function getCustomTableRow($fieldHandle)
215
    {
216
        // If table row paths haven't been loaded
217 2
        if (!$this->_loadedTableRowPaths) {
218
219
            // Call hook for all plugins
220 2
            $responses = craft()->plugins->call('registerExportTableRowPaths');
221
222
            // Loop through responses from each plugin
223 2
            foreach ($responses as $customPaths) {
224
225
                // Append custom paths to master list
226 1
                $this->customTableRowPaths = array_merge($this->customTableRowPaths, $customPaths);
227 2
            }
228
229
            // Table row paths have been loaded
230 2
            $this->_loadedTableRowPaths = true;
231 2
        }
232
233
        // If fieldtype has been registered and is not falsey
234 2
        if (array_key_exists($fieldHandle, $this->customTableRowPaths) && $this->customTableRowPaths[$fieldHandle]) {
235
236
            // Return specified custom path
237 1
            return $this->customTableRowPaths[$fieldHandle];
238
        }
239
240 1
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by Craft\ExportService::getCustomTableRow of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
241
    }
242
243
    /**
244
     * Get data from sources.
245
     *
246
     * @param array $settings
247
     *
248
     * @return array
249
     */
250 6
    protected function getData(array $settings)
251
    {
252
        // Get other sources
253 6
        $sources = craft()->plugins->call('registerExportSource', array($settings));
254
255
        // Loop through sources, see if we can get any data
256 6
        $data = array();
257 6
        foreach ($sources as $plugin) {
258 1
            if (is_array($plugin)) {
259 1
                foreach ($plugin as $source) {
260 1
                    $data[] = $source;
261 1
                }
262 1
            }
263 6
        }
264
265
        // Cut up data from source
266 6
        if (!empty($settings['offset']) && !empty($settings['limit'])) {
267
            $data = array_slice($data, $settings['offset'], $settings['limit']);
268
        }
269
270
        // If no data from source, get data by ourselves
271 6
        if (!count($data)) {
272
273
            // Find data
274 5
            $criteria = $this->_service->setCriteria($settings);
275
276
            // Gather element ids
277 5
            $data = $criteria->ids();
278 5
        }
279
280 6
        return $data;
281
    }
282
283
    /**
284
     * Parse fields.
285
     *
286
     * @param array $settings
287
     * @param       $element
288
     *
289
     * @return array
290
     */
291 3
    protected function parseFields(array $settings, $element)
292
    {
293 3
        $fields = array();
294
295
        // Only get element attributes and content attributes
296 3
        $attributes = $element;
297 3
        if ($element instanceof BaseElementModel) {
1 ignored issue
show
Bug introduced by
The class Craft\BaseElementModel does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
298
299
            // Get service
300 2
            $attributes = $this->_service->getAttributes($settings['map'], $element);
301 2
        }
302
303
        // Loop through the map
304 3
        foreach ($settings['map'] as $handle => $data) {
305
306
            // Only get checked fields
307 3
            if ($data['checked'] == '1' && (array_key_exists($handle, $attributes) || array_key_exists(substr($handle, 0, 5), $attributes))) {
308
309
                // Fill them with data
310 3
                $fields[$handle] = $attributes[$handle];
311 3
            }
312 3
        }
313
314 3
        return $fields;
315
    }
316
317
    /**
318
     * Parse column names.
319
     *
320
     * @param array $settings [description]
321
     *
322
     * @return string
323
     */
324 3
    protected function parseColumns(array $settings)
325
    {
326 3
        $columns = array();
327
328
        // Loop trough map
329 3
        foreach ($settings['map'] as $handle => $data) {
330
331
            // If checked
332 3
            if ($data['checked'] == 1) {
333
334
                // Add column
335 3
                $columns[] = StringHelper::convertToUTF8($data['label']);
336 3
            }
337 3
        }
338
339 3
        return $columns;
340
    }
341
342
    /**
343
     * Parse reserved element values.
344
     *
345
     * @param  $handle
346
     * @param  $data
347
     *
348
     * @return string
349
     */
350 3
    protected function parseElementData($handle, $data)
351
    {
352
        switch ($handle) {
353
354 3
            case ExportModel::HandleAuthor:
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
355
356
                // Get username of author
357 1
                $data = craft()->users->getUserById($data)->username;
358
359 1
                break;
360
361 3
            case ExportModel::HandleEnabled:
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
362
363
                // Make data human readable
364
                switch ($data) {
365
366 2
                    case '0':
367 1
                        $data = Craft::t('No');
368 1
                        break;
369
370 1
                    case '1':
371 1
                        $data = Craft::t('Yes');
372 1
                        break;
373
374
                }
375
376 2
                break;
377
378 3
            case ExportModel::HandlePostDate:
379 3
            case ExportModel::HandleExpiryDate:
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
380
381
                // Resolve to string
382 1
                $data = (string) $data;
383
384 1
                break;
385
386
        }
387
388 3
        return $data;
389
    }
390
391
    /**
392
     * Parse field values.
393
     *
394
     * @param string $handle
395
     * @param mixed  $data
396
     *
397
     * @return string
398
     */
399 11
    public function parseFieldData($handle, $data)
400
    {
401
402
        // Do we have any data at all
403 11
        if (!is_null($data)) {
404
405
            // Get field info
406 11
            $field = craft()->fields->getFieldByHandle($handle);
407
408
            // If it's a field ofcourse
409 11
            if (!is_null($field)) {
410
411
                // For some fieldtypes the're special rules
412 11
                switch ($field->type) {
413
414 11
                    case ExportModel::FieldTypeEntries:
415 11
                    case ExportModel::FieldTypeCategories:
416 11
                    case ExportModel::FieldTypeAssets:
417 11
                    case ExportModel::FieldTypeUsers:
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
418
419
                        // Show names
420 1
                        $data = $data instanceof ElementCriteriaModel ? implode(', ', $data->find()) : $data;
1 ignored issue
show
Bug introduced by
The class Craft\ElementCriteriaModel does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
421
422 1
                        break;
423
424 10
                    case ExportModel::FieldTypeLightswitch:
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
425
426
                        // Make data human readable
427
                        switch ($data) {
428
429 2
                            case '0':
430 1
                                $data = Craft::t('No');
431 1
                                break;
432
433 1
                            case '1':
434 1
                                $data = Craft::t('Yes');
435 1
                                break;
436
437
                        }
438
439 2
                        break;
440
441 8
                    case ExportModel::FieldTypeTable:
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
442
443
                        // Parse table checkboxes
444 1
                        $table = array();
445 1
                        foreach ($data as $row) {
0 ignored issues
show
Bug introduced by
The expression $data of type object|integer|double|string|array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
446
447
                            // Keep track of column #
448 1
                            $i = 1;
449
450
                            // Loop through columns
451 1
                            foreach ($row as $column => $value) {
452
453
                                // Get column
454 4
                                $column = isset($field->settings['columns'][$column]) ? $field->settings['columns'][$column] : (isset($field->settings['columns']['col'.$i]) ? $field->settings['columns']['col'.$i] : array('type' => 'dummy'));
455
456
                                // Keep track of column #
457 1
                                ++$i;
458
459
                                // Parse
460 1
                                $table[] = $column['type'] == 'checkbox' ? ($value == 1 ? Craft::t('Yes') : Craft::t('No')) : $value;
461 1
                            }
462 1
                        }
463
464
                        // Return parsed data as array
465 1
                        $data = $table;
466
467 1
                        break;
468
469 7
                    case ExportModel::FieldTypeRichText:
470 7
                    case ExportModel::FieldTypeDate:
471 7
                    case ExportModel::FieldTypeRadioButtons:
472 7
                    case ExportModel::FieldTypeDropdown:
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
473
474
                        // Resolve to string
475 1
                        $data = (string) $data;
476
477 1
                        break;
478
479 6
                    case ExportModel::FieldTypeCheckboxes:
480 6
                    case ExportModel::FieldTypeMultiSelect:
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
481
482
                        // Parse multi select values
483 1
                        $multi = array();
484 1
                        foreach ($data as $row) {
0 ignored issues
show
Bug introduced by
The expression $data of type object|integer|double|string|array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
485 1
                            $multi[] = $row->value;
486 1
                        }
487
488
                        // Return parsed data as array
489 1
                        $data = $multi;
490
491 1
                        break;
492
493 11
                }
494 11
            }
495
496
            // Get other operations
497 11
            craft()->plugins->call('registerExportOperation', array(&$data, $handle));
498 11
        } else {
499
500
            // Don't return null, return empty
501 1
            $data = '';
502
        }
503
504
        // If it's an object or an array, make it a string
505 11
        if (is_array($data)) {
506 2
            $data = StringHelper::arrayToString(ArrayHelper::filterEmptyStringsFromArray(ArrayHelper::flattenArray($data)), ', ');
507 2
        }
508
509
        // If it's an object, make it a string
510 11
        if (is_object($data)) {
511 1
            $data = StringHelper::arrayToString(ArrayHelper::filterEmptyStringsFromArray(ArrayHelper::flattenArray(get_object_vars($data))), ', ');
512 1
        }
513
514 11
        return $data;
515
    }
516
}
517