diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1aaa294..982eeff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.4' coverage: none tools: composer-normalize env: @@ -39,7 +39,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.4' coverage: none tools: cs2pr, phpcs env: @@ -56,7 +56,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.4' coverage: none tools: cs2pr, php-cs-fixer env: @@ -73,7 +73,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.4' coverage: none tools: composer:v2, cs2pr, phpstan env: @@ -97,7 +97,7 @@ jobs: runs-on: "ubuntu-latest" strategy: matrix: - php-version: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] + php-version: ['8.2', '8.3', '8.4'] steps: - name: Checkout uses: actions/checkout@v4 @@ -121,4 +121,4 @@ jobs: - name: Install project dependencies run: composer upgrade --no-interaction --no-progress --prefer-dist - name: Tests (phpunit) - run: vendor/bin/phpunit --testdox --verbose + run: vendor/bin/phpunit --testdox diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml deleted file mode 100644 index 5c792e6..0000000 --- a/.github/workflows/sonarcloud.yml +++ /dev/null @@ -1,109 +0,0 @@ -name: sonarcloud -on: - workflow_dispatch: - push: - branches: [ "main" ] - -# Actions -# shivammathur/setup-php@v2 https://github.com/marketplace/actions/setup-php-action -# sonarsource/sonarcloud-github-action@master https://github.com/marketplace/actions/sonarcloud-scan - -jobs: - - tests-coverage: - name: Build code coverage - runs-on: "ubuntu-latest" - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - coverage: xdebug - tools: composer:v2 - env: - fail-fast: true - - name: Get composer cache directory - id: composer-cache - run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - name: Install project dependencies - run: composer upgrade --no-interaction --no-progress --prefer-dist - - name: Create code coverage - run: vendor/bin/phpunit --testdox --verbose --coverage-xml=build/coverage --coverage-clover=build/coverage/clover.xml --log-junit=build/coverage/junit.xml - - name: Store code coverage - uses: actions/upload-artifact@v4 - with: - name: code-coverage - path: build/coverage - - sonarcloud-secrets: - name: SonarCloud check secrets are present - runs-on: ubuntu-latest - outputs: - github: ${{ steps.check-secrets.outputs.github }} - sonar: ${{ steps.check-secrets.outputs.sonar }} - steps: - - name: Check secrets are present - id: check-secrets - run: | - if [ -n "${{ secrets.GITHUB_TOKEN }}" ]; then - echo "github=yes" >> $GITHUB_OUTPUT - else - echo "github=no" >> $GITHUB_OUTPUT - echo "::warning ::GITHUB_TOKEN non set" - fi - if [ -n "${{ secrets.SONAR_TOKEN }}" ]; then - echo "sonar=yes" >> $GITHUB_OUTPUT - else - echo "sonar=no" >> $GITHUB_OUTPUT - echo "::warning ::SONAR_TOKEN non set" - fi - - sonarcloud: - name: SonarCloud Scan and Report - needs: [ "tests-coverage", "sonarcloud-secrets" ] - if: ${{ needs.sonarcloud-secrets.outputs.github == 'yes' && needs.sonarcloud-secrets.outputs.sonar == 'yes' }} - runs-on: "ubuntu-latest" - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Unshallow clone to provide blame information - run: git fetch --unshallow - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - coverage: none - tools: composer:v2 - - name: Get composer cache directory - id: composer-cache - run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - name: Install project dependencies - run: composer upgrade --no-interaction --no-progress --prefer-dist - - name: Obtain code coverage - uses: actions/download-artifact@v4 - with: - name: code-coverage - path: build/coverage - - name: Prepare SonarCloud Code Coverage Files - run: | - sed 's#'$GITHUB_WORKSPACE'#/github/workspace#g' build/coverage/junit.xml > build/sonar-junit.xml - sed 's#'$GITHUB_WORKSPACE'#/github/workspace#g' build/coverage/clover.xml > build/sonar-coverage.xml - - name: SonarCloud Scan - uses: sonarsource/sonarcloud-github-action@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/sonarqube-cloud.yml b/.github/workflows/sonarqube-cloud.yml new file mode 100644 index 0000000..5f098d8 --- /dev/null +++ b/.github/workflows/sonarqube-cloud.yml @@ -0,0 +1,53 @@ +name: "SonarQube Cloud" +on: + workflow_dispatch: + push: + branches: [ "main" ] + +# Actions +# shivammathur/setup-php@v2 https://github.com/marketplace/actions/setup-php-action +# SonarSource/sonarqube-scan-action@v5 https://github.com/marketplace/actions/official-sonarqube-scan + +jobs: + + sonarqube-cloud: + name: SonarCloud Scan and Report + runs-on: "ubuntu-latest" + steps: + - name: Check SONAR_TOKEN secret + run: | + if [ -z "${{ secrets.SONAR_TOKEN }}" ]; then + echo "::warning ::SONAR_TOKEN non set" + exit 1 + fi + - name: Checkout + uses: actions/checkout@v4 + - name: Unshallow clone to provide blame information + run: git fetch --unshallow + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + coverage: xdebug + tools: composer:v2 + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + - name: Install project dependencies + run: composer upgrade --no-interaction --no-progress --prefer-dist + - name: Create code coverage + run: vendor/bin/phpunit --testdox --coverage-xml=build/coverage --coverage-clover=build/coverage/clover.xml --log-junit=build/coverage/junit.xml + - name: Prepare SonarCloud Code Coverage Files + run: | + sed 's#'$GITHUB_WORKSPACE'#/github/workspace#g' build/coverage/junit.xml > build/sonar-junit.xml + sed 's#'$GITHUB_WORKSPACE'#/github/workspace#g' build/coverage/clover.xml > build/sonar-coverage.xml + - name: SonarCloud Scan + uses: SonarSource/sonarqube-scan-action@v5 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.phive/phars.xml b/.phive/phars.xml index 694e12d..14544eb 100644 --- a/.phive/phars.xml +++ b/.phive/phars.xml @@ -1,8 +1,8 @@ - - - - - + + + + + diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index d0f7f78..98cbc5f 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -15,14 +15,15 @@ ->setRules([ '@PSR12' => true, '@PSR12:risky' => true, - '@PHP71Migration:risky' => true, - '@PHP73Migration' => true, + '@PHP82Migration:risky' => true, + '@PHP82Migration' => true, // symfony 'class_attributes_separation' => true, 'whitespace_after_comma_in_array' => true, 'no_empty_statement' => true, 'no_extra_blank_lines' => true, 'type_declaration_spaces' => true, + 'trailing_comma_in_multiline' => ['after_heredoc' => true, 'elements' => ['arrays', 'match', 'arguments', 'parameters']], 'no_blank_lines_after_phpdoc' => true, 'object_operator_without_whitespace' => true, 'binary_operator_spaces' => true, @@ -35,16 +36,18 @@ 'standardize_not_equals' => true, 'concat_space' => ['spacing' => 'one'], 'linebreak_after_opening_tag' => true, + 'fully_qualified_strict_types' => true, // symfony:risky 'no_alias_functions' => true, 'self_accessor' => true, // contrib 'not_operator_with_successor_space' => true, + 'ordered_imports' => ['imports_order' => ['class', 'function', 'const']], // @PSR12 sort_algorithm: none ]) ->setFinder( PhpCsFixer\Finder::create() ->in(__DIR__) ->append([__FILE__]) - ->exclude(['vendor', 'tools', 'build']) + ->exclude(['vendor', 'tools', 'build']), ) ; diff --git a/LICENSE b/LICENSE index 0d96a4a..80685b7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2021 - 2024 PhpCfdi https://www.phpcfdi.com/ +Copyright (c) 2021 - 2025 PhpCfdi https://www.phpcfdi.com/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 95580dc..defc81b 100644 --- a/README.md +++ b/README.md @@ -321,6 +321,6 @@ and licensed for use under the MIT License (MIT). Please see [LICENSE][] for mor [badge-build]: https://img.shields.io/github/actions/workflow/status/phpcfdi/image-captcha-resolver/build.yml?branch=main&logo=github-actions [badge-reliability]: https://sonarcloud.io/api/project_badges/measure?project=phpcfdi_image-captcha-resolver&metric=reliability_rating [badge-maintainability]: https://sonarcloud.io/api/project_badges/measure?project=phpcfdi_image-captcha-resolver&metric=sqale_rating -[badge-coverage]: https://img.shields.io/sonar/coverage/phpcfdi_image-captcha-resolver/main?logo=sonarcloud&server=https%3A%2F%2Fsonarcloud.io -[badge-violations]: https://img.shields.io/sonar/violations/phpcfdi_image-captcha-resolver/main?format=long&logo=sonarcloud&server=https%3A%2F%2Fsonarcloud.io +[badge-coverage]: https://img.shields.io/sonar/coverage/phpcfdi_image-captcha-resolver/main?logo=sonarqubecloud&server=https%3A%2F%2Fsonarcloud.io +[badge-violations]: https://img.shields.io/sonar/violations/phpcfdi_image-captcha-resolver/main?format=long&logo=sonarqubecloud&server=https%3A%2F%2Fsonarcloud.io [badge-downloads]: https://img.shields.io/packagist/dt/phpcfdi/image-captcha-resolver?logo=packagist diff --git a/composer.json b/composer.json index adb6ac7..22ea6ee 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,6 @@ "name": "phpcfdi/image-captcha-resolver", "description": "Conectores para resolver captchas de imágenes", "license": "MIT", - "keywords": [], "authors": [ { "name": "Carlos C Soto", @@ -16,7 +15,7 @@ "source": "https://github.com/phpcfdi/image-captcha-resolver" }, "require": { - "php": ">=7.3", + "php": ">=8.2", "ext-fileinfo": "*", "ext-json": "*", "php-http/discovery": "^1.14", @@ -30,8 +29,8 @@ "guzzlehttp/psr7": "^2.0", "php-http/guzzle7-adapter": "^1.0", "php-http/mock-client": "^1.4", - "phpunit/phpunit": "^9.5", - "symfony/process": "^5.4 || ^6.4 || ^7.1", + "phpunit/phpunit": "^11.5", + "symfony/process": "^6.4 || ^7.1", "vlucas/phpdotenv": "^5.3" }, "suggest": { @@ -63,21 +62,21 @@ "@dev:test" ], "dev:check-style": [ - "@php -r 'exit(intval(PHP_VERSION_ID >= 80000));' || $PHP_BINARY tools/composer-normalize normalize --dry-run", - "@php -r 'exit(intval(PHP_VERSION_ID >= 74000));' || $PHP_BINARY tools/php-cs-fixer fix --dry-run --verbose || true", + "@php tools/composer-normalize normalize --dry-run", + "@php tools/php-cs-fixer fix --dry-run --verbose || true", "@php tools/phpcs --colors -sp" ], "dev:coverage": [ - "@php -dzend_extension=xdebug.so -dxdebug.mode=coverage vendor/bin/phpunit --verbose --coverage-html build/coverage/html/" + "@php -dzend_extension=xdebug.so -dxdebug.mode=coverage vendor/bin/phpunit --coverage-html build/coverage/html/" ], "dev:fix-style": [ - "@php -r 'exit(intval(PHP_VERSION_ID >= 80000));' || $PHP_BINARY tools/composer-normalize normalize", - "@php -r 'exit(intval(PHP_VERSION_ID >= 74000));' || $PHP_BINARY tools/php-cs-fixer fix --verbose", + "@php tools/composer-normalize normalize", + "@php tools/php-cs-fixer fix --verbose", "@php tools/phpcbf --colors -sp" ], "dev:test": [ "@dev:check-style", - "@php vendor/bin/phpunit --testdox --verbose --stop-on-failure", + "@php vendor/bin/phpunit --testdox --stop-on-failure", "@php tools/phpstan analyse --no-progress --verbose" ] }, diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 90bced6..8de17e5 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,24 @@ Usamos [Versionado Semántico 2.0.0](SEMVER.md) por lo que puedes usar esta libr Pueden aparecer cambios no liberados que se integran a la rama principal, pero no ameritan una nueva liberación de versión, aunque sí su incorporación en la rama principal de trabajo. Generalmente, se tratan de cambios en el desarrollo. +## Versión 0.3.0 2025-09-13 + +- Se revisa la compatibilidad de PHP 8.4. +- Se cambian las definiciones implícitas a tipos *nullables* a explícitas. +- Se actualizó el año de la licencia. +- Se hacen diversos cambios para asegurar los tipos de datos y satisfacer PHPStan. + +Cambios de mantenimiento al entorno de desarrollo: + +- Se normalizó `composer.json`. +- Se actualiza a PHPUnit 11.5. +- Se actualizan las reglas para `php-cs-fixer` y `phpcs`. +- En los flujos de trabajo de GitHub: + - Se agrega PHP 8.4 a la matrix de pruebas. + - Los trabajos se ejecutan en PHP 8.4. + - Se actualiza la integración con SonarQube Cloud. +- Se actualizaron las herramientas de desarrollo. + ## Versión 0.2.4 2024-10-03 - Se agrega la compatibilidad de `symfony/process` a las versiones `6.x` y `7.x`. diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 299f73e..3a5a85a 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -18,7 +18,6 @@ - diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9ac7d4d..275c07d 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,10 +1,12 @@ - + + defaultTestSuite="default" + colors="true" + displayDetailsOnAllIssues="true" +> tests @@ -14,9 +16,9 @@ tests/Integration - + - ./src/ + src - + diff --git a/src/CaptchaAnswer.php b/src/CaptchaAnswer.php index 39f66bd..f4f4151 100644 --- a/src/CaptchaAnswer.php +++ b/src/CaptchaAnswer.php @@ -8,10 +8,9 @@ use LogicException; use Throwable; -final class CaptchaAnswer implements JsonSerializable, CaptchaAnswerInterface +final readonly class CaptchaAnswer implements JsonSerializable, CaptchaAnswerInterface { - /** @var string */ - private $value; + private string $value; public function __construct(string $value) { @@ -31,7 +30,7 @@ public function equalsTo($value): bool { try { return $this->value === strval($value); - } catch (Throwable $ex) { + } catch (Throwable) { return false; } } diff --git a/src/CaptchaAnswerInterface.php b/src/CaptchaAnswerInterface.php index 4bc6687..caec41d 100644 --- a/src/CaptchaAnswerInterface.php +++ b/src/CaptchaAnswerInterface.php @@ -15,7 +15,7 @@ public function getValue(): string; * Compare the current value to another * * @param Stringable|scalar $value hould be a string or string compatible - * @return bool + * @noinspection PhpMissingParamTypeInspection */ public function equalsTo($value): bool; } diff --git a/src/CaptchaImage.php b/src/CaptchaImage.php index 599da5d..0cc0610 100644 --- a/src/CaptchaImage.php +++ b/src/CaptchaImage.php @@ -6,14 +6,13 @@ use finfo; use InvalidArgumentException; +use LogicException; -final class CaptchaImage implements CaptchaImageInterface +final readonly class CaptchaImage implements CaptchaImageInterface { - /** @var string */ - private $contents; + private string $contents; - /** @var string */ - private $mimeType; + private string $mimeType; /** * CaptchaImage constructor. @@ -33,8 +32,8 @@ public function __construct(string $contents) throw new InvalidArgumentException('The captcha image is not base64 encoded'); } - $mimeType = self::finfo()->buffer($binary, FILEINFO_MIME_TYPE) ?: ''; - if ('image/' !== substr($mimeType, 0, 6)) { + $mimeType = $this->finfo()->buffer($binary, FILEINFO_MIME_TYPE) ?: ''; + if (! str_starts_with($mimeType, 'image/')) { throw new InvalidArgumentException('The captcha image is not an image'); } @@ -66,13 +65,16 @@ public static function newFromInlineHtml(string $contents): self return self::newFromBase64((string) preg_replace('#\s#', '', $parts['image'])); } - private static function finfo(): finfo + private function finfo(): finfo { // if finfo is used in other places on the project then move it to a static class static $finfo = null; if (null === $finfo) { $finfo = new finfo(); // @codeCoverageIgnore } + if (! $finfo instanceof finfo) { + throw new LogicException('Unable to create a finfo instance.'); + } return $finfo; } diff --git a/src/CaptchaResolverInterface.php b/src/CaptchaResolverInterface.php index 6d84f61..298bfdb 100644 --- a/src/CaptchaResolverInterface.php +++ b/src/CaptchaResolverInterface.php @@ -10,8 +10,6 @@ interface CaptchaResolverInterface * Perform the required operations to resolve the captcha * It must never return an empty string * - * @param CaptchaImageInterface $image - * @return CaptchaAnswerInterface * @throws UnableToResolveCaptchaException */ public function resolve(CaptchaImageInterface $image): CaptchaAnswerInterface; diff --git a/src/HttpClient/HttpClient.php b/src/HttpClient/HttpClient.php index edbbcb0..c6cbf4d 100644 --- a/src/HttpClient/HttpClient.php +++ b/src/HttpClient/HttpClient.php @@ -15,25 +15,13 @@ use RuntimeException; use Throwable; -final class HttpClient implements HttpClientInterface +final readonly class HttpClient implements HttpClientInterface { - /** @var ClientInterface */ - private $client; - - /** @var RequestFactoryInterface */ - private $requestFactory; - - /** @var StreamFactoryInterface */ - private $streamFactory; - public function __construct( - ClientInterface $client, - RequestFactoryInterface $requestFactory, - StreamFactoryInterface $streamFactory + private ClientInterface $client, + private RequestFactoryInterface $requestFactory, + private StreamFactoryInterface $streamFactory, ) { - $this->client = $client; - $this->requestFactory = $requestFactory; - $this->streamFactory = $streamFactory; } /** @@ -47,7 +35,7 @@ public static function discover(): self return new self( Psr18ClientDiscovery::find(), Psr17FactoryDiscovery::findRequestFactory(), - Psr17FactoryDiscovery::findStreamFactory() + Psr17FactoryDiscovery::findStreamFactory(), ); // @codeCoverageIgnoreStart } catch (Throwable $exception) { @@ -57,10 +45,7 @@ public static function discover(): self } /** - * @param string $method - * @param string $uri * @param array $headers - * @return RequestInterface */ public function createRequest(string $method, string $uri, array $headers): RequestInterface { @@ -74,10 +59,7 @@ public function createRequest(string $method, string $uri, array $headers): Requ } /** - * @param string $method - * @param string $uri * @param array $headers - * @return RequestInterface */ public function createJsonRequest(string $method, string $uri, array $headers): RequestInterface { @@ -88,20 +70,18 @@ public function createJsonRequest(string $method, string $uri, array $headers): return $this->createRequest($method, $uri, array_merge($jsonHeaders, $headers)); } - public function postJson(string $uri, array $headers = [], $data = null): ResponseInterface + public function postJson(string $uri, array $headers = [], mixed $data = null): ResponseInterface { $request = $this->createJsonRequest('POST', $uri, $headers); $request = $request->withBody( - $this->streamFactory->createStream(json_encode($data) ?: '') + $this->streamFactory->createStream(json_encode($data) ?: ''), ); return $this->send($request); } /** - * @param RequestInterface $request - * @return ResponseInterface * @throws HttpException */ public function send(RequestInterface $request): ResponseInterface diff --git a/src/HttpClient/HttpClientInterface.php b/src/HttpClient/HttpClientInterface.php index 8f94c3f..6d50cc7 100644 --- a/src/HttpClient/HttpClientInterface.php +++ b/src/HttpClient/HttpClientInterface.php @@ -9,11 +9,9 @@ interface HttpClientInterface { /** - * @param string $uri * @param array $headers * @param array|object|mixed $data - * @return ResponseInterface * @throws HttpException */ - public function postJson(string $uri, array $headers = [], $data = null): ResponseInterface; + public function postJson(string $uri, array $headers = [], mixed $data = null): ResponseInterface; } diff --git a/src/HttpClient/HttpException.php b/src/HttpClient/HttpException.php index de282fb..e081b35 100644 --- a/src/HttpClient/HttpException.php +++ b/src/HttpClient/HttpException.php @@ -12,20 +12,15 @@ class HttpException extends RuntimeException { - /** @var RequestInterface */ - private $request; - - /** @var ResponseInterface|null */ - private $response; + private readonly RequestInterface $request; public function __construct( RequestInterface $request, - ResponseInterface $response = null, - Throwable $previous = null + private readonly ?ResponseInterface $response = null, + ?Throwable $previous = null, ) { parent::__construct("Error on {$request->getMethod()} {$request->getUri()}", 0, $previous); $this->request = $request; - $this->response = $response; } public function getRequest(): RequestInterface diff --git a/src/Internal/TemporaryFile.php b/src/Internal/TemporaryFile.php index 4db41dd..90a74a1 100644 --- a/src/Internal/TemporaryFile.php +++ b/src/Internal/TemporaryFile.php @@ -10,10 +10,9 @@ * Class to create a temporary file and remove it on object destruction * @internal */ -final class TemporaryFile +final readonly class TemporaryFile { - /** @var string */ - private $path; + private string $path; public function __construct(string $prefix = '', string $directory = '') { diff --git a/src/Resolvers/AntiCaptchaResolver.php b/src/Resolvers/AntiCaptchaResolver.php index 4c5b0bb..2422835 100644 --- a/src/Resolvers/AntiCaptchaResolver.php +++ b/src/Resolvers/AntiCaptchaResolver.php @@ -16,7 +16,7 @@ use RuntimeException; use Throwable; -final class AntiCaptchaResolver implements CaptchaResolverInterface +final readonly class AntiCaptchaResolver implements CaptchaResolverInterface { public const DEFAULT_INITIAL_WAIT = 4; @@ -24,18 +24,10 @@ final class AntiCaptchaResolver implements CaptchaResolverInterface public const DEFAULT_WAIT = 2000; - /** @var AntiCaptchaConnector */ - private $connector; - - /** @var TimerInterface */ - private $timer; - public function __construct( - AntiCaptchaConnector $connector, - TimerInterface $timer + private AntiCaptchaConnector $connector, + private TimerInterface $timer, ) { - $this->connector = $connector; - $this->timer = $timer; } public function getConnector(): AntiCaptchaConnector @@ -51,22 +43,17 @@ public function getTimer(): TimerInterface /** * Factory method with defaults * - * @param string $clientKey - * @param int $initialWaitSeconds - * @param int $timeoutSeconds - * @param int $waitMilliseconds - * @return self * @throws UndiscoverableClientException */ public static function create( string $clientKey, int $initialWaitSeconds = self::DEFAULT_INITIAL_WAIT, int $timeoutSeconds = self::DEFAULT_TIMEOUT, - int $waitMilliseconds = self::DEFAULT_WAIT + int $waitMilliseconds = self::DEFAULT_WAIT, ): self { return new self( new AntiCaptchaConnector($clientKey), - new Timer($initialWaitSeconds, $timeoutSeconds, $waitMilliseconds) + new Timer($initialWaitSeconds, $timeoutSeconds, $waitMilliseconds), ); } diff --git a/src/Resolvers/AntiCaptchaResolver/AntiCaptchaConnector.php b/src/Resolvers/AntiCaptchaResolver/AntiCaptchaConnector.php index c0deeeb..93bec27 100644 --- a/src/Resolvers/AntiCaptchaResolver/AntiCaptchaConnector.php +++ b/src/Resolvers/AntiCaptchaResolver/AntiCaptchaConnector.php @@ -22,22 +22,15 @@ class AntiCaptchaConnector { public const BASE_URL = 'https://api.anti-captcha.com/'; - /** @var string */ - private $clientKey; - - /** @var HttpClientInterface */ - private $httpClient; + private readonly HttpClientInterface $httpClient; /** * AntiCaptchaConnector constructor. * - * @param string $clientKey - * @param HttpClientInterface|null $httpClient * @throws UndiscoverableClientException */ - public function __construct(string $clientKey, HttpClientInterface $httpClient = null) + public function __construct(private readonly string $clientKey, ?HttpClientInterface $httpClient = null) { - $this->clientKey = $clientKey; $this->httpClient = $httpClient ?? HttpClient::discover(); } @@ -51,12 +44,8 @@ public function getHttpClient(): HttpClientInterface return $this->httpClient; } - /** - * @param string|Stringable $base64Image - * @return string - * @throws RuntimeException - */ - public function createTask($base64Image): string + /** @throws RuntimeException */ + public function createTask(string|Stringable $base64Image): string { /** @see https://anti-captcha.com/es/apidoc/task-types/ImageToTextTask */ $postData = [ @@ -67,37 +56,59 @@ public function createTask($base64Image): string ], ]; + /** + * @see https://anti-captcha.com/es/apidoc/methods/createTask + * @phpstan-var stdClass&object{ + * errorId: int, + * errorCode?: string, + * errorDescription?: string, + * taskId: int, + * } $result + */ $result = $this->request('createTask', $postData); return (string) $result->taskId; } /** - * @param string $taskId - * @return string * @throws RuntimeException */ public function getTaskResult(string $taskId): string { - /** @see https://anti-captcha.com/es/apidoc/methods/getTaskResult */ + /** + * @see https://anti-captcha.com/es/apidoc/methods/getTaskResult + * @phpstan-var stdClass&object{ + * errorId: int, + * errorCode?: string, + * errorDescription?: string, + * status?: string, + * solution?: stdClass&object{text: string, url: string}, + * cost?: string, + * ip?: string, + * createTime?: int, + * endTime?: int, + * solveCount?: int, + * } $result + */ $result = $this->request('getTaskResult', [ 'taskId' => $taskId, ]); - $antiCaptchaStatus = strtolower($result->status); + $antiCaptchaStatus = strtolower($result->status ?? ''); if ('processing' === $antiCaptchaStatus) { return ''; } if ('ready' === $antiCaptchaStatus) { + if (! isset($result->solution)) { + throw new LogicException('Expected solution object was not received.'); + } return strval($result->solution->text ?? ''); } - throw new LogicException("Unknown status '$result->status' for task"); + throw new LogicException(sprintf("Unknown status '%s' for task", $result->status ?? '')); } /** - * @param string $methodName * @param array $postData - * @return stdClass * @throws RuntimeException When anti-captcha service return an error status */ public function request(string $methodName, array $postData): stdClass @@ -115,10 +126,11 @@ public function request(string $methodName, array $postData): stdClass if (! $result instanceof stdClass) { $result = (object) ['errorId' => 1, 'errorDescription' => 'Response is not a JSON object']; } + /** @phpstan-var stdClass&object{errorId?: scalar, errorDescription?: scalar} $result */ $errorId = intval($result->errorId ?? 0); if ($errorId > 0) { throw new RuntimeException( - sprintf('Anti-Captcha Error (%d): %s', $errorId, strval($result->errorDescription ?? '')) + sprintf('Anti-Captcha Error (%d): %s', $errorId, $result->errorDescription ?? ''), ); } diff --git a/src/Resolvers/CaptchaLocalResolver.php b/src/Resolvers/CaptchaLocalResolver.php index 21f2e74..c5f8fdc 100644 --- a/src/Resolvers/CaptchaLocalResolver.php +++ b/src/Resolvers/CaptchaLocalResolver.php @@ -14,7 +14,7 @@ use PhpCfdi\ImageCaptchaResolver\UnableToResolveCaptchaException; use Throwable; -class CaptchaLocalResolver implements CaptchaResolverInterface +readonly class CaptchaLocalResolver implements CaptchaResolverInterface { public const DEFAULT_INITIAL_WAIT = 5; @@ -22,37 +22,22 @@ class CaptchaLocalResolver implements CaptchaResolverInterface public const DEFAULT_WAIT = 500; - /** @var CaptchaLocalResolverConnector */ - private $connector; - - /** @var TimerInterface */ - private $timer; - public function __construct( - CaptchaLocalResolverConnector $connector, - TimerInterface $timer + private CaptchaLocalResolverConnector $connector, + private TimerInterface $timer, ) { - $this->connector = $connector; - $this->timer = $timer; } - /** - * @param string $baseUrl - * @param int $initialWaitSeconds - * @param int $timeoutSeconds - * @param int $sleepMilliseconds - * @return self - * @throws UndiscoverableClientException - */ + /** @throws UndiscoverableClientException */ public static function create( string $baseUrl, int $initialWaitSeconds = self::DEFAULT_INITIAL_WAIT, int $timeoutSeconds = self::DEFAULT_TIMEOUT, - int $sleepMilliseconds = self::DEFAULT_WAIT + int $sleepMilliseconds = self::DEFAULT_WAIT, ): self { return new self( new CaptchaLocalResolverConnector($baseUrl), - new Timer($initialWaitSeconds, $timeoutSeconds, $sleepMilliseconds) + new Timer($initialWaitSeconds, $timeoutSeconds, $sleepMilliseconds), ); } diff --git a/src/Resolvers/CaptchaLocalResolver/CaptchaLocalResolverConnector.php b/src/Resolvers/CaptchaLocalResolver/CaptchaLocalResolverConnector.php index 1a903ef..2a14390 100644 --- a/src/Resolvers/CaptchaLocalResolver/CaptchaLocalResolverConnector.php +++ b/src/Resolvers/CaptchaLocalResolver/CaptchaLocalResolverConnector.php @@ -23,31 +23,26 @@ class CaptchaLocalResolverConnector { /** @var string Full URL to access service, by example http://localhost:9095 */ - private $baseUrl; + private readonly string $baseUrl; - /** @var HttpClientInterface */ - private $httpClient; + private readonly HttpClientInterface $httpClient; /** * Connector constructor * * @param string $baseUrl Full URL to access service, by example http://localhost:9095 - * @param HttpClientInterface|null $httpClient * @throws UndiscoverableClientException */ - public function __construct(string $baseUrl, HttpClientInterface $httpClient = null) + public function __construct(string $baseUrl, ?HttpClientInterface $httpClient = null) { $this->baseUrl = rtrim($baseUrl, '/'); $this->httpClient = $httpClient ?? HttpClient::discover(); } /** - * @param CaptchaImageInterface $image - * @param TimerInterface $timer - * @return CaptchaAnswerInterface * @throws RuntimeException if unable to get an answer after seconds * @throws RuntimeException if unable to send image - * @throws RuntimeException if code does not exists + * @throws RuntimeException if code does not exist * @throws RuntimeException if unable to check code * @throws RuntimeException if http transaction error occurs */ @@ -70,8 +65,6 @@ public function resolveImage(CaptchaImageInterface $image, TimerInterface $timer } /** - * @param CaptchaImageInterface $image - * @return string * @throws RuntimeException if unable to send image * @throws RuntimeException if image was sent but service returns empty code * @throws RuntimeException if http transaction error occurs @@ -86,7 +79,7 @@ public function sendImage(CaptchaImageInterface $image): string } $contents = strval($response->getBody()); $data = json_decode($contents, true); - $code = (is_array($data)) ? strval($data['code'] ?? '') : ''; + $code = (is_array($data) && isset($data['code']) && is_scalar($data['code'])) ? strval($data['code']) : ''; if ('' === $code) { throw new RuntimeException('Image was sent but service returns empty code'); } @@ -94,11 +87,9 @@ public function sendImage(CaptchaImageInterface $image): string } /** - * Check code for answer, if empty string means answer does not exists yet + * Check code for answer, if empty string means answer does not exist yet * - * @param string $code - * @return string - * @throws RuntimeException if code does not exists + * @throws RuntimeException if code does not exist * @throws RuntimeException if unable to check code * @throws RuntimeException if http transaction error occurs */ @@ -119,7 +110,7 @@ public function checkCode(string $code): string } $contents = strval($response->getBody()); $data = json_decode($contents, true); - return (is_array($data)) ? strval($data['answer'] ?? '') : ''; + return (is_array($data) && isset($data['answer']) && is_scalar($data['answer'])) ? strval($data['answer']) : ''; } public function buildUri(string $action): string diff --git a/src/Resolvers/CommandLineResolver.php b/src/Resolvers/CommandLineResolver.php index b4ceced..3cd7b2e 100644 --- a/src/Resolvers/CommandLineResolver.php +++ b/src/Resolvers/CommandLineResolver.php @@ -13,28 +13,20 @@ use RuntimeException; use Throwable; -final class CommandLineResolver implements CaptchaResolverInterface +final readonly class CommandLineResolver implements CaptchaResolverInterface { /** @var string[] */ - private $command; - - /** @var CommandLineResolver\AnswerBuilderInterface */ - private $answerBuilder; - - /** @var CommandLineResolver\ProcessRunnerInterface */ - private $processRunner; + private array $command; /** * CommandLineResolver constructor. * * @param string[] $command - * @param CommandLineResolver\AnswerBuilderInterface $answerBuilder - * @param CommandLineResolver\ProcessRunnerInterface $processRunner */ public function __construct( array $command, - CommandLineResolver\AnswerBuilderInterface $answerBuilder, - CommandLineResolver\ProcessRunnerInterface $processRunner + private CommandLineResolver\AnswerBuilderInterface $answerBuilder, + private CommandLineResolver\ProcessRunnerInterface $processRunner, ) { if ([] === $command) { throw new LogicException('Invalid command argument'); @@ -43,25 +35,20 @@ public function __construct( throw new LogicException('Command cannot be "{file}"'); } $this->command = $command; - $this->answerBuilder = $answerBuilder; - $this->processRunner = $processRunner; } /** * @param string[] $command - * @param CommandLineResolver\AnswerBuilderInterface|null $answerBuilder - * @param CommandLineResolver\ProcessRunnerInterface|null $processRunner - * @return static */ public static function create( array $command, - CommandLineResolver\AnswerBuilderInterface $answerBuilder = null, - CommandLineResolver\ProcessRunnerInterface $processRunner = null + ?CommandLineResolver\AnswerBuilderInterface $answerBuilder = null, + ?CommandLineResolver\ProcessRunnerInterface $processRunner = null, ): self { return new self( $command, $answerBuilder ?? new CommandLineResolver\LastLineAnswerBuilder(), - $processRunner ?? new CommandLineResolver\SymfonyProcessRunner() + $processRunner ?? new CommandLineResolver\SymfonyProcessRunner(), ); } @@ -104,8 +91,6 @@ public function buildCommand(string $fileNameArgument): array } /** - * @param CaptchaImageInterface $image - * @return CaptchaAnswerInterface * @throws RuntimeException */ private function realResolve(CaptchaImageInterface $image): CaptchaAnswerInterface diff --git a/src/Resolvers/CommandLineResolver/ProcessResult.php b/src/Resolvers/CommandLineResolver/ProcessResult.php index 1afcf2e..aa5d316 100644 --- a/src/Resolvers/CommandLineResolver/ProcessResult.php +++ b/src/Resolvers/CommandLineResolver/ProcessResult.php @@ -6,20 +6,11 @@ final class ProcessResult { - /** @var int */ - private $exitCode; - - /** @var string[] */ - private $output; - - /** - * @param int $exitCode - * @param string[] $output - */ - public function __construct(int $exitCode, array $output) - { - $this->exitCode = $exitCode; - $this->output = $output; + /** @param string[] $output */ + public function __construct( + private readonly int $exitCode, + private array $output, + ) { } public function getExitCode(): int diff --git a/src/Resolvers/CommandLineResolver/ProcessRunnerInterface.php b/src/Resolvers/CommandLineResolver/ProcessRunnerInterface.php index ccfbc34..2302f97 100644 --- a/src/Resolvers/CommandLineResolver/ProcessRunnerInterface.php +++ b/src/Resolvers/CommandLineResolver/ProcessRunnerInterface.php @@ -9,8 +9,6 @@ interface ProcessRunnerInterface { /** - * @param string ...$command - * @return ProcessResult * @throws RuntimeException on any kind of execution error */ public function run(string ...$command): ProcessResult; diff --git a/src/Resolvers/CommandLineResolver/SymfonyProcessRunner.php b/src/Resolvers/CommandLineResolver/SymfonyProcessRunner.php index 81b03f9..ed9ec51 100644 --- a/src/Resolvers/CommandLineResolver/SymfonyProcessRunner.php +++ b/src/Resolvers/CommandLineResolver/SymfonyProcessRunner.php @@ -15,8 +15,7 @@ class SymfonyProcessRunner implements ProcessRunnerInterface { public const DEFAULT_TIMEOUT = 60; - /** @var float */ - private $timeoutSeconds; + private readonly int|float $timeoutSeconds; public function __construct(float $timeoutSeconds = self::DEFAULT_TIMEOUT) { @@ -40,7 +39,7 @@ public function run(string ...$command): ProcessResult $process->run( function (string $type, string $text) use (&$output): void { $output[] = rtrim($text, PHP_EOL); - } + }, ); } catch (ProcessTimedOutException $exception) { throw new RuntimeException("Process timeout after {$this->getTimeoutSeconds()} seconds", 0, $exception); diff --git a/src/Resolvers/ConsoleResolver.php b/src/Resolvers/ConsoleResolver.php index e8a1b25..939d9e5 100644 --- a/src/Resolvers/ConsoleResolver.php +++ b/src/Resolvers/ConsoleResolver.php @@ -24,16 +24,14 @@ class ConsoleResolver implements CaptchaResolverInterface public const MAX_WAIT = 300; - /** @var string */ - private $captchaOutputFile; - - /** @var float|int */ - private $waitForAnswerSeconds; - - public function __construct(string $captchaOutputFile = '', float $waitForAnswerSeconds = self::DEFAULT_WAIT) - { - $this->captchaOutputFile = $captchaOutputFile ?: (getcwd() . '/captcha.png'); - $this->waitForAnswerSeconds = $waitForAnswerSeconds; + private readonly string $captchaOutputFile; + + public function __construct( + string $captchaOutputFile = '', + private float|int $waitForAnswerSeconds = self::DEFAULT_WAIT, + ) { + $this->captchaOutputFile = ('' !== $captchaOutputFile) ? $captchaOutputFile : (getcwd() . '/captcha.png'); + $this->waitForAnswerSeconds = min(max(0, $waitForAnswerSeconds), self::MAX_WAIT); } public function resolve(CaptchaImageInterface $image): CaptchaAnswerInterface @@ -83,7 +81,6 @@ protected function openStdInStream() /** @throws RuntimeException */ protected function readLine(): string { - $this->waitForAnswerSeconds = min($this->waitForAnswerSeconds, self::MAX_WAIT); $timeoutSecs = intval(floor($this->waitForAnswerSeconds) ?: 0); $timeoutUsecs = intval(1000000 * ($this->waitForAnswerSeconds - $timeoutSecs)); diff --git a/src/Resolvers/MockResolver.php b/src/Resolvers/MockResolver.php index 13e03b7..8dc860f 100644 --- a/src/Resolvers/MockResolver.php +++ b/src/Resolvers/MockResolver.php @@ -14,7 +14,7 @@ final class MockResolver implements CaptchaResolverInterface, Countable { /** @var CaptchaAnswerInterface[]|UnableToResolveCaptchaException[] */ - private $resolveResponses; + private array $resolveResponses; /** * MockResolver constructor. diff --git a/src/Resolvers/MultiResolver.php b/src/Resolvers/MultiResolver.php index 51386d4..694c7a4 100644 --- a/src/Resolvers/MultiResolver.php +++ b/src/Resolvers/MultiResolver.php @@ -13,18 +13,21 @@ final class MultiResolver implements CaptchaResolverInterface, Countable { - /** @var CaptchaResolverInterface[] */ - private $resolvers; + /** @var list */ + private readonly array $resolvers; - /** @var array */ - private $lastResults = []; + private readonly int $resolversCount; + + /** @var list */ + private array $lastResults = []; public function __construct(CaptchaResolverInterface ...$resolvers) { - $this->resolvers = $resolvers; + $this->resolvers = array_values($resolvers); + $this->resolversCount = count($this->resolvers); } - /** @return CaptchaResolverInterface[] */ + /** @return list */ public function getResolvers(): array { return $this->resolvers; @@ -50,7 +53,7 @@ public function resolve(CaptchaImageInterface $image): CaptchaAnswerInterface public function count(): int { - return count($this->resolvers); + return $this->resolversCount; } public function clearResults(): void @@ -58,7 +61,7 @@ public function clearResults(): void $this->lastResults = []; } - /** @return array */ + /** @return list */ public function getLastResults(): array { return $this->lastResults; diff --git a/src/Timer/Timer.php b/src/Timer/Timer.php index 27ae1fc..9aa6fca 100644 --- a/src/Timer/Timer.php +++ b/src/Timer/Timer.php @@ -6,23 +6,13 @@ final class Timer implements TimerInterface { - /** @var int */ - private $initialSeconds; + private float $untilTime = 0; - /** @var int */ - private $timeoutSeconds; - - /** @var int */ - private $waitMilliseconds; - - /** @var float */ - private $untilTime = 0; - - public function __construct(int $initialSeconds, int $timeoutSeconds, int $waitMilliseconds) - { - $this->initialSeconds = $initialSeconds; - $this->timeoutSeconds = $timeoutSeconds; - $this->waitMilliseconds = $waitMilliseconds; + public function __construct( + private readonly int $initialSeconds, + private readonly int $timeoutSeconds, + private readonly int $waitMilliseconds, + ) { } public function getInitialSeconds(): int diff --git a/src/Timer/TimerInterface.php b/src/Timer/TimerInterface.php index f37b82a..6f91264 100644 --- a/src/Timer/TimerInterface.php +++ b/src/Timer/TimerInterface.php @@ -18,13 +18,11 @@ public function wait(): void; /** * Return true if the timer is expired - * @return bool */ public function isExpired(): bool; /** * The total time to wait to define that the timer is expired - * @return int */ public function getTimeoutSeconds(): int; } diff --git a/src/UnableToResolveCaptchaException.php b/src/UnableToResolveCaptchaException.php index 047b29b..b4db39c 100644 --- a/src/UnableToResolveCaptchaException.php +++ b/src/UnableToResolveCaptchaException.php @@ -9,20 +9,12 @@ class UnableToResolveCaptchaException extends RuntimeException { - /** @var CaptchaResolverInterface */ - private $resolver; - - /** @var CaptchaImageInterface */ - private $image; - public function __construct( - CaptchaResolverInterface $resolver, - CaptchaImageInterface $image, - Throwable $previous = null + private readonly CaptchaResolverInterface $resolver, + private readonly CaptchaImageInterface $image, + ?Throwable $previous = null, ) { parent::__construct('Unable to resolve captcha image', 0, $previous); - $this->resolver = $resolver; - $this->image = $image; } public function getResolver(): CaptchaResolverInterface diff --git a/tests/Extending/AssertHasPreviousExceptionTrait.php b/tests/Extending/AssertHasPreviousExceptionTrait.php index 72f469d..fb9fa7c 100644 --- a/tests/Extending/AssertHasPreviousExceptionTrait.php +++ b/tests/Extending/AssertHasPreviousExceptionTrait.php @@ -12,12 +12,12 @@ trait AssertHasPreviousExceptionTrait public static function assertHasPreviousException( Throwable $expectedException, Throwable $exception, - string $message = '' + string $message = '', ): void { Assert::assertThat( $expectedException, static::hasPreviousException($exception), - $message + $message, ); } diff --git a/tests/Extending/HasPreviousException.php b/tests/Extending/HasPreviousException.php index 6e20fd9..3d171e5 100644 --- a/tests/Extending/HasPreviousException.php +++ b/tests/Extending/HasPreviousException.php @@ -9,37 +9,32 @@ class HasPreviousException extends Constraint { - /** @var Throwable */ - private $exception; - - public function __construct(Throwable $exception) + public function __construct(private readonly Throwable $exception) { - $this->exception = $exception; } public function toString(): string { return sprintf( ' is part of previous exception chain of %s &%s', - get_class($this->exception), - spl_object_hash($this->exception) + $this->exception::class, + spl_object_hash($this->exception), ); } - protected function failureDescription($other): string + protected function failureDescription(mixed $other): string { return sprintf( '%s &%s has previous exception %s &%s', - get_class($this->exception), + $this->exception::class, spl_object_hash($this->exception), - (is_object($other)) ? get_class($other) : gettype($other), + get_debug_type($other), (is_object($other)) ? spl_object_hash($other) : '', ); } /** * @param Throwable $other - * @return bool */ protected function matches($other): bool { diff --git a/tests/HttpTestCase.php b/tests/HttpTestCase.php index 4c0e572..0d77e0a 100644 --- a/tests/HttpTestCase.php +++ b/tests/HttpTestCase.php @@ -16,14 +16,11 @@ abstract class HttpTestCase extends TestCase { - /** @var StreamFactoryInterface */ - private $streamFactory; + private StreamFactoryInterface $streamFactory; - /** @var RequestFactoryInterface */ - private $requestFactory; + private RequestFactoryInterface $requestFactory; - /** @var ResponseFactoryInterface */ - private $responseFactory; + private ResponseFactoryInterface $responseFactory; /** @noinspection PhpUnhandledExceptionInspection */ protected function setUp(): void @@ -47,7 +44,6 @@ protected function createHttpClient(ClientInterface $client): HttpClientInterfac /** * @param array $data - * @return ResponseInterface */ protected function createJsonRespose(array $data): ResponseInterface { @@ -56,7 +52,7 @@ protected function createJsonRespose(array $data): ResponseInterface return $responseFactory->createResponse() ->withHeader('Content-Type', 'application/json') ->withBody( - $streamFactory->createStream(json_encode($data) ?: '') + $streamFactory->createStream(json_encode($data) ?: ''), ); } diff --git a/tests/Integration/CaptchaLocalResolver/CaptchaLocalResolverUsageTest.php b/tests/Integration/CaptchaLocalResolver/CaptchaLocalResolverUsageTest.php index ea5c305..a228fd8 100644 --- a/tests/Integration/CaptchaLocalResolver/CaptchaLocalResolverUsageTest.php +++ b/tests/Integration/CaptchaLocalResolver/CaptchaLocalResolverUsageTest.php @@ -11,8 +11,7 @@ final class CaptchaLocalResolverUsageTest extends TestCase { - /** @var string */ - private $localResolverUrl; + private string $localResolverUrl; protected function setUp(): void { @@ -31,7 +30,7 @@ protected function setUp(): void $port = intval(parse_url($localResolverUrl, PHP_URL_PORT) ?: 80); try { $this->checkPortIsOpen($host, $port); - } catch (RuntimeException $exception) { + } catch (RuntimeException) { $this->markTestSkipped("Captcha local resolver service is not open at $host:$port"); } diff --git a/tests/TestCase.php b/tests/TestCase.php index 364f907..96cf0c5 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -18,9 +18,9 @@ public static function fileContents(string $filename): string return file_get_contents(static::filePath($filename)) ?: ''; } - public function checkPortIsOpen(string $hostname, int $port, float $timeoutSeconds = null): void + public function checkPortIsOpen(string $hostname, int $port, ?float $timeoutSeconds = null): void { - $timeoutSeconds = $timeoutSeconds ?? intval(ini_get('default_socket_timeout')); + $timeoutSeconds ??= intval(ini_get('default_socket_timeout')); $socket = fsockopen($hostname, $port, $errorNumber, $errorMessage, $timeoutSeconds); if (false === $socket) { throw new RuntimeException($errorMessage, $errorNumber); diff --git a/tests/Unit/CaptchaAnswerTest.php b/tests/Unit/CaptchaAnswerTest.php index 2b94e54..6947c04 100644 --- a/tests/Unit/CaptchaAnswerTest.php +++ b/tests/Unit/CaptchaAnswerTest.php @@ -23,12 +23,11 @@ public function testObjectProperties(): void $this->assertSame($value, $answer->jsonSerialize()); $this->assertJsonStringEqualsJsonString( json_encode($value) ?: '', - json_encode($answer, JSON_THROW_ON_ERROR) + json_encode($answer, JSON_THROW_ON_ERROR), ); } /** - * @param string $value * @testWith [""] * ["\n"] */ @@ -49,7 +48,6 @@ public function testEqualsTo(): void $this->assertTrue($answer->equalsTo($answer)); $this->assertTrue($answer->equalsTo(new CaptchaAnswer($value))); - $this->assertFalse($answer->equalsTo($other)); $this->assertFalse($answer->equalsTo($other)); $this->assertFalse($answer->equalsTo(new CaptchaAnswer($other))); diff --git a/tests/Unit/CaptchaImageTest.php b/tests/Unit/CaptchaImageTest.php index 2e0d771..bec8f3c 100644 --- a/tests/Unit/CaptchaImageTest.php +++ b/tests/Unit/CaptchaImageTest.php @@ -7,6 +7,7 @@ use InvalidArgumentException; use PhpCfdi\ImageCaptchaResolver\CaptchaImage; use PhpCfdi\ImageCaptchaResolver\Tests\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; final class CaptchaImageTest extends TestCase { @@ -29,7 +30,7 @@ public function testObjectProperties(): void $this->assertSame($contentBase64, $image->jsonSerialize()); $this->assertJsonStringEqualsJsonString( json_encode($contentBase64) ?: '', - json_encode($image, JSON_THROW_ON_ERROR) + json_encode($image, JSON_THROW_ON_ERROR), ); } @@ -57,7 +58,7 @@ public function testNewFromFile(): void } /** @return array */ - public function providerNewFromBase64Malformed(): array + public static function providerNewFromBase64Malformed(): array { return [ 'empty' => ['', 'The captcha image is empty'], @@ -66,7 +67,7 @@ public function providerNewFromBase64Malformed(): array ]; } - /** @dataProvider providerNewFromBase64Malformed */ + #[DataProvider('providerNewFromBase64Malformed')] public function testNewFromBase64Malformed(string $contents, string $expectedMessage): void { $this->expectException(InvalidArgumentException::class); @@ -75,9 +76,9 @@ public function testNewFromBase64Malformed(string $contents, string $expectedMes } /** @return array */ - public function providerNewFromInlineImageMalformed(): array + public static function providerNewFromInlineImageMalformed(): array { - $image = CaptchaImage::newFromFile($this->filePath('red-pixel.gif')); + $image = CaptchaImage::newFromFile(self::filePath('red-pixel.gif')); return [ 'empty' => [''], 'no data part' => ["{$image->getMimeType()};base64,{$image->asBase64()}"], @@ -88,7 +89,7 @@ public function providerNewFromInlineImageMalformed(): array ]; } - /** @dataProvider providerNewFromInlineImageMalformed */ + #[DataProvider('providerNewFromInlineImageMalformed')] public function testNewFromInlineImageMalformed(string $inlineImage): void { $this->expectException(InvalidArgumentException::class); diff --git a/tests/Unit/ConsoleResolverWithInput.php b/tests/Unit/ConsoleResolverWithInput.php index 7491033..da58a68 100644 --- a/tests/Unit/ConsoleResolverWithInput.php +++ b/tests/Unit/ConsoleResolverWithInput.php @@ -14,7 +14,6 @@ final class ConsoleResolverWithInput extends ConsoleResolver private $stdin; /** - * @param string $input * @return $this * @throws RuntimeException */ diff --git a/tests/Unit/FakeExpiredTimer.php b/tests/Unit/FakeExpiredTimer.php index d7dac54..9a6e239 100644 --- a/tests/Unit/FakeExpiredTimer.php +++ b/tests/Unit/FakeExpiredTimer.php @@ -8,16 +8,10 @@ class FakeExpiredTimer implements TimerInterface { - /** @var int */ - public $expireAfter; + private int|float $waitCount = 0; - /** @var int */ - private $waitCount; - - public function __construct(int $expireAfter = 0) + public function __construct(public int $expireAfter = 0) { - $this->expireAfter = $expireAfter; - $this->waitCount = 0; } public function getTimeoutSeconds(): int diff --git a/tests/Unit/Resolvers/AntiCaptchaResolver/AntiCaptchaConnectorTest.php b/tests/Unit/Resolvers/AntiCaptchaResolver/AntiCaptchaConnectorTest.php index 89907a2..eecf32b 100644 --- a/tests/Unit/Resolvers/AntiCaptchaResolver/AntiCaptchaConnectorTest.php +++ b/tests/Unit/Resolvers/AntiCaptchaResolver/AntiCaptchaConnectorTest.php @@ -11,6 +11,7 @@ use PhpCfdi\ImageCaptchaResolver\Resolvers\AntiCaptchaResolver\AntiCaptchaConnector; use PhpCfdi\ImageCaptchaResolver\Tests\Extending\AssertHasPreviousExceptionTrait; use PhpCfdi\ImageCaptchaResolver\Tests\HttpTestCase; +use Psr\Http\Message\RequestInterface; use RuntimeException; use stdClass; @@ -18,10 +19,9 @@ final class AntiCaptchaConnectorTest extends HttpTestCase { use AssertHasPreviousExceptionTrait; - /** @var string */ - private $clientKey = 'client-key'; + private string $clientKey = 'client-key'; - public function createConnector(HttpClientInterface $client = null): AntiCaptchaConnector + public function createConnector(?HttpClientInterface $client = null): AntiCaptchaConnector { return new AntiCaptchaConnector($this->clientKey, $client); } @@ -55,12 +55,13 @@ public function testCreateTask(): void $taskId = $connector->createTask($image); $this->assertSame($expectedTaskId, $taskId); + /** @var RequestInterface $lastRequest */ $lastRequest = $phpHttpMockClient->getLastRequest(); - /** @var stdClass $sentValues */ + /** @phpstan-var stdClass&object{clientKey: string, task: stdClass&object{type: string, body: string}} $sentValues */ $sentValues = json_decode((string) $lastRequest->getBody()); - $this->assertSame($this->clientKey, $sentValues->clientKey ?? ''); - $this->assertSame('ImageToTextTask', $sentValues->task->type ?? ''); - $this->assertSame($image->asBase64(), $sentValues->task->body ?? ''); + $this->assertSame($this->clientKey, $sentValues->clientKey); + $this->assertSame('ImageToTextTask', $sentValues->task->type); + $this->assertSame($image->asBase64(), $sentValues->task->body); } public function testGetTaskResultProcessing(): void @@ -82,7 +83,7 @@ public function testGetTaskResultReady(): void $phpHttpMockClient = $this->createPhpHttpMockClient(); $phpHttpMockClient->addResponse( - $this->createJsonRespose(['status' => 'Ready', 'solution' => ['text' => $expectedResult]]) + $this->createJsonRespose(['status' => 'Ready', 'solution' => ['text' => $expectedResult]]), ); $connector = $this->createConnector($this->createHttpClient($phpHttpMockClient)); @@ -112,17 +113,18 @@ public function testRequest(): void $connector = $this->createConnector($this->createHttpClient($phpHttpMockClient)); $requestData = ['foo' => 'bar']; $result = $connector->request('fake-method', $requestData); + /** @var RequestInterface $request */ $request = $phpHttpMockClient->getLastRequest(); $this->assertJsonStringEqualsJsonString( json_encode($requestData + ['clientKey' => $this->clientKey]) ?: '', - (string) $request->getBody() + (string) $request->getBody(), ); $this->assertEquals( json_decode(json_encode($responseData) ?: ''), $result, - 'result was expected to be an object with the same values' + 'result was expected to be an object with the same values', ); } @@ -130,7 +132,7 @@ public function testRequestWithErrorInResponse(): void { $phpHttpMockClient = $this->createPhpHttpMockClient(); $phpHttpMockClient->addResponse( - $this->createJsonRespose(['errorId' => '1', 'errorDescription' => 'description']) + $this->createJsonRespose(['errorId' => '1', 'errorDescription' => 'description']), ); $connector = $this->createConnector($this->createHttpClient($phpHttpMockClient)); diff --git a/tests/Unit/Resolvers/CaptchaLocalResolver/CaptchaLocalResolverConnectorTest.php b/tests/Unit/Resolvers/CaptchaLocalResolver/CaptchaLocalResolverConnectorTest.php index 254ef99..26016a0 100644 --- a/tests/Unit/Resolvers/CaptchaLocalResolver/CaptchaLocalResolverConnectorTest.php +++ b/tests/Unit/Resolvers/CaptchaLocalResolver/CaptchaLocalResolverConnectorTest.php @@ -13,14 +13,14 @@ use PhpCfdi\ImageCaptchaResolver\Tests\Extending\AssertHasPreviousExceptionTrait; use PhpCfdi\ImageCaptchaResolver\Tests\HttpTestCase; use PhpCfdi\ImageCaptchaResolver\Tests\Unit\FakeExpiredTimer; +use Psr\Http\Message\RequestInterface; use RuntimeException; final class CaptchaLocalResolverConnectorTest extends HttpTestCase { use AssertHasPreviousExceptionTrait; - /** @var string */ - private $baseUrl = 'http://localhost:9095'; + private string $baseUrl = 'http://localhost:9095'; public function createConnectorWithMockClient(HttpClientInterface $client): CaptchaLocalResolverConnector { @@ -57,11 +57,12 @@ public function testSendImage(): void $code = $connector->sendImage($image); + /** @var RequestInterface $lastRequest */ $lastRequest = $phpHttpMockClient->getLastRequest(); $this->assertSame($serviceCode, $code); $this->assertJsonStringEqualsJsonString( json_encode(['image' => $image->asBase64()]) ?: '', - (string) $lastRequest->getBody() + (string) $lastRequest->getBody(), ); } @@ -105,11 +106,12 @@ public function testCheckCodeWithAnswer(): void $answer = $connector->checkCode($serviceCode); + /** @var RequestInterface $lastRequest */ $lastRequest = $phpHttpMockClient->getLastRequest(); $this->assertSame($serviceAnswer, $answer); $this->assertJsonStringEqualsJsonString( json_encode(['code' => $serviceCode]) ?: '', - (string) $lastRequest->getBody() + (string) $lastRequest->getBody(), ); } @@ -125,11 +127,12 @@ public function testCheckCodeWithoutAnswer(): void $answer = $connector->checkCode($serviceCode); + /** @var RequestInterface $lastRequest */ $lastRequest = $phpHttpMockClient->getLastRequest(); $this->assertSame($serviceAnswer, $answer); $this->assertJsonStringEqualsJsonString( json_encode(['code' => $serviceCode]) ?: '', - (string) $lastRequest->getBody() + (string) $lastRequest->getBody(), ); }