diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd534c6..5c439d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest] - php-versions: ['5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] + php-versions: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] runs-on: ${{ matrix.operating-system }} diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..b8b52e4 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,35 @@ +on: + pull_request: + push: + branches: + - master + +name: phpstan +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.2' + + - name: Install dependencies with composer + run: composer install + + - name: Cache phpstan cache + uses: actions/cache@v4 + with: + path: .phpstan-cache + key: phpstan-cache-${{ github.run_id }} # see https://github.com/phpstan/phpstan/discussions/9301 + restore-keys: | + phpstan-cache- + + - name: PHPStan + run: make phpstan diff --git a/.gitignore b/.gitignore index f6ceaf8..970c903 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ phpunit.xml tests/user_agents.json *.cache +/.phpstan-cache diff --git a/.helpers/constants.php b/.helpers/constants.php index dc750a9..33242b6 100644 --- a/.helpers/constants.php +++ b/.helpers/constants.php @@ -2,6 +2,7 @@ require __DIR__ . '/../vendor/autoload.php'; +// @phpstan-ignore argument.type $class = new ReflectionClass($argv[1]); echo "Predefined helper constants from `{$class->getName()}`\n\n"; @@ -9,7 +10,8 @@ echo "| Constant | {$argv[2]} | \n|----------|----------| \n"; foreach( $class->getConstants() as $constant => $value ) { + assert(is_string($value)); echo "| `{$class->getShortName()}::{$constant}` | {$value} | \n"; } -echo "\n"; \ No newline at end of file +echo "\n"; diff --git a/Makefile b/Makefile index 61f4653..7eead1b 100644 --- a/Makefile +++ b/Makefile @@ -16,3 +16,7 @@ generate: init: php bin/init_user_agent.php > tests/user_agents.tmp.json && mv tests/user_agents.tmp.json tests/user_agents.dist.json make generate + +.PHONY: phpstan +phpstan: + php vendor/bin/phpstan analyse --memory-limit 2g diff --git a/README.md b/README.md index cf3a00c..de2f2dc 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ All that said, there is the start of a [branch to do it](https://github.com/dona ## Requirements -- **php**: >=5.4.0 +- **php**: >=7.2.0 - **ext-ctype**: * ## Installing diff --git a/bin/benchmark.php b/bin/benchmark.php index 4f06139..58b9700 100644 --- a/bin/benchmark.php +++ b/bin/benchmark.php @@ -3,9 +3,15 @@ require __DIR__ . '/../src/UserAgentParser.php'; $time = microtime(true); +$jsonfile = __DIR__ . '/../tests/user_agents.dist.json'; +$content = file_get_contents($jsonfile); +if( $content === false ) { + echo "Failed to read file: $jsonfile\n"; + exit(1); +} -$uas = json_decode(file_get_contents(__DIR__ . '/../tests/user_agents.dist.json'), true); - +$uas = json_decode($content, true); +assert(is_array($uas)); foreach( $uas as $ua => $junk ) { $uatime = microtime(true); diff --git a/bin/constant_generator.php b/bin/constant_generator.php index ffd1420..a618e94 100644 --- a/bin/constant_generator.php +++ b/bin/constant_generator.php @@ -3,12 +3,19 @@ require __DIR__ . '/../vendor/autoload.php'; $jsonfile = __DIR__ . '/../tests/user_agents.dist.json'; +$content = file_get_contents($jsonfile); +if( $content === false ) { + echo "Failed to read file: $jsonfile\n"; + die(1); +} $uas = json_decode( - file_get_contents($jsonfile), + $content, true ); +assert(is_array($uas)); + $platforms = []; $browsers = []; foreach( $uas as $key => $val ) { @@ -55,7 +62,7 @@ $browserBody = "{$header}namespace donatj\UserAgent;\n\ninterface Browsers {\n\n"; $maxKey = max(array_map('strlen', array_keys($browsers))); foreach( $browsers as $const => $val ) { - $browserBody .= sprintf("\tconst %-{$maxKey}s = %s;\n", $const, var_export(key($val), true)); + $browserBody .= sprintf("\tpublic const %-{$maxKey}s = %s;\n", $const, var_export(key($val), true)); } $browserBody .= "\n}\n\n"; @@ -69,9 +76,9 @@ $platformBody = "{$header}namespace donatj\UserAgent;\n\ninterface Platforms {\n\n"; $maxKey = max(array_map('strlen', array_keys($platforms))); foreach( $platforms as $const => $val ) { - $platformBody .= sprintf("\tconst %-{$maxKey}s = %s;\n", $const, var_export(key($val), true)); + $platformBody .= sprintf("\tpublic const %-{$maxKey}s = %s;\n", $const, var_export(key($val), true)); } $platformBody .= "\n}\n\n"; file_put_contents(__DIR__ . '/../src/UserAgent/Browsers.php', $browserBody); -file_put_contents(__DIR__ . '/../src/UserAgent/Platforms.php', $platformBody); \ No newline at end of file +file_put_contents(__DIR__ . '/../src/UserAgent/Platforms.php', $platformBody); diff --git a/bin/init_user_agent.php b/bin/init_user_agent.php index 170d8e3..8acc059 100644 --- a/bin/init_user_agent.php +++ b/bin/init_user_agent.php @@ -4,7 +4,14 @@ $jsonfile = __DIR__ . '/../Tests/user_agents.dist.json'; -$uas = json_decode(file_get_contents($jsonfile), true); +$content = file_get_contents($jsonfile); +if( $content === false ) { + echo "Failed to read file: $jsonfile\n"; + exit(1); +} + +$uas = json_decode($content, true); +assert(is_array($uas)); foreach( $uas as $key => &$val ) { $val = parse_user_agent($key); diff --git a/bin/user_agent_sorter.php b/bin/user_agent_sorter.php index f52d107..82d3c33 100644 --- a/bin/user_agent_sorter.php +++ b/bin/user_agent_sorter.php @@ -4,7 +4,14 @@ $jsonfile = __DIR__ . '/../Tests/user_agents.dist.json'; -$uas = json_decode(file_get_contents($jsonfile), true); +$content = file_get_contents($jsonfile); +if( $content === false ) { + echo "Failed to read file: $jsonfile\n"; + exit(1); +} + +$uas = json_decode($content, true); +assert(is_array($uas)); foreach( $uas as $key => &$val ) { $val['key'] = $key; @@ -73,6 +80,11 @@ echo $json; +/** + * @param string $a + * @param string $b + * @return 0|1|-1 + */ function compare_version( $a, $b ) { $cmp_a = explode('.', $a); $cmp_b = explode('.', $b); @@ -91,10 +103,10 @@ function compare_version( $a, $b ) { } if( $cmp = strcmp($aa, $bb) ) { - $value = $cmp / abs($cmp); + $value = intval($cmp / abs($cmp)); break; } } return $value; -} \ No newline at end of file +} diff --git a/composer.json b/composer.json index 1d64183..79f9ef9 100644 --- a/composer.json +++ b/composer.json @@ -12,14 +12,15 @@ "homepage": "https://donatstudios.com/PHP-Parser-HTTP_USER_AGENT", "license": "MIT", "require": { - "php": ">=5.4.0", + "php": ">=7.2.0", "ext-ctype": "*" }, "require-dev": { "camspiers/json-pretty": "~1.0", "phpunit/phpunit": "~4.8|~9", "donatj/drop": "*", - "ext-json": "*" + "ext-json": "*", + "phpstan/phpstan": "^1.11" }, "authors": [ { diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..2750f24 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,9 @@ +parameters: + level: 9 + tmpDir: .phpstan-cache + paths: + - src + - bin + - examples + - .helpers + phpVersion: 70200 diff --git a/src/UserAgent/Browsers.php b/src/UserAgent/Browsers.php index 990ed7b..d5514a6 100644 --- a/src/UserAgent/Browsers.php +++ b/src/UserAgent/Browsers.php @@ -6,57 +6,57 @@ interface Browsers { - const ADSBOT_GOOGLE = 'AdsBot-Google'; - const ANDROID_BROWSER = 'Android Browser'; - const APPLEBOT = 'Applebot'; - const BAIDUSPIDER = 'Baiduspider'; - const BINGBOT = 'bingbot'; - const BLACKBERRY_BROWSER = 'BlackBerry Browser'; - const BROWSER = 'Browser'; - const BUNJALLOO = 'Bunjalloo'; - const CAMINO = 'Camino'; - const CHATGPT_USER = 'ChatGPT-User'; - const CHROME = 'Chrome'; - const CURL = 'curl'; - const EDGE = 'Edge'; - const FACEBOOKEXTERNALHIT = 'facebookexternalhit'; - const FEEDVALIDATOR = 'FeedValidator'; - const FIREFOX = 'Firefox'; - const GOOGLEBOT = 'Googlebot'; - const GOOGLEBOT_IMAGE = 'Googlebot-Image'; - const GOOGLEBOT_VIDEO = 'Googlebot-Video'; - const GPTBOT = 'GPTBot'; - const HEADLESSCHROME = 'HeadlessChrome'; - const IEMOBILE = 'IEMobile'; - const IMESSAGEBOT = 'iMessageBot'; - const KINDLE = 'Kindle'; - const LYNX = 'Lynx'; - const MASTODON = 'Mastodon'; - const MIDORI = 'Midori'; - const MIUIBROWSER = 'MiuiBrowser'; - const MSIE = 'MSIE'; - const MSNBOT_MEDIA = 'msnbot-media'; - const NETFRONT = 'NetFront'; - const NINTENDOBROWSER = 'NintendoBrowser'; - const OAI_SEARCHBOT = 'OAI-SearchBot'; - const OCULUSBROWSER = 'OculusBrowser'; - const OPERA = 'Opera'; - const PUFFIN = 'Puffin'; - const SAFARI = 'Safari'; - const SAILFISHBROWSER = 'SailfishBrowser'; - const SAMSUNGBROWSER = 'SamsungBrowser'; - const SILK = 'Silk'; - const SLACKBOT = 'Slackbot'; - const TELEGRAMBOT = 'TelegramBot'; - const TIZENBROWSER = 'TizenBrowser'; - const TWITTERBOT = 'Twitterbot'; - const UC_BROWSER = 'UC Browser'; - const VALVE_STEAM_TENFOOT = 'Valve Steam Tenfoot'; - const VIVALDI = 'Vivaldi'; - const WGET = 'Wget'; - const WORDPRESS = 'WordPress'; - const YANDEX = 'Yandex'; - const YANDEXBOT = 'YandexBot'; + public const ADSBOT_GOOGLE = 'AdsBot-Google'; + public const ANDROID_BROWSER = 'Android Browser'; + public const APPLEBOT = 'Applebot'; + public const BAIDUSPIDER = 'Baiduspider'; + public const BINGBOT = 'bingbot'; + public const BLACKBERRY_BROWSER = 'BlackBerry Browser'; + public const BROWSER = 'Browser'; + public const BUNJALLOO = 'Bunjalloo'; + public const CAMINO = 'Camino'; + public const CHATGPT_USER = 'ChatGPT-User'; + public const CHROME = 'Chrome'; + public const CURL = 'curl'; + public const EDGE = 'Edge'; + public const FACEBOOKEXTERNALHIT = 'facebookexternalhit'; + public const FEEDVALIDATOR = 'FeedValidator'; + public const FIREFOX = 'Firefox'; + public const GOOGLEBOT = 'Googlebot'; + public const GOOGLEBOT_IMAGE = 'Googlebot-Image'; + public const GOOGLEBOT_VIDEO = 'Googlebot-Video'; + public const GPTBOT = 'GPTBot'; + public const HEADLESSCHROME = 'HeadlessChrome'; + public const IEMOBILE = 'IEMobile'; + public const IMESSAGEBOT = 'iMessageBot'; + public const KINDLE = 'Kindle'; + public const LYNX = 'Lynx'; + public const MASTODON = 'Mastodon'; + public const MIDORI = 'Midori'; + public const MIUIBROWSER = 'MiuiBrowser'; + public const MSIE = 'MSIE'; + public const MSNBOT_MEDIA = 'msnbot-media'; + public const NETFRONT = 'NetFront'; + public const NINTENDOBROWSER = 'NintendoBrowser'; + public const OAI_SEARCHBOT = 'OAI-SearchBot'; + public const OCULUSBROWSER = 'OculusBrowser'; + public const OPERA = 'Opera'; + public const PUFFIN = 'Puffin'; + public const SAFARI = 'Safari'; + public const SAILFISHBROWSER = 'SailfishBrowser'; + public const SAMSUNGBROWSER = 'SamsungBrowser'; + public const SILK = 'Silk'; + public const SLACKBOT = 'Slackbot'; + public const TELEGRAMBOT = 'TelegramBot'; + public const TIZENBROWSER = 'TizenBrowser'; + public const TWITTERBOT = 'Twitterbot'; + public const UC_BROWSER = 'UC Browser'; + public const VALVE_STEAM_TENFOOT = 'Valve Steam Tenfoot'; + public const VIVALDI = 'Vivaldi'; + public const WGET = 'Wget'; + public const WORDPRESS = 'WordPress'; + public const YANDEX = 'Yandex'; + public const YANDEXBOT = 'YandexBot'; } diff --git a/src/UserAgent/Platforms.php b/src/UserAgent/Platforms.php index 3048f53..d4bfa95 100644 --- a/src/UserAgent/Platforms.php +++ b/src/UserAgent/Platforms.php @@ -6,37 +6,37 @@ interface Platforms { - const MACINTOSH = 'Macintosh'; - const CHROME_OS = 'Chrome OS'; - const LINUX = 'Linux'; - const WINDOWS = 'Windows'; - const ANDROID = 'Android'; - const BLACKBERRY = 'BlackBerry'; - const FREEBSD = 'FreeBSD'; - const IPAD = 'iPad'; - const IPHONE = 'iPhone'; - const IPOD = 'iPod'; - const KINDLE = 'Kindle'; - const KINDLE_FIRE = 'Kindle Fire'; - const NETBSD = 'NetBSD'; - const NEW_NINTENDO_3DS = 'New Nintendo 3DS'; - const NINTENDO_3DS = 'Nintendo 3DS'; - const NINTENDO_DS = 'Nintendo DS'; - const NINTENDO_SWITCH = 'Nintendo Switch'; - const NINTENDO_WII = 'Nintendo Wii'; - const NINTENDO_WIIU = 'Nintendo WiiU'; - const OPENBSD = 'OpenBSD'; - const PLAYBOOK = 'PlayBook'; - const PLAYSTATION_3 = 'PlayStation 3'; - const PLAYSTATION_4 = 'PlayStation 4'; - const PLAYSTATION_5 = 'PlayStation 5'; - const PLAYSTATION_VITA = 'PlayStation Vita'; - const SAILFISH = 'Sailfish'; - const SYMBIAN = 'Symbian'; - const TIZEN = 'Tizen'; - const WINDOWS_PHONE = 'Windows Phone'; - const XBOX = 'Xbox'; - const XBOX_ONE = 'Xbox One'; + public const MACINTOSH = 'Macintosh'; + public const CHROME_OS = 'Chrome OS'; + public const LINUX = 'Linux'; + public const WINDOWS = 'Windows'; + public const ANDROID = 'Android'; + public const BLACKBERRY = 'BlackBerry'; + public const FREEBSD = 'FreeBSD'; + public const IPAD = 'iPad'; + public const IPHONE = 'iPhone'; + public const IPOD = 'iPod'; + public const KINDLE = 'Kindle'; + public const KINDLE_FIRE = 'Kindle Fire'; + public const NETBSD = 'NetBSD'; + public const NEW_NINTENDO_3DS = 'New Nintendo 3DS'; + public const NINTENDO_3DS = 'Nintendo 3DS'; + public const NINTENDO_DS = 'Nintendo DS'; + public const NINTENDO_SWITCH = 'Nintendo Switch'; + public const NINTENDO_WII = 'Nintendo Wii'; + public const NINTENDO_WIIU = 'Nintendo WiiU'; + public const OPENBSD = 'OpenBSD'; + public const PLAYBOOK = 'PlayBook'; + public const PLAYSTATION_3 = 'PlayStation 3'; + public const PLAYSTATION_4 = 'PlayStation 4'; + public const PLAYSTATION_5 = 'PlayStation 5'; + public const PLAYSTATION_VITA = 'PlayStation Vita'; + public const SAILFISH = 'Sailfish'; + public const SYMBIAN = 'Symbian'; + public const TIZEN = 'Tizen'; + public const WINDOWS_PHONE = 'Windows Phone'; + public const XBOX = 'Xbox'; + public const XBOX_ONE = 'Xbox One'; } diff --git a/src/UserAgentParser.php b/src/UserAgentParser.php index 1da2734..aa8f210 100644 --- a/src/UserAgentParser.php +++ b/src/UserAgentParser.php @@ -17,7 +17,7 @@ * This method is defined for backwards comparability with the old global method. * * @param string|null $u_agent User agent string to parse or null. Uses $_SERVER['HTTP_USER_AGENT'] on NULL - * @return string[] an array with 'browser', 'version' and 'platform' keys + * @return array{platform:string|null,browser:string|null,version:string|null} array with 'browser', 'version' and 'platform' keys * @throws \InvalidArgumentException on not having a proper user agent to parse. * * @deprecated This exists for backwards compatibility with 0.x and will likely be removed in 2.x @@ -38,7 +38,7 @@ function parse_user_agent( $u_agent = null ) { * Parses a user agent string into its important parts * * @param string|null $u_agent User agent string to parse or null. Uses $_SERVER['HTTP_USER_AGENT'] on NULL - * @return string[] an array with 'browser', 'version' and 'platform' keys + * @return array{platform:string|null,browser:string|null,version:string|null} an array with 'browser', 'version' and 'platform' keys * @throws \InvalidArgumentException on not having a proper user agent to parse. */ function parse_user_agent( $u_agent = null ) { diff --git a/tests/UserAgentParserFunctionTest.php b/tests/UserAgentParserFunctionTest.php index 89f09aa..0167ede 100644 --- a/tests/UserAgentParserFunctionTest.php +++ b/tests/UserAgentParserFunctionTest.php @@ -5,12 +5,12 @@ class UserAgentParserFunctionTest extends \PHPUnit\Framework\TestCase { /** * @dataProvider userAgentDataProvider */ - public function test_parse_user_agent( $string, $expected ) { + public function test_parse_user_agent( string $string, array $expected ) : void { $result = parse_user_agent($string); $this->assertSame($expected, $result, $string . " test failed!"); } - public function userAgentDataProvider() { + public static function userAgentDataProvider() : array { $out = []; if( file_exists(__DIR__ . '/user_agents.json') && filesize(__DIR__ . '/user_agents.json') > 0 ) { $uas = json_decode(file_get_contents(__DIR__ . '/user_agents.json'), true); @@ -25,7 +25,7 @@ public function userAgentDataProvider() { return $out; } - public function test_parse_user_agent_empty() { + public function test_parse_user_agent_empty() : void { $expected = [ 'platform' => null, 'browser' => null, @@ -47,7 +47,7 @@ public function test_parse_user_agent_empty() { * * @see https://thephp.cc/news/2016/02/questioning-phpunit-best-practices */ - public function test_no_user_agent_exception() { + public function test_no_user_agent_exception() : void { unset($_SERVER['HTTP_USER_AGENT']); try { parse_user_agent(); @@ -59,7 +59,7 @@ public function test_no_user_agent_exception() { $this->fail("Expected \InvalidArgumentException"); } - public function test_global_user_agent() { + public function test_global_user_agent() : void { $_SERVER['HTTP_USER_AGENT'] = 'Test/1.0'; $this->assertSame([ 'platform' => null, 'browser' => 'Test', 'version' => '1.0' ], parse_user_agent()); } diff --git a/tests/UserAgentParserObjectTest.php b/tests/UserAgentParserObjectTest.php index 320e2d4..ae7817c 100644 --- a/tests/UserAgentParserObjectTest.php +++ b/tests/UserAgentParserObjectTest.php @@ -4,7 +4,7 @@ class UserAgentParserObjectTest extends \PHPUnit\Framework\TestCase { - public function userAgentDataProvider() { + public function userAgentDataProvider() : array { $out = []; if( file_exists(__DIR__ . '/user_agents.json') && filesize(__DIR__ . '/user_agents.json') > 0 ) { $uas = json_decode(file_get_contents(__DIR__ . '/user_agents.json'), true); @@ -22,7 +22,7 @@ public function userAgentDataProvider() { /** * @dataProvider userAgentDataProvider */ - public function test_parse( $string ) { + public function test_parse( $string ) : void { $parser = new UserAgentParser; $result = $parser->parse($string); @@ -36,7 +36,7 @@ public function test_parse( $string ) { /** * @dataProvider userAgentDataProvider */ - public function test_invoke( $string ) { + public function test_invoke( $string ) : void { $parser = new UserAgentParser; $result = $parser($string);