From 42da8af363b3363d9771d778798717ec08f32680 Mon Sep 17 00:00:00 2001 From: Viktor Smagin Date: Sun, 13 Jul 2025 00:10:45 +0300 Subject: [PATCH] Version 1.0.0 --- .editorconfig | 22 + .gitignore | 3 + LICENSE | 19 + README.md | 171 +++ composer.json | 29 + composer.lock | 1635 ++++++++++++++++++++++ docs/OBJECTS.md | 152 ++ phpunit.xml | 27 + src/Builder.php | 8 + src/Builder/HostRelative.php | 28 + src/Builder/ReplaceAttributes.php | 36 + src/Dto/Replace.php | 41 + src/Exception/UrlNotContainsHostname.php | 11 + src/Exception/UrlNotContainsPath.php | 11 + src/Exception/UrlNotContainsPort.php | 11 + src/Exception/UrlNotContainsQuery.php | 11 + src/Exception/UrlNotContainsScheme.php | 11 + src/Exception/UrlRuntimeException.php | 13 + src/Facade.php | 73 + src/Parser.php | 200 +++ src/Port.php | 31 + src/Regex.php | 13 + src/Scheme.php | 20 + src/Serializer.php | 8 + src/Serializer/RFC3986.php | 64 + src/Url.php | 61 + src/Util.php | 54 + tests/Builder/HostRelativeTest.php | 36 + tests/Builder/ReplaceAttributesTest.php | 83 ++ tests/Dto/ReplaceTest.php | 36 + tests/FacadeTest.php | 86 ++ tests/ParserTest.php | 118 ++ tests/Serializer/RFC3986Test.php | 183 +++ tests/UrlTest.php | 55 + tests/UtilTest.php | 108 ++ 35 files changed, 3468 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 docs/OBJECTS.md create mode 100644 phpunit.xml create mode 100644 src/Builder.php create mode 100644 src/Builder/HostRelative.php create mode 100644 src/Builder/ReplaceAttributes.php create mode 100644 src/Dto/Replace.php create mode 100644 src/Exception/UrlNotContainsHostname.php create mode 100644 src/Exception/UrlNotContainsPath.php create mode 100644 src/Exception/UrlNotContainsPort.php create mode 100644 src/Exception/UrlNotContainsQuery.php create mode 100644 src/Exception/UrlNotContainsScheme.php create mode 100644 src/Exception/UrlRuntimeException.php create mode 100644 src/Facade.php create mode 100644 src/Parser.php create mode 100644 src/Port.php create mode 100644 src/Regex.php create mode 100644 src/Scheme.php create mode 100644 src/Serializer.php create mode 100644 src/Serializer/RFC3986.php create mode 100644 src/Url.php create mode 100644 src/Util.php create mode 100644 tests/Builder/HostRelativeTest.php create mode 100644 tests/Builder/ReplaceAttributesTest.php create mode 100644 tests/Dto/ReplaceTest.php create mode 100644 tests/FacadeTest.php create mode 100644 tests/ParserTest.php create mode 100644 tests/Serializer/RFC3986Test.php create mode 100644 tests/UrlTest.php create mode 100644 tests/UtilTest.php diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e994499 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false + +[*.php] +indent_style = space +indent_size = 4 + +[*.yml] +indent_size = 2 + +[*.json] +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7dd5252 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +vendor/ + +.phpunit.cache/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c1acc01 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright 2025 Viktor S. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the “Software”), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1b76fb0 --- /dev/null +++ b/README.md @@ -0,0 +1,171 @@ +# PHP Url +## Description + +A simple library for interacting with URLs using an object-oriented approach. + +Provides tools to build, modify, and parse URLs. +Can be used in API client classes or in dependency injection contexts. + +Requires PHP 8.2 or higher. + +* [Objects API](./docs/OBJECTS.md) + +## Components + +* **Builder** - URL instance builder +* **Facade** - Facade simplify usage +* **Parser** - Raw string url parser +* **Serializer** - URL instance serializer +* **Url** - URL instance representation +* **Util** - Inner utilities + +#### Builders + +* **HostRelative** - Builds a url relative to the hostname, setup scheme and port +* **ReplaceAttributes** - Builds new url instance with passed params replacement + +#### Serializers + +* **RFC3986** - Serializes url instance to RFC3986 string + +## Installation + +```bash +composer require diffhead/php-url +composer test +``` + +## Usage + +```php +use Diffhead\PHP\Url\Facade; +use Diffhead\PHP\Url\Dto\Replace; + +/** + * @var \Diffhead\PHP\Url\Url $url + */ +$url = Facade::parse('www.google.com'); + +/** + * @var string + */ +$string = Facade::toRfc3986String($url); + +/** + * Parameters are optionally. + * If null passed then will not + * be replaced. + */ +$dto = new Replace( + scheme: 'https', + hostname: 'www.github.com', + port: 443, + path: '/', + parameters: [] +); + +/** + * @var \Diffhead\PHP\Url\Url $replaced + */ +$replaced = Facade::replace($url, $dto); +``` + +#### Parsing URL +```php +use Diffhead\PHP\Url\Parser; +use Diffhead\PHP\Url\Exception\UrlNotContainsPort; + +$parser = new Parser('ftp://localhost/some/entity?public=1'); + +/** + * @var string $scheme + */ +$scheme = $parser->getScheme(); + +/** + * @var string $hostname + */ +$hostname = $parser->getHostname(); + +/** + * @var int $port + */ +try { + $port = $parser->getPort(); +} catch (UrlNotContainsPort $e) { + $port = 0; +} + +/** + * @var string $path + */ +$path = $parser->getPath(); + +/** + * @var array{public:string} + */ +$query = $parser->getParameters(); +``` + +#### Using with the DI container +```php +use Diffhead\PHP\Url\Builder; +use Diffhead\PHP\Url\Builder\HostRelative; +use Diffhead\PHP\Url\Port; +use Diffhead\PHP\Url\Scheme; +use Diffhead\PHP\Url\Serializer; +use Diffhead\PHP\Url\Serializer\RFC3986; +use GuzzleHttp\Client; + +class Url +{ + public function __construct( + private Builder $builder, + private Serializer $serializer + ) {} + + public function get(string $path, array $query = []): string + { + return $this->serializer->serialize( + $this->builder->build($path, $query) + ); + } +} + +class Client +{ + public function __construct( + private Client $http, + private Url $url + ) {} + + public function items(FetchItemsRequest $request): FetchItemsResponse + { + $response = $this->http->post( + $this->url->get('/v1/things'), + $this->getJsonRequestOptions($request) + ); + + return FetchItemsResponse::fromArray( + json_decode((string) $response->getBody(), true) + ); + } +} + +/** + * @var \DI\Container $container + */ +$container->set(Url::class, function (): Url { + $domain = getenv('API_DOMAIN'); + $scheme = Scheme::Https->value; + $port = Port::WebSecure->value; + + $builder = new HostRelative($domain, $scheme, $port); + $serializer = new RFC3986(); + + return new Url($builder, $serializer); +}); + + +$container->get(Client::class); +``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..8a08bbf --- /dev/null +++ b/composer.json @@ -0,0 +1,29 @@ +{ + "name": "diffhead/php-url", + "description": "PHP url interaction library using object-oriented style", + "type": "library", + "license": "MIT", + "version": "1.0.0", + "autoload": { + "psr-4": { + "Diffhead\\PHP\\Url\\": "src/", + "Diffhead\\PHP\\Url\\Tests\\": "tests/" + } + }, + "require": { + "php": "^8.2" + }, + "require-dev": { + "phpunit/phpunit": "^12.2" + }, + "scripts": { + "test": "phpunit" + }, + "minimum-stability": "stable", + "authors": [ + { + "name": "Viktor S.", + "email": "thinlineseverywhere@gmail.com" + } + ] +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..4b13aa8 --- /dev/null +++ b/composer.lock @@ -0,0 +1,1635 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "59f8c5b33b4a00952ecc83decd8e18ee", + "packages": [], + "packages-dev": [ + { + "name": "myclabs/deep-copy", + "version": "1.13.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "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" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-04-29T12:36:36+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.5.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", + "shasum": "" + }, + "require": { + "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": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" + }, + "time": "2025-05-31T08:24:38+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "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": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "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": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "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" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.3.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "ddec29dfc128eba9c204389960f2063f3b7fa170" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ddec29dfc128eba9c204389960f2063f3b7fa170", + "reference": "ddec29dfc128eba9c204389960f2063f3b7fa170", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.4.0", + "php": ">=8.3", + "phpunit/php-file-iterator": "^6.0", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.1" + }, + "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", + "extra": { + "branch-alias": { + "dev-main": "12.3.x-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 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/12.3.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-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-06-18T08:58:13+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "961bc913d42fe24a257bfff826a5068079ac7782" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/961bc913d42fe24a257bfff826a5068079ac7782", + "reference": "961bc913d42fe24a257bfff826a5068079ac7782", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.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": "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/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:37+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "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", + "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/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "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": "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/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:16+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.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/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:38+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "12.2.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "b71849b29f7a8d7574e4401873cb8b539896613f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b71849b29f7a8d7574e4401873cb8b539896613f", + "reference": "b71849b29f7a8d7574e4401873cb8b539896613f", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.1", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.3.1", + "phpunit/php-file-iterator": "^6.0.0", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.0.0", + "sebastian/comparator": "^7.1.0", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.0.2", + "sebastian/exporter": "^7.0.0", + "sebastian/global-state": "^8.0.0", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/type": "^6.0.2", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.2-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/12.2.5" + }, + "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": "2025-06-27T04:37:55+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "6d584c727d9114bcdc14c86711cd1cad51778e7c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/6d584c727d9114bcdc14c86711cd1cad51778e7c", + "reference": "6d584c727d9114bcdc14c86711cd1cad51778e7c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.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 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/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:53:50+00:00" + }, + { + "name": "sebastian/comparator", + "version": "7.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "03d905327dccc0851c9a08d6a979dfc683826b6f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/03d905327dccc0851c9a08d6a979dfc683826b6f", + "reference": "03d905327dccc0851c9a08d6a979dfc683826b6f", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.2" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.1-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/7.1.0" + }, + "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": "2025-06-17T07:41:58+00:00" + }, + { + "name": "sebastian/complexity", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "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": "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/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" + }, + "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" + }, + { + "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/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "8.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "d364b9e5d0d3b18a2573351a1786fbf96b7e0792" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d364b9e5d0d3b18a2573351a1786fbf96b7e0792", + "reference": "d364b9e5d0d3b18a2573351a1786fbf96b7e0792", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.2" + }, + "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/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T15:05:44+00:00" + }, + { + "name": "sebastian/exporter", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "76432aafc58d50691a00d86d0632f1217a47b688" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/76432aafc58d50691a00d86d0632f1217a47b688", + "reference": "76432aafc58d50691a00d86d0632f1217a47b688", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.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" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:56:42+00:00" + }, + { + "name": "sebastian/global-state", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "570a2aeb26d40f057af686d63c4e99b075fb6cbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/570a2aeb26d40f057af686d63c4e99b075fb6cbc", + "reference": "570a2aeb26d40f057af686d63c4e99b075fb6cbc", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "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/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:56:59+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.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 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/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:28+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.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" + } + ], + "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/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:48+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "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" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "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/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:17+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "c405ae3a63e01b32eb71577f8ec1604e39858a7c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/c405ae3a63e01b32eb71577f8ec1604e39858a7c", + "reference": "c405ae3a63e01b32eb71577f8ec1604e39858a7c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.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" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "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/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:01+00:00" + }, + { + "name": "sebastian/type", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "1d7cd6e514384c36d7a390347f57c385d4be6069" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/1d7cd6e514384c36d7a390347f57c385d4be6069", + "reference": "1d7cd6e514384c36d7a390347f57c385d4be6069", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.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": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "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/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-18T13:37:31+00:00" + }, + { + "name": "sebastian/version", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "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", + "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/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:38+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": [ + "static analysis" + ], + "support": { + "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/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "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.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.2" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/docs/OBJECTS.md b/docs/OBJECTS.md new file mode 100644 index 0000000..f4a8913 --- /dev/null +++ b/docs/OBJECTS.md @@ -0,0 +1,152 @@ +# Objects API + +#### Diffhead\PHP\Url\Facade +```php +class Facade +{ + public static function parse(string $url): Url; + public static function toRfc3986String(Url $url): string; + public static function replace(Url $url, Replace $replacements): Url; +} +``` + +#### Diffhead\PHP\Url\Url +```php +class Url +{ + public function __construct( + string $scheme, + string $hostname, + string $path, + int $port, + array $parameters + ); + + public function scheme(): string; + public function hostname(): string; + public function path(): string; + public function port(): int; + public function parameters(): array; +} +``` + +#### Diffhead\PHP\Url\Builder +```php +namespace Diffhead\PHP\Url; + +use Diffhead\PHP\Url\Url; + +interface Builder +{ + public function build(string $resource, array $parameters = []): Url; +} +``` + +#### Diffhead\PHP\Url\Builder\HostRelative +```php +namespace Diffhead\PHP\Url\Builder; + +use Diffhead\PHP\Url\Url; +use Diffhead\PHP\Url\Builder; + +class HostRelative implements Builder +{ + public function __construct(string $hostname, string $scheme, int $port); + public function build(string $path, array $query = []): Url; +} +``` + +#### Diffhead\PHP\Url\Builder\ReplaceAttributes +```php +namespace Diffhead\PHP\Url\Builder; + +use Diffhead\PHP\Url\Url; +use Diffhead\PHP\Url\Builder; + +class ReplaceAttributes implements Builder +{ + public function __construct(Url $url); + + /** + * Pass the hostname and/or another params + * to replace it in the URL instance. + * + * Empty hostname argument means + * it will not be replaced. + * + * @param string $hostname + * @param array{scheme?:string,port?:int,path?:string,parameters?:array} $parameters + */ + public function build(string $hostname = '', array $parameters = []): Url; +} +``` + +#### Diffhead\PHP\Url\Parser +```php +namespace Diffhead\PHP\Url; + +class Parser +{ + public function __construct(string $url); + + /** + * @return string + * + * @throws \Diffhead\PHP\Url\Exception\UrlNotContainsScheme + */ + public function getScheme(): string; + + /** + * @return string + * + * @throws \Diffhead\PHP\Url\Exception\UrlNotContainsHostname + */ + public function getHostname(): string; + + /** + * @return int + * + * @throws \Diffhead\PHP\Url\Exception\UrlNotContainsPort + */ + public function getPort(): int; + + /** + * @return string + * + * @throws \Diffhead\PHP\Url\Exception\UrlNotContainsPath + */ + public function getPath(): string; + + /** + * @return array + * + * @throws \Diffhead\PHP\Url\Exception\UrlNotContainsQuery + */ + public function getParameters(): array; +} +``` + +#### Diffhead\PHP\Url\Serializer +```php +namespace Diffhead\PHP\Url; + +use Diffhead\PHP\Url\Url; + +interface Serializer +{ + public function toString(Url $url): string; +} +``` + +#### Diffhead\PHP\Url\Serializer\RFC3986 +```php +namespace Diffhead\PHP\Url; + +use Diffhead\PHP\Url\Serializer; +use Diffhead\PHP\Url\Url; + +class RFC3986 implements Serializer +{ + public function toString(Url $url): string; +} +``` diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..82cf31e --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,27 @@ + + + + + tests + + + + + + src + + + diff --git a/src/Builder.php b/src/Builder.php new file mode 100644 index 0000000..3a429d8 --- /dev/null +++ b/src/Builder.php @@ -0,0 +1,8 @@ +scheme, + $this->hostname, + $path, + $this->port, + $parameters + ); + } +} diff --git a/src/Builder/ReplaceAttributes.php b/src/Builder/ReplaceAttributes.php new file mode 100644 index 0000000..dde4022 --- /dev/null +++ b/src/Builder/ReplaceAttributes.php @@ -0,0 +1,36 @@ +url->scheme(); + $hostname = $hostname ?: $this->url->hostname(); + $path = $parameters['path'] ?? $this->url->path(); + $port = $parameters['port'] ?? $this->url->port(); + $params = $parameters['parameters'] ?? $this->url->parameters(); + + return Url::create($scheme, $hostname, $path, $port, $params); + } +} diff --git a/src/Dto/Replace.php b/src/Dto/Replace.php new file mode 100644 index 0000000..da1c8dc --- /dev/null +++ b/src/Dto/Replace.php @@ -0,0 +1,41 @@ +scheme; + } + + public function hostname(): ?string + { + return $this->hostname; + } + + public function path(): ?string + { + return $this->path; + } + + public function port(): ?int + { + return $this->port; + } + + public function parameters(): ?array + { + return $this->parameters; + } +} diff --git a/src/Exception/UrlNotContainsHostname.php b/src/Exception/UrlNotContainsHostname.php new file mode 100644 index 0000000..6c6ebde --- /dev/null +++ b/src/Exception/UrlNotContainsHostname.php @@ -0,0 +1,11 @@ + $parser->getScheme(), ''), + self::valueOrDefault(fn () => $parser->getHostname(), ''), + self::valueOrDefault(fn () => $parser->getPath(), ''), + self::valueOrDefault(fn () => $parser->getPort(), 0), + self::valueOrDefault(fn () => $parser->getParameters(), []), + ); + } + + public static function toRfc3986String(Url $url): string + { + $serializer = new RFC3986(); + return $serializer->toString($url); + } + + public static function replace(Url $url, Replace $replacements): Url + { + $builder = new ReplaceAttributes($url); + + $parameters = []; + + if ($replacements->scheme() !== null) { + $parameters['scheme'] = $replacements->scheme(); + } + + if ($replacements->path() !== null) { + $parameters['path'] = $replacements->path(); + } + + if ($replacements->port() !== null) { + $parameters['port'] = $replacements->port(); + } + + if ($replacements->parameters() !== null) { + $parameters['parameters'] = $replacements->parameters(); + } + + return $builder->build($replacements->hostname() ?? '', $parameters); + } + + /** + * @param Closure():mixed $getter + * @param mixed $default + * + * @return mixed + */ + private static function valueOrDefault(Closure $getter, mixed $default = null): mixed + { + try { + return $getter(); + } catch (UrlRuntimeException $exception) { + return $default; + } + } +} diff --git a/src/Parser.php b/src/Parser.php new file mode 100644 index 0000000..bf13935 --- /dev/null +++ b/src/Parser.php @@ -0,0 +1,200 @@ +url = urldecode($url); + } + + /** + * @return string + * + * @throws \Diffhead\PHP\Url\Exception\UrlNotContainsScheme + */ + public function getScheme(): string + { + if (is_null($this->scheme)) { + if (! preg_match(Regex::Scheme->value, $this->url, $matches)) { + throw new UrlNotContainsScheme($this->url); + } + + $this->scheme = strtolower($matches[1]); + } + + return $this->scheme; + } + + /** + * @return string + * + * @throws \Diffhead\PHP\Url\Exception\UrlNotContainsHostname + */ + public function getHostname(): string + { + if (is_null($this->hostname)) { + $matched = preg_match(Regex::Hostname->value, $this->url, $matches); + + switch (true) { + case $matched === false: + case empty($matches[1]): + case str_starts_with($this->url, sprintf('%s://', $matches[1])): + $this->throwUrlNotContainsHostname(); + } + + $this->hostname = $matches[1]; + } + + return $this->hostname; + } + + /** + * @return int + * + * @throws \Diffhead\PHP\Url\Exception\UrlNotContainsPort + */ + public function getPort(): int + { + if (is_null($this->port)) { + $matched = preg_match( + Regex::Port->value, + $this->url, + $matches, + PREG_UNMATCHED_AS_NULL + ); + + if (! $matched) { + throw new UrlNotContainsPort($this->url); + } + + $this->port = (int) $matches[2]; + } + + return $this->port; + } + + /** + * @return string + * + * @throws \Diffhead\PHP\Url\Exception\UrlNotContainsPath + */ + public function getPath(): string + { + if (is_null($this->path)) { + if (! preg_match(Regex::Path->value, $this->url, $matches)) { + $this->throwUrlNotContainsPath(); + } + + if (empty($matches[1])) { + $this->throwUrlNotContainsPath(); + } + + $this->path = $matches[1]; + } + + return $this->path; + } + + /** + * @return array + * + * @throws \Diffhead\PHP\Url\Exception\UrlNotContainsQuery + */ + public function getParameters(): array + { + if (is_null($this->parameters)) { + if (! preg_match(Regex::Query->value, $this->url, $matches)) { + throw new UrlNotContainsQuery($this->url); + } + + /** + * @var array $parameters + */ + $parametersRaw = explode('&', $matches[1]); + $parameters = []; + + foreach ($parametersRaw as $parameter) { + [$key, $value] = explode('=', $parameter); + + if ($this->isNestedKey($key)) { + $keys = $this->splitNestedKey($key); + + $this->setNestedValue($keys, $value, $parameters); + } else { + $this->setFlatValue($key, $value, $parameters); + } + } + + $this->parameters = $parameters; + } + + return $this->parameters; + } + + private function isNestedKey(string $key): bool + { + return is_numeric(strpos($key, '[')); + } + + private function splitNestedKey(string $nestedKey): array + { + preg_match_all(Regex::QueryArrayKeys->value, $nestedKey, $matches); + + return array_map(fn (string $key) => $key, $matches[1]); + } + + /** + * @param array $keys + * @param string $value + * @param array $parameters + * + * @return void + */ + private function setNestedValue(array $keys, string $value, array &$parameters): void + { + $current = array_shift($keys); + + if (! empty($keys)) { + $temporary = []; + + $this->setNestedValue($keys, $value, $temporary); + + $persisted = $parameters[$current] ?? []; + $parameters[$current] = array_merge($persisted, $temporary); + } else if (empty($current)) { + $parameters[] = $value; + } else { + $parameters[$current] = $value; + } + } + + private function setFlatValue(string $key, string $value, array &$parameters): void + { + $parameters[$key] = $value; + } + + private function throwUrlNotContainsHostname(): void + { + throw new UrlNotContainsHostname($this->url); + } + + private function throwUrlNotContainsPath(): void + { + throw new UrlNotContainsPath($this->url); + } +} diff --git a/src/Port.php b/src/Port.php new file mode 100644 index 0000000..2e12b42 --- /dev/null +++ b/src/Port.php @@ -0,0 +1,31 @@ +scheme(), + $this->getHost($url), + $this->getPath($url), + $this->getQueryString($url) + ); + } + + private function getHost(Url $url): string + { + switch (true) { + /** + * If http protocol with 80 port + * or https protocol with 443 port + */ + case Util::isDefaultHttpUrl($url): + /** + * If ws protocol with 80 port + * or wss protocol with 443 port + */ + case Util::isDefaultWebSocketUrl($url): + return $url->hostname(); + + default: + return sprintf('%s:%d', $url->hostname(), $url->port()); + } + } + + private function getPath(Url $url): string + { + $pathIsEmpty = empty($url->path()); + $pathStartsWithSlash = str_starts_with($url->path(), '/'); + + if ($pathIsEmpty || $pathStartsWithSlash) { + return $url->path(); + } + + return sprintf('/%s', $url->path()); + } + + private function getQueryString(Url $url): string + { + if ($parameters = $url->parameters()) { + return '?' . http_build_query($parameters, '', '&', PHP_QUERY_RFC3986); + } + + return ''; + } +} diff --git a/src/Url.php b/src/Url.php new file mode 100644 index 0000000..0127838 --- /dev/null +++ b/src/Url.php @@ -0,0 +1,61 @@ + $parameters + */ + public function __construct( + private string $scheme, + private string $hostname, + private string $path, + private int $port, + private array $parameters + ) {} + + public function scheme(): string + { + return $this->scheme; + } + + public function hostname(): string + { + return $this->hostname; + } + + public function path(): string + { + return $this->path; + } + + public function port(): int + { + return $this->port; + } + + /** + * @return array + */ + public function parameters(): array + { + return $this->parameters; + } +} diff --git a/src/Util.php b/src/Util.php new file mode 100644 index 0000000..a75572c --- /dev/null +++ b/src/Util.php @@ -0,0 +1,54 @@ +scheme() === Scheme::Http->value + && static::webOrEmptyPort($url); + } + + private static function httpWithTls(Url $url): bool + { + return $url->scheme() === Scheme::Https->value + && static::webSecureOrEmptyPort($url); + } + + private static function webSocketWithoutTls(Url $url): bool + { + return $url->scheme() === Scheme::WebSocket->value + && static::webOrEmptyPort($url); + } + + private static function webSocketWithTls(Url $url): bool + { + return $url->scheme() === Scheme::WebSocketSecure->value + && static::webSecureOrEmptyPort($url); + } + + private static function webOrEmptyPort(Url $url): bool + { + return $url->port() === Port::Web->value || empty($url->port()); + } + + private static function webSecureOrEmptyPort(Url $url): bool + { + return $url->port() === Port::WebSecure->value || empty($url->port()); + } +} diff --git a/tests/Builder/HostRelativeTest.php b/tests/Builder/HostRelativeTest.php new file mode 100644 index 0000000..31f663f --- /dev/null +++ b/tests/Builder/HostRelativeTest.php @@ -0,0 +1,36 @@ + true, 'page' => 2, 'perPage' => 100]; + + $builder = new HostRelative($hostname, $scheme->value, $port->value); + + $url = $builder->build($path, $query); + + $this->assertSame($hostname, $url->hostname()); + $this->assertSame($scheme->value, $url->scheme()); + $this->assertSame($port->value, $url->port()); + $this->assertSame($path, $url->path()); + $this->assertEquals($query, $url->parameters()); + } +} diff --git a/tests/Builder/ReplaceAttributesTest.php b/tests/Builder/ReplaceAttributesTest.php new file mode 100644 index 0000000..2fe2084 --- /dev/null +++ b/tests/Builder/ReplaceAttributesTest.php @@ -0,0 +1,83 @@ + '1']); + + $builder = new ReplaceAttributes($origin); + + $replaced = $builder->build(); + + $this->assertSame('http', $replaced->scheme()); + $this->assertSame('example.com', $replaced->hostname()); + $this->assertSame('/path', $replaced->path()); + $this->assertSame(80, $replaced->port()); + $this->assertSame(['a' => '1'], $replaced->parameters()); + } + + public function testPartiallyReplacement(): void + { + $origin = Url::create('http', 'example.com', '/path', 80, ['a' => '1']); + + $builder = new ReplaceAttributes($origin); + + $replaced = $builder->build('new-example.com', [ + 'scheme' => 'https', + 'port' => 443, + ]); + + $this->assertSame('https', $replaced->scheme()); + $this->assertSame('new-example.com', $replaced->hostname()); + $this->assertSame('/path', $replaced->path()); + $this->assertSame(443, $replaced->port()); + $this->assertSame(['a' => '1'], $replaced->parameters()); + } + + public function testFullReplacement(): void + { + $origin = Url::create('http', 'example.com', '/path', 80, ['a' => '1']); + + $builder = new ReplaceAttributes($origin); + + $replaced = $builder->build('new-example.com', [ + 'scheme' => 'https', + 'path' => '/new-path', + 'port' => 443, + 'parameters' => ['b' => '2'], + ]); + + $this->assertSame('https', $replaced->scheme()); + $this->assertSame('new-example.com', $replaced->hostname()); + $this->assertSame('/new-path', $replaced->path()); + $this->assertSame(443, $replaced->port()); + $this->assertSame(['b' => '2'], $replaced->parameters()); + } + + public function testInvalidParametersTypes(): void + { + $this->expectException(TypeError::class); + + $origin = Url::create('http', 'example.com', '/path', 80, ['a' => '1']); + + $builder = new ReplaceAttributes($origin); + + $builder->build('new-example.com', [ + 'scheme' => 123, + ]); + } +} diff --git a/tests/Dto/ReplaceTest.php b/tests/Dto/ReplaceTest.php new file mode 100644 index 0000000..e01c5e7 --- /dev/null +++ b/tests/Dto/ReplaceTest.php @@ -0,0 +1,36 @@ + 'value'], + ); + + $this->assertSame('https', $replace->scheme()); + $this->assertSame('example.com', $replace->hostname()); + $this->assertSame('/path', $replace->path()); + $this->assertSame(443, $replace->port()); + $this->assertSame(['param' => 'value'], $replace->parameters()); + } +} diff --git a/tests/FacadeTest.php b/tests/FacadeTest.php new file mode 100644 index 0000000..94db3eb --- /dev/null +++ b/tests/FacadeTest.php @@ -0,0 +1,86 @@ +assertSame('', $url->scheme()); + $this->assertSame('', $url->hostname()); + $this->assertSame('', $url->path()); + $this->assertSame(0, $url->port()); + $this->assertSame([], $url->parameters()); + } + + public function testParsingOnProperlyWebUrl(): void + { + $url = Facade::parse('https://example.com:8080/path/to/resource?param1=value1¶m2=value2'); + + $this->assertSame('https', $url->scheme()); + $this->assertSame('example.com', $url->hostname()); + $this->assertSame('/path/to/resource', $url->path()); + $this->assertSame(8080, $url->port()); + $this->assertSame(['param1' => 'value1', 'param2' => 'value2'], $url->parameters()); + } + + public function testParsingOnDsn(): void + { + $url = Facade::parse('mysql://user:password@localhost:3306/database_name'); + + $this->assertSame('mysql', $url->scheme()); + $this->assertSame('localhost', $url->hostname()); + $this->assertSame('/database_name', $url->path()); + $this->assertSame(3306, $url->port()); + $this->assertSame([], $url->parameters()); + } + + public function testStringConversionToRfc3986(): void + { + $originUrl = 'https://example.com:8080/path/to/resource?param1=value1¶m2=value2'; + $url = Facade::parse($originUrl); + $convertedUrl = Facade::toRfc3986String($url); + + $this->assertSame($originUrl, $convertedUrl); + } + + public function testUrlAttributesReplacement(): void + { + $raw = 'http://example.com/path?param=oldValue'; + $origin = Facade::parse($raw); + + $replace = new Replace( + scheme: 'https', + hostname: 'new-example.com', + path: '/new-path', + port: 443, + parameters: ['param' => 'newValue', 'addedParam' => 'addedValue'], + ); + + $replaced = Facade::replace($origin, $replace); + + $this->assertSame('https', $replaced->scheme()); + $this->assertSame('new-example.com', $replaced->hostname()); + $this->assertSame('/new-path', $replaced->path()); + $this->assertSame(443, $replaced->port()); + + $this->assertSame( + ['param' => 'newValue', 'addedParam' => 'addedValue'], + $replaced->parameters() + ); + } +} diff --git a/tests/ParserTest.php b/tests/ParserTest.php new file mode 100644 index 0000000..7de7e61 --- /dev/null +++ b/tests/ParserTest.php @@ -0,0 +1,118 @@ +assertSame('http', $this->getParser('http://example.info')->getScheme()); + $this->assertSame('https', $this->getParser('https://example.info')->getScheme()); + } + + public function testEmptySchemeParsing(): void + { + $this->expectException(UrlNotContainsScheme::class); + $this->getParser('example.info')->getScheme(); + } + + public function testExistingHostnameParsing(): void + { + $this->assertSame('example.info', $this->getParser('example.info')->getHostname()); + $this->assertSame('example.info', $this->getParser('https://example.info')->getHostname()); + } + + public function testEmptyHostnameParsing(): void + { + $this->expectException(UrlNotContainsHostname::class); + $this->getParser('https:///api/webhooks/event')->getHostname(); + } + + public function testExistingPortParsing(): void + { + $this->assertSame(80, $this->getParser('localhost:80')->getPort()); + $this->assertSame(443, $this->getParser('https://google.com:443/index')->getPort()); + } + + public function testEmptyPortParsing(): void + { + $this->expectException(UrlNotContainsPort::class); + $this->getParser('localhost')->getPort(); + } + + public function testExistingPathParsing(): void + { + $this->assertSame('/', $this->getParser('https://google.com/')->getPath()); + $this->assertSame('/api/endpoint', $this->getParser('https://something.org/api/endpoint')->getPath()); + $this->assertSame('/api/endpoint', $this->getParser('something.org:1234/api/endpoint')->getPath()); + } + + public function testEmptyPathParsing(): void + { + $this->expectException(UrlNotContainsPath::class); + $this->getParser('https://google.com')->getPath(); + } + + public function testExistingFlatParametersParsing(): void + { + $url = 'https://example.com?parameter=value&secondParameter=secondValue&a=100'; + $parser = $this->getParser($url); + + $urlParameters = [ + 'parameter' => 'value', + 'secondParameter' => 'secondValue', + 'a' => '100' + ]; + + $this->assertEquals($urlParameters, $parser->getParameters()); + } + + public function testExistingNestedParametersParsing(): void + { + $url = 'https://example.com/search?query=book&filters%5Bprice%5D%5Bmin%5D=10&filters%5Bprice%5D%5Bmax%5D=100&filters%5Bcategories%5D%5B%5D=fiction&filters%5Bcategories%5D%5B%5D=history'; + $parser = $this->getParser($url); + + $urlParameters = [ + 'query' => 'book', + 'price' => [ + 'min' => '10', + 'max' => '100' + ], + 'categories' => [ + 'fiction', + 'history' + ] + ]; + + $this->assertEquals($urlParameters, $parser->getParameters()); + } + + public function testNonExistingParametersParsing(): void + { + $this->expectException(UrlNotContainsQuery::class); + $this->getParser('https://google.com')->getParameters(); + } + + private function getParser(string $url): Parser + { + return new Parser($url); + } +} diff --git a/tests/Serializer/RFC3986Test.php b/tests/Serializer/RFC3986Test.php new file mode 100644 index 0000000..9fe737d --- /dev/null +++ b/tests/Serializer/RFC3986Test.php @@ -0,0 +1,183 @@ +value, + 'localhost', + '/api/endpoint', + Port::Web->value, + [ + 'isActive' => true, + 'page' => 1, + 'perPage' => 100 + ] + ); + + $this->assertSame( + 'http://localhost/api/endpoint?isActive=1&page=1&perPage=100', + $this->getSerializer()->toString($url) + ); + } + + public function serializeHttpUrlWithNonDefaultPort(): void + { + $url = new Url( + Scheme::Http->value, + 'localhost', + '/api/endpoint', + Port::WebSecure->value, + [ + 'isActive' => true, + 'page' => 1, + 'perPage' => 100 + ] + ); + + $this->assertSame( + 'http://localhost:443/api/endpoint?isActive=1&page=1&perPage=100', + $this->getSerializer()->toString($url) + ); + } + + public function serializeHttpsUrlWithDefaultPort(): void + { + $url = new Url( + Scheme::Https->value, + 'localhost', + '/api/endpoint', + Port::WebSecure->value, + [ + 'isActive' => true, + 'page' => 1, + 'perPage' => 100 + ] + ); + + $this->assertSame( + 'https://localhost/api/endpoint?isActive=1&page=1&perPage=100', + $this->getSerializer()->toString($url) + ); + } + + public function serializeHttpsUrlWithNonDefaultPort(): void + { + $url = new Url( + Scheme::Https->value, + 'localhost', + '/api/endpoint', + Port::MySql->value, + [ + 'isActive' => true, + 'page' => 1, + 'perPage' => 100 + ] + ); + + $this->assertSame( + 'https://localhost:3306/api/endpoint?isActive=1&page=1&perPage=100', + $this->getSerializer()->toString($url) + ); + } + + public function testSerializeWebSocketUrlWithDefaultPort(): void + { + $url = new Url( + Scheme::WebSocket->value, + 'localhost', + '/api/endpoint', + Port::Web->value, + [ + 'isActive' => true, + 'page' => 1, + 'perPage' => 100 + ] + ); + + $this->assertSame( + 'ws://localhost/api/endpoint?isActive=1&page=1&perPage=100', + $this->getSerializer()->toString($url) + ); + } + + public function serializeWebSocketUrlWithNonDefaultPort(): void + { + $url = new Url( + Scheme::Http->value, + 'localhost', + '/api/endpoint', + Port::WebSecure->value, + [ + 'isActive' => true, + 'page' => 1, + 'perPage' => 100 + ] + ); + + $this->assertSame( + 'ws://localhost:443/api/endpoint?isActive=1&page=1&perPage=100', + $this->getSerializer()->toString($url) + ); + } + + public function serializeWebSocketSecureUrlWithDefaultPort(): void + { + $url = new Url( + Scheme::WebSocketSecure->value, + 'localhost', + '/api/endpoint', + Port::WebSecure->value, + [ + 'isActive' => true, + 'page' => 1, + 'perPage' => 100 + ] + ); + + $this->assertSame( + 'wss://localhost/api/endpoint?isActive=1&page=1&perPage=100', + $this->getSerializer()->toString($url) + ); + } + + public function serializeWebSocketSecureUrlWithNonDefaultPort(): void + { + $url = new Url( + Scheme::WebSocketSecure->value, + 'localhost', + '/api/endpoint', + Port::Web->value, + [ + 'isActive' => true, + 'page' => 1, + 'perPage' => 100 + ] + ); + + $this->assertSame( + 'wss://localhost:80/api/endpoint?isActive=1&page=1&perPage=100', + $this->getSerializer()->toString($url) + ); + } + + private function getSerializer(): RFC3986 + { + return new RFC3986(); + } +} diff --git a/tests/UrlTest.php b/tests/UrlTest.php new file mode 100644 index 0000000..bd23632 --- /dev/null +++ b/tests/UrlTest.php @@ -0,0 +1,55 @@ +value, + 'file.storage.info', + '/home/user/Downloads', + Port::FileTransferOverSsh->value, + ['file' => 'something.txt'] + ); + + $this->assertSame(Scheme::FileTransferOverSsh->value, $url->scheme()); + $this->assertSame('file.storage.info', $url->hostname()); + $this->assertSame('/home/user/Downloads', $url->path()); + $this->assertSame(Port::FileTransferOverSsh->value, $url->port()); + $this->assertEquals(['file' => 'something.txt'], $url->parameters()); + } + + public function testStaticInitialization(): void + { + $url = Url::create( + Scheme::WebSocketSecure->value, + 'localhost', + '/api/endpoint', + Port::WebSecure->value, + ['token' => 'token1234'] + ); + + $this->assertSame(Scheme::WebSocketSecure->value, $url->scheme()); + $this->assertSame('localhost', $url->hostname()); + $this->assertSame('/api/endpoint', $url->path()); + $this->assertSame(Port::WebSecure->value, $url->port()); + $this->assertEquals(['token' => 'token1234'], $url->parameters()); + } +} diff --git a/tests/UtilTest.php b/tests/UtilTest.php new file mode 100644 index 0000000..5a3de59 --- /dev/null +++ b/tests/UtilTest.php @@ -0,0 +1,108 @@ +value, + 'localhost', + '/', + Port::Web->value, + [] + ); + + $httpsWith443Port = new Url( + Scheme::Https->value, + 'localhost', + '/', + Port::WebSecure->value, + [] + ); + + $this->assertTrue(Util::isDefaultHttpUrl($httpWith80Port)); + $this->assertTrue(Util::isDefaultHttpUrl($httpsWith443Port)); + } + + public function testHttpHttpsWithNonDefaultPortsTesting(): void + { + + $httpWith443Port = new Url( + Scheme::Http->value, + 'localhost', + '/', + Port::WebSecure->value, + [] + ); + + $httpsWith80Port = new Url( + Scheme::Https->value, + 'localhost', + '/', + Port::Web->value, + [] + ); + + $this->assertFalse(Util::isDefaultHttpUrl($httpWith443Port)); + $this->assertFalse(Util::isDefaultHttpUrl($httpsWith80Port)); + } + + public function testWebSocketWithDefaultPortsTesting(): void + { + $webSocketWith80Port = new Url( + Scheme::WebSocket->value, + 'localhost', + '/', + Port::Web->value, + [] + ); + + $webSocketSecureWith443Port = new Url( + Scheme::WebSocketSecure->value, + 'localhost', + '/', + Port::WebSecure->value, + [] + ); + + $this->assertTrue(Util::isDefaultWebSocketUrl($webSocketWith80Port)); + $this->assertTrue(Util::isDefaultWebSocketUrl($webSocketSecureWith443Port)); + } + + public function testWebSocketWithNonDefaultPortsTesting(): void + { + $webSocketWith443Port = new Url( + Scheme::WebSocket->value, + 'localhost', + '/', + Port::WebSecure->value, + [] + ); + + $webSocketSecureWith80Port = new Url( + Scheme::WebSocketSecure->value, + 'localhost', + '/', + Port::Web->value, + [] + ); + + $this->assertFalse(Util::isDefaultWebSocketUrl($webSocketWith443Port)); + $this->assertFalse(Util::isDefaultWebSocketUrl($webSocketSecureWith80Port)); + } +}