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;