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

Expander::parseUserAgent()   C

Complexity

Conditions 11
Paths 90

Size

Total Lines 85
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 38
CRAP Score 13.0039

Importance

Changes 0
Metric Value
dl 0
loc 85
ccs 38
cts 51
cp 0.7451
rs 5.2653
c 0
b 0
f 0
cc 11
eloc 56
nc 90
nop 5
crap 13.0039

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
declare(strict_types = 1);
3
namespace Browscap\Data;
4
5
use Browscap\Data\Helper\TrimProperty;
6
use Psr\Log\LoggerInterface;
7
8
class Expander
9
{
10
    /**
11
     * @var DataCollection
12
     */
13
    private $collection;
14
15
    /**
16
     * @var LoggerInterface
17
     */
18
    private $logger;
19
20
    /**
21
     * This store the components of the pattern id that are later merged into a string. Format for this
22
     * can be seen in the {@see resetPatternId} method.
23
     *
24
     * @var array
25
     */
26
    private $patternId = [];
27
28
    /**
29
     * @var TrimProperty
30
     */
31
    private $trimProperty;
32
33
    /**
34
     * Create a new data expander
35
     *
36
     * @param LoggerInterface $logger
37
     * @param DataCollection  $collection
38
     */
39 13
    public function __construct(LoggerInterface $logger, DataCollection $collection)
40
    {
41 13
        $this->logger       = $logger;
42 13
        $this->collection   = $collection;
43 13
        $this->trimProperty = new TrimProperty();
44 13
    }
45
46
    /**
47
     * @param Division $division
48
     * @param string   $divisionName
49
     *
50
     * @throws \UnexpectedValueException
51
     *
52
     * @return array[]
53
     */
54 13
    public function expand(Division $division, string $divisionName) : array
55
    {
56 13
        $allInputDivisions = $this->parseDivision(
57 13
            $division,
58 13
            $divisionName
59
        );
60
61 13
        return $this->expandProperties($allInputDivisions);
62
    }
63
64
    /**
65
     * Resets the pattern id
66
     *
67
     * @return void
68
     */
69 12
    private function resetPatternId() : void
70
    {
71 12
        $this->patternId = [
72
            'division' => '',
73
            'useragent' => '',
74
            'platform' => '',
75
            'device' => '',
76
            'child' => '',
77
        ];
78 12
    }
79
80
    /**
81
     * parses and expands a single division
82
     *
83
     * @param Division $division
84
     * @param string   $divisionName
85
     *
86
     * @return array[]
87
     */
88 13
    private function parseDivision(Division $division, string $divisionName) : array
89
    {
90 13
        $output = [];
91
92 13
        $i = 0;
93 13
        foreach ($division->getUserAgents() as $uaData) {
94 12
            $this->resetPatternId();
95 12
            $this->patternId['division']  = $division->getFileName();
96 12
            $this->patternId['useragent'] = $i;
97
98 12
            $output = array_merge(
99 12
                $output,
100 12
                $this->parseUserAgent(
101 12
                    $uaData,
102 12
                    $division->isLite(),
103 12
                    $division->isStandard(),
104 12
                    $division->getSortIndex(),
105 12
                    $divisionName
106
                )
107
            );
108 12
            ++$i;
109
        }
110
111 13
        return $output;
112
    }
113
114
    /**
115
     * parses and expands a single User Agent block
116
     *
117
     * @param UserAgent $uaData
118
     * @param bool      $lite
119
     * @param bool      $standard
120
     * @param int       $sortIndex
121
     * @param string    $divisionName
122
     *
123
     * @return array[]
124
     */
125 12
    private function parseUserAgent(UserAgent $uaData, bool $lite, bool $standard, int $sortIndex, string $divisionName) : array
126
    {
127 12
        $uaProperties = $uaData->getProperties();
128
129 12
        if (null !== $uaData->getPlatform()) {
130
            $this->patternId['platform'] = $uaData->getPlatform();
131
            $platform                    = $this->collection->getPlatform($uaData->getPlatform());
132
133
            if (!$platform->isLite()) {
134
                $lite = false;
135
            }
136
137
            if (!$platform->isStandard()) {
138
                $standard = false;
139
            }
140
141
            $platformProperties = $platform->getProperties();
142
        } else {
143 12
            $this->patternId['platform'] = '';
144 12
            $platformProperties          = [];
145
        }
146
147 12
        if (null !== $uaData->getEngine()) {
148
            $engine           = $this->collection->getEngine($uaData->getEngine());
149
            $engineProperties = $engine->getProperties();
150
        } else {
151 12
            $engineProperties = [];
152
        }
153
154 12
        if (null !== $uaData->getDevice()) {
155
            $device           = $this->collection->getDevice($uaData->getDevice());
156
            $deviceProperties = $device->getProperties();
157
158
            if (!$device->isStandard()) {
159
                $standard = false;
160
            }
161
        } else {
162 12
            $deviceProperties = [];
163
        }
164
165 12
        $ua = $uaData->getUserAgent();
166
167
        $output = [
168 12
            $ua => array_merge(
169
                [
170 12
                    'lite' => $lite,
171 12
                    'standard' => $standard,
172 12
                    'sortIndex' => $sortIndex,
173 12
                    'division' => $divisionName,
174
                ],
175 12
                $platformProperties,
176 12
                $engineProperties,
177 12
                $deviceProperties,
178 12
                $uaProperties
179
            ),
180
        ];
181
182 12
        $i = 0;
183 12
        foreach ($uaData->getChildren() as $child) {
184 6
            $this->patternId['child'] = $i;
185 6
            if (isset($child['devices']) && is_array($child['devices'])) {
186
                // Replace our device array with a single device property with our #DEVICE# token replaced
187 3
                foreach ($child['devices'] as $deviceMatch => $deviceName) {
188 3
                    $this->patternId['device'] = $deviceMatch;
189 3
                    $subChild                  = $child;
190 3
                    $subChild['match']         = str_replace('#DEVICE#', $deviceMatch, $subChild['match']);
191 3
                    $subChild['device']        = $deviceName;
192 3
                    unset($subChild['devices']);
193 3
                    $output = array_merge(
194 3
                        $output,
195 3
                        $this->parseChildren($ua, $subChild, $lite, $standard)
196
                    );
197
                }
198
            } else {
199 3
                $this->patternId['device'] = '';
200 3
                $output                    = array_merge(
201 3
                    $output,
202 3
                    $this->parseChildren($ua, $child, $lite, $standard)
203
                );
204
            }
205 6
            ++$i;
206
        }
207
208 12
        return $output;
209
    }
210
211
    /**
212
     * parses and expands the children section in a single User Agent block
213
     *
214
     * @param string $ua
215
     * @param array  $uaDataChild
216
     * @param bool   $lite
217
     * @param bool   $standard
218
     *
219
     * @return array[]
220
     */
221 6
    private function parseChildren(string $ua, array $uaDataChild, bool $lite = true, bool $standard = true) : array
222
    {
223 6
        $output = [];
224
225 6
        if (isset($uaDataChild['platforms']) && is_array($uaDataChild['platforms'])) {
226 2
            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...
227 2
                $this->patternId['platform'] = $platform;
228 2
                $properties                  = ['Parent' => $ua, 'lite' => $lite, 'standard' => $standard];
229 2
                $platformProperties          = $this->collection->getPlatform($platform);
230
231 2
                if (!$platformProperties->isLite()) {
232 2
                    $properties['lite'] = false;
233
                }
234
235 2
                if (!$platformProperties->isStandard()) {
236 2
                    $properties['standard'] = false;
237
                }
238
239 2
                $uaBase = str_replace('#PLATFORM#', $platformProperties->getMatch(), $uaDataChild['match']);
240
241 2 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...
242
                    $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...
243
                    $engineProperties = $engine->getProperties();
244
                } else {
245 2
                    $engineProperties = [];
246
                }
247
248 2 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...
249 1
                    $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...
250 1
                    $deviceProperties = $device->getProperties();
251
252 1
                    if (!$device->isStandard()) {
253 1
                        $properties['standard'] = false;
254
                    }
255
                } else {
256 1
                    $deviceProperties = [];
257
                }
258
259 2
                $properties = array_merge(
260 2
                    $properties,
261 2
                    $engineProperties,
262 2
                    $deviceProperties,
263 2
                    $platformProperties->getProperties()
264
                );
265
266 2
                if (isset($uaDataChild['properties'])
267 2
                    && is_array($uaDataChild['properties'])
268
                ) {
269 2
                    $childProperties = $uaDataChild['properties'];
270
271 2
                    $properties = array_merge($properties, $childProperties);
272
                }
273
274 2
                $properties['PatternId'] = $this->getPatternId();
275
276 2
                $output[$uaBase] = $properties;
277
            }
