diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d874bfe --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf + +[**.{yml,yml.dist,neon,neon.dist}] +indent_size = 2 + +[**.{php,xml,yml,json,dist}] +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[**.md] +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = true diff --git a/.gitattributes b/.gitattributes index 27d5633..8169a6c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,6 +11,7 @@ /.phive/ export-ignore /build/ export-ignore /tests/ export-ignore +/.editorconfig export-ignore /.dockerignore export-ignore /.gitattributes export-ignore /.gitignore export-ignore @@ -19,7 +20,6 @@ /phpcs.xml.dist export-ignore /phpstan.neon.dist export-ignore /phpunit.xml.dist export-ignore -/psalm.xml.dist export-ignore /sonar-project.properties export-ignore # Do not count these files on github code language diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e52c947..f1056ab 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,16 +22,16 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.4' coverage: none tools: composer-normalize env: fail-fast: true - name: Composer normalize - run: composer-normalize + run: composer-normalize --dry-run phpcs: - name: Code style (phpcs) + name: Coding standards (phpcs) runs-on: "ubuntu-latest" steps: - name: Checkout @@ -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: @@ -48,7 +48,7 @@ jobs: run: phpcs -q --report=checkstyle | cs2pr php-cs-fixer: - name: Code style (php-cs-fixer) + name: Coding standards (php-cs-fixer) runs-on: "ubuntu-latest" steps: - name: Checkout @@ -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,14 +73,14 @@ 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 + tools: composer:v2, phpstan env: fail-fast: true - name: Get composer cache directory id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies uses: actions/cache@v4 with: @@ -90,44 +90,14 @@ jobs: - name: Install project dependencies run: composer upgrade --no-interaction --no-progress --prefer-dist - name: PHPStan - run: phpstan analyse --no-progress - - psalm: - name: Code analysis (psalm) - 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: none - tools: composer:v2, cs2pr, psalm - env: - fail-fast: true - - name: Get composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - 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: Psalm version - run: psalm --version - - name: Psalm - run: psalm --no-progress --output-format=github + run: phpstan analyse --no-progress --verbose tests: name: Tests on PHP ${{ matrix.php-version }} runs-on: "ubuntu-latest" strategy: matrix: - php-version: ['8.3'] + php-version: ['8.3', '8.4'] steps: - name: Checkout uses: actions/checkout@v4 @@ -141,7 +111,7 @@ jobs: fail-fast: true - name: Get composer cache directory id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies uses: actions/cache@v4 with: diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml deleted file mode 100644 index c0a4707..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 --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..50e01b2 --- /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@v6 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@v6 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.gitignore b/.gitignore index 4f230a1..b127cf9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ # do not include this files on git -/tools -/vendor +/tools/ +/vendor/ /composer.lock diff --git a/.phive/phars.xml b/.phive/phars.xml index 64aa96b..6d8c6d5 100644 --- a/.phive/phars.xml +++ b/.phive/phars.xml @@ -1,9 +1,8 @@ - - - - - - + + + + + diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index e6c37fd..a1cee11 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -15,15 +15,16 @@ ->setRules([ '@PSR12' => true, '@PSR12:risky' => true, - '@PHP80Migration:risky' => true, - '@PHP80Migration' => true, + '@PHP8x3Migration' => true, + '@PHP8x2Migration:risky' => true, // symfony + 'array_indentation' => true, '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', 'arguments']], + 'trailing_comma_in_multiline' => ['after_heredoc' => true, 'elements' => ['array_destructuring', 'arrays', 'match', 'arguments', 'parameters']], 'no_blank_lines_after_phpdoc' => true, 'object_operator_without_whitespace' => true, 'binary_operator_spaces' => true, @@ -37,6 +38,7 @@ 'concat_space' => ['spacing' => 'one'], 'linebreak_after_opening_tag' => true, 'fully_qualified_strict_types' => true, + 'global_namespace_import' => ['import_classes' => true], // symfony:risky 'no_alias_functions' => true, 'self_accessor' => true, diff --git a/Dockerfile b/Dockerfile index 85ae4f8..9eeb4ab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,27 @@ -FROM php:8.3-cli +FROM php:8.4-cli-alpine COPY . /opt/generator +COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer -# COPY --from=composer:latest /usr/bin/composer /usr/bin/composer +# install dependencies for php modules +RUN set -e \ + && apk add git libzip libzip-dev \ + && docker-php-ext-install zip \ + && apk del libzip-dev -RUN set -e && \ - apt-get update && \ - apt-get install -y --no-install-recommends git zip unzip libzip-dev && \ - apt-get clean -y && \ - rm -rf /var/lib/apt/lists/* +# set up php +RUN set -e \ + && mv /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini \ + && sed -i 's/^variables_order.*/variables_order=EGPCS/' /usr/local/etc/php/php.ini \ + && php -i \ + && php -m -RUN set -e && \ - php -i && \ - php -m && \ - docker-php-ext-install zip +# build project +RUN set -e \ + && export COMPOSER_ALLOW_SUPERUSER=1 \ + && composer diagnose || true \ + && rm -r -f /opt/generator/composer.lock /opt/generator/vendor \ + && composer update --working-dir=/opt/generator --ansi --no-dev --prefer-dist --optimize-autoloader --no-interaction \ + && rm -rf "$(composer config cache-dir --global)" "$(composer config data-dir --global)" "$(composer config home --global)" -RUN set -e && \ - curl -o /usr/bin/composer https://getcomposer.org/download/latest-stable/composer.phar && \ - chmod +x /usr/bin/composer && \ - /usr/bin/composer diagnose || true - -RUN set -e && \ - cd /opt/generator && \ - export COMPOSER_ALLOW_SUPERUSER=1 && \ - composer install --ansi --no-dev --no-cache --prefer-dist --no-progress --no-interaction - -ENTRYPOINT ["php", "/opt/generator/bin/resources-sat-xml-generator"] +ENTRYPOINT ["/usr/local/bin/php", "/opt/generator/bin/resources-sat-xml-generator"] diff --git a/LICENSE b/LICENSE index d512e45..6099604 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2020 - 2024 PhpCfdi https://www.phpcfdi.com/ +Copyright (c) 2020 - 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 64b0b5b..2f4ca3a 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,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/resources-sat-xml-generator/build.yml?branch=main&logo=github-actions [badge-reliability]: https://sonarcloud.io/api/project_badges/measure?project=phpcfdi_resources-sat-xml-generator&metric=reliability_rating [badge-maintainability]: https://sonarcloud.io/api/project_badges/measure?project=phpcfdi_resources-sat-xml-generator&metric=sqale_rating -[badge-coverage]: https://img.shields.io/sonar/coverage/phpcfdi_resources-sat-xml-generator/main?logo=sonarcloud&server=https%3A%2F%2Fsonarcloud.io -[badge-violations]: https://img.shields.io/sonar/violations/phpcfdi_resources-sat-xml-generator/main?format=long&logo=sonarcloud&server=https%3A%2F%2Fsonarcloud.io +[badge-coverage]: https://img.shields.io/sonar/coverage/phpcfdi_resources-sat-xml-generator/main?logo=sonarqubecloud&server=https%3A%2F%2Fsonarcloud.io +[badge-violations]: https://img.shields.io/sonar/violations/phpcfdi_resources-sat-xml-generator/main?format=long&logo=sonarqubecloud&server=https%3A%2F%2Fsonarcloud.io [badge-downloads]: https://img.shields.io/packagist/dt/phpcfdi/resources-sat-xml-generator?logo=packagist diff --git a/composer.json b/composer.json index 93944c2..3c5277b 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ }, "require-dev": { "fakerphp/faker": "^1.17", - "phpunit/phpunit": "^11.1.3", + "phpunit/phpunit": "^12.4.3", "symfony/finder": "^7.0" }, "autoload": { @@ -39,6 +39,12 @@ "PhpCfdi\\ResourcesSatXmlGenerator\\Tests\\": "tests/" } }, + "config": { + "optimize-autoloader": true, + "preferred-install": { + "*": "dist" + } + }, "scripts": { "dev:build": [ "@dev:fix-style", @@ -55,13 +61,12 @@ "dev:fix-style": [ "@php tools/composer-normalize normalize", "@php tools/php-cs-fixer fix --verbose", - "@php tools/phpcbf --colors -sp" + "@php tools/phpcbf --colors -sp || true" ], "dev:test": [ "@dev:check-style", "@php vendor/bin/phpunit --testdox --stop-on-failure", - "@php tools/phpstan analyse --no-progress", - "@php tools/psalm --no-progress" + "@php tools/phpstan analyse --no-progress --no-interaction" ] }, "scripts-descriptions": { @@ -69,6 +74,6 @@ "dev:check-style": "DEV: search for code style errors using composer-normalize, php-cs-fixer and phpcs", "dev:coverage": "DEV: run phpunit with xdebug and storage coverage in build/coverage/html/", "dev:fix-style": "DEV: fix code style errors using composer-normalize, php-cs-fixer and phpcbf", - "dev:test": "DEV: run dev:check-style, phpunit, phpstan and psalm" + "dev:test": "DEV: run dev:check-style, phpunit and phpstan" } } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 40b663c..e8c822d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,28 @@ Utilizamos [Versionado Semántico 2.0.0](SEMVER.md). Importante: **Cambiar la versión en `Application::__construct`**. +## Versión 3.0.3 2025-11-13 + +- Se actualiza el soporte de PHP 8.4. +- Se corrigen las insignias de SonarQube Cloud. +- Se actualiza el año de la licencia a 2025. +- Se cambia la base de la construcción de la imagen de Docker a `php:8.4-cli-alpine`. + +Actualizaciones de mantenimiento: + +- Se actualiza PHPUnit a la versión 12. +- Se actualiza la configuración de PHPUnit para que muestre todos los problemas. +- Se actualiza la integración con SonarQube Cloud. +- Se elimina la herramienta Psalm de las herramientas de desarrollo. +- En los flujos de trabajo de GitHub: + - Se usa la variable `GITHUB_OUTPUT` en lugar de la directiva obsoleta `::set-output`. + - Se agrega PHP 8.4 a la matriz de pruebas. + - Se ejecutan los trabajos en PHP 8.4. + - Se corrige el trabajo `composer-normalize` para que detecte cuando hay un problema. +- Se agrega el archivo `.editorconfig`. +- Se actualiza el estándar de código. +- Se actualizan las herramientas de desarrollo. + ## Versión 3.0.2 2024-05-15 - Se establece correctamente el número de versión. diff --git a/phpcs.xml.dist b/phpcs.xml.dist index beca74d..063db20 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -1,6 +1,6 @@ - The EngineWorks (PSR-2 based) coding standard. + The EngineWorks (PSR-12 based) coding standard. bin src @@ -21,7 +21,6 @@ - diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a4b266f..1d30278 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -4,21 +4,19 @@ cacheDirectory="build/phpunit" bootstrap="tests/bootstrap.php" colors="true" - displayDetailsOnIncompleteTests="true" - displayDetailsOnSkippedTests="true" - displayDetailsOnTestsThatTriggerDeprecations="true" - displayDetailsOnTestsThatTriggerNotices="true" - displayDetailsOnTestsThatTriggerWarnings="true" - displayDetailsOnTestsThatTriggerErrors="true" - > + displayDetailsOnAllIssues="true" +> + - + tests + src + diff --git a/psalm.xml.dist b/psalm.xml.dist deleted file mode 100644 index c899f1a..0000000 --- a/psalm.xml.dist +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - diff --git a/src/CLI/Application.php b/src/CLI/Application.php index 96d4dc1..a0a94b5 100644 --- a/src/CLI/Application.php +++ b/src/CLI/Application.php @@ -10,7 +10,7 @@ class Application extends SymfonyApplication { public function __construct() { - parent::__construct('resources-sat-xml-generator', '3.0.2'); + parent::__construct('resources-sat-xml-generator', '3.0.3'); $this->add(new FetchSatCommand()); $this->add(new FetchCommand()); } diff --git a/src/Downloader.php b/src/Downloader.php index 101eeee..f98a5e7 100644 --- a/src/Downloader.php +++ b/src/Downloader.php @@ -58,7 +58,7 @@ public function setOverridePairs(string ...$overridePairs): void foreach ($overridePairs as $overridePair) { $overridePair = (string) preg_replace('/\s+/', ' ', $overridePair); $overridePairParts = explode(' ', $overridePair, 2); - if (! isset($overridePairParts[0], $overridePairParts[1])) { + if (! isset($overridePairParts[1])) { continue; } [$source, $override] = $overridePairParts; diff --git a/src/DownloaderException.php b/src/DownloaderException.php index 9e7827e..2eae474 100644 --- a/src/DownloaderException.php +++ b/src/DownloaderException.php @@ -13,7 +13,7 @@ final class DownloaderException extends Exception private readonly string $destination; - public function __construct(string $source, string $destination, Throwable $previous = null) + public function __construct(string $source, string $destination, ?Throwable $previous = null) { parent::__construct( message: "Unable to download $source to $destination", diff --git a/src/NsRegistry/Locations.php b/src/NsRegistry/Locations.php index 9479387..468f387 100644 --- a/src/NsRegistry/Locations.php +++ b/src/NsRegistry/Locations.php @@ -13,12 +13,12 @@ */ final readonly class Locations implements IteratorAggregate { - /** @var string[] */ + /** @var list */ private array $locations; public function __construct(string ...$locations) { - $this->locations = $locations; + $this->locations = array_values($locations); } public function append(string ...$locations): self diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 529db27..e2bfe9f 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -27,8 +27,8 @@ $pid = (int) $output[0]; // Kill the web server when the process ends - /** @var callable(): void $shutdownKillPid */ - $shutdownKillPid = function () use ($pid): void { + /** @var Closure(): void $shutdownKillPid */ + $shutdownKillPid = static function () use ($pid): void { exec('kill ' . $pid); }; register_shutdown_function($shutdownKillPid); @@ -36,6 +36,7 @@ // wait until server is responding do { usleep(10000); // wait 0.01 seconds before each try + /** @phpstan-var list $headers */ $headers = @get_headers('http://localhost:8999/README.md') ?: []; $httpResponse = strval($headers[0] ?? ''); } while (! str_contains($httpResponse, '200 OK'));