Completed
Pull Request — master (#1596)
by Thomas
03:22
created

Expander::expandProperties()   D

Complexity

Conditions 15
Paths 115

Size

Total Lines 92
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 240

Importance

Changes 0
Metric Value
dl 0
loc 92
ccs 0
cts 50
cp 0
rs 4.6692
c 0
b 0
f 0
cc 15
eloc 53
nc 115
nop 1
crap 240

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * This file is part of the browscap package.
4
 *
5
 * Copyright (c) 1998-2017, Browser Capabilities Project
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
declare(strict_types = 1);
12
namespace Browscap\Data;
13
14
use Browscap\Data\Helper\CheckDeviceData;
15
use Browscap\Data\Helper\CheckEngineData;
16
use Browscap\Data\Helper\CheckPlatformData;
17
use Psr\Log\LoggerInterface;
18
19
/**
20
 * Class Expander
21
 *
22
 * @category   Browscap
23
 *
24
 * @author     Thomas Müller <[email protected]>
25
 */
26
class Expander
27
{
28
    /**
29
     * @var \Browscap\Data\DataCollection
30
     */
31
    private $collection;
32
33
    /**
34
     * @var \Psr\Log\LoggerInterface
35
     */
36
    private $logger;
37
38
    /**
39
     * This store the components of the pattern id that are later merged into a string. Format for this
40
     * can be seen in the {@see resetPatternId} method.
41
     *
42
     * @var array
43
     */
44
    private $patternId = [];
45
46
    /**
47
     * @var \Browscap\Data\Helper\CheckDeviceData
48
     */
49
    private $checkDeviceData;
50
51
    /**
52
     * @var \Browscap\Data\Helper\CheckEngineData
53
     */
54
    private $checkEngineData;
55
56
    /**
57
     * @var \Browscap\Data\Helper\CheckPlatformData
58
     */
59
    private $checkPlatformData;
60
61
    /**
62
     * Create a new data expander
63
     *
64
     * @param \Psr\Log\LoggerInterface      $logger
65
     * @param \Browscap\Data\DataCollection $collection
66
     */
67
    public function __construct(LoggerInterface $logger, DataCollection $collection)
68
    {
69
        $this->logger            = $logger;
70
        $this->collection        = $collection;
71
        $this->checkDeviceData   = new CheckDeviceData();
72
        $this->checkEngineData   = new CheckEngineData();
73
        $this->checkPlatformData = new CheckPlatformData();
74
    }
75
76
    /**
77
     * @param \Browscap\Data\Division $division
78
     * @param string                  $divisionName
79
     *
80
     * @throws \UnexpectedValueException
81
     *
82
     * @return array
83
     */
84
    public function expand(Division $division, string $divisionName) : array
85
    {
86
        $allInputDivisions = $this->parseDivision(
87
            $division,
88
            $divisionName
89
        );
90
91
        return $this->expandProperties($allInputDivisions);
92
    }
93
94
    /**
95
     * Resets the pattern id
96
     *
97
     * @return void
98
     */
99
    private function resetPatternId() : void
100
    {
101
        $this->patternId = [
102
            'division' => '',
103
            'useragent' => '',
104
            'platform' => '',
105
            'device' => '',
106
            'child' => '',
107
        ];
108
    }
109
110
    /**
111
     * Render a single division
112
     *
113
     * @param \Browscap\Data\Division $division
114
     * @param string                  $divisionName
115
     *
116
     * @return array
117
     */
118
    private function parseDivision(Division $division, string $divisionName) : array
119
    {
120
        $output = [];
121
122
        $i = 0;
123
        foreach ($division->getUserAgents() as $uaData) {
124
            $this->resetPatternId();
125
            $this->patternId['division']  = $division->getFileName();
126
            $this->patternId['useragent'] = $i;
127
128
            $output = array_merge(
129
                $output,
130
                $this->parseUserAgent(
131
                    $uaData,
132
                    $division->isLite(),
133
                    $division->isStandard(),
134
                    $division->getSortIndex(),
135
                    $divisionName
136
                )
137
            );
138
            ++$i;
139
        }
140
141
        return $output;
142
    }
143
144
    /**
145
     * Render a single User Agent block
146
     *
147
     * @param \Browscap\Data\Useragent $uaData
148
     * @param bool                     $lite
149
     * @param bool                     $standard
150
     * @param int                      $sortIndex
151
     * @param string                   $divisionName
152
     *
153
     * @return array
154
     */
155
    private function parseUserAgent(Useragent $uaData, bool $lite, bool $standard, int $sortIndex, string $divisionName) : array
156
    {
157
        $uaProperties = $uaData->getProperties();
158
159
        if (null !== $uaData->getPlatform()) {
160
            $this->patternId['platform'] = $uaData->getPlatform();
161
            $platform                    = $this->collection->getPlatform($uaData->getPlatform());
162
163
            if (!$platform->isLite()) {
164
                $lite = false;
165
            }
166
167
            if (!$platform->isStandard()) {
168
                $standard = false;
169
            }
170
171
            $platformData = $platform->getProperties();
172
        } else {
173
            $this->patternId['platform'] = '';
174
            $platformData                = [];
175
        }
176
177
        if (null !== $uaData->getEngine()) {
178
            $engine     = $this->collection->getEngine($uaData->getEngine());
179
            $engineData = $engine->getProperties();
180
        } else {
181
            $engineData = [];
182
        }
183
184
        if (null !== $uaData->getDevice()) {
185
            $device     = $this->collection->getDevice($uaData->getDevice());
186
            $deviceData = $device->getProperties();
187
188
            if (!$device->isStandard()) {
189
                $standard = false;
190
            }
191
        } else {
192
            $deviceData = [];
193
        }
194
195
        $ua = $uaData->getUserAgent();
196
197
        $output = [
198
            $ua => array_merge(
199
                [
200
                    'lite' => $lite,
201
                    'standard' => $standard,
202
                    'sortIndex' => $sortIndex,
203
                    'division' => $divisionName,
204
                ],
205
                $platformData,
206
                $engineData,
207
                $deviceData,
208
                $uaProperties
209
            ),
210
        ];
211
212
        $i = 0;
213
        foreach ($uaData->getChildren() as $child) {
214
            $this->patternId['child'] = $i;
215
            if (isset($child['devices']) && is_array($child['devices'])) {
216
                // Replace our device array with a single device property with our #DEVICE# token replaced
217
                foreach ($child['devices'] as $deviceMatch => $deviceName) {
218
                    $this->patternId['device'] = $deviceMatch;
219
                    $subChild                  = $child;
220
                    $subChild['match']         = str_replace('#DEVICE#', $deviceMatch, $subChild['match']);
221
                    $subChild['device']        = $deviceName;
222
                    unset($subChild['devices']);
223
                    $output = array_merge(
224
                        $output,
225
                        $this->parseChildren($ua, $subChild, $lite, $standard)
226
                    );
227
                }
228
            } else {
229
                $this->patternId['device'] = '';
230
                $output                    = array_merge(
231
                    $output,
232
                    $this->parseChildren($ua, $child, $lite, $standard)
233
                );
234
            }
235
            ++$i;
236
        }
237
238
        return $output;
239
    }
240
241
    /**
242
     * Render the children section in a single User Agent block
243
     *
244
     * @param string $ua
245
     * @param array  $uaDataChild
246
     * @param bool   $lite
247
     * @param bool   $standard
248
     *
249
     * @return array[]
250
     */
251
    private function parseChildren(string $ua, array $uaDataChild, bool $lite = true, bool $standard = true) : array
252
    {
253
        $output = [];
254
255
        if (isset($uaDataChild['platforms']) && is_array($uaDataChild['platforms'])) {
256
            foreach ($uaDataChild['platforms'] as $platform) {
0 ignored issues
show
Bug introduced by
The expression $uaDataChild['platforms'] of type array|null 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...
257
                $this->patternId['platform'] = $platform;
258
                $properties                  = ['Parent' => $ua, 'lite' => $lite, 'standard' => $standard];
259
                $platformData                = $this->collection->getPlatform($platform);
260
261
                if (!$platformData->isLite()) {
262
                    $properties['lite'] = false;
263
                }
264
265
                if (!$platformData->isStandard()) {
266
                    $properties['standard'] = false;
267
                }
268
269
                $uaBase = str_replace('#PLATFORM#', $platformData->getMatch(), $uaDataChild['match']);
270
271 View Code Duplication
                if (array_key_exists('engine', $uaDataChild)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
272
                    $engine     = $this->collection->getEngine($uaDataChild['engine']);
0 ignored issues
show
Documentation introduced by
$uaDataChild['engine'] is of type array|null, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
273
                    $engineData = $engine->getProperties();
274
                } else {
275
                    $engineData = [];
276
                }
277
278 View Code Duplication
                if (array_key_exists('device', $uaDataChild)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
279
                    $device     = $this->collection->getDevice($uaDataChild['device']);
0 ignored issues
show
Documentation introduced by
$uaDataChild['device'] is of type array|null, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
280
                    $deviceData = $device->getProperties();
281
282
                    if (!$device->isStandard()) {
283
                        $properties['standard'] = false;
284
                    }
285
                } else {
286
                    $deviceData = [];
287
                }
288
289
                $properties = array_merge(
290
                    $properties,
291
                    $engineData,
292
                    $deviceData,
293
                    $platformData->getProperties()
294
                );
295
296
                if (isset($uaDataChild['properties'])
297
                    && is_array($uaDataChild['properties'])
298
                ) {
299
                    $childProperties = $uaDataChild['properties'];
300
301
                    $properties = array_merge($properties, $childProperties);
302
                }
303
304
                $properties['PatternId'] = $this->getPatternId();
305
306
                $output[$uaBase] = $properties;
307
            }
308
        } else {
309
            $properties = ['Parent' => $ua, 'lite' => $lite, 'standard' => $standard];
310
311 View Code Duplication
            if (array_key_exists('engine', $uaDataChild)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
312
                $engine     = $this->collection->getEngine($uaDataChild['engine']);
313
                $engineData = $engine->getProperties();
314
            } else {
315
                $engineData = [];
316
            }
317
318 View Code Duplication
            if (array_key_exists('device', $uaDataChild)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
319
                $device     = $this->collection->getDevice($uaDataChild['device']);
320
                $deviceData = $device->getProperties();
321
322
                if (!$device->isStandard()) {
323
                    $properties['standard'] = false;
324
                }
325
            } else {
326
                $deviceData = [];
327
            }
328
329
            $properties = array_merge($properties, $engineData, $deviceData);
330
331
            if (isset($uaDataChild['properties'])
332
                && is_array($uaDataChild['properties'])
333
            ) {
334
                $childProperties = $uaDataChild['properties'];
335
336
                $this->checkPlatformData->check(
337
                    $childProperties,
338
                    'the properties array contains platform data for key "' . $ua
339
                    . '", please use the "platforms" keyword'
340
                );
341
342
                $this->checkEngineData->check(
343
                    $childProperties,
344
                    'the properties array contains engine data for key "' . $ua
345
                    . '", please use the "engine" keyword'
346
                );
347
348
                $this->checkDeviceData->check(
349
                    $childProperties,
350
                    'the properties array contains device data for key "' . $ua
351
                    . '", please use the "device" or the "devices" keyword'
352
                );
353
354
                $properties = array_merge($properties, $childProperties);
355
            }
356
357
            $uaBase                      = str_replace('#PLATFORM#', '', $uaDataChild['match']);
358
            $this->patternId['platform'] = '';
359
360
            $properties['PatternId'] = $this->getPatternId();
361
362
            $output[$uaBase] = $properties;
363
        }
364
365
        return $output;
366
    }
367
368
    /**
369
     * Builds and returns the string pattern id from the array components
370
     *
371
     * @return string
372
     */
373
    private function getPatternId() : string
374
    {
375
        return sprintf(
376
            '%s::u%d::c%d::d%s::p%s',
377
            $this->patternId['division'],
378
            $this->patternId['useragent'],
379
            $this->patternId['child'],
380
            $this->patternId['device'],
381
            $this->patternId['platform']
382
        );
383
    }
384
385
    /**
386
     * expands all properties for all useragents to make sure all properties are set and make it possible to skip
387
     * incomplete properties and remove duplicate definitions
388
     *
389
     * @param array $allInputDivisions
390
     *
391
     * @throws \UnexpectedValueException
392
     *
393
     * @return array
394
     */
395
    private function expandProperties(array $allInputDivisions) : array
396
    {
397
        $this->logger->debug('expand all properties');
398
        $allDivisions = [];
399
400
        $ua                = $this->collection->getDefaultProperties()->getUserAgents()[0];
401
        $defaultproperties = $ua->getProperties();
402
403
        foreach (array_keys($allInputDivisions) as $key) {
404
            $this->logger->debug('expand all properties for key "' . $key . '"');
405
406
            $userAgent = $key;
407
            $parents   = [$userAgent];
408
409
            while (isset($allInputDivisions[$userAgent]['Parent'])) {
410
                if ($allInputDivisions[$userAgent]['Parent'] === $userAgent) {
411
                    break;
412
                }
413
414
                $parents[] = $allInputDivisions[$userAgent]['Parent'];
415
                $userAgent = $allInputDivisions[$userAgent]['Parent'];
416
            }
417
            unset($userAgent);
418
419
            $parents     = array_reverse($parents);
420
            $browserData = $defaultproperties;
421
            $properties  = $allInputDivisions[$key];
422
423
            foreach ($parents as $parent) {
424
                if (!isset($allInputDivisions[$parent])) {
425
                    continue;
426
                }
427
428
                if (!is_array($allInputDivisions[$parent])) {
429
                    throw new \UnexpectedValueException(
430
                        'Parent "' . $parent . '" is not an array for key "' . $key . '"'
431
                    );
432
                }
433
434
                if ($key !== $parent
435
                    && isset($allInputDivisions[$parent]['sortIndex'], $properties['sortIndex'])
436
437
                    && ($allInputDivisions[$parent]['division'] !== $properties['division'])
438
                ) {
439
                    if ($allInputDivisions[$parent]['sortIndex'] >= $properties['sortIndex']) {
440
                        throw new \UnexpectedValueException(
441
                            'sorting not ready for key "'
442
                            . $key . '"'
443
                        );
444
                    }
445
                }
446
447
                $browserData = array_merge($browserData, $allInputDivisions[$parent]);
448
            }
449
450
            array_pop($parents);
451
            $browserData['Parents'] = implode(',', $parents);
452
            unset($parents);
453
454
            foreach (array_keys($browserData) as $propertyName) {
455
                if (is_bool($browserData[$propertyName])) {
456
                    $properties[$propertyName] = $browserData[$propertyName];
457
                } else {
458
                    $properties[$propertyName] = $this->trimProperty((string) $browserData[$propertyName]);
459
                }
460
            }
461
462
            unset($browserData);
463
464
            $allDivisions[$key] = $properties;
465
466
            if (!isset($properties['Version'])) {
467
                throw new \UnexpectedValueException('Version property not found for key "' . $key . '"');
468
            }
469
470
            $completeVersions = explode('.', $properties['Version'], 2);
471
472
            $properties['MajorVer'] = (string) $completeVersions[0];
473
474
            if (isset($completeVersions[1])) {
475
                $minorVersion = (string) $completeVersions[1];
476
            } else {
477
                $minorVersion = '0';
478
            }
479
480
            $properties['MinorVer'] = $minorVersion;
481
482
            $allDivisions[$key] = $properties;
483
        }
484
485
        return $allDivisions;
486
    }
487
488
    /**
489
     * trims the value of a property and converts the string values "true" and "false" to boolean
490
     *
491
     * @param string $propertyValue
492
     *
493
     * @return bool|string
494
     */
495
    public function trimProperty(string $propertyValue)
496
    {
497
        switch ($propertyValue) {
498
            case 'true':
499
                $propertyValue = true;
500
501
                break;
502
            case 'false':
503
                $propertyValue = false;
504
505
                break;
506
            default:
507
                $propertyValue = trim($propertyValue);
508
509
                break;
510
        }
511
512
        return $propertyValue;
513
    }
514
}
515