278
        } else {
279 4
            $properties = ['Parent' => $ua, 'lite' => $lite, 'standard' => $standard];
280
281 4 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...
282
                $engine           = $this->collection->getEngine($uaDataChild['engine']);
283
                $engineProperties = $engine->getProperties();
284
            } else {
285 4
                $engineProperties = [];
286
            }
287
288 4 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...
289 2
                $device           = $this->collection->getDevice($uaDataChild['device']);
290 2
                $deviceProperties = $device->getProperties();
291
292 2
                if (!$device->isStandard()) {
293 2
                    $properties['standard'] = false;
294
                }
295
            } else {
296 2
                $deviceProperties = [];
297
            }
298
299 4
            $properties = array_merge($properties, $engineProperties, $deviceProperties);
300
301 4
            if (isset($uaDataChild['properties'])
302 4
                && is_array($uaDataChild['properties'])
303
            ) {
304 4
                $properties = array_merge($properties, $uaDataChild['properties']);
305
            }
306
307 4
            $uaBase                      = str_replace('#PLATFORM#', '', $uaDataChild['match']);
308 4
            $this->patternId['platform'] = '';
309
310 4
            $properties['PatternId'] = $this->getPatternId();
311
312 4
            $output[$uaBase] = $properties;
313
        }
314
315 6
        return $output;
