diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c700470..49a5af5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,3 +51,29 @@ jobs: - name: Run PHPStan run: vendor/bin/phpstan analyse --no-progress + + phpunit: + name: phpunit (PHP ${{ matrix.php-version }}) + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + php-version: ['8.2', '8.3', '8.4', '8.5', '8.6'] + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0 + with: + php-version: ${{ matrix.php-version }} + coverage: none + + - name: Install Composer dependencies + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + + - name: Run PHPUnit + run: vendor/bin/phpunit diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index b045953..6d14afa 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -14,6 +14,7 @@ 'empty_loop_body' => ['style' => 'semicolon'], 'method_argument_space' => false, 'native_constant_invocation' => false, + 'no_null_property_initialization' => false, 'phpdoc_align' => false, 'phpdoc_line_span' => [ 'class' => 'single', diff --git a/composer.json b/composer.json index 3bf0d65..b43f6d5 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,8 @@ "require-dev": { "ext-imagick": "*", "friendsofphp/php-cs-fixer": "^3.95.1", - "phpstan/phpstan": "^2.1.54" + "phpstan/phpstan": "^2.1.54", + "phpunit/phpunit": "^11.5.55" }, "config": { "platform": { @@ -28,6 +29,7 @@ "phpstan:no-cache": [ "phpstan clear-result-cache --no-interaction", "phpstan analyse -v" - ] + ], + "phpunit": "phpunit" } } diff --git a/composer.lock b/composer.lock index 06c8c29..f26527c 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": "a263a06c5b8320a66667a97415132a9e", + "content-hash": "31c559dbc4629d81bd63bdf7f08ebd50", "packages": [], "packages-dev": [ { @@ -576,762 +576,2383 @@ "time": "2026-04-12T17:00:09+00:00" }, { - "name": "phpstan/phpstan", - "version": "2.1.54", + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd", - "reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { - "php": "^7.4|^8.0" + "php": "^7.1 || ^8.0" }, "conflict": { - "phpstan/phpstan-shim": "*" + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, - "bin": [ - "phpstan", - "phpstan.phar" - ], "type": "library", "autoload": { "files": [ - "bootstrap.php" - ] + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "PHPStan - PHP Static Analysis Tool", + "description": "Create deep copies (clones) of your objects", "keywords": [ - "dev", - "static analysis" + "clone", + "copy", + "duplicate", + "object", + "object graph" ], "support": { - "docs": "https://phpstan.org/user-guide/getting-started", - "forum": "https://github.com/phpstan/phpstan/discussions", - "issues": "https://github.com/phpstan/phpstan/issues", - "security": "https://github.com/phpstan/phpstan/security/policy", - "source": "https://github.com/phpstan/phpstan-src" + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { - "url": "https://github.com/ondrejmirtes", - "type": "github" - }, - { - "url": "https://github.com/phpstan", - "type": "github" + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" } ], - "time": "2026-04-29T13:31:09+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { - "name": "psr/container", - "version": "2.0.2", + "name": "nikic/php-parser", + "version": "v5.7.0", "source": { "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { - "php": ">=7.4.0" + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" }, + "bin": [ + "bin/php-parse" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "5.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Container\\": "src/" + "PhpParser\\": "lib/PhpParser" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Nikita Popov" } ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", + "description": "A PHP parser written in PHP", "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" + "parser", + "php" ], "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2021-11-05T16:47:00+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { - "name": "psr/event-dispatcher", - "version": "1.0.0", + "name": "phar-io/manifest", + "version": "2.0.4", "source": { "type": "git", - "url": "https://github.com/php-fig/event-dispatcher.git", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { - "php": ">=7.2.0" + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { - "psr-4": { - "Psr\\EventDispatcher\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" } ], - "description": "Standard interfaces for event handling.", - "keywords": [ - "events", - "psr", - "psr-14" - ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { - "issues": "https://github.com/php-fig/event-dispatcher/issues", - "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2019-01-08T18:20:26+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { - "name": "psr/log", - "version": "3.0.2", + "name": "phar-io/version", + "version": "3.2.1", "source": { "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", "shasum": "" }, "require": { - "php": ">=8.0.0" + "php": "^7.2 || ^8.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "autoload": { - "psr-4": { - "Psr\\Log\\": "src" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" } ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], + "description": "Library for handling version information and constraints", "support": { - "source": "https://github.com/php-fig/log/tree/3.0.2" + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" }, - "time": "2024-09-11T13:17:53+00:00" + "time": "2022-02-21T01:04:05+00:00" }, { - "name": "react/cache", - "version": "v1.2.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/cache.git", - "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" - }, + "name": "phpstan/phpstan", + "version": "2.1.54", "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", - "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd", + "reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd", "shasum": "" }, "require": { - "php": ">=5.3.0", - "react/promise": "^3.0 || ^2.0 || ^1.1" + "php": "^7.4|^8.0" }, - "require-dev": { - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + "conflict": { + "phpstan/phpstan-shim": "*" }, + "bin": [ + "phpstan", + "phpstan.phar" + ], "type": "library", "autoload": { - "psr-4": { - "React\\Cache\\": "src/" - } + "files": [ + "bootstrap.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Async, Promise-based cache interface for ReactPHP", + "description": "PHPStan - PHP Static Analysis Tool", "keywords": [ - "cache", - "caching", - "promise", - "reactphp" + "dev", + "static analysis" ], "support": { - "issues": "https://github.com/reactphp/cache/issues", - "source": "https://github.com/reactphp/cache/tree/v1.2.0" + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" }, "funding": [ { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" } ], - "time": "2022-11-30T15:59:55+00:00" + "time": "2026-04-29T13:31:09+00:00" }, { - "name": "react/child-process", - "version": "v0.6.7", + "name": "phpunit/php-code-coverage", + "version": "11.0.12", "source": { "type": "git", - "url": "https://github.com/reactphp/child-process.git", - "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3" + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/child-process/zipball/970f0e71945556422ee4570ccbabaedc3cf04ad3", - "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", "shasum": "" }, "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.0", - "react/event-loop": "^1.2", - "react/stream": "^1.4" + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.1", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.3.1" }, "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", - "react/socket": "^1.16", - "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" + "phpunit/phpunit": "^11.5.46" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "type": "library", - "autoload": { - "psr-4": { - "React\\ChildProcess\\": "src/" + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "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/11.0.12" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-12-24T07:01:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T13:52:54+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.55", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.3", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.2", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", + "sebastian/type": "^5.1.3", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-02-18T12:37:06+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/child-process", + "version": "v0.6.7", + "source": { + "type": "git", + "url": "https://github.com/reactphp/child-process.git", + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/970f0e71945556422ee4570ccbabaedc3cf04ad3", + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/event-loop": "^1.2", + "react/stream": "^1.4" + }, + "require-dev": { + "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/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven library for executing child processes with ReactPHP.", + "keywords": [ + "event-driven", + "process", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/child-process/issues", + "source": "https://github.com/reactphp/child-process/tree/v0.6.7" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-12-23T15:25:20+00:00" + }, + { + "name": "react/dns", + "version": "v1.14.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.14.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-18T19:34:28+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-17T20:46:25+00:00" + }, + { + "name": "react/promise", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.12.28 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-08-19T18:57:03+00:00" + }, + { + "name": "react/socket", + "version": "v1.17.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.17.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-19T20:47:34+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { "name": "Jan Sorgalla", "email": "jsorgalla@gmail.com", "homepage": "https://sorgalla.com/" }, { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:26:40+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "Event-driven library for executing child processes with ReactPHP.", + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", "keywords": [ - "event-driven", - "process", - "reactphp" + "Xdebug", + "environment", + "hhvm" ], "support": { - "issues": "https://github.com/reactphp/child-process/issues", - "source": "https://github.com/reactphp/child-process/tree/v0.6.7" + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" }, "funding": [ { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" } ], - "time": "2025-12-23T15:25:20+00:00" + "time": "2025-05-21T11:55:47+00:00" }, { - "name": "react/dns", - "version": "v1.14.0", + "name": "sebastian/exporter", + "version": "6.3.2", "source": { "type": "git", - "url": "https://github.com/reactphp/dns.git", - "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3" + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3", - "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", "shasum": "" }, "require": { - "php": ">=5.3.0", - "react/cache": "^1.0 || ^0.6 || ^0.5", - "react/event-loop": "^1.2", - "react/promise": "^3.2 || ^2.7 || ^1.2.1" + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" }, "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", - "react/async": "^4.3 || ^3 || ^2", - "react/promise-timer": "^1.11" + "phpunit/phpunit": "^11.3" }, "type": "library", - "autoload": { - "psr-4": { - "React\\Dns\\": "src/" + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" }, { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" }, { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" + "name": "Volker Dusch", + "email": "github@wallbash.com" }, { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" } ], - "description": "Async DNS resolver for ReactPHP", + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", "keywords": [ - "async", - "dns", - "dns-resolver", - "reactphp" + "export", + "exporter" ], "support": { - "issues": "https://github.com/reactphp/dns/issues", - "source": "https://github.com/reactphp/dns/tree/v1.14.0" + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" }, "funding": [ { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2025-11-18T19:34:28+00:00" + "time": "2025-09-24T06:12:51+00:00" }, { - "name": "react/event-loop", - "version": "v1.6.0", + "name": "sebastian/global-state", + "version": "7.0.2", "source": { "type": "git", - "url": "https://github.com/reactphp/event-loop.git", - "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a" + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a", - "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" }, "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" - }, - "suggest": { - "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + "ext-dom": "*", + "phpunit/phpunit": "^11.0" }, "type": "library", - "autoload": { - "psr-4": { - "React\\EventLoop\\": "src/" + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", "keywords": [ - "asynchronous", - "event-loop" + "global state" ], "support": { - "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" }, "funding": [ { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "time": "2025-11-17T20:46:25+00:00" + "time": "2024-07-03T04:57:36+00:00" }, { - "name": "react/promise", - "version": "v3.3.0", + "name": "sebastian/lines-of-code", + "version": "3.0.1", "source": { "type": "git", - "url": "https://github.com/reactphp/promise.git", - "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", - "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", "shasum": "" }, "require": { - "php": ">=7.1.0" + "nikic/php-parser": "^5.0", + "php": ">=8.2" }, "require-dev": { - "phpstan/phpstan": "1.12.28 || 1.4.10", - "phpunit/phpunit": "^9.6 || ^7.5" + "phpunit/phpunit": "^11.0" }, "type": "library", - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "React\\Promise\\": "src/" + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" ], - "description": "A lightweight implementation of CommonJS Promises/A for PHP", - "keywords": [ - "promise", - "promises" + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { - "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.3.0" + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" }, "funding": [ { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "time": "2025-08-19T18:57:03+00:00" + "time": "2024-07-03T05:01:32+00:00" }, { - "name": "react/socket", - "version": "v1.17.0", + "name": "sebastian/recursion-context", + "version": "6.0.3", "source": { "type": "git", - "url": "https://github.com/reactphp/socket.git", - "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08" + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08", - "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", "shasum": "" }, "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.0", - "react/dns": "^1.13", - "react/event-loop": "^1.2", - "react/promise": "^3.2 || ^2.6 || ^1.2.1", - "react/stream": "^1.4" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", - "react/async": "^4.3 || ^3.3 || ^2", - "react/promise-stream": "^1.4", - "react/promise-timer": "^1.11" + "phpunit/phpunit": "^11.3" }, "type": "library", - "autoload": { - "psr-4": { - "React\\Socket\\": "src/" + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" }, { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" }, { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" + "name": "Adam Harvey", + "email": "aharvey@php.net" } ], - "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", - "keywords": [ - "Connection", - "Socket", - "async", - "reactphp", - "stream" - ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { - "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.17.0" + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" }, "funding": [ { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2025-11-19T20:47:34+00:00" + "time": "2025-08-13T04:42:22+00:00" }, { - "name": "react/stream", - "version": "v1.4.0", + "name": "sebastian/type", + "version": "5.1.3", "source": { "type": "git", - "url": "https://github.com/reactphp/stream.git", - "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", - "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", "shasum": "" }, "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.8", - "react/event-loop": "^1.2" + "php": ">=8.2" }, "require-dev": { - "clue/stream-filter": "~1.2", - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + "phpunit/phpunit": "^11.3" }, "type": "library", - "autoload": { - "psr-4": { - "React\\Stream\\": "src/" + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", - "keywords": [ - "event-driven", - "io", - "non-blocking", - "pipe", - "reactphp", - "readable", - "stream", - "writable" - ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", "support": { - "issues": "https://github.com/reactphp/stream/issues", - "source": "https://github.com/reactphp/stream/tree/v1.4.0" + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" }, "funding": [ { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" } ], - "time": "2024-06-11T12:45:25+00:00" + "time": "2025-08-09T06:55:48+00:00" }, { - "name": "sebastian/diff", - "version": "6.0.2", + "name": "sebastian/version", + "version": "5.0.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", "shasum": "" }, "require": { "php": ">=8.2" }, - "require-dev": { - "phpunit/phpunit": "^11.0", - "symfony/process": "^4.2 || ^5" - }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -1346,33 +2967,76 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" + "static analysis" ], "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://github.com/staabm", "type": "github" } ], - "time": "2024-07-03T04:53:05+00:00" + "time": "2024-10-20T05:08:20+00:00" }, { "name": "symfony/console", @@ -2804,6 +4468,56 @@ } ], "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" } ], "aliases": [], diff --git a/phpstan.dist.neon b/phpstan.dist.neon index ca49aa1..c9d6e4b 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -5,3 +5,4 @@ parameters: - bin - bin/build - src + - tests diff --git a/phpunit.dist.xml b/phpunit.dist.xml new file mode 100644 index 0000000..ba12c6e --- /dev/null +++ b/phpunit.dist.xml @@ -0,0 +1,18 @@ + + + + + tests + + + diff --git a/src/workflow.php b/src/workflow.php index e08d6a9..632fda6 100644 --- a/src/workflow.php +++ b/src/workflow.php @@ -1,7 +1,7 @@ */ + public static function dataGetHeader(): iterable + { + yield 'simple header' => [ + "Content-Type: text/plain\r\n", + 'Content-Type', + 'text/plain', + ]; + yield 'key match is case-insensitive' => [ + "content-type: foo\r\n", + 'Content-Type', + 'foo', + ]; + yield 'finds header among many' => [ + "Foo: 1\r\nBar: 2\r\nBaz: 3\r\n", + 'Bar', + '2', + ]; + yield 'value preserves inner spaces' => [ + "Link: ; rel=\"next\"\r\n", + 'Link', + '; rel="next"', + ]; + yield 'returns null when missing' => [ + "Foo: 1\r\n", + 'Bar', + null, + ]; + yield 'returns first occurrence on duplicate' => [ + "X: first\r\nX: second\r\n", + 'X', + 'first', + ]; + yield 'empty header string returns null' => [ + '', + 'X', + null, + ]; + yield 'value stops at line break' => [ + "ETag: \"abc\"\r\nNext: ignored\r\n", + 'ETag', + '"abc"', + ]; + } + + public function testExecuteFetchesBodyStatusAndResponseHeaders(): void + { + /** @var list $received */ + $received = []; + $request = new CurlRequest( + self::baseUrl() . '/hello', + null, + null, + static function (CurlResponse $r) use (&$received): void { + $received[] = $r; + }, + ); + $curl = new Curl(); + $curl->add($request); + self::assertTrue($curl->execute()); + + self::assertCount(1, $received); + $response = $received[0]; + + self::assertSame(200, $response->status); + self::assertSame('Hello, World!', $response->content); + self::assertSame('"etag-hello"', $response->etag); + self::assertNotNull($response->contentType); + self::assertStringStartsWith('text/plain', $response->contentType); + self::assertSame('; rel="next"', $response->link); + self::assertSame($request, $response->request); + } + + public function testExecuteSendsAuthorizationTokenAndUserAgent(): void + { + /** @var list $received */ + $received = []; + $curl = new Curl(); + $curl->add(new CurlRequest( + self::baseUrl() . '/echo-headers', + null, + 'secret-token', + static function (CurlResponse $r) use (&$received): void { + $received[] = $r; + }, + )); + $curl->execute(); + + self::assertCount(1, $received); + self::assertNotNull($received[0]->content); + $body = json_decode($received[0]->content, true); + self::assertIsArray($body); + self::assertSame('token secret-token', $body['authorization']); + self::assertSame('alfred-github-workflow', $body['user-agent']); + self::assertSame(self::baseUrl() . '/echo-headers', $body['x-url']); + } + + public function testExecuteOmitsAuthorizationHeaderWhenTokenIsNull(): void + { + /** @var list $received */ + $received = []; + $curl = new Curl(); + $curl->add(new CurlRequest( + self::baseUrl() . '/echo-headers', + null, + null, + static function (CurlResponse $r) use (&$received): void { + $received[] = $r; + }, + )); + $curl->execute(); + + self::assertNotNull($received[0]->content); + $body = json_decode($received[0]->content, true); + self::assertIsArray($body); + self::assertSame('', $body['authorization']); + } + + public function testExecuteSendsIfNoneMatchAndHandles304(): void + { + /** @var list $received */ + $received = []; + $curl = new Curl(); + $curl->add(new CurlRequest( + self::baseUrl() . '/etag', + '"v1"', + null, + static function (CurlResponse $r) use (&$received): void { + $received[] = $r; + }, + )); + $curl->execute(); + + self::assertCount(1, $received); + self::assertSame(304, $received[0]->status); + self::assertNull($received[0]->content); + self::assertSame('"v1"', $received[0]->etag); + } + + public function testExecuteFetchesFreshContentWhenEtagDoesNotMatch(): void + { + /** @var list $received */ + $received = []; + $curl = new Curl(); + $curl->add(new CurlRequest( + self::baseUrl() . '/etag', + '"stale"', + null, + static function (CurlResponse $r) use (&$received): void { + $received[] = $r; + }, + )); + $curl->execute(); + + self::assertSame(200, $received[0]->status); + self::assertSame('fresh content', $received[0]->content); + self::assertSame('"v1"', $received[0]->etag); + } + + public function testExecuteDoesNotStoreContentForNon200Status(): void + { + /** @var list $received */ + $received = []; + $curl = new Curl(); + $curl->add(new CurlRequest( + self::baseUrl() . '/server-error', + null, + null, + static function (CurlResponse $r) use (&$received): void { + $received[] = $r; + }, + )); + $curl->execute(); + + self::assertSame(500, $received[0]->status); + self::assertNull($received[0]->content); + } + + public function testExecuteRunsMultipleRequestsThroughCurlMulti(): void + { + // 5 requests through one Curl instance. Parallelism is structural (curl_multi_*) — we + // assert correctness rather than timing, since CI runners' worker scheduling is too + // noisy to derive a stable wall-clock threshold. + /** @var array $bodies */ + $bodies = []; + $curl = new Curl(); + foreach (['a', 'b', 'c', 'd', 'e'] as $id) { + $curl->add(new CurlRequest( + self::baseUrl() . '/echo-id?id=' . $id, + null, + null, + static function (CurlResponse $r) use (&$bodies, $id): void { + $bodies[$id] = (string) $r->content; + }, + )); + } + $curl->execute(); + + // Order of arrival depends on completion timing; normalize before comparing. + ksort($bodies); + self::assertSame( + ['a' => 'id-a', 'b' => 'id-b', 'c' => 'id-c', 'd' => 'id-d', 'e' => 'id-e'], + $bodies, + ); + } +} diff --git a/tests/FetcherTest.php b/tests/FetcherTest.php new file mode 100644 index 0000000..0571add --- /dev/null +++ b/tests/FetcherTest.php @@ -0,0 +1,277 @@ + self::$hitsFile]; + } + + public static function setUpBeforeClass(): void + { + self::$tmpDir = sys_get_temp_dir() . '/agw-fetcher-test-' . bin2hex(random_bytes(6)); + mkdir(self::$tmpDir, 0o755, true); + putenv('alfred_workflow_data=' . self::$tmpDir); + + Workflow::init(); + + self::$hitsFile = self::$tmpDir . '/hits.log'; + + parent::setUpBeforeClass(); + } + + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + + // Avoid the Workflow shutdown handler firing background-refresh subprocesses on PHP exit. + self::clearRefreshUrls(); + + if (is_dir(self::$tmpDir)) { + $files = glob(self::$tmpDir . '/*') ?: []; + foreach ($files as $f) { + if (is_file($f)) { + @unlink($f); + } + } + @rmdir(self::$tmpDir); + } + } + + protected function setUp(): void + { + Workflow::deleteCache(); + file_put_contents(self::$hitsFile, ''); + self::clearRefreshUrls(); + } + + private static function clearRefreshUrls(): void + { + $prop = new ReflectionProperty(Workflow::class, 'refreshUrls'); + $prop->setValue(null, []); + } + + private function totalHits(): int + { + $contents = file_get_contents(self::$hitsFile); + if (false === $contents || '' === $contents) { + return 0; + } + + return substr_count($contents, "\n"); + } + + public function testRequestUrlFetchesAndCachesJson(): void + { + $url = self::baseUrl() . '/json?id=hello'; + + $first = Fetcher::requestUrl($url); + self::assertInstanceOf(stdClass::class, $first); + self::assertSame('hello', $first->id); + self::assertSame('val-hello', $first->value); + self::assertSame(1, $this->totalHits()); + + // Second call within maxAge window: served from cache, no HTTP roundtrip. + $second = Fetcher::requestUrl($url); + self::assertInstanceOf(stdClass::class, $second); + self::assertSame('hello', $second->id); + self::assertSame(1, $this->totalHits()); + } + + public function testRequestUrlFollowsPaginationViaLinkHeader(): void + { + $url = self::baseUrl() . '/paginated?p=1'; + + $merged = Fetcher::requestUrl($url); + self::assertIsArray($merged); + self::assertCount(6, $merged); + self::assertSame(1, $merged[0]->page); + self::assertSame(3, $merged[5]->page); + self::assertSame(3, $this->totalHits()); + + // Re-request: full chain served from cache via parent linkage, no extra hits. + $cached = Fetcher::requestUrl($url); + self::assertCount(6, $cached); + self::assertSame(3, $this->totalHits()); + } + + public function testRequestUrlFirstPageOnlyStopsAfterOneRequest(): void + { + $first = Fetcher::requestUrl( + self::baseUrl() . '/paginated?p=1', + new FetchOptions(firstPageOnly: true), + ); + + self::assertIsArray($first); + self::assertCount(2, $first); + self::assertSame(1, $first[0]->page); + self::assertSame(1, $this->totalHits()); + } + + public function testRequestUrlNonOkDeletesCacheRow(): void + { + $url = self::baseUrl() . '/error500'; + + // Pre-populate so we can observe deletion. + Workflow::getStatement('REPLACE INTO request_cache VALUES(?, ?, ?, ?, 0, NULL)') + ->execute([$url, time() - 3600, '"x"', json_encode(['x' => 1])]); + + $result = Fetcher::requestUrl($url, new FetchOptions(refreshInBackground: false)); + self::assertSame([], $result); + + $stmt = Workflow::getStatement('SELECT 1 FROM request_cache WHERE url = ?'); + $stmt->execute([$url]); + self::assertFalse($stmt->fetchColumn()); + } + + public function testRequestUrlReusesCachedContentOn304(): void + { + $url = self::baseUrl() . '/etag?v=v1'; + + Fetcher::requestUrl($url); + self::assertSame(1, $this->totalHits()); + + // Make stale; refreshInBackground=false forces a synchronous live fetch. + Workflow::getStatement('UPDATE request_cache SET timestamp = ? WHERE url = ?') + ->execute([time() - 3600, $url]); + + $second = Fetcher::requestUrl($url, new FetchOptions(refreshInBackground: false)); + self::assertSame('v1', $second->value); + self::assertSame(2, $this->totalHits()); + } + + public function testRequestUrlStaleWithBackgroundRefreshServesCacheAndMarksUrl(): void + { + $url = self::baseUrl() . '/json?id=stale'; + + Fetcher::requestUrl($url); + self::assertSame(1, $this->totalHits()); + + // Force stale + outside the 3-minute background-refresh debounce. + Workflow::getStatement('UPDATE request_cache SET timestamp = ?, refresh = ? WHERE url = ?') + ->execute([time() - 3600, time() - 3600, $url]); + + $cached = Fetcher::requestUrl($url); + // Stale-while-revalidate: cached content served, no live fetch. + self::assertSame(1, $this->totalHits()); + self::assertInstanceOf(stdClass::class, $cached); + self::assertSame('stale', $cached->id); + + $prop = new ReflectionProperty(Workflow::class, 'refreshUrls'); + $refreshUrls = $prop->getValue(); + self::assertIsArray($refreshUrls); + self::assertArrayHasKey($url, $refreshUrls); + } + + public function testRequestUrlUnwrapsItemsKey(): void + { + $result = Fetcher::requestUrl(self::baseUrl() . '/items-wrapper'); + + self::assertIsArray($result); + self::assertCount(2, $result); + self::assertSame('one', $result[0]->name); + self::assertSame('two', $result[1]->name); + } + + public function testRequestUrlAppliesFieldWhitelist(): void + { + $options = new FetchOptions(fields: [ + 'sha', + 'commit' => ['author' => ['date']], + ]); + + $result = Fetcher::requestUrl(self::baseUrl() . '/picky', $options); + + self::assertInstanceOf(stdClass::class, $result); + self::assertSame('abc', $result->sha); + self::assertObjectNotHasProperty('extra', $result); + self::assertObjectNotHasProperty('message', $result); + self::assertInstanceOf(stdClass::class, $result->commit); + self::assertInstanceOf(stdClass::class, $result->commit->author); + self::assertSame('2024-01-01', $result->commit->author->date); + self::assertObjectNotHasProperty('name', $result->commit->author); + } + + public function testRequestUrlReencodesNonJsonResponse(): void + { + $result = Fetcher::requestUrl(self::baseUrl() . '/non-json'); + + self::assertSame('Hello, World!', $result); + } + + public function testRequestRawReturnsBodyAndDoesNotCache(): void + { + $url = self::baseUrl() . '/non-json'; + $body = Fetcher::requestRaw($url); + + self::assertSame('Hello, World!', $body); + + $stmt = Workflow::getStatement('SELECT 1 FROM request_cache WHERE url = ?'); + $stmt->execute([$url]); + self::assertFalse($stmt->fetchColumn()); + } + + public function testStreamUrlYieldsEachItemFromCacheChain(): void + { + $url = self::baseUrl() . '/paginated?p=1'; + Fetcher::requestUrl($url); + $hitsBefore = $this->totalHits(); + + $items = iterator_to_array(Fetcher::streamUrl($url), false); + + self::assertCount(6, $items); + self::assertSame(1, $items[0]->page); + self::assertSame(1, $items[0]->item); + self::assertSame(3, $items[5]->page); + self::assertSame(6, $items[5]->item); + // Walks cache only — no fresh HTTP. + self::assertSame($hitsBefore, $this->totalHits()); + } + + public function testStreamUrlFirstPageOnlyStopsAtFirstPage(): void + { + $url = self::baseUrl() . '/paginated?p=1'; + Fetcher::requestUrl($url); + + $items = iterator_to_array( + Fetcher::streamUrl($url, new FetchOptions(firstPageOnly: true)), + false, + ); + + self::assertCount(2, $items); + self::assertSame(1, $items[0]->page); + } + + public function testQueueRunDispatchesAllRequests(): void + { + // 5 requests through one Fetcher. Parallelism is structural (curl_multi_* under the + // hood) — we assert correctness rather than timing, since CI runners' worker scheduling + // is too noisy to derive a stable wall-clock threshold. + /** @var array $bodies */ + $bodies = []; + $f = new Fetcher(); + foreach (['a', 'b', 'c', 'd', 'e'] as $id) { + $f->queueUrl( + self::baseUrl() . '/slow?id=' . $id, + static function ($r) use (&$bodies, $id): void { + $bodies[$id] = $r instanceof stdClass ? $r->id : null; + }, + ); + } + $f->run(); + + ksort($bodies); + self::assertSame( + ['a' => 'a', 'b' => 'b', 'c' => 'c', 'd' => 'd', 'e' => 'e'], + $bodies, + ); + } +} diff --git a/tests/HttpServerTestCase.php b/tests/HttpServerTestCase.php new file mode 100644 index 0000000..b13a494 --- /dev/null +++ b/tests/HttpServerTestCase.php @@ -0,0 +1,94 @@ + + */ + protected static function serverEnv(): array + { + return []; + } + + public static function setUpBeforeClass(): void + { + $port = self::findFreePort(); + self::$baseUrl = 'http://127.0.0.1:' . $port; + + $cmd = sprintf( + 'exec php -S 127.0.0.1:%d %s', + $port, + escapeshellarg(static::routerPath()), + ); + $env = ['PHP_CLI_SERVER_WORKERS' => '4'] + static::serverEnv() + getenv(); + + $process = proc_open( + $cmd, + [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ], + $pipes, + null, + $env, + ); + if (!is_resource($process)) { + self::fail('failed to spawn test server'); + } + self::$serverProcess = $process; + + for ($i = 0; $i < 100; ++$i) { + $sock = @fsockopen('127.0.0.1', $port, $errno, $errstr, 0.1); + if (false !== $sock) { + fclose($sock); + + return; + } + usleep(50_000); + } + self::fail('test server did not start within 5 seconds'); + } + + public static function tearDownAfterClass(): void + { + if (is_resource(self::$serverProcess)) { + proc_terminate(self::$serverProcess); + proc_close(self::$serverProcess); + self::$serverProcess = null; + } + } + + protected static function baseUrl(): string + { + return self::$baseUrl; + } + + private static function findFreePort(): int + { + $sock = stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr); + if (false === $sock) { + self::fail("could not allocate test port: $errstr"); + } + $name = stream_socket_get_name($sock, false); + if (false === $name) { + fclose($sock); + self::fail('could not read socket name'); + } + fclose($sock); + + return (int) substr($name, strrpos($name, ':') + 1); + } +} diff --git a/tests/ItemTest.php b/tests/ItemTest.php new file mode 100644 index 0000000..878850b --- /dev/null +++ b/tests/ItemTest.php @@ -0,0 +1,248 @@ +match($query)); + } + + /** @return iterable */ + public static function dataMatch(): iterable + { + $repo = static fn () => Item::create()->title('user/repo'); + + yield 'subsequence usr' => [$repo(), 'usr', true]; + yield 'full prefix' => [$repo(), 'user', true]; + yield 'spans separator' => [$repo(), 'u/r', true]; + yield 'missing chars' => [$repo(), 'xyz', false]; + yield 'trailing extra char' => [$repo(), 'repo!', false]; + yield 'uppercase query, mixed-case title' => [Item::create()->title('User/Repo'), 'USER', true]; + yield 'mixed-case query, mixed-case title' => [Item::create()->title('User/Repo'), 'uSeR', true]; + yield 'comparator overrides title (match)' => [ + Item::create()->title('Display Title')->comparator('user/repo'), + 'user', + true, + ]; + yield 'comparator overrides title (no match against display)' => [ + Item::create()->title('Display Title')->comparator('user/repo'), + 'display', + false, + ]; + yield 'prefix stripped from query (onlyTitle=false)' => [ + Item::create()->title('repo')->prefix('gh ', false), + 'gh repo', + true, + ]; + yield 'prefix kept in query (onlyTitle=true)' => [ + Item::create()->title('repo')->prefix('gh '), + 'gh repo', + false, + ]; + } + + /** + * @param list $titles + * @param list $expectedOrder + */ + #[DataProvider('dataMatchRankingOrder')] + public function testMatchRankingOrder(string $query, array $titles, array $expectedOrder): void + { + $items = []; + foreach ($titles as $title) { + $item = Item::create()->title($title); + self::assertTrue($item->match($query), "expected '$title' to match '$query'"); + $items[] = $item; + } + usort($items, static fn (Item $a, Item $b) => $a->compare($b)); + + $titleProp = new ReflectionProperty(Item::class, 'title'); + $sorted = array_map(static fn (Item $i) => (string) $titleProp->getValue($i), $items); + self::assertSame($expectedOrder, $sorted); + } + + /** @return array, list}> */ + public static function dataMatchRankingOrder(): array + { + return [ + 'consecutive prefix beats interspersed' => [ + 'user/r', + ['user/something-with-r-later', 'user/repo'], + ['user/repo', 'user/something-with-r-later'], + ], + 'shorter title wins on equal sameChars' => [ + 'rep', + ['repository', 'repo'], + ['repo', 'repository'], + ], + ]; + } + + public function testCompareFallsBackToPrioWhenSameCharsEqual(): void + { + $high = Item::create()->title('alpha')->prio(10); + $low = Item::create()->title('alpha')->prio(1); + + $high->match('a'); + $low->match('a'); + + self::assertSame(-1, $high->compare($low)); + self::assertSame(1, $low->compare($high)); + } + + public function testToXmlRendersTitleSubtitleAndIcon(): void + { + $item = Item::create() + ->title('Hello') + ->subtitle('World') + ->icon('file') + ->arg('https://example.com'); + + $xml = self::parseXml(Item::toXml([$item], false, false, null)); + + self::assertSame('Hello', (string) $xml->item[0]->title); + self::assertSame('World', (string) $xml->item[0]->subtitle); + self::assertSame('icons/file.png', (string) $xml->item[0]->icon); + } + + public function testToXmlFallsBackToDefaultIconWhenFileMissing(): void + { + $item = Item::create()->title('Hello')->icon('does-not-exist'); + + $xml = self::parseXml(Item::toXml([$item], false, false, null)); + + self::assertSame('icon.png', (string) $xml->item[0]->icon); + } + + public function testToXmlEscapesSpecialChars(): void + { + $item = Item::create()->title('A & B ')->subtitle('"q\'s"'); + + $raw = Item::toXml([$item], false, false, null); + self::assertIsString($raw); + self::assertStringContainsString('A & B <c>', $raw); + + $xml = self::parseXml($raw); + self::assertSame('A & B ', (string) $xml->item[0]->title); + self::assertSame('"q\'s"', (string) $xml->item[0]->subtitle); + } + + #[DataProvider('dataToXmlArg')] + public function testToXmlArg(string $arg, bool $enterprise, ?string $baseUrl, string $expected): void + { + $item = Item::create()->title('t')->arg($arg); + + $xml = self::parseXml(Item::toXml([$item], $enterprise, false, $baseUrl)); + + self::assertSame($expected, (string) $xml->item[0]['arg']); + } + + /** @return iterable */ + public static function dataToXmlArg(): iterable + { + yield 'enterprise prefixes non-url' => ['> login', true, null, 'e > login']; + yield 'url passes through unchanged' => ['https://example.com', true, null, 'https://example.com']; + yield 'leading slash is expanded with baseUrl' => ['/user/repo', false, 'https://github.com', 'https://github.com/user/repo']; + } + + public function testToXmlMarksInvalidItemAndAppendsAdd(): void + { + $item = Item::create()->title('search')->valid(false, ' …'); + + $xml = self::parseXml(Item::toXml([$item], false, false, null)); + + self::assertSame('no', (string) $xml->item[0]['valid']); + self::assertSame('search …', (string) $xml->item[0]->title); + } + + #[DataProvider('dataToXmlAutocomplete')] + public function testToXmlAutocomplete(Item $item, bool $hotkey, ?string $expected): void + { + $xml = self::parseXml(Item::toXml([$item], false, $hotkey, null)); + + if (null === $expected) { + self::assertNull($xml->item[0]['autocomplete']); + } else { + self::assertSame($expected, (string) $xml->item[0]['autocomplete']); + } + } + + /** @return iterable */ + public static function dataToXmlAutocomplete(): iterable + { + yield 'title default with leading space (no hotkey)' => [ + Item::create()->title('repo')->autocomplete(), + false, + ' repo', + ]; + yield 'title default without space (hotkey)' => [ + Item::create()->title('repo')->autocomplete(), + true, + 'repo', + ]; + yield 'comparator overrides title' => [ + Item::create()->title('Display')->comparator('cmp')->autocomplete(), + true, + 'cmp', + ]; + yield 'explicit string wins over title' => [ + Item::create()->title('Display')->autocomplete('forced'), + true, + 'forced', + ]; + yield 'prefix included when not onlyTitle' => [ + Item::create()->title('repo')->prefix('gh ', false), + true, + 'gh repo', + ]; + yield 'autocomplete=false → no attribute' => [ + Item::create()->title('repo')->autocomplete(false), + true, + null, + ]; + } + + public function testToXmlPrefixIsPrependedToTitle(): void + { + $item = Item::create()->title('repo')->prefix('gh '); + + $xml = self::parseXml(Item::toXml([$item], false, false, null)); + + self::assertSame('gh repo', (string) $xml->item[0]->title); + } + + public function testToXmlUid(): void + { + $a = Item::create()->title('repo'); + $b = Item::create()->title('repo'); + $random = Item::create()->title('repo')->randomUid(); + + $xmlA = self::parseXml(Item::toXml([$a], false, false, null)); + $xmlB = self::parseXml(Item::toXml([$b], false, false, null)); + $xmlR = self::parseXml(Item::toXml([$random], false, false, null)); + + self::assertSame( + (string) $xmlA->item[0]['uid'], + (string) $xmlB->item[0]['uid'], + 'same title must produce identical uid', + ); + self::assertNotSame( + (string) $xmlA->item[0]['uid'], + (string) $xmlR->item[0]['uid'], + 'randomUid() must produce a different uid for the same title', + ); + } + + private static function parseXml(string|false $xml): SimpleXMLElement + { + self::assertIsString($xml); + $parsed = simplexml_load_string($xml); + self::assertInstanceOf(SimpleXMLElement::class, $parsed); + + return $parsed; + } +} diff --git a/tests/WorkflowTest.php b/tests/WorkflowTest.php new file mode 100644 index 0000000..cbec06e --- /dev/null +++ b/tests/WorkflowTest.php @@ -0,0 +1,213 @@ +execute(); + Workflow::deleteCache(); + } + + private static function resetStaticState(): void + { + foreach (['items', 'refreshUrls', 'statements'] as $name) { + $prop = new ReflectionProperty(Workflow::class, $name); + $prop->setValue(null, []); + } + + // Workflow::init() only mutates the URL state inside the enterprise branch, + // never restores it. Reset to class defaults so iterations don't leak into each other. + foreach ([ + 'baseUrl' => 'https://github.com', + 'apiUrl' => 'https://api.github.com', + 'gistUrl' => 'https://gist.github.com', + ] as $name => $default) { + $prop = new ReflectionProperty(Workflow::class, $name); + $prop->setValue(null, $default); + } + } + + /** Re-init Workflow after wiping state, so prepared statements bind to the freshly-created PDO. */ + private static function reInit(bool $enterprise = false, ?string $query = null): void + { + self::resetStaticState(); + Workflow::init(enterprise: $enterprise, query: $query); + } + + #[DataProvider('dataGetApiUrl')] + public function testGetApiUrl(?string $path, string $expected): void + { + self::assertSame($expected, Workflow::getApiUrl($path)); + } + + /** @return iterable */ + public static function dataGetApiUrl(): iterable + { + yield 'no path returns base api url' => [null, 'https://api.github.com']; + yield 'path without query gets ?per_page=100' => ['/user', 'https://api.github.com/user?per_page=100']; + yield 'path with query gets &per_page=100' => ['/search?q=foo', 'https://api.github.com/search?q=foo&per_page=100']; + } + + public function testAccessTokenIsScopedByEnterpriseMode(): void + { + Workflow::setAccessToken('user-token'); + self::assertSame('user-token', Workflow::getAccessToken()); + self::assertSame('user-token', Workflow::getConfig('access_token')); + self::assertNull(Workflow::getConfig('enterprise_access_token')); + + // Switching to enterprise mode swaps the config key getAccessToken reads. + self::reInit(enterprise: true); + self::assertNull(Workflow::getAccessToken(), 'enterprise mode must not see the user token'); + + Workflow::setAccessToken('enterprise-token'); + self::assertSame('enterprise-token', Workflow::getAccessToken()); + self::assertSame('enterprise-token', Workflow::getConfig('enterprise_access_token')); + // Writes in enterprise mode must not touch the user token. + self::assertSame('user-token', Workflow::getConfig('access_token')); + + // removeAccessToken targets the current mode's key only. + Workflow::removeAccessToken(); + self::assertNull(Workflow::getAccessToken()); + self::assertSame('user-token', Workflow::getConfig('access_token')); + } + + #[DataProvider('dataGetConfigDefault')] + public function testGetConfigDefault(?string $stored, mixed $default, mixed $expected): void + { + if (null !== $stored) { + Workflow::setConfig('key', $stored); + } + self::assertSame($expected, Workflow::getConfig('key', $default)); + } + + /** @return iterable */ + public static function dataGetConfigDefault(): iterable + { + yield 'missing key returns null without default' => [null, null, null]; + yield 'missing key returns the supplied default' => [null, 'fallback', 'fallback']; + yield 'present key wins over default' => ['stored', 'fallback', 'stored']; + } + + public function testCleanCacheRemovesOnlyOldRows(): void + { + $now = time(); + $oldUrl = 'https://example.com/old'; + $recentUrl = 'https://example.com/recent'; + + $insert = Workflow::getStatement('REPLACE INTO request_cache VALUES(?, ?, ?, ?, 0, NULL)'); + $insert->execute([$oldUrl, $now - 101 * 86400, null, '{}']); + $insert->execute([$recentUrl, $now - 99 * 86400, null, '{}']); + + Workflow::cleanCache(); + + $stmt = Workflow::getStatement('SELECT url FROM request_cache ORDER BY url'); + $stmt->execute(); + $remaining = $stmt->fetchAll(PDO::FETCH_COLUMN); + + self::assertSame([$recentUrl], $remaining); + } + + public function testAddItemIfMatchesFiltersByQuery(): void + { + self::reInit(query: 'foo'); + + Workflow::addItemIfMatches(Item::create()->title('foo')); + Workflow::addItemIfMatches(Item::create()->title('bar')); + + $rendered = Workflow::getItemsAsXml(); + self::assertIsString($rendered); + $xml = simplexml_load_string($rendered); + self::assertInstanceOf(SimpleXMLElement::class, $xml); + + self::assertCount(1, $xml->item); + self::assertSame('foo', (string) $xml->item[0]->title); + } + + #[DataProvider('dataInitEnterpriseUrls')] + public function testInitEnterpriseUrls(?string $enterpriseUrl, ?string $expectedBaseUrl, ?string $expectedApiUrl, ?string $expectedGistUrl): void + { + if (null !== $enterpriseUrl) { + Workflow::setConfig('enterprise_url', $enterpriseUrl); + } + self::reInit(enterprise: true); + + self::assertSame($expectedBaseUrl, Workflow::getBaseUrl()); + self::assertSame($expectedApiUrl, Workflow::getApiUrl()); + self::assertSame($expectedGistUrl, Workflow::getGistUrl()); + } + + /** @return iterable */ + public static function dataInitEnterpriseUrls(): iterable + { + yield 'unset enterprise_url collapses URLs to null' => [null, null, null, null]; + yield 'custom host derives api/gist subpaths' => [ + 'https://ghe.example.com', + 'https://ghe.example.com', + 'https://ghe.example.com/api/v3', + 'https://ghe.example.com/gist', + ]; + yield 'github.com /enterprises/ keeps default URLs (cloud mode)' => [ + 'https://github.com/enterprises/foo', + 'https://github.com', + 'https://api.github.com', + 'https://gist.github.com', + ]; + } + + #[DataProvider('dataCheckUpdate')] + public function testCheckUpdate(int $autoupdate, ?string $cachedTag, bool $expected): void + { + Workflow::setConfig('autoupdate', $autoupdate); + + if (null !== $cachedTag) { + // Pre-populate the release endpoint so Fetcher serves from cache instead of + // hitting the real api.github.com. + Workflow::getStatement('REPLACE INTO request_cache VALUES(?, ?, ?, ?, 0, NULL)') + ->execute([ + 'https://api.github.com/repos/gharlan/alfred-github-workflow/releases/latest', + time(), + null, + json_encode(['tag_name' => $cachedTag]), + ]); + } + + self::assertSame($expected, Workflow::checkUpdate()); + } + + /** @return iterable */ + public static function dataCheckUpdate(): iterable + { + yield 'autoupdate disabled never reports an update' => [0, null, false]; + yield 'cached current version is not an update' => [1, 'v' . Workflow::VERSION, false]; + yield 'cached older version is not an update' => [1, 'v0.0.1', false]; + yield 'cached newer version is an update' => [1, 'v99.99.99', true]; + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..bcf8b03 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,5 @@ +; rel="next"'); + echo 'Hello, World!'; + + return true; + + case '/echo-headers': + header('Content-Type: application/json'); + echo json_encode([ + 'authorization' => $_SERVER['HTTP_AUTHORIZATION'] ?? '', + 'user-agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', + 'x-url' => $_SERVER['HTTP_X_URL'] ?? '', + ]); + + return true; + + case '/etag': + header('ETag: "v1"'); + if ('"v1"' === ($_SERVER['HTTP_IF_NONE_MATCH'] ?? '')) { + http_response_code(304); + + return true; + } + header('Content-Type: text/plain'); + echo 'fresh content'; + + return true; + + case '/echo-id': + $id = $_GET['id'] ?? ''; + header('Content-Type: text/plain'); + echo 'id-' . $id; + + return true; + + case '/server-error': + http_response_code(500); + echo 'boom'; + + return true; +} + +http_response_code(404); + +return true; diff --git a/tests/fixtures/fetcher-server.php b/tests/fixtures/fetcher-server.php new file mode 100644 index 0000000..7ab85e6 --- /dev/null +++ b/tests/fixtures/fetcher-server.php @@ -0,0 +1,99 @@ + $id, 'value' => 'val-' . $id]); + + return true; + + case '/paginated': + // 3 pages of 2 items each, chained via Link: rel="next". + header('Content-Type: application/json'); + $page = (int) ($_GET['p'] ?? 1); + $total = 3; + if ($page < $total) { + header('Link: <' . $baseUrl . '/paginated?p=' . ($page + 1) . '>; rel="next"'); + } + echo json_encode([ + ['page' => $page, 'item' => ($page - 1) * 2 + 1], + ['page' => $page, 'item' => ($page - 1) * 2 + 2], + ]); + + return true; + + case '/etag': + $value = $_GET['v'] ?? 'default'; + $etag = '"' . $value . '"'; + header('ETag: ' . $etag); + header('Content-Type: application/json'); + if (($_SERVER['HTTP_IF_NONE_MATCH'] ?? '') === $etag) { + http_response_code(304); + + return true; + } + echo json_encode(['value' => $value]); + + return true; + + case '/items-wrapper': + header('Content-Type: application/json'); + echo json_encode([ + 'total_count' => 2, + 'items' => [ + ['name' => 'one'], + ['name' => 'two'], + ], + ]); + + return true; + + case '/picky': + // Rich object for the field-whitelist test. + header('Content-Type: application/json'); + echo json_encode([ + 'sha' => 'abc', + 'message' => 'msg', + 'extra' => 'should-be-stripped', + 'commit' => [ + 'message' => 'commit-msg', + 'author' => [ + 'name' => 'Alice', + 'date' => '2024-01-01', + ], + ], + ]); + + return true; + + case '/non-json': + header('Content-Type: text/plain'); + echo 'Hello, World!'; + + return true; + + case '/error500': + http_response_code(500); + echo 'boom'; + + return true; + + case '/slow': + header('Content-Type: application/json'); + echo json_encode(['id' => $_GET['id'] ?? '']); + + return true; +} + +http_response_code(404); + +return true;