diff --git a/.travis.yml b/.travis.yml
index df410aa..c4e89fc 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -22,5 +22,7 @@ install:
script:
- ./vendor/bin/phpunit
+ - ./vendor/bin/psalm
+ - ./vendor/bin/phpstan analyse -l 5 src
sudo: false
diff --git a/composer.json b/composer.json
index 8e20dce..1c62c77 100644
--- a/composer.json
+++ b/composer.json
@@ -6,11 +6,13 @@
"composer/ca-bundle": "^1.1"
},
"require-dev": {
- "phpunit/phpunit": "^7 || ^8 || ^9",
- "symfony/yaml": "^3.4 || ^4.3 || ^5.0",
- "symfony/filesystem": "^3.4 || ^4.3 || ^5.0",
- "symfony/finder": "^3.4 || ^4.3 || ^5.0",
- "symfony/console": "^3.4 || ^4.3 || ^5.0"
+ "phpunit/phpunit": "^8 || ^9",
+ "symfony/yaml": "^3.4 || ^4.2 || ^4.3 || ^5.0",
+ "symfony/filesystem": "^3.4 || ^4.2 || ^4.3 || ^5.0",
+ "symfony/finder": "^3.4 || ^4.2 || ^4.3 || ^5.0",
+ "symfony/console": "^3.4 || ^4.2 || ^4.3 || ^5.0",
+ "phpstan/phpstan": "^0.12.33",
+ "vimeo/psalm": "^3.12"
},
"suggest": {
"symfony/yaml": "Required for CLI usage - ^3.4 || ^4.3 || ^5.0",
diff --git a/psalm.xml b/psalm.xml
new file mode 100644
index 0000000..30258a7
--- /dev/null
+++ b/psalm.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/AbstractParser.php b/src/AbstractParser.php
index 7dee44c..b1b6b25 100644
--- a/src/AbstractParser.php
+++ b/src/AbstractParser.php
@@ -14,9 +14,6 @@
abstract class AbstractParser
{
- /** @var string */
- public static $defaultFile;
-
/** @var array */
protected $regexes = array();
@@ -25,49 +22,6 @@ public function __construct(array $regexes)
$this->regexes = $regexes;
}
- /**
- * Create parser instance
- *
- * Either pass a custom regexes.php file or leave the argument empty and use the default file.
- * @throws FileNotFoundException
- */
- public static function create(?string $file = null): self
- {
- return $file ? static::createCustom($file) : static::createDefault();
- }
-
- /** @throws FileNotFoundException */
- protected static function createDefault(): self
- {
- return static::createInstance(
- static::getDefaultFile(),
- [FileNotFoundException::class, 'defaultFileNotFound']
- );
- }
-
- /** @throws FileNotFoundException */
- protected static function createCustom(string $file): self
- {
- return static::createInstance(
- $file,
- [FileNotFoundException::class, 'customRegexFileNotFound']
- );
- }
-
- private static function createInstance(string $file, callable $exceptionFactory): self
- {
- if (!file_exists($file)) {
- throw $exceptionFactory($file);
- }
-
- static $map = [];
- if (!isset($map[$file])) {
- $map[$file] = include $file;
- }
-
- return new static($map[$file]);
- }
-
protected static function tryMatch(array $regexes, string $userAgent): array
{
foreach ($regexes as $regex) {
@@ -108,13 +62,8 @@ static function ($m) use ($matches) {
private static function emptyStringToNull(?string $string): ?string
{
- $string = trim($string);
+ $string = trim($string ?? '');
return $string === '' ? null : $string;
}
-
- protected static function getDefaultFile(): string
- {
- return static::$defaultFile ?: dirname(__DIR__).'/resources'.DIRECTORY_SEPARATOR.'regexes.php';
- }
}
diff --git a/src/Command/ConvertCommand.php b/src/Command/ConvertCommand.php
index 84f51fe..e00f1f8 100644
--- a/src/Command/ConvertCommand.php
+++ b/src/Command/ConvertCommand.php
@@ -52,7 +52,11 @@ protected function configure(): void
protected function execute(InputInterface $input, OutputInterface $output): int
{
- $this->getConverter()->convertFile($input->getArgument('file'), $input->getOption('no-backup'));
+ $file = $input->getArgument('file');
+ assert(is_string($file));
+ $noBackup = $input->getOption('no-backup');
+ assert(is_bool($noBackup));
+ $this->getConverter()->convertFile($file, $noBackup);
return 0;
}
diff --git a/src/Command/FetchCommand.php b/src/Command/FetchCommand.php
index d1d91ef..1140ca2 100644
--- a/src/Command/FetchCommand.php
+++ b/src/Command/FetchCommand.php
@@ -42,9 +42,10 @@ protected function configure(): void
protected function execute(InputInterface $input, OutputInterface $output): int
{
- $fs = new Filesystem();
- $fetcher = new Fetcher();
- $fs->dumpFile($input->getArgument('file'), $fetcher->fetch());
+ $file = $input->getArgument('file');
+ assert(is_string($file));
+
+ (new Filesystem())->dumpFile($file, (new Fetcher())->fetch());
return 0;
}
diff --git a/src/Command/LogfileCommand.php b/src/Command/LogfileCommand.php
index a8e5679..8ea29d8 100644
--- a/src/Command/LogfileCommand.php
+++ b/src/Command/LogfileCommand.php
@@ -71,7 +71,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$parser = Parser::create();
$undefinedClients = array();
- /** @var $file SplFileInfo */
foreach ($this->getFiles($input) as $file) {
$path = $this->getPath($file);
@@ -85,8 +84,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$firstLine = reset($lines);
- $reader = AbstractReader::factory($firstLine);
- if (!$reader) {
+ try {
+ $reader = AbstractReader::factory($firstLine);
+ } catch (ReaderException $e) {
$output->writeln(sprintf('Could not find reader for file "%s"', $file->getPathname()));
$output->writeln('');
continue;
@@ -124,8 +124,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$undefinedClients = $this->filter($undefinedClients);
- $fs = new Filesystem();
- $fs->dumpFile($input->getArgument('output'), implode(PHP_EOL, $undefinedClients));
+
+ $outputFile = $input->getArgument('output');
+ assert(is_string($outputFile));
+ (new Filesystem())->dumpFile($outputFile, implode(PHP_EOL, $undefinedClients));
return 0;
}
@@ -133,7 +135,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
private function outputProgress(OutputInterface $output, string $result, int $count, int $totalCount, bool $end = false): int
{
if (($count % 70) === 0 || $end) {
- $formatString = '%s %'.strlen($totalCount).'d / %-'.strlen($totalCount).'d (%3d%%)';
+ $formatString = '%s %'.strlen((string)$totalCount).'d / %-'.strlen((string)$totalCount).'d (%3d%%)';
$result = $end ? str_repeat(' ', 70 - ($count % 70)) : $result;
$output->writeln(sprintf($formatString, $result, $count, $totalCount, $count / $totalCount * 100));
} else {
@@ -164,20 +166,21 @@ private function getResult(Client $client): string
return '.';
}
- private function getFiles(InputInterface $input): Finder
+ /** @psalm-return array|iterable */
+ private function getFiles(InputInterface $input): iterable
{
$finder = Finder::create();
- if ($input->getOption('log-file')) {
- $file = $input->getOption('log-file');
- $finder->append(Finder::create()->in(dirname($file))->name(basename($file)));
+ $logFile = $input->getOption('log-file');
+ if (is_string($logFile)) {
+ $finder->append(Finder::create()->in(dirname($logFile))->name(basename($logFile)));
}
- if ($input->getOption('log-dir')) {
- $dirFinder = Finder::create()
- ->in($input->getOption('log-dir'));
- array_map(array($dirFinder, 'name'), $input->getOption('include'));
- array_map(array($dirFinder, 'notName'), $input->getOption('exclude'));
+ $logDir = $input->getOption('log-dir');
+ if (is_string($logDir)) {
+ $dirFinder = Finder::create()->in($logDir);
+ array_map(array($dirFinder, 'name'), array_filter((array)$input->getOption('include'),'is_string'));
+ array_map(array($dirFinder, 'notName'), array_filter((array)$input->getOption('exclude'),'is_string'));
$finder->append($dirFinder);
}
diff --git a/src/Command/ParserCommand.php b/src/Command/ParserCommand.php
index ae087dc..4d1396b 100644
--- a/src/Command/ParserCommand.php
+++ b/src/Command/ParserCommand.php
@@ -23,7 +23,6 @@ protected function configure()
->setDescription('Parses a user agent string and dumps the results.')
->addArgument(
'user-agent',
- null,
InputArgument::REQUIRED,
'User agent string to analyze'
)
@@ -32,7 +31,9 @@ protected function configure()
protected function execute(InputInterface $input, OutputInterface $output): int
{
- $result = Parser::create()->parse($input->getArgument('user-agent'));
+ $userAgent = $input->getArgument('user-agent');
+ assert(is_string($userAgent));
+ $result = Parser::create()->parse($userAgent);
$output->writeln(json_encode($result, JSON_PRETTY_PRINT));
diff --git a/src/DeviceParser.php b/src/DeviceParser.php
index 9bbb3d2..c3baef5 100644
--- a/src/DeviceParser.php
+++ b/src/DeviceParser.php
@@ -14,6 +14,8 @@
class DeviceParser extends AbstractParser
{
+ use ParserFactoryMethods;
+
/** Attempts to see if the user agent matches a device regex from regexes.php */
public function parseDevice(string $userAgent): Device
{
@@ -22,7 +24,7 @@ public function parseDevice(string $userAgent): Device
[$regex, $matches] = self::tryMatch($this->regexes['device_parsers'], $userAgent);
if ($matches) {
- $device->family = self::multiReplace($regex, 'device_replacement', $matches[1], $matches);
+ $device->family = self::multiReplace($regex, 'device_replacement', $matches[1], $matches) ?? $device->family;
$device->brand = self::multiReplace($regex, 'brand_replacement', null, $matches);
$deviceModelDefault = $matches[1] !== 'Other' ? $matches[1] : null;
$device->model = self::multiReplace($regex, 'model_replacement', $deviceModelDefault, $matches);
diff --git a/src/Exception/FetcherException.php b/src/Exception/FetcherException.php
index dacff25..3b9a0bd 100644
--- a/src/Exception/FetcherException.php
+++ b/src/Exception/FetcherException.php
@@ -8,11 +8,11 @@
*/
namespace UAParser\Exception;
-class FetcherException extends DomainException
+final class FetcherException extends DomainException
{
public static function httpError(string $resource, string $error): self
{
- return new static(
+ return new self(
sprintf('Could not fetch HTTP resource "%s": %s', $resource, $error)
);
}
diff --git a/src/Exception/FileNotFoundException.php b/src/Exception/FileNotFoundException.php
index 6117cf9..58454e4 100644
--- a/src/Exception/FileNotFoundException.php
+++ b/src/Exception/FileNotFoundException.php
@@ -10,16 +10,16 @@
use Exception;
-class FileNotFoundException extends Exception
+final class FileNotFoundException extends Exception
{
public static function fileNotFound(string $file): self
{
- return new static(sprintf('File "%s" does not exist', $file));
+ return new self(sprintf('File "%s" does not exist', $file));
}
public static function customRegexFileNotFound(string $file): self
{
- return new static(
+ return new self(
sprintf(
'ua-parser cannot find the custom regexes file you supplied ("%s"). Please make sure you have the correct path.',
$file
@@ -29,7 +29,7 @@ public static function customRegexFileNotFound(string $file): self
public static function defaultFileNotFound(string $file): self
{
- return new static(
+ return new self(
sprintf(
'Please download the "%s" file before using ua-parser by running "php bin/uaparser ua-parser:update"',
$file
diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php
index 3e32671..d2fab74 100644
--- a/src/Exception/InvalidArgumentException.php
+++ b/src/Exception/InvalidArgumentException.php
@@ -10,11 +10,11 @@
use InvalidArgumentException as BaseInvalidArgumentException;
-class InvalidArgumentException extends BaseInvalidArgumentException
+final class InvalidArgumentException extends BaseInvalidArgumentException
{
public static function oneOfCommandArguments(string ...$args): self
{
- return new static(
+ return new self(
sprintf('One of the command arguments "%s" is required', implode('", "', $args))
);
}
diff --git a/src/Exception/ReaderException.php b/src/Exception/ReaderException.php
index d4b278e..50dfe28 100644
--- a/src/Exception/ReaderException.php
+++ b/src/Exception/ReaderException.php
@@ -8,10 +8,15 @@
*/
namespace UAParser\Exception;
-class ReaderException extends DomainException
+final class ReaderException extends DomainException
{
public static function userAgentParserError(string $line): self
{
- return new static(sprintf('Cannot extract user agent string from line "%s"', $line));
+ return new self(sprintf('Cannot extract user agent string from line "%s"', $line));
+ }
+
+ public static function readerNotFound(string $line): self
+ {
+ return new self(sprintf('Cannot find reader that can handle "%s"', $line));
}
}
diff --git a/src/OperatingSystemParser.php b/src/OperatingSystemParser.php
index 90ada6b..1816a59 100644
--- a/src/OperatingSystemParser.php
+++ b/src/OperatingSystemParser.php
@@ -14,6 +14,8 @@
class OperatingSystemParser extends AbstractParser
{
+ use ParserFactoryMethods;
+
/** Attempts to see if the user agent matches an operating system regex from regexes.php */
public function parseOperatingSystem(string $userAgent): OperatingSystem
{
@@ -22,7 +24,7 @@ public function parseOperatingSystem(string $userAgent): OperatingSystem
[$regex, $matches] = self::tryMatch($this->regexes['os_parsers'], $userAgent);
if ($matches) {
- $os->family = self::multiReplace($regex, 'os_replacement', $matches[1], $matches);
+ $os->family = self::multiReplace($regex, 'os_replacement', $matches[1], $matches) ?? $os->family;
$os->major = self::multiReplace($regex, 'os_v1_replacement', $matches[2], $matches);
$os->minor = self::multiReplace($regex, 'os_v2_replacement', $matches[3], $matches);
$os->patch = self::multiReplace($regex, 'os_v3_replacement', $matches[4], $matches);
diff --git a/src/Parser.php b/src/Parser.php
index 6a74ebf..857fe7b 100644
--- a/src/Parser.php
+++ b/src/Parser.php
@@ -13,6 +13,8 @@
class Parser extends AbstractParser
{
+ use ParserFactoryMethods;
+
/** @var DeviceParser */
private $deviceParser;
diff --git a/src/ParserFactoryMethods.php b/src/ParserFactoryMethods.php
new file mode 100644
index 0000000..a5a672a
--- /dev/null
+++ b/src/ParserFactoryMethods.php
@@ -0,0 +1,68 @@
+regexes['user_agent_parsers'], $userAgent);
if ($matches) {
- $ua->family = self::multiReplace($regex, 'family_replacement', $matches[1], $matches);
+ $ua->family = self::multiReplace($regex, 'family_replacement', $matches[1], $matches) ?? $ua->family;
$ua->major = self::multiReplace($regex, 'v1_replacement', $matches[2], $matches);
$ua->minor = self::multiReplace($regex, 'v2_replacement', $matches[3], $matches);
$ua->patch = self::multiReplace($regex, 'v3_replacement', $matches[4], $matches);
@@ -42,7 +44,11 @@ public function parseUserAgent(string $userAgent, array $jsParseBits = array()):
$jsUserAgentString = $jsParseBits['js_user_agent_string'];
if (strpos($jsUserAgentString, 'Chrome/') !== false && strpos($userAgent, 'chromeframe') !== false) {
$override = $this->parseUserAgent($jsUserAgentString);
- $ua->family = sprintf('Chrome Frame (%s %s)', $ua->family, $ua->major);
+ $family = $ua->family;
+ $ua->family = 'Chrome Frame';
+ if ($family !== null && $ua->major !== null) {
+ $ua->family .= sprintf(' (%s %s)', $family, $ua->major);
+ }
$ua->major = $override->major;
$ua->minor = $override->minor;
$ua->patch = $override->patch;
diff --git a/src/Util/Fetcher.php b/src/Util/Fetcher.php
index cc87c50..3c7a40c 100644
--- a/src/Util/Fetcher.php
+++ b/src/Util/Fetcher.php
@@ -41,12 +41,12 @@ public function __construct($streamContext = null)
public function fetch()
{
$level = error_reporting(0);
- $result = file_get_contents($this->resourceUri, null, $this->streamContext);
+ $result = file_get_contents($this->resourceUri, false, $this->streamContext);
error_reporting($level);
if ($result === false) {
$error = error_get_last();
- throw FetcherException::httpError($this->resourceUri, $error['message']);
+ throw FetcherException::httpError($this->resourceUri, $error['message'] ?? 'Undefined error');
}
return $result;
diff --git a/src/Util/Logfile/AbstractReader.php b/src/Util/Logfile/AbstractReader.php
index 8820a18..1512262 100644
--- a/src/Util/Logfile/AbstractReader.php
+++ b/src/Util/Logfile/AbstractReader.php
@@ -23,6 +23,8 @@ public static function factory(string $line): ReaderInterface
return $reader;
}
}
+
+ throw ReaderException::readerNotFound($line);
}
private static function getReaders(): array
diff --git a/tests/AbstractParserTest.php b/tests/AbstractParserTest.php
index 7172625..5593ee9 100644
--- a/tests/AbstractParserTest.php
+++ b/tests/AbstractParserTest.php
@@ -8,7 +8,6 @@
*/
namespace UAParser\Test;
-use UAParser\AbstractParser;
use UAParser\Exception\FileNotFoundException;
abstract class AbstractParserTest extends AbstractTestCase
@@ -56,7 +55,7 @@ public function testExceptionOnFileNotFoundInvalidDefault(): void
public function testDefaultFileIsAbsolute(): void
{
- $class = new \ReflectionClass(AbstractParser::class);
+ $class = new \ReflectionClass($this->getParserClassName());
$method = $class->getMethod('getDefaultFile');
$method->setAccessible(true);
@@ -65,7 +64,9 @@ public function testDefaultFileIsAbsolute(): void
public function tearDown(): void
{
- AbstractParser::$defaultFile = null;
+ $parserClassName = $this->getParserClassName();
+
+ $parserClassName::$defaultFile = null;
}
abstract protected function getParserClassName(): string;