Completed
Pull Request — master (#155)
by Thomas
11:34
created

LogfileCommand::execute()   D

Complexity

Conditions 15
Paths 176

Size

Total Lines 134
Code Lines 80

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 240

Importance

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