Completed
Pull Request — master (#157)
by Thomas
11:57
created

LogfileCommand   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 498
Duplicated Lines 1.81 %

Coupling/Cohesion

Components 2
Dependencies 20

Test Coverage

Coverage 20.46%

Importance

Changes 5
Bugs 1 Features 1
Metric Value
wmc 62
c 5
b 1
f 1
lcom 2
cbo 20
dl 9
loc 498
ccs 53
cts 259
cp 0.2046
rs 2.1568

14 Methods

Rating   Name   Duplication   Size   Complexity  
A createAmountTypeContent() 0 19 4
A getBrowscap() 0 4 1
B createAmountContent() 0 24 5
D handleLine() 0 62 14
C getResult() 0 28 7
A getFiles() 0 20 3
A createSqlContent() 0 16 2
A outputProgress() 0 19 4
A getPath() 0 16 3
A setCache() 0 6 1
A configure() 0 52 1
A __construct() 0 6 1
F execute() 0 120 14
A getCache() 9 9 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like LogfileCommand often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use LogfileCommand, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Copyright (c) 1998-2015 Browser Capabilities Project
4
 *
5
 * Permission is hereby granted, free of charge, to any person obtaining a
6
 * copy of this software and associated documentation files (the "Software"),
7
 * to deal in the Software without restriction, including without limitation
8
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
9
 * and/or sell copies of the Software, and to permit persons to whom the
10
 * Software is furnished to do so, subject to the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be included
13
 * in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
 * THE SOFTWARE.
22
 *
23
 * @category   Browscap-PHP
24
 * @package    Command
25
 * @copyright  1998-2015 Browser Capabilities Project
26
 * @license    http://www.opensource.org/licenses/MIT MIT License
27
 * @link       https://github.com/browscap/browscap-php/
28
 * @since      added with version 3.0
29
 */
30
31
namespace BrowscapPHP\Command;
32
33
use BrowscapPHP\Browscap;
34
use BrowscapPHP\Cache\BrowscapCache;
35
use BrowscapPHP\Cache\BrowscapCacheInterface;
36
use BrowscapPHP\Exception\InvalidArgumentException;
37
use BrowscapPHP\Exception\ReaderException;
38
use BrowscapPHP\Exception\UnknownBrowserException;
39
use BrowscapPHP\Exception\UnknownBrowserTypeException;
40
use BrowscapPHP\Exception\UnknownDeviceException;
41
use BrowscapPHP\Exception\UnknownEngineException;
42
use BrowscapPHP\Exception\UnknownPlatformException;
43
use BrowscapPHP\Helper\Filesystem;
44
use BrowscapPHP\Helper\IniLoader;
45
use BrowscapPHP\Helper\LoggerHelper;
46
use BrowscapPHP\Util\Logfile\ReaderCollection;
47
use BrowscapPHP\Util\Logfile\ReaderFactory;
48
use Symfony\Component\Console\Command\Command;
49
use Symfony\Component\Console\Input\InputArgument;
50
use Symfony\Component\Console\Input\InputInterface;
51
use Symfony\Component\Console\Input\InputOption;
52
use Symfony\Component\Console\Output\OutputInterface;
53
use Symfony\Component\Filesystem\Exception\IOException;
54
use Symfony\Component\Finder\Finder;
55
use Symfony\Component\Finder\SplFileInfo;
56
use WurflCache\Adapter\File;
57
58
/**
59
 * commands to parse a log file and parse the useragents in it
60
 *
61
 * @category   Browscap-PHP
62
 * @package    Command
63
 * @author     Dave Olsen, http://dmolsen.com
64
 * @author     Thomas Müller <[email protected]>
65
 * @copyright  Copyright (c) 1998-2015 Browser Capabilities Project
66
 * @version    3.0
67
 * @license    http://www.opensource.org/licenses/MIT MIT License
68
 * @link       https://github.com/browscap/browscap-php/
69
 */
70
class LogfileCommand extends Command
71
{
72
    /**
73
     * @var array
74
     */
75
    private $undefinedClients = array();
76
77
    private $uas         = array();
78
    private $uasWithType = array();
79
80
    private $countOk = 0;
81
    private $countNok = 0;
82
    private $totalCount = 0;
83
84
    /**
85
     * @var BrowscapCacheInterface
86
     */
87
    private $cache = null;
88
89
    /**
90
     * @var string
91
     */
92
    private $defaultCacheFolder;
93
94
    /**
95
     * @param string $defaultCacheFolder
96
     */
97 1
    public function __construct($defaultCacheFolder)
98
    {
99 1
        $this->defaultCacheFolder = $defaultCacheFolder;
100
101 1
        parent::__construct();
102 1
    }
103
104
    /**
105
     * @param \BrowscapPHP\Cache\BrowscapCacheInterface $cache
106
     *
107
     * @return $this
108
     */
109 1
    public function setCache(BrowscapCacheInterface $cache)
110
    {
111 1
        $this->cache = $cache;
112
113 1
        return $this;
114
    }
115
116
    /**
117
     * Configures the current command.
118
     */
119 1
    protected function configure()
120
    {
121 1
        $this
122 1
            ->setName('browscap:log')
123 1
            ->setDescription('Parses the supplied webserver log file.')
124 1
            ->addArgument(
125 1
                'output',
126 1
                InputArgument::REQUIRED,
127 1
                'Path to output log file',
128
                null
129 1
            )
130 1
            ->addOption(
131 1
                'log-file',
132 1
                'f',
133 1
                InputOption::VALUE_REQUIRED,
134
                'Path to a webserver log file'
135 1
            )
136 1
            ->addOption(
137 1
                'log-dir',
138 1
                'd',
139 1
                InputOption::VALUE_REQUIRED,
140
                'Path to webserver log directory'
141 1
            )
142 1
            ->addOption(
143 1
                'include',
144 1
                'i',
145 1
                InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
146 1
                'Include glob expressions for log files in the log directory',
147 1
                array('*.log', '*.log*.gz', '*.log*.bz2')
148 1
            )
149 1
            ->addOption(
150 1
                'exclude',
151 1
                'e',
152 1
                InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
153 1
                'Exclude glob expressions for log files in the log directory',
154 1
                array('*error*')
155 1
            )
156 1
            ->addOption(
157 1
                'debug',
158 1
                null,
159 1
                InputOption::VALUE_NONE,
160
                'Should the debug mode entered?'
161 1
            )
162 1
            ->addOption(
163 1
                'cache',
164 1
                'c',
165 1
                InputOption::VALUE_OPTIONAL,
166 1
                'Where the cache files are located',
167 1
                $this->defaultCacheFolder
168 1
            )
169
        ;
170 1
    }
171
172
    /**
173
     * @param InputInterface  $input
174
     * @param OutputInterface $output
175
     *
176
     * @throws \UnexpectedValueException
177
     * @throws \BrowscapPHP\Exception\InvalidArgumentException
178
     * @return int|null|void
179
     */
180
    protected function execute(InputInterface $input, OutputInterface $output)
181
    {
182
        if (!$input->getOption('log-file') && !$input->getOption('log-dir')) {
183
            throw InvalidArgumentException::oneOfCommandArguments('log-file', 'log-dir');
184
        }
185
186
        $loggerHelper = new LoggerHelper();
187
        $logger       = $loggerHelper->create($input->getOption('debug'));
188
189
        $browscap   = $this->getBrowscap();
190
        $loader     = new IniLoader();
0 ignored issues
show
Unused Code introduced by
$loader is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
191
        $collection = ReaderFactory::factory();
192
        $fs         = new Filesystem();
193
194
        $browscap
195
            ->setLogger($logger)
196
            ->setCache($this->getCache($input))
197
        ;
198
199
        /** @var $file \Symfony\Component\Finder\SplFileInfo */
200
        foreach ($this->getFiles($input) as $file) {
201
            $this->uas = array();
202
            $path      = $this->getPath($file);
203
204
            $this->countOk  = 0;
205
            $this->countNok = 0;
206
207
            $logger->info('Analyzing file "'.$file->getPathname().'"');
208
209
            $lines = file($path);
210
211
            if (empty($lines)) {
212
                $logger->info('Skipping empty file "'.$file->getPathname().'"');
213
                continue;
214
            }
215
216
            $this->totalCount = count($lines);
217
218
            foreach ($lines as $line) {
219
                $this->handleLine(
220
                    $output,
221
                    $collection,
222
                    $browscap,
223
                    $line
224
                );
225
            }
226
227
            $this->outputProgress($output, '', true);
228
229
            arsort($this->uas, SORT_NUMERIC);
230
231
            try {
232
                $fs->dumpFile($input->getArgument('output').'/output.sql', $this->createSqlContent());
233
            } catch (IOException $e) {
234
                // do nothing
235
            }
236
237
            try {
238
                $fs->dumpFile(
239
                    $input->getArgument('output').'/output.txt',
240
                    implode(PHP_EOL, array_unique($this->undefinedClients))
241
                );
242
            } catch (IOException $e) {
243
                // do nothing
244
            }
245
246
            try {
247
                $fs->dumpFile(
248
                    $input->getArgument('output').'/output-with-amount.txt',
249
                    $this->createAmountContent()
250
                );
251
            } catch (IOException $e) {
252
                // do nothing
253
            }
254
255
            try {
256
                $fs->dumpFile(
257
                    $input->getArgument('output').'/output-with-amount-and-type.txt',
258
                    $this->createAmountTypeContent()
259
                );
260
            } catch (IOException $e) {
261
                // do nothing
262
            }
263
        }
264
265
        try {
266
            $fs->dumpFile($input->getArgument('output').'/output.sql', $this->createSqlContent());
267
        } catch (IOException $e) {
268
            // do nothing
269
        }
270
271
        $outputFile = $input->getArgument('output').'/output.txt';
272
273
        try {
274
            $fs->dumpFile(
275
                $outputFile,
276
                implode(PHP_EOL, array_unique($this->undefinedClients))
277
            );
278
        } catch (IOException $e) {
279
            throw new \UnexpectedValueException('writing to file "'.$outputFile.'" failed', 0, $e);
280
        }
281
282
        try {
283
            $fs->dumpFile(
284
                $input->getArgument('output').'/output-with-amount.txt',
285
                $this->createAmountContent()
286
            );
287
        } catch (IOException $e) {
288
            // do nothing
289
        }
290
291
        try {
292
            $fs->dumpFile(
293
                $input->getArgument('output').'/output-with-amount-and-type.txt',
294
                $this->createAmountTypeContent()
295
            );
296
        } catch (IOException $e) {
297
            // do nothing
298
        }
299
    }
300
301
    private function createSqlContent()
302
    {
303
        $content = '';
304
305
        arsort($this->uas, SORT_NUMERIC);
306
307
        foreach ($this->uas as $agentOfLine => $count) {
308
            $content .= "
309
                INSERT INTO `agents` (`agent`, `count`)
310
                VALUES ('".addslashes($agentOfLine)."', ".addslashes($count).")
311
                ON DUPLICATE KEY UPDATE `count`=`count`+".addslashes($count).";
312
            ";
313
        }
314
315
        return $content;
316
    }
317
318
    private function createAmountContent()
319
    {
320
        $counts = array();
321
322
        foreach ($this->uasWithType as $uas) {
323
            foreach ($uas as $userAgentString => $count) {
324
                if (isset($counts[$userAgentString])) {
325
                    $counts[$userAgentString] += $count;
326
                } else {
327
                    $counts[$userAgentString] = $count;
328
                }
329
            }
330
        }
331
332
        $content = '';
333
334
        arsort($counts, SORT_NUMERIC);
335
336
        foreach ($counts as $agentOfLine => $count) {
337
            $content .= "$count\t$agentOfLine\n";
338
        }
339
340
        return $content;
341
    }
342
343
    private function createAmountTypeContent()
344
    {
345
        $content = '';
346
        $types   = array('B', 'T', 'P', 'D', 'N', 'U');
347
348
        foreach ($types as $type) {
349
            if (!isset($this->uasWithType[$type])) {
350
                continue;
351
            }
352
353
            arsort($this->uasWithType[$type], SORT_NUMERIC);
354
355
            foreach ($this->uasWithType[$type] as $agentOfLine => $count) {
356
                $content .= "$type\t$count\t$agentOfLine\n";
357
            }
358
        }
359
360
        return $content;
361
    }
362
363
    /**
364
     * @param \Symfony\Component\Console\Output\OutputInterface $output
365
     * @param \BrowscapPHP\Util\Logfile\ReaderCollection        $collection
366
     * @param \BrowscapPHP\Browscap                             $browscap
367
     * @param integer                                           $line
368
     *
369
     * @throws UnknownBrowserException
370
     * @throws UnknownBrowserTypeException
371
     * @throws UnknownDeviceException
372
     * @throws UnknownEngineException
373
     * @throws UnknownPlatformException
374
     * @throws \Exception
375
     */
376
    private function handleLine(OutputInterface $output, ReaderCollection $collection, Browscap $browscap, $line)
377
    {
378
        $userAgentString = '';
379
380
        try {
381
            $userAgentString = $collection->read($line);
382
383
            try {
384
                $this->getResult($browscap->getBrowser($userAgentString));
385
            } catch (\Exception $e) {
386
                $this->undefinedClients[] = $userAgentString;
387
388
                throw $e;
389
            }
390
391
            $type = '.';
392
            $this->countOk++;
393
        } catch (ReaderException $e) {
394
            $type = 'E';
395
            $this->countNok++;
396
        } catch (UnknownBrowserTypeException $e) {
397
            $type = 'T';
398
            $this->countNok++;
399
        } catch (UnknownBrowserException $e) {
400
            $type = 'B';
401
            $this->countNok++;
402
        } catch (UnknownPlatformException $e) {
403
            $type = 'P';
404
            $this->countNok++;
405
        } catch (UnknownDeviceException $e) {
406
            $type = 'D';
407
            $this->countNok++;
408
        } catch (UnknownEngineException $e) {
409
            $type = 'N';
410
            $this->countNok++;
411
        } catch (\Exception $e) {
412
            $type = 'U';
413
            $this->countNok++;
414
        }
415
416
        $this->outputProgress($output, $type);
417
418
        // count all useragents
419
        if (isset($this->uas[$userAgentString])) {
420
            $this->uas[$userAgentString]++;
421
        } else {
422
            $this->uas[$userAgentString] = 1;
423
        }
424
425
        if ('.' !== $type && 'E' !== $type) {
426
            // count all undetected useragents grouped by detection error
427
            if (!isset($this->uasWithType[$type])) {
428
                $this->uasWithType[$type] = array();
429
            }
430
431
            if (isset($this->uasWithType[$type][$userAgentString])) {
432
                $this->uasWithType[$type][$userAgentString]++;
433
            } else {
434
                $this->uasWithType[$type][$userAgentString] = 1;
435
            }
436
        }
437
    }
438
439
    /**
440
     * @param \Symfony\Component\Console\Output\OutputInterface $output
441
     * @param string                                            $result
442
     * @param bool                                              $end
443
     *
444
     * @return int
445
     */
446
    private function outputProgress(OutputInterface $output, $result, $end = false)
447
    {
448
        if (($this->totalCount % 70) === 0 || $end) {
449
            $formatString = '  %'.strlen($this->countOk).'d OK, %'.strlen($this->countNok).'d NOK, Summary %'
450
                .strlen($this->totalCount).'d';
451
452
            if ($end) {
453
                $result = str_pad($result, 70 - ($this->totalCount % 70), ' ', STR_PAD_RIGHT);
454
            }
455
456
            $endString = sprintf($formatString, $this->countOk, $this->countNok, $this->totalCount);
457
458
            $output->writeln($result.$endString);
459
460
            return;
461
        }
462
463
        $output->write($result);
464
    }
465
466
    /**
467
     * @param \stdClass $result
468
     *
469
     * @return string
470
     */
471
    private function getResult(\stdClass $result)
472
    {
473
        if ('Default Browser' === $result->browser) {
474
            throw new UnknownBrowserException('unknwon browser found');
475
        }
476
477
        if ('unknown' === $result->browser_type) {
478
            throw new UnknownBrowserTypeException('unknwon browser type found');
479
        }
480
481
        if (in_array($result->browser_type, array('Bot/Crawler', 'Library'))) {
482
            return '.';
483
        }
484
485
        if ('unknown' === $result->platform) {
486
            throw new UnknownPlatformException('unknown platform found');
487
        }
488
489
        if ('unknown' === $result->device_type) {
490
            throw new UnknownDeviceException('unknwon device type found');
491
        }
492
493
        if ('unknown' === $result->renderingengine_name) {
494
            throw new UnknownEngineException('unknown rendering engine found');
495
        }
496
497
        return '.';
498
    }
499
500
    /**
501
     * @param \Symfony\Component\Console\Input\InputInterface $input
502
     *
503
     * @return \Symfony\Component\Finder\Finder
504
     */
505
    private function getFiles(InputInterface $input)
506
    {
507
        $finder = Finder::create();
508
509
        if ($input->getOption('log-file')) {
510
            $file = $input->getOption('log-file');
511
            $finder->append(Finder::create()->in(dirname($file))->name(basename($file)));
512
        }
513
514
        if ($input->getOption('log-dir')) {
515
            $dirFinder = Finder::create()
516
                ->in($input->getOption('log-dir'));
517
            array_map(array($dirFinder, 'name'), $input->getOption('include'));
518
            array_map(array($dirFinder, 'notName'), $input->getOption('exclude'));
519
520
            $finder->append($dirFinder);
521
        }
522
523
        return $finder;
524
    }
525
526
    /**
527
     * @param \Symfony\Component\Finder\SplFileInfo $file
528
     *
529
     * @return string
530
     */
531
    private function getPath(SplFileInfo $file)
532
    {
533
        switch ($file->getExtension()) {
534
            case 'gz':
535
                $path = 'compress.zlib://'.$file->getPathname();
536
                break;
537
            case 'bz2':
538
                $path = 'compress.bzip2://'.$file->getPathname();
539
                break;
540
            default:
541
                $path = $file->getPathname();
542
                break;
543
        }
544
545
        return $path;
546
    }
547
548
    private function getBrowscap()
549
    {
550
        return new Browscap();
551
    }
552
553
    /**
554
     * @param InputInterface $input
555
     *
556
     * @return BrowscapCacheInterface
557
     */
558 View Code Duplication
    private function getCache(InputInterface $input)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
559
    {
560
        if (null === $this->cache) {
561
            $cacheAdapter = new File(array(File::DIR => $input->getOption('cache')));
562
            $this->cache  = new BrowscapCache($cacheAdapter);
563
        }
564
565
        return $this->cache;
566
    }
567
}
568