diff --git a/composer.json b/composer.json index 1126626..ef9fb71 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,15 @@ "require": { "php": ">=8.1", + "ext-dom": "*", + "ext-curl": "*", "phpgt/input": "^1.2", + "phpgt/typesafegetter": "^1.3", + "phpgt/promise": "^2.2", + "phpgt/async": "^1.0", + "phpgt/json": "^1.2", + "phpgt/curl": "^3.1", + "phpgt/propfunc": "^1.0", "psr/http-message": "^2.0", "willdurand/negotiation": "3.1.0" }, diff --git a/composer.lock b/composer.lock index d6d0cd7..984ffa3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,169 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "cd41e35ef1bc48a0b4ac5fd4c4e6491a", + "content-hash": "afa459c231a6a5b34ee72c903a462d2b", "packages": [ + { + "name": "phpgt/async", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/PhpGt/Async.git", + "reference": "3d2bdeca8cafc8573b416da3ac591d5d88f6dea9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PhpGt/Async/zipball/3d2bdeca8cafc8573b416da3ac591d5d88f6dea9", + "reference": "3d2bdeca8cafc8573b416da3ac591d5d88f6dea9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=7.4", + "phpgt/promise": "^2.0" + }, + "require-dev": { + "phpstan/phpstan": "^v1.8", + "phpunit/phpunit": "^v9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Gt\\Async\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Promise-based non-blocking operations.", + "support": { + "issues": "https://github.com/PhpGt/Async/issues", + "source": "https://github.com/PhpGt/Async/tree/v1.0.0" + }, + "funding": [ + { + "url": "https://github.com/phpgt", + "type": "github" + } + ], + "time": "2023-01-19T11:11:58+00:00" + }, + { + "name": "phpgt/curl", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/PhpGt/Curl.git", + "reference": "a7e0856d3735f8f69d6d5fbf2f6f26664e55e3b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PhpGt/Curl/zipball/a7e0856d3735f8f69d6d5fbf2f6f26664e55e3b7", + "reference": "a7e0856d3735f8f69d6d5fbf2f6f26664e55e3b7", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "php": ">=8.1", + "phpgt/json": "^1.2" + }, + "require-dev": { + "phpmd/phpmd": "^2.13", + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^10.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Gt\\Curl\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Greg Bowler", + "email": "greg.bowler@g105b.com" + } + ], + "description": "cURL object wrapper.", + "keywords": [ + "curl", + "curl_multi", + "http", + "interface" + ], + "support": { + "issues": "https://github.com/PhpGt/Curl/issues", + "source": "https://github.com/PhpGt/Curl/tree/v3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/PhpGt", + "type": "github" + } + ], + "time": "2023-04-29T17:28:12+00:00" + }, + { + "name": "phpgt/dataobject", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/PhpGt/DataObject.git", + "reference": "d8edf5fc6f8dd4a2f3674f034ae8802873a4e1ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PhpGt/DataObject/zipball/d8edf5fc6f8dd4a2f3674f034ae8802873a4e1ce", + "reference": "d8edf5fc6f8dd4a2f3674f034ae8802873a4e1ce", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=8.0", + "phpgt/typesafegetter": "^1.0" + }, + "require-dev": { + "phpmd/phpmd": "^2.13", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.1", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Gt\\DataObject\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Greg Bowler", + "email": "greg.bowler@g105b.com" + } + ], + "description": " Structured, type-safe, immutable data transfer.", + "support": { + "issues": "https://github.com/PhpGt/DataObject/issues", + "source": "https://github.com/PhpGt/DataObject/tree/v1.0.5" + }, + "funding": [ + { + "url": "https://github.com/phpgt", + "type": "github" + } + ], + "time": "2023-04-28T15:05:31+00:00" + }, { "name": "phpgt/input", "version": "v1.2.3", @@ -53,6 +214,219 @@ ], "time": "2023-05-02T17:16:00+00:00" }, + { + "name": "phpgt/json", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/PhpGt/Json.git", + "reference": "26b3972a9d95b8d7d2985841f3b69d8876fc7c38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PhpGt/Json/zipball/26b3972a9d95b8d7d2985841f3b69d8876fc7c38", + "reference": "26b3972a9d95b8d7d2985841f3b69d8876fc7c38", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=8.1", + "phpgt/dataobject": "^1.0" + }, + "require-dev": { + "phpstan/phpstan": "^v1.8", + "phpunit/phpunit": "^v9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Gt\\Json\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Greg Bowler", + "email": "greg.bowler@g105b.com" + } + ], + "description": " Structured, type-safe, immutable JSON objects.", + "support": { + "issues": "https://github.com/PhpGt/Json/issues", + "source": "https://github.com/PhpGt/Json/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/PhpGt", + "type": "github" + } + ], + "time": "2022-09-15T11:31:25+00:00" + }, + { + "name": "phpgt/promise", + "version": "v2.2.3", + "source": { + "type": "git", + "url": "https://github.com/PhpGt/Promise.git", + "reference": "907b3a450f3252077f80b50289f73ab930ca2cdc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PhpGt/Promise/zipball/907b3a450f3252077f80b50289f73ab930ca2cdc", + "reference": "907b3a450f3252077f80b50289f73ab930ca2cdc", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpmd/phpmd": "^2.13", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.1", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Gt\\Promise\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Pleasantly work with asynchronous code.", + "keywords": [ + "W3C", + "async", + "asynchronous", + "callback", + "concurrency", + "concurrent", + "deferred", + "promise", + "then" + ], + "support": { + "issues": "https://github.com/PhpGt/Promise/issues", + "source": "https://github.com/PhpGt/Promise/tree/v2.2.3" + }, + "funding": [ + { + "url": "https://github.com/sponsors/PhpGt", + "type": "github" + } + ], + "time": "2023-07-01T10:59:01+00:00" + }, + { + "name": "phpgt/propfunc", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/PhpGt/PropFunc.git", + "reference": "091213649e89ff22d1ef640b46fbee5215c65520" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PhpGt/PropFunc/zipball/091213649e89ff22d1ef640b46fbee5215c65520", + "reference": "091213649e89ff22d1ef640b46fbee5215c65520", + "shasum": "" + }, + "require": { + "php": ">=8.0" + }, + "require-dev": { + "phpstan/phpstan": ">=0.12", + "phpunit/phpunit": ">=9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Gt\\PropFunc\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Greg Bowler", + "email": "greg.bowler@g105b.com", + "homepage": "https://www.g105b.com", + "role": "Developer" + } + ], + "description": "Property accessor and mutator functions.", + "support": { + "issues": "https://github.com/PhpGt/PropFunc/issues", + "source": "https://github.com/PhpGt/PropFunc/tree/v1.0.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/PhpGt", + "type": "github" + } + ], + "time": "2021-03-23T12:46:44+00:00" + }, + { + "name": "phpgt/typesafegetter", + "version": "v1.3.2", + "source": { + "type": "git", + "url": "https://github.com/PhpGt/TypeSafeGetter.git", + "reference": "f760c05a37b1cc188dcbf800c5fdfab8a926b4b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PhpGt/TypeSafeGetter/zipball/f760c05a37b1cc188dcbf800c5fdfab8a926b4b0", + "reference": "f760c05a37b1cc188dcbf800c5fdfab8a926b4b0", + "shasum": "" + }, + "require": { + "php": ">=8.0" + }, + "require-dev": { + "phpmd/phpmd": "^2.13", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.1", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Gt\\TypeSafeGetter\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Greg Bowler", + "email": "greg.bowler@g105b.com" + } + ], + "description": "An interface for objects that expose type-safe getter methods.", + "support": { + "issues": "https://github.com/PhpGt/TypeSafeGetter/issues", + "source": "https://github.com/PhpGt/TypeSafeGetter/tree/v1.3.2" + }, + "funding": [ + { + "url": "https://github.com/sponsors/PhpGt", + "type": "github" + } + ], + "time": "2023-04-28T14:42:27+00:00" + }, { "name": "psr/http-message", "version": "2.0", @@ -362,16 +736,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.15.4", + "version": "v4.16.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290" + "reference": "19526a33fb561ef417e822e85f08a00db4059c17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6bb5176bc4af8bcb7d926f88718db9b96a2d4290", - "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17", + "reference": "19526a33fb561ef417e822e85f08a00db4059c17", "shasum": "" }, "require": { @@ -412,22 +786,22 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.4" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0" }, - "time": "2023-03-05T19:49:14+00:00" + "time": "2023-06-25T14:52:30+00:00" }, { "name": "pdepend/pdepend", - "version": "2.13.0", + "version": "2.14.0", "source": { "type": "git", "url": "https://github.com/pdepend/pdepend.git", - "reference": "31be7cd4f305f3f7b52af99c1cb13fc938d1cfad" + "reference": "1121d4b04af06e33e9659bac3a6741b91cab1de1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pdepend/pdepend/zipball/31be7cd4f305f3f7b52af99c1cb13fc938d1cfad", - "reference": "31be7cd4f305f3f7b52af99c1cb13fc938d1cfad", + "url": "https://api.github.com/repos/pdepend/pdepend/zipball/1121d4b04af06e33e9659bac3a6741b91cab1de1", + "reference": "1121d4b04af06e33e9659bac3a6741b91cab1de1", "shasum": "" }, "require": { @@ -461,9 +835,15 @@ "BSD-3-Clause" ], "description": "Official version of pdepend to be handled with Composer", + "keywords": [ + "PHP Depend", + "PHP_Depend", + "dev", + "pdepend" + ], "support": { "issues": "https://github.com/pdepend/pdepend/issues", - "source": "https://github.com/pdepend/pdepend/tree/2.13.0" + "source": "https://github.com/pdepend/pdepend/tree/2.14.0" }, "funding": [ { @@ -471,7 +851,7 @@ "type": "tidelift" } ], - "time": "2023-02-28T20:56:15+00:00" + "time": "2023-05-26T13:15:18+00:00" }, { "name": "phar-io/manifest", @@ -669,16 +1049,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.14", + "version": "1.10.22", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "d232901b09e67538e5c86a724be841bea5768a7c" + "reference": "97d694dfd4ceb57bcce4e3b38548f13ea62e4287" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d232901b09e67538e5c86a724be841bea5768a7c", - "reference": "d232901b09e67538e5c86a724be841bea5768a7c", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/97d694dfd4ceb57bcce4e3b38548f13ea62e4287", + "reference": "97d694dfd4ceb57bcce4e3b38548f13ea62e4287", "shasum": "" }, "require": { @@ -727,20 +1107,20 @@ "type": "tidelift" } ], - "time": "2023-04-19T13:47:27+00:00" + "time": "2023-06-30T20:04:11+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "10.1.1", + "version": "10.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "884a0da7f9f46f28b2cb69134217fd810b793974" + "reference": "db1497ec8dd382e82c962f7abbe0320e4882ee4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/884a0da7f9f46f28b2cb69134217fd810b793974", - "reference": "884a0da7f9f46f28b2cb69134217fd810b793974", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/db1497ec8dd382e82c962f7abbe0320e4882ee4e", + "reference": "db1497ec8dd382e82c962f7abbe0320e4882ee4e", "shasum": "" }, "require": { @@ -797,7 +1177,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.1" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.2" }, "funding": [ { @@ -805,20 +1185,20 @@ "type": "github" } ], - "time": "2023-04-17T12:15:40+00:00" + "time": "2023-05-22T09:04:27+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "4.0.1", + "version": "4.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "fd9329ab3368f59fe1fe808a189c51086bd4b6bd" + "reference": "5647d65443818959172645e7ed999217360654b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/fd9329ab3368f59fe1fe808a189c51086bd4b6bd", - "reference": "fd9329ab3368f59fe1fe808a189c51086bd4b6bd", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/5647d65443818959172645e7ed999217360654b6", + "reference": "5647d65443818959172645e7ed999217360654b6", "shasum": "" }, "require": { @@ -857,7 +1237,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.0.1" + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.0.2" }, "funding": [ { @@ -865,7 +1246,7 @@ "type": "github" } ], - "time": "2023-02-10T16:53:14+00:00" + "time": "2023-05-07T09:13:23+00:00" }, { "name": "phpunit/php-invoker", @@ -1050,16 +1431,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.1.2", + "version": "10.2.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "6f0cd95be71add539f8fd2be25b2a4a29789000b" + "reference": "35c8cac1734ede2ae354a6644f7088356ff5b08e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6f0cd95be71add539f8fd2be25b2a4a29789000b", - "reference": "6f0cd95be71add539f8fd2be25b2a4a29789000b", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/35c8cac1734ede2ae354a6644f7088356ff5b08e", + "reference": "35c8cac1734ede2ae354a6644f7088356ff5b08e", "shasum": "" }, "require": { @@ -1099,7 +1480,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "10.1-dev" + "dev-main": "10.2-dev" } }, "autoload": { @@ -1131,7 +1512,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.1.2" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.2.3" }, "funding": [ { @@ -1147,7 +1528,7 @@ "type": "tidelift" } ], - "time": "2023-04-22T07:38:19+00:00" + "time": "2023-06-30T06:17:38+00:00" }, { "name": "psr/container", @@ -2221,37 +2602,35 @@ }, { "name": "symfony/config", - "version": "v6.2.7", + "version": "v6.3.0", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "249271da6f545d6579e0663374f8249a80be2893" + "reference": "a5e00dec161b08c946a2c16eed02adbeedf827ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/249271da6f545d6579e0663374f8249a80be2893", - "reference": "249271da6f545d6579e0663374f8249a80be2893", + "url": "https://api.github.com/repos/symfony/config/zipball/a5e00dec161b08c946a2c16eed02adbeedf827ae", + "reference": "a5e00dec161b08c946a2c16eed02adbeedf827ae", "shasum": "" }, "require": { "php": ">=8.1", - "symfony/deprecation-contracts": "^2.1|^3", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/filesystem": "^5.4|^6.0", "symfony/polyfill-ctype": "~1.8" }, "conflict": { - "symfony/finder": "<5.4" + "symfony/finder": "<5.4", + "symfony/service-contracts": "<2.5" }, "require-dev": { "symfony/event-dispatcher": "^5.4|^6.0", "symfony/finder": "^5.4|^6.0", "symfony/messenger": "^5.4|^6.0", - "symfony/service-contracts": "^1.1|^2|^3", + "symfony/service-contracts": "^2.5|^3", "symfony/yaml": "^5.4|^6.0" }, - "suggest": { - "symfony/yaml": "To use the yaml reference dumper" - }, "type": "library", "autoload": { "psr-4": { @@ -2278,7 +2657,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v6.2.7" + "source": "https://github.com/symfony/config/tree/v6.3.0" }, "funding": [ { @@ -2294,34 +2673,34 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:44:56+00:00" + "time": "2023-04-25T10:46:17+00:00" }, { "name": "symfony/dependency-injection", - "version": "v6.2.10", + "version": "v6.3.1", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "d732a66a2672669232c0b4536c8c96724a679780" + "reference": "7abf242af21f196b65f20ab00ff251fdf3889b8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/d732a66a2672669232c0b4536c8c96724a679780", - "reference": "d732a66a2672669232c0b4536c8c96724a679780", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/7abf242af21f196b65f20ab00ff251fdf3889b8d", + "reference": "7abf242af21f196b65f20ab00ff251fdf3889b8d", "shasum": "" }, "require": { "php": ">=8.1", "psr/container": "^1.1|^2.0", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/service-contracts": "^1.1.6|^2.0|^3.0", - "symfony/var-exporter": "^6.2.7" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.2.10" }, "conflict": { "ext-psr": "<1.1|>=2", "symfony/config": "<6.1", "symfony/finder": "<5.4", - "symfony/proxy-manager-bridge": "<6.2", + "symfony/proxy-manager-bridge": "<6.3", "symfony/yaml": "<5.4" }, "provide": { @@ -2333,12 +2712,6 @@ "symfony/expression-language": "^5.4|^6.0", "symfony/yaml": "^5.4|^6.0" }, - "suggest": { - "symfony/config": "", - "symfony/expression-language": "For using expressions in service container configuration", - "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required", - "symfony/yaml": "" - }, "type": "library", "autoload": { "psr-4": { @@ -2365,7 +2738,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.2.10" + "source": "https://github.com/symfony/dependency-injection/tree/v6.3.1" }, "funding": [ { @@ -2381,20 +2754,20 @@ "type": "tidelift" } ], - "time": "2023-04-21T15:42:15+00:00" + "time": "2023-06-24T11:51:27+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.2.1", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e" + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", - "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", "shasum": "" }, "require": { @@ -2403,7 +2776,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.3-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -2432,7 +2805,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.2.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0" }, "funding": [ { @@ -2448,20 +2821,20 @@ "type": "tidelift" } ], - "time": "2023-03-01T10:25:55+00:00" + "time": "2023-05-23T14:45:45+00:00" }, { "name": "symfony/filesystem", - "version": "v6.2.10", + "version": "v6.3.1", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "fd588debf7d1bc16a2c84b4b3b71145d9946b894" + "reference": "edd36776956f2a6fcf577edb5b05eb0e3bdc52ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/fd588debf7d1bc16a2c84b4b3b71145d9946b894", - "reference": "fd588debf7d1bc16a2c84b4b3b71145d9946b894", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/edd36776956f2a6fcf577edb5b05eb0e3bdc52ae", + "reference": "edd36776956f2a6fcf577edb5b05eb0e3bdc52ae", "shasum": "" }, "require": { @@ -2495,7 +2868,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.2.10" + "source": "https://github.com/symfony/filesystem/tree/v6.3.1" }, "funding": [ { @@ -2511,7 +2884,7 @@ "type": "tidelift" } ], - "time": "2023-04-18T13:46:08+00:00" + "time": "2023-06-01T08:30:39+00:00" }, { "name": "symfony/polyfill-ctype", @@ -2680,16 +3053,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.2.1", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "a8c9cedf55f314f3a186041d19537303766df09a" + "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/a8c9cedf55f314f3a186041d19537303766df09a", - "reference": "a8c9cedf55f314f3a186041d19537303766df09a", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", + "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", "shasum": "" }, "require": { @@ -2699,13 +3072,10 @@ "conflict": { "ext-psr": "<1.1|>=2" }, - "suggest": { - "symfony/service-implementation": "" - }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.3-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -2745,7 +3115,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.2.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.3.0" }, "funding": [ { @@ -2761,20 +3131,20 @@ "type": "tidelift" } ], - "time": "2023-03-01T10:32:47+00:00" + "time": "2023-05-23T14:45:45+00:00" }, { "name": "symfony/var-exporter", - "version": "v6.2.10", + "version": "v6.3.0", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "9a07920c2058bafee921ce4d90aeef2193837d63" + "reference": "db5416d04269f2827d8c54331ba4cfa42620d350" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/9a07920c2058bafee921ce4d90aeef2193837d63", - "reference": "9a07920c2058bafee921ce4d90aeef2193837d63", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/db5416d04269f2827d8c54331ba4cfa42620d350", + "reference": "db5416d04269f2827d8c54331ba4cfa42620d350", "shasum": "" }, "require": { @@ -2819,7 +3189,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v6.2.10" + "source": "https://github.com/symfony/var-exporter/tree/v6.3.0" }, "funding": [ { @@ -2835,7 +3205,7 @@ "type": "tidelift" } ], - "time": "2023-04-21T08:33:05+00:00" + "time": "2023-04-21T08:48:44+00:00" }, { "name": "theseer/tokenizer", @@ -2894,7 +3264,9 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=8.1" + "php": ">=8.1", + "ext-dom": "*", + "ext-curl": "*" }, "platform-dev": [], "plugin-api-version": "2.3.0" diff --git a/phpcs.xml b/phpcs.xml index b771656..1fa70a0 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -19,7 +19,6 @@ - @@ -30,7 +29,6 @@ - diff --git a/phpmd.xml b/phpmd.xml index aa59a1f..01cda56 100644 --- a/phpmd.xml +++ b/phpmd.xml @@ -7,6 +7,7 @@ Custom ruleset + @@ -16,10 +17,12 @@ + + @@ -31,6 +34,12 @@ + + + + + + diff --git a/phpunit.xml b/phpunit.xml index 9c01cb1..dea441b 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -6,6 +6,7 @@ cacheDirectory="test/phpunit/.phpunit.cache" bootstrap="vendor/autoload.php" displayDetailsOnTestsThatTriggerWarnings="true" + > diff --git a/src/ArrayBuffer.php b/src/ArrayBuffer.php new file mode 100644 index 0000000..8cceb71 --- /dev/null +++ b/src/ArrayBuffer.php @@ -0,0 +1,49 @@ + + */ +class ArrayBuffer extends SplFixedArray implements Stringable { + public function __toString():string { + return implode("", iterator_to_array($this)); + } + + public function __get(string $name):mixed { + switch($name) { + case "byteLength": + return count($this); + } + + throw new RuntimeException("Undefined property: $name"); + } + + /** + * @SuppressWarnings("UnusedFormalParameter") + * @noinspection PhpUnusedParameterInspection + */ + // phpcs:ignore + public function transfer( + self $oldBuffer, + int $newByteLength = null + ):self { + return $this; + } + + /** + * @SuppressWarnings("UnusedFormalParameter") + * @noinspection PhpUnusedParameterInspection + */ + // phpcs:ignore + public function slice( + int $begin, + int $end, + ):self { + return $this; + } +} diff --git a/src/Blob.php b/src/Blob.php new file mode 100644 index 0000000..839246a --- /dev/null +++ b/src/Blob.php @@ -0,0 +1,65 @@ + $blobParts + * @param array $options + */ + public function __construct( + iterable $blobParts, + array $options = [], + ) { + $this->name = "blob"; + $this->type = $options["type"] ?? ""; + $this->content = $this->loadIterable($blobParts); + } + + public function __toString():string { + return $this->getContent(); + } + + public function __get(string $name):mixed { + switch($name) { + case "size": + return $this->size; + + case "type": + return $this->type; + } + + throw new RuntimeException("Undefined property: $name"); + } + + public function getContent():string { + return $this->content; + } + + /** @param iterable $input */ + protected function loadIterable(iterable $input):string { + $buffer = ""; + + foreach($input as $i) { + $i = str_replace( + ["\n", "\r\n"], + PHP_EOL, + $i + ); + + $buffer .= $i; + } + + return $buffer; + } +} diff --git a/src/File.php b/src/File.php new file mode 100644 index 0000000..9e85beb --- /dev/null +++ b/src/File.php @@ -0,0 +1,18 @@ + $bits + * @param string $name + * @param array $options + */ + public function __construct( + array $bits, + string $name, + array $options = [], + ) { + parent::__construct($bits, $options); + $this->name = $name; + } +} diff --git a/src/FormData.php b/src/FormData.php new file mode 100644 index 0000000..d2c8e60 --- /dev/null +++ b/src/FormData.php @@ -0,0 +1,186 @@ + entries() + * @method array getAll(string $name) + * @method array values() + * @implements Iterator + * @see https://developer.mozilla.org/en-US/docs/Web/API/FormData + */ +class FormData extends KeyValuePairStore implements Stringable, Countable, Iterator { + use NullableTypeSafeGetter; + + const USER_INPUT_ELEMENTS = [ + "input", + "textarea", + "select", + ]; + + /** + * @param ?DOMElement $form An HTML
element — when specified, + * the FormData object will be populated with the form's current + * keys/values using the name property of each element for the keys and + * their submitted value for the values. It will also encode file + * input content. + * @param ?DOMElement $submitter A submit button that is a member + * of the form. If the submitter has a name attribute or is an + * , its data will be included in the FormData + * object (e.g. btnName=btnValue). + */ + public function __construct( + private readonly ?DOMElement $form = null, + ?DOMElement $submitter = null, + ) { + if($form) { + $this->kvp = $this->extractKvpFromForm($form); + } + else { + $this->kvp = []; + } + + if($submitter) { + $foundForm = false; + while($parent = $submitter->parentNode) { + if($parent === $this->form) { + $foundForm = true; + break; + } + } + + if(!$foundForm) { + throw new HttpException( + "Submitter is not part of the form" + ); + } + + $this->append( + $submitter->getAttribute("name"), + $submitter->getAttribute("value"), + ); + } + + $this->rewind(); + } + + public function getFile(string $name):?File { + return $this->getInstance($name, File::class); + } + + public function getBlob(string $name):?Blob { + return $this->getInstance($name, Blob::class); + } + + /** + * The append() method of the FormData interface appends a new value + * onto an existing key inside a FormData object, or adds the key if + * it does not already exist. + * + * The difference between set() and append() is that if the specified + * key already exists, set() will overwrite all existing values with + * the new one, whereas append() will append the new value onto the + * end of the existing set of values. + * + * @param string $name The name of the field whose data is contained + * in value. + * @param string $value The field's value. This can be a string or + * Blob (including subclasses such as File). If none of these are + * specified the value is converted to a string. + * @param ?string $filename The filename reported to the server , when + * a Blob or File is passed as the second parameter. + * The default filename for Blob objects is "blob". The default + * filename for File objects is the file's filename. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/FormData/append + */ + public function append( + string $name, + Blob|File|string $value, + string $filename = null + ):void { + $this->appendAnyValue($name, $value, $filename); + } + + public function set( + string $name, + Blob|File|string $value, + ?string $filename = null, + ):void { + $this->setAnyValue($name, $value, $filename); + } + + /** + * @return array> + * @SuppressWarnings("CyclomaticComplexity") + */ + private function extractKvpFromForm(DOMElement $form):array { + $kvp = []; + + $xpath = new DOMXPath($form->ownerDocument); + $nameElementList = $xpath->query(".//*[@name]", $form); + if(!$nameElementList || $nameElementList->length === 0) { + return $kvp; + } + + /** @var DOMNodeList $nameElementList */ + for($i = 0, $len = $nameElementList->length; $i < $len; $i++) { + /** @var DOMElement $item */ + $item = $nameElementList->item($i); + if(!in_array($item->tagName, self::USER_INPUT_ELEMENTS)) { + continue; + } + $key = $item->getAttribute("name"); + if(str_ends_with($key, "[]")) { + $key = substr($key, 0, -2); + } + $value = ""; + + if($item->tagName === "textarea") { + $value = $item->nodeValue ?? ""; + } + elseif($item->tagName === "select") { + $value = $this->getValueFromSelect($item); + } + else { + $value = $item->getAttribute("value"); + } + + if(isset($kvp[$key])) { + if(is_array($kvp[$key])) { + array_push($kvp[$key], $value); + } + else { + $kvp[$key] = [$kvp[$key], $value]; + } + } + else { + $kvp[$key] = $value; + } + } + + return $kvp; + } + + private function getValueFromSelect(DOMElement $select):string { + $optionList = $select->getElementsByTagName("option"); + for($j = 0, $optLen = $optionList->length; $j < $optLen; $j++) { + /** @var DOMElement $option */ + $option = $optionList->item($j); + if($option->hasAttribute("selected")) { + return $option->getAttribute("value") + ?: $option->nodeValue; + } + } + + return ""; + } +} diff --git a/src/IntegrityMismatchException.php b/src/IntegrityMismatchException.php new file mode 100644 index 0000000..7c5c2f8 --- /dev/null +++ b/src/IntegrityMismatchException.php @@ -0,0 +1,4 @@ +> */ + protected array $kvp; + protected int $iteratorIndex; + /** @var array> */ + protected array $iteratorCache; + + /** + * @return string A string, without the question mark. Returns an + * empty string if no search parameters have been set. + */ + public function __toString():string { + if(!isset($this->kvp)) { + return ""; + } + + $string = http_build_query($this->kvp); + return preg_replace( + "/%5B(\d)%5D/", + "[]", + $string + ); + } + + public function count():int { + return count($this->kvp); + } + + public function rewind():void { + $this->iteratorIndex = 0; + $this->iteratorCache = $this->cacheIterator(); + } + + public function valid():bool { + return isset($this->iteratorCache[$this->iteratorIndex]); + } + + public function current():string|Blob { + return $this->iteratorCache[$this->iteratorIndex][1]; + } + + public function key():string { + /** @var string $key */ + $key = $this->iteratorCache[$this->iteratorIndex][0]; + return $key; + } + + public function next():void { + $this->iteratorIndex++; + } + + /** + * The delete() method of the URLSearchParams interface deletes the + * given search parameter and all its associated values, from the list + * of all search parameters. + * + * @param string $name The name of the parameter to be deleted. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/delete + */ + public function delete(string $name):void { + $name = rtrim($name, "[]"); + if(!isset($this->kvp[$name])) { + return; + } + + unset($this->kvp[$name]); + } + + /** + * The entries() method of the URLSearchParams interface returns an + * iterator allowing iteration through all key/value pairs contained + * in this object. The iterator returns key/value pairs in the same + * order as they appear in the query string. The key and value of each + * pair are string objects. + * + * @return Generator + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/entries + */ + public function entries():Generator { + if(!isset($this->kvp)) { + return; + } + foreach($this->kvp as $key => $value) { + if(is_array($value)) { + foreach($value as $subValue) { + yield "{$key}[]" => $subValue; + } + } + else { + yield $key => $value; + } + } + } + + /** + * The forEach() method of the URLSearchParams interface allows + * iteration through all values contained in this object via a + * callback function. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/forEach + */ + public function forEach(callable $callback):void { + foreach($this->kvp as $key => $value) { + if(is_array($value)) { + foreach($value as $subValue) { + call_user_func( + $callback, + "{$key}[]", + $subValue + ); + } + } + else { + call_user_func($callback, $key, $value); + } + } + } + + /** + * The get() method of the URLSearchParams interface returns the first + * value associated to the given search parameter. + * + * Note: This class implements type-safe getters, getInt, getBool, etc. + * + * @param string $name The name of the parameter to return. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/get + */ + public function get(string $name):mixed { + $name = rtrim($name, "[]"); + $value = $this->kvp[$name] ?? null; + if(!$value) { + return null; + } + + if(is_array($value)) { + return $value[0]; + } + + return $value; + } + + /** + * The getAll() method of the URLSearchParams interface returns all + * the values associated with a given search parameter as an array. + * + * @param string $name The name of the parameter to return. + * @return array + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/getAll + */ + public function getAll(string $name):array { + $name = rtrim($name, "[]"); + $value = $this->kvp[$name] ?? []; + if(!is_array($value)) { + $value = [$value]; + } + + return $value; + } + + /** + * The has() method of the URLSearchParams interface returns a boolean + * value that indicates whether a parameter with the specified name + * exists. + * + * @param string $name The name of the parameter to find. + * @return bool + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/has + */ + public function has(string $name):bool { + $name = rtrim($name, "[]"); + return isset($this->kvp[$name]); + } + + /** + * The keys() method of the URLSearchParams interface returns an + * iterator allowing iteration through all keys contained in this + * object. The keys are string objects. + * + * @return array + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/keys + */ + public function keys():array { + $keys = []; + foreach($this->kvp as $key => $value) { + if(is_array($value)) { + array_push($keys, "{$key}[]"); + } + else { + array_push($keys, $key); + } + } + + return $keys; + } + + /** + * The URLSearchParams.sort() method sorts all key/value pairs + * contained in this object in place and returns undefined. The sort + * order is according to unicode code points of the keys. This method + * uses a stable sorting algorithm (i.e. the relative order between + * key/value pairs with equal keys will be preserved). + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/sort + */ + public function sort():void { + ksort($this->kvp); + } + + /** + * The values() method of the URLsearchParams interface returns an + * iterator allowing iteration through all values contained in this + * object. The values are string objects. + * + * @return array + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/values + */ + public function values():array { + $values = []; + foreach($this->kvp as $value) { + if(is_array($value)) { + foreach($value as $subValue) { + array_push($values, $subValue); + } + } + else { + array_push($values, $value); + } + } + return $values; + } + + protected function appendAnyValue( + string $name, + mixed $value, + string $filename = null + ):void { + if(is_array($this->kvp[$name] ?? null)) { + array_push($this->kvp[$name], $value); + } + else { + if(isset($this->kvp[$name])) { + $this->kvp[$name] = [ + $this->kvp[$name], + $value, + ]; + } + else { + $this->setAnyValue($name, $value, $filename); + } + } + } + + protected function setAnyValue( + string $name, + mixed $value, + string $filename = null + ):void { + if(!is_null($filename)) { + if($value instanceof Blob) { + $value->name = $filename; + } + } + + $this->kvp[$name] = $value; + } + + /** @return array> */ + private function cacheIterator():array { + $cache = []; + foreach($this->entries() as $key => $value) { + array_push($cache, [$key, $value]); + } + return $cache; + } +} diff --git a/src/Response.php b/src/Response.php index 5606f85..3e09312 100644 --- a/src/Response.php +++ b/src/Response.php @@ -1,21 +1,36 @@ -stream = new Stream(); } + /** @phpstan-ignore-next-line */ + private function __prop_get_headers():ResponseHeaders { + return $this->getResponseHeaders(); + } + + /** @phpstan-ignore-next-line */ + private function __prop_get_ok():bool { + return ($this->getStatusCode() >= 200 + && $this->getStatusCode() < 300); + } + + /** @phpstan-ignore-next-line */ + private function __prop_get_redirected():bool { + if(!isset($this->curl)) { + return false; + } + + $redirectCount = $this->curl->getInfo( + CURLINFO_REDIRECT_COUNT + ); + return $redirectCount > 0; + } + + /** @phpstan-ignore-next-line */ + private function __prop_get_status():int { + return $this->getStatusCode(); + } + + /** @phpstan-ignore-next-line */ + private function __prop_get_statusText():?string { + return StatusCode::REASON_PHRASE[$this->status] ?? null; + } + + /** @phpstan-ignore-next-line */ + private function __prop_get_uri():string { + if(!isset($this->curl)) { + return $this->request->getUri(); + } + return $this->curl->getInfo(CURLINFO_EFFECTIVE_URL); + } + + /** @phpstan-ignore-next-line */ + private function __prop_get_url():string { + return $this->uri; + } + + /** @phpstan-ignore-next-line */ + private function __prop_get_type():string { + return $this->headers->get("content-type")?->getValue() ?? ""; + } + public function setExitCallback(callable $callback):void { $this->exitCallback = $callback; } @@ -75,4 +141,152 @@ public function getReasonPhrase():string { public function getResponseHeaders():ResponseHeaders { return $this->headers; } + + public function startDeferredResponse( + CurlInterface $curl + ):Deferred { + $this->deferred = new Deferred(); + $this->curl = $curl; + return $this->deferred; + } + + public function endDeferredResponse(string $integrity = null):void { + $position = $this->stream->tell(); + $this->stream->rewind(); + $contents = $this->stream->getContents(); + $this->stream->seek($position); + $this->checkIntegrity($integrity, $contents); + $this->deferred->resolve($contents); + } + + /** + * Takes the Response's stream and reads it to completion. Returns a Promise which resolves with the result + * as a Gt\Http\ArrayBuffer. + * + * Note: if no Async loop is set up, the returned Promise will resolve in a blocking way, always being + * resolved or rejected. See https://www.php.gt/fetch for a complete async implementation. + */ + public function arrayBuffer():Promise { + $promise = $this->deferred->getPromise(); + $promise->then(function(string $responseText) { + $bytes = strlen($responseText); + $arrayBuffer = new ArrayBuffer($bytes); + for($i = 0; $i < $bytes; $i++) { + $arrayBuffer->offsetSet($i, ord($responseText[$i])); + } + + $this->deferred->resolve($arrayBuffer); + }); + + return $promise; + } + + /** + * Takes the Response's stream and reads it to completion. Returns a Promise which resolves with the result + * as a Gt\Http\Blob. + * + * Note: if no Async loop is set up, the returned Promise will resolve in a blocking way, always being + * resolved or rejected. See https://www.php.gt/fetch for a complete async implementation. + */ + public function blob():Promise { + $promise = $this->deferred->getPromise(); + $promise->then(function(string $responseText) { + $blobOptions = [ + "type" => $this->getResponseHeaders()->get("content-type")->getValues()[0], + ]; + $this->deferred->resolve(new Blob([$responseText], $blobOptions)); + }); + + return $promise; + } + + /** + * Takes the Response's stream and reads it to completion. Returns a Promise which resolves with the result + * as a Gt\Http\FormData. + * + * Note: if no Async loop is set up, the returned Promise will resolve in a blocking way, always being + * resolved or rejected. See https://www.php.gt/fetch for a complete async implementation. + */ + public function formData():Promise { + $newDeferred = new Deferred(); + $newPromise = $newDeferred->getPromise(); + + $deferredPromise = $this->deferred->getPromise(); + $deferredPromise->then(function(string $resolvedValue) + use($newDeferred) { + parse_str($resolvedValue, $bodyData); + $formData = new FormData(); + foreach($bodyData as $key => $value) { + if(is_array($value)) { + $value = implode(",", $value); + } + $formData->set((string)$key, (string)$value); + } + $newDeferred->resolve($formData); + }); + + return $newPromise; + } + + /** + * Takes the Response's stream and reads it to completion. Returns a Promise which resolves with the result + * as a Gt\Json\JsonObject. + * + * Note: if no Async loop is set up, the returned Promise will resolve in a blocking way, always being + * resolved or rejected. See https://www.php.gt/fetch for a complete async implementation. + */ + public function json(int $depth = 512, int $options = 0):Promise { + $promise = $this->getPromise(); + $promise->then(function(string $responseText)use($depth, $options) { + $builder = new JsonObjectBuilder($depth, $options); + $json = $builder->fromJsonString($responseText); + $this->deferred->resolve($json); + }); + + return $promise; + } + + /** + * Takes the Response's stream and reads it to completion. Returns a Promise which resolves with the result + * as a string. + * + * Note: if no Async loop is set up, the returned Promise will resolve in a blocking way, always being + * resolved or rejected. See https://www.php.gt/fetch for a complete async implementation. + */ + public function text():Promise { + $promise = $this->deferred->getPromise(); + $promise->then(function(string $responseText) { + $this->deferred->resolve($responseText); + }); + + return $promise; + } + + private function getPromise():Promise { + if(!isset($this->deferred)) { + $this->deferred = new Deferred(); + $this->deferred->resolve($this->stream->getContents()); + } + + return $this->deferred->getPromise(); + } + + private function checkIntegrity(?string $integrity, string $contents):void { + if(is_null($integrity)) { + return; + } + + [$algo, $hash] = explode("-", $integrity); + + $availableAlgos = hash_algos(); + if(!in_array($algo, $availableAlgos)) { + throw new InvalidIntegrityAlgorithmException($algo); + } + + $hashedContents = hash($algo, $contents); + + if($hashedContents !== $hash) { + throw new IntegrityMismatchException(); + } + } } diff --git a/src/URLSearchParams.php b/src/URLSearchParams.php new file mode 100644 index 0000000..77b945f --- /dev/null +++ b/src/URLSearchParams.php @@ -0,0 +1,67 @@ + + * @see https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams + */ +class URLSearchParams extends KeyValuePairStore implements Stringable, Countable, Iterator { + use MagicProp; + use NullableTypeSafeGetter; + + /** + * @param string|array|FormData $options + * Options is one of: + * 1) A string, which will be parsed from + * application/x-www-form-urlencoded format. A leading '?' character + * is ignored. + * 2) A literal sequence of name-value string pairs, or any object — + * such as a FormData object — with an iterator that produces a + * sequence of string pairs. Note that File entries will be + * serialized as [object File] rather than as their filename (as they + * would in an application/x-www-form-urlencoded form). + * 3) A record of string keys and string values. Note that nesting is + * not supported. + */ + public function __construct( + string|array|FormData $options = "" + ) { + if(is_string($options)) { + $options = trim($options, "?"); + parse_str($options, $query); + /** @var array $query */ + $this->kvp = $query; + } + elseif($options instanceof FormData) { + foreach($options as $key => $value) { + $this->append($key, (string)$value); + } + } + else { + $this->kvp = $options; + } + + $this->rewind(); + } + + public function __prop_get_size():int { + return count($this); + } + + public function append(string $name, string $value):void { + $this->appendAnyValue($name, $value); + } + + public function set(string $name, string $value):void { + $this->setAnyValue($name, $value); + } +} diff --git a/test/phpunit/FormDataTest.php b/test/phpunit/FormDataTest.php new file mode 100644 index 0000000..2c801a3 --- /dev/null +++ b/test/phpunit/FormDataTest.php @@ -0,0 +1,634 @@ + + + + + + + + + + + HTML; + $document = new DOMDocument("1.0", "utf-8"); + $document->loadHTML($html); + $form = $document->getElementsByTagName("form")[0]; + $button = $document->getElementsByTagName("button")[0]; + $sut = new FormData($form, $button); + self::assertSame("your-name=Cody&email=cody%40g105b.com&do=submit", (string)$sut); + } + + public function testConstruct_noFormFields():void { + $html = << + + +
+ + + +
+ + + HTML; + $document = new DOMDocument("1.0", "utf-8"); + $document->loadHTML($html); + $form = $document->getElementsByTagName("form")[0]; + $sut = new FormData($form); + $key = null; + foreach($sut as $key => $value) { +// this line should never execute: + self::assertNull($key); + } + + self::assertNull($key); + } + + public function testConstruct_textArea():void { + $html = << + + +
+ + + +
+ + + HTML; + $document = new DOMDocument("1.0", "utf-8"); + $document->loadHTML($html); + $form = $document->getElementsByTagName("form")[0]; + $sut = new FormData($form); + self::assertSame("Hello, PHP.Gt!", $sut->getString("message")); + } + + public function testConstruct_multiple():void { + $html = << + + +
+ + + +
+ + + HTML; + $document = new DOMDocument("1.0", "utf-8"); + $document->loadHTML($html); + $form = $document->getElementsByTagName("form")[0]; + $sut = new FormData($form); + self::assertSame("221B Baker Street, London", $sut->getString("address")); +// Should always return the first of the same value. + self::assertSame("221B Baker Street, London", $sut->getString("address")); + $allAddresses = $sut->getAll("address"); + self::assertCount(3, $allAddresses); + self::assertSame("30 Wellington Square in Chelsea, London", $allAddresses[2]); + } + + public function testConstruct_multipleSquareBrackets():void { + $html = << + + +
+ + + +
+ + + HTML; + $document = new DOMDocument("1.0", "utf-8"); + $document->loadHTML($html); + $form = $document->getElementsByTagName("form")[0]; + $sut = new FormData($form); + self::assertSame("221B Baker Street, London", $sut->getString("address")); +// Should always return the first of the same value. + self::assertSame("221B Baker Street, London", $sut->getString("address")); + $allAddresses = $sut->getAll("address"); + self::assertCount(3, $allAddresses); + self::assertSame("30 Wellington Square in Chelsea, London", $allAddresses[2]); + } + + public function testToString_empty():void { + $sut = new FormData(); + self::assertSame("", (string)$sut); + } + + public function testToString_form():void { + $kvp = [ + "key1" => "value1", + "key2" => "value2", + ]; + + $form = self::mockForm($kvp); + $sut = new FormData($form); + + self::assertSame("key1=value1&key2=value2", (string)$sut); + } + + public function testToString_formMultipleValues():void { + $kvp = [ + "name" => "Cody", + "food" => ["mushrooms", "biscuits"], + ]; + + $form = self::mockForm($kvp); + $sut = new FormData($form); + + self::assertSame("name=Cody&food[]=mushrooms&food[]=biscuits", (string)$sut); + } + + public function testToString_buttonsAreNotIncludedByDefault():void { + $html = << + + +
+ + + +
+ + + HTML; + $form = self::mockForm($html); + $sut = new FormData($form); + self::assertSame("your-name=Cody&email=cody%40g105b.com", (string)$sut); + } + + public function testToString_HtmlWithSelectButNoMatchingOptions():void { + $html = << + + +
+ + + + +
+ + + HTML; + $form = self::mockForm($html); + $sut = new FormData($form); + self::assertSame("your-name=Cody&email=cody%40g105b.com&colour=", (string)$sut); + } + + public function testToString_HtmlWithSelect():void { + $html = << + + +
+ + + + +
+ + + HTML; + $form = self::mockForm($html); + $sut = new FormData($form); + self::assertSame("your-name=Cody&email=cody%40g105b.com&colour=Orange", (string)$sut); + } + + public function testAppend():void { + $sut = new FormData(); + $sut->append("name", "Cody"); + self::assertSame("name=Cody", (string)$sut); + } + + public function testAppend_double():void { + $sut = new FormData(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + self::assertSame("name=Cody&food[]=mushroom&food[]=pepper", (string)$sut); + } + + public function testAppend_triple():void { + $sut = new FormData(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + $sut->append("food", "corn"); + self::assertSame("name=Cody&food[]=mushroom&food[]=pepper&food[]=corn", (string)$sut); + } + + public function testDelete_notExists():void { + $sut = new FormData(); + $sut->delete("nothing"); + self::assertSame("", (string)$sut); + } + public function testDelete_single():void { + $sut = new FormData(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + $sut->delete("name"); + self::assertSame("food[]=mushroom&food[]=pepper", (string)$sut); + } + + public function testDelete_multiple():void { + $sut = new FormData(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + $sut->delete("food"); + self::assertSame("name=Cody", (string)$sut); + } + + public function testDelete_multipleWithSquareBrackets():void { + $sut = new FormData(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + $sut->delete("food[]"); + self::assertSame("name=Cody", (string)$sut); + } + + public function testEntries_empty():void { + $sut = new FormData(); + self::assertSame([], iterator_to_array($sut->entries())); + } + + public function testEntries():void { + $sut = new FormData(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + + $i = 0; + foreach($sut->entries() as $key => $value) { + if($i === 0) { + self::assertSame("name", $key); + self::assertSame("Cody", $value); + } + if($i === 1) { + self::assertSame("food[]", $key); + self::assertSame("mushroom", $value); + } + if($i === 2) { + self::assertSame("food[]", $key); + self::assertSame("pepper", $value); + } + $i++; + } + } + + public function testForEach_empty():void { + $sut = new FormData(); + $calls = []; + $sut->forEach(function()use(&$calls) { + array_push($calls, func_get_args()); + }); + self::assertEmpty($calls); + } + + public function testForEach():void { + $sut = new FormData(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + + $calls = []; + $sut->forEach(function()use(&$calls) { + array_push($calls, func_get_args()); + }); + self::assertCount(3, $calls); + self::assertSame("name", $calls[0][0]); + self::assertSame("Cody", $calls[0][1]); + + self::assertSame("food[]", $calls[1][0]); + self::assertSame("mushroom", $calls[1][1]); + self::assertSame("food[]", $calls[2][0]); + self::assertSame("pepper", $calls[2][1]); + } + + public function testGet_empty():void { + $sut = new FormData(); + self::assertNull($sut->get("something")); + } + + public function testGet():void { + $form = self::mockForm(["name" => "Cody"]); + $sut = new FormData($form); + self::assertSame("Cody", $sut->get("name")); + } + + public function testGet_multiple():void { + $sut = new FormData(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + + self::assertSame("mushroom", $sut->get("food")); + } + + public function testGet_multipleWithSquareBrackets():void { + $sut = new FormData(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + + self::assertSame("mushroom", $sut->get("food[]")); + } + + public function testGetAll_empty():void { + $sut = new FormData(); + self::assertEmpty($sut->getAll("something")); + } + + public function testGetAll_single():void { + $form = self::mockForm(["name" => "Cody"]); + $sut = new FormData($form); + self::assertSame(["Cody"], $sut->getAll("name")); + } + + public function testGetAll():void { + $sut = new FormData(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + self::assertSame(["mushroom", "pepper"], $sut->getAll("food")); + } + + public function testGetAll_withSquareBrackets():void { + $sut = new FormData(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + self::assertSame(["mushroom", "pepper"], $sut->getAll("food[]")); + } + + public function testHas():void { + $sut = new FormData(); + self::assertFalse($sut->has("name")); + $sut->append("name", "Cody"); + self::assertTrue($sut->has("name")); + } + + public function testHas_withSquareBrackets():void { + $sut = new FormData(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + self::assertTrue($sut->has("food[]")); + } + + public function testKeys_empty():void { + $sut = new FormData(); + self::assertEmpty($sut->keys()); + } + + public function testKeys():void { + $form = self::mockForm([ + "name" => "Cody", + "colour" => "orange", + ]); + $sut = new FormData($form); + self::assertSame(["name", "colour"], $sut->keys()); + } + + public function testKeys_multiple():void { + $sut = new FormData(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + self::assertSame(["name", "food[]"], $sut->keys()); + } + + public function testSet():void { + $sut = new FormData(); + $sut->set("name", "Cody"); + self::assertSame("name=Cody", (string)$sut); + } + + public function testSet_overwrite():void { + $sut = new FormData(); + $sut->set("name", "Cody"); + $sut->set("name", "Scarlett"); + self::assertSame("name=Scarlett", (string)$sut); + } + + public function testSet_fileFilename():void { + $sut = new FormData(); + $sut->set("upload", new File([], "picture.jpg")); + self::assertSame("picture.jpg", $sut->getFile("upload")->name); + $sut->set("upload", new File([], "picture.jpg"), "updated.jpg"); + self::assertSame("updated.jpg", $sut->getFile("upload")->name); + } + + public function testSet_blobFilename():void { + $sut = new FormData(); + $sut->set("upload", new Blob([])); + self::assertSame("blob", $sut->getBlob("upload")->name); + $sut->set("upload", new Blob([]), "updated.jpg"); + self::assertSame("updated.jpg", $sut->getBlob("upload")->name); + } + + public function testSort():void { + $sut = new FormData(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + $sut->sort(); + self::assertSame("food[]=mushroom&food[]=pepper&name=Cody", (string)$sut); + } + + public function testValues_empty():void { + $sut = new FormData(); + self::assertEmpty($sut->values()); + } + + public function testValues():void { + $form = self::mockForm([ + "name" => "Cody", + "colour" => "orange", + ]); + $sut = new FormData($form); + self::assertSame(["Cody", "orange"], $sut->values()); + } + + public function testValues_multiple():void { + $form = self::mockForm([ + "name" => "Cody", + "food" => ["mushroom", "pepper"], + ]); + $sut = new FormData($form); + self::assertSame(["Cody", "mushroom", "pepper"], $sut->values()); + } + + public function testIterator_empty():void { + $sut = new FormData(); + $key = $value = null; + foreach($sut as $key => $value) { + self::assertNull($key, "Iterator should not run"); + } + self::assertNull($key); + self::assertNull($value); + } + + public function testIterator():void { + $form = self::mockForm([ + "name" => "Cody", + "colour" => "orange", + ]); + $sut = new FormData($form); + $iterations = []; + foreach($sut as $key => $value) { + array_push($iterations, [$key, $value]); + } + + self::assertCount(2, $iterations); + self::assertSame(["name", "Cody"], $iterations[0]); + self::assertSame(["colour", "orange"], $iterations[1]); + } + + public function testIterator_multipleKeys():void { + $form = self::mockForm([ + "name" => "Cody", + "food" => ["mushrooms", "biscuits"] + ]); + $sut = new FormData($form); + $iterations = []; + foreach($sut as $key => $value) { + array_push($iterations, [$key, $value]); + } + + self::assertCount(3, $iterations); + self::assertSame(["name", "Cody"], $iterations[0]); + self::assertSame(["food[]", "mushrooms"], $iterations[1]); + self::assertSame(["food[]", "biscuits"], $iterations[2]); + } + + private static function mockForm(array|string $input):DOMElement { + $document = new DOMDocument("1.0", "utf-8"); + if(is_array($input)) { + $kvp = $input; + $document->loadHTML(""); + $body = $document->getElementsByTagName("body")[0]; + $form = $document->createElement("form"); + + foreach($kvp as $key => $value) { + if(is_array($value)) { + foreach($value as $subValue) { + $input = $document->createElement("input"); + $input->setAttribute("name", $key); + $input->setAttribute("value", $subValue); + $form->append($input); + } + } + else { + $input = $document->createElement("input"); + $input->setAttribute("name", $key); + $input->setAttribute("value", $value); + $form->append($input); + } + } + + $body->append($form); + } + else { + $document->loadHTML($input); + $form = $document->getElementsByTagName("form")[0]; + } + + return $form; + } +} diff --git a/test/phpunit/ResponseTest.php b/test/phpunit/ResponseTest.php index 067b50c..5e8654d 100644 --- a/test/phpunit/ResponseTest.php +++ b/test/phpunit/ResponseTest.php @@ -5,7 +5,9 @@ use Gt\Http\Request; use Gt\Http\Response; use Gt\Http\StatusCode; +use Gt\Http\Stream; use Gt\Http\Uri; +use Gt\Json\JsonObject; use PHPUnit\Framework\TestCase; class ResponseTest extends TestCase { @@ -99,4 +101,21 @@ public function testReloadWithoutQuery() { self::assertSame(StatusCode::SEE_OTHER, $sut->getStatusCode()); self::assertSame($expectedRelativePath, $sut->getHeaderLine("Location")); } + + public function testJson():void { + $jsonString = json_encode(["name" => "phpgt"]); + + $actualJson = null; + + $stream = new Stream(); + $stream->write($jsonString); + + $sut = (new Response())->withBody($stream); + + $sut->json()->then(function(JsonObject $json) use(&$actualJson) { + $actualJson = $json; + }); + + self::assertSame("phpgt", $actualJson->getString("name")); + } } diff --git a/test/phpunit/SteamTest.php b/test/phpunit/SteamTest.php index 4c5df42..6cb1036 100644 --- a/test/phpunit/SteamTest.php +++ b/test/phpunit/SteamTest.php @@ -1,10 +1,12 @@ "value1", + "testKey2" => "value2", + "testKey3" => "value3", + ]; + $sut = new URLSearchParams($kvp); + $expected = http_build_query($kvp); + self::assertSame($expected, (string)$sut); + } + + public function testToString_formDataOptionsEmpty():void { + $formData = self::createMock(FormData::class); + $sut = new URLSearchParams($formData); + self::assertSame("", (string)$sut); + } + + public function testToString_formDataOptions():void { + $formData = self::createMock(FormData::class); + $formData->method("valid") + ->willReturnOnConsecutiveCalls(true, true, true, false); + $formData->method("key") + ->willReturnOnConsecutiveCalls("testKey1", "testKey2", "testKey3"); + $formData->method("current") + ->willReturnOnConsecutiveCalls("value1", "value2", "value3"); + $sut = new URLSearchParams($formData); + self::assertSame("testKey1=value1&testKey2=value2&testKey3=value3", (string)$sut); + } + + public function testSize_empty():void { + $sut = new URLSearchParams(); + self::assertSame(0, $sut->size); + } + + public function testSize():void { + $sut = new URLSearchParams(["one" => 123, "two" => 234]); + self::assertSame(2, $sut->size); + } + + public function testAppend():void { + $sut = new URLSearchParams(); + $sut->append("name", "Cody"); + self::assertSame("name=Cody", (string)$sut); + } + + public function testAppend_double():void { + $sut = new URLSearchParams(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + self::assertSame("name=Cody&food[]=mushroom&food[]=pepper", (string)$sut); + } + + public function testAppend_triple():void { + $sut = new URLSearchParams(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + $sut->append("food", "corn"); + self::assertSame("name=Cody&food[]=mushroom&food[]=pepper&food[]=corn", (string)$sut); + } + + public function testDelete_notExists():void { + $sut = new URLSearchParams(); + $sut->delete("nothing"); + self::assertSame("", (string)$sut); + } + public function testDelete_single():void { + $sut = new URLSearchParams(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + $sut->delete("name"); + self::assertSame("food[]=mushroom&food[]=pepper", (string)$sut); + } + + public function testDelete_multiple():void { + $sut = new URLSearchParams(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + $sut->delete("food"); + self::assertSame("name=Cody", (string)$sut); + } + + public function testDelete_multipleWithSquareBrackets():void { + $sut = new URLSearchParams(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + $sut->delete("food[]"); + self::assertSame("name=Cody", (string)$sut); + } + + public function testEntries_empty():void { + $sut = new URLSearchParams(); + self::assertSame([], iterator_to_array($sut->entries())); + } + + public function testEntries():void { + $sut = new URLSearchParams(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + + $i = 0; + foreach($sut->entries() as $key => $value) { + if($i === 0) { + self::assertSame("name", $key); + self::assertSame("Cody", $value); + } + if($i === 1) { + self::assertSame("food[]", $key); + self::assertSame("mushroom", $value); + } + if($i === 2) { + self::assertSame("food[]", $key); + self::assertSame("pepper", $value); + } + $i++; + } + } + + public function testForEach_empty():void { + $sut = new URLSearchParams(); + $calls = []; + $sut->forEach(function()use(&$calls) { + array_push($calls, func_get_args()); + }); + self::assertEmpty($calls); + } + + public function testForEach():void { + $sut = new URLSearchParams(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + + $calls = []; + $sut->forEach(function()use(&$calls) { + array_push($calls, func_get_args()); + }); + self::assertCount(3, $calls); + self::assertSame("name", $calls[0][0]); + self::assertSame("Cody", $calls[0][1]); + + self::assertSame("food[]", $calls[1][0]); + self::assertSame("mushroom", $calls[1][1]); + self::assertSame("food[]", $calls[2][0]); + self::assertSame("pepper", $calls[2][1]); + } + + public function testGet_empty():void { + $sut = new URLSearchParams(); + self::assertNull($sut->get("something")); + } + + public function testGet():void { + $sut = new URLSearchParams("name=Cody"); + self::assertSame("Cody", $sut->get("name")); + } + + public function testGet_multiple():void { + $sut = new URLSearchParams(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + + self::assertSame("mushroom", $sut->get("food")); + } + + public function testGet_multipleWithSquareBrackets():void { + $sut = new URLSearchParams(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + + self::assertSame("mushroom", $sut->get("food[]")); + } + + public function testGetAll_empty():void { + $sut = new URLSearchParams(); + self::assertEmpty($sut->getAll("something")); + } + + public function testGetAll_single():void { + $sut = new URLSearchParams("name=Cody"); + self::assertSame(["Cody"], $sut->getAll("name")); + } + + public function testGetAll():void { + $sut = new URLSearchParams(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + self::assertSame(["mushroom", "pepper"], $sut->getAll("food")); + } + + public function testGetAll_withSquareBrackets():void { + $sut = new URLSearchParams(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + self::assertSame(["mushroom", "pepper"], $sut->getAll("food[]")); + } + + public function testHas():void { + $sut = new URLSearchParams(); + self::assertFalse($sut->has("name")); + $sut->append("name", "Cody"); + self::assertTrue($sut->has("name")); + } + + public function testHas_withSquareBrackets():void { + $sut = new URLSearchParams(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + self::assertTrue($sut->has("food[]")); + } + + public function testKeys_empty():void { + $sut = new URLSearchParams(); + self::assertEmpty($sut->keys()); + } + + public function testKeys():void { + $sut = new URLSearchParams("name=Cody&colour=orange"); + self::assertSame(["name", "colour"], $sut->keys()); + } + + public function testKeys_multiple():void { + $sut = new URLSearchParams(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + self::assertSame(["name", "food[]"], $sut->keys()); + } + + public function testSet():void { + $sut = new URLSearchParams(); + $sut->set("name", "Cody"); + self::assertSame("name=Cody", (string)$sut); + } + + public function testSet_overwrite():void { + $sut = new URLSearchParams(); + $sut->set("name", "Cody"); + $sut->set("name", "Scarlett"); + self::assertSame("name=Scarlett", (string)$sut); + } + + public function testSort():void { + $sut = new URLSearchParams(); + $sut->append("name", "Cody"); + $sut->append("food", "mushroom"); + $sut->append("food", "pepper"); + $sut->sort(); + self::assertSame("food[]=mushroom&food[]=pepper&name=Cody", (string)$sut); + } + + public function testValues_empty():void { + $sut = new URLSearchParams(); + self::assertEmpty($sut->values()); + } + + public function testValues():void { + $sut = new URLSearchParams("name=Cody&colour=orange"); + self::assertSame(["Cody", "orange"], $sut->values()); + } + + public function testValues_multiple():void { + $sut = new URLSearchParams("name=Cody&food[]=mushroom&food[]=pepper"); + self::assertSame(["Cody", "mushroom", "pepper"], $sut->values()); + } + + public function testIterator_empty():void { + $sut = new URLSearchParams(); + $key = $value = null; + foreach($sut as $key => $value) { + self::assertNull($key, "Iterator should not run"); + } + self::assertNull($key); + self::assertNull($value); + } + + public function testIterator():void { + $sut = new URLSearchParams("name=Cody&colour=orange"); + $iterations = []; + foreach($sut as $key => $value) { + array_push($iterations, [$key, $value]); + } + + self::assertCount(2, $iterations); + self::assertSame(["name", "Cody"], $iterations[0]); + self::assertSame(["colour", "orange"], $iterations[1]); + } + + public function testIterator_multipleKeys():void { + $sut = new URLSearchParams("name=Cody&food[]=mushrooms&food[]=biscuits"); + $iterations = []; + foreach($sut as $key => $value) { + array_push($iterations, [$key, $value]); + } + + self::assertCount(3, $iterations); + self::assertSame(["name", "Cody"], $iterations[0]); + self::assertSame(["food[]", "mushrooms"], $iterations[1]); + self::assertSame(["food[]", "biscuits"], $iterations[2]); + } +}