316
    }
317
318
    /**
319
     * Builds and returns the string pattern id from the array components
320
     *
321
     * @return string
322
     */
323 6
    private function getPatternId() : string
324
    {
325 6
        return sprintf(
326 6
            '%s::u%d::c%d::d%s::p%s',
327 6
            $this->patternId['division'],
328 6
            $this->patternId['useragent'],
329 6
            $this->patternId['child'],
330 6
            $this->patternId['device'],
331 6
            $this->patternId['platform']
332
        );
333
    }
334
335
    /**
336
     * expands all properties for all useragents to make sure all properties are set and make it possible to skip
337
     * incomplete properties and remove duplicate definitions
338
     *
339
     * @param array[] $allInputDivisions
340
     *
341
     * @throws \UnexpectedValueException
342
     *
343
     * @return array[]
344
     */
345 13
    private function expandProperties(array $allInputDivisions) : array
346
    {
347 13
        $this->logger->debug('expand all properties');
348 13
        $allDivisions = [];
349
350 13
        $ua                = $this->collection->getDefaultProperties()->getUserAgents()[0];
351 13
        $defaultproperties = $ua->getProperties();
352
353 13
        foreach (array_keys($allInputDivisions) as $key) {
354 12
            $this->logger->debug('expand all properties for key "' . $key . '"');
355
356 12
            $userAgent = $key;
357 12
            $parents   = [$userAgent];
358
359 12
            while (isset($allInputDivisions[$userAgent]['Parent'])) {
360 12
                if ($allInputDivisions[$userAgent]['Parent'] === $userAgent) {
361
                    break;
362
                }
363
364 12
                $parents[] = $allInputDivisions[$userAgent]['Parent'];
365 12
                $userAgent = $allInputDivisions[$userAgent]['Parent'];
366
            }
367 12
            unset($userAgent);
368
369 12
            $parents     = array_reverse($parents);
370 12
            $browserData = $defaultproperties;
371 12
            $properties  = $allInputDivisions[$key];
372
373 12
            foreach ($parents as $parent) {
374 12
                if (!isset($allInputDivisions[$parent])) {
375 12
                    continue;
376
                }
377
378 12
                if (!is_array($allInputDivisions[$parent])) {
379
                    throw new \UnexpectedValueException(
380
                        'Parent "' . $parent . '" is not an array for key "' . $key . '"'
381
                    );
382
                }
383
384 12
                if ($key !== $parent
385 6
                    && isset($allInputDivisions[$parent]['sortIndex'], $properties['sortIndex'])
386
387
                    && ($allInputDivisions[$parent]['division'] !== $properties['division'])
388
                ) {
389
                    if ($allInputDivisions[$parent]['sortIndex'] >= $properties['sortIndex']) {
390
                        throw new \UnexpectedValueException(
391
                            'sorting not ready for key "'
392
                            . $key . '"'
393
                        );
394
                    }
395
                }
396
397 12
                $browserData = array_merge($browserData, $allInputDivisions[$parent]);
398
            }
399
400 12
            array_pop($parents);
401 12
            $browserData['Parents'] = implode(',', $parents);
402 12
            unset($parents);
403
404 12
            foreach (array_keys($browserData) as $propertyName) {
405 12
                if (is_bool($browserData[$propertyName])) {
406 12
                    $properties[$propertyName] = $browserData[$propertyName];
407
                } else {
408 12
                    $properties[$propertyName] = $this->trimProperty->trimProperty((string) $browserData[$propertyName]);
409
                }
410
            }
411
412 12
            unset($browserData);
413
414 12
            $allDivisions[$key] = $properties;
415
416 12
            if (!isset($properties['Version'])) {
417
                throw new \UnexpectedValueException('Version property not found for key "' . $key . '"');
418
            }
419
420 12
            $completeVersions = explode('.', $properties['Version'], 2);
421
422 12
            $properties['MajorVer'] = (string) $completeVersions[0];
423
424 12
            if (isset($completeVersions[1])) {
425 12
                $minorVersion = (string) $completeVersions[1];
426
            } else {
427
                $minorVersion = '0';
428
            }
429
430 12
            $properties['MinorVer'] = $minorVersion;
431
432 12
            $allDivisions[$key] = $properties;
433
        }
434
435 13
        return $allDivisions;
436
    }
437
}
438