diff --git a/.editorconfig b/.editorconfig index 55de6f8..187eb38 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,7 +8,10 @@ indent_style = space indent_size = 4 trim_trailing_whitespace = true -[*.{yml, yaml, sh, conf, neon*}] +[*.yaml] +indent_size = 2 + +[*.yml] indent_size = 2 [*.http] diff --git a/.gitattributes b/.gitattributes index 56f46df..1f2ca4f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,9 +10,6 @@ composer-require-* export-ignore docker-compose.yaml export-ignore Makefile export-ignore phpunit.xml* export-ignore -pest.xml* export-ignore -phpstan.* export-ignore -phpstan-baseline.neon export-ignore psalm.* export-ignore psalm-baseline.xml export-ignore infection.* export-ignore diff --git a/.github/workflows/apply-labels.yml b/.github/workflows/apply-labels.yml index f2b6f59..d40e673 100644 --- a/.github/workflows/apply-labels.yml +++ b/.github/workflows/apply-labels.yml @@ -14,7 +14,7 @@ name: ๐Ÿท๏ธ Add labels jobs: label: - uses: wayofdev/gh-actions/.github/workflows/apply-labels.yml@v3.1.0 + uses: wayofdev/gh-actions/.github/workflows/apply-labels.yml@v3.2.0 with: os: ubuntu-latest secrets: diff --git a/.github/workflows/build-phar-release.yml b/.github/workflows/build-phar-release.yml index 956feff..a75e403 100644 --- a/.github/workflows/build-phar-release.yml +++ b/.github/workflows/build-phar-release.yml @@ -24,16 +24,15 @@ jobs: GPG_KEYS_ENCRYPTED: ".github/phar/keys.asc.gpg" steps: - name: ๐Ÿ“ฆ Check out the codebase - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4 - name: ๐Ÿ› ๏ธ Setup PHP - uses: shivammathur/setup-php@2.30.4 + uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - extensions: none, ctype, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, sockets ini-values: error_reporting=E_ALL coverage: none - tools: phive + tools: composer, box - name: ๐Ÿ› ๏ธ Setup problem matchers run: | @@ -42,35 +41,19 @@ jobs: - name: ๐Ÿค– Validate composer.json and composer.lock run: composer validate --ansi --strict - - name: ๐Ÿ” Get composer cache directory - uses: wayofdev/gh-actions/actions/composer/get-cache-directory@v3.1.0 - - - name: โ™ป๏ธ Restore cached dependencies installed with composer - uses: actions/cache@v4.0.2 - with: - path: ${{ env.COMPOSER_CACHE_DIR }} - key: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }} - restore-keys: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}- - - - name: ๐Ÿ“ฅ Install "${{ matrix.dependencies }}" dependencies with composer - uses: wayofdev/gh-actions/actions/composer/install@v3.1.0 - with: - dependencies: ${{ matrix.dependencies }} - - - name: ๐Ÿ“ฅ Install dependencies with phive - uses: wayofdev/gh-actions/actions/phive/install@v3.1.0 + - name: ๐Ÿ“ฅ Install dependencies with composer + uses: ramsey/composer-install@v3 with: - phive-home: '.phive' - trust-gpg-keys: '0xC00543248C87FB13,0x033E5F8D801A2F8D,0x2DF45277AEF09A2F' + composer-options: "--no-dev" - name: ๐Ÿ” Validate configuration for box-project/box - run: .phive/box validate box.json.dist --ansi + run: box info validate box.json.dist --ansi - - name: ๐Ÿค– Compile dload.phar with box-project/box - run: .phive/box compile --ansi + - name: ๐Ÿ“ฆ Build PHAR + run: box compile - name: ๐Ÿ’ฅ Show info about dload.phar with box-project/box - run: .phive/box info ${{ env.DLOAD_PHAR }} --ansi + run: box info info ${{ env.DLOAD_PHAR }} --ansi - name: ๐Ÿค” Run dload.phar help command run: ${{ env.DLOAD_PHAR }} --help diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index c47afa0..e71eb4c 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -19,7 +19,7 @@ jobs: pull-requests: read steps: - name: ๐Ÿ“ฆ Check out the codebase - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4 - name: ๐Ÿง Lint commits using "commitlint" uses: wagoid/commitlint-github-action@v6.0.1 @@ -37,10 +37,10 @@ jobs: pull-requests: read steps: - name: ๐Ÿ“ฆ Check out the codebase - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4 - name: ๐Ÿง Lint YAML files - uses: ibiqlik/action-yamllint@v3.1.1 + uses: ibiqlik/action-yamllint@v3 with: config_file: .github/.yamllint.yaml file_or_dir: '.' @@ -54,143 +54,16 @@ jobs: group: markdown-linting-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} steps: - name: ๐Ÿ“ฆ Check out the codebase - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4 - name: ๐Ÿง Lint Markdown files uses: DavidAnson/markdownlint-cli2-action@v16.0.0 with: globs: | - **/*.md + *.md !CHANGELOG.md - composer-linting: - timeout-minutes: 4 - runs-on: ${{ matrix.os }} - concurrency: - cancel-in-progress: true - group: composer-linting-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - strategy: - matrix: - os: - - ubuntu-latest - php-version: - - '8.1' - dependencies: - - locked - permissions: - contents: write - steps: - - name: ๐Ÿ› ๏ธ Setup PHP - uses: shivammathur/setup-php@2.30.4 - with: - php-version: ${{ matrix.php-version }} - extensions: none, ctype, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, phar, sockets - ini-values: error_reporting=E_ALL - coverage: none - tools: phive - - - name: ๐Ÿ“ฆ Check out the codebase - uses: actions/checkout@v4.1.6 - - - name: ๐Ÿ› ๏ธ Setup problem matchers - run: | - echo "::add-matcher::${{ runner.tool_cache }}/php.json" - - - name: ๐Ÿค– Validate composer.json and composer.lock - run: composer validate --ansi --strict - - - name: ๐Ÿ” Get composer cache directory - uses: wayofdev/gh-actions/actions/composer/get-cache-directory@v3.1.0 - - - name: โ™ป๏ธ Restore cached dependencies installed with composer - uses: actions/cache@v4.0.2 - with: - path: ${{ env.COMPOSER_CACHE_DIR }} - key: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }} - restore-keys: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}- - - - name: ๐Ÿ“ฅ Install "${{ matrix.dependencies }}" dependencies with composer - uses: wayofdev/gh-actions/actions/composer/install@v3.1.0 - with: - dependencies: ${{ matrix.dependencies }} - - - name: ๐Ÿ“ฅ Install dependencies with phive - uses: wayofdev/gh-actions/actions/phive/install@v3.1.0 - with: - phive-home: '.phive' - trust-gpg-keys: '0xC00543248C87FB13,0x033E5F8D801A2F8D,0x2DF45277AEF09A2F' - - - name: ๐Ÿ” Run ergebnis/composer-normalize - run: .phive/composer-normalize --ansi --dry-run - coding-standards: - timeout-minutes: 4 - runs-on: ${{ matrix.os }} - concurrency: - cancel-in-progress: true - group: coding-standards-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - strategy: - matrix: - os: - - ubuntu-latest - php-version: - - '8.1' - dependencies: - - locked permissions: contents: write - steps: - - name: โš™๏ธ Set git to use LF line endings - run: | - git config --global core.autocrlf false - git config --global core.eol lf - - - name: ๐Ÿ› ๏ธ Setup PHP - uses: shivammathur/setup-php@2.30.4 - with: - php-version: ${{ matrix.php-version }} - extensions: none, ctype, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, sockets - ini-values: error_reporting=E_ALL - coverage: none - - - name: ๐Ÿ“ฆ Check out the codebase - uses: actions/checkout@v4.1.6 - - - name: ๐Ÿ› ๏ธ Setup problem matchers - run: | - echo "::add-matcher::${{ runner.tool_cache }}/php.json" - - - name: ๐Ÿค– Validate composer.json and composer.lock - run: composer validate --ansi --strict - - - name: ๐Ÿ” Get composer cache directory - uses: wayofdev/gh-actions/actions/composer/get-cache-directory@v3.1.0 - - - name: โ™ป๏ธ Restore cached dependencies installed with composer - uses: actions/cache@v4.0.2 - with: - path: ${{ env.COMPOSER_CACHE_DIR }} - key: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }} - restore-keys: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}- - - - name: ๐Ÿ“ฅ Install "${{ matrix.dependencies }}" dependencies with composer - uses: wayofdev/gh-actions/actions/composer/install@v3.1.0 - with: - dependencies: ${{ matrix.dependencies }} - - - name: ๐Ÿ› ๏ธ Prepare environment - run: make prepare - - - name: ๐Ÿšจ Run coding standards task - run: composer cs:fix - env: - PHP_CS_FIXER_IGNORE_ENV: true - - - name: ๐Ÿ“ค Commit and push changed files back to GitHub - uses: stefanzweifel/git-auto-commit-action@v5.0.1 - with: - commit_message: 'style(php-cs-fixer): lint php files and fix coding standards' - branch: ${{ github.head_ref }} - commit_author: 'github-actions ' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: spiral/gh-actions/.github/workflows/cs-fix.yml@master diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 7c92c9d..fe984dc 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -24,13 +24,12 @@ jobs: - locked steps: - name: ๐Ÿ“ฆ Check out the codebase - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4 - name: ๐Ÿ› ๏ธ Setup PHP - uses: shivammathur/setup-php@2.30.4 + uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - extensions: none, ctype, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, sockets ini-values: error_reporting=E_ALL coverage: none @@ -40,20 +39,10 @@ jobs: - name: ๐Ÿค– Validate composer.json and composer.lock run: composer validate --ansi --strict - - name: ๐Ÿ” Get composer cache directory - uses: wayofdev/gh-actions/actions/composer/get-cache-directory@v3.1.0 - - - name: โ™ป๏ธ Restore cached dependencies installed with composer - uses: actions/cache@v4.0.2 - with: - path: ${{ env.COMPOSER_CACHE_DIR }} - key: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }} - restore-keys: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}- - - - name: ๐Ÿ“ฅ Install "${{ matrix.dependencies }}" dependencies - uses: wayofdev/gh-actions/actions/composer/install@v3.1.0 + - name: ๐Ÿ“ฅ Install dependencies with composer + uses: ramsey/composer-install@v3 with: - dependencies: ${{ matrix.dependencies }} + dependency-versions: ${{ matrix.dependencies }} - name: ๐Ÿ› Check installed packages for security vulnerability advisories run: composer audit --ansi diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 4c9dcfb..a064654 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -7,7 +7,6 @@ on: # yamllint disable-line rule:truthy - 'bin/dload' - '.php-cs-fixer.dist.php' - 'psalm*' - - 'phpstan*' - 'composer.*' push: paths: @@ -15,7 +14,6 @@ on: # yamllint disable-line rule:truthy - 'bin/dload' - '.php-cs-fixer.dist.php' - 'psalm*' - - 'phpstan*' - 'composer.*' name: ๐Ÿ” Static analysis @@ -38,13 +36,12 @@ jobs: - locked steps: - name: ๐Ÿ“ฆ Check out the codebase - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4 - name: ๐Ÿ› ๏ธ Setup PHP - uses: shivammathur/setup-php@2.30.4 + uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - extensions: none, ctype, curl, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, sockets, opcache, pcntl, posix ini-values: error_reporting=E_ALL coverage: none @@ -54,20 +51,10 @@ jobs: - name: ๐Ÿค– Validate composer.json and composer.lock run: composer validate --ansi --strict - - name: ๐Ÿ” Get composer cache directory - uses: wayofdev/gh-actions/actions/composer/get-cache-directory@v3.1.0 - - - name: โ™ป๏ธ Restore cached dependencies installed with composer - uses: actions/cache@v4.0.2 - with: - path: ${{ env.COMPOSER_CACHE_DIR }} - key: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }} - restore-keys: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}- - - - name: ๐Ÿ“ฅ Install "${{ matrix.dependencies }}" dependencies - uses: wayofdev/gh-actions/actions/composer/install@v3.1.0 + - name: ๐Ÿ“ฅ Install dependencies with composer + uses: ramsey/composer-install@v3 with: - dependencies: ${{ matrix.dependencies }} + dependency-versions: ${{ matrix.dependencies }} - name: ๐Ÿ” Run static analysis using vimeo/psalm run: composer psalm:ci diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 1d42212..09ef77f 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -28,13 +28,12 @@ jobs: - locked steps: - name: ๐Ÿ“ฆ Check out the codebase - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4 - name: ๐Ÿ› ๏ธ Setup PHP - uses: shivammathur/setup-php@2.30.4 + uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - extensions: none, ctype, curl, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, sockets, opcache, pcntl, posix ini-values: error_reporting=E_ALL coverage: xdebug @@ -46,28 +45,18 @@ jobs: - name: ๐Ÿค– Validate composer.json and composer.lock run: composer validate --ansi --strict - - name: ๐Ÿ” Get composer cache directory - uses: wayofdev/gh-actions/actions/composer/get-cache-directory@v3.1.0 - - - name: โ™ป๏ธ Restore cached dependencies installed with composer - uses: actions/cache@v4.0.2 - with: - path: ${{ env.COMPOSER_CACHE_DIR }} - key: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }} - restore-keys: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}- - - - name: ๐Ÿ“ฅ Install "${{ matrix.dependencies }}" dependencies - uses: wayofdev/gh-actions/actions/composer/install@v3.1.0 + - name: ๐Ÿ“ฅ Install dependencies with composer + uses: ramsey/composer-install@v3 with: - dependencies: ${{ matrix.dependencies }} + dependency-versions: ${{ matrix.dependencies }} - - name: ๐Ÿงช Collect code coverage with Xdebug and pestphp/pest + - name: ๐Ÿงช Collect code coverage with Xdebug and PhpUnit run: composer test:cc - name: ๐Ÿ“ค Upload code coverage report to Codecov - uses: codecov/codecov-action@v4.3.0 + uses: codecov/codecov-action@v4 with: - files: .build/phpunit/logs/clover.xml + files: runtime/phpunit/logs/clover.xml token: ${{ secrets.CODECOV_TOKEN }} verbose: true @@ -92,13 +81,12 @@ jobs: - highest steps: - name: ๐Ÿ“ฆ Check out the codebase - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4 - name: ๐Ÿ› ๏ธ Setup PHP - uses: shivammathur/setup-php@2.30.4 + uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - extensions: none, ctype, curl, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, sockets, opcache, pcntl, posix ini-values: error_reporting=E_ALL coverage: xdebug @@ -110,31 +98,14 @@ jobs: - name: ๐Ÿค– Validate composer.json and composer.lock run: composer validate --ansi --strict - - name: ๐Ÿ” Get composer cache directory - uses: wayofdev/gh-actions/actions/composer/get-cache-directory@v3.1.0 - - - name: โ™ป๏ธ Restore cached dependencies installed with composer - uses: actions/cache@v4.0.2 - with: - path: ${{ env.COMPOSER_CACHE_DIR }} - key: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }} - restore-keys: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}- - - - name: โš™๏ธ Remove platform configuration with composer - if: matrix.dependencies != 'locked' - run: composer config platform.php --ansi --unset - - - name: ๐Ÿ“ฅ Install "${{ matrix.dependencies }}" dependencies - uses: wayofdev/gh-actions/actions/composer/install@v3.1.0 + - name: ๐Ÿ“ฅ Install dependencies with composer + uses: ramsey/composer-install@v3 with: - dependencies: ${{ matrix.dependencies }} + dependency-versions: ${{ matrix.dependencies }} - - name: ๐Ÿงช Run unit tests using phpunit/phpunit + - name: ๐Ÿงช Run tests run: composer test - - name: ๐Ÿงช Run arch tests using pestphp/pest - run: composer test:arch - compile-phar: timeout-minutes: 4 runs-on: ${{ matrix.os }} @@ -155,52 +126,36 @@ jobs: DLOAD_PHAR_SIGNATURE: ".build/phar/dload.phar.asc" steps: - name: ๐Ÿ“ฆ Check out the codebase - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: ๐Ÿ› ๏ธ Setup PHP - uses: shivammathur/setup-php@2.30.4 + uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - extensions: none, ctype, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, sockets ini-values: 'error_reporting=E_ALL, memory_limit=-1, phar.readonly=0' + tools: composer, box coverage: none - tools: phive - name: ๐Ÿ› ๏ธ Setup problem matchers run: | echo "::add-matcher::${{ runner.tool_cache }}/php.json" - - name: ๐Ÿ” Get composer cache directory - uses: wayofdev/gh-actions/actions/composer/get-cache-directory@v3.1.0 - - - name: โ™ป๏ธ Restore cached dependencies installed with composer - uses: actions/cache@v4.0.2 - with: - path: ${{ env.COMPOSER_CACHE_DIR }} - key: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }} - restore-keys: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}- - - - name: ๐Ÿ“ฅ Install "${{ matrix.dependencies }}" dependencies with composer - uses: wayofdev/gh-actions/actions/composer/install@v3.1.0 - with: - dependencies: ${{ matrix.dependencies }} - - - name: ๐Ÿ“ฅ Install dependencies with phive - uses: wayofdev/gh-actions/actions/phive/install@v3.1.0 + - name: ๐Ÿ“ฅ Install dependencies with composer + uses: ramsey/composer-install@v3 with: - phive-home: '.phive' - trust-gpg-keys: '0xC00543248C87FB13,0x033E5F8D801A2F8D,0x2DF45277AEF09A2F' + composer-options: "--no-dev" + dependency-versions: ${{ matrix.dependencies }} - name: ๐Ÿ” Validate configuration for box-project/box - run: .phive/box validate box.json.dist --ansi + run: box validate box.json.dist --ansi - - name: ๐Ÿค– Compile dload.phar with box-project/box - run: .phive/box compile --ansi + - name: ๐Ÿ“ฆ Build PHAR + run: box compile - name: ๐Ÿ’ฅ Show info about dload.phar with box-project/box - run: .phive/box info ${{ env.DLOAD_PHAR }} --ansi + run: box info ${{ env.DLOAD_PHAR }} --ansi - name: ๐Ÿค” Run dload.phar help command run: ${{ env.DLOAD_PHAR }} --help diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 56f7620..5200938 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -2,21 +2,11 @@ declare(strict_types=1); -use WayOfDev\PhpCsFixer\Config\ConfigBuilder; -use WayOfDev\PhpCsFixer\Config\RuleSets\ExtendedPERSet; - require_once 'vendor/autoload.php'; -$config = ConfigBuilder::createFromRuleSet(new ExtendedPERSet()) - ->inDir(__DIR__ . '/bin') - ->inDir(__DIR__ . '/src') - ->inDir(__DIR__ . '/tests') - ->exclude([ - __DIR__ . '/src/Test/Proto', - ]) - ->addFiles([__FILE__]) - ->getConfig(); - -$config->setCacheFile(__DIR__ . '/.build/php-cs-fixer/php-cs-fixer.cache'); - -return $config; +return \Spiral\CodeStyle\Builder::create() + ->include(__DIR__ . '/bin') + ->include(__DIR__ . '/src') + ->include(__DIR__ . '/tests') + ->include(__FILE__) + ->build(); diff --git a/Makefile b/Makefile index c873bfa..12ba0ca 100644 --- a/Makefile +++ b/Makefile @@ -264,11 +264,11 @@ infect-ci: ## Runs infection โ€“ mutation testing framework with github output ( $(APP_COMPOSER) infect:ci .PHONY: lint-infect-ci -test: ## Run project php-unit and pest tests +test: ## Run project tests $(APP_COMPOSER) test .PHONY: test -test-cc: ## Run project php-unit and pest tests in coverage mode and build report +test-cc: ## Run project tests in coverage mode and build report $(APP_COMPOSER) test:cc .PHONY: test-cc diff --git a/bin/dload b/bin/dload index 8378262..f6c9b5c 100644 --- a/bin/dload +++ b/bin/dload @@ -8,10 +8,6 @@ use Internal\DLoad\Info; use Symfony\Component\Console\Application; use Symfony\Component\Console\CommandLoader\FactoryCommandLoader; -if ('cli' !== PHP_SAPI) { - throw new Exception('This script must be run from the command line.'); -} - (static function () { $cwd = \getcwd(); diff --git a/box.json.dist b/box.json.dist index 05c31ce..583c3b8 100644 --- a/box.json.dist +++ b/box.json.dist @@ -4,6 +4,8 @@ "KevinGH\\Box\\Compactor\\Json", "KevinGH\\Box\\Compactor\\Php" ], + "check-requirements": false, + "dump-autoload": false, "compression": "GZ", "git": "git", "directories": [ diff --git a/composer.json b/composer.json index 9249bdf..5715965 100644 --- a/composer.json +++ b/composer.json @@ -31,12 +31,10 @@ "require-dev": { "buggregator/trap": "^1.10", "dereuromark/composer-prefer-lowest": "^0.1.10", - "ergebnis/phpunit-slow-test-detector": "^2.14", - "friendsofphp/php-cs-fixer": "^3.54", - "pestphp/pest": "^2.34", "phpunit/phpunit": "^10.5", - "vimeo/psalm": "^5.11", - "wayofdev/cs-fixer-config": "^1.4" + "spiral/code-style": "^2.2.2", + "ta-tikoma/phpunit-architecture-test": "^0.8.4", + "vimeo/psalm": "^6.10" }, "suggest": { "ext-simplexml": "to support XML configs parsing" @@ -59,15 +57,11 @@ "config": { "allow-plugins": { "ergebnis/composer-normalize": true, - "infection/extension-installer": true, - "pestphp/pest-plugin": true + "infection/extension-installer": true }, "audit": { "abandoned": "report" }, - "platform": { - "php": "8.1.27" - }, "sort-packages": true }, "scripts": { @@ -90,10 +84,17 @@ "@putenv XDEBUG_MODE=coverage", "phpunit --color=always" ], - "test:arch": "pest --color=always --configuration pest.xml.dist", + "test:unit": [ + "@putenv XDEBUG_MODE=coverage", + "phpunit --color=always --testsuite=Unit" + ], + "test:arch": [ + "@putenv XDEBUG_MODE=coverage", + "phpunit --color=always --testsuite=Arch" + ], "test:cc": [ "@putenv XDEBUG_MODE=coverage", - "phpunit --coverage-clover=.build/phpunit/logs/clover.xml --color=always" + "phpunit --coverage-clover=runtime/phpunit/logs/clover.xml --color=always" ] } } diff --git a/composer.lock b/composer.lock index a726289..64ad0a8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8bf4dbc45a6c90ea713d6adb7f38ae5d", + "content-hash": "9993c6c2bb8ef3d475d105e3d098b2d1", "packages": [ { "name": "psr/container", @@ -61,16 +61,16 @@ }, { "name": "psr/log", - "version": "3.0.0", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { @@ -105,9 +105,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.0" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2021-07-14T16:46:02+00:00" + "time": "2024-09-11T13:17:53+00:00" }, { "name": "react/async", @@ -331,16 +331,16 @@ }, { "name": "symfony/console", - "version": "v6.4.10", + "version": "v6.4.20", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "504974cbe43d05f83b201d6498c206f16fc0cdbc" + "reference": "2e4af9c952617cc3f9559ff706aee420a8464c36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/504974cbe43d05f83b201d6498c206f16fc0cdbc", - "reference": "504974cbe43d05f83b201d6498c206f16fc0cdbc", + "url": "https://api.github.com/repos/symfony/console/zipball/2e4af9c952617cc3f9559ff706aee420a8464c36", + "reference": "2e4af9c952617cc3f9559ff706aee420a8464c36", "shasum": "" }, "require": { @@ -405,7 +405,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.10" + "source": "https://github.com/symfony/console/tree/v6.4.20" }, "funding": [ { @@ -421,20 +421,20 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:30:32+00:00" + "time": "2025-03-03T17:16:38+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", "shasum": "" }, "require": { @@ -442,12 +442,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -472,7 +472,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" }, "funding": [ { @@ -488,27 +488,27 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/http-client", - "version": "v6.4.10", + "version": "v6.4.19", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "b5e498f763e0bf5eed8dcd946e50a3b3f71d4ded" + "reference": "3294a433fc9d12ae58128174896b5b1822c28dad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/b5e498f763e0bf5eed8dcd946e50a3b3f71d4ded", - "reference": "b5e498f763e0bf5eed8dcd946e50a3b3f71d4ded", + "url": "https://api.github.com/repos/symfony/http-client/zipball/3294a433fc9d12ae58128174896b5b1822c28dad", + "reference": "3294a433fc9d12ae58128174896b5b1822c28dad", "shasum": "" }, "require": { "php": ">=8.1", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "^3.4.1", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -565,7 +565,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.10" + "source": "https://github.com/symfony/http-client/tree/v6.4.19" }, "funding": [ { @@ -581,20 +581,20 @@ "type": "tidelift" } ], - "time": "2024-07-15T09:26:24+00:00" + "time": "2025-02-13T09:55:13+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.5.0", + "version": "v3.5.2", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "20414d96f391677bf80078aa55baece78b82647d" + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", - "reference": "20414d96f391677bf80078aa55baece78b82647d", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", "shasum": "" }, "require": { @@ -602,12 +602,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -643,7 +643,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2" }, "funding": [ { @@ -659,24 +659,24 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-12-07T08:49:48+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "0424dff1c58f028c451efff2045f5d92410bd540" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", - "reference": "0424dff1c58f028c451efff2045f5d92410bd540", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-ctype": "*" @@ -687,8 +687,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -722,7 +722,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" }, "funding": [ { @@ -738,24 +738,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", - "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -763,8 +763,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -800,7 +800,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" }, "funding": [ { @@ -816,24 +816,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -841,8 +841,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -881,7 +881,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" }, "funding": [ { @@ -897,24 +897,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-mbstring": "*" @@ -925,8 +925,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -961,7 +961,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" }, "funding": [ { @@ -977,20 +977,20 @@ "type": "tidelift" } ], - "time": "2024-06-19T12:30:46+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.5.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", "shasum": "" }, "require": { @@ -1003,12 +1003,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -1044,7 +1044,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" }, "funding": [ { @@ -1060,20 +1060,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/string", - "version": "v6.4.10", + "version": "v6.4.15", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ccf9b30251719567bfd46494138327522b9a9446" + "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ccf9b30251719567bfd46494138327522b9a9446", - "reference": "ccf9b30251719567bfd46494138327522b9a9446", + "url": "https://api.github.com/repos/symfony/string/zipball/73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", + "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", "shasum": "" }, "require": { @@ -1130,7 +1130,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.10" + "source": "https://github.com/symfony/string/tree/v6.4.15" }, "funding": [ { @@ -1146,7 +1146,7 @@ "type": "tidelift" } ], - "time": "2024-07-22T10:21:14+00:00" + "time": "2024-11-13T13:31:12+00:00" }, { "name": "yiisoft/injector", @@ -1222,43 +1222,36 @@ "packages-dev": [ { "name": "amphp/amp", - "version": "v2.6.4", + "version": "v3.1.0", "source": { "type": "git", "url": "https://github.com/amphp/amp.git", - "reference": "ded3d9be08f526089eb7ee8d9f16a9768f9dec2d" + "reference": "7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/ded3d9be08f526089eb7ee8d9f16a9768f9dec2d", - "reference": "ded3d9be08f526089eb7ee8d9f16a9768f9dec2d", + "url": "https://api.github.com/repos/amphp/amp/zipball/7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9", + "reference": "7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "amphp/phpunit-util": "^1", - "ext-json": "*", - "jetbrains/phpstorm-stubs": "^2019.3", - "phpunit/phpunit": "^7 | ^8 | ^9", - "react/promise": "^2", - "vimeo/psalm": "^3.12" + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23.1" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, "autoload": { "files": [ - "lib/functions.php", - "lib/Internal/functions.php" + "src/functions.php", + "src/Future/functions.php", + "src/Internal/functions.php" ], "psr-4": { - "Amp\\": "lib" + "Amp\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1266,10 +1259,6 @@ "MIT" ], "authors": [ - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - }, { "name": "Aaron Piotrowski", "email": "aaron@trowski.com" @@ -1281,6 +1270,10 @@ { "name": "Niklas Keller", "email": "me@kelunik.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" } ], "description": "A non-blocking concurrency framework for PHP applications.", @@ -1297,9 +1290,8 @@ "promise" ], "support": { - "irc": "irc://irc.freenode.org/amphp", "issues": "https://github.com/amphp/amp/issues", - "source": "https://github.com/amphp/amp/tree/v2.6.4" + "source": "https://github.com/amphp/amp/tree/v3.1.0" }, "funding": [ { @@ -1307,41 +1299,45 @@ "type": "github" } ], - "time": "2024-03-21T18:52:26+00:00" + "time": "2025-01-26T16:07:39+00:00" }, { "name": "amphp/byte-stream", - "version": "v1.8.2", + "version": "v2.1.2", "source": { "type": "git", "url": "https://github.com/amphp/byte-stream.git", - "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc" + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/byte-stream/zipball/4f0e968ba3798a423730f567b1b50d3441c16ddc", - "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46", "shasum": "" }, "require": { - "amphp/amp": "^2", - "php": ">=7.1" + "amphp/amp": "^3", + "amphp/parser": "^1.1", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2.3" }, "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "amphp/phpunit-util": "^1.4", - "friendsofphp/php-cs-fixer": "^2.3", - "jetbrains/phpstorm-stubs": "^2019.3", - "phpunit/phpunit": "^6 || ^7 || ^8", - "psalm/phar": "^3.11.4" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.22.1" }, "type": "library", "autoload": { "files": [ - "lib/functions.php" + "src/functions.php", + "src/Internal/functions.php" ], "psr-4": { - "Amp\\ByteStream\\": "lib" + "Amp\\ByteStream\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1370,7 +1366,7 @@ ], "support": { "issues": "https://github.com/amphp/byte-stream/issues", - "source": "https://github.com/amphp/byte-stream/tree/v1.8.2" + "source": "https://github.com/amphp/byte-stream/tree/v2.1.2" }, "funding": [ { @@ -1378,61 +1374,39 @@ "type": "github" } ], - "time": "2024-04-13T18:00:56+00:00" + "time": "2025-03-16T17:10:27+00:00" }, { - "name": "brianium/paratest", - "version": "v7.3.1", + "name": "amphp/cache", + "version": "v2.0.1", "source": { "type": "git", - "url": "https://github.com/paratestphp/paratest.git", - "reference": "551f46f52a93177d873f3be08a1649ae886b4a30" + "url": "https://github.com/amphp/cache.git", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/551f46f52a93177d873f3be08a1649ae886b4a30", - "reference": "551f46f52a93177d873f3be08a1649ae886b4a30", + "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-simplexml": "*", - "fidry/cpu-core-counter": "^0.5.1 || ^1.0.0", - "jean85/pretty-package-versions": "^2.0.5", - "php": "~8.1.0 || ~8.2.0 || ~8.3.0", - "phpunit/php-code-coverage": "^10.1.7", - "phpunit/php-file-iterator": "^4.1.0", - "phpunit/php-timer": "^6.0", - "phpunit/phpunit": "^10.4.2", - "sebastian/environment": "^6.0.1", - "symfony/console": "^6.3.4 || ^7.0.0", - "symfony/process": "^6.3.4 || ^7.0.0" + "amphp/amp": "^3", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "doctrine/coding-standard": "^12.0.0", - "ext-pcov": "*", - "ext-posix": "*", - "infection/infection": "^0.27.6", - "phpstan/phpstan": "^1.10.40", - "phpstan/phpstan-deprecation-rules": "^1.1.4", - "phpstan/phpstan-phpunit": "^1.3.15", - "phpstan/phpstan-strict-rules": "^1.5.2", - "squizlabs/php_codesniffer": "^3.7.2", - "symfony/filesystem": "^6.3.1 || ^7.0.0" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" }, - "bin": [ - "bin/paratest", - "bin/paratest.bat", - "bin/paratest_for_phpstorm" - ], "type": "library", "autoload": { "psr-4": { - "ParaTest\\": [ - "src/" - ] + "Amp\\Cache\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1441,175 +1415,164 @@ ], "authors": [ { - "name": "Brian Scaturro", - "email": "scaturrob@gmail.com", - "role": "Developer" + "name": "Niklas Keller", + "email": "me@kelunik.com" }, { - "name": "Filippo Tessarotto", - "email": "zoeslam@gmail.com", - "role": "Developer" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" } ], - "description": "Parallel testing for PHP", - "homepage": "https://github.com/paratestphp/paratest", - "keywords": [ - "concurrent", - "parallel", - "phpunit", - "testing" - ], + "description": "A fiber-aware cache API based on Amp and Revolt.", + "homepage": "https://amphp.org/cache", "support": { - "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.3.1" + "issues": "https://github.com/amphp/cache/issues", + "source": "https://github.com/amphp/cache/tree/v2.0.1" }, "funding": [ { - "url": "https://github.com/sponsors/Slamdunk", + "url": "https://github.com/amphp", "type": "github" - }, - { - "url": "https://paypal.me/filippotessarotto", - "type": "paypal" } ], - "time": "2023-10-31T09:24:17+00:00" + "time": "2024-04-19T03:38:06+00:00" }, { - "name": "buggregator/trap", - "version": "1.10.1", + "name": "amphp/dns", + "version": "v2.4.0", "source": { "type": "git", - "url": "https://github.com/buggregator/trap.git", - "reference": "156ac1e0386a40454e71f1282f3b10bed6e34a12" + "url": "https://github.com/amphp/dns.git", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/buggregator/trap/zipball/156ac1e0386a40454e71f1282f3b10bed6e34a12", - "reference": "156ac1e0386a40454e71f1282f3b10bed6e34a12", + "url": "https://api.github.com/repos/amphp/dns/zipball/78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", "shasum": "" }, "require": { - "clue/stream-filter": "^1.6", + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/process": "^2", + "daverandom/libdns": "^2.0.2", "ext-filter": "*", - "ext-sockets": "*", - "nunomaduro/termwind": "^1.15 || ^2", - "nyholm/psr7": "^1.8", + "ext-json": "*", "php": ">=8.1", - "php-http/message": "^1.15", - "psr/container": "^1.1 || ^2.0", - "psr/http-message": "^1.1 || ^2", - "symfony/console": "^6.4 || ^7", - "symfony/var-dumper": "^6.3 || ^7", - "yiisoft/injector": "^1.2" + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "dereuromark/composer-prefer-lowest": "^0.1.10", - "ergebnis/phpunit-slow-test-detector": "^2.14", - "friendsofphp/php-cs-fixer": "^3.54", - "google/protobuf": "^3.23", - "pestphp/pest": "^2.34", - "phpstan/extension-installer": "^1.3", - "phpstan/phpstan": "^1.10", - "phpstan/phpstan-deprecation-rules": "^1.1", - "phpstan/phpstan-phpunit": "^1.3", - "phpstan/phpstan-strict-rules": "^1.5", - "phpunit/phpunit": "^10.5", - "roxblnfk/unpoly": "^1.8.1", - "vimeo/psalm": "^5.11", - "wayofdev/cs-fixer-config": "^1.4" - }, - "suggest": { - "ext-simplexml": "To load trap.xml", - "roxblnfk/unpoly": "If you want to remove unnecessary PHP polyfills depend on PHP version." + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" }, - "bin": [ - "bin/trap" - ], "type": "library", "autoload": { "files": [ "src/functions.php" ], "psr-4": { - "Buggregator\\Trap\\": "src/" + "Amp\\Dns\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Aleksei Gagarin (roxblnfk)", - "homepage": "https://github.com/roxblnfk" + "name": "Chris Wright", + "email": "addr@daverandom.com" }, { - "name": "Pavel Buchnev (butschster)", - "homepage": "https://github.com/butschster" + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" } ], - "description": "A simple and powerful tool for debugging PHP applications.", - "homepage": "https://buggregator.dev/", + "description": "Async DNS resolution for Amp.", + "homepage": "https://github.com/amphp/dns", "keywords": [ - "WebSockets", - "binary dump", - "cli", - "console", - "debug", - "dev", - "dump", - "helper", - "sentry", - "server", - "smtp" + "amp", + "amphp", + "async", + "client", + "dns", + "resolve" ], "support": { - "issues": "https://github.com/buggregator/trap/issues", - "source": "https://github.com/buggregator/trap/tree/1.10.1" + "issues": "https://github.com/amphp/dns/issues", + "source": "https://github.com/amphp/dns/tree/v2.4.0" }, "funding": [ { - "url": "https://github.com/sponsors/buggregator", + "url": "https://github.com/amphp", "type": "github" - }, - { - "url": "https://patreon.com/butschster", - "type": "patreon" - }, - { - "url": "https://patreon.com/roxblnfk", - "type": "patreon" } ], - "time": "2024-06-23T14:24:42+00:00" + "time": "2025-01-19T15:43:40+00:00" }, { - "name": "clue/ndjson-react", - "version": "v1.3.0", + "name": "amphp/parallel", + "version": "v2.3.1", "source": { "type": "git", - "url": "https://github.com/clue/reactphp-ndjson.git", - "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" + "url": "https://github.com/amphp/parallel.git", + "reference": "5113111de02796a782f5d90767455e7391cca190" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", - "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "url": "https://api.github.com/repos/amphp/parallel/zipball/5113111de02796a782f5d90767455e7391cca190", + "reference": "5113111de02796a782f5d90767455e7391cca190", "shasum": "" }, "require": { - "php": ">=5.3", - "react/stream": "^1.2" + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/pipeline": "^1", + "amphp/process": "^2", + "amphp/serialization": "^1", + "amphp/socket": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1" }, "require-dev": { - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", - "react/event-loop": "^1.2" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" }, "type": "library", "autoload": { + "files": [ + "src/Context/functions.php", + "src/Context/Internal/functions.php", + "src/Ipc/functions.php", + "src/Worker/functions.php" + ], "psr-4": { - "Clue\\React\\NDJson\\": "src/" + "Amp\\Parallel\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1618,63 +1581,65 @@ ], "authors": [ { - "name": "Christian Lรผck", - "email": "christian@clue.engineering" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" } ], - "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", - "homepage": "https://github.com/clue/reactphp-ndjson", + "description": "Parallel processing component for Amp.", + "homepage": "https://github.com/amphp/parallel", "keywords": [ - "NDJSON", - "json", - "jsonlines", - "newline", - "reactphp", - "streaming" + "async", + "asynchronous", + "concurrent", + "multi-processing", + "multi-threading" ], "support": { - "issues": "https://github.com/clue/reactphp-ndjson/issues", - "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + "issues": "https://github.com/amphp/parallel/issues", + "source": "https://github.com/amphp/parallel/tree/v2.3.1" }, "funding": [ { - "url": "https://clue.engineering/support", - "type": "custom" - }, - { - "url": "https://github.com/clue", + "url": "https://github.com/amphp", "type": "github" } ], - "time": "2022-12-23T10:58:28+00:00" + "time": "2024-12-21T01:56:09+00:00" }, { - "name": "clue/stream-filter", - "version": "v1.7.0", + "name": "amphp/parser", + "version": "v1.1.1", "source": { "type": "git", - "url": "https://github.com/clue/stream-filter.git", - "reference": "049509fef80032cb3f051595029ab75b49a3c2f7" + "url": "https://github.com/amphp/parser.git", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/clue/stream-filter/zipball/049509fef80032cb3f051595029ab75b49a3c2f7", - "reference": "049509fef80032cb3f051595029ab75b49a3c2f7", + "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", "shasum": "" }, "require": { - "php": ">=5.3" + "php": ">=7.4" }, "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" }, "type": "library", "autoload": { - "files": [ - "src/functions_include.php" - ], "psr-4": { - "Clue\\StreamFilter\\": "src/" + "Amp\\Parser\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1683,76 +1648,63 @@ ], "authors": [ { - "name": "Christian Lรผck", - "email": "christian@clue.engineering" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "A simple and modern approach to stream filtering in PHP", - "homepage": "https://github.com/clue/stream-filter", + "description": "A generator parser to make streaming parsers simple.", + "homepage": "https://github.com/amphp/parser", "keywords": [ - "bucket brigade", - "callback", - "filter", - "php_user_filter", - "stream", - "stream_filter_append", - "stream_filter_register" + "async", + "non-blocking", + "parser", + "stream" ], "support": { - "issues": "https://github.com/clue/stream-filter/issues", - "source": "https://github.com/clue/stream-filter/tree/v1.7.0" + "issues": "https://github.com/amphp/parser/issues", + "source": "https://github.com/amphp/parser/tree/v1.1.1" }, "funding": [ { - "url": "https://clue.engineering/support", - "type": "custom" - }, - { - "url": "https://github.com/clue", + "url": "https://github.com/amphp", "type": "github" } ], - "time": "2023-12-20T15:40:13+00:00" + "time": "2024-03-21T19:16:53+00:00" }, { - "name": "composer/pcre", - "version": "3.2.0", + "name": "amphp/pipeline", + "version": "v1.2.3", "source": { "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90" + "url": "https://github.com/amphp/pipeline.git", + "reference": "7b52598c2e9105ebcddf247fc523161581930367" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/ea4ab6f9580a4fd221e0418f2c357cdd39102a90", - "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/7b52598c2e9105ebcddf247fc523161581930367", + "reference": "7b52598c2e9105ebcddf247fc523161581930367", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<1.11.8" + "amphp/amp": "^3", + "php": ">=8.1", + "revolt/event-loop": "^1" }, "require-dev": { - "phpstan/phpstan": "^1.11.8", - "phpstan/phpstan-strict-rules": "^1.1", - "phpunit/phpunit": "^8 || ^9" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - }, - "phpstan": { - "includes": [ - "extension.neon" - ] - } - }, "autoload": { "psr-4": { - "Composer\\Pcre\\": "src" + "Amp\\Pipeline\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1761,21 +1713,615 @@ ], "authors": [ { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "description": "Asynchronous iterators and operators.", + "homepage": "https://amphp.org/pipeline", "keywords": [ - "PCRE", - "preg", - "regex", - "regular expression" + "amp", + "amphp", + "async", + "io", + "iterator", + "non-blocking" + ], + "support": { + "issues": "https://github.com/amphp/pipeline/issues", + "source": "https://github.com/amphp/pipeline/tree/v1.2.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-03-16T16:33:53+00:00" + }, + { + "name": "amphp/process", + "version": "v2.0.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/process.git", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/process/zipball/52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Process\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A fiber-aware process manager based on Amp and Revolt.", + "homepage": "https://amphp.org/process", + "support": { + "issues": "https://github.com/amphp/process/issues", + "source": "https://github.com/amphp/process/tree/v2.0.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:13:44+00:00" + }, + { + "name": "amphp/serialization", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/serialization.git", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "phpunit/phpunit": "^9 || ^8 || ^7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Serialization\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Serialization tools for IPC and data storage in PHP.", + "homepage": "https://github.com/amphp/serialization", + "keywords": [ + "async", + "asynchronous", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/amphp/serialization/issues", + "source": "https://github.com/amphp/serialization/tree/master" + }, + "time": "2020-03-25T21:39:07+00:00" + }, + { + "name": "amphp/socket", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/socket.git", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/socket/zipball/58e0422221825b79681b72c50c47a930be7bf1e1", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/dns": "^2", + "ext-openssl": "*", + "kelunik/certificate": "^1.1", + "league/uri": "^6.5 | ^7", + "league/uri-interfaces": "^2.3 | ^7", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "amphp/process": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php", + "src/SocketAddress/functions.php" + ], + "psr-4": { + "Amp\\Socket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.", + "homepage": "https://github.com/amphp/socket", + "keywords": [ + "amp", + "async", + "encryption", + "non-blocking", + "sockets", + "tcp", + "tls" + ], + "support": { + "issues": "https://github.com/amphp/socket/issues", + "source": "https://github.com/amphp/socket/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-21T14:33:03+00:00" + }, + { + "name": "amphp/sync", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/sync.git", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Sync\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.", + "homepage": "https://github.com/amphp/sync", + "keywords": [ + "async", + "asynchronous", + "mutex", + "semaphore", + "synchronization" + ], + "support": { + "issues": "https://github.com/amphp/sync/issues", + "source": "https://github.com/amphp/sync/tree/v2.3.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-08-03T19:31:26+00:00" + }, + { + "name": "buggregator/trap", + "version": "1.13.12", + "source": { + "type": "git", + "url": "https://github.com/buggregator/trap.git", + "reference": "2a0a6060f7d312e5296a393a303d5867005ff296" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/buggregator/trap/zipball/2a0a6060f7d312e5296a393a303d5867005ff296", + "reference": "2a0a6060f7d312e5296a393a303d5867005ff296", + "shasum": "" + }, + "require": { + "clue/stream-filter": "^1.6", + "ext-filter": "*", + "ext-sockets": "*", + "nunomaduro/termwind": "^1.15.1 || ^2", + "nyholm/psr7": "^1.8", + "php": ">=8.1", + "php-http/message": "^1.15", + "psr/container": "^1.1 || ^2.0", + "psr/http-message": "^1.1 || ^2", + "symfony/console": "^6.4 || ^7", + "symfony/var-dumper": "^6.3 || ^7", + "yiisoft/injector": "^1.2" + }, + "require-dev": { + "dereuromark/composer-prefer-lowest": "^0.1.10", + "ergebnis/phpunit-slow-test-detector": "^2.14", + "google/protobuf": "^3.25 || ^4.30", + "phpunit/phpunit": "^10.5.10", + "rector/rector": "^1.1", + "roxblnfk/unpoly": "^1.8.1", + "spiral/code-style": "^2.2.2", + "ta-tikoma/phpunit-architecture-test": "^0.8.4", + "vimeo/psalm": "^6.5" + }, + "suggest": { + "ext-simplexml": "To load trap.xml", + "roxblnfk/unpoly": "If you want to remove unnecessary PHP polyfills depend on PHP version." + }, + "bin": [ + "bin/trap" + ], + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Buggregator\\Trap\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Aleksei Gagarin (roxblnfk)", + "homepage": "https://github.com/roxblnfk" + }, + { + "name": "Pavel Buchnev (butschster)", + "homepage": "https://github.com/butschster" + } + ], + "description": "A simple and powerful tool for debugging PHP applications.", + "homepage": "https://buggregator.dev/", + "keywords": [ + "Fibers", + "WebSockets", + "binary dump", + "cli", + "console", + "debug", + "dev", + "dump", + "dumper", + "helper", + "sentry", + "server", + "smtp" + ], + "support": { + "issues": "https://github.com/buggregator/trap/issues", + "source": "https://github.com/buggregator/trap/tree/1.13.12" + }, + "funding": [ + { + "url": "https://boosty.to/roxblnfk", + "type": "boosty" + }, + { + "url": "https://patreon.com/roxblnfk", + "type": "patreon" + } + ], + "time": "2025-04-03T01:29:54+00:00" + }, + { + "name": "clue/ndjson-react", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lรผck", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", + "keywords": [ + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" + }, + { + "name": "clue/stream-filter", + "version": "v1.7.0", + "source": { + "type": "git", + "url": "https://github.com/clue/stream-filter.git", + "reference": "049509fef80032cb3f051595029ab75b49a3c2f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/stream-filter/zipball/049509fef80032cb3f051595029ab75b49a3c2f7", + "reference": "049509fef80032cb3f051595029ab75b49a3c2f7", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "Clue\\StreamFilter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lรผck", + "email": "christian@clue.engineering" + } + ], + "description": "A simple and modern approach to stream filtering in PHP", + "homepage": "https://github.com/clue/stream-filter", + "keywords": [ + "bucket brigade", + "callback", + "filter", + "php_user_filter", + "stream", + "stream_filter_append", + "stream_filter_register" + ], + "support": { + "issues": "https://github.com/clue/stream-filter/issues", + "source": "https://github.com/clue/stream-filter/tree/v1.7.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2023-12-20T15:40:13+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.2.0" + "source": "https://github.com/composer/pcre/tree/3.3.2" }, "funding": [ { @@ -1791,28 +2337,28 @@ "type": "tidelift" } ], - "time": "2024-07-25T09:36:02+00:00" + "time": "2024-11-12T16:29:46+00:00" }, { "name": "composer/semver", - "version": "3.4.2", + "version": "3.4.3", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6" + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/c51258e759afdb17f1fd1fe83bc12baaef6309d6", - "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", "shasum": "" }, "require": { "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^1.4", - "symfony/phpunit-bridge": "^4.2 || ^5" + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" }, "type": "library", "extra": { @@ -1856,7 +2402,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.2" + "source": "https://github.com/composer/semver/tree/3.4.3" }, "funding": [ { @@ -1872,7 +2418,7 @@ "type": "tidelift" } ], - "time": "2024-07-12T11:35:52+00:00" + "time": "2024-09-19T14:15:21+00:00" }, { "name": "composer/xdebug-handler", @@ -1940,6 +2486,102 @@ ], "time": "2024-05-06T16:37:16+00:00" }, + { + "name": "danog/advanced-json-rpc", + "version": "v3.2.2", + "source": { + "type": "git", + "url": "https://github.com/danog/php-advanced-json-rpc.git", + "reference": "aadb1c4068a88c3d0530cfe324b067920661efcb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/danog/php-advanced-json-rpc/zipball/aadb1c4068a88c3d0530cfe324b067920661efcb", + "reference": "aadb1c4068a88c3d0530cfe324b067920661efcb", + "shasum": "" + }, + "require": { + "netresearch/jsonmapper": "^5", + "php": ">=8.1", + "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" + }, + "replace": { + "felixfbecker/php-advanced-json-rpc": "^3" + }, + "require-dev": { + "phpunit/phpunit": "^9" + }, + "type": "library", + "autoload": { + "psr-4": { + "AdvancedJsonRpc\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + }, + { + "name": "Daniil Gentili", + "email": "daniil@daniil.it" + } + ], + "description": "A more advanced JSONRPC implementation", + "support": { + "issues": "https://github.com/danog/php-advanced-json-rpc/issues", + "source": "https://github.com/danog/php-advanced-json-rpc/tree/v3.2.2" + }, + "time": "2025-02-14T10:55:15+00:00" + }, + { + "name": "daverandom/libdns", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/DaveRandom/LibDNS.git", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "Required for IDN support" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "LibDNS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "DNS protocol implementation written in pure PHP", + "keywords": [ + "dns" + ], + "support": { + "issues": "https://github.com/DaveRandom/LibDNS/issues", + "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0" + }, + "time": "2024-04-12T12:12:48+00:00" + }, { "name": "dereuromark/composer-prefer-lowest", "version": "0.1.10", @@ -2028,29 +2670,27 @@ }, { "name": "doctrine/deprecations", - "version": "1.1.3", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab" + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", - "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/31610dbb31faa98e6b5447b62340826f54fbc4e9", + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^9", - "phpstan/phpstan": "1.4.10 || 1.10.15", - "phpstan/phpstan-phpunit": "^1.0", + "doctrine/coding-standard": "^9 || ^12", + "phpstan/phpstan": "1.4.10 || 2.0.3", + "phpstan/phpstan-phpunit": "^1.0 || ^2", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "psalm/plugin-phpunit": "0.18.4", - "psr/log": "^1 || ^2 || ^3", - "vimeo/psalm": "4.30.0 || 5.12.0" + "psr/log": "^1 || ^2 || ^3" }, "suggest": { "psr/log": "Allows logging deprecations via PSR-3 logger implementation" @@ -2058,7 +2698,7 @@ "type": "library", "autoload": { "psr-4": { - "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + "Doctrine\\Deprecations\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2069,76 +2709,9 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.3" - }, - "time": "2024-01-30T19:34:25+00:00" - }, - { - "name": "ergebnis/phpunit-slow-test-detector", - "version": "2.15.0", - "source": { - "type": "git", - "url": "https://github.com/ergebnis/phpunit-slow-test-detector.git", - "reference": "d9e4ea11d0e7bf1e54df5385bfd94b5156ab0a29" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ergebnis/phpunit-slow-test-detector/zipball/d9e4ea11d0e7bf1e54df5385bfd94b5156ab0a29", - "reference": "d9e4ea11d0e7bf1e54df5385bfd94b5156ab0a29", - "shasum": "" - }, - "require": { - "php": "~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", - "phpunit/phpunit": "^6.5.0 || ^7.5.0 || ^8.5.19 || ^9.0.0 || ^10.0.0 || ^11.0.0" - }, - "require-dev": { - "ergebnis/composer-normalize": "^2.43.0", - "ergebnis/license": "^2.4.0", - "ergebnis/php-cs-fixer-config": "^6.31.0", - "fakerphp/faker": "~1.20.0", - "psalm/plugin-phpunit": "~0.19.0", - "psr/container": "~1.0.0", - "rector/rector": "^1.1.0", - "vimeo/psalm": "^5.24.0" - }, - "type": "library", - "extra": { - "composer-normalize": { - "indent-size": 2, - "indent-style": "space" - } - }, - "autoload": { - "psr-4": { - "Ergebnis\\PHPUnit\\SlowTestDetector\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Andreas Mรถller", - "email": "am@localheinz.com", - "homepage": "https://localheinz.com" - } - ], - "description": "Provides facilities for detecting slow tests in phpunit/phpunit.", - "homepage": "https://github.com/ergebnis/phpunit-slow-test-detector", - "keywords": [ - "detector", - "extension", - "phpunit", - "slow", - "test" - ], - "support": { - "issues": "https://github.com/ergebnis/phpunit-slow-test-detector/issues", - "security": "https://github.com/ergebnis/phpunit-slow-test-detector/blob/main/.github/SECURITY.md", - "source": "https://github.com/ergebnis/phpunit-slow-test-detector" + "source": "https://github.com/doctrine/deprecations/tree/1.1.4" }, - "time": "2024-06-16T18:21:35+00:00" + "time": "2024-12-07T21:18:45+00:00" }, { "name": "evenement/evenement", @@ -2187,63 +2760,18 @@ }, "time": "2023-08-08T05:53:35+00:00" }, - { - "name": "felixfbecker/advanced-json-rpc", - "version": "v3.2.1", - "source": { - "type": "git", - "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git", - "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/b5f37dbff9a8ad360ca341f3240dc1c168b45447", - "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447", - "shasum": "" - }, - "require": { - "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", - "php": "^7.1 || ^8.0", - "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" - }, - "require-dev": { - "phpunit/phpunit": "^7.0 || ^8.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "AdvancedJsonRpc\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "ISC" - ], - "authors": [ - { - "name": "Felix Becker", - "email": "felix.b@outlook.com" - } - ], - "description": "A more advanced JSONRPC implementation", - "support": { - "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues", - "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.1" - }, - "time": "2021-06-11T22:34:44+00:00" - }, { "name": "felixfbecker/language-server-protocol", - "version": "v1.5.2", + "version": "v1.5.3", "source": { "type": "git", "url": "https://github.com/felixfbecker/php-language-server-protocol.git", - "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842" + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/6e82196ffd7c62f7794d778ca52b69feec9f2842", - "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/a9e113dbc7d849e35b8776da39edaf4313b7b6c9", + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9", "shasum": "" }, "require": { @@ -2284,22 +2812,22 @@ ], "support": { "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", - "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.2" + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.3" }, - "time": "2022-03-02T22:36:06+00:00" + "time": "2024-04-30T00:40:11+00:00" }, { "name": "fidry/cpu-core-counter", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42" + "reference": "8520451a140d3f46ac33042715115e290cf5785f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/f92996c4d5c1a696a6a970e20f7c4216200fcc42", - "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", "shasum": "" }, "require": { @@ -2339,7 +2867,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.1.0" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" }, "funding": [ { @@ -2347,45 +2875,78 @@ "type": "github" } ], - "time": "2024-02-07T09:43:46+00:00" + "time": "2024-08-06T10:04:20+00:00" }, { - "name": "filp/whoops", - "version": "2.15.4", + "name": "friendsofphp/php-cs-fixer", + "version": "v3.75.0", "source": { "type": "git", - "url": "https://github.com/filp/whoops.git", - "reference": "a139776fa3f5985a50b509f2a02ff0f709d2a546" + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "399a128ff2fdaf4281e4e79b755693286cdf325c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/a139776fa3f5985a50b509f2a02ff0f709d2a546", - "reference": "a139776fa3f5985a50b509f2a02ff0f709d2a546", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/399a128ff2fdaf4281e4e79b755693286cdf325c", + "reference": "399a128ff2fdaf4281e4e79b755693286cdf325c", "shasum": "" }, "require": { - "php": "^5.5.9 || ^7.0 || ^8.0", - "psr/log": "^1.0.1 || ^2.0 || ^3.0" + "clue/ndjson-react": "^1.0", + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.3", + "ext-filter": "*", + "ext-hash": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "fidry/cpu-core-counter": "^1.2", + "php": "^7.4 || ^8.0", + "react/child-process": "^0.6.5", + "react/event-loop": "^1.0", + "react/promise": "^2.0 || ^3.0", + "react/socket": "^1.0", + "react/stream": "^1.0", + "sebastian/diff": "^4.0 || ^5.1 || ^6.0 || ^7.0", + "symfony/console": "^5.4 || ^6.4 || ^7.0", + "symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.4 || ^7.0", + "symfony/finder": "^5.4 || ^6.4 || ^7.0", + "symfony/options-resolver": "^5.4 || ^6.4 || ^7.0", + "symfony/polyfill-mbstring": "^1.31", + "symfony/polyfill-php80": "^1.31", + "symfony/polyfill-php81": "^1.31", + "symfony/process": "^5.4 || ^6.4 || ^7.2", + "symfony/stopwatch": "^5.4 || ^6.4 || ^7.0" }, "require-dev": { - "mockery/mockery": "^0.9 || ^1.0", - "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3", - "symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0" + "facile-it/paraunit": "^1.3.1 || ^2.6", + "infection/infection": "^0.29.14", + "justinrainbow/json-schema": "^5.3 || ^6.2", + "keradus/cli-executor": "^2.1", + "mikey179/vfsstream": "^1.6.12", + "php-coveralls/php-coveralls": "^2.7", + "php-cs-fixer/accessible-object": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", + "phpunit/phpunit": "^9.6.22 || ^10.5.45 || ^11.5.12", + "symfony/var-dumper": "^5.4.48 || ^6.4.18 || ^7.2.3", + "symfony/yaml": "^5.4.45 || ^6.4.18 || ^7.2.3" }, "suggest": { - "symfony/var-dumper": "Pretty print complex values better with var-dumper available", - "whoops/soap": "Formats errors as SOAP responses" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev" - } + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", "autoload": { "psr-4": { - "Whoops\\": "src/Whoops/" - } + "PhpCsFixer\\": "src/" + }, + "exclude-from-classmap": [ + "src/Fixer/Internal/*" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2393,102 +2954,132 @@ ], "authors": [ { - "name": "Filipe Dobreira", - "homepage": "https://github.com/filp", - "role": "Developer" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiล„ski", + "email": "dariusz.ruminski@gmail.com" } ], - "description": "php error handling for cool kids", - "homepage": "https://filp.github.io/whoops/", + "description": "A tool to automatically fix PHP code style", "keywords": [ - "error", - "exception", - "handling", - "library", - "throwable", - "whoops" + "Static code analysis", + "fixer", + "standards", + "static analysis" ], "support": { - "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.15.4" + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.75.0" }, "funding": [ { - "url": "https://github.com/denis-sokolov", + "url": "https://github.com/keradus", "type": "github" } ], - "time": "2023-11-03T12:00:00+00:00" + "time": "2025-03-31T18:40:42+00:00" }, { - "name": "friendsofphp/php-cs-fixer", - "version": "v3.61.1", + "name": "kelunik/certificate", + "version": "v1.1.3", "source": { "type": "git", - "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "94a87189f55814e6cabca2d9a33b06de384a2ab8" + "url": "https://github.com/kelunik/certificate.git", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/94a87189f55814e6cabca2d9a33b06de384a2ab8", - "reference": "94a87189f55814e6cabca2d9a33b06de384a2ab8", + "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e", "shasum": "" }, "require": { - "clue/ndjson-react": "^1.0", - "composer/semver": "^3.4", - "composer/xdebug-handler": "^3.0.3", - "ext-filter": "*", - "ext-json": "*", - "ext-tokenizer": "*", - "fidry/cpu-core-counter": "^1.0", - "php": "^7.4 || ^8.0", - "react/child-process": "^0.6.5", - "react/event-loop": "^1.0", - "react/promise": "^2.0 || ^3.0", - "react/socket": "^1.0", - "react/stream": "^1.0", - "sebastian/diff": "^4.0 || ^5.0 || ^6.0", - "symfony/console": "^5.4 || ^6.0 || ^7.0", - "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", - "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", - "symfony/finder": "^5.4 || ^6.0 || ^7.0", - "symfony/options-resolver": "^5.4 || ^6.0 || ^7.0", - "symfony/polyfill-mbstring": "^1.28", - "symfony/polyfill-php80": "^1.28", - "symfony/polyfill-php81": "^1.28", - "symfony/process": "^5.4 || ^6.0 || ^7.0", - "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0" + "ext-openssl": "*", + "php": ">=7.0" }, "require-dev": { - "facile-it/paraunit": "^1.3 || ^2.3", - "infection/infection": "^0.29.5", - "justinrainbow/json-schema": "^5.2", - "keradus/cli-executor": "^2.1", - "mikey179/vfsstream": "^1.6.11", - "php-coveralls/php-coveralls": "^2.7", - "php-cs-fixer/accessible-object": "^1.1", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.5", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.5", - "phpunit/phpunit": "^9.6.19 || ^10.5.21 || ^11.2", - "symfony/var-dumper": "^5.4 || ^6.0 || ^7.0", - "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^6 | 7 | ^8 | ^9" }, - "suggest": { - "ext-dom": "For handling output formats in XML", - "ext-mbstring": "For handling non-UTF8 characters." + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } }, - "bin": [ - "php-cs-fixer" + "autoload": { + "psr-4": { + "Kelunik\\Certificate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" ], - "type": "application", + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Access certificate details and transform between different formats.", + "keywords": [ + "DER", + "certificate", + "certificates", + "openssl", + "pem", + "x509" + ], + "support": { + "issues": "https://github.com/kelunik/certificate/issues", + "source": "https://github.com/kelunik/certificate/tree/v1.1.3" + }, + "time": "2023-02-03T21:26:53+00:00" + }, + { + "name": "league/uri", + "version": "7.5.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "81fb5145d2644324614cc532b28efd0215bda430" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", + "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.5", + "php": "^8.1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "league/uri-components": "Needed to easily manipulate URI objects components", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, "autoload": { "psr-4": { - "PhpCsFixer\\": "src/" - }, - "exclude-from-classmap": [ - "src/Fixer/Internal/*" - ] + "League\\Uri\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2496,67 +3087,84 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Dariusz Rumiล„ski", - "email": "dariusz.ruminski@gmail.com" + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" } ], - "description": "A tool to automatically fix PHP code style", + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", "keywords": [ - "Static code analysis", - "fixer", - "standards", - "static analysis" + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "uri-template", + "url", + "ws" ], "support": { - "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.61.1" + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.5.1" }, "funding": [ { - "url": "https://github.com/keradus", + "url": "https://github.com/sponsors/nyamsprod", "type": "github" } ], - "time": "2024-07-31T14:33:15+00:00" + "time": "2024-12-08T08:40:02+00:00" }, { - "name": "jean85/pretty-package-versions", - "version": "2.0.6", + "name": "league/uri-interfaces", + "version": "7.5.0", "source": { "type": "git", - "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4" + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/f9fdd29ad8e6d024f52678b570e5593759b550b4", - "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", "shasum": "" }, "require": { - "composer-runtime-api": "^2.0.0", - "php": "^7.1|^8.0" + "ext-filter": "*", + "php": "^8.1", + "psr/http-factory": "^1", + "psr/http-message": "^1.1 || ^2.0" }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.2", - "jean85/composer-provided-replaced-stub-package": "^1.0", - "phpstan/phpstan": "^1.4", - "phpunit/phpunit": "^7.5|^8.5|^9.4", - "vimeo/psalm": "^4.3" + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-master": "7.x-dev" } }, "autoload": { "psr-4": { - "Jean85\\": "src/" + "League\\Uri\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -2565,35 +3173,58 @@ ], "authors": [ { - "name": "Alessandro Lai", - "email": "alessandro.lai85@gmail.com" + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" } ], - "description": "A library to get pretty versions strings of installed dependencies", + "description": "Common interfaces and classes for URI representation and interaction", + "homepage": "https://uri.thephpleague.com", "keywords": [ - "composer", - "package", - "release", - "versions" + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" ], "support": { - "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.6" + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" }, - "time": "2024-03-08T09:58:59+00:00" + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:18:47+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.12.0", + "version": "1.13.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + "reference": "024473a478be9df5fdaca2c793f2232fe788e414" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", + "reference": "024473a478be9df5fdaca2c793f2232fe788e414", "shasum": "" }, "require": { @@ -2632,7 +3263,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" }, "funding": [ { @@ -2640,20 +3271,20 @@ "type": "tidelift" } ], - "time": "2024-06-12T14:39:25+00:00" + "time": "2025-02-12T12:17:51+00:00" }, { "name": "netresearch/jsonmapper", - "version": "v4.4.1", + "version": "v5.0.0", "source": { "type": "git", "url": "https://github.com/cweiske/jsonmapper.git", - "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0" + "reference": "8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/132c75c7dd83e45353ebb9c6c9f591952995bbf0", - "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c", + "reference": "8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c", "shasum": "" }, "require": { @@ -2689,31 +3320,33 @@ "support": { "email": "cweiske@cweiske.de", "issues": "https://github.com/cweiske/jsonmapper/issues", - "source": "https://github.com/cweiske/jsonmapper/tree/v4.4.1" + "source": "https://github.com/cweiske/jsonmapper/tree/v5.0.0" }, - "time": "2024-01-31T06:18:54+00:00" + "time": "2024-09-08T10:20:00+00:00" }, { "name": "nikic/php-parser", - "version": "v4.19.1", + "version": "v5.4.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b" + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4e1b88d21c69391150ace211e9eaf05810858d0b", - "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.1" + "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -2721,7 +3354,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -2745,135 +3378,38 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.1" - }, - "time": "2024-03-17T08:10:35+00:00" - }, - { - "name": "nunomaduro/collision", - "version": "v7.10.0", - "source": { - "type": "git", - "url": "https://github.com/nunomaduro/collision.git", - "reference": "49ec67fa7b002712da8526678abd651c09f375b2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/49ec67fa7b002712da8526678abd651c09f375b2", - "reference": "49ec67fa7b002712da8526678abd651c09f375b2", - "shasum": "" - }, - "require": { - "filp/whoops": "^2.15.3", - "nunomaduro/termwind": "^1.15.1", - "php": "^8.1.0", - "symfony/console": "^6.3.4" - }, - "conflict": { - "laravel/framework": ">=11.0.0" - }, - "require-dev": { - "brianium/paratest": "^7.3.0", - "laravel/framework": "^10.28.0", - "laravel/pint": "^1.13.3", - "laravel/sail": "^1.25.0", - "laravel/sanctum": "^3.3.1", - "laravel/tinker": "^2.8.2", - "nunomaduro/larastan": "^2.6.4", - "orchestra/testbench-core": "^8.13.0", - "pestphp/pest": "^2.23.2", - "phpunit/phpunit": "^10.4.1", - "sebastian/environment": "^6.0.1", - "spatie/laravel-ignition": "^2.3.1" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" - ] - } - }, - "autoload": { - "files": [ - "./src/Adapters/Phpunit/Autoload.php" - ], - "psr-4": { - "NunoMaduro\\Collision\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nuno Maduro", - "email": "enunomaduro@gmail.com" - } - ], - "description": "Cli error handling for console/command-line PHP applications.", - "keywords": [ - "artisan", - "cli", - "command-line", - "console", - "error", - "handling", - "laravel", - "laravel-zero", - "php", - "symfony" - ], - "support": { - "issues": "https://github.com/nunomaduro/collision/issues", - "source": "https://github.com/nunomaduro/collision" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" }, - "funding": [ - { - "url": "https://www.paypal.com/paypalme/enunomaduro", - "type": "custom" - }, - { - "url": "https://github.com/nunomaduro", - "type": "github" - }, - { - "url": "https://www.patreon.com/nunomaduro", - "type": "patreon" - } - ], - "time": "2023-10-11T15:45:01+00:00" + "time": "2024-12-30T11:07:19+00:00" }, { "name": "nunomaduro/termwind", - "version": "v1.15.1", + "version": "v1.17.0", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "8ab0b32c8caa4a2e09700ea32925441385e4a5dc" + "reference": "5369ef84d8142c1d87e4ec278711d4ece3cbf301" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/8ab0b32c8caa4a2e09700ea32925441385e4a5dc", - "reference": "8ab0b32c8caa4a2e09700ea32925441385e4a5dc", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/5369ef84d8142c1d87e4ec278711d4ece3cbf301", + "reference": "5369ef84d8142c1d87e4ec278711d4ece3cbf301", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": "^8.0", - "symfony/console": "^5.3.0|^6.0.0" + "php": "^8.1", + "symfony/console": "^6.4.15" }, "require-dev": { - "ergebnis/phpstan-rules": "^1.0.", - "illuminate/console": "^8.0|^9.0", - "illuminate/support": "^8.0|^9.0", - "laravel/pint": "^1.0.0", - "pestphp/pest": "^1.21.0", - "pestphp/pest-plugin-mock": "^1.0", - "phpstan/phpstan": "^1.4.6", - "phpstan/phpstan-strict-rules": "^1.1.0", - "symfony/var-dumper": "^5.2.7|^6.0.0", + "illuminate/console": "^10.48.24", + "illuminate/support": "^10.48.24", + "laravel/pint": "^1.18.2", + "pestphp/pest": "^2.36.0", + "pestphp/pest-plugin-mock": "2.0.0", + "phpstan/phpstan": "^1.12.11", + "phpstan/phpstan-strict-rules": "^1.6.1", + "symfony/var-dumper": "^6.4.15", "thecodingmachine/phpstan-strict-rules": "^1.0.0" }, "type": "library", @@ -2913,7 +3449,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/termwind/issues", - "source": "https://github.com/nunomaduro/termwind/tree/v1.15.1" + "source": "https://github.com/nunomaduro/termwind/tree/v1.17.0" }, "funding": [ { @@ -2929,20 +3465,20 @@ "type": "github" } ], - "time": "2023-02-08T01:06:31+00:00" + "time": "2024-11-21T10:36:35+00:00" }, { "name": "nyholm/psr7", - "version": "1.8.1", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/Nyholm/psr7.git", - "reference": "aa5fc277a4f5508013d571341ade0c3886d4d00e" + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Nyholm/psr7/zipball/aa5fc277a4f5508013d571341ade0c3886d4d00e", - "reference": "aa5fc277a4f5508013d571341ade0c3886d4d00e", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", "shasum": "" }, "require": { @@ -2984,279 +3520,30 @@ }, { "name": "Martijn van der Ven", - "email": "martijn@vanderven.se" - } - ], - "description": "A fast PHP7 implementation of PSR-7", - "homepage": "https://tnyholm.se", - "keywords": [ - "psr-17", - "psr-7" - ], - "support": { - "issues": "https://github.com/Nyholm/psr7/issues", - "source": "https://github.com/Nyholm/psr7/tree/1.8.1" - }, - "funding": [ - { - "url": "https://github.com/Zegnat", - "type": "github" - }, - { - "url": "https://github.com/nyholm", - "type": "github" - } - ], - "time": "2023-11-13T09:31:12+00:00" - }, - { - "name": "pestphp/pest", - "version": "v2.35.0", - "source": { - "type": "git", - "url": "https://github.com/pestphp/pest.git", - "reference": "d0ff2c8ec294b7aa7fcb0f3ddc4fdec864234646" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/d0ff2c8ec294b7aa7fcb0f3ddc4fdec864234646", - "reference": "d0ff2c8ec294b7aa7fcb0f3ddc4fdec864234646", - "shasum": "" - }, - "require": { - "brianium/paratest": "^7.3.1", - "nunomaduro/collision": "^7.10.0|^8.3.0", - "nunomaduro/termwind": "^1.15.1|^2.0.1", - "pestphp/pest-plugin": "^2.1.1", - "pestphp/pest-plugin-arch": "^2.7.0", - "php": "^8.1.0", - "phpunit/phpunit": "^10.5.17" - }, - "conflict": { - "phpunit/phpunit": ">10.5.17", - "sebastian/exporter": "<5.1.0", - "webmozart/assert": "<1.11.0" - }, - "require-dev": { - "pestphp/pest-dev-tools": "^2.16.0", - "pestphp/pest-plugin-type-coverage": "^2.8.5", - "symfony/process": "^6.4.0|^7.1.3" - }, - "bin": [ - "bin/pest" - ], - "type": "library", - "extra": { - "pest": { - "plugins": [ - "Pest\\Plugins\\Bail", - "Pest\\Plugins\\Cache", - "Pest\\Plugins\\Coverage", - "Pest\\Plugins\\Init", - "Pest\\Plugins\\Environment", - "Pest\\Plugins\\Help", - "Pest\\Plugins\\Memory", - "Pest\\Plugins\\Only", - "Pest\\Plugins\\Printer", - "Pest\\Plugins\\ProcessIsolation", - "Pest\\Plugins\\Profile", - "Pest\\Plugins\\Retry", - "Pest\\Plugins\\Snapshot", - "Pest\\Plugins\\Verbose", - "Pest\\Plugins\\Version", - "Pest\\Plugins\\Parallel" - ] - }, - "phpstan": { - "includes": [ - "extension.neon" - ] - } - }, - "autoload": { - "files": [ - "src/Functions.php", - "src/Pest.php" - ], - "psr-4": { - "Pest\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nuno Maduro", - "email": "enunomaduro@gmail.com" - } - ], - "description": "The elegant PHP Testing Framework.", - "keywords": [ - "framework", - "pest", - "php", - "test", - "testing", - "unit" - ], - "support": { - "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v2.35.0" - }, - "funding": [ - { - "url": "https://www.paypal.com/paypalme/enunomaduro", - "type": "custom" - }, - { - "url": "https://github.com/nunomaduro", - "type": "github" - } - ], - "time": "2024-08-02T10:57:29+00:00" - }, - { - "name": "pestphp/pest-plugin", - "version": "v2.1.1", - "source": { - "type": "git", - "url": "https://github.com/pestphp/pest-plugin.git", - "reference": "e05d2859e08c2567ee38ce8b005d044e72648c0b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/e05d2859e08c2567ee38ce8b005d044e72648c0b", - "reference": "e05d2859e08c2567ee38ce8b005d044e72648c0b", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^2.0.0", - "composer-runtime-api": "^2.2.2", - "php": "^8.1" - }, - "conflict": { - "pestphp/pest": "<2.2.3" - }, - "require-dev": { - "composer/composer": "^2.5.8", - "pestphp/pest": "^2.16.0", - "pestphp/pest-dev-tools": "^2.16.0" - }, - "type": "composer-plugin", - "extra": { - "class": "Pest\\Plugin\\Manager" - }, - "autoload": { - "psr-4": { - "Pest\\Plugin\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "The Pest plugin manager", - "keywords": [ - "framework", - "manager", - "pest", - "php", - "plugin", - "test", - "testing", - "unit" - ], - "support": { - "source": "https://github.com/pestphp/pest-plugin/tree/v2.1.1" - }, - "funding": [ - { - "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", - "type": "custom" - }, - { - "url": "https://github.com/nunomaduro", - "type": "github" - }, - { - "url": "https://www.patreon.com/nunomaduro", - "type": "patreon" - } - ], - "time": "2023-08-22T08:40:06+00:00" - }, - { - "name": "pestphp/pest-plugin-arch", - "version": "v2.7.0", - "source": { - "type": "git", - "url": "https://github.com/pestphp/pest-plugin-arch.git", - "reference": "d23b2d7498475354522c3818c42ef355dca3fcda" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/d23b2d7498475354522c3818c42ef355dca3fcda", - "reference": "d23b2d7498475354522c3818c42ef355dca3fcda", - "shasum": "" - }, - "require": { - "nunomaduro/collision": "^7.10.0|^8.1.0", - "pestphp/pest-plugin": "^2.1.1", - "php": "^8.1", - "ta-tikoma/phpunit-architecture-test": "^0.8.4" - }, - "require-dev": { - "pestphp/pest": "^2.33.0", - "pestphp/pest-dev-tools": "^2.16.0" - }, - "type": "library", - "extra": { - "pest": { - "plugins": [ - "Pest\\Arch\\Plugin" - ] - } - }, - "autoload": { - "files": [ - "src/Autoload.php" - ], - "psr-4": { - "Pest\\Arch\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" + "email": "martijn@vanderven.se" + } ], - "description": "The Arch plugin for Pest PHP.", + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", "keywords": [ - "arch", - "architecture", - "framework", - "pest", - "php", - "plugin", - "test", - "testing", - "unit" + "psr-17", + "psr-7" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-arch/tree/v2.7.0" + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" }, "funding": [ { - "url": "https://www.paypal.com/paypalme/enunomaduro", - "type": "custom" + "url": "https://github.com/Zegnat", + "type": "github" }, { - "url": "https://github.com/nunomaduro", + "url": "https://github.com/nyholm", "type": "github" } ], - "time": "2024-01-26T09:46:42+00:00" + "time": "2024-09-09T07:06:30+00:00" }, { "name": "phar-io/manifest", @@ -3378,16 +3665,16 @@ }, { "name": "php-http/message", - "version": "1.16.1", + "version": "1.16.2", "source": { "type": "git", "url": "https://github.com/php-http/message.git", - "reference": "5997f3289332c699fa2545c427826272498a2088" + "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/message/zipball/5997f3289332c699fa2545c427826272498a2088", - "reference": "5997f3289332c699fa2545c427826272498a2088", + "url": "https://api.github.com/repos/php-http/message/zipball/06dd5e8562f84e641bf929bfe699ee0f5ce8080a", + "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a", "shasum": "" }, "require": { @@ -3441,9 +3728,9 @@ ], "support": { "issues": "https://github.com/php-http/message/issues", - "source": "https://github.com/php-http/message/tree/1.16.1" + "source": "https://github.com/php-http/message/tree/1.16.2" }, - "time": "2024-03-07T13:22:09+00:00" + "time": "2024-10-02T11:34:13+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -3500,16 +3787,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.4.1", + "version": "5.6.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c" + "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c", - "reference": "9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", + "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", "shasum": "" }, "require": { @@ -3518,17 +3805,17 @@ "php": "^7.4 || ^8.0", "phpdocumentor/reflection-common": "^2.2", "phpdocumentor/type-resolver": "^1.7", - "phpstan/phpdoc-parser": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", "webmozart/assert": "^1.9.1" }, "require-dev": { - "mockery/mockery": "~1.3.5", + "mockery/mockery": "~1.3.5 || ~1.6.0", "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.8", "phpstan/phpstan-mockery": "^1.1", "phpstan/phpstan-webmozart-assert": "^1.2", "phpunit/phpunit": "^9.5", - "vimeo/psalm": "^5.13" + "psalm/phar": "^5.26" }, "type": "library", "extra": { @@ -3558,29 +3845,29 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.4.1" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.1" }, - "time": "2024-05-21T05:55:05+00:00" + "time": "2024-12-07T09:39:29+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.8.2", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "153ae662783729388a584b4361f2545e4d841e3c" + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/153ae662783729388a584b4361f2545e4d841e3c", - "reference": "153ae662783729388a584b4361f2545e4d841e3c", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", "shasum": "" }, "require": { "doctrine/deprecations": "^1.0", "php": "^7.3 || ^8.0", "phpdocumentor/reflection-common": "^2.0", - "phpstan/phpdoc-parser": "^1.13" + "phpstan/phpdoc-parser": "^1.18|^2.0" }, "require-dev": { "ext-tokenizer": "*", @@ -3616,36 +3903,36 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.2" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" }, - "time": "2024-02-23T11:10:43+00:00" + "time": "2024-11-09T15:12:26+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.29.1", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4" + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4", - "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { "doctrine/annotations": "^2.0", - "nikic/php-parser": "^4.15", + "nikic/php-parser": "^5.3.0", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.5", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", "symfony/process": "^5.2" }, "type": "library", @@ -3663,38 +3950,38 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" }, - "time": "2024-05-31T08:52:43+00:00" + "time": "2025-02-19T13:28:12+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "10.1.15", + "version": "10.1.16", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae" + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae", - "reference": "5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.18 || ^5.0", + "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=8.1", - "phpunit/php-file-iterator": "^4.0", - "phpunit/php-text-template": "^3.0", - "sebastian/code-unit-reverse-lookup": "^3.0", - "sebastian/complexity": "^3.0", - "sebastian/environment": "^6.0", - "sebastian/lines-of-code": "^2.0", - "sebastian/version": "^4.0", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { "phpunit/phpunit": "^10.1" @@ -3706,7 +3993,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "10.1-dev" + "dev-main": "10.1.x-dev" } }, "autoload": { @@ -3735,7 +4022,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.15" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" }, "funding": [ { @@ -3743,7 +4030,7 @@ "type": "github" } ], - "time": "2024-06-29T08:25:15+00:00" + "time": "2024-08-22T04:31:57+00:00" }, { "name": "phpunit/php-file-iterator", @@ -3990,16 +4277,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.17", + "version": "10.5.45", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c1f736a473d21957ead7e94fcc029f571895abf5" + "reference": "bd68a781d8e30348bc297449f5234b3458267ae8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c1f736a473d21957ead7e94fcc029f571895abf5", - "reference": "c1f736a473d21957ead7e94fcc029f571895abf5", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bd68a781d8e30348bc297449f5234b3458267ae8", + "reference": "bd68a781d8e30348bc297449f5234b3458267ae8", "shasum": "" }, "require": { @@ -4009,26 +4296,26 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.3", - "phar-io/version": "^3.0.2", + "myclabs/deep-copy": "^1.12.1", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", "php": ">=8.1", - "phpunit/php-code-coverage": "^10.1.5", - "phpunit/php-file-iterator": "^4.0", - "phpunit/php-invoker": "^4.0", - "phpunit/php-text-template": "^3.0", - "phpunit/php-timer": "^6.0", - "sebastian/cli-parser": "^2.0", - "sebastian/code-unit": "^2.0", - "sebastian/comparator": "^5.0", - "sebastian/diff": "^5.0", - "sebastian/environment": "^6.0", - "sebastian/exporter": "^5.1", - "sebastian/global-state": "^6.0.1", - "sebastian/object-enumerator": "^5.0", - "sebastian/recursion-context": "^5.0", - "sebastian/type": "^4.0", - "sebastian/version": "^4.0" + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.3", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.2", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.0", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" }, "suggest": { "ext-soap": "To be able to generate mocks based on WSDL files" @@ -4071,7 +4358,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.17" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.45" }, "funding": [ { @@ -4087,7 +4374,7 @@ "type": "tidelift" } ], - "time": "2024-04-05T04:39:01+00:00" + "time": "2025-02-06T16:08:12+00:00" }, { "name": "psr/event-dispatcher", @@ -4321,33 +4608,33 @@ }, { "name": "react/child-process", - "version": "v0.6.5", + "version": "v0.6.6", "source": { "type": "git", "url": "https://github.com/reactphp/child-process.git", - "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43" + "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/child-process/zipball/e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", - "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/1721e2b93d89b745664353b9cfc8f155ba8a6159", + "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159", "shasum": "" }, "require": { "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "php": ">=5.3.0", "react/event-loop": "^1.2", - "react/stream": "^1.2" + "react/stream": "^1.4" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", - "react/socket": "^1.8", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/socket": "^1.16", "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" }, "type": "library", "autoload": { "psr-4": { - "React\\ChildProcess\\": "src" + "React\\ChildProcess\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4384,19 +4671,15 @@ ], "support": { "issues": "https://github.com/reactphp/child-process/issues", - "source": "https://github.com/reactphp/child-process/tree/v0.6.5" + "source": "https://github.com/reactphp/child-process/tree/v0.6.6" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2022-09-16T13:41:56+00:00" + "time": "2025-01-01T16:37:48+00:00" }, { "name": "react/dns", @@ -4632,6 +4915,78 @@ ], "time": "2024-06-11T12:45:25+00:00" }, + { + "name": "revolt/event-loop", + "version": "v1.0.7", + "source": { + "type": "git", + "url": "https://github.com/revoltphp/event-loop.git", + "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/09bf1bf7f7f574453efe43044b06fafe12216eb3", + "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.15" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Revolt\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "ceesjank@gmail.com" + }, + { + "name": "Christian Lรผck", + "email": "christian@clue.engineering" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Rock-solid event loop for concurrent PHP applications.", + "keywords": [ + "async", + "asynchronous", + "concurrency", + "event", + "event-loop", + "non-blocking", + "scheduler" + ], + "support": { + "issues": "https://github.com/revoltphp/event-loop/issues", + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.7" + }, + "time": "2025-01-25T19:27:39+00:00" + }, { "name": "sebastian/cli-parser", "version": "2.0.1", @@ -4802,16 +5157,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.1", + "version": "5.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "2db5010a484d53ebf536087a70b4a5423c102372" + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2db5010a484d53ebf536087a70b4a5423c102372", - "reference": "2db5010a484d53ebf536087a70b4a5423c102372", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", "shasum": "" }, "require": { @@ -4822,7 +5177,7 @@ "sebastian/exporter": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^10.3" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { @@ -4867,7 +5222,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.1" + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3" }, "funding": [ { @@ -4875,7 +5230,7 @@ "type": "github" } ], - "time": "2023-08-14T13:18:12+00:00" + "time": "2024-10-18T14:56:07+00:00" }, { "name": "sebastian/complexity", @@ -5550,16 +5905,16 @@ }, { "name": "spatie/array-to-xml", - "version": "3.3.0", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/spatie/array-to-xml.git", - "reference": "f56b220fe2db1ade4c88098d83413ebdfc3bf876" + "reference": "7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/f56b220fe2db1ade4c88098d83413ebdfc3bf876", - "reference": "f56b220fe2db1ade4c88098d83413ebdfc3bf876", + "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67", + "reference": "7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67", "shasum": "" }, "require": { @@ -5602,7 +5957,7 @@ "xml" ], "support": { - "source": "https://github.com/spatie/array-to-xml/tree/3.3.0" + "source": "https://github.com/spatie/array-to-xml/tree/3.4.0" }, "funding": [ { @@ -5614,20 +5969,76 @@ "type": "github" } ], - "time": "2024-05-01T10:20:27+00:00" + "time": "2024-12-16T12:45:15+00:00" + }, + { + "name": "spiral/code-style", + "version": "v2.2.2", + "source": { + "type": "git", + "url": "https://github.com/spiral/code-style.git", + "reference": "3803c38baf6cda714e9ebbc7e515622b22ea798d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spiral/code-style/zipball/3803c38baf6cda714e9ebbc7e515622b22ea798d", + "reference": "3803c38baf6cda714e9ebbc7e515622b22ea798d", + "shasum": "" + }, + "require": { + "friendsofphp/php-cs-fixer": "^3.64", + "php": ">=8.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5", + "spiral/dumper": "^3.3", + "vimeo/psalm": "^5.26" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spiral\\CodeStyle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aleksandr Novikov", + "email": "aleksandr.novikov@spiralscout.com" + }, + { + "name": "Aleksei Gagarin", + "email": "alexey.gagarin@spiralscout.com" + } + ], + "description": "Code style and static analysis tools rulesets collection", + "homepage": "https://github.com/spiral/code-style", + "support": { + "source": "https://github.com/spiral/code-style/tree/v2.2.2" + }, + "funding": [ + { + "url": "https://github.com/sponsors/spiral", + "type": "github" + } + ], + "time": "2025-01-24T07:31:21+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.4.8", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b" + "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/8d7507f02b06e06815e56bb39aa0128e3806208b", - "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", + "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", "shasum": "" }, "require": { @@ -5678,7 +6089,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.8" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.13" }, "funding": [ { @@ -5694,20 +6105,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-09-25T14:18:03+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", - "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", "shasum": "" }, "require": { @@ -5716,12 +6127,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -5754,7 +6165,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" }, "funding": [ { @@ -5770,20 +6181,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/filesystem", - "version": "v6.4.9", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "b51ef8059159330b74a4d52f68e671033c0fe463" + "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/b51ef8059159330b74a4d52f68e671033c0fe463", - "reference": "b51ef8059159330b74a4d52f68e671033c0fe463", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/4856c9cf585d5a0313d8d35afd681a526f038dd3", + "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3", "shasum": "" }, "require": { @@ -5820,7 +6231,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.9" + "source": "https://github.com/symfony/filesystem/tree/v6.4.13" }, "funding": [ { @@ -5836,20 +6247,20 @@ "type": "tidelift" } ], - "time": "2024-06-28T09:49:33+00:00" + "time": "2024-10-25T15:07:50+00:00" }, { "name": "symfony/finder", - "version": "v6.4.10", + "version": "v6.4.17", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "af29198d87112bebdd397bd7735fbd115997824c" + "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/af29198d87112bebdd397bd7735fbd115997824c", - "reference": "af29198d87112bebdd397bd7735fbd115997824c", + "url": "https://api.github.com/repos/symfony/finder/zipball/1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", + "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", "shasum": "" }, "require": { @@ -5884,7 +6295,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.10" + "source": "https://github.com/symfony/finder/tree/v6.4.17" }, "funding": [ { @@ -5900,20 +6311,20 @@ "type": "tidelift" } ], - "time": "2024-07-24T07:06:38+00:00" + "time": "2024-12-29T13:51:37+00:00" }, { "name": "symfony/options-resolver", - "version": "v6.4.8", + "version": "v6.4.16", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "22ab9e9101ab18de37839074f8a1197f55590c1b" + "reference": "368128ad168f20e22c32159b9f761e456cec0c78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/22ab9e9101ab18de37839074f8a1197f55590c1b", - "reference": "22ab9e9101ab18de37839074f8a1197f55590c1b", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/368128ad168f20e22c32159b9f761e456cec0c78", + "reference": "368128ad168f20e22c32159b9f761e456cec0c78", "shasum": "" }, "require": { @@ -5951,7 +6362,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v6.4.8" + "source": "https://github.com/symfony/options-resolver/tree/v6.4.16" }, "funding": [ { @@ -5967,30 +6378,30 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-11-20T10:57:02+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -6031,7 +6442,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" }, "funding": [ { @@ -6047,30 +6458,30 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "3fb075789fb91f9ad9af537c4012d523085bd5af" + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/3fb075789fb91f9ad9af537c4012d523085bd5af", - "reference": "3fb075789fb91f9ad9af537c4012d523085bd5af", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -6107,7 +6518,83 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "e5493eb51311ab0b1cc2243416613f06ed8f18bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/e5493eb51311ab0b1cc2243416613f06ed8f18bd", + "reference": "e5493eb51311ab0b1cc2243416613f06ed8f18bd", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.31.0" }, "funding": [ { @@ -6123,20 +6610,20 @@ "type": "tidelift" } ], - "time": "2024-06-19T12:30:46+00:00" + "time": "2024-09-09T12:04:04+00:00" }, { "name": "symfony/process", - "version": "v6.4.8", + "version": "v6.4.20", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "8d92dd79149f29e89ee0f480254db595f6a6a2c5" + "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/8d92dd79149f29e89ee0f480254db595f6a6a2c5", - "reference": "8d92dd79149f29e89ee0f480254db595f6a6a2c5", + "url": "https://api.github.com/repos/symfony/process/zipball/e2a61c16af36c9a07e5c9906498b73e091949a20", + "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20", "shasum": "" }, "require": { @@ -6168,7 +6655,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.8" + "source": "https://github.com/symfony/process/tree/v6.4.20" }, "funding": [ { @@ -6184,20 +6671,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2025-03-10T17:11:00+00:00" }, { "name": "symfony/stopwatch", - "version": "v6.4.8", + "version": "v6.4.19", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "63e069eb616049632cde9674c46957819454b8aa" + "reference": "dfe1481c12c06266d0c3d58c0cb4b09bd497ab9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/63e069eb616049632cde9674c46957819454b8aa", - "reference": "63e069eb616049632cde9674c46957819454b8aa", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/dfe1481c12c06266d0c3d58c0cb4b09bd497ab9c", + "reference": "dfe1481c12c06266d0c3d58c0cb4b09bd497ab9c", "shasum": "" }, "require": { @@ -6230,7 +6717,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v6.4.8" + "source": "https://github.com/symfony/stopwatch/tree/v6.4.19" }, "funding": [ { @@ -6246,20 +6733,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2025-02-21T10:06:30+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.4.10", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "a71cc3374f5fb9759da1961d28c452373b343dd4" + "reference": "4ad10cf8b020e77ba665305bb7804389884b4837" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/a71cc3374f5fb9759da1961d28c452373b343dd4", - "reference": "a71cc3374f5fb9759da1961d28c452373b343dd4", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/4ad10cf8b020e77ba665305bb7804389884b4837", + "reference": "4ad10cf8b020e77ba665305bb7804389884b4837", "shasum": "" }, "require": { @@ -6315,7 +6802,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.10" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.18" }, "funding": [ { @@ -6331,7 +6818,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:30:32+00:00" + "time": "2025-01-17T11:26:11+00:00" }, { "name": "ta-tikoma/phpunit-architecture-test", @@ -6444,24 +6931,26 @@ }, { "name": "vimeo/psalm", - "version": "5.25.0", + "version": "6.10.0", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "01a8eb06b9e9cc6cfb6a320bf9fb14331919d505" + "reference": "9c0add4eb88d4b169ac04acb7c679918cbb9c252" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/01a8eb06b9e9cc6cfb6a320bf9fb14331919d505", - "reference": "01a8eb06b9e9cc6cfb6a320bf9fb14331919d505", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/9c0add4eb88d4b169ac04acb7c679918cbb9c252", + "reference": "9c0add4eb88d4b169ac04acb7c679918cbb9c252", "shasum": "" }, "require": { - "amphp/amp": "^2.4.2", - "amphp/byte-stream": "^1.5", + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/parallel": "^2.3", "composer-runtime-api": "^2", "composer/semver": "^1.4 || ^2.0 || ^3.0", "composer/xdebug-handler": "^2.0 || ^3.0", + "danog/advanced-json-rpc": "^3.1", "dnoegel/php-xdg-base-dir": "^0.1.1", "ext-ctype": "*", "ext-dom": "*", @@ -6470,27 +6959,26 @@ "ext-mbstring": "*", "ext-simplexml": "*", "ext-tokenizer": "*", - "felixfbecker/advanced-json-rpc": "^3.1", - "felixfbecker/language-server-protocol": "^1.5.2", + "felixfbecker/language-server-protocol": "^1.5.3", "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", - "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", - "nikic/php-parser": "^4.16", - "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", - "sebastian/diff": "^4.0 || ^5.0 || ^6.0", + "netresearch/jsonmapper": "^5.0", + "nikic/php-parser": "^5.0.0", + "php": "~8.1.31 || ~8.2.27 || ~8.3.16 || ~8.4.3", + "sebastian/diff": "^4.0 || ^5.0 || ^6.0 || ^7.0", "spatie/array-to-xml": "^2.17.0 || ^3.0", - "symfony/console": "^4.1.6 || ^5.0 || ^6.0 || ^7.0", - "symfony/filesystem": "^5.4 || ^6.0 || ^7.0" - }, - "conflict": { - "nikic/php-parser": "4.17.0" + "symfony/console": "^6.0 || ^7.0", + "symfony/filesystem": "~6.3.12 || ~6.4.3 || ^7.0.3", + "symfony/polyfill-php84": "^1.31.0" }, "provide": { "psalm/psalm": "self.version" }, "require-dev": { - "amphp/phpunit-util": "^2.0", + "amphp/phpunit-util": "^3", "bamarni/composer-bin-plugin": "^1.4", "brianium/paratest": "^6.9", + "danog/class-finder": "^0.4.8", + "dg/bypass-finals": "^1.5", "ext-curl": "*", "mockery/mockery": "^1.5", "nunomaduro/mock-final-classes": "^1.1", @@ -6498,10 +6986,10 @@ "phpstan/phpdoc-parser": "^1.6", "phpunit/phpunit": "^9.6", "psalm/plugin-mockery": "^1.1", - "psalm/plugin-phpunit": "^0.18", + "psalm/plugin-phpunit": "^0.19", "slevomat/coding-standard": "^8.4", "squizlabs/php_codesniffer": "^3.6", - "symfony/process": "^4.4 || ^5.0 || ^6.0 || ^7.0" + "symfony/process": "^6.0 || ^7.0" }, "suggest": { "ext-curl": "In order to send data to shepherd", @@ -6512,16 +7000,19 @@ "psalm-language-server", "psalm-plugin", "psalm-refactor", + "psalm-review", "psalter" ], "type": "project", "extra": { "branch-alias": { - "dev-master": "5.x-dev", - "dev-4.x": "4.x-dev", - "dev-3.x": "3.x-dev", + "dev-1.x": "1.x-dev", "dev-2.x": "2.x-dev", - "dev-1.x": "1.x-dev" + "dev-3.x": "3.x-dev", + "dev-4.x": "4.x-dev", + "dev-5.x": "5.x-dev", + "dev-6.x": "6.x-dev", + "dev-master": "7.x-dev" } }, "autoload": { @@ -6536,6 +7027,10 @@ "authors": [ { "name": "Matthew Brown" + }, + { + "name": "Daniil Gentili", + "email": "daniil@daniil.it" } ], "description": "A static analysis tool for finding errors in PHP applications", @@ -6550,87 +7045,7 @@ "issues": "https://github.com/vimeo/psalm/issues", "source": "https://github.com/vimeo/psalm" }, - "time": "2024-06-16T15:08:35+00:00" - }, - { - "name": "wayofdev/cs-fixer-config", - "version": "v1.5.3", - "source": { - "type": "git", - "url": "https://github.com/wayofdev/php-cs-fixer-config.git", - "reference": "aa0aae244772672f617de6e74d85ff81532f7e82" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/wayofdev/php-cs-fixer-config/zipball/aa0aae244772672f617de6e74d85ff81532f7e82", - "reference": "aa0aae244772672f617de6e74d85ff81532f7e82", - "shasum": "" - }, - "require": { - "friendsofphp/php-cs-fixer": "^3.57", - "php": "^8.1" - }, - "require-dev": { - "ergebnis/phpunit-slow-test-detector": "^2.14", - "pestphp/pest": "^2.34", - "phpstan/extension-installer": "^1.3", - "phpstan/phpstan": "^1.11", - "phpstan/phpstan-deprecation-rules": "^1.2", - "phpstan/phpstan-phpunit": "^1.4", - "phpstan/phpstan-strict-rules": "^1.6", - "phpunit/phpunit": "^10.5", - "psalm/plugin-phpunit": "^0.19", - "rector/rector": "^1.1", - "roave/infection-static-analysis-plugin": "^1.35", - "vimeo/psalm": "^5.24" - }, - "type": "library", - "extra": { - "composer-normalize": { - "indent-size": 4, - "indent-style": "space" - } - }, - "autoload": { - "psr-4": { - "WayOfDev\\PhpCsFixer\\Config\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Andrij Orlenko", - "email": "the@wayof.dev" - } - ], - "description": "๐Ÿงน Adds custom rule-sets to PHP CS Fixer for consistent coding standards.", - "homepage": "https://wayof.dev", - "keywords": [ - "code-quality", - "code-standards", - "code-style", - "configuration", - "php", - "php-cs-fixer", - "php-cs-fixer-config", - "php-cs-fixer-rules", - "static-analysis" - ], - "support": { - "issues": "https://github.com/wayofdev/php-cs-fixer-config/issues", - "security": "https://github.com/wayofdev/php-cs-fixer-config/blob/master/.github/SECURITY.md", - "source": "https://github.com/wayofdev/php-cs-fixer-config" - }, - "funding": [ - { - "url": "https://github.com/wayofdev", - "type": "github" - } - ], - "time": "2024-06-18T09:13:20+00:00" + "time": "2025-03-31T10:12:50+00:00" }, { "name": "webmozart/assert", @@ -6693,15 +7108,12 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": ">=8.1" }, - "platform-dev": [], - "platform-overrides": { - "php": "8.1.27" - }, + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/context.yaml b/context.yaml new file mode 100644 index 0000000..765edc3 --- /dev/null +++ b/context.yaml @@ -0,0 +1,53 @@ +--- + +$schema: 'https://raw.githubusercontent.com/context-hub/generator/refs/heads/main/json-schema.json' + +import: + - path: resources/prompts.yaml + +documents: + # Project structure overview + - description: 'Project structure overview' + outputPath: project-structure.md + overwrite: true + sources: + - type: text + content: | + The PSR-4 is used in the project. + - type: tree + sourcePaths: [ 'src' ] + showCharCount: true + showSize: true + - type: file + sourcePaths: [ 'README.md' ] + + # Modules API + - description: 'Modules API allowed to be used in the project' + outputPath: modules-api.md + overwrite: true + sources: + - type: file + sourcePaths: [ 'src/Module', 'src/Service' ] + filePattern: '*.php' + excludePatterns: [ '/Internal/' ] + modifiers: + - name: php-content-filter + options: + method_visibility: + - public + keep_method_bodies: false + + # Guidelines + - description: 'Guidelines and instructions' + outputPath: guidelines.md + overwrite: true + sources: + - type: text + tag: instruction + content: | + There are all the guidelines about how to do some things in the project. + Feel free to load any related guideline to the current context to make the work more efficient. + - type: tree + sourcePaths: 'docs/guidelines' + showCharCount: true + showSize: true diff --git a/docs/guidelines/how-to-write-tests.md b/docs/guidelines/how-to-write-tests.md new file mode 100644 index 0000000..51c336a --- /dev/null +++ b/docs/guidelines/how-to-write-tests.md @@ -0,0 +1,407 @@ +# PHP Unit Testing Guidelines + +This document outlines the standards and best practices for writing unit tests for the project. + +## Test Structure + +- Unit tests should be in `tests/Unit`, integration tests in `tests/Integration`, acceptance tests in `tests/Acceptance`, architecture tests in `tests/Arch`, etc. +- Tests should be organized to mirror the project structure. +- Each concrete PHP class should have a corresponding test class +- Place tests in the appropriate namespace matching the source code structure +- Use `final` keyword for test classes + +``` +Source: src/ExternalContext/LocalExternalContextSource.php +Test: tests/Unit/ExternalContext/LocalExternalContextSourceTest.php +``` + +## What to Test + +- **Test Concrete Implementations, Not Interfaces**: Interfaces define contracts but don't contain actual logic to test. Only test concrete implementations of interfaces. +- **Focus on Behavior**: Test the behavior of classes rather than their internal implementation details. +- **Test Public API**: Focus on testing the public methods and functionality that clients of the class will use. +- **Test Edge Cases**: Include tests for boundary conditions, invalid inputs, and error scenarios. + +## Module Testing + +Modules located in `src/Module` are treated as independent units with their own test structure: + +- Each module should have its own test directory with the following structure: + ``` + tests/Unit/Module/{ModuleName}/ + โ”œโ”€โ”€ Stub/ (Contains stubs for the module's dependencies) + โ””โ”€โ”€ Internal/ (Tests for module's internal implementations) + ``` + +- Internal implementations of a module are located in the `Internal` folder of each module +- Tests for public classes of the module should be placed directly in the module's test directory corresponding to the source code structure +- Each module's tests should be structured as independent areas with their own stubs + +## Arrange-Act-Assert (AAA) Pattern + +All tests should follow the AAA pattern: + +1. **Arrange**: Set up the test environment and prepare inputs +2. **Act**: Execute the code being tested +3. **Assert**: Verify the results are as expected + +Use comments to separate these sections for clarity: + +```php +public function testFetchContextReturnsDecodedJson(): void +{ + // Arrange + $filePath = '/path/to/context.json'; + $fileContent = '{"key":"value"}'; + $this->fileSystem->method('exists')->willReturn(true); + $this->fileSystem->method('readFile')->willReturn($fileContent); + + // Act + $result = $this->contextSource->fetchContext($filePath); + + // Assert + self::assertSame(['key' => 'value'], $result); +} +``` + +## Naming Conventions + +- Test classes should be named with the pattern `{ClassUnderTest}Test` +- Test methods should follow the pattern `test{MethodName}{Scenario}` +- Use descriptive method names that explain what is being tested + +```php +#[CoversClass(LocalExternalContextSource::class)] +final class LocalExternalContextSourceTest extends TestCase +{ + public function testFetchContextReturnsValidData(): void + { + // Test implementation + } + + public function testFetchContextThrowsExceptionWhenFileNotFound(): void + { + // Test implementation + } +} +``` + +## Test Implementation + +- Use strict typing: `declare(strict_types=1);` +- Use namespaces consistent with the project structure +- Extend `PHPUnit\Framework\TestCase` +- Use assertion methods with descriptive error messages +- Test one behavior per test method +- Use data providers with PHP 8.1+ attributes and generators + +```php +#[DataProvider('provideValidFilePaths')] +public function testFetchContextWithVariousPaths(string $path, array $expectedData): void +{ + // Arrange + $this->fileSystem->method('exists')->willReturn(true); + $this->fileSystem->method('readFile')->willReturn(\json_encode($expectedData)); + + // Act + $result = $this->contextSource->fetchContext($path); + + // Assert + self::assertSame($expectedData, $result); +} + +public static function provideValidFilePaths(): Generator +{ + yield 'relative path' => ['relative/path.json', ['expected' => 'data']]; + yield 'absolute path' => ['/absolute/path.json', ['expected' => 'data']]; +} +``` + +## Test Isolation + +- Each test should be independent of others +- Use setUp() and tearDown() methods for common test preparation and cleanup +- Use test doubles (mocks, stubs) for isolating the code under test from dependencies +- Reset global/static state between tests +- For module tests, use the dedicated Stub directory to store all stub implementations + +```php +protected function setUp(): void +{ + // Arrange (common setup) + $this->fileSystem = $this->createMock(FileSystemInterface::class); + $this->contextSource = new LocalExternalContextSource($this->fileSystem); +} +``` + +### Module-Specific Test Isolation + +When testing modules from `src/Module`: + +- Module tests should use stubs from their dedicated `Stub` directory +- Tests should only rely on the public interfaces of the module, not internal implementations +- Internal tests can have additional stubs specific to internal components +- Cross-module dependencies should be stubbed if possible (for interfaces), treating each module as an independent unit + +```php +// Example of module test setup with stubs +namespace Tests\Unit\Module\Payment; + +use Tests\Unit\Module\Payment\Stub\PaymentGatewayStub; +use Tests\Unit\Module\Payment\Stub\LoggerStub; + +final class PaymentProcessorTest extends TestCase +{ + private PaymentGatewayStub $paymentGateway; + private LoggerStub $logger; + + protected function setUp(): void + { + $this->paymentGateway = new PaymentGatewayStub(); + $this->logger = new LoggerStub(); + $this->processor = new PaymentProcessor($this->paymentGateway, $this->logger); + } +} +``` + +## Assertions + +- Use specific assertion methods instead of generic ones +- Provide meaningful failure messages in assertions +- Test both positive and negative scenarios +- Assert state changes and side effects, not just return values + +```php +// Good +self::assertSame('expected', $actual, 'Context data should match the expected format'); + +// Instead of +self::assertTrue($expected === $actual); +``` + +## Error Handling Tests + +When testing exceptions, the AAA pattern is slightly modified: + +```php +public function testFetchContextThrowsExceptionWhenFileNotFound(): void +{ + // Arrange + $filePath = '/non-existent/path.json'; + $this->fileSystem->method('exists')->willReturn(false); + + // Assert (before Act for exceptions) + $this->expectException(ContextSourceException::class); + $this->expectExceptionMessage('Cannot read context from file'); + + // Act + $this->contextSource->fetchContext($filePath); +} +``` + +## Mock Objects + +- Only mock direct dependencies of the class under test +- Mock only what is necessary for the test +- **Never mock final classes** - instead use real instances or create test alternatives +- Prefer typed mock method returns +- Verify critical interactions with mocks + +```php +// Arrange +$this->fileSystem->expects(self::once()) + ->method('readFile') + ->with('/path/to/file.json') + ->willReturn('{"key": "value"}'); +``` + +### Dealing with Final Classes + +When a class under test depends on a final class: + +1. **Use real instances** when possible +2. **Create test doubles** by implementing the same interface +3. **Use wrapper/adapter pattern** to create a non-final class that delegates to the final class +4. **Refactor dependencies** to use interfaces where appropriate + +```php +// Instead of mocking a final class: +final class FileReader +{ + public function readFile(string $path): string {...} +} + +// Create an interface: +interface FileReaderInterface +{ + public function readFile(string $path): string; +} + +// Create a test implementation: +class TestFileReader implements FileReaderInterface +{ + public function readFile(string $path): string + { + return '{"test":"data"}'; + } +} + +// Use in tests: +$fileReader = new TestFileReader(); +$myService = new MyService($fileReader); +``` + +## Additional Modern PHPUnit Features + +### PHP 8.1+ Attributes + +Replace annotations with attributes throughout your tests: + +- `#[CoversClass(ClassUnderTest::class)]` - Specify which class is being tested +- `#[CoversMethod('methodName')]` - Specify which method is being tested +- `#[DataProvider('provideTestData')]` - Link to data provider method +- `#[Group('slow')]` - Categorize tests +- `#[TestDox('Class should handle errors gracefully')]` - Better test documentation + +### Using depends with Attributes + +```php +public function testBasicFunctionality(): SomeClass +{ + // Arrange + $object = new SomeClass(); + + // Act & Assert + self::assertInstanceOf(SomeClass::class, $object); + return $object; +} + +#[Depends('testBasicFunctionality')] +public function testAdvancedFeature(SomeClass $object): void +{ + // Arrange is handled by the dependency + + // Act + $result = $object->advancedMethod(); + + // Assert + self::assertTrue($result); +} +``` + +### Test Extension with Traits + +Use traits to share test functionality: + +```php +trait CreatesTempFilesTrait +{ + private string $tempFilePath; + + protected function createTempFile(string $content): string + { + // Arrange test environment + $this->tempFilePath = sys_get_temp_dir() . '/' . uniqid('test_', true); + file_put_contents($this->tempFilePath, $content); + return $this->tempFilePath; + } + + protected function tearDown(): void + { + // Clean up test environment + if (isset($this->tempFilePath) && file_exists($this->tempFilePath)) { + unlink($this->tempFilePath); + } + parent::tearDown(); + } +} + +final class MyTest extends TestCase +{ + use CreatesTempFilesTrait; + + public function testFileProcessing(): void + { + // Arrange + $path = $this->createTempFile('{"data":"value"}'); + + // Act + $result = $this->processor->process($path); + + // Assert + self::assertNotEmpty($result); + } +} +``` + +## Example Test Class + +```php +fileSystem = $this->createMock(FileSystem::class); + $this->contextSource = new LocalExternalContextSource($this->fileSystem); + } + + public function testFetchContextReturnsDecodedJsonWhenFileExists(): void + { + // Arrange + $filePath = '/path/to/context.json'; + $fileContent = '{"key":"value","nested":{"data":true}}'; + $expectedData = ['key' => 'value', 'nested' => ['data' => true]]; + + $this->fileSystem->method('exists')->with($filePath)->willReturn(true); + $this->fileSystem->method('readFile')->with($filePath)->willReturn($fileContent); + + // Act + $result = $this->contextSource->fetchContext($filePath); + + // Assert + self::assertSame($expectedData, $result); + } + + #[DataProvider('provideInvalidPaths')] + public function testFetchContextThrowsExceptionForInvalidPaths( + string $path, + string $expectedExceptionMessage + ): void { + // Arrange + $this->fileSystem->method('exists')->willReturn(false); + + // Assert (before Act for exceptions) + $this->expectException(ContextSourceException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + + // Act + $this->contextSource->fetchContext($path); + } + + public static function provideInvalidPaths(): Generator + { + yield 'empty path' => ['', 'Cannot read context from empty path']; + yield 'non-existent file' => ['/missing.json', 'File not found: /missing.json']; + } +} +``` diff --git a/pest.xml.dist b/pest.xml.dist deleted file mode 100644 index 2d40f52..0000000 --- a/pest.xml.dist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - tests/Arch - - - - - src - - - tests - src/Test - - - diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 447a7e3..cb41381 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -3,30 +3,34 @@ xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" - cacheResultFile=".build/phpunit/result.cache" + cacheResultFile="runtime/phpunit/result.cache" failOnWarning="true" failOnRisky="true" executionOrder="random" stderr="true" beStrictAboutOutputDuringTests="true" + displayDetailsOnTestsThatTriggerWarnings="true" > - - - tests/Unit + + tests/Integration + + + tests/Arch + - - - + + + - + diff --git a/psalm-baseline.xml b/psalm-baseline.xml index f03abfa..9bdfa79 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,9 +1,13 @@ - + + + getArguments()]]> + getOptions()]]> + @@ -37,31 +41,21 @@ - - - - - type]]> - uri]]> - - - repositories]]> - homepage]]> - pattern]]> - - files]]> - + + + + @@ -87,34 +81,10 @@ - - - - File::fromArray($fileArray), - $softwareArray['files'] ?? [], - )]]> - Repository::fromArray($repositoryArray), - $softwareArray['repositories'] ?? [], - )]]> - - - - - - - - factory]]> - - - - - class()]]> @@ -123,33 +93,59 @@ + + + + + + + + + factory]]> + + + + + + + cache]]> + + + + + + cache]]> + + + + + - repoConfig->assetPattern]]> + + - - - + + + - - - + + + + ($context->onProgress)(]]> - - repoConfig]]> - - - repoConfig]]> - + + + @@ -170,6 +166,18 @@ + + + + + + + repositories as $repository) { + yield from $repository->getReleases(); + } + }]]> + @@ -183,16 +191,45 @@ + + + ]]> + + + + + + + + static::create($items())]]> + + + + + + + - + + + + + + + + + + + items instanceof CachedGenerator + ? $this->items->first() + : ($this->items === [] ? null : $this->items[\array_key_first($this->items)])]]> + - items, $callback))]]> - items, $filter))]]> - items))]]> - + + @@ -206,6 +243,9 @@ createClient())]]> + + + @@ -240,6 +280,12 @@ + @@ -271,12 +317,15 @@ + + + - client, $data)]]> + client, $releaseData)]]> - client, $data)]]> + client, $releaseData)]]> @@ -288,15 +337,14 @@ name]]> releases]]> - - - - - getName())]]> - - - - + + + + + + + valid() ? $loader->current() : []]]> + @@ -314,22 +362,9 @@ - - - uri]]> - - - - - - - - - - diff --git a/psalm.xml b/psalm.xml index 61df208..48f9878 100644 --- a/psalm.xml +++ b/psalm.xml @@ -6,10 +6,12 @@ findUnusedBaselineEntry="false" findUnusedCode="false" errorBaseline="psalm-baseline.xml" + phpVersion="8.1" > + @@ -18,4 +20,15 @@ + + + + + + + + + + + diff --git a/resources/prompts.yaml b/resources/prompts.yaml new file mode 100644 index 0000000..d75c488 --- /dev/null +++ b/resources/prompts.yaml @@ -0,0 +1,6 @@ +--- + +$schema: 'https://raw.githubusercontent.com/context-hub/generator/refs/heads/main/json-schema.json' + +import: + - path: ./prompts/cover-class-by-docs.yaml diff --git a/resources/prompts/cover-class-by-docs.yaml b/resources/prompts/cover-class-by-docs.yaml new file mode 100644 index 0000000..ae47518 --- /dev/null +++ b/resources/prompts/cover-class-by-docs.yaml @@ -0,0 +1,57 @@ +--- + +$schema: 'https://raw.githubusercontent.com/context-hub/generator/refs/heads/main/json-schema.json' + +prompts: + - id: cover-class-by-docs + description: Generate documentation for a specific class in the project + schema: + properties: + class-name: + description: Class name to cover with docs + required: + - class-name + + messages: + - role: user + content: | + Read {{class-name}} class(es) or interface(s). + + Cover it with comments using following important instructions: + - Don't break signatures. You can only add or improve comments. + - Don't remove existing @internal, @link, @see, @api, or @deprecated annotations. + - Use passive forms instead of we or you. + - Separate title and content with empty line. + - Use @link annotation if it's necessary to write some URL. + - Use @see annotation if it's necessary to reference to a class or method. + - Use extended types like class-string, non-empty-string, non-empty-array, non-empty-list, etc, if it's logically correct. + - Don't use @inheritDoc annotations. + - Keep docs clean: + - If the method has the same signature in the parent, don't comment the implementation if there is no additional things. + - Keep only essential and non-obvious documentation. It's important to avoid redundancy. + - remove trailing spaces even in empty comment lines + - Use generic annotations if possible based on the logic. For example for a `count()` has `int<0, max>` return type because the amount of items can't be negative. + - Use inline annotations like {@see ClassName} if it needed to mention in a text block. + - Add class or method usage example using markdown like. + Note that there is leading space before inside the code block: + ```php + // Comment for the following code + some code + ``` + If commenting a console command, feel fre to provide a usage example in the form of a command line: + ```bash + # Comment for the following command + some command + ``` + Don't skip internal classes. + - Inner comments order: title, description, example, template annotations, params and return annotations, references and internal/deprecated annotations. + - Property comments rules: If it possible, write inline comment without title like `/** @var non-empty-string $name User name */`. + - Break long lines into multiple lines. Maximum line length is 120 characters. + Multiline comments rules: if a comment starts with an annotation, the second line should start with whitespaces aligned with the annotation length: + ```php + /** + * @var non-empty-string $name Comment on the first line + * comment on the second line... + * comment on the third line + */ + ``` diff --git a/src/Bootstrap.php b/src/Bootstrap.php index 9745f6b..3ca60a9 100644 --- a/src/Bootstrap.php +++ b/src/Bootstrap.php @@ -6,12 +6,28 @@ use Internal\DLoad\Module\Common\Architecture; use Internal\DLoad\Module\Common\Internal\Injection\ConfigLoader; +use Internal\DLoad\Module\Common\Internal\ObjectContainer; use Internal\DLoad\Module\Common\OperatingSystem; use Internal\DLoad\Module\Common\Stability; +use Internal\DLoad\Module\Repository\Internal\GitHub\Factory as GithubRepositoryFactory; +use Internal\DLoad\Module\Repository\RepositoryProvider; use Internal\DLoad\Service\Container; /** - * Build the container based on the configuration. + * Bootstraps the application by configuring the dependency container. + * + * Initializes the application container with configuration values and core services. + * Serves as the entry point for the dependency injection setup. + * + * ```php + * // Initialize application with default container and XML config + * $container = Bootstrap::init() + * ->withConfig('dload.xml', $inputOptions, $inputArguments) + * ->finish(); + * + * // Use container to access services + * $downloader = $container->get(Downloader::class); + * ``` * * @internal */ @@ -21,11 +37,22 @@ private function __construct( private Container $container, ) {} - public static function init(Container $container = new Module\Common\Internal\Container()): self + /** + * Creates a new bootstrap instance with the specified container. + * + * @param Container $container Dependency injection container (defaults to ObjectContainer) + * @return self Bootstrap instance + */ + public static function init(Container $container = new ObjectContainer()): self { return new self($container); } + /** + * Finalizes the bootstrap process and returns the configured container. + * + * @return Container Fully configured dependency container + */ public function finish(): Container { $c = $this->container; @@ -35,7 +62,18 @@ public function finish(): Container } /** - * @param non-empty-string|null $xml File or XML content + * Configures the container with XML configuration and input values. + * + * Registers core services and bindings for system architecture, OS detection, + * and stability settings. + * + * @param non-empty-string|null $xml Path to XML file or raw XML content + * @param array $inputOptions Command-line options + * @param array $inputArguments Command-line arguments + * @param array $environment Environment variables + * @return self Configured bootstrap instance + * @throws \InvalidArgumentException When config file is not found + * @throws \RuntimeException When config file cannot be read */ public function withConfig( ?string $xml = null, @@ -57,10 +95,23 @@ public function withConfig( $this->container->bind(Architecture::class); $this->container->bind(OperatingSystem::class); $this->container->bind(Stability::class); + $this->container->bind( + RepositoryProvider::class, + static fn(Container $container): RepositoryProvider => (new RepositoryProvider()) + ->addRepositoryFactory($container->get(GithubRepositoryFactory::class)), + ); return $this; } + /** + * Reads XML configuration from file or direct content string. + * + * @param non-empty-string $fileOrContent Path to XML file or raw XML content + * @return string Parsed XML content + * @throws \InvalidArgumentException When config file is not found + * @throws \RuntimeException When config file cannot be read + */ private function readXml(string $fileOrContent): string { // Load content diff --git a/src/Command/Base.php b/src/Command/Base.php index 57fd5ee..c6c7a54 100644 --- a/src/Command/Base.php +++ b/src/Command/Base.php @@ -15,20 +15,55 @@ use Symfony\Component\Console\Style\SymfonyStyle; /** + * Base abstract class for all DLoad commands. + * + * Provides common functionality for command initialization, container setup, + * and configuration handling. + * + * ```php + * // Extend to create a custom command + * final class CustomCommand extends Base + * { + * protected function execute(InputInterface $input, OutputInterface $output): int + * { + * parent::execute($input, $output); + * // Command implementation + * return Command::SUCCESS; + * } + * } + * ``` + * * @internal */ abstract class Base extends Command { + /** @var Logger Service for logging command execution */ protected Logger $logger; + /** @var Container IoC container with services */ protected Container $container; + /** + * Configures command options. + * + * Adds option for specifying configuration file location. + */ public function configure(): void { parent::configure(); $this->addOption('config', null, InputOption::VALUE_OPTIONAL, 'Path to the configuration file'); } + /** + * Initializes the command execution environment. + * + * Sets up logger, container, and registers input/output in the container. + * + * @param InputInterface $input Command input + * @param OutputInterface $output Command output + * + * @return int Command success code + */ protected function execute( InputInterface $input, OutputInterface $output, @@ -50,6 +85,10 @@ protected function execute( } /** + * Resolves configuration file path from input or default location. + * + * @param InputInterface $input Command input + * * @return non-empty-string|null Path to the configuration file */ private function getConfigFile(InputInterface $input): ?string diff --git a/src/Command/Get.php b/src/Command/Get.php index 5f522db..63c0e59 100644 --- a/src/Command/Get.php +++ b/src/Command/Get.php @@ -18,6 +18,26 @@ use Symfony\Component\Console\Output\OutputInterface; /** + * Fetches software packages based on command arguments or configuration. + * + * Downloads software binaries from configured repositories with proper + * system/architecture detection. Can work with both CLI arguments and + * configuration file definitions. + * + * ```bash + * # Download single software + * ./vendor/bin/dload get rr + * + * # Download specific version of software + * ./vendor/bin/dload get rr --stability=beta + * + * # Download multiple software packages + * ./vendor/bin/dload get rr dolt temporal + * + * # Download software defined in config file + * ./vendor/bin/dload get --config=./dload.xml + * ``` + * * @internal */ #[AsCommand( @@ -26,18 +46,39 @@ )] final class Get extends Base { + /** @var string Argument name for software identifiers */ private const ARG_SOFTWARE = 'software'; + /** + * Configures command arguments and options. + */ public function configure(): void { parent::configure(); - $this->addArgument(self::ARG_SOFTWARE, InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Software name, e.g. "rr", "dolt", "temporal" etc.'); + $this->addArgument( + self::ARG_SOFTWARE, + InputArgument::OPTIONAL | InputArgument::IS_ARRAY, + 'Software name, e.g. "rr", "dolt", "temporal" etc.', + ); $this->addOption('path', null, InputOption::VALUE_OPTIONAL, 'Path to store the binary, e.g. "./bin"', "."); $this->addOption('arch', null, InputOption::VALUE_OPTIONAL, 'Architecture, e.g. "amd64", "arm64" etc.'); $this->addOption('os', null, InputOption::VALUE_OPTIONAL, 'Operating system, e.g. "linux", "darwin" etc.'); $this->addOption('stability', null, InputOption::VALUE_OPTIONAL, 'Stability, e.g. "stable", "beta" etc.'); } + /** + * Executes the command to download specified software. + * + * Determines system parameters, resolves download actions based on input, + * and runs the download tasks. + * + * @param InputInterface $input Command input + * @param OutputInterface $output Command output + * + * @return int Command result code + * + * @throws \RuntimeException When no software is specified to download + */ protected function execute(InputInterface $input, OutputInterface $output): int { parent::execute($input, $output); @@ -65,7 +106,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int } /** - * @return list + * Resolves download actions from input arguments or config. + * + * Uses explicitly specified software names from CLI arguments if present, + * otherwise falls back to configuration file definitions. + * + * @param InputInterface $input Command input + * @param Actions $actionsConfig Available actions from config + * + * @return list List of download configurations to process */ private function getDownloadActions(InputInterface $input, Actions $actionsConfig): array { diff --git a/src/Command/ListSoftware.php b/src/Command/ListSoftware.php index f2a211f..275b79d 100644 --- a/src/Command/ListSoftware.php +++ b/src/Command/ListSoftware.php @@ -12,6 +12,19 @@ use Symfony\Component\Console\Output\OutputInterface; /** + * Displays a list of available software packages. + * + * Shows all registered software packages with their IDs, names, + * repository information, and descriptions. + * + * ```bash + * # List all available software packages + * ./vendor/bin/dload software + * + * # List software with custom configuration + * ./vendor/bin/dload software --config=./custom-dload.xml + * ``` + * * @internal */ #[AsCommand( @@ -20,6 +33,17 @@ )] final class ListSoftware extends Base { + /** + * Lists all available software packages in a formatted output. + * + * Displays the software ID, name, homepage (if available), + * repository information, and description for each registered software. + * + * @param InputInterface $input Command input + * @param OutputInterface $output Command output + * + * @return int Command result code + */ protected function execute( InputInterface $input, OutputInterface $output, diff --git a/src/DLoad.php b/src/DLoad.php index cabd90a..cf48c24 100644 --- a/src/DLoad.php +++ b/src/DLoad.php @@ -22,12 +22,22 @@ use function React\Promise\resolve; /** - * To have a short syntax. + * Main application facade providing simplified access to download functionality. + * + * Acts as a high-level interface for downloading and extracting software packages + * based on configuration actions. + * + * ```php + * $dload = $container->get(DLoad::class); + * $dload->addTask(new DownloadConfig('rr', '^2.12.0')); + * $dload->run(); + * ``` * * @internal */ final class DLoad { + /** @var bool Flag to use mock data instead of actual downloads for testing */ public bool $useMock = false; public function __construct( @@ -41,6 +51,14 @@ public function __construct( private readonly StyleInterface $io, ) {} + /** + * Adds a download task to the execution queue. + * + * Creates and schedules a task to download and extract a software package based on the provided action. + * + * @param DownloadConfig $action Download configuration action + * @throws \RuntimeException When software package is not found + */ public function addTask(DownloadConfig $action): void { $this->taskManager->addTask(function () use ($action): void { @@ -57,11 +75,25 @@ public function addTask(DownloadConfig $action): void }); } + /** + * Executes all queued download tasks. + * + * Processes all scheduled tasks sequentially until completion. + */ public function run(): void { $this->taskManager->await(); } + /** + * Creates a download task for the specified software package. + * + * Either uses a mock task (for testing) or creates a real download task. + * + * @param Software $software Software package configuration + * @param DownloadConfig $action Download action configuration + * @return DownloadTask Task object for downloading the specified software + */ private function prepareDownloadTask(Software $software, DownloadConfig $action): DownloadTask { return $this->useMock @@ -79,7 +111,10 @@ private function prepareDownloadTask(Software $software, DownloadConfig $action) } /** - * @return \Closure(DownloadResult): void + * Creates a closure to handle extraction of downloaded files. + * + * @param Software $software Software package configuration + * @return \Closure(DownloadResult): void Function that extracts files from the downloaded archive */ private function prepareExtractTask(Software $software): \Closure { @@ -117,7 +152,10 @@ private function prepareExtractTask(Software $software): \Closure } /** - * @return bool True if the file should be extracted, false otherwise. + * Checks if a file already exists and prompts for confirmation to overwrite. + * + * @param \SplFileInfo $bin Target file information + * @return bool True if the file should be extracted, false otherwise */ private function checkExisting(\SplFileInfo $bin): bool { @@ -133,7 +171,11 @@ private function checkExisting(\SplFileInfo $bin): bool } /** - * @param list $mapping + * Determines the target path for an extracted file based on file mapping configurations. + * + * @param \SplFileInfo $source Source file from the archive + * @param list $mapping File mapping configurations + * @return \SplFileInfo|null Target file path or null if file should not be extracted */ private function shouldBeExtracted(\SplFileInfo $source, array $mapping): ?\SplFileInfo { @@ -141,7 +183,7 @@ private function shouldBeExtracted(\SplFileInfo $source, array $mapping): ?\SplF foreach ($mapping as $conf) { if (\preg_match($conf->pattern, $source->getFilename())) { - $newName = match(true) { + $newName = match (true) { $conf->rename === null => $source->getFilename(), $source->getExtension() === '' => $conf->rename, default => $conf->rename . '.' . $source->getExtension(), diff --git a/src/Info.php b/src/Info.php index 4983905..7719c1c 100644 --- a/src/Info.php +++ b/src/Info.php @@ -7,14 +7,11 @@ /** * @internal */ -class Info +final class Info { public const NAME = 'DLoad'; - public const LOGO_CLI_COLOR = ''; - public const ROOT_DIR = __DIR__ . '/..'; - private const VERSION = 'experimental'; /** diff --git a/src/Module/Archive/Archive.php b/src/Module/Archive/Archive.php index 0965fb8..6d0a078 100644 --- a/src/Module/Archive/Archive.php +++ b/src/Module/Archive/Archive.php @@ -4,13 +4,35 @@ namespace Internal\DLoad\Module\Archive; +use Internal\DLoad\Module\Archive\Exception\ArchiveException; + +/** + * Archive extraction interface + * + * Provides methods to extract contents from archive files. + */ interface Archive { /** - * Iterate archive files. If a {@see \SplFileInfo} is backed into the generator, the file will be + * Iterate through archive files and extract them + * + * Iterates through all files in the archive and yields {@see \SplFileInfo} objects. + * If a {@see \SplFileInfo} is yielded back into the generator, the file will be * extracted to the given location. * + * ```php + * // Extract only specific files + * $archive = $factory->create(new \SplFileInfo('archive.zip')); + * foreach ($archive->extract() as $path => $fileInfo) { + * if (str_ends_with($path, '.php')) { + * // Extract PHP files to a specific directory + * yield new \SplFileInfo('/path/to/extract/' . basename($path)); + * } + * } + * ``` + * * @return \Generator + * @throws ArchiveException */ public function extract(): \Generator; } diff --git a/src/Module/Archive/ArchiveFactory.php b/src/Module/Archive/ArchiveFactory.php index d645b2c..dbbf0f0 100644 --- a/src/Module/Archive/ArchiveFactory.php +++ b/src/Module/Archive/ArchiveFactory.php @@ -10,20 +10,30 @@ use Internal\DLoad\Module\Archive\Internal\ZipPharArchive; /** + * Factory for creating archive handlers + * + * Creates appropriate archive handlers based on file extension or custom matchers. + * + * ```php + * $factory = new ArchiveFactory(); + * $archive = $factory->create(new \SplFileInfo('archive.zip')); + * foreach ($archive->extract() as $path => $fileInfo) { + * // Process extracted files + * } + * ``` + * * @psalm-type ArchiveMatcher = \Closure(\SplFileInfo): ?Archive */ final class ArchiveFactory { - /** @var list */ + /** @var list List of supported file extensions */ private array $extensions = []; - /** - * @var array - */ + /** @var array List of archive type matchers */ private array $matchers = []; /** - * FactoryTrait constructor. + * Creates factory with default archive type handlers */ public function __construct() { @@ -31,6 +41,20 @@ public function __construct() } /** + * Extends factory with custom archive matcher + * + * Adds a custom matcher to the beginning of the matchers list. + * + * ```php + * $factory->extend( + * fn(\SplFileInfo $file) => str_ends_with($file->getFilename(), '.rar') + * ? new RarArchive($file) + * : null, + * ['rar'] + * ); + * ``` + * + * @param \Closure $matcher Function that creates archive handler or returns null * @param list $extensions List of supported extensions */ public function extend(\Closure $matcher, array $extensions = []): void @@ -39,6 +63,13 @@ public function extend(\Closure $matcher, array $extensions = []): void $this->extensions = \array_unique(\array_merge($this->extensions, $extensions)); } + /** + * Creates archive handler for the given file + * + * @param \SplFileInfo $file Archive file + * @return Archive Archive handler + * @throws \InvalidArgumentException When no suitable archive handler found + */ public function create(\SplFileInfo $file): Archive { $errors = []; @@ -60,6 +91,8 @@ public function create(\SplFileInfo $file): Archive } /** + * Returns list of supported archive extensions + * * @return list */ public function getSupportedExtensions(): array @@ -67,6 +100,9 @@ public function getSupportedExtensions(): array return $this->extensions; } + /** + * Registers default archive type handlers + */ private function bootDefaultMatchers(): void { $this->extend($this->matcher( @@ -86,10 +122,10 @@ private function bootDefaultMatchers(): void } /** - * @param string $extension - * @param ArchiveMatcher $then + * Creates a matcher function for files with specific extension * - * @return ArchiveMatcher + * @param string $extension File extension to match + * @param ArchiveMatcher $then Function to create archive handler */ private function matcher(string $extension, \Closure $then): \Closure { diff --git a/src/Module/Archive/Exception/ArchiveException.php b/src/Module/Archive/Exception/ArchiveException.php new file mode 100644 index 0000000..a72c151 --- /dev/null +++ b/src/Module/Archive/Exception/ArchiveException.php @@ -0,0 +1,7 @@ +getPathname() => $file; + * // Extract file if requested + * } + * } + * } + * ``` + * + * @internal + */ abstract class Archive implements ArchiveInterface { /** - * @param \SplFileInfo $archive + * Creates archive handler and validates the archive file + * + * @param \SplFileInfo $archive Archive file + * @throws \InvalidArgumentException When archive is invalid */ public function __construct(\SplFileInfo $archive) { @@ -17,7 +42,10 @@ public function __construct(\SplFileInfo $archive) } /** - * @param \SplFileInfo $archive + * Validates that archive file exists and is readable + * + * @param \SplFileInfo $archive Archive file to validate + * @throws \InvalidArgumentException When archive is invalid */ private function assertArchiveValid(\SplFileInfo $archive): void { diff --git a/src/Module/Archive/Internal/PharArchive.php b/src/Module/Archive/Internal/PharArchive.php index b02d287..b47fc0b 100644 --- a/src/Module/Archive/Internal/PharArchive.php +++ b/src/Module/Archive/Internal/PharArchive.php @@ -4,8 +4,28 @@ namespace Internal\DLoad\Module\Archive\Internal; +/** + * Archive handler for standard PHAR archives + * + * ```php + * // Creating and using a PHAR archive handler + * $archive = new PharArchive(new \SplFileInfo('package.phar')); + * foreach ($archive->extract() as $path => $fileInfo) { + * if (str_ends_with($path, '.php')) { + * yield new \SplFileInfo('/tmp/extract/' . basename($path)); + * } + * } + * ``` + * + * @internal + */ final class PharArchive extends PharAwareArchive { + /** + * Opens PHAR archive for reading + * + * @param \SplFileInfo $file Archive file + */ protected function open(\SplFileInfo $file): \PharData { return new \PharData($file->getPathname()); diff --git a/src/Module/Archive/Internal/PharAwareArchive.php b/src/Module/Archive/Internal/PharAwareArchive.php index 9ccfd66..d0190b7 100644 --- a/src/Module/Archive/Internal/PharAwareArchive.php +++ b/src/Module/Archive/Internal/PharAwareArchive.php @@ -4,10 +4,45 @@ namespace Internal\DLoad\Module\Archive\Internal; +use Internal\DLoad\Module\Archive\Exception\ArchiveException; + +/** + * Base class for PharData-based archive handlers + * + * Provides common functionality for archives handled by PHP's PharData. + * + * ```php + * // Example implementation of a custom PharData-based archive + * class CustomPharArchive extends PharAwareArchive + * { + * protected function open(\SplFileInfo $file): \PharData + * { + * // Custom opening logic + * return new \PharData($file->getPathname(), $customFlags); + * } + * } + * + * // Usage + * $archive = new CustomPharArchive(new \SplFileInfo('archive.custom')); + * foreach ($archive->extract() as $path => $fileInfo) { + * // Extract to destination + * yield new \SplFileInfo('/path/to/extract/' . basename($path)); + * } + * ``` + * + * @internal + */ abstract class PharAwareArchive extends Archive { + /** @var \PharData Opened archive instance */ protected \PharData $archive; + /** + * Creates and opens archive + * + * @param \SplFileInfo $archive Archive file + * @throws \LogicException When archive cannot be opened + */ public function __construct(\SplFileInfo $archive) { parent::__construct($archive); @@ -17,7 +52,7 @@ public function __construct(\SplFileInfo $archive) public function extract(): \Generator { $phar = $this->archive; - $phar->isReadable() or throw new \LogicException( + $phar->isReadable() or throw new ArchiveException( \sprintf('Could not open "%s" for reading.', $this->archive->getPathname()), ); @@ -32,5 +67,10 @@ public function extract(): \Generator } } + /** + * Opens archive with specific format + * + * @param \SplFileInfo $file Archive file + */ abstract protected function open(\SplFileInfo $file): \PharData; } diff --git a/src/Module/Archive/Internal/TarPharArchive.php b/src/Module/Archive/Internal/TarPharArchive.php index 98b9e7a..fbc3783 100644 --- a/src/Module/Archive/Internal/TarPharArchive.php +++ b/src/Module/Archive/Internal/TarPharArchive.php @@ -4,8 +4,27 @@ namespace Internal\DLoad\Module\Archive\Internal; +/** + * Archive handler for TAR.GZ archives + * + * ```php + * // Extracting from TAR.GZ archive + * $archive = new TarPharArchive(new \SplFileInfo('package.tar.gz')); + * foreach ($archive->extract() as $path => $fileInfo) { + * // Extract all files to a directory + * yield new \SplFileInfo('/tmp/extract/' . basename($path)); + * } + * ``` + * + * @internal + */ final class TarPharArchive extends PharAwareArchive { + /** + * Opens TAR.GZ archive for reading + * + * @param \SplFileInfo $file Archive file + */ protected function open(\SplFileInfo $file): \PharData { return new \PharData($file->getPathname()); diff --git a/src/Module/Archive/Internal/ZipPharArchive.php b/src/Module/Archive/Internal/ZipPharArchive.php index 436d2d3..76bbef1 100644 --- a/src/Module/Archive/Internal/ZipPharArchive.php +++ b/src/Module/Archive/Internal/ZipPharArchive.php @@ -4,8 +4,33 @@ namespace Internal\DLoad\Module\Archive\Internal; +/** + * Archive handler for ZIP archives + * + * ```php + * // Extracting specific file types from ZIP archive + * $archive = new ZipPharArchive(new \SplFileInfo('package.zip')); + * $extractDir = '/tmp/extract'; + * + * foreach ($archive->extract() as $path => $fileInfo) { + * // Only extract binary files + * if (pathinfo($path, PATHINFO_EXTENSION) === 'bin') { + * yield new \SplFileInfo($extractDir . '/' . basename($path)); + * } + * } + * ``` + * + * @internal + */ final class ZipPharArchive extends PharAwareArchive { + /** + * Opens ZIP archive for reading + * + * Uses ZIP and GZ formats to properly handle ZIP archives. + * + * @param \SplFileInfo $file Archive file + */ protected function open(\SplFileInfo $file): \PharData { $format = \Phar::ZIP | \Phar::GZ; diff --git a/src/Module/Common/Architecture.php b/src/Module/Common/Architecture.php index 9fb0d6a..907dcf0 100644 --- a/src/Module/Common/Architecture.php +++ b/src/Module/Common/Architecture.php @@ -10,6 +10,16 @@ /** * Architecture enumeration. * + * Represents CPU architectures supported by the downloader system. + * + * ```php + * // To get the current system architecture, use DI Container: + * $arch = $container->get(Architecture::class); + * + * // To create an architecture instance from a build configuration name: + * $arch = Architecture::tryFromBuildName('my-software_amd64'); + * ``` + * * @internal */ enum Architecture: string implements Factoriable diff --git a/src/Module/Common/Config/Action/Download.php b/src/Module/Common/Config/Action/Download.php index 7c616d7..6618d87 100644 --- a/src/Module/Common/Config/Action/Download.php +++ b/src/Module/Common/Config/Action/Download.php @@ -7,20 +7,31 @@ use Internal\DLoad\Module\Common\Internal\Attribute\XPath; /** + * Download action configuration. + * + * Represents a single download action from the configuration file. + * Defines what software to download considering its version constraint. + * + * ```php + * $action = Download::fromSoftwareId('roadrunner'); + * ``` + * * @internal */ final class Download { - /** @var non-empty-string */ + /** @var non-empty-string $software Software identifier to download */ #[XPath('@software')] public string $software; - /** @var non-empty-string|null */ + /** @var non-empty-string|null $version Version constraint (composer-style) */ #[XPath('@version')] public ?string $version = null; /** - * @param non-empty-string $software + * Creates a download action from a software identifier. + * + * @param non-empty-string $software Software identifier */ public static function fromSoftwareId(string $software): self { diff --git a/src/Module/Common/Config/Actions.php b/src/Module/Common/Config/Actions.php index ec6daad..0dd0c69 100644 --- a/src/Module/Common/Config/Actions.php +++ b/src/Module/Common/Config/Actions.php @@ -8,11 +8,15 @@ use Internal\DLoad\Module\Common\Internal\Attribute\XPathEmbedList; /** + * Configuration actions container. + * + * Contains the list of download actions defined in the configuration file. + * * @internal */ final class Actions { - /** @var list */ + /** @var list $downloads Collection of download actions */ #[XPathEmbedList('/dload/actions/download', Download::class)] public array $downloads = []; } diff --git a/src/Module/Common/Config/CustomSoftwareRegistry.php b/src/Module/Common/Config/CustomSoftwareRegistry.php index e18b264..c4d6da9 100644 --- a/src/Module/Common/Config/CustomSoftwareRegistry.php +++ b/src/Module/Common/Config/CustomSoftwareRegistry.php @@ -7,13 +7,19 @@ use Internal\DLoad\Module\Common\Internal\Attribute\XPath; use Internal\DLoad\Module\Common\Internal\Attribute\XPathEmbedList; +/** + * Custom software registry configuration. + * + * Holds settings for custom software definitions provided in the configuration. + */ final class CustomSoftwareRegistry { + /** @var bool $overwrite Replace the built-in software collection with custom ones */ #[XPath('/dload/registry/@overwrite')] public bool $overwrite = false; /** - * @var \Internal\DLoad\Module\Common\Config\Embed\Software[] + * @var Embed\Software[] $software Custom software definitions from the configuration */ #[XPathEmbedList('/dload/registry/software', Embed\Software::class)] public array $software = []; diff --git a/src/Module/Common/Config/Downloader.php b/src/Module/Common/Config/Downloader.php index 855ca8b..6a24d4d 100644 --- a/src/Module/Common/Config/Downloader.php +++ b/src/Module/Common/Config/Downloader.php @@ -6,8 +6,14 @@ use Internal\DLoad\Module\Common\Internal\Attribute\XPath; +/** + * Downloader configuration. + * + * Contains global settings for the download functionality. + */ final class Downloader { + /** @var string|null $tmpDir Temporary directory for downloads */ #[XPath('/dload/@temp-dir')] public ?string $tmpDir = null; } diff --git a/src/Module/Common/Config/Embed/File.php b/src/Module/Common/Config/Embed/File.php index 3f4f60d..4de3032 100644 --- a/src/Module/Common/Config/Embed/File.php +++ b/src/Module/Common/Config/Embed/File.php @@ -7,6 +7,17 @@ use Internal\DLoad\Module\Common\Internal\Attribute\XPath; /** + * File configuration. + * + * Defines how a file should be extracted and optionally renamed after download. + * + * ```php + * $file = File::fromArray([ + * 'pattern' => '/^roadrunner(?:\.exe)?$/', + * 'rename' => 'rr' + * ]); + * ``` + * * @psalm-type FileArray = array{ * rename?: non-empty-string, * pattern?: non-empty-string @@ -15,16 +26,20 @@ final class File { /** - * @var non-empty-string|null In case of not null, found file will be renamed to this value with the same extension. + * @var non-empty-string|null $rename In case of not null, found file will be renamed to this value + * with the same extension. */ #[XPath('@rename')] public ?string $rename = null; + /** @var non-empty-string $pattern Regular expression pattern to match files */ #[XPath('@pattern')] public string $pattern = '/^.*$/'; /** - * @param FileArray $fileArray + * Creates a File configuration from an array. + * + * @param FileArray $fileArray Configuration array */ public static function fromArray(mixed $fileArray): self { diff --git a/src/Module/Common/Config/Embed/Repository.php b/src/Module/Common/Config/Embed/Repository.php index e5e8ae4..947b637 100644 --- a/src/Module/Common/Config/Embed/Repository.php +++ b/src/Module/Common/Config/Embed/Repository.php @@ -7,6 +7,18 @@ use Internal\DLoad\Module\Common\Internal\Attribute\XPath; /** + * Repository configuration. + * + * Defines a source repository for software packages. + * + * ```php + * $repo = Repository::fromArray([ + * 'type' => 'github', + * 'uri' => 'roadrunner-server/roadrunner', + * 'asset-pattern' => '#^roadrunner-.*#' + * ]); + * ``` + * * @psalm-type RepositoryArray = array{ * type: non-empty-string, * uri: non-empty-string, @@ -15,17 +27,22 @@ */ final class Repository { + /** @var non-empty-string $type Repository type identifier */ #[XPath('@type')] public string $type = 'github'; + /** @var non-empty-string $uri Repository URI identifier */ #[XPath('@uri')] public string $uri; + /** @var non-empty-string $assetPattern Regular expression pattern to match assets */ #[XPath('@asset-pattern')] public string $assetPattern = '/^.*$/'; /** - * @param RepositoryArray $repositoryArray + * Creates a Repository configuration from an array. + * + * @param RepositoryArray $repositoryArray Configuration array */ public static function fromArray(mixed $repositoryArray): self { diff --git a/src/Module/Common/Config/Embed/Software.php b/src/Module/Common/Config/Embed/Software.php index 42f336c..8ade1ad 100644 --- a/src/Module/Common/Config/Embed/Software.php +++ b/src/Module/Common/Config/Embed/Software.php @@ -8,6 +8,22 @@ use Internal\DLoad\Module\Common\Internal\Attribute\XPathEmbedList; /** + * Software configuration entity. + * + * Represents a software package that can be downloaded through the system. + * Contains all necessary metadata for proper identification and retrieval. + * + * ```php + * $software = Software::fromArray([ + * 'name' => 'RoadRunner', + * 'alias' => 'rr', + * 'description' => 'High performance PHP application server', + * 'repositories' => [ + * ['type' => 'github', 'uri' => 'roadrunner-server/roadrunner'] + * ] + * ]); + * ``` + * * @psalm-import-type RepositoryArray from Repository * @psalm-import-type FileArray from File * @psalm-type SoftwareArray = array{ @@ -21,35 +37,37 @@ */ final class Software { - /** - * @var non-empty-string - */ + /** @var non-empty-string $name Software package name */ #[XPath('@name')] public string $name; /** - * If {@see null}, the name in lower case will be used. - * @var non-empty-string|null + * @var non-empty-string|null $alias CLI command alias + * If null, the name in lowercase will be used as the identifier. */ #[XPath('@alias')] public ?string $alias = null; + /** @var string|null $homepage Official software homepage URL */ #[XPath('@homepage')] public ?string $homepage = null; + /** @var string $description Short description of the software */ #[XPath('@description')] public string $description = ''; - /** @var File */ + /** @var Repository[] $repositories List of repositories where the software can be found */ #[XPathEmbedList('repository', Repository::class)] public array $repositories = []; - /** @var File */ + /** @var File[] $files List of files to be extracted after download */ #[XPathEmbedList('file', File::class)] public array $files = []; /** - * @param SoftwareArray $softwareArray + * Creates a Software instance from array configuration. + * + * @param SoftwareArray $softwareArray Configuration array */ public static function fromArray(mixed $softwareArray): self { @@ -71,7 +89,9 @@ public static function fromArray(mixed $softwareArray): self } /** - * @return non-empty-string + * Returns the software identifier. + * + * @return non-empty-string Software identifier (alias or lowercase name) */ public function getId(): string { diff --git a/src/Module/Common/Config/GitHub.php b/src/Module/Common/Config/GitHub.php index 34706c8..8f4d549 100644 --- a/src/Module/Common/Config/GitHub.php +++ b/src/Module/Common/Config/GitHub.php @@ -7,10 +7,15 @@ use Internal\DLoad\Module\Common\Internal\Attribute\Env; /** + * GitHub API configuration. + * + * Contains authentication settings for GitHub API access. + * * @internal */ final class GitHub { + /** @var string|null $token API token for GitHub authentication */ #[Env('GITHUB_TOKEN')] public ?string $token = null; } diff --git a/src/Module/Common/Input/Build.php b/src/Module/Common/Input/Build.php index 41bc720..4582701 100644 --- a/src/Module/Common/Input/Build.php +++ b/src/Module/Common/Input/Build.php @@ -10,6 +10,11 @@ use Internal\DLoad\Module\Common\Stability; /** + * Build configuration options. + * + * Contains input options that define the build parameters for software selection. + * These values are typically provided via command-line options. + * * @internal */ final class Build diff --git a/src/Module/Common/Input/Destination.php b/src/Module/Common/Input/Destination.php index ad0dc09..1abdeb1 100644 --- a/src/Module/Common/Input/Destination.php +++ b/src/Module/Common/Input/Destination.php @@ -7,10 +7,15 @@ use Internal\DLoad\Module\Common\Internal\Attribute\InputOption; /** + * Destination configuration. + * + * Contains the destination path where downloaded software will be saved. + * * @internal */ final class Destination { + /** @var string|null $path Target path for downloaded files */ #[InputOption('path')] public ?string $path = null; } diff --git a/src/Module/Common/Internal/Attribute/ConfigAttribute.php b/src/Module/Common/Internal/Attribute/ConfigAttribute.php index da16ae8..4307de0 100644 --- a/src/Module/Common/Internal/Attribute/ConfigAttribute.php +++ b/src/Module/Common/Internal/Attribute/ConfigAttribute.php @@ -5,6 +5,8 @@ namespace Internal\DLoad\Module\Common\Internal\Attribute; /** + * Base interface for all configuration attributes. + * * @internal */ interface ConfigAttribute {} diff --git a/src/Module/Common/Internal/Attribute/Env.php b/src/Module/Common/Internal/Attribute/Env.php index 781f928..dbab5b1 100644 --- a/src/Module/Common/Internal/Attribute/Env.php +++ b/src/Module/Common/Internal/Attribute/Env.php @@ -5,11 +5,18 @@ namespace Internal\DLoad\Module\Common\Internal\Attribute; /** + * Environment variable configuration attribute. + * + * Maps a property to an environment variable. + * * @internal */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)] final class Env implements ConfigAttribute { + /** + * @param non-empty-string $name Environment variable name + */ public function __construct( public string $name, ) {} diff --git a/src/Module/Common/Internal/Attribute/InputArgument.php b/src/Module/Common/Internal/Attribute/InputArgument.php index e141eea..19de71f 100644 --- a/src/Module/Common/Internal/Attribute/InputArgument.php +++ b/src/Module/Common/Internal/Attribute/InputArgument.php @@ -5,11 +5,18 @@ namespace Internal\DLoad\Module\Common\Internal\Attribute; /** + * Command line argument configuration attribute. + * + * Maps a property to a command line argument. + * * @internal */ #[\Attribute(\Attribute::TARGET_PROPERTY)] final class InputArgument implements ConfigAttribute { + /** + * @param non-empty-string $name Argument name + */ public function __construct( public string $name, ) {} diff --git a/src/Module/Common/Internal/Attribute/InputOption.php b/src/Module/Common/Internal/Attribute/InputOption.php index dd85785..6f2209e 100644 --- a/src/Module/Common/Internal/Attribute/InputOption.php +++ b/src/Module/Common/Internal/Attribute/InputOption.php @@ -5,11 +5,18 @@ namespace Internal\DLoad\Module\Common\Internal\Attribute; /** + * Command line option configuration attribute. + * + * Maps a property to a command line option. + * * @internal */ #[\Attribute(\Attribute::TARGET_PROPERTY)] final class InputOption implements ConfigAttribute { + /** + * @param non-empty-string $name Option name + */ public function __construct( public string $name, ) {} diff --git a/src/Module/Common/Internal/Attribute/PhpIni.php b/src/Module/Common/Internal/Attribute/PhpIni.php index 6b1d4bd..9b981d9 100644 --- a/src/Module/Common/Internal/Attribute/PhpIni.php +++ b/src/Module/Common/Internal/Attribute/PhpIni.php @@ -5,11 +5,18 @@ namespace Internal\DLoad\Module\Common\Internal\Attribute; /** + * PHP INI configuration attribute. + * + * Maps a property to a PHP INI setting. + * * @internal */ -#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)] +#[\Attribute(\Attribute::TARGET_PROPERTY)] final class PhpIni implements ConfigAttribute { + /** + * @param non-empty-string $option PHP INI option name + */ public function __construct( public string $option, ) {} diff --git a/src/Module/Common/Internal/Attribute/XPath.php b/src/Module/Common/Internal/Attribute/XPath.php index aa7b98c..0293ab9 100644 --- a/src/Module/Common/Internal/Attribute/XPath.php +++ b/src/Module/Common/Internal/Attribute/XPath.php @@ -5,11 +5,19 @@ namespace Internal\DLoad\Module\Common\Internal\Attribute; /** + * XPath configuration attribute. + * + * Maps a property to an XML element or attribute using XPath expression. + * * @internal */ #[\Attribute(\Attribute::TARGET_PROPERTY)] final class XPath implements ConfigAttribute { + /** + * @param non-empty-string $path XPath expression + * @param int<0, max> $key Index in the result array (if XPath returns multiple items) + */ public function __construct( public string $path, public int $key = 0, diff --git a/src/Module/Common/Internal/Attribute/XPathEmbedList.php b/src/Module/Common/Internal/Attribute/XPathEmbedList.php index 0a0c6d6..d2dcd81 100644 --- a/src/Module/Common/Internal/Attribute/XPathEmbedList.php +++ b/src/Module/Common/Internal/Attribute/XPathEmbedList.php @@ -5,14 +5,19 @@ namespace Internal\DLoad\Module\Common\Internal\Attribute; /** + * XPath embedding list configuration attribute. + * + * Maps a property to a list of objects by loading each from an XML element + * that matches the XPath expression. + * * @internal */ #[\Attribute(\Attribute::TARGET_PROPERTY)] final class XPathEmbedList implements ConfigAttribute { /** - * @param non-empty-string $path - * @param class-string $class + * @param non-empty-string $path XPath expression to locate elements + * @param class-string $class Class to instantiate for each matched element */ public function __construct( public string $path, diff --git a/src/Module/Common/Internal/Injection/ConfigLoader.php b/src/Module/Common/Internal/Injection/ConfigLoader.php index 8aaacc0..7b2d3da 100644 --- a/src/Module/Common/Internal/Injection/ConfigLoader.php +++ b/src/Module/Common/Internal/Injection/ConfigLoader.php @@ -14,6 +14,11 @@ use Internal\DLoad\Service\Logger; /** + * Configuration loader service. + * + * Hydrates configuration objects with values from different sources + * based on their property attributes. + * * @internal */ final class ConfigLoader @@ -21,6 +26,8 @@ final class ConfigLoader private \SimpleXMLElement|null $xml = null; /** + * Creates a new configuration loader. + * * @psalm-suppress RiskyTruthyFalsyComparison */ public function __construct( @@ -40,6 +47,9 @@ public function __construct( } } + /** + * Hydrates a configuration object with values from the configured sources. + */ public function hydrate(object $config): void { // Read class properties @@ -55,7 +65,8 @@ public function hydrate(object $config): void } /** - * @param \ReflectionProperty $property + * Injects values into a property based on its configuration attributes. + * * @param list<\ReflectionAttribute> $attributes */ private function injectValue(object $config, \ReflectionProperty $property, array $attributes): void @@ -115,6 +126,9 @@ private function injectValue(object $config, \ReflectionProperty $property, arra } } + /** + * Gets a value from XML using an XPath expression. + */ private function getXPath(XPath $attribute): mixed { $value = $this->xml?->xpath($attribute->path); @@ -124,6 +138,9 @@ private function getXPath(XPath $attribute): mixed : null; } + /** + * Gets a list of objects from XML using an XPath expression. + */ private function getXPathEmbeddedList(XPathEmbedList $attribute): array { if ($this->xml === null) { @@ -147,6 +164,9 @@ private function getXPathEmbeddedList(XPathEmbedList $attribute): array return $result; } + /** + * Creates a new loader instance with the specified XML element. + */ private function withXml(\SimpleXMLElement $xml): self { $self = clone $this; diff --git a/src/Module/Common/Internal/Container.php b/src/Module/Common/Internal/ObjectContainer.php similarity index 74% rename from src/Module/Common/Internal/Container.php rename to src/Module/Common/Internal/ObjectContainer.php index 3997eb6..481ac02 100644 --- a/src/Module/Common/Internal/Container.php +++ b/src/Module/Common/Internal/ObjectContainer.php @@ -5,19 +5,21 @@ namespace Internal\DLoad\Module\Common\Internal; use Internal\DLoad\Module\Common\Internal\Injection\ConfigLoader; -use Internal\DLoad\Service\Container as AppContainerInterface; -use Internal\DLoad\Service\Destroyable; +use Internal\DLoad\Service\Container; use Internal\DLoad\Service\Factoriable; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; use Yiisoft\Injector\Injector; /** - * Simple container. + * Simple dependency injection container. + * + * Provides service creation and caching with autowiring capabilities. + * Automatically loads configuration for config classes. * * @internal */ -final class Container implements AppContainerInterface, ContainerInterface, Destroyable +final class ObjectContainer implements Container, ContainerInterface { /** @var array */ private array $cache = []; @@ -35,51 +37,27 @@ public function __construct() $this->injector = (new Injector($this))->withCacheReflections(false); $this->cache[Injector::class] = $this->injector; $this->cache[self::class] = $this; + $this->cache[Container::class] = $this; $this->cache[ContainerInterface::class] = $this; } - /** - * @template T of object - * @param class-string $id - * @param array $arguments Will be used if the object is created for the first time. - * @return T - * - * @psalm-suppress MoreSpecificImplementedParamType, InvalidReturnType - */ public function get(string $id, array $arguments = []): object { /** @psalm-suppress InvalidReturnStatement */ return $this->cache[$id] ??= $this->make($id, $arguments); } - /** - * @param class-string $id - * - * @psalm-suppress MoreSpecificImplementedParamType - */ public function has(string $id): bool { return \array_key_exists($id, $this->cache) || \array_key_exists($id, $this->factory); } - /** - * @template T of object - * @param T $service - * @param class-string|null $id - */ public function set(object $service, ?string $id = null): void { \assert($id === null || $service instanceof $id, "Service must be instance of {$id}."); $this->cache[$id ?? \get_class($service)] = $service; } - /** - * Create an object of the specified class without caching. - * - * @template T - * @param class-string $class - * @return T - */ public function make(string $class, array $arguments = []): object { $binding = $this->factory[$class] ?? null; @@ -109,11 +87,9 @@ public function make(string $class, array $arguments = []): object } /** - * Declare a factory or predefined arguments for the specified class. - * - * @template T of object - * @param class-string $id - * @param array|\Closure(Container): T $binding + * @template T + * @param class-string $id Service identifier + * @param null|array|\Closure(Container): T $binding Factory function or constructor arguments */ public function bind(string $id, \Closure|array|null $binding = null): void { @@ -126,7 +102,9 @@ public function bind(string $id, \Closure|array|null $binding = null): void "Class `$id` must have a factory or be a factory itself and implement `Factoriable`.", ); - $this->factory[$id] = $id::create(...); + /** @var T $object */ + $object = $id::create(...); + $this->factory[$id] = $object; } public function destroy(): void diff --git a/src/Module/Common/OperatingSystem.php b/src/Module/Common/OperatingSystem.php index c67c9d3..7fead08 100644 --- a/src/Module/Common/OperatingSystem.php +++ b/src/Module/Common/OperatingSystem.php @@ -10,6 +10,16 @@ /** * Operating system enumeration. * + * Represents the different operating systems supported by the downloader. + * + * ```php + * // Recommended: Get from container (autowired with build config) + * $os = $container->get(OperatingSystem::class); + * + * // Or create from build config name + * $os = OperatingSystem::tryFromBuildName('my-software_darwin'); + * ``` + * * @internal */ enum OperatingSystem: string implements Factoriable diff --git a/src/Module/Common/Stability.php b/src/Module/Common/Stability.php index d211558..383eac5 100644 --- a/src/Module/Common/Stability.php +++ b/src/Module/Common/Stability.php @@ -10,6 +10,17 @@ /** * Software stability level. * + * Defines the stability level of software releases from most stable to least. + * Used to filter releases based on desired stability. + * + * ```php + * // Getting stability from build config + * $stability = Stability::create($buildConfig); + * + * // Getting the stability level weight (higher number means more stable) + * $weight = $stability->getWeight(); + * ``` + * * @internal */ enum Stability: string implements Factoriable diff --git a/src/Module/Downloader/Downloader.php b/src/Module/Downloader/Downloader.php index 0f4bf81..4c95640 100644 --- a/src/Module/Downloader/Downloader.php +++ b/src/Module/Downloader/Downloader.php @@ -16,7 +16,7 @@ use Internal\DLoad\Module\Downloader\Task\DownloadTask; use Internal\DLoad\Module\Repository\AssetInterface; use Internal\DLoad\Module\Repository\ReleaseInterface; -use Internal\DLoad\Module\Repository\RepositoryInterface; +use Internal\DLoad\Module\Repository\Repository; use Internal\DLoad\Module\Repository\RepositoryProvider; use Internal\DLoad\Service\Destroyable; use Internal\DLoad\Service\Logger; @@ -25,6 +25,22 @@ use function React\Async\await; use function React\Async\coroutine; +/** + * Core downloader service responsible for fetching software assets. + * + * Manages the entire download process from repository selection to asset downloading. + * Supports multiple repositories and provides fallback capability when a repository fails. + * + * ```php + * // Create a download task + * $task = $downloader->download($software, $config, function(Progress $progress) { + * echo sprintf("Downloaded: %d/%d bytes\n", $progress->current, $progress->total); + * }); + * + * // Execute the task + * $result = await($task->handler()); + * ``` + */ final class Downloader { public function __construct( @@ -38,10 +54,16 @@ public function __construct( ) {} /** - * Create task to download software. + * Creates a task to download software. * - * @param \Closure(Progress): mixed $onProgress Callback to report progress. + * Prepares a download task that can be executed to obtain the software asset. The task tries repositories + * sequentially until one succeeds. + * + * @param Software $software Software package configuration + * @param DownloadConfig $actionConfig Download action configuration + * @param \Closure(Progress): mixed $onProgress Callback to report download progress. * Exception thrown in this callback will stop and revert the task. + * @return DownloadTask Executable download task object */ public function download( Software $software, @@ -89,9 +111,15 @@ public function download( } /** - * @return \Closure(): ReleaseInterface + * Processes the repository to find suitable releases. + * + * Fetches and filters releases from the repository based on stability and version constraints. + * + * @param Repository $repository Repository to process + * @param DownloadContext $context Download context information + * @return \Closure(): ReleaseInterface Closure that returns the selected release */ - private function processRepository(RepositoryInterface $repository, DownloadContext $context): \Closure + private function processRepository(Repository $repository, DownloadContext $context): \Closure { return function () use ($repository, $context): ReleaseInterface { $this->logger->info( @@ -108,11 +136,13 @@ private function processRepository(RepositoryInterface $repository, DownloadCont ->satisfies($context->actionConfig->version); /** @var ReleaseInterface[] $releases */ - $releases = $releasesCollection->sortByVersion()->toArray(); + $releases = $releasesCollection->limit(10)->sortByVersion()->toArray(); $this->logger->debug('%d releases found.', \count($releases)); process_release: + // Try without limit + $releases === [] and $releases = $releasesCollection->limit(0)->toArray(); $releases === [] and throw new \RuntimeException('No relevant release found.'); $context->release = \array_shift($releases); @@ -130,7 +160,12 @@ private function processRepository(RepositoryInterface $repository, DownloadCont } /** - * @return \Closure(): AssetInterface + * Processes a release to find suitable assets. + * + * Filters assets from the release based on architecture, operating system, and name pattern. + * + * @param DownloadContext $context Download context information + * @return \Closure(): AssetInterface Closure that returns the selected asset */ private function processRelease(DownloadContext $context): \Closure { @@ -160,7 +195,12 @@ private function processRelease(DownloadContext $context): \Closure } /** - * @return \Closure(): \SplFileObject + * Downloads the selected asset to a temporary file. + * + * Creates a temporary file and downloads the asset content, reporting progress via callback. + * + * @param DownloadContext $context Download context information + * @return \Closure(): \SplFileObject Closure that returns the downloaded file */ private function processAsset(DownloadContext $context): \Closure { @@ -196,6 +236,14 @@ private function processAsset(DownloadContext $context): \Closure }; } + /** + * Returns the temporary directory path for file downloads. + * + * Uses the configured directory if available and writable, otherwise defaults to system temp directory. + * + * @return non-empty-string Path to temporary directory + * @throws \LogicException When configured directory is not writable + */ private function getTempDirectory(): string { $temp = $this->config->tmpDir; diff --git a/src/Module/Downloader/Internal/DownloadContext.php b/src/Module/Downloader/Internal/DownloadContext.php index 2ef349e..d1a9038 100644 --- a/src/Module/Downloader/Internal/DownloadContext.php +++ b/src/Module/Downloader/Internal/DownloadContext.php @@ -11,23 +11,35 @@ use Internal\DLoad\Module\Repository\AssetInterface; use Internal\DLoad\Module\Repository\ReleaseInterface; +/** + * Context object for download operations. + * + * Contains all contextual information needed during a download operation, including + * configurations, selected components, and callback functions. + * + * @internal + */ final class DownloadContext { - /** Current repository config */ + /** @var Repository Current repository configuration */ public Repository $repoConfig; - /** Downloaded file */ + /** @var \SplFileObject Downloaded file handle */ public \SplFileObject $file; - /** Current asset */ + /** @var AssetInterface Current asset being processed */ public AssetInterface $asset; - /** Current release */ + /** @var ReleaseInterface Current release being processed */ public ReleaseInterface $release; /** + * Creates a new download context. + * + * @param Software $software Software package configuration * @param \Closure(Progress): mixed $onProgress Callback to report progress. * Exception thrown in this callback will stop and revert the task. + * @param DownloadConfig $actionConfig Download action configuration */ public function __construct( public readonly Software $software, diff --git a/src/Module/Downloader/Progress.php b/src/Module/Downloader/Progress.php index 4fde937..1d9bb8b 100644 --- a/src/Module/Downloader/Progress.php +++ b/src/Module/Downloader/Progress.php @@ -4,11 +4,20 @@ namespace Internal\DLoad\Module\Downloader; +/** + * Represents download progress information. + * + * Immutable data object containing download progress metrics and status message. + * Used for reporting progress to listeners during download operations. + */ final class Progress { public function __construct( + /** @var int<0, max> Total size in bytes */ public readonly int $total = 100, + /** @var int<0, max> Current progress in bytes */ public readonly int $current = 0, + /** @var string Status message */ public readonly string $message = '', ) {} } diff --git a/src/Module/Downloader/SoftwareCollection.php b/src/Module/Downloader/SoftwareCollection.php index bce7f0f..3f61d50 100644 --- a/src/Module/Downloader/SoftwareCollection.php +++ b/src/Module/Downloader/SoftwareCollection.php @@ -10,13 +10,26 @@ use IteratorAggregate; /** + * Collection of software package configurations. + * + * Manages both custom and default software registry entries. + * Provides lookup functionality to find software by name or alias. + * * @implements IteratorAggregate */ final class SoftwareCollection implements \IteratorAggregate, \Countable { - /** @var array */ + /** @var array Map of software ID to configuration */ private array $registry = []; + /** + * Creates a collection from registry and optionally loads default entries. + * + * Processes custom registry entries first, then loads default registry unless + * overwrite flag is set in the custom registry. + * + * @param CustomSoftwareRegistry $softwareRegistry Custom software configuration registry + */ public function __construct(CustomSoftwareRegistry $softwareRegistry) { foreach ($softwareRegistry->software as $software) { @@ -26,12 +39,26 @@ public function __construct(CustomSoftwareRegistry $softwareRegistry) $softwareRegistry->overwrite or $this->loadDefaultRegistry(); } + /** + * Finds software configuration by name. + * + * Searches for software using exact name or alias match. + * + * ```php + * $software = $collection->findSoftware('rr') ?? throw new \RuntimeException('Software not found'); + * ``` + * + * @param non-empty-string $name Software name or alias + * @return Software|null Found software configuration or null + */ public function findSoftware(string $name): ?Software { return $this->registry[$name] ?? null; } /** + * Returns iterator for all software configurations. + * * @return \Traversable */ public function getIterator(): \Traversable @@ -40,13 +67,24 @@ public function getIterator(): \Traversable } /** - * @return int<0, max> + * Returns the number of software packages in the collection. + * + * @return int<0, max> Number of software packages */ public function count(): int { return \count($this->registry); } + /** + * Loads default software registry from the embedded JSON file. + * + * Parses the default software.json file and adds entries to the registry + * without overwriting existing entries. + * + * @link resources/software.json + * @see Software::fromArray() For parsing logic + */ private function loadDefaultRegistry(): void { $json = \json_decode( diff --git a/src/Module/Downloader/Task/DownloadResult.php b/src/Module/Downloader/Task/DownloadResult.php index c7ae061..15218ed 100644 --- a/src/Module/Downloader/Task/DownloadResult.php +++ b/src/Module/Downloader/Task/DownloadResult.php @@ -4,10 +4,18 @@ namespace Internal\DLoad\Module\Downloader\Task; +/** + * Represents the result of a successful download operation. + * + * Contains the downloaded file reference and version information. + */ final class DownloadResult { /** - * @param non-empty-string $version + * Creates a new download result. + * + * @param \SplFileInfo $file Downloaded file information + * @param non-empty-string $version Version of the downloaded software */ public function __construct( public readonly \SplFileInfo $file, diff --git a/src/Module/Downloader/Task/DownloadTask.php b/src/Module/Downloader/Task/DownloadTask.php index 4752021..8449394 100644 --- a/src/Module/Downloader/Task/DownloadTask.php +++ b/src/Module/Downloader/Task/DownloadTask.php @@ -8,12 +8,21 @@ use Internal\DLoad\Module\Downloader\Progress; use React\Promise\PromiseInterface; +/** + * Represents an executable download task. + * + * Contains all the information needed to execute a download operation, + * including handler function and progress callback. + */ final class DownloadTask { /** + * Creates a new download task. + * + * @param Software $software Software package configuration * @param \Closure(Progress): mixed $onProgress Callback to report progress. * Exception thrown in this callback will stop and revert the task. - * @param \Closure(): PromiseInterface $handler + * @param \Closure(): PromiseInterface $handler Function that executes the download */ public function __construct( public readonly Software $software, diff --git a/src/Module/Downloader/TaskManager.php b/src/Module/Downloader/TaskManager.php index 687e7ec..1fc8bc7 100644 --- a/src/Module/Downloader/TaskManager.php +++ b/src/Module/Downloader/TaskManager.php @@ -6,20 +6,59 @@ use Internal\DLoad\Service\Logger; +/** + * Task Manager Service + * + * Manages async tasks with Fiber-based coroutines. Tasks are executed concurrently + * and can be suspended/resumed. + * + * ```php + * // Create a task that can be suspended and resumed + * $task = function () { + * // First part of execution + * \Fiber::suspend(); + * // Execution continues after resume + * }; + * + * $taskManager = new TaskManager($logger); + * $taskManager->addTask($task); + * + * // Await for all tasks to finish + * $taskManager->await(); + * ``` + */ final class TaskManager { - /** @var array<\Fiber> */ + /** @var array<\Fiber> Active fiber tasks */ private array $tasks = []; + /** + * Constructor + * + * @param Logger $logger Error logging service + */ public function __construct( - private Logger $logger, + private readonly Logger $logger, ) {} + /** + * Adds a new task to the execution queue + * + * @param \Closure $callback Task implementation + */ public function addTask(\Closure $callback): void { $this->tasks[] = new \Fiber($callback); } + /** + * Creates a task processor generator + * + * Returns a generator that manages the execution cycle of all registered tasks. + * Each yield represents a step in task execution. + * + * @return \Generator Task processing generator + */ public function getProcessor(): \Generator { start: @@ -51,6 +90,12 @@ public function getProcessor(): \Generator goto start; } + /** + * Executes all tasks until completion + * + * Runs the processor generator until all tasks are complete. + * Blocks execution until all tasks are finished. + */ public function await(): void { $processor = $this->getProcessor(); diff --git a/src/Module/Repository/AssetInterface.php b/src/Module/Repository/AssetInterface.php index 8a61435..c663ae0 100644 --- a/src/Module/Repository/AssetInterface.php +++ b/src/Module/Repository/AssetInterface.php @@ -7,28 +7,66 @@ use Internal\DLoad\Module\Common\Architecture; use Internal\DLoad\Module\Common\OperatingSystem; +/** + * Represents a downloadable asset from a software release. + * + * Assets are individual binary files or archives associated with a specific release, + * often targeting particular operating systems or architectures. They represent + * the actual files that will be downloaded and extracted by the application. + * + * ```php + * $assets = $release->getAssets() + * ->whereArchitecture($architecture) + * ->whereOperatingSystem($operatingSystem) + * ->whereNameMatches($pattern); + * + * foreach ($assets as $asset) { + * $chunks = $asset->download(); + * // Process downloaded chunks + * } + * ``` + */ interface AssetInterface { + /** + * Returns the release this asset belongs to. + * + * @return ReleaseInterface The parent release + */ public function getRelease(): ReleaseInterface; /** - * @return non-empty-string + * Returns the name of the asset. + * + * @return non-empty-string Asset name, typically the filename */ public function getName(): string; /** - * @return non-empty-string + * Returns the URI from which the asset can be downloaded. + * + * @return non-empty-string Download URI */ public function getUri(): string; + /** + * Returns the operating system this asset is compatible with, if specified. + * + * @return OperatingSystem|null The target OS or null if not OS-specific + */ public function getOperatingSystem(): ?OperatingSystem; + /** + * Returns the CPU architecture this asset is compatible with, if specified. + * + * @return Architecture|null The target architecture or null if not architecture-specific + */ public function getArchitecture(): ?Architecture; /** - * Load content from the asset. + * Downloads the asset content. * - * @return \Traversable + * @return \Traversable Stream of content chunks */ public function download(): \Traversable; } diff --git a/src/Module/Repository/Collection/AssetsCollection.php b/src/Module/Repository/Collection/AssetsCollection.php index e25637b..a43c89d 100644 --- a/src/Module/Repository/Collection/AssetsCollection.php +++ b/src/Module/Repository/Collection/AssetsCollection.php @@ -10,12 +10,34 @@ use Internal\DLoad\Module\Repository\Internal\Collection; /** + * Collection of release assets with filtering capabilities. + * + * Provides methods to filter assets by architecture, operating system, + * file extension, and name patterns. + * + * ```php + * // Get Linux x86_64 assets with certain file extensions + * $assets = $release->getAssets() + * ->whereOperatingSystem(OperatingSystem::Linux) + * ->whereArchitecture(Architecture::X86_64) + * ->whereFileExtensions(['tar.gz', 'zip']) + * ->exceptDebPackages(); + * + * // Find assets matching a pattern + * $assets = $release->getAssets()->whereNameMatches('/^app-v\d+/'); + * ``` + * * @template-extends Collection * @internal * @psalm-internal Internal\DLoad\Module */ final class AssetsCollection extends Collection { + /** + * Filters out Debian package assets. + * + * @return self New filtered collection + */ public function exceptDebPackages(): self { return $this->except( @@ -24,6 +46,12 @@ public function exceptDebPackages(): self ); } + /** + * Filters assets to include only those matching the specified architecture. + * + * @param Architecture $arch Required architecture + * @return self New filtered collection + */ public function whereArchitecture(Architecture $arch): self { return $this->filter( @@ -31,6 +59,12 @@ public function whereArchitecture(Architecture $arch): self ); } + /** + * Filters assets to include only those matching the specified operating system. + * + * @param OperatingSystem $os Required operating system + * @return self New filtered collection + */ public function whereOperatingSystem(OperatingSystem $os): self { return $this->filter( @@ -39,7 +73,10 @@ public function whereOperatingSystem(OperatingSystem $os): self } /** - * @param list $extensions + * Filters assets to include only those with specified file extensions. + * + * @param list $extensions List of file extensions without leading dot + * @return self New filtered collection */ public function whereFileExtensions(array $extensions): self { @@ -58,14 +95,19 @@ static function (AssetInterface $asset) use ($extensions): bool { } /** - * Select all the assets with names that match the given pattern. + * Filters assets to include only those with names matching the given regex pattern. * - * @param non-empty-string $pattern + * @param non-empty-string $pattern Regular expression pattern + * @return self New filtered collection */ public function whereNameMatches(string $pattern): self { return $this->filter( - static fn(AssetInterface $asset): bool => \preg_match($pattern, $asset->getName()) === 1, + static fn(AssetInterface $asset): bool => @\preg_match( + $pattern, + $asset->getName(), + flags: \PREG_NO_ERROR, + ) === 1, ); } } diff --git a/src/Module/Repository/Collection/CompositeRepository.php b/src/Module/Repository/Collection/CompositeRepository.php new file mode 100644 index 0000000..674b0ca --- /dev/null +++ b/src/Module/Repository/Collection/CompositeRepository.php @@ -0,0 +1,62 @@ +getReleases(); + * ``` + * + * @internal + * @psalm-internal Internal\DLoad\Module + */ +final class CompositeRepository implements Repository +{ + /** + * @var array + */ + private array $repositories; + + /** + * @param array $repositories List of repositories to include + */ + public function __construct(array $repositories) + { + $this->repositories = $repositories; + } + + /** + * @return non-empty-string + */ + public function getName(): string + { + return 'unknown/unknown'; + } + + /** + * Returns a collection of all releases from all repositories. + * + * @return ReleasesCollection Combined collection of releases + */ + public function getReleases(): ReleasesCollection + { + return ReleasesCollection::create(function () { + foreach ($this->repositories as $repository) { + yield from $repository->getReleases(); + } + }); + } +} diff --git a/src/Module/Repository/Collection/ReleasesCollection.php b/src/Module/Repository/Collection/ReleasesCollection.php index 7191d88..aa8b57e 100644 --- a/src/Module/Repository/Collection/ReleasesCollection.php +++ b/src/Module/Repository/Collection/ReleasesCollection.php @@ -9,6 +9,23 @@ use Internal\DLoad\Module\Repository\ReleaseInterface; /** + * Collection of software releases with filtering and sorting capabilities. + * + * Provides methods to filter releases by version constraints, stability levels, + * and availability of assets. + * + * ```php + * // Get stable releases that satisfy a version constraint + * $releases = $repository->getReleases() + * ->stable() + * ->satisfies('^2.0.0') + * ->withAssets() + * ->sortByVersion(); + * + * // Get the most recent release + * $latestRelease = $releases->first(); + * ``` + * * @template-extends Collection * @internal * @psalm-internal Internal\DLoad\Module @@ -16,8 +33,10 @@ final class ReleasesCollection extends Collection { /** - * @param non-empty-string $constraint - * @return $this + * Filters releases to those that satisfy the given version constraint. + * + * @param non-empty-string $constraint Version constraint in Composer format + * @return $this New filtered collection */ public function satisfies(string $constraint): self { @@ -25,8 +44,10 @@ public function satisfies(string $constraint): self } /** - * @param non-empty-string $constraint - * @return $this + * Filters releases to those that do not satisfy the given version constraint. + * + * @param non-empty-string $constraint Version constraint in Composer format + * @return $this New filtered collection */ public function notSatisfies(string $constraint): self { @@ -34,22 +55,26 @@ public function notSatisfies(string $constraint): self } /** - * @return $this + * Filters releases to those that have at least one asset. + * + * @return $this New filtered collection */ public function withAssets(): self { return $this->filter( - static fn(ReleaseInterface $r): bool => ! $r->getAssets() + static fn(ReleaseInterface $r): bool => !$r->getAssets() ->empty(), ); } /** - * @return $this + * Sorts releases by version in descending order (newest first) and maintain index association. + * + * @return $this New sorted collection */ public function sortByVersion(): self { - $result = $this->items; + $result = \iterator_to_array($this->getIterator()); $sort = function (ReleaseInterface $a, ReleaseInterface $b): int { return \version_compare($this->comparisonVersionString($b), $this->comparisonVersionString($a)); @@ -61,7 +86,9 @@ public function sortByVersion(): self } /** - * @return $this + * Filters releases to only include stable versions. + * + * @return $this New filtered collection */ public function stable(): self { @@ -69,7 +96,10 @@ public function stable(): self } /** - * @return $this + * Filters releases to include only those matching the exact stability level. + * + * @param Stability $stability Required stability level + * @return $this New filtered collection */ public function stability(Stability $stability): self { @@ -77,7 +107,10 @@ public function stability(Stability $stability): self } /** - * @return $this + * Filters releases to include those with at least the specified minimum stability. + * + * @param Stability $stability Minimum stability level + * @return $this New filtered collection */ public function minimumStability(Stability $stability): self { @@ -88,7 +121,11 @@ public function minimumStability(Stability $stability): self } /** - * @return non-empty-string + * Converts version string to a format suitable for comparison. + * + * Normalizes version strings to handle stability suffixes properly. + * + * @return non-empty-string Normalized version string */ private function comparisonVersionString(ReleaseInterface $release): string { diff --git a/src/Module/Repository/Collection/RepositoriesCollection.php b/src/Module/Repository/Collection/RepositoriesCollection.php deleted file mode 100644 index 2bb5c91..0000000 --- a/src/Module/Repository/Collection/RepositoriesCollection.php +++ /dev/null @@ -1,44 +0,0 @@ - - */ - private array $repositories; - - /** - * @param array $repositories - */ - public function __construct(array $repositories) - { - $this->repositories = $repositories; - } - - /** - * @return non-empty-string - */ - public function getName(): string - { - return 'unknown/unknown'; - } - - public function getReleases(): ReleasesCollection - { - return ReleasesCollection::from(function () { - foreach ($this->repositories as $repository) { - yield from $repository->getReleases(); - } - }); - } -} diff --git a/src/Module/Repository/Internal/CachedGenerator.php b/src/Module/Repository/Internal/CachedGenerator.php new file mode 100644 index 0000000..b304174 --- /dev/null +++ b/src/Module/Repository/Internal/CachedGenerator.php @@ -0,0 +1,143 @@ + + * + * @internal + * @psalm-internal Internal\DLoad\Module\Repository + */ +final class CachedGenerator implements \IteratorAggregate +{ + /** + * @var array Cached items from the generator + */ + private array $cache = []; + + private ?\Generator $generator; + private bool $inited = false; + + /** + * @param \Traversable $generator The generator to cache + */ + public function __construct( + \Traversable $generator, + ) { + $this->generator = (static function (\Traversable $generator) { + foreach ($generator as $key => $value) { + yield $key => $value; + } + })($generator); + } + + /** + * Returns an iterator that yields the cached items and continues + * fetching from the generator when the cache is exhausted. + * + * @return \Traversable + */ + public function getIterator(): \Traversable + { + $index = 0; + + // First yield all cached items + while ($index < \count($this->cache)) { + yield $this->cache[$index]; + $index++; + } + + start: + $next = $this->rollItem(); + if ($this->generator === null) { + return; + } + + yield $next; + goto start; + } + + /** + * Returns the first item in the cache or from the generator. + * + * @return T|null The first item or null if the generator is empty + */ + public function first(): mixed + { + // If we have cached items, return the first one + return $this->cache === [] + ? $this->rollItem() + : $this->cache[0]; + } + + /** + * Returns whether the generator has any items. + * + * @return bool True if there are items, false otherwise + */ + public function isEmpty(): bool + { + // If we already have items, it's not empty + if ($this->cache !== []) { + return false; + } + + // Try to get the first item to determine if it's empty + return $this->first() === null; + } + + /** + * Returns the count of items in the generator. + * + * @return int<0, max> The number of items + */ + public function count(): int + { + if ($this->generator !== null) { + while ($this->generator !== null && $this->generator->valid()) { + $this->rollItem(); + } + } + + return \count($this->cache); + } + + /** + * Get the next item from the generator. + * + * @return T The next item + */ + private function rollItem(): mixed + { + if ($this->generator === null) { + return null; + } + + try { + if ($this->inited) { + $this->generator->next(); + } + + $this->inited = true; + /** @var T $next */ + $next = $this->generator->current(); + if (!$this->generator->valid()) { + $this->generator = null; + return null; + } + + $this->cache[] = $next; + } catch (\Throwable $e) { + $this->generator = null; + throw $e; + } + + return $next; + } +} diff --git a/src/Module/Repository/Internal/Collection.php b/src/Module/Repository/Internal/Collection.php index b0c63c6..8afb1b3 100644 --- a/src/Module/Repository/Internal/Collection.php +++ b/src/Module/Repository/Internal/Collection.php @@ -5,6 +5,25 @@ namespace Internal\DLoad\Module\Repository\Internal; /** + * Generic collection base class with filtering, mapping, and traversal capabilities. + * + * This abstract class provides common collection functionality for specialized + * collections like ReleasesCollection and AssetsCollection. + * + * ```php + * // Creating a collection from an array + * $collection = SomeCollection::create($itemsArray); + * + * // Creating a collection from a generator function + * $collection = SomeCollection::from(function() { + * yield new Item('one'); + * yield new Item('two'); + * }); + * + * // Finding the first item matching a condition + * $item = $collection->first(fn($item) => $item->getName() === 'specific'); + * ``` + * * @template T * @template-implements \IteratorAggregate * @@ -14,29 +33,44 @@ abstract class Collection implements \IteratorAggregate, \Countable { /** - * @var array + * Array of filter callbacks to apply when iterating + * + * @var array */ - protected array $items; + protected array $filters = []; /** - * @param array $items + * Maximum number of items to return from the collection + * Zero means no limit */ - final public function __construct(array $items) - { - $this->items = $items; - } + protected int $limitCount = 0; /** - * @param self|iterable|\Closure $items - * @return static + * @param array|CachedGenerator $items Collection items + */ + final public function __construct( + protected readonly array|CachedGenerator $items, + ) {} + + /** + * Creates a new collection from various sources. + * + * Supports creating from an existing collection, a traversable, + * an array, or a generator function. + * + * @template TNew + * + * @param iterable $items Source of items + * @return static New collection instance + * @throws \InvalidArgumentException If the input cannot be converted to a collection */ public static function create(mixed $items): static { return match (true) { $items instanceof static => $items, - $items instanceof \Traversable => new static(\iterator_to_array($items)), \is_array($items) => new static($items), - $items instanceof \Closure => static::from($items), + $items instanceof \Traversable => new static(new CachedGenerator($items)), + $items instanceof \Closure => static::create($items()), default => throw new \InvalidArgumentException( \sprintf('Unsupported iterable type %s.', \get_debug_type($items)), ), @@ -44,100 +78,210 @@ public static function create(mixed $items): static } /** - * @param \Closure $generator - * @return static - */ - public static function from(\Closure $generator): static - { - return static::create($generator()); - } - - /** - * @param callable(T): bool $filter - * @return $this + * Adds a filter to the collection. + * This does not immediately apply the filter, but stores it for later use during iteration. + * + * @param callable(T): bool $filter Function that returns true for items to keep + * @return $this New filtered collection */ public function filter(callable $filter): static { - return new static(\array_filter($this->items, $filter)); + // For iterables or collections with existing filters, add the filter to the pipeline + $clone = clone $this; + $clone->filters[] = $filter; + return $clone; } /** - * @param callable(T): mixed $map - * @return $this + * Maps each item in the collection using the provided callback. + * + * @param callable(T): mixed $map Function that transforms each item + * @return $this New collection with mapped items */ public function map(callable $map): static { - return new static(\array_map($map, $this->items)); + // For iterables or collections with filters, we need to materialize and map + $items = []; + foreach ($this as $item) { + $items[] = $map($item); + } + + return new static($items); } /** - * @param callable(T): bool $filter - * @return $this + * Creates a new collection excluding items that match the filter. + * + * @param callable(T): bool $filter Function that returns true for items to exclude + * @return $this New filtered collection * * @psalm-suppress MissingClosureParamType * @psalm-suppress MixedArgument */ public function except(callable $filter): static { - $callback = static fn(...$args): bool => ! $filter(...$args); - - return new static(\array_filter($this->items, $callback)); + $callback = static fn(...$args): bool => !$filter(...$args); + return $this->filter($callback); } /** - * @param null|callable(T): bool $filter - * @return T|null + * Returns the first item that matches the filter, or null if no matches. + * + * If no filter is provided, returns the first item in the collection. + * + * @param null|callable(T): bool $filter Optional filter function + * @return T|null First matching item or null */ - public function first(callable $filter = null): ?object + public function first(?callable $filter = null): ?object { - $self = $filter === null ? $this : $this->filter($filter); + $combinedFilter = $this->getCombinedFilter($filter); + if ($combinedFilter === null) { + return $this->items instanceof CachedGenerator + ? $this->items->first() + : ($this->items === [] ? null : $this->items[\array_key_first($this->items)]); + } + + // For iterables, iterate until we find a match + foreach ($this->getIterator() as $item) { + if ($filter === null || $filter($item)) { + return $item; + } + } - return $self->items === [] ? null : \reset($self->items); + return null; } /** - * @param callable(): T $otherwise - * @param null|callable(T): bool $filter - * @return T + * Returns the first matching item or a default value from the callback. + * + * @param callable(): T $otherwise Function to provide default value if no match found + * @param null|callable(T): bool $filter Optional filter function + * @return T First matching item or default value */ - public function firstOr(callable $otherwise, callable $filter = null): object + public function firstOr(callable $otherwise, ?callable $filter = null): object { return $this->first($filter) ?? $otherwise(); } + /** + * Limits the collection to a specified number of items. + * Using 0 as a count will reset any previous limit and return all items. + * + * @param int<0, max> $count Maximum number of items to include + * @return $this New collection with limited items + * @throws \InvalidArgumentException If count is negative + */ + public function limit(int $count): static + { + $clone = clone $this; + $clone->limitCount = $count; + return $clone; + } + + /** + * Returns an iterator for traversing the collection. + * + * @return \Traversable Collection iterator + */ public function getIterator(): \Traversable { - return new \ArrayIterator($this->items); + $combinedFilter = $this->getCombinedFilter(); + $itemCount = 0; + + // Apply filters and limit during iteration + foreach ($this->items as $item) { + if ($combinedFilter === null || $combinedFilter($item)) { + yield $item; + if ($this->limitCount > 0 && ++$itemCount >= $this->limitCount) { + return; + } + } + } } + /** + * Returns the number of items in the collection. + * + * @return int<0, max> Item count + */ public function count(): int { - return \count($this->items); + if ($this->filters === [] && $this->limitCount === 0) { + return $this->items instanceof CachedGenerator + ? $this->items->count() + : \count($this->items); + } + + // For iterables or with filters or limits, we need to count matching items + $count = 0; + foreach ($this as $item) { + $count++; + } + + return $count; } /** - * @param callable $then - * @return $this + * Executes a callback if the collection is empty. + * + * @param callable $then Function to execute if collection is empty + * @return $this This collection instance */ public function whenEmpty(callable $then): static { - if ($this->empty()) { - $then(); - } - + $this->empty() and $then(); return $this; } + /** + * Checks if the collection is empty. + * + * @return bool True if the collection has no items + */ public function empty(): bool { - return $this->items === []; + if ($this->filters === []) { + return $this->items instanceof CachedGenerator + ? $this->items->isEmpty() + : $this->items === []; + } + + // For iterables or with filters or limits, try to get the first item + return $this->first() === null; } /** - * @return array + * Converts the collection to an indexed array. + * + * @return array Array of collection items */ public function toArray(): array { - return \array_values($this->items); + return \iterator_to_array($this->getIterator()); + } + + /** + * Combines all filters with an optional additional filter. + * Returns null if there are no filters to apply. + * + * @param null|callable(T): bool $additionalFilter + * @return null|callable(T): bool + */ + private function getCombinedFilter(?callable $additionalFilter = null): ?callable + { + if ($this->filters === []) { + return $additionalFilter; + } + + // Combine collection filters with additional filter + return function ($item) use ($additionalFilter) { + foreach ($this->filters as $filter) { + if (!$filter($item)) { + return false; + } + } + + return $additionalFilter === null ? true : $additionalFilter($item); + }; } } diff --git a/src/Module/Repository/Internal/GitHub/Factory.php b/src/Module/Repository/Internal/GitHub/Factory.php index 3a9ea3e..55ad829 100644 --- a/src/Module/Repository/Internal/GitHub/Factory.php +++ b/src/Module/Repository/Internal/GitHub/Factory.php @@ -4,7 +4,9 @@ namespace Internal\DLoad\Module\Repository\Internal\GitHub; +use Internal\DLoad\Module\Common\Config\Embed\Repository as RepositoryConfig; use Internal\DLoad\Module\Common\Config\GitHub as GitHubConfig; +use Internal\DLoad\Module\Repository\RepositoryFactory; use Symfony\Component\HttpClient\HttpClient; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -12,18 +14,20 @@ * @internal * @psalm-internal Internal\DLoad\Module\Repository */ -final class Factory +final class Factory implements RepositoryFactory { public function __construct( private readonly GitHubConfig $config, ) {} - /** - * @param non-empty-string $uri Package name in format "owner/repository" or full URL - */ - public function create(string $uri): GitHubRepository + public function supports(RepositoryConfig $config): bool { - $uri = \parse_url($uri, PHP_URL_PATH) ?? $uri; + return \strtolower($config->type) === 'github'; + } + + public function create(RepositoryConfig $config): GitHubRepository + { + $uri = \parse_url($config->uri, PHP_URL_PATH) ?? $config->uri; [$org, $repo] = \array_slice(\explode('/', $uri), -2); return new GitHubRepository($org, $repo, $this->createClient()); diff --git a/src/Module/Repository/Internal/GitHub/GitHubAsset.php b/src/Module/Repository/Internal/GitHub/GitHubAsset.php index d005823..a1ed32e 100644 --- a/src/Module/Repository/Internal/GitHub/GitHubAsset.php +++ b/src/Module/Repository/Internal/GitHub/GitHubAsset.php @@ -67,7 +67,7 @@ public static function fromApiResponse(HttpClientInterface $client, GitHubReleas * * @throws ExceptionInterface */ - public function download(\Closure $progress = null): \Traversable + public function download(?\Closure $progress = null): \Traversable { $response = $this->client->request('GET', $this->getUri(), [ 'on_progress' => $progress, diff --git a/src/Module/Repository/Internal/GitHub/GitHubRelease.php b/src/Module/Repository/Internal/GitHub/GitHubRelease.php index a23539f..639ca88 100644 --- a/src/Module/Repository/Internal/GitHub/GitHubRelease.php +++ b/src/Module/Repository/Internal/GitHub/GitHubRelease.php @@ -51,7 +51,7 @@ public static function fromApiResponse(GitHubRepository $repository, HttpClientI $version = $data['tag_name'] ?? (string) $data['name']; $result = new self($client, $repository, $name, $version); - $result->assets = AssetsCollection::from(static function () use ($client, $result, $data): \Generator { + $result->assets = AssetsCollection::create(static function () use ($client, $result, $data): \Generator { /** @var GitHubAssetApiResponse $item */ foreach ($data['assets'] ?? [] as $item) { yield GitHubAsset::fromApiResponse($client, $result, $item); @@ -89,7 +89,6 @@ public function destroy(): void * @note The return value is "pretty", but that does not mean that the tag physically exists. * * @param array{tag_name: string|null, name: string|null} $data - * @return string */ private static function getTagName(array $data): string { diff --git a/src/Module/Repository/Internal/GitHub/GitHubRepository.php b/src/Module/Repository/Internal/GitHub/GitHubRepository.php index 98e296d..7819317 100644 --- a/src/Module/Repository/Internal/GitHub/GitHubRepository.php +++ b/src/Module/Repository/Internal/GitHub/GitHubRepository.php @@ -5,7 +5,8 @@ namespace Internal\DLoad\Module\Repository\Internal\GitHub; use Internal\DLoad\Module\Repository\Collection\ReleasesCollection; -use Internal\DLoad\Module\Repository\RepositoryInterface; +use Internal\DLoad\Module\Repository\Internal\Paginator; +use Internal\DLoad\Module\Repository\Repository; use Internal\DLoad\Service\Destroyable; use Symfony\Component\HttpClient\HttpClient; use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; @@ -18,12 +19,11 @@ * @internal * @psalm-internal Internal\DLoad\Module\Repository\GitHub */ -final class GitHubRepository implements RepositoryInterface, Destroyable +final class GitHubRepository implements Repository, Destroyable { private const URL_RELEASES = 'https://api.github.com/repos/%s/releases'; private HttpClientInterface $client; - private ?ReleasesCollection $releases = null; /** @@ -44,35 +44,65 @@ final class GitHubRepository implements RepositoryInterface, Destroyable * @param non-empty-string $org * @param non-empty-string $repo */ - public function __construct(string $org, string $repo, HttpClientInterface $client = null) + public function __construct(string $org, string $repo, ?HttpClientInterface $client = null) { $this->name = $org . '/' . $repo; $this->client = $client ?? HttpClient::create(); } /** + * Returns a lazily loaded collection of repository releases. + * Pages are loaded only when needed during iteration or filtering. + * * @throws ExceptionInterface */ public function getReleases(): ReleasesCollection { - return $this->releases ??= ReleasesCollection::from(function () { + if ($this->releases !== null) { + return $this->releases; + } + + // Create a generator function for lazy loading release pages + $pageLoader = function (): \Generator { $page = 0; - // Iterate over all pages do { - $response = $this->releasesRequest(++$page); + try { + // to avoid first eager loading because of generator + yield []; + + $response = $this->releasesRequest(++$page); + + /** @psalm-var array $data */ + $data = $response->toArray(); + + // If empty response, no more pages + if ($data === []) { + return; + } - /** @psalm-var GitHubReleaseApiResponse $data */ - foreach ($response->toArray() as $data) { - yield GitHubRelease::fromApiResponse($this, $this->client, $data); + yield \array_map( + fn(array $releaseData): GitHubRelease => GitHubRelease::fromApiResponse($this, $this->client, $releaseData), + $data, + ); + + // Check if there are more pages + $hasMorePages = $this->hasNextPage($response); + } catch (ExceptionInterface) { + return; } - } while ($this->hasNextPage($response)); - }); + } while ($hasMorePages); + }; + + // Create paginator + $paginator = Paginator::createFromGenerator($pageLoader(), null); + + // Create a collection with the paginator + $this->releases = ReleasesCollection::create($paginator); + + return $this->releases; } - /** - * @return string - */ public function getName(): string { return $this->name; @@ -88,10 +118,6 @@ public function destroy(): void } /** - * @param string $method - * @param string $uri - * @param array $options - * @return ResponseInterface * @throws TransportExceptionInterface * @see HttpClientInterface::request() */ diff --git a/src/Module/Repository/Internal/Paginator.php b/src/Module/Repository/Internal/Paginator.php new file mode 100644 index 0000000..032d519 --- /dev/null +++ b/src/Module/Repository/Internal/Paginator.php @@ -0,0 +1,127 @@ + + * + * @internal + * @psalm-internal Internal\DLoad\Module\Repository + */ +final class Paginator implements \IteratorAggregate, \Countable +{ + /** @var list */ + private array $collection; + + /** @var self|null */ + private ?self $nextPage = null; + + private ?int $totalItems = null; + + /** + * @param \Generator, mixed, mixed> $loader + * @param int<1, max> $pageNumber + * @param null|\Closure(): int<0, max> $counter + */ + private function __construct( + private readonly \Generator $loader, + private readonly int $pageNumber, + private ?\Closure $counter, + ) { + $this->collection = $loader->valid() ? $loader->current() : []; + } + + /** + * @template TInitItem + * + * @param \Generator> $loader + * @param null|callable(): int<0, max> $counter Returns total number of items. + * + * @return self + */ + public static function createFromGenerator(\Generator $loader, ?callable $counter): self + { + return new self($loader, 1, $counter === null ? null : $counter(...)); + } + + /** + * Load next page. + * + * @return self|null + */ + public function getNextPage(): ?self + { + if ($this->nextPage !== null) { + return $this->nextPage; + } + + $this->loader->next(); + if (!$this->loader->valid()) { + return null; + } + $this->nextPage = new self($this->loader, $this->pageNumber + 1, $this->counter); + /** @psalm-suppress UnsupportedPropertyReferenceUsage */ + $this->nextPage->counter = &$this->counter; + + return $this->nextPage; + } + + /** + * @return array + */ + public function getPageItems(): array + { + return $this->collection; + } + + /** + * Iterate all items from current page and all next pages. + * + * @return \Traversable + */ + public function getIterator(): \Traversable + { + $paginator = $this; + while ($paginator !== null) { + foreach ($paginator->getPageItems() as $item) { + yield $item; + } + + $paginator = $paginator->getNextPage(); + } + } + + /** + * @return int<1, max> + */ + public function getPageNumber(): int + { + return $this->pageNumber; + } + + /** + * Value is cached in all produced pages after first call in any page. + * + * Note: the method may call yet another RPC to get total number of items. + * It means that the result may be different from the number of items at the moment of the pagination start. + * + * @throws \LogicException If counter is not set. + */ + public function count(): int + { + if ($this->totalItems !== null) { + return $this->totalItems; + } + + if ($this->counter === null) { + throw new \LogicException('Paginator does not support counting.'); + } + + return $this->totalItems = ($this->counter)(); + } +} diff --git a/src/Module/Repository/Internal/Release.php b/src/Module/Repository/Internal/Release.php index 854cfa0..7b9213b 100644 --- a/src/Module/Repository/Internal/Release.php +++ b/src/Module/Repository/Internal/Release.php @@ -9,7 +9,7 @@ use Internal\DLoad\Module\Common\Stability; use Internal\DLoad\Module\Repository\Collection\AssetsCollection; use Internal\DLoad\Module\Repository\ReleaseInterface; -use Internal\DLoad\Module\Repository\RepositoryInterface; +use Internal\DLoad\Module\Repository\Repository; /** * @internal @@ -25,16 +25,14 @@ abstract class Release implements ReleaseInterface protected string $name; protected Stability $stability; - protected AssetsCollection $assets; /** * @param non-empty-string $name * @param non-empty-string $version - * @param iterable $assets */ public function __construct( - protected RepositoryInterface $repository, + protected Repository $repository, string $name, protected string $version, ?Stability $stability = null, @@ -45,7 +43,7 @@ public function __construct( $this->stability = $stability ?? $this->parseStability($version); } - public function getRepository(): RepositoryInterface + public function getRepository(): Repository { return $this->repository; } diff --git a/src/Module/Repository/ReleaseInterface.php b/src/Module/Repository/ReleaseInterface.php index 1c29fe8..4528dbe 100644 --- a/src/Module/Repository/ReleaseInterface.php +++ b/src/Module/Repository/ReleaseInterface.php @@ -7,29 +7,73 @@ use Internal\DLoad\Module\Common\Stability; use Internal\DLoad\Module\Repository\Collection\AssetsCollection; +/** + * Represents a single release of software from a repository. + * + * A release contains information about its version, stability and associated assets + * that can be downloaded. Release objects provide access to all distributable files + * for a particular software version. + * + * ```php + * $releases = $repository->getReleases() + * ->minimumStability(Stability::Stable) + * ->satisfies('^2.0.0') + * ->sortByVersion(); + * + * $latestRelease = $releases->first(); + * $assets = $latestRelease->getAssets(); + * ``` + */ interface ReleaseInterface { - public function getRepository(): RepositoryInterface; + /** + * Returns the repository this release belongs to. + * + * @return Repository The parent repository + */ + public function getRepository(): Repository; /** * Returns Composer's compatible "pretty" release version. * - * @return non-empty-string + * This version is formatted for semantic versioning compatibility. + * + * @return non-empty-string Formatted version string (e.g. "1.2.3") */ public function getName(): string; /** * Returns internal release tag version. * - * @note this version may not be compatible with Composer's comparators. + * This version string may include prefixes or suffixes that aren't + * compatible with Composer's comparators. * - * @return non-empty-string + * @return non-empty-string Raw version string (e.g. "v1.2.3-beta") */ public function getVersion(): string; + /** + * Returns the stability level of this release. + * + * @return Stability Enum representing the stability level (Stable, RC, Beta, etc.) + */ public function getStability(): Stability; + /** + * Returns all assets associated with this release. + * + * @return AssetsCollection Collection of downloadable assets + */ public function getAssets(): AssetsCollection; + /** + * Checks if this release satisfies the given version constraint. + * + * Uses Composer's version comparison logic to determine if this release + * satisfies the specified constraint. + * + * @param string $constraint Version constraint in Composer format (e.g. "^1.0", ">2.5") + * @return bool True if the release satisfies the constraint + */ public function satisfies(string $constraint): bool; } diff --git a/src/Module/Repository/Repository.php b/src/Module/Repository/Repository.php new file mode 100644 index 0000000..2297e90 --- /dev/null +++ b/src/Module/Repository/Repository.php @@ -0,0 +1,36 @@ +getByConfig($repoConfig); + * $releases = $repository->getReleases(); + * $filteredReleases = $releases->satisfies('^2.0.0')->sortByVersion(); + * ``` + */ +interface Repository +{ + /** + * Returns the unique identifier of the repository. + * + * @return non-empty-string Repository identifier, typically in vendor/package format + */ + public function getName(): string; + + /** + * Retrieves all available releases from this repository. + * + * @return ReleasesCollection Collection of releases available in this repository + */ + public function getReleases(): ReleasesCollection; +} diff --git a/src/Module/Repository/RepositoryFactory.php b/src/Module/Repository/RepositoryFactory.php new file mode 100644 index 0000000..00ec5d7 --- /dev/null +++ b/src/Module/Repository/RepositoryFactory.php @@ -0,0 +1,30 @@ +get(RepositoryProvider::class); + * + * // Create a repository instance from configuration + * $config = new Repository('github', 'vendor/package'); + * $repository = $provider->getByConfig($config); + * ``` + * * @internal */ final class RepositoryProvider { - public function __construct( - private readonly GithubFactory $githubFactory, - ) {} + /** @var RepositoryFactory[] $factories */ + private array $factories = []; - public function getByConfig(Repository $config): RepositoryInterface + /** + * Adds a repository factory to the provider. + */ + public function addRepositoryFactory(RepositoryFactory $factory): self { - return match (\strtolower($config->type)) { - 'github' => $this->githubFactory->create($config->uri), - default => throw new \RuntimeException("Unknown repository type `$config->type`."), - }; + $this->factories[] = $factory; + return $this; + } + + /** + * Creates a repository instance based on the provided configuration. + * + * @param RepositoryConfig $config Repository configuration + * @return Repository Created repository instance + * @throws \RuntimeException When no factory supports the repository type + */ + public function getByConfig(RepositoryConfig $config): Repository + { + foreach ($this->factories as $factory) { + if ($factory->supports($config)) { + return $factory->create($config); + } + } + + throw new \RuntimeException("No factory found for repository type `$config->type`."); } } diff --git a/src/Service/Container.php b/src/Service/Container.php index 7a27efb..f39b318 100644 --- a/src/Service/Container.php +++ b/src/Service/Container.php @@ -5,53 +5,79 @@ namespace Internal\DLoad\Service; /** - * Application container. + * Application dependency injection container. + * + * Manages service instances throughout the application lifecycle. Services are lazy-loaded + * and cached for reuse. The container handles dependencies between services and + * provides a way to customize service instantiation through bindings. + * + * ```php + * // Retrieving a service instance + * $downloader = $container->get(Downloader::class); + * + * // Binding a factory for service creation + * $container->bind(Logger::class, function (Container $c) { + * return new Logger($c->get(OutputInterface::class)); + * }); + * ``` * * @internal */ -interface Container +interface Container extends Destroyable { /** + * Retrieves a service from the container. + * + * If the service is requested for the first time, it will be instantiated and persisted for future requests. + * * @template T - * @param class-string $id - * @param array $arguments Will be used if the object is created for the first time. - * @return T + * @param class-string $id Service identifier + * @param array $arguments Constructor arguments used only on first instantiation + * @return T The requested service instance * * @psalm-suppress MoreSpecificImplementedParamType, InvalidReturnType */ public function get(string $id, array $arguments = []): object; /** - * @param class-string $id + * Checks if the service is registered in the container. + * + * It means that the container has a cached service instance or a binding. + * + * @param class-string $id Service identifier + * @return bool Whether the service is available * * @psalm-suppress MoreSpecificImplementedParamType */ public function has(string $id): bool; /** - * @template T of object - * @param T $service - * @param class-string|null $id + * Registers an existing service instance in the container. + * + * @template T + * @param T $service Service instance to register + * @param class-string|null $id Optional service identifier (defaults to object's class) */ public function set(object $service, ?string $id = null): void; /** - * Create an object of the specified class without caching. + * Creates a new instance without storing it in the container. * * @template T - * @param class-string $class - * @return T + * @param class-string $class Class to instantiate + * @param array $arguments Constructor arguments + * @return T Newly created instance */ public function make(string $class, array $arguments = []): object; /** - * Declare a factory or predefined arguments for the specified class. + * Declares a factory or predefined arguments for the specified class. * - * @template T of object - * @param class-string $id - * @param array|\Closure(Container): T $binding + * Configures how a service should be instantiated. + * + * @template T + * @param class-string $id Service identifier + * @param null|array|\Closure(Container): T $binding Factory function or constructor arguments */ public function bind(string $id, \Closure|array|null $binding = null): void; - - public function destroy(): void; } diff --git a/src/Service/Destroyable.php b/src/Service/Destroyable.php index 4e70e34..7c1f715 100644 --- a/src/Service/Destroyable.php +++ b/src/Service/Destroyable.php @@ -5,11 +5,33 @@ namespace Internal\DLoad\Service; /** - * Should be used to destroy objects and free resources. + * Interface for services that need cleanup on destruction. + * + * Implementing classes should release resources properly when no longer needed. + * + * ```php + * class FileHandler implements Destroyable + * { + * private $handle; + * + * public function destroy(): void + * { + * if ($this->handle) { + * fclose($this->handle); + * $this->handle = null; + * } + * } + * } + * ``` * * @internal */ interface Destroyable { + /** + * Performs cleanup before object destruction. + * + * Called by the Container when shutting down or releasing services. + */ public function destroy(): void; } diff --git a/src/Service/Factoriable.php b/src/Service/Factoriable.php index 2b6075b..085de08 100644 --- a/src/Service/Factoriable.php +++ b/src/Service/Factoriable.php @@ -5,10 +5,29 @@ namespace Internal\DLoad\Service; /** - * Class creates new instances of itself. + * Interface for classes that can create instances of themselves. * - * @method static create - * Method creates new instance of the class. May contain any injectable parameters. + * Implementing classes should provide the static factory method `create()` for instantiation. + * This pattern is useful for objects that require complex initialization logic or dependency resolution. + * + * ```php + * class Config implements Factoriable + * { + * private function __construct( + * private Logger $logger, + * ) {} + * + * public static function create(Logger $logger): self + * { + * return new self($logger); + * } + * } + * + * $container->get(Config::class); // Will be created via the `create()` method with autowiring + * ``` + * + * @method static self create + * Method creates new instance of the class with injectable parameters * * @internal */ diff --git a/src/Service/Logger.php b/src/Service/Logger.php index 6dd0a79..9c0add7 100644 --- a/src/Service/Logger.php +++ b/src/Service/Logger.php @@ -7,16 +7,29 @@ use Symfony\Component\Console\Output\OutputInterface; /** - * Console color logger + * Console color logger for terminal output. + * + * Provides formatted console output with color support and verbosity control. + * + * ```php + * $logger = new Logger($output); + * $logger->info("Processing %s files", $count); + * $logger->error("Failed to download %s", $url); + * ``` * * @internal */ final class Logger { + /** @var bool Whether debug messages should be displayed */ private readonly bool $debug; + /** @var bool Whether verbose messages should be displayed */ private readonly bool $verbose; + /** + * @param OutputInterface|null $output Console output interface + */ public function __construct( private readonly ?OutputInterface $output = null, ) { @@ -24,31 +37,68 @@ public function __construct( $this->verbose = $output?->isVerbose() ?? false; } + /** + * Outputs a message with a newline. + * + * @param string $message Text to output + */ public function print(string $message): void { $this->echo($message . "\n", false); } + /** + * Displays a highlighted status message with sender identification. + * + * @param non-empty-string $sender Source component name + * @param non-empty-string $message Message format string + * @param string|int|float|bool ...$values Values to format the message + */ public function status(string $sender, string $message, string|int|float|bool ...$values): void { - $this->echo("\033[47;1;30m " . $sender . " \033[0m " . \sprintf($message, ...$values) . "\n\n", false); + $this->echo("\033[47;1;30m " . $sender . " \033[0m " . \sprintf($message, ...self::values($values)) . "\n\n", false); } + /** + * Outputs a success/confirmation message in green. + * + * @param string $message Message format string + * @param string|int|float|bool ...$values Format values + */ public function info(string $message, string|int|float|bool ...$values): void { - $this->echo("\033[32m" . \sprintf($message, ...$values) . "\033[0m\n", false); + $this->echo("\033[32m" . \sprintf($message, ...self::values($values)) . "\033[0m\n", false); } + /** + * Outputs a debug message in blue (only when debug mode is enabled). + * + * @param string $message Message format string + * @param string|int|float|bool ...$values Format values + */ public function debug(string $message, string|int|float|bool ...$values): void { - $this->echo("\033[34m" . \sprintf($message, ...$values) . "\033[0m\n"); + $this->echo("\033[34m" . \sprintf($message, ...self::values($values)) . "\033[0m\n"); } + /** + * Outputs an error message in red. + * + * @param string $message Message format string + * @param string|int|float|bool ...$values Format values + */ public function error(string $message, string|int|float|bool ...$values): void { - $this->echo("\033[31m" . \sprintf($message, ...$values) . "\033[0m\n"); + $this->echo("\033[31m" . \sprintf($message, ...self::values($values)) . "\033[0m\n"); } + /** + * Formats and outputs exception details. + * + * @param \Throwable $e Exception to display + * @param string|null $header Optional header text + * @param bool $important Whether to show regardless of debug setting + */ public function exception(\Throwable $e, ?string $header = null, bool $important = false): void { $r = "----------------------\n"; @@ -69,6 +119,31 @@ public function exception(\Throwable $e, ?string $header = null, bool $important $this->echo($r, !$important); } + /** + * Converts values to string representations. + * + * @param array<\Stringable|string|int|float|bool> $values Values to convert + * @return array Converted values + */ + private static function values(array $values): array + { + $result = []; + foreach ($values as $k => $value) { + $result[$k] = match (true) { + \is_bool($value) => $value ? 'TRUE' : 'FALSE', + default => (string) $value, + }; + } + + return $result; + } + + /** + * Internal method to output text based on verbosity settings. + * + * @param string $message Text to output + * @param bool $debug Whether message should be shown only in debug mode + */ private function echo(string $message, bool $debug = true): void { if ($debug && !$this->debug) { diff --git a/tests/Arch/ArchTest.php b/tests/Arch/ArchTest.php new file mode 100644 index 0000000..2599b16 --- /dev/null +++ b/tests/Arch/ArchTest.php @@ -0,0 +1,43 @@ +layer(); + + foreach ($layer as $object) { + foreach ($object->uses as $use) { + foreach ($functions as $function) { + $function === $use and throw new \Exception( + \sprintf( + 'Function `%s()` is used in %s.', + $function, + $object->name, + ), + ); + } + } + } + + $this->assertTrue(true); + } +} diff --git a/tests/Arch/DebugTest.php b/tests/Arch/DebugTest.php deleted file mode 100644 index 8c12935..0000000 --- a/tests/Arch/DebugTest.php +++ /dev/null @@ -1,8 +0,0 @@ -expect(['dd', 'exit', 'die', 'var_dump', 'echo', 'print', 'trap', 'td', 'tr', 'error_log']) - ->not - ->toBeUsed(); diff --git a/tests/Integration/Module/Archive/ArchiveIntegrationTest.php b/tests/Integration/Module/Archive/ArchiveIntegrationTest.php new file mode 100644 index 0000000..fe3c1d6 --- /dev/null +++ b/tests/Integration/Module/Archive/ArchiveIntegrationTest.php @@ -0,0 +1,134 @@ + ['zip', 'Internal\DLoad\Module\Archive\Internal\ZipPharArchive']; + yield 'tar.gz' => ['tar.gz', 'Internal\DLoad\Module\Archive\Internal\TarPharArchive']; + yield 'phar' => ['phar', 'Internal\DLoad\Module\Archive\Internal\PharArchive']; + } + + #[DataProvider('provideArchiveTypes')] + public function testFactoryCreateReturnsCorrectImplementation( + string $extension, + string $className, + ): void { + // Skip if we can't verify the implementation type + if (!\class_exists($className)) { + self::markTestSkipped("Class $className not available"); + } + + // Arrange - create mock file with extension + $file = $this->createMock(\SplFileInfo::class); + $file->method('getFilename')->willReturn('test.' . $extension); + $file->method('isFile')->willReturn(true); + $file->method('isReadable')->willReturn(true); + + // Act - create archive handler + try { + $archive = $this->factory->create($file); + + // Assert - check implementation type + self::assertInstanceOf($className, $archive); + } catch (\InvalidArgumentException $e) { + // If creation fails due to real file requirements, just verify supported extensions + $extensions = $this->factory->getSupportedExtensions(); + self::assertContains($extension, $extensions); + } + } + + public function testFactoryExtendWithCustomImplementation(): void + { + // Arrange - create custom archive mock + $customArchive = $this->createMock('Internal\DLoad\Module\Archive\Archive'); + + // Register custom implementation for .custom extension + $this->factory->extend( + static fn(\SplFileInfo $file) => + \str_ends_with($file->getFilename(), '.custom') ? $customArchive : null, + ['custom'], + ); + + // Create mock file with custom extension + $file = $this->createMock(\SplFileInfo::class); + $file->method('getFilename')->willReturn('test.custom'); + $file->method('isFile')->willReturn(true); + $file->method('isReadable')->willReturn(true); + + // Act + $archive = $this->factory->create($file); + + // Assert + self::assertSame($customArchive, $archive); + } + + protected function setUp(): void + { + // Skip tests if phar extension is not available + if (!\class_exists(\PharData::class)) { + self::markTestSkipped('Phar extension is not available'); + } + + // Create temporary directory for test files in project runtime + $projectRoot = \dirname(__DIR__, 4); // Four levels up from this file + $this->tempDir = $projectRoot . '/runtime/tests/archive-integration-' . \uniqid(); + \mkdir($this->tempDir, 0777, true); + + // Create factory + $this->factory = new ArchiveFactory(); + } + + protected function tearDown(): void + { + // Clean up temporary directory + if (\is_dir($this->tempDir)) { + $this->removeDirectory($this->tempDir); + } + } + + /** + * Recursively remove a directory and its contents + */ + private function removeDirectory(string $dir): void + { + if (!\is_dir($dir)) { + return; + } + + $items = \scandir($dir); + foreach ($items as $item) { + if ($item === '.' || $item === '..') { + continue; + } + + $path = $dir . '/' . $item; + if (\is_dir($path)) { + $this->removeDirectory($path); + } else { + \unlink($path); + } + } + + \rmdir($dir); + } +} diff --git a/tests/Unit/InfoTest.php b/tests/Unit/InfoTest.php deleted file mode 100644 index bece496..0000000 --- a/tests/Unit/InfoTest.php +++ /dev/null @@ -1,15 +0,0 @@ -getMessage()); + } + + public function testExceptionCanHaveCustomCode(): void + { + // Arrange + $code = 123; + + // Act + $exception = new ArchiveException('Test message', $code); + + // Assert + self::assertSame($code, $exception->getCode()); + } + + public function testExceptionCanHavePreviousException(): void + { + // Arrange + $previous = new \Exception('Previous error'); + + // Act + $exception = new ArchiveException('Test message', 0, $previous); + + // Assert + self::assertSame($previous, $exception->getPrevious()); + } +} diff --git a/tests/Unit/Module/Archive/API/ArchiveFactoryTest.php b/tests/Unit/Module/Archive/API/ArchiveFactoryTest.php new file mode 100644 index 0000000..c4bab0f --- /dev/null +++ b/tests/Unit/Module/Archive/API/ArchiveFactoryTest.php @@ -0,0 +1,197 @@ + ['zip']; + yield 'tar.gz extension' => ['tar.gz']; + yield 'phar extension' => ['phar']; + } + + public static function provideArchiveFiles(): \Generator + { + yield 'zip file' => ['archive.zip', ZipPharArchive::class]; + yield 'tar.gz file' => ['archive.tar.gz', TarPharArchive::class]; + yield 'phar file' => ['archive.phar', PharArchive::class]; + } + + #[DataProvider('provideDefaultSupportedExtensions')] + public function testGetSupportedExtensionsReturnsDefaultExtensions(string $extension): void + { + // Act + $extensions = $this->factory->getSupportedExtensions(); + + // Assert + self::assertContains($extension, $extensions); + } + + #[DataProvider('provideArchiveFiles')] + public function testCreateReturnsCorrectArchiveTypeForExtension( + string $filename, + string $expectedClass, + ): void { + // Skip test if the fixture wasn't created + $extension = \pathinfo($filename, PATHINFO_EXTENSION); + if ($extension === 'gz') { + $extension = 'tar.gz'; + } + + if (!isset(self::$archiveFixtures[$extension])) { + self::markTestSkipped("Archive fixture for {$extension} could not be created"); + } + + // Arrange - use actual file + $filePath = self::$archiveFixtures[$extension]; + $file = new \SplFileInfo($filePath); + + // Act + $archive = $this->factory->create($file); + + // Assert + self::assertInstanceOf($expectedClass, $archive); + } + + public function testExtendAddsCustomMatcher(): void + { + // Arrange + $mockArchive = $this->createMock(Archive::class); + $customExtension = 'custom'; + + $this->factory->extend( + static fn(\SplFileInfo $file) => + \str_ends_with($file->getFilename(), '.custom') ? $mockArchive : null, + [$customExtension], + ); + + $file = $this->createFileInfoMock('test.custom'); + + // Act + $result = $this->factory->create($file); + $extensions = $this->factory->getSupportedExtensions(); + + // Assert + self::assertSame($mockArchive, $result); + self::assertContains($customExtension, $extensions); + } + + public function testCreateThrowsExceptionWhenNoMatcherFound(): void + { + // Arrange + $file = $this->createFileInfoMock('unsupported.format'); + + // Assert + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Can not open the archive "unsupported.format"'); + + // Act + $this->factory->create($file); + } + + public function testExtendPrioritizesNewMatchersOverExisting(): void + { + // Arrange + $mockArchive = $this->createMock(Archive::class); + $zipFile = $this->createFileInfoMock('test.zip'); + + // Override the default zip handler + $this->factory->extend( + static fn(\SplFileInfo $file) => + \str_ends_with($file->getFilename(), '.zip') ? $mockArchive : null, + [], + ); + + // Act + $result = $this->factory->create($zipFile); + + // Assert + self::assertSame($mockArchive, $result); + } + + public function testCreateCollectsErrorsFromFailedMatchers(): void + { + // Arrange + $file = $this->createFileInfoMock('test.zip'); + + // Add matchers that throw exceptions + $this->factory->extend( + static function (): never { + throw new \RuntimeException('First matcher error'); + }, + ); + + $this->factory->extend( + static function (): never { + throw new \RuntimeException('Second matcher error'); + }, + ); + + // Assert + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('First matcher error'); + $this->expectExceptionMessage('Second matcher error'); + + // Act + $this->factory->create($file); + } + + public static function setUpBeforeClass(): void + { + // Define project's test runtime directory + $projectRoot = \dirname(__DIR__, 5); // Five levels up from this file + self::$fixturesDir = $projectRoot . '/runtime/tests/archive-fixtures'; + + // Create archive fixtures + self::$fixtureGenerator = new ArchiveFixtureGenerator(self::$fixturesDir); + self::$archiveFixtures = self::$fixtureGenerator->generateArchives(); + } + + public static function tearDownAfterClass(): void + { + // Clean up fixtures + if (self::$fixtureGenerator !== null) { + self::$fixtureGenerator->cleanup(); + } + } + + protected function setUp(): void + { + // Arrange + $this->factory = new ArchiveFactory(); + } + + /** + * Creates a mock SplFileInfo that returns the given filename + * and is configured as a valid, readable file + */ + private function createFileInfoMock(string $filename): \SplFileInfo + { + $file = $this->createMock(\SplFileInfo::class); + $file->method('getFilename')->willReturn($filename); + $file->method('isFile')->willReturn(true); + $file->method('isReadable')->willReturn(true); + $file->method('getPathname')->willReturn('/path/to/' . $filename); + + return $file; + } +} diff --git a/tests/Unit/Module/Archive/API/ArchiveTest.php b/tests/Unit/Module/Archive/API/ArchiveTest.php new file mode 100644 index 0000000..6927bff --- /dev/null +++ b/tests/Unit/Module/Archive/API/ArchiveTest.php @@ -0,0 +1,119 @@ + new \SplFileInfo('/path/to/file1.txt'), + 'file2.php' => new \SplFileInfo('/path/to/file2.php'), + 'file3.php' => new \SplFileInfo('/path/to/file3.php'), + 'file4.json' => new \SplFileInfo('/path/to/file4.json'), + ]; + + yield 'txt files' => [$files, 'txt', 1]; + yield 'php files' => [$files, 'php', 2]; + yield 'json files' => [$files, 'json', 1]; + yield 'non-existent extension' => [$files, 'jpg', 0]; + } + + public function testExtractYieldsFilesFromArchive(): void + { + // Arrange + $file1 = new \SplFileInfo('/path/to/file1.txt'); + $file2 = new \SplFileInfo('/path/to/file2.txt'); + + $archive = new TestArchive($this->archiveFile); + $archive->addFile('file1.txt', $file1); + $archive->addFile('file2.txt', $file2); + + // Act + $result = []; + foreach ($archive->extract() as $path => $fileInfo) { + $result[$path] = $fileInfo; + } + + // Assert + self::assertCount(2, $result); + self::assertSame($file1, $result['file1.txt']); + self::assertSame($file2, $result['file2.txt']); + } + + public function testExtractThrowsArchiveException(): void + { + // Arrange + $archive = new TestArchive($this->archiveFile); + $archive->throwExceptionOnExtract('Custom error message'); + + // Assert + $this->expectException(ArchiveException::class); + $this->expectExceptionMessage('Custom error message'); + + // Act + \iterator_to_array($archive->extract()); + } + + public function testExtractReturnsDestinationFileWhenRequested(): void + { + // Arrange + $sourceFile = new \SplFileInfo('/path/to/source.txt'); + $destinationFile = new \SplFileInfo('/path/to/destination.txt'); + + $archive = new TestArchive($this->archiveFile); + $archive->addFile('source.txt', $sourceFile); + + // Act + $generator = $archive->extract(); + $path = $generator->key(); + $info = $generator->current(); + $result = $generator->send($destinationFile); + + // Assert + self::assertSame('source.txt', $path); + self::assertSame($sourceFile, $info); + self::assertSame($destinationFile, $result); + } + + #[DataProvider('provideFileTypes')] + public function testExtractFilteringByFileType(array $files, string $extension, int $expectedCount): void + { + // Arrange + $archive = new TestArchive($this->archiveFile); + + foreach ($files as $path => $fileInfo) { + $archive->addFile($path, $fileInfo); + } + + // Act + $extracted = []; + foreach ($archive->extract() as $path => $fileInfo) { + // Filter files by extension + if (\pathinfo($path, PATHINFO_EXTENSION) === $extension) { + $extracted[$path] = $fileInfo; + } + } + + // Assert + self::assertCount($expectedCount, $extracted); + } + + protected function setUp(): void + { + // Arrange + $this->archiveFile = new \SplFileInfo(__FILE__); // Use this file as a valid file + } +} diff --git a/tests/Unit/Module/Archive/Internal/ArchiveTest.php b/tests/Unit/Module/Archive/Internal/ArchiveTest.php new file mode 100644 index 0000000..2569c3c --- /dev/null +++ b/tests/Unit/Module/Archive/Internal/ArchiveTest.php @@ -0,0 +1,72 @@ +createMock(\SplFileInfo::class); + $file->method('isFile')->willReturn(false); + $file->method('getFilename')->willReturn('non-existent.zip'); + + // Assert + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Archive "non-existent.zip" is not a file.'); + + // Act + $this->createArchiveInstance($file); + } + + public function testConstructorThrowsExceptionWhenFileIsNotReadable(): void + { + // Arrange + $file = $this->createMock(\SplFileInfo::class); + $file->method('isFile')->willReturn(true); + $file->method('isReadable')->willReturn(false); + $file->method('getFilename')->willReturn('unreadable.zip'); + + // Assert + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Archive file "unreadable.zip" is not readable.'); + + // Act + $this->createArchiveInstance($file); + } + + public function testConstructorSucceedsWithValidFile(): void + { + // Arrange + $file = $this->createMock(\SplFileInfo::class); + $file->method('isFile')->willReturn(true); + $file->method('isReadable')->willReturn(true); + + // Act + $archive = $this->createArchiveInstance($file); + + // Assert + self::assertInstanceOf(Archive::class, $archive); + } + + /** + * Creates a concrete implementation of the abstract Archive class for testing + */ + private function createArchiveInstance(\SplFileInfo $file): Archive + { + return new class($file) extends Archive { + public function extract(): \Generator + { + // Minimal implementation for testing the constructor + yield 'test' => new \SplFileInfo('test'); + } + }; + } +} diff --git a/tests/Unit/Module/Archive/Internal/PharAwareArchiveTest.php b/tests/Unit/Module/Archive/Internal/PharAwareArchiveTest.php new file mode 100644 index 0000000..c5cabbf --- /dev/null +++ b/tests/Unit/Module/Archive/Internal/PharAwareArchiveTest.php @@ -0,0 +1,62 @@ +createMock(\PharData::class); + $pharData->method('isReadable')->willReturn(false); + $pharData->method('getPathname')->willReturn('unreadable.phar'); + + $archive = $this->createPharAwareArchive($pharData); + + // Assert + $this->expectException(ArchiveException::class); + $this->expectExceptionMessage('Could not open "unreadable.phar" for reading.'); + + // Act + \iterator_to_array($archive->extract()); + } + + protected function setUp(): void + { + // Arrange + $this->fileInfo = $this->createMock(\SplFileInfo::class); + $this->fileInfo->method('isFile')->willReturn(true); + $this->fileInfo->method('isReadable')->willReturn(true); + } + + /** + * Creates a concrete implementation of the abstract PharAwareArchive for testing + */ + private function createPharAwareArchive(\PharData $pharData): PharAwareArchive + { + return new class($this->fileInfo, $pharData) extends PharAwareArchive { + private \PharData $testPharData; + + public function __construct(\SplFileInfo $archive, \PharData $pharData) + { + $this->testPharData = $pharData; + parent::__construct($archive); + } + + protected function open(\SplFileInfo $file): \PharData + { + return $this->testPharData; + } + }; + } +} diff --git a/tests/Unit/Module/Archive/Internal/TarPharArchiveTest.php b/tests/Unit/Module/Archive/Internal/TarPharArchiveTest.php new file mode 100644 index 0000000..b6c0097 --- /dev/null +++ b/tests/Unit/Module/Archive/Internal/TarPharArchiveTest.php @@ -0,0 +1,29 @@ +createMock(\SplFileInfo::class); + $file->method('isFile')->willReturn(true); + $file->method('isReadable')->willReturn(false); + $file->method('getFilename')->willReturn('unreadable.tar.gz'); + + // Assert + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Archive file "unreadable.tar.gz" is not readable.'); + + // Act + new TarPharArchive($file); + } +} diff --git a/tests/Unit/Module/Archive/Internal/ZipPharArchiveTest.php b/tests/Unit/Module/Archive/Internal/ZipPharArchiveTest.php new file mode 100644 index 0000000..4211987 --- /dev/null +++ b/tests/Unit/Module/Archive/Internal/ZipPharArchiveTest.php @@ -0,0 +1,28 @@ +createMock(\SplFileInfo::class); + $file->method('isFile')->willReturn(false); + $file->method('getFilename')->willReturn('invalid.zip'); + + // Assert + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Archive "invalid.zip" is not a file.'); + + // Act + new ZipPharArchive($file); + } +} diff --git a/tests/Unit/Module/Archive/Stub/ArchiveFixtureGenerator.php b/tests/Unit/Module/Archive/Stub/ArchiveFixtureGenerator.php new file mode 100644 index 0000000..b305dec --- /dev/null +++ b/tests/Unit/Module/Archive/Stub/ArchiveFixtureGenerator.php @@ -0,0 +1,129 @@ +fixturesDir = $fixturesDir; + + if (!\is_dir($fixturesDir)) { + \mkdir($fixturesDir, 0777, true); + } + } + + /** + * Create test archive files of different formats + * + * Creates a fixture file for each supported archive format + * + * @return array Map of archive type to file path + */ + public function generateArchives(): array + { + $fixtures = []; + + // Create a simple text file for content + $contentFile = $this->fixturesDir . '/test-content.txt'; + \file_put_contents($contentFile, 'This is test content for archive'); + + // Create ZIP archive + $zipPath = $this->fixturesDir . '/archive.zip'; + if ($this->canCreateZip()) { + $zip = new \ZipArchive(); + if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) === true) { + $zip->addFile($contentFile, 'test-content.txt'); + $zip->close(); + $fixtures['zip'] = $zipPath; + } + } + + // Create TAR.GZ archive + $tarGzPath = $this->fixturesDir . '/archive.tar.gz'; + if ($this->canCreatePhar()) { + try { + $phar = new \PharData($this->fixturesDir . '/archive.tar'); + $phar->addFile($contentFile, 'test-content.txt'); + $phar->compress(\Phar::GZ); + $fixtures['tar.gz'] = $tarGzPath; + } catch (\Exception $e) { + // Skip if creating tar.gz fails + } + } + + // Create PHAR archive + $pharPath = $this->fixturesDir . '/archive.phar'; + if ($this->canCreatePhar() && \ini_get('phar.readonly') == 0) { + try { + $phar = new \Phar($pharPath); + $phar->addFile($contentFile, 'test-content.txt'); + $fixtures['phar'] = $pharPath; + } catch (\Exception $e) { + // Skip if creating phar fails + } + } + + return $fixtures; + } + + /** + * Clean up generated fixtures + */ + public function cleanup(): void + { + $this->removeDirectory($this->fixturesDir); + } + + /** + * Check if ZIP archive creation is possible + */ + private function canCreateZip(): bool + { + return \class_exists(\ZipArchive::class); + } + + /** + * Check if PHAR archive creation is possible + */ + private function canCreatePhar(): bool + { + return \class_exists(\PharData::class); + } + + /** + * Recursively remove a directory and its contents + */ + private function removeDirectory(string $dir): void + { + if (!\is_dir($dir)) { + return; + } + + $items = \scandir($dir); + foreach ($items as $item) { + if ($item === '.' || $item === '..') { + continue; + } + + $path = $dir . '/' . $item; + if (\is_dir($path)) { + $this->removeDirectory($path); + } else { + \unlink($path); + } + } + + \rmdir($dir); + } +} diff --git a/tests/Unit/Module/Archive/Stub/TestArchive.php b/tests/Unit/Module/Archive/Stub/TestArchive.php new file mode 100644 index 0000000..efd6341 --- /dev/null +++ b/tests/Unit/Module/Archive/Stub/TestArchive.php @@ -0,0 +1,64 @@ +throwsException = true; + if ($message !== null) { + $this->exceptionMessage = $message; + } + return $this; + } + + /** + * Add a test file to be returned during extraction + */ + public function addFile(string $path, \SplFileInfo $fileInfo): self + { + $this->files[$path] = $fileInfo; + return $this; + } + + public function extract(): \Generator + { + if ($this->throwsException) { + throw new ArchiveException($this->exceptionMessage); + } + + foreach ($this->files as $path => $fileInfo) { + $fileTo = yield $path => $fileInfo; + + // Simulate file extraction if destination is specified + if ($fileTo instanceof \SplFileInfo) { + // In a real implementation, this would copy the file + // For testing, we just record the extraction occurred + yield $path => $fileTo; + } + } + } +} diff --git a/tests/Unit/Module/Repository/AssetsCollectionTest.php b/tests/Unit/Module/Repository/AssetsCollectionTest.php new file mode 100644 index 0000000..bb0c2fb --- /dev/null +++ b/tests/Unit/Module/Repository/AssetsCollectionTest.php @@ -0,0 +1,219 @@ + [ + '/^package-1\.2\.3-linux-x64\.tar\.gz$/', + ['package-1.2.3-linux-x64.tar.gz'], + ]; + + yield 'all linux assets' => [ + '/linux/', + [ + 'package-1.2.3-linux-x64.tar.gz', + 'package-1.2.3-linux-arm64.tar.gz', + ], + ]; + + yield 'all tar.gz assets' => [ + '/\.tar\.gz$/', + [ + 'package-1.2.3-linux-x64.tar.gz', + 'package-1.2.3-linux-arm64.tar.gz', + 'package-1.2.3-darwin-x64.tar.gz', + ], + ]; + + yield 'all zip assets' => [ + '/\.zip$/', + [ + 'package-1.2.3-windows-x64.zip', + 'package-1.2.3-source.zip', + ], + ]; + + yield 'no matches' => [ + '/nonexistent/', + [], + ]; + } + + public function testWhereOperatingSystemFiltersAssetsByOs(): void + { + // Act + $result = $this->collection->whereOperatingSystem(OperatingSystem::Linux); + + // Assert + self::assertCount(2, $result); + + foreach ($result as $asset) { + self::assertSame(OperatingSystem::Linux, $asset->getOperatingSystem()); + } + } + + public function testWhereArchitectureFiltersAssetsByArchitecture(): void + { + // Act + $result = $this->collection->whereArchitecture(Architecture::ARM_64); + + // Assert + self::assertCount(3, $result); + + foreach ($result as $asset) { + self::assertSame(Architecture::ARM_64, $asset->getArchitecture()); + } + } + + #[DataProvider('provideNamePatterns')] + public function testWhereNameMatchesFiltersAssetsByNamePattern( + string $pattern, + array $expectedMatches, + ): void { + // Act + $result = $this->collection->whereNameMatches($pattern); + + // Assert + self::assertCount(\count($expectedMatches), $result); + + $actualNames = \array_map( + static fn($asset) => $asset->getName(), + \iterator_to_array($result), + ); + + foreach ($expectedMatches as $expectedName) { + self::assertContains($expectedName, $actualNames); + } + } + + public function testWhereNameMatchesWithInvalidPattern(): void + { + // Act + $new = $this->collection->whereNameMatches('/invalid[pattern/'); + + // Assert + self::assertCount(0, $new); + } + + public function testChainedFiltersWorkCorrectly(): void + { + // Act + $result = $this->collection + ->whereOperatingSystem(OperatingSystem::Linux) + ->whereArchitecture(Architecture::ARM_64); + + // Assert + self::assertCount(1, $result); + $asset = $result->first(); + self::assertSame('package-1.2.3-linux-arm64.tar.gz', $asset->getName()); + } + + public function testFirstReturnsFirstAssetOrNull(): void + { + // Act with non-empty collection + $first = $this->collection->first(); + + // Assert + self::assertNotNull($first); + self::assertSame('package-1.2.3-linux-x64.tar.gz', $first->getName()); + + // Act with empty collection + $empty = new AssetsCollection([]); + $result = $empty->first(); + + // Assert + self::assertNull($result); + } + + public function testEmptyReturnsTrueForEmptyCollection(): void + { + // Act with non-empty collection + $resultNonEmpty = $this->collection->empty(); + + // Assert + self::assertFalse($resultNonEmpty); + + // Act with empty collection + $empty = new AssetsCollection([]); + $resultEmpty = $empty->empty(); + + // Assert + self::assertTrue($resultEmpty); + } + + protected function setUp(): void + { + // Arrange + $this->repository = new RepositoryStub('vendor/package'); + $this->release = new ReleaseStub( + $this->repository, + '1.2.3', + 'v1.2.3', + Stability::Stable, + [], + ); + + // Create a variety of assets with different characteristics + $this->assets = [ + new AssetStub( + $this->release, + 'package-1.2.3-linux-x64.tar.gz', + 'https://example.com/downloads/package-1.2.3-linux-x64.tar.gz', + OperatingSystem::Linux, + Architecture::X86_64, + ), + new AssetStub( + $this->release, + 'package-1.2.3-linux-arm64.tar.gz', + 'https://example.com/downloads/package-1.2.3-linux-arm64.tar.gz', + OperatingSystem::Linux, + Architecture::ARM_64, + ), + new AssetStub( + $this->release, + 'package-1.2.3-windows-x64.zip', + 'https://example.com/downloads/package-1.2.3-windows-x64.zip', + OperatingSystem::Windows, + Architecture::ARM_64, + ), + new AssetStub( + $this->release, + 'package-1.2.3-darwin-x64.tar.gz', + 'https://example.com/downloads/package-1.2.3-darwin-x64.tar.gz', + OperatingSystem::Darwin, + Architecture::ARM_64, + ), + new AssetStub( + $this->release, + 'package-1.2.3-source.zip', + 'https://example.com/downloads/package-1.2.3-source.zip', + null, + null, + ), + ]; + + $this->collection = new AssetsCollection($this->assets); + } +} diff --git a/tests/Unit/Module/Repository/Internal/CachedGeneratorTest.php b/tests/Unit/Module/Repository/Internal/CachedGeneratorTest.php new file mode 100644 index 0000000..cb992ad --- /dev/null +++ b/tests/Unit/Module/Repository/Internal/CachedGeneratorTest.php @@ -0,0 +1,307 @@ + [ + (static function () { + yield 'a'; + yield 'b'; + yield 'c'; + })(), + ['a', 'b', 'c'], + ]; + + yield 'array iterator' => [ + new \ArrayIterator(['x', 'y', 'z']), + ['x', 'y', 'z'], + ]; + } + + /** + * Tests that the generator correctly caches items as they are yielded. + */ + public function testIterationCachesYieldedItems(): void + { + // Arrange + $generator = $this->createGenerator(5); + $cachedGenerator = new CachedGenerator($generator); + + // Act - Partially iterate through generator + $i = 0; + $iteratedItems = []; + foreach ($cachedGenerator->getIterator() as $item) { + $iteratedItems[] = $item; + // Only consume first 3 items + if (++$i >= 3) { + break; + } + } + + // Get all items using a new iteration + $allItems = \iterator_to_array($cachedGenerator); + + // Assert + self::assertCount(3, $iteratedItems); + self::assertSame([0, 1, 2], $iteratedItems); + // self::assertCount(5, $allItems); + self::assertSame([0, 1, 2, 3, 4], $allItems); + } + + /** + * Tests that first() returns the first element from the generator. + */ + public function testFirstReturnsFirstElement(): void + { + // Arrange + $generator = $this->createGenerator(3); + $cachedGenerator = new CachedGenerator($generator); + + // Act + $firstItem = $cachedGenerator->first(); + + // Assert + self::assertSame(0, $firstItem); + } + + /** + * Tests that first() returns null when the generator is empty. + */ + public function testFirstReturnsNullForEmptyGenerator(): void + { + // Arrange + $generator = $this->createGenerator(0); + $cachedGenerator = new CachedGenerator($generator); + + // Act + $firstItem = $cachedGenerator->first(); + + // Assert + self::assertNull($firstItem); + } + + /** + * Tests that first() can retrieve the first item without affecting future iterations. + */ + public function testFirstDoesNotConsumeItemFromIteration(): void + { + // Arrange + $generator = $this->createGenerator(3); + $cachedGenerator = new CachedGenerator($generator); + + // Act + $firstItem = $cachedGenerator->first(); + $allItems = \iterator_to_array($cachedGenerator); + + // Assert + self::assertSame(0, $firstItem); + self::assertCount(3, $allItems); + self::assertSame([0, 1, 2], $allItems); + } + + /** + * Tests that isEmpty() correctly identifies empty generators. + */ + public function testIsEmptyReturnsTrueForEmptyGenerator(): void + { + // Arrange + $generator = $this->createGenerator(0); + $cachedGenerator = new CachedGenerator($generator); + + // Act + $isEmpty = $cachedGenerator->isEmpty(); + + // Assert + self::assertTrue($isEmpty); + } + + /** + * Tests that isEmpty() correctly identifies non-empty generators. + */ + public function testIsEmptyReturnsFalseForNonEmptyGenerator(): void + { + // Arrange + $generator = $this->createGenerator(1); + $cachedGenerator = new CachedGenerator($generator); + + // Act + $isEmpty = $cachedGenerator->isEmpty(); + + // Assert + self::assertFalse($isEmpty); + } + + /** + * Tests that count() correctly returns the number of items in the generator. + */ + public function testCountReturnsCorrectItemCount(): void + { + // Arrange + $generator = $this->createGenerator(5); + $cachedGenerator = new CachedGenerator($generator); + + // Act + $count = $cachedGenerator->count(); + + // Assert + self::assertSame(5, $count); + } + + /** + * Tests that count() returns zero for an empty generator. + */ + public function testCountReturnsZeroForEmptyGenerator(): void + { + // Arrange + $generator = $this->createGenerator(0); + $cachedGenerator = new CachedGenerator($generator); + + // Act + $count = $cachedGenerator->count(); + + // Assert + self::assertSame(0, $count); + } + + /** + * Tests that partial iteration followed by count() returns the correct total count. + */ + public function testPartialIterationFollowedByCountReturnsCorrectTotal(): void + { + // Arrange + $generator = $this->createGenerator(5); + $cachedGenerator = new CachedGenerator($generator); + + // Act - Partially iterate + $i = 0; + foreach ($cachedGenerator as $item) { + if (++$i >= 2) { + break; + } + } + + // Get count + $count = $cachedGenerator->count(); + + // Assert + self::assertSame(5, $count); + } + + /** + * Tests that the cache persists after a complete iteration. + */ + public function testCachePersistsAfterCompleteIteration(): void + { + // Arrange + $generator = $this->createGenerator(3); + $cachedGenerator = new CachedGenerator($generator); + + // Act - First iteration + $firstIteration = \iterator_to_array($cachedGenerator); + + // Second iteration should use cache + $secondIteration = \iterator_to_array($cachedGenerator); + + // Assert + self::assertSame($firstIteration, $secondIteration); + self::assertSame([0, 1, 2], $firstIteration); + } + + /** + * Tests that the cached generator correctly handles various traversable types. + * + * @param \Traversable $traversable The traversable to test + * @param array $expected The expected result + */ + #[DataProvider('provideTraversables')] + public function testHandlesVariousTraversableTypes(\Traversable $traversable, array $expected): void + { + // Arrange + $cachedGenerator = new CachedGenerator($traversable); + + // Act + $result = \iterator_to_array($cachedGenerator); + + // Assert + self::assertSame($expected, $result); + } + + /** + * Tests that cached generator works correctly with nested generators. + */ + public function testHandlesNestedGenerators(): void + { + // Arrange + $nestedGenerator = function () { + yield from $this->createGenerator(2); + yield from $this->createGenerator(2, 10); + }; + + $cachedGenerator = new CachedGenerator($nestedGenerator()); + + // Act + $result = \iterator_to_array($cachedGenerator); + + // Assert + self::assertSame([0, 1, 10, 11], $result); + } + + /** + * Tests behavior with large datasets to ensure memory effectiveness. + */ + public function testHandlesLargeDatasets(): void + { + // Arrange + $largeGenerator = $this->createGenerator(1000); + $cachedGenerator = new CachedGenerator($largeGenerator); + + // Act + $firstAccess = $cachedGenerator->first(); + + // Partially iterate + $partialIteration = []; + $i = 0; + foreach ($cachedGenerator as $item) { + $partialIteration[] = $item; + if (++$i >= 10) { + break; + } + } + + // Get count without completing iteration + $totalCount = $cachedGenerator->count(); + + // Assert + self::assertSame(0, $firstAccess); + self::assertCount(10, $partialIteration); + self::assertSame(1000, $totalCount); + } + + /** + * Creates a simple generator that yields consecutive integers. + * + * @param int $count Number of items to yield + * @param int $start Starting value + */ + private function createGenerator(int $count, int $start = 0): \Generator + { + for ($i = $start; $i < $start + $count; $i++) { + yield $i; + } + } +} diff --git a/tests/Unit/Module/Repository/Internal/CollectionTest.php b/tests/Unit/Module/Repository/Internal/CollectionTest.php new file mode 100644 index 0000000..9aed9b9 --- /dev/null +++ b/tests/Unit/Module/Repository/Internal/CollectionTest.php @@ -0,0 +1,293 @@ + [ + [], 5, [], + ]; + + yield 'limit less than collection size' => [ + [1, 2, 3, 4, 5], 3, [1, 2, 3], + ]; + + yield 'limit equal to collection size' => [ + [1, 2, 3], 3, [1, 2, 3], + ]; + + yield 'limit greater than collection size' => [ + [1, 2, 3], 5, [1, 2, 3], + ]; + + yield 'zero limit returns all items' => [ + [1, 2, 3, 4, 5], 0, [1, 2, 3, 4, 5], + ]; + } + + public function testCollectionWithIterable(): void + { + // Create a test collection class + $testCollection = new class([]) extends Collection {}; + + // Test with array + $arrayCollection = $testCollection::create(['item1', 'item2', 'item3']); + $this->assertEquals(['item1', 'item2', 'item3'], $arrayCollection->toArray()); + + // Test with generator + $generator = static function () { + yield 'gen1'; + yield 'gen2'; + }; + + $generatorCollection = $testCollection::create($generator()); + $this->assertEquals(['gen1', 'gen2'], $generatorCollection->toArray()); + } + + public function testCollectionWithPaginator(): void + { + // Create a test collection class + $testCollection = new class([]) extends Collection {}; + + // Create a paginator + $pageLoader = static function (): \Generator { + yield ['page1-item1', 'page1-item2']; + yield ['page2-item1', 'page2-item2']; + }; + + $paginator = Paginator::createFromGenerator($pageLoader(), null); + + // Create collection with paginator + $collection = $testCollection::create($paginator); + + // Test toArray() loads all pages + $this->assertEquals( + ['page1-item1', 'page1-item2', 'page2-item1', 'page2-item2'], + $collection->toArray(), + ); + } + + public function testFilterChaining(): void + { + // Create a test collection class + $testCollection = new class([]) extends Collection {}; + + // Create a collection with array + $collection = $testCollection::create([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + + // Apply multiple filters + $filtered = $collection + ->filter(static fn($item) => $item > 3) + ->filter(static fn($item) => $item < 8) + ->filter(static fn($item) => $item % 2 === 0); + + // Check result + $this->assertEquals([4, 6], $filtered->toArray()); + } + + public function testFilterWithPaginator(): void + { + // Create a test collection class + $testCollection = new class([]) extends Collection {}; + + // Create a paginator + $pageLoader = static function (): \Generator { + yield [1, 2, 3, 4, 5]; + yield [6, 7, 8, 9, 10]; + }; + + $paginator = Paginator::createFromGenerator($pageLoader(), null); + + // Create collection with paginator + $collection = $testCollection::create($paginator); + + // Apply multiple filters + $filtered = $collection + ->filter(static fn($item) => $item > 3) + ->filter(static fn($item) => $item < 9) + ->filter(static fn($item) => $item % 2 === 0); + + // Check result + $this->assertEquals([4, 6, 8], $filtered->toArray()); + } + + public function testFirst(): void + { + // Create a test collection class + $testCollection = new class([]) extends Collection {}; + + // Create a paginator that will verify lazy loading + $pagesLoaded = [0 => false, 1 => false, 2 => false]; + $f = static fn(int $num): object => (object) ['num' => $num]; + $pageLoader = static function () use (&$pagesLoaded, $f): \Generator { + $pagesLoaded[0] = true; + yield [$f(0)]; + + $pagesLoaded[1] = true; + yield [$f(1), $f(2), $f(3), $f(4), $f(5)]; + + $pagesLoaded[2] = true; + yield [$f(6), $f(7), $f(8), $f(9), $f(10)]; + }; + + $paginator = Paginator::createFromGenerator($pageLoader(), null); + // It always starts the generator when the closure is called + $this->assertTrue($pagesLoaded[0]); + + // Create collection with paginator + $collection = $testCollection::create($paginator); + + // Apply filter that will only match items on the 2nd page + $filtered = $collection->filter(static fn($item) => $item->num < 7); + + // At this point, no pages should be loaded + $this->assertFalse($pagesLoaded[1]); + $this->assertFalse($pagesLoaded[2]); + + // Get first matching item + $first = $filtered->first(); + self::assertSame(0, $first->num); + // We got '0' from the 0-page, so we need to load the next pages + $this->assertFalse($pagesLoaded[1]); + $this->assertFalse($pagesLoaded[2]); + + + // Now the 1st page should be loaded to get "1" value + $first = $filtered->first(static fn($item) => $item->num > 0); + self::assertSame(1, $first->num); + $this->assertTrue($pagesLoaded[1]); + $this->assertFalse($pagesLoaded[2]); + } + + #[DataProvider('provideLimitCases')] + public function testLimit(array $sourceItems, int $limit, array $expectedItems): void + { + // Arrange + $testCollection = new class([]) extends Collection {}; + $collection = $testCollection::create($sourceItems); + + // Act + $limited = $collection->limit($limit); + + // Assert + self::assertEquals($expectedItems, $limited->toArray()); + + // Check count matches expected + self::assertCount(\count($expectedItems), $limited); + } + + public function testLimitWithFilter(): void + { + // Arrange + $testCollection = new class([]) extends Collection {}; + $collection = $testCollection::create([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + + // Act + $filtered = $collection + ->filter(static fn($item) => $item > 3) + ->limit(2); + + // Assert + self::assertEquals([4, 5], $filtered->toArray()); + self::assertCount(2, $filtered); + } + + public function testLimitWithPaginator(): void + { + // Arrange + $testCollection = new class([]) extends Collection {}; + + $pageLoader = static function (): \Generator { + yield [1, 2, 3]; + yield [4, 5, 6]; + yield [7, 8, 9]; + }; + + $paginator = Paginator::createFromGenerator($pageLoader(), null); + $collection = $testCollection::create($paginator); + + // Act + $limited = $collection->limit(4); + + // Assert + self::assertEquals([1, 2, 3, 4], $limited->toArray()); + self::assertCount(4, $limited); + } + + public function testLimitWithZeroCountResetsLimit(): void + { + // Arrange + $testCollection = new class([]) extends Collection {}; + $collection = $testCollection::create([1, 2, 3, 4, 5]); + $limitedCollection = $collection->limit(2); + + // Verify the limit was applied + self::assertEquals([1, 2], $limitedCollection->toArray()); + + // Act - apply zero limit to reset the limit + $resetCollection = $limitedCollection->limit(0); + + // Assert - should have all items + self::assertEquals([1, 2, 3, 4, 5], $resetCollection->toArray()); + } + + public function testLimitResetWithFilteredCollection(): void + { + // Arrange + $testCollection = new class([]) extends Collection {}; + $collection = $testCollection::create([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + + // Apply filter and limit + $filtered = $collection + ->filter(static fn($item) => $item > 3) + ->limit(2); + + // Verify initial state + self::assertEquals([4, 5], $filtered->toArray()); + + // Act - reset limit + $resetLimited = $filtered->limit(0); + + // Assert - filter should still be applied, but not the limit + self::assertEquals([4, 5, 6, 7, 8, 9, 10], $resetLimited->toArray()); + } + + public function testCountWithLimit(): void + { + // Arrange + $testCollection = new class([]) extends Collection {}; + $collection = $testCollection::create([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + + // Act + $limited = $collection->limit(3); + + // Assert + self::assertCount(3, $limited); + } + + public function testEmptyWithLimit(): void + { + // Arrange + $testCollection = new class([]) extends Collection {}; + $collection = $testCollection::create([1, 2, 3]); + + // Act & Assert + self::assertFalse($collection->limit(1)->empty()); + self::assertFalse($collection->limit(0)->empty()); + + // With an empty source collection + $emptyCollection = $testCollection::create([]); + self::assertTrue($emptyCollection->limit(5)->empty()); + } +} diff --git a/tests/Unit/Module/Repository/Internal/GitHub/GitHubRepositoryLazyLoadingTest.php b/tests/Unit/Module/Repository/Internal/GitHub/GitHubRepositoryLazyLoadingTest.php new file mode 100644 index 0000000..25cad08 --- /dev/null +++ b/tests/Unit/Module/Repository/Internal/GitHub/GitHubRepositoryLazyLoadingTest.php @@ -0,0 +1,219 @@ + "Release $releaseNumber", + 'tag_name' => "v1.$releaseNumber.0", + 'assets' => [], + ]; + } + } + + // Add Link header for pagination + $headers = []; + if ($page < 3) { + $nextPage = $page + 1; + $headers['Link'] = ["; rel=\"next\""]; + } + + return new MockResponse(\json_encode($releases), [ + 'response_headers' => $headers, + ]); + }); + + // Create repository + $repository = new GitHubRepository('test', 'repo', $mockClient); + + // At this point, no requests should have been made + $this->assertEmpty($requestLog); + + // Get releases collection + $releases = $repository->getReleases(); + + // Still no requests should have been made + $this->assertEmpty($requestLog); + + // Get first release - this should load the first page + $firstRelease = $releases->first(); + $this->assertCount(1, $requestLog); + $this->assertEquals("Requested page 1", $requestLog[0]); + $this->assertEquals("v1.1.0", $firstRelease?->getVersion()); + + // Filter to releases with version greater than v1.2.0 + // This should NOT load additional pages yet + $laterReleases = $releases->filter(static fn($release) => \version_compare($release->getVersion(), "v1.2.0", ">")); + + // No additional pages should have been loaded after filter call + $this->assertCount(1, $requestLog); + + // Add another filter - this should still not load additional pages + $laterReleases = $laterReleases->filter(static fn($release) => \str_contains($release->getVersion(), "v1.")); + + // Still no additional pages loaded + $this->assertCount(1, $requestLog); + + // Get the first matching item - this should load pages until a match is found + $firstLaterRelease = $laterReleases->first(); + + // Now page 2 should be loaded because v1.1.0 and v1.2.0 don't match our filter + $this->assertCount(2, $requestLog); + $this->assertEquals("Requested page 2", $requestLog[1]); + $this->assertEquals("v1.3.0", $firstLaterRelease?->getVersion()); + + // Converting to array to force loading all pages + $allLaterReleases = $laterReleases->toArray(); + + // Now all pages should have been requested + $this->assertCount(3, $requestLog); + $this->assertEquals("Requested page 3", $requestLog[2]); + + // Check that we have the correct releases + $this->assertCount(2, $allLaterReleases); + $this->assertEquals("v1.3.0", $allLaterReleases[0]->getVersion()); + $this->assertEquals("v1.4.0", $allLaterReleases[1]->getVersion()); + } + + public function testMultipleFiltersWithoutIteration(): void + { + $requestLog = []; + + // Create a mock HTTP client that tracks page requests + $mockClient = new MockHttpClient(static function (string $method, string $url) use (&$requestLog): MockResponse { + // Extract page number from URL query + \parse_str(\parse_url($url, PHP_URL_QUERY) ?? '', $query); + $page = (int) ($query['page'] ?? 1); + + // Log this request + $requestLog[] = "Requested page $page"; + + return new MockResponse(\json_encode([ + [ + 'name' => "Release $page", + 'tag_name' => "v1.$page.0", + 'assets' => [], + ], + ])); + }); + + // Create repository + $repository = new GitHubRepository('test', 'repo', $mockClient); + $releases = $repository->getReleases(); + + // Apply multiple filters without iterating + $filtered = $releases + ->filter(static fn($release) => \str_contains($release->getVersion(), "v1")) + ->filter(static fn($release) => \version_compare($release->getVersion(), "v1.0.0", ">")) + ->filter(static fn($release) => $release->getName() !== "Ignored"); + + // No requests should have been made yet + $this->assertEmpty($requestLog); + } + + public function testEmptyRepository(): void + { + // Create a mock HTTP client that returns empty responses + $mockClient = new MockHttpClient([ + new MockResponse('[]'), // Empty first page + ]); + + // Create repository + $repository = new GitHubRepository('test', 'empty-repo', $mockClient); + + // Get releases collection + $releases = $repository->getReleases(); + + // Check that the collection is empty + $this->assertTrue($releases->empty()); + $this->assertNull($releases->first()); + $this->assertCount(0, \iterator_to_array($releases)); + } + + public function testChainingMultipleFilters(): void + { + // Create a mock HTTP client with predefined responses + $mockClient = new MockHttpClient([ + // Page 1 + new MockResponse(\json_encode([ + [ + 'name' => 'Release 1', + 'tag_name' => 'v1.0.0', + 'assets' => [ + ['name' => 'asset1.zip', 'browser_download_url' => 'https://example.com/asset1.zip'], + ], + ], + [ + 'name' => 'Release 2', + 'tag_name' => 'v2.0.0', + 'assets' => [], + ], + ]), [ + 'response_headers' => [ + 'Link' => ['; rel="next"'], + ], + ]), + // Page 2 + new MockResponse(\json_encode([ + [ + 'name' => 'Release 3', + 'tag_name' => 'v3.0.0', + 'assets' => [ + ['name' => 'asset3.zip', 'browser_download_url' => 'https://example.com/asset3.zip'], + ], + ], + [ + 'name' => 'Release 4', + 'tag_name' => 'v4.0.0', + 'assets' => [ + ['name' => 'asset4.zip', 'browser_download_url' => 'https://example.com/asset4.zip'], + ], + ], + ])), + ]); + + // Create repository and get releases + $repository = new GitHubRepository('test', 'repo', $mockClient); + $releases = $repository->getReleases(); + + // Create a filtered collection with multiple filters + $filteredReleases = $releases + ->filter(static fn($release) => \version_compare($release->getVersion(), "v2.0.0", ">=")) // v2.0.0 and above + ->filter(static fn($release) => !$release->getAssets()->empty()); // Only with assets + + // Convert to array to execute the filters + $result = $filteredReleases->toArray(); + + // Should have 2 releases: v3.0.0 and v4.0.0 (both have assets and >= v2.0.0) + $this->assertCount(2, $result); + $this->assertEquals("v3.0.0", $result[0]->getVersion()); + $this->assertEquals("v4.0.0", $result[1]->getVersion()); + } +} diff --git a/tests/Unit/Module/Repository/Internal/PaginatorTest.php b/tests/Unit/Module/Repository/Internal/PaginatorTest.php new file mode 100644 index 0000000..fa8df1b --- /dev/null +++ b/tests/Unit/Module/Repository/Internal/PaginatorTest.php @@ -0,0 +1,89 @@ +assertEquals(['Item 1', 'Item 2'], $paginator->getPageItems()); + $this->assertEquals(1, $paginator->getPageNumber()); + + // Test getting next page + $page2 = $paginator->getNextPage(); + $this->assertNotNull($page2); + $this->assertEquals(['Item 3', 'Item 4'], $page2->getPageItems()); + $this->assertEquals(2, $page2->getPageNumber()); + + // Test getting third page + $page3 = $page2->getNextPage(); + $this->assertNotNull($page3); + $this->assertEquals(['Item 5'], $page3->getPageItems()); + $this->assertEquals(3, $page3->getPageNumber()); + + // Test that there's no fourth page + $page4 = $page3->getNextPage(); + $this->assertNull($page4); + } + + public function testIteration(): void + { + // Create a generator that yields arrays of items for each page + $loader = static function (): \Generator { + yield ['Item 1', 'Item 2']; // Page 1 + yield ['Item 3', 'Item 4']; // Page 2 + }; + + $paginator = Paginator::createFromGenerator($loader(), null); + + // Test iterating through all items + $items = \iterator_to_array($paginator); + $this->assertEquals(['Item 1', 'Item 2', 'Item 3', 'Item 4'], $items); + } + + public function testCountWithCounter(): void + { + // Create a generator + $loader = static function (): \Generator { + yield ['Item 1', 'Item 2']; // Page 1 + yield ['Item 3', 'Item 4']; // Page 2 + }; + + // Create a counter function + $counter = static fn() => 4; // Total number of items + + $paginator = Paginator::createFromGenerator($loader(), $counter); + + // Test count() + $this->assertEquals(4, $paginator->count()); + } + + public function testCountWithoutCounter(): void + { + // Create a generator + $loader = static function (): \Generator { + yield ['Item 1', 'Item 2']; // Page 1 + }; + + $paginator = Paginator::createFromGenerator($loader(), null); + + // Test that count() throws an exception when no counter is provided + $this->expectException(\LogicException::class); + $paginator->count(); + } +} diff --git a/tests/Unit/Module/Repository/ReleasesCollectionTest.php b/tests/Unit/Module/Repository/ReleasesCollectionTest.php new file mode 100644 index 0000000..dbab924 --- /dev/null +++ b/tests/Unit/Module/Repository/ReleasesCollectionTest.php @@ -0,0 +1,220 @@ + */ + private array $releases; + + private ReleasesCollection $collection; + + public function testSatisfiesFiltersReleasesByVersionConstraint(): void + { + // Act + $result = $this->collection->satisfies('^1.0.0'); + + // Assert + self::assertCount(2, $result, 'Should only include 1.0.0 and 1.5.0 versions'); + + $versions = \array_map( + static fn(ReleaseInterface $release): string => $release->getName(), + \iterator_to_array($result), + ); + + self::assertContains('1.0.0', $versions); + self::assertContains('1.5.0', $versions); + self::assertNotContains('2.0.0', $versions); + } + + public function testNotSatisfiesFiltersOutReleasesByVersionConstraint(): void + { + // Act + $result = $this->collection->notSatisfies('^1.0.0'); + + // Assert + self::assertGreaterThan(0, $result->count()); + self::assertNotContains('1.0.0', $this->getVersionsFromCollection($result)); + self::assertNotContains('1.5.0', $this->getVersionsFromCollection($result)); + self::assertContains('2.0.0', $this->getVersionsFromCollection($result)); + } + + public function testStabilityFiltersReleasesByExactStability(): void + { + // Act + $result = $this->collection->stability(Stability::Beta); + + // Assert + self::assertCount(1, $result); + self::assertSame('2.1.0-beta', $result->first()->getName()); + } + + public function testStableFiltersToOnlyStableReleases(): void + { + // Act + $result = $this->collection->stable(); + + // Assert + self::assertCount(3, $result, 'Should only include stable versions'); + + foreach ($result as $release) { + self::assertSame(Stability::Stable, $release->getStability()); + } + } + + public function testMinimumStabilityFiltersReleasesByMinimumStabilityLevel(): void + { + // Act + $result = $this->collection->minimumStability(Stability::RC); + + // Assert + $stabilities = []; + foreach ($result as $release) { + $stabilities[] = $release->getStability(); + } + + self::assertContains(Stability::Stable, $stabilities); + self::assertContains(Stability::RC, $stabilities); + self::assertNotContains(Stability::Beta, $stabilities); + self::assertNotContains(Stability::Alpha, $stabilities); + } + + public function testSortByVersionSortsReleasesByVersionDescending(): void + { + // Act + $result = $this->collection->sortByVersion(); + $versions = $this->getVersionsFromCollection($result); + + // Assert + self::assertSame([ + // Expected order by semantic version, newest first + '2.1.0-beta', + '2.1.0-alpha', + '2.0.1-rc1', + '2.0.0', + '1.5.0', + '1.0.0', + ], \array_values($versions)); + } + + public function testChainedFiltersWorkCorrectly(): void + { + // Act - Get stable releases that satisfy version constraint and sort them + $result = $this->collection + ->stable() + ->satisfies('^1.0.0') + ->sortByVersion(); + + // Assert + $versions = $this->getVersionsFromCollection($result); + self::assertSame(['1.5.0', '1.0.0'], \array_values($versions)); + } + + public function testWithAssetsFiltersReleasesWithAssets(): void + { + // Arrange - Set up assets for the second release (1.5.0) + $release = $this->releases[1]; // 1.5.0 release + + $assets = [ + new AssetStub( + $release, + 'package-1.5.0-linux-x64.tar.gz', + 'https://example.com/downloads/package-1.5.0-linux-x64.tar.gz', + OperatingSystem::Linux, + Architecture::X86_64, + ), + ]; + + $release->setAssets($assets); + + // Act + $result = $this->collection->withAssets(); + + // Assert + self::assertCount(1, $result); + self::assertSame('1.5.0', $result->first()->getName()); + self::assertCount(1, $result->first()->getAssets()); + } + + protected function setUp(): void + { + // Arrange + $this->repository = new RepositoryStub('vendor/package'); + + // Create a series of releases with different versions and stabilities + $this->releases = [ + new ReleaseStub( + $this->repository, + '2.0.0', + 'v2.0.0', + Stability::Stable, + [], + ), + new ReleaseStub( + $this->repository, + '1.5.0', + 'v1.5.0', + Stability::Stable, + [], + ), + new ReleaseStub( + $this->repository, + '1.0.0', + 'v1.0.0', + Stability::Stable, + [], + ), + new ReleaseStub( + $this->repository, + '2.1.0-beta', + 'v2.1.0-beta', + Stability::Beta, + [], + ), + new ReleaseStub( + $this->repository, + '2.1.0-alpha', + 'v2.1.0-alpha', + Stability::Alpha, + [], + ), + new ReleaseStub( + $this->repository, + '2.0.1-rc1', + 'v2.0.1-rc1', + Stability::RC, + [], + ), + ]; + + // Create the collection with all releases + $this->collection = new ReleasesCollection($this->releases); + } + + /** + * Helper method to extract version names from a collection + */ + private function getVersionsFromCollection(ReleasesCollection $collection): array + { + return \array_map( + static fn(ReleaseInterface $release): string => $release->getName(), + \iterator_to_array($collection), + ); + } +} diff --git a/tests/Unit/Module/Repository/RepositoryProviderTest.php b/tests/Unit/Module/Repository/RepositoryProviderTest.php new file mode 100644 index 0000000..8f82a6d --- /dev/null +++ b/tests/Unit/Module/Repository/RepositoryProviderTest.php @@ -0,0 +1,163 @@ +type = 'github'; + $githubConfig->uri = 'vendor/package'; + + $gitlabConfig = new RepositoryConfig(); + $gitlabConfig->type = 'gitlab'; + $gitlabConfig->uri = 'group/project'; + $gitlabConfig->assetPattern = '/^release-.*$/'; + + $customConfig = new RepositoryConfig(); + $customConfig->type = 'custom'; + $customConfig->uri = 'https://example.com/repo'; + + yield 'github config with support' => [$githubConfig, true]; + yield 'gitlab config with support' => [$gitlabConfig, true]; + yield 'custom config without support' => [$customConfig, false]; + } + + public function testAddRepositoryFactoryReturnsSelf(): void + { + // Arrange + $factory = $this->createMock(RepositoryFactory::class); + + // Act + $result = $this->repositoryProvider->addRepositoryFactory($factory); + + // Assert + self::assertSame($this->repositoryProvider, $result); + } + + public function testGetByConfigReturnsRepositoryFromSupportingFactory(): void + { + // Arrange + $config = new RepositoryConfig(); + $config->type = 'github'; + $config->uri = 'vendor/package'; + + $repository = $this->createMock(Repository::class); + + $unsupportedFactory = $this->createMock(RepositoryFactory::class); + $unsupportedFactory->method('supports')->with($config)->willReturn(false); + $unsupportedFactory->expects(self::never())->method('create'); + + $supportedFactory = $this->createMock(RepositoryFactory::class); + $supportedFactory->method('supports')->with($config)->willReturn(true); + $supportedFactory->method('create')->with($config)->willReturn($repository); + + // Add factories to provider (order matters - first unsupported, then supported) + $this->repositoryProvider->addRepositoryFactory($unsupportedFactory); + $this->repositoryProvider->addRepositoryFactory($supportedFactory); + + // Act + $result = $this->repositoryProvider->getByConfig($config); + + // Assert + self::assertSame($repository, $result); + } + + public function testGetByConfigUsesFirstSupportingFactory(): void + { + // Arrange + $config = new RepositoryConfig(); + $config->type = 'github'; + $config->uri = 'vendor/package'; + + $repository1 = $this->createMock(Repository::class); + $repository2 = $this->createMock(Repository::class); + + $firstFactory = $this->createMock(RepositoryFactory::class); + $firstFactory->method('supports')->with($config)->willReturn(true); + $firstFactory->method('create')->with($config)->willReturn($repository1); + + $secondFactory = $this->createMock(RepositoryFactory::class); + $secondFactory->method('supports')->with($config)->willReturn(true); + $secondFactory->expects(self::never())->method('create'); + + // Add both factories (both support the config, but first one should be used) + $this->repositoryProvider->addRepositoryFactory($firstFactory); + $this->repositoryProvider->addRepositoryFactory($secondFactory); + + // Act + $result = $this->repositoryProvider->getByConfig($config); + + // Assert + self::assertSame($repository1, $result); + } + + public function testGetByConfigThrowsExceptionWhenNoFactorySupportsConfig(): void + { + // Arrange + $config = new RepositoryConfig(); + $config->type = 'unsupported'; + $config->uri = 'vendor/package'; + + $factory = $this->createMock(RepositoryFactory::class); + $factory->method('supports')->with($config)->willReturn(false); + $this->repositoryProvider->addRepositoryFactory($factory); + + // Assert (before Act for exceptions) + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage("No factory found for repository type `unsupported`."); + + // Act + $this->repositoryProvider->getByConfig($config); + } + + #[DataProvider('provideRepositoryConfigs')] + public function testGetByConfigWithVariousConfigs(RepositoryConfig $config, bool $factorySupports): void + { + // Arrange + $repository = $this->createMock(Repository::class); + + $factory = $this->createMock(RepositoryFactory::class); + $factory->method('supports')->with($config)->willReturn($factorySupports); + + if ($factorySupports) { + $factory->method('create')->with($config)->willReturn($repository); + } + + $this->repositoryProvider->addRepositoryFactory($factory); + + // Assert expectation for exception if no factory supports + if (!$factorySupports) { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage("No factory found for repository type `{$config->type}`."); + } + + // Act + $result = $this->repositoryProvider->getByConfig($config); + + // Assert result if factory supports + if ($factorySupports) { + self::assertSame($repository, $result); + } + } + + protected function setUp(): void + { + // Arrange (common setup) + $this->repositoryProvider = new RepositoryProvider(); + } +} diff --git a/tests/Unit/Module/Repository/Stub/AssetStub.php b/tests/Unit/Module/Repository/Stub/AssetStub.php new file mode 100644 index 0000000..0c14637 --- /dev/null +++ b/tests/Unit/Module/Repository/Stub/AssetStub.php @@ -0,0 +1,59 @@ +release; + } + + public function getName(): string + { + return $this->name; + } + + public function getUri(): string + { + return $this->uri; + } + + public function getOperatingSystem(): ?OperatingSystem + { + return $this->operatingSystem; + } + + public function getArchitecture(): ?Architecture + { + return $this->architecture; + } + + public function download(): \Traversable + { + // Simulate a download stream with mock content + yield 'Mock download content for ' . $this->name; + } +} diff --git a/tests/Unit/Module/Repository/Stub/Collection/ReleasesCollectionStub.php b/tests/Unit/Module/Repository/Stub/Collection/ReleasesCollectionStub.php new file mode 100644 index 0000000..5788e7c --- /dev/null +++ b/tests/Unit/Module/Repository/Stub/Collection/ReleasesCollectionStub.php @@ -0,0 +1,49 @@ +releases); + } + + public function getIterator(): \Traversable + { + return new \ArrayIterator($this->releases); + } + + public function satisfies(string $constraint): ReleasesCollection + { + // Simple implementation for testing - returns same collection + return $this; + } + + public function sortByVersion(bool $descending = true): ReleasesCollection + { + // Simple implementation for testing - returns same collection + return $this; + } + + public function last(): ?ReleaseInterface + { + if (empty($this->releases)) { + return null; + } + + return $this->releases[\count($this->releases) - 1]; + } +} diff --git a/tests/Unit/Module/Repository/Stub/ReleaseStub.php b/tests/Unit/Module/Repository/Stub/ReleaseStub.php new file mode 100644 index 0000000..fe751a7 --- /dev/null +++ b/tests/Unit/Module/Repository/Stub/ReleaseStub.php @@ -0,0 +1,70 @@ + $assets + */ + public function __construct( + private readonly RepositoryStub $repository, + private readonly string $name, + private readonly string $version, + private readonly Stability $stability, + private array $assets = [], + ) {} + + public function getRepository(): Repository + { + return $this->repository; + } + + public function getName(): string + { + return $this->name; + } + + public function getVersion(): string + { + return $this->version; + } + + public function getStability(): Stability + { + return $this->stability; + } + + public function getAssets(): AssetsCollection + { + return new AssetsCollection($this->assets); + } + + /** + * @param array $assets + */ + public function setAssets(array $assets): void + { + $this->assets = $assets; + } + + public function satisfies(string $constraint): bool + { + // Using Composer's semver for consistent version comparison + return Semver::satisfies($this->name, $constraint); + } +} diff --git a/tests/Unit/Module/Repository/Stub/RepositoryFactoryStub.php b/tests/Unit/Module/Repository/Stub/RepositoryFactoryStub.php new file mode 100644 index 0000000..3c4b876 --- /dev/null +++ b/tests/Unit/Module/Repository/Stub/RepositoryFactoryStub.php @@ -0,0 +1,42 @@ + $supportedTypes Types this factory will support + * @param Repository|null $repository Repository to return when create() is called + */ + public function __construct(array $supportedTypes = ['github'], ?Repository $repository = null) + { + $this->supportedTypes = $supportedTypes; + $this->repository = $repository; + } + + public function supports(RepositoryConfig $config): bool + { + return \in_array($config->type, $this->supportedTypes, true); + } + + public function create(RepositoryConfig $config): Repository + { + if ($this->repository === null) { + return new RepositoryStub($config->uri); + } + + return $this->repository; + } +} diff --git a/tests/Unit/Module/Repository/Stub/RepositoryStub.php b/tests/Unit/Module/Repository/Stub/RepositoryStub.php new file mode 100644 index 0000000..837e05d --- /dev/null +++ b/tests/Unit/Module/Repository/Stub/RepositoryStub.php @@ -0,0 +1,47 @@ + */ + private ?Collection $releases; + + /** + * @param string $name Repository name + * @param ReleasesCollection|null $releases Collection of releases to return + */ + public function __construct(string $name, ?ReleasesCollection $releases = null) + { + $this->name = $name; + $this->releases = $releases ?? new ReleasesCollectionStub([]); + } + + public function getName(): string + { + return $this->name; + } + + public function getReleases(): ReleasesCollection + { + return $this->releases; + } + + public function setAsset(ReleaseStub $release, array $assets): void + { + $release->setAssets($assets); + } +}