diff --git a/.gitattributes b/.gitattributes index 60a9b2dc5..3e9ca7c36 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,8 +5,9 @@ /.php_cs.dist export-ignore /.scrutinizer.yml export-ignore Dockerfile export-ignore +/benchmark/ export-ignore /hack/ export-ignore -/phpspec.ci.yml export-ignore +/phpbench.json export-ignore /phpspec.yml.dist export-ignore /phpstan-baseline.neon export-ignore /phpstan.neon.dist export-ignore diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 856c825b1..f9d6b7bb4 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -2,8 +2,6 @@ name: Checks on: push: - branches: - - master pull_request: jobs: @@ -13,7 +11,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Install uses: docker://composer @@ -36,7 +34,7 @@ jobs: # steps: # - name: Checkout code - # uses: actions/checkout@v1 + # uses: actions/checkout@v2 # - name: Roave BC Check # uses: docker://nyholm/roave-bc-check-ga diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66de72ca8..b1c1a6c9c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,48 +2,45 @@ name: CI on: push: - branches: - - master pull_request: jobs: - build-lowest-version: + lowest-version-tests: name: Build lowest version runs-on: ubuntu-latest steps: - name: Set up PHP - uses: shivammathur/setup-php@1.7.0 + uses: shivammathur/setup-php@v2 with: - php-version: '5.6' + php-version: '8.0' extensions: bcmath, gmp, intl, dom, mbstring - name: Setup Problem Matchers for PHPUnit run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - name: Checkout code - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Set up locales run: ./hack/setup-locales.sh - name: Download dependencies - run: composer update --prefer-stable --prefer-dist --no-interaction --no-progress --no-suggest --prefer-lowest + run: composer update --prefer-stable --prefer-dist --no-interaction --no-progress --no-suggest --prefer-lowest --classmap-authoritative - name: Run tests run: composer test - build: + tests: name: Build runs-on: ubuntu-latest strategy: - max-parallel: 10 matrix: - php: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4'] + php: ['8.0'] steps: - name: Set up PHP - uses: shivammathur/setup-php@1.6.2 + uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: bcmath, gmp, intl, dom, mbstring @@ -52,17 +49,64 @@ jobs: run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - name: Checkout code - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Set up locales run: ./hack/setup-locales.sh - name: Download dependencies - run: composer update --no-ansi --prefer-stable --prefer-dist --no-interaction --no-progress --no-suggest + run: composer install --classmap-authoritative - name: Run tests run: composer test + psalm: + name: Psalm + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + extensions: bcmath, gmp, intl, dom, mbstring + + - name: Download dependencies + run: composer install --classmap-authoritative + + - name: Psalm + run: vendor/bin/psalm + + mutation-tests: + name: Infection + runs-on: ubuntu-latest + needs: + - tests + - psalm + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + extensions: bcmath, gmp, intl, dom, mbstring + coverage: pcov + + - name: Set up locales + run: ./hack/setup-locales.sh + + - name: Download dependencies + run: composer install --classmap-authoritative + + - name: Psalm + run: vendor/bin/roave-infection-static-analysis-plugin + docs: name: Docs runs-on: ubuntu-latest @@ -74,7 +118,7 @@ jobs: architecture: 'x64' - name: Checkout code - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Install dependencies run: | diff --git a/.github/workflows/phpstan.entrypoint b/.github/workflows/phpstan.entrypoint deleted file mode 100755 index 0810152fb..000000000 --- a/.github/workflows/phpstan.entrypoint +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -l - -sh -c "/composer/vendor/bin/phpstan $*" diff --git a/.github/workflows/psalm.entrypoint b/.github/workflows/psalm.entrypoint deleted file mode 100755 index 6ce643dc7..000000000 --- a/.github/workflows/psalm.entrypoint +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -l - -sh -c "/composer/vendor/bin/psalm $*" diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index cec3eb8f9..5507ef1ba 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -2,58 +2,25 @@ name: Static analysis on: push: - branches: - - master pull_request: jobs: - phpstan: - name: PHPStan + phpcs: + name: PHP-CodeSniffer runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v1 + uses: actions/checkout@v2 - - name: Install - uses: docker://composer + - name: Set up PHP + uses: shivammathur/setup-php@v2 with: - args: install --ignore-platform-reqs --no-ansi --no-suggest + php-version: '8.0' + extensions: bcmath, gmp, intl, dom, mbstring - - name: PHPStan - uses: docker://oskarstark/phpstan-ga:0.12.28 - with: - entrypoint: ./.github/workflows/phpstan.entrypoint - args: analyze --no-progress - - php-cs-fixer: - name: PHP-CS-Fixer - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v1 - - - name: PHP-CS-Fixer - uses: docker://oskarstark/php-cs-fixer-ga:2.16.3.1 - with: - args: --dry-run --diff-format udiff - - psalm: - name: Psalm - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v1 - - - name: Install - uses: docker://composer - with: - args: install --ignore-platform-reqs --no-ansi --no-suggest + - name: Download dependencies + run: composer install --classmap-authoritative - name: Psalm - uses: docker://vimeo/psalm-github-actions - with: - entrypoint: ./.github/workflows/psalm.entrypoint - args: --threads=8 --diff --diff-methods + run: vendor/bin/phpcs diff --git a/.gitignore b/.gitignore index 4cd77e8ff..6ccb5ba8c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ +/.phpbench .php_cs .php_cs.cache .phpunit.result.cache /build/ -/composer.lock /phpspec.yml /phpunit.xml /vendor/ diff --git a/.php_cs.dist b/.php_cs.dist deleted file mode 100644 index f41361dc5..000000000 --- a/.php_cs.dist +++ /dev/null @@ -1,17 +0,0 @@ -setRules([ - '@Symfony' => true, - 'array_syntax' => ['syntax' => 'short'], - 'yoda_style' => false, - 'self_accessor' => false, - ]) - ->setFinder( - PhpCsFixer\Finder::create() - ->exclude('spec') - ->exclude('resources') - ->notPath('src/MoneyFactory.php') - ->in(__DIR__) - ) -; diff --git a/Dockerfile b/Dockerfile index 1bc72cec2..747a5c078 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM php:7.2-cli +FROM php:8.0-cli RUN set -xe \ && apt-get update \ diff --git a/benchmark/MoneyInstantiationBench.php b/benchmark/MoneyInstantiationBench.php new file mode 100644 index 000000000..e3eac71e2 --- /dev/null +++ b/benchmark/MoneyInstantiationBench.php @@ -0,0 +1,53 @@ +currency = new Currency('EUR'); + } + + public function benchConstructorWithZeroIntegerAmount(): void + { + new Money(0, $this->currency); + } + + public function benchConstructorWithPositiveIntegerAmount(): void + { + new Money(1234567890, $this->currency); + } + + public function benchConstructorWithNegativeIntegerAmount(): void + { + new Money(-1234567890, $this->currency); + } + + public function benchConstructorWithZeroStringAmount(): void + { + new Money('0', $this->currency); + } + + public function benchConstructorWithPositiveStringAmount(): void + { + new Money('1234567890', $this->currency); + } + + public function benchConstructorWithNegativeStringAmount(): void + { + new Money('-1234567890', $this->currency); + } +} diff --git a/benchmark/MoneyOperationBench.php b/benchmark/MoneyOperationBench.php new file mode 100644 index 000000000..4fe5d2775 --- /dev/null +++ b/benchmark/MoneyOperationBench.php @@ -0,0 +1,132 @@ +a = new Money('100', $currency); + $this->b = new Money('50', $currency); + } + + public function benchAdd(): void + { + $this->a->add($this->b); + } + + public function benchSubtract(): void + { + $this->a->subtract($this->b); + } + + public function benchMultiply(): void + { + $this->a->multiply('5'); + } + + public function benchDivide(): void + { + $this->a->divide('5'); + } + + public function benchSum(): void + { + Money::sum($this->a, $this->b, $this->a, $this->b); + } + + public function benchMin(): void + { + Money::min($this->a, $this->b, $this->a, $this->b); + } + + public function benchMax(): void + { + Money::min($this->a, $this->b, $this->a, $this->b); + } + + public function benchAvg(): void + { + Money::min($this->a, $this->b, $this->a, $this->b); + } + + public function benchRatioOf(): void + { + $this->a->ratioOf($this->b); + } + + public function benchMod(): void + { + $this->a->mod($this->b); + } + + public function benchIsSameCurrency(): void + { + $this->a->isSameCurrency($this->b); + } + + public function benchIsZero(): void + { + $this->a->isZero(); + } + + public function benchAbsolute(): void + { + $this->a->absolute(); + } + + public function benchNegative(): void + { + $this->a->negative(); + } + + public function benchIsPositive(): void + { + $this->a->isPositive(); + } + + public function benchCompare(): void + { + $this->a->compare($this->b); + } + + public function benchLessThan(): void + { + $this->a->lessThan($this->b); + } + + public function benchLessThanOrEqual(): void + { + $this->a->lessThanOrEqual($this->b); + } + + public function benchEquals(): void + { + $this->a->equals($this->b); + } + + public function benchGreaterThan(): void + { + $this->a->greaterThan($this->b); + } + + public function benchGreaterThanOrEqual(): void + { + $this->a->greaterThanOrEqual($this->b); + } +} diff --git a/benchmark/NumberInstantiationBench.php b/benchmark/NumberInstantiationBench.php new file mode 100644 index 000000000..7364eccae --- /dev/null +++ b/benchmark/NumberInstantiationBench.php @@ -0,0 +1,83 @@ +currency = new Currency('EUR'); + } + + public function benchConstructorWithZeroIntegerAmount(): void + { + new Number('0', ''); + } + + public function benchConstructorWithPositiveIntegerAmount(): void + { + new Number('1234567890', ''); + } + + public function benchConstructorWithNegativeIntegerAmount(): void + { + new Number('-1234567890', ''); + } + + public function benchConstructorWithZeroAndFractionalAmount(): void + { + new Number('0', '1234567890'); + } + + public function benchConstructorWithFractionalAmount(): void + { + new Number('1234567890', '1234567890'); + } + + public function benchConstructorWithNegativeFractionalAmount(): void + { + new Number('-1234567890', '1234567890'); + } + + public function benchFromStringWithZeroIntegerAmount(): void + { + Number::fromString('0'); + } + + public function benchFromStringWithPositiveIntegerAmount(): void + { + Number::fromString('1234567890'); + } + + public function benchFromStringWithNegativeIntegerAmount(): void + { + Number::fromString('-1234567890'); + } + + public function benchFromStringWithZeroAndFractionalAmount(): void + { + Number::fromString('0.1234567890'); + } + + public function benchFromStringWithFractionalAmount(): void + { + Number::fromString('1234567890.1234567890'); + } + + public function benchFromStringWithNegativeFractionalAmount(): void + { + Number::fromString('-1234567890.1234567890'); + } +} diff --git a/composer.json b/composer.json index ce480a487..9bcf0aed2 100644 --- a/composer.json +++ b/composer.json @@ -24,28 +24,30 @@ } ], "require": { - "php": ">=5.6", + "php": "^8.0", + "ext-bcmath": "*", "ext-json": "*" }, "require-dev": { - "ext-bcmath": "*", "ext-gmp": "*", "ext-intl": "*", - "cache/taggable-cache": "^0.4.0", - "doctrine/instantiator": "^1.0.5", - "florianv/exchanger": "^1.0", - "florianv/swap": "^3.0", - "friends-of-phpspec/phpspec-code-coverage": "^3.1.1 || ^4.3", + "cache/taggable-cache": "^1.1.0", + "doctrine/coding-standard": "^9.0", + "doctrine/instantiator": "^1.4.0", + "florianv/exchanger": "^2.6.3", + "florianv/swap": "^4.3.0", "moneyphp/iso-currencies": "^3.2.1", - "php-http/message": "^1.4", - "php-http/mock-client": "^1.0.0", - "phpspec/phpspec": "^3.4.3", - "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.18 || ^8.5", - "psr/cache": "^1.0", - "symfony/phpunit-bridge": "^4" + "php-http/message": "^1.11.0", + "php-http/mock-client": "^1.4.1", + "phpbench/phpbench": "1.0.0-beta1@BETA", + "phpspec/phpspec": "^7.0.1", + "phpunit/phpunit": "^9.5.4", + "psalm/plugin-phpunit": "^0.15.1", + "psr/cache": "^1.0.1", + "roave/infection-static-analysis-plugin": "^1.7", + "vimeo/psalm": "^4.7" }, "suggest": { - "ext-bcmath": "Calculate without integer limits", "ext-gmp": "Calculate without integer limits", "ext-intl": "Format Money objects with intl", "florianv/exchanger": "Exchange rates library for PHP", @@ -67,6 +69,7 @@ }, "autoload-dev": { "psr-4": { + "Benchmark\\Money\\": "benchmark/", "Tests\\Money\\": "tests/", "spec\\Money\\": "spec/" } @@ -74,13 +77,16 @@ "minimum-stability": "dev", "prefer-stable": true, "scripts": { + "benchmark": [ + "vendor/bin/phpbench run --retry-threshold=3 --iterations=15 --revs=1000 --warmup=2" + ], "clean": "rm -rf build/ vendor/", "test": [ "vendor/bin/phpspec run", - "vendor/bin/phpunit -v" + "vendor/bin/phpunit -v", + "vendor/bin/phpbench run" ], "test-coverage": [ - "vendor/bin/phpspec run -c phpspec.ci.yml", "vendor/bin/phpunit -v --coverage-text --coverage-clover=build/unit_coverage.xml" ], "update-currencies": [ diff --git a/composer.lock b/composer.lock new file mode 100644 index 000000000..949a466f9 --- /dev/null +++ b/composer.lock @@ -0,0 +1,6950 @@ +{ + "_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": "02771e390a25b34fc35bdda89f3caf5c", + "packages": [], + "packages-dev": [ + { + "name": "amphp/amp", + "version": "v2.5.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/amp.git", + "reference": "efca2b32a7580087adb8aabbff6be1dc1bb924a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/amp/zipball/efca2b32a7580087adb8aabbff6be1dc1bb924a9", + "reference": "efca2b32a7580087adb8aabbff6be1dc1bb924a9", + "shasum": "" + }, + "require": { + "php": ">=7" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1", + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^6.0.9 | ^7", + "psalm/phar": "^3.11@dev", + "react/promise": "^2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Amp\\": "lib" + }, + "files": [ + "lib/functions.php", + "lib/Internal/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "http://amphp.org/amp", + "keywords": [ + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" + ], + "support": { + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v2.5.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2021-01-10T17:06:37+00:00" + }, + { + "name": "amphp/byte-stream", + "version": "v1.8.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/byte-stream.git", + "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/acbd8002b3536485c997c4e019206b3f10ca15bd", + "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd", + "shasum": "" + }, + "require": { + "amphp/amp": "^2", + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1.4", + "friendsofphp/php-cs-fixer": "^2.3", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^6 || ^7 || ^8", + "psalm/phar": "^3.11.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Amp\\ByteStream\\": "lib" + }, + "files": [ + "lib/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "http://amphp.org/byte-stream", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" + ], + "support": { + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v1.8.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2021-03-30T17:13:30+00:00" + }, + { + "name": "beberlei/assert", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/beberlei/assert.git", + "reference": "5367e3895976b49704ae671f75bc5f0ba1b986ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/beberlei/assert/zipball/5367e3895976b49704ae671f75bc5f0ba1b986ab", + "reference": "5367e3895976b49704ae671f75bc5f0ba1b986ab", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-intl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "php": "^7.0 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "*", + "phpstan/phpstan": "*", + "phpunit/phpunit": ">=6.0.0", + "yoast/phpunit-polyfills": "^0.1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Assert\\": "lib/Assert" + }, + "files": [ + "lib/Assert/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de", + "role": "Lead Developer" + }, + { + "name": "Richard Quadling", + "email": "rquadling@gmail.com", + "role": "Collaborator" + } + ], + "description": "Thin assertion library for input validation in business models.", + "keywords": [ + "assert", + "assertion", + "validation" + ], + "support": { + "issues": "https://github.com/beberlei/assert/issues", + "source": "https://github.com/beberlei/assert/tree/v3.3.0" + }, + "time": "2020-11-13T20:02:54+00:00" + }, + { + "name": "cache/tag-interop", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-cache/tag-interop.git", + "reference": "909a5df87e698f1665724a9e84851c11c45fbfb9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-cache/tag-interop/zipball/909a5df87e698f1665724a9e84851c11c45fbfb9", + "reference": "909a5df87e698f1665724a9e84851c11c45fbfb9", + "shasum": "" + }, + "require": { + "php": "^5.5 || ^7.0 || ^8.0", + "psr/cache": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Cache\\TagInterop\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/nyholm" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com", + "homepage": "https://github.com/nicolas-grekas" + } + ], + "description": "Framework interoperable interfaces for tags", + "homepage": "https://www.php-cache.com/en/latest/", + "keywords": [ + "cache", + "psr", + "psr6", + "tag" + ], + "support": { + "issues": "https://github.com/php-cache/tag-interop/issues", + "source": "https://github.com/php-cache/tag-interop/tree/1.0.1" + }, + "time": "2020-12-04T14:11:04+00:00" + }, + { + "name": "cache/taggable-cache", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-cache/taggable-cache.git", + "reference": "78a38bbd63648da3ad9595f1f0347a6b21145a8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-cache/taggable-cache/zipball/78a38bbd63648da3ad9595f1f0347a6b21145a8a", + "reference": "78a38bbd63648da3ad9595f1f0347a6b21145a8a", + "shasum": "" + }, + "require": { + "cache/tag-interop": "^1.0", + "php": "^5.6 || ^7.0 || ^8.0", + "psr/cache": "^1.0" + }, + "require-dev": { + "cache/integration-tests": "^0.16", + "phpunit/phpunit": "^5.7.21", + "symfony/cache": "^3.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Cache\\Taggable\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Scherer", + "email": "aequasi@gmail.com", + "homepage": "https://github.com/aequasi" + }, + { + "name": "Magnus Nordlander", + "email": "magnus@fervo.se", + "homepage": "https://github.com/magnusnordlander" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/nyholm" + } + ], + "description": "Add tag support to your PSR-6 cache implementation", + "homepage": "http://www.php-cache.com/en/latest/", + "keywords": [ + "Taggable", + "cache", + "psr6", + "tag" + ], + "support": { + "source": "https://github.com/php-cache/taggable-cache/tree/1.1.0" + }, + "time": "2020-12-14T12:17:39+00:00" + }, + { + "name": "clue/stream-filter", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/clue/stream-filter.git", + "reference": "aeb7d8ea49c7963d3b581378955dbf5bc49aa320" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/stream-filter/zipball/aeb7d8ea49c7963d3b581378955dbf5bc49aa320", + "reference": "aeb7d8ea49c7963d3b581378955dbf5bc49aa320", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\StreamFilter\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "A simple and modern approach to stream filtering in PHP", + "homepage": "https://github.com/clue/php-stream-filter", + "keywords": [ + "bucket brigade", + "callback", + "filter", + "php_user_filter", + "stream", + "stream_filter_append", + "stream_filter_register" + ], + "support": { + "issues": "https://github.com/clue/stream-filter/issues", + "source": "https://github.com/clue/stream-filter/tree/v1.5.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2020-10-02T12:38:20+00:00" + }, + { + "name": "composer/semver", + "version": "3.2.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/a02fdf930a3c1c3ed3a49b5f63859c0c20e10464", + "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.54", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.2.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-11-13T08:59:24+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "1.4.6", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "f27e06cd9675801df441b3656569b328e04aa37c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/f27e06cd9675801df441b3656569b328e04aa37c", + "reference": "f27e06cd9675801df441b3656569b328e04aa37c", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0", + "psr/log": "^1.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.55", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/1.4.6" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2021-03-25T17:01:18+00:00" + }, + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v0.7.1", + "source": { + "type": "git", + "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", + "reference": "fe390591e0241955f22eb9ba327d137e501c771c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/fe390591e0241955f22eb9ba327d137e501c771c", + "reference": "fe390591e0241955f22eb9ba327d137e501c771c", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.0 || ^3.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "*", + "phpcompatibility/php-compatibility": "^9.0", + "sensiolabs/security-checker": "^4.1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "franck.nijhof@dealerdirect.com", + "homepage": "http://www.frenck.nl", + "role": "Developer / IT Manager" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "homepage": "http://www.dealerdirect.com", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", + "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + }, + "time": "2020-12-07T18:04:37+00:00" + }, + { + "name": "dnoegel/php-xdg-base-dir", + "version": "v0.1.1", + "source": { + "type": "git", + "url": "https://github.com/dnoegel/php-xdg-base-dir.git", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "XdgBaseDir\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "implementation of xdg base directory specification for php", + "support": { + "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues", + "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1" + }, + "time": "2019-12-04T15:06:13+00:00" + }, + { + "name": "doctrine/annotations", + "version": "1.12.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "b17c5014ef81d212ac539f07a1001832df1b6d3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/b17c5014ef81d212ac539f07a1001832df1b6d3b", + "reference": "b17c5014ef81d212ac539f07a1001832df1b6d3b", + "shasum": "" + }, + "require": { + "doctrine/lexer": "1.*", + "ext-tokenizer": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/cache": "1.*", + "doctrine/coding-standard": "^6.0 || ^8.1", + "phpstan/phpstan": "^0.12.20", + "phpunit/phpunit": "^7.5 || ^9.1.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/1.12.1" + }, + "time": "2021-02-21T21:00:45+00:00" + }, + { + "name": "doctrine/coding-standard", + "version": "9.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/coding-standard.git", + "reference": "35a2452c6025cb739c3244b3348bcd1604df07d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/coding-standard/zipball/35a2452c6025cb739c3244b3348bcd1604df07d1", + "reference": "35a2452c6025cb739c3244b3348bcd1604df07d1", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", + "php": "^7.1 || ^8.0", + "slevomat/coding-standard": "^7.0.0", + "squizlabs/php_codesniffer": "^3.6.0" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Steve Müller", + "email": "st.mueller@dzh-online.de" + } + ], + "description": "The Doctrine Coding Standard is a set of PHPCS rules applied to all Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/coding-standard.html", + "keywords": [ + "checks", + "code", + "coding", + "cs", + "doctrine", + "rules", + "sniffer", + "sniffs", + "standard", + "style" + ], + "support": { + "issues": "https://github.com/doctrine/coding-standard/issues", + "source": "https://github.com/doctrine/coding-standard/tree/9.0.0" + }, + "time": "2021-04-12T15:11:14+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^8.0", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.4.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2020-11-10T18:47:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "e864bbf5904cb8f5bb334f99209b48018522f042" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042", + "reference": "e864bbf5904cb8f5bb334f99209b48018522f042", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpstan/phpstan": "^0.11.8", + "phpunit/phpunit": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/1.2.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2020-05-25T17:44:05+00:00" + }, + { + "name": "felixfbecker/advanced-json-rpc", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git", + "reference": "06f0b06043c7438959dbdeed8bb3f699a19be22e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/06f0b06043c7438959dbdeed8bb3f699a19be22e", + "reference": "06f0b06043c7438959dbdeed8bb3f699a19be22e", + "shasum": "" + }, + "require": { + "netresearch/jsonmapper": "^1.0 || ^2.0", + "php": "^7.1 || ^8.0", + "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "AdvancedJsonRpc\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "description": "A more advanced JSONRPC implementation", + "support": { + "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues", + "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.0" + }, + "time": "2021-01-10T17:48:47+00:00" + }, + { + "name": "felixfbecker/language-server-protocol", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/felixfbecker/php-language-server-protocol.git", + "reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/9d846d1f5cf101deee7a61c8ba7caa0a975cd730", + "reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpstan/phpstan": "*", + "squizlabs/php_codesniffer": "^3.1", + "vimeo/psalm": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "LanguageServerProtocol\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "description": "PHP classes for the Language Server Protocol", + "keywords": [ + "language", + "microsoft", + "php", + "server" + ], + "support": { + "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/1.5.1" + }, + "time": "2021-02-22T14:02:09+00:00" + }, + { + "name": "florianv/exchanger", + "version": "2.6.3", + "source": { + "type": "git", + "url": "https://github.com/florianv/exchanger.git", + "reference": "3235701a9ff2bb912d011f17d42b8b291ebbd437" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/florianv/exchanger/zipball/3235701a9ff2bb912d011f17d42b8b291ebbd437", + "reference": "3235701a9ff2bb912d011f17d42b8b291ebbd437", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "php": "^7.1.3 || ^8.0", + "php-http/client-implementation": "^1.0", + "php-http/discovery": "^1.6", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0.2", + "psr/simple-cache": "^1.0" + }, + "require-dev": { + "nyholm/psr7": "^1.0", + "php-http/message": "^1.7", + "php-http/mock-client": "^1.0", + "phpunit/phpunit": "^7 || ^8 || ^9.4" + }, + "suggest": { + "php-http/guzzle6-adapter": "Required to use Guzzle for sending HTTP requests", + "php-http/message": "Required to use Guzzle for sending HTTP requests" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Exchanger\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florian Voutzinos", + "email": "florian@voutzinos.com", + "homepage": "https://voutzinos.com" + } + ], + "description": "Currency exchange rates framework for PHP", + "homepage": "https://github.com/florianv/exchanger", + "keywords": [ + "Rate", + "conversion", + "currency", + "exchange rates", + "money" + ], + "support": { + "issues": "https://github.com/florianv/exchanger/issues", + "source": "https://github.com/florianv/exchanger/tree/2.6.3" + }, + "time": "2021-04-11T06:05:03+00:00" + }, + { + "name": "florianv/swap", + "version": "4.3.0", + "source": { + "type": "git", + "url": "https://github.com/florianv/swap.git", + "reference": "88edd27fcb95bdc58bbbf9e4b00539a2843d97fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/florianv/swap/zipball/88edd27fcb95bdc58bbbf9e4b00539a2843d97fd", + "reference": "88edd27fcb95bdc58bbbf9e4b00539a2843d97fd", + "shasum": "" + }, + "require": { + "florianv/exchanger": "^2.0", + "php": "^7.1.3 || ^8.0" + }, + "require-dev": { + "nyholm/psr7": "^1.0", + "php-http/message": "^1.7", + "php-http/mock-client": "^1.0", + "phpunit/phpunit": "^7 || ^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "psr-4": { + "Swap\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florian Voutzinos", + "email": "florian@voutzinos.com", + "homepage": "https://voutzinos.com" + } + ], + "description": "Exchange rates library for PHP", + "keywords": [ + "Rate", + "conversion", + "currency", + "exchange rates", + "money" + ], + "support": { + "issues": "https://github.com/florianv/swap/issues", + "source": "https://github.com/florianv/swap/tree/4.3.0" + }, + "time": "2020-12-28T10:14:12+00:00" + }, + { + "name": "infection/abstract-testframework-adapter", + "version": "0.3.1", + "source": { + "type": "git", + "url": "https://github.com/infection/abstract-testframework-adapter.git", + "reference": "c52539339f28d6b67625ff24496289b3e6d66025" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/infection/abstract-testframework-adapter/zipball/c52539339f28d6b67625ff24496289b3e6d66025", + "reference": "c52539339f28d6b67625ff24496289b3e6d66025", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.8", + "friendsofphp/php-cs-fixer": "^2.16", + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Infection\\AbstractTestFramework\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Maks Rafalko", + "email": "maks.rafalko@gmail.com" + } + ], + "description": "Abstract Test Framework Adapter for Infection", + "support": { + "issues": "https://github.com/infection/abstract-testframework-adapter/issues", + "source": "https://github.com/infection/abstract-testframework-adapter/tree/0.3" + }, + "time": "2020-08-30T13:50:12+00:00" + }, + { + "name": "infection/extension-installer", + "version": "0.1.1", + "source": { + "type": "git", + "url": "https://github.com/infection/extension-installer.git", + "reference": "ff30c0adffcdbc747c96adf92382ccbe271d0afd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/infection/extension-installer/zipball/ff30c0adffcdbc747c96adf92382ccbe271d0afd", + "reference": "ff30c0adffcdbc747c96adf92382ccbe271d0afd", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.1 || ^2.0" + }, + "require-dev": { + "composer/composer": "^1.9", + "friendsofphp/php-cs-fixer": "^2.16", + "infection/infection": "^0.15.2", + "php-coveralls/php-coveralls": "^2.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12.10", + "phpstan/phpstan-phpunit": "^0.12.6", + "phpstan/phpstan-strict-rules": "^0.12.2", + "phpstan/phpstan-webmozart-assert": "^0.12.2", + "phpunit/phpunit": "^8.5", + "vimeo/psalm": "^3.8" + }, + "type": "composer-plugin", + "extra": { + "class": "Infection\\ExtensionInstaller\\Plugin" + }, + "autoload": { + "psr-4": { + "Infection\\ExtensionInstaller\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Maks Rafalko", + "email": "maks.rafalko@gmail.com" + } + ], + "description": "Infection Extension Installer", + "support": { + "issues": "https://github.com/infection/extension-installer/issues", + "source": "https://github.com/infection/extension-installer/tree/0.1.1" + }, + "time": "2020-04-25T22:40:05+00:00" + }, + { + "name": "infection/include-interceptor", + "version": "0.2.4", + "source": { + "type": "git", + "url": "https://github.com/infection/include-interceptor.git", + "reference": "e3cf9317a7fd554ab60a5587f028b16418cc4264" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/infection/include-interceptor/zipball/e3cf9317a7fd554ab60a5587f028b16418cc4264", + "reference": "e3cf9317a7fd554ab60a5587f028b16418cc4264", + "shasum": "" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "infection/infection": "^0.15.0", + "phan/phan": "^2.4 || ^3", + "php-coveralls/php-coveralls": "^2.2", + "phpstan/phpstan": "^0.12.8", + "phpunit/phpunit": "^8.5", + "vimeo/psalm": "^3.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Infection\\StreamWrapper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Maks Rafalko", + "email": "maks.rafalko@gmail.com" + } + ], + "description": "Stream Wrapper: Include Interceptor. Allows to replace included (autoloaded) file with another one.", + "support": { + "issues": "https://github.com/infection/include-interceptor/issues", + "source": "https://github.com/infection/include-interceptor/tree/0.2.4" + }, + "time": "2020-08-07T22:40:37+00:00" + }, + { + "name": "infection/infection", + "version": "0.20.2", + "source": { + "type": "git", + "url": "https://github.com/infection/infection.git", + "reference": "6035c1566af6a5a8d833a276351e5e18ed412305" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/infection/infection/zipball/6035c1566af6a5a8d833a276351e5e18ed412305", + "reference": "6035c1566af6a5a8d833a276351e5e18ed412305", + "shasum": "" + }, + "require": { + "composer/xdebug-handler": "^1.3.3", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "infection/abstract-testframework-adapter": "^0.3.1", + "infection/extension-installer": "^0.1.0", + "infection/include-interceptor": "^0.2.4", + "justinrainbow/json-schema": "^5.2", + "nikic/php-parser": "^4.10.2", + "ocramius/package-versions": "^1.2 || ^2.0", + "ondram/ci-detector": "^3.3.0", + "php": "^7.4 || ^8.0", + "sanmai/pipeline": "^3.1 || ^5.0", + "sebastian/diff": "^3.0.2 || ^4.0", + "seld/jsonlint": "^1.7", + "symfony/console": "^3.4.29 || ^4.1.19 || ^5.0", + "symfony/filesystem": "^3.4.29 || ^4.1.19 || ^5.0", + "symfony/finder": "^3.4.29 || ^4.1.19 || ^5.0", + "symfony/process": "^3.4.29 || ^4.1.19 || ^5.0", + "thecodingmachine/safe": "^1.0", + "webmozart/assert": "^1.3", + "webmozart/path-util": "^2.3" + }, + "conflict": { + "phpunit/php-code-coverage": ">9 <9.1.4", + "symfony/console": "=4.1.5" + }, + "require-dev": { + "ext-simplexml": "*", + "helmich/phpunit-json-assert": "^3.0", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12.8", + "phpstan/phpstan-phpunit": "^0.12.6", + "phpstan/phpstan-strict-rules": "^0.12.5", + "phpstan/phpstan-webmozart-assert": "^0.12.2", + "phpunit/phpunit": "^9.3.11", + "symfony/phpunit-bridge": "^4.4.14 || ^5.1.6", + "symfony/yaml": "^5.0", + "thecodingmachine/phpstan-safe-rule": "^1.0", + "vimeo/psalm": "^4.0" + }, + "bin": [ + "bin/infection" + ], + "type": "library", + "autoload": { + "psr-4": { + "Infection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Maks Rafalko", + "email": "maks.rafalko@gmail.com", + "homepage": "https://twitter.com/maks_rafalko" + }, + { + "name": "Oleg Zhulnev", + "homepage": "https://github.com/sidz" + }, + { + "name": "Gert de Pagter", + "homepage": "https://github.com/BackEndTea" + }, + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com", + "homepage": "https://twitter.com/tfidry" + }, + { + "name": "Alexey Kopytko", + "email": "alexey@kopytko.com", + "homepage": "https://www.alexeykopytko.com" + }, + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Infection is a Mutation Testing framework for PHP. The mutation adequacy score can be used to measure the effectiveness of a test set in terms of its ability to detect faults.", + "keywords": [ + "coverage", + "mutant", + "mutation framework", + "mutation testing", + "testing", + "unit testing" + ], + "support": { + "issues": "https://github.com/infection/infection/issues", + "source": "https://github.com/infection/infection/tree/0.20.2" + }, + "time": "2020-11-20T17:15:57+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "5.2.10", + "source": { + "type": "git", + "url": "https://github.com/justinrainbow/json-schema.git", + "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b", + "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/justinrainbow/json-schema/issues", + "source": "https://github.com/justinrainbow/json-schema/tree/5.2.10" + }, + "time": "2020-05-27T16:41:55+00:00" + }, + { + "name": "moneyphp/iso-currencies", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/moneyphp/iso-currencies.git", + "reference": "53cbf85384577b88226aec7dbe156244895aa9c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/moneyphp/iso-currencies/zipball/53cbf85384577b88226aec7dbe156244895aa9c8", + "reference": "53cbf85384577b88226aec7dbe156244895aa9c8", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "symfony/yaml": "^3.2 | ^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "MoneyPHP\\IsoCurrencies\\": [ + "src" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagi-kazar@gmail.com" + }, + { + "name": "Frederik Bosch", + "email": "f.bosch@genkgo.nl" + } + ], + "description": "Provides up-to-date list of ISO 4217 currencies as provide by the official ISO Maintenance Agency", + "support": { + "issues": "https://github.com/moneyphp/iso-currencies/issues", + "source": "https://github.com/moneyphp/iso-currencies/tree/3.2.1" + }, + "time": "2019-11-06T09:37:02+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.10.2", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "replace": { + "myclabs/deep-copy": "self.version" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "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.10.2" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2020-11-13T09:40:50+00:00" + }, + { + "name": "netresearch/jsonmapper", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/cweiske/jsonmapper.git", + "reference": "e0f1e33a71587aca81be5cffbb9746510e1fe04e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/e0f1e33a71587aca81be5cffbb9746510e1fe04e", + "reference": "e0f1e33a71587aca81be5cffbb9746510e1fe04e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "~4.8.35 || ~5.7 || ~6.4 || ~7.0", + "squizlabs/php_codesniffer": "~3.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JsonMapper": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@cweiske.de", + "homepage": "http://github.com/cweiske/jsonmapper/", + "role": "Developer" + } + ], + "description": "Map nested JSON structures onto PHP classes", + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/cweiske/jsonmapper/tree/master" + }, + "time": "2020-04-16T18:48:43+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.10.4", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "c6d052fc58cb876152f89f532b95a8d7907e7f0e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/c6d052fc58cb876152f89f532b95a8d7907e7f0e", + "reference": "c6d052fc58cb876152f89f532b95a8d7907e7f0e", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-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/v4.10.4" + }, + "time": "2020-12-20T10:01:03+00:00" + }, + { + "name": "ocramius/package-versions", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/Ocramius/PackageVersions.git", + "reference": "f64411e9a63a35f8645d5fe04a9f55a2df2895c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Ocramius/PackageVersions/zipball/f64411e9a63a35f8645d5fe04a9f55a2df2895c9", + "reference": "f64411e9a63a35f8645d5fe04a9f55a2df2895c9", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0.0", + "php": "~8.0.0" + }, + "replace": { + "composer/package-versions-deprecated": "*" + }, + "require-dev": { + "composer/composer": "^2.0.0@dev", + "doctrine/coding-standard": "^8.1.0", + "ext-zip": "^1.15.0", + "infection/infection": "dev-master#8d6c4d6b15ec58d3190a78b7774a5d604ec1075a", + "phpunit/phpunit": "~9.3.11", + "vimeo/psalm": "^4.0.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "PackageVersions\\": "src/PackageVersions" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Provides efficient querying for installed package versions (no runtime IO)", + "support": { + "issues": "https://github.com/Ocramius/PackageVersions/issues", + "source": "https://github.com/Ocramius/PackageVersions/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/Ocramius", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ocramius/package-versions", + "type": "tidelift" + } + ], + "time": "2020-12-23T03:16:25+00:00" + }, + { + "name": "ondram/ci-detector", + "version": "3.5.1", + "source": { + "type": "git", + "url": "https://github.com/OndraM/ci-detector.git", + "reference": "594e61252843b68998bddd48078c5058fe9028bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/OndraM/ci-detector/zipball/594e61252843b68998bddd48078c5058fe9028bd", + "reference": "594e61252843b68998bddd48078c5058fe9028bd", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.2", + "lmc/coding-standard": "^1.3 || ^2.0", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpstan/extension-installer": "^1.0.3", + "phpstan/phpstan": "^0.12.0", + "phpstan/phpstan-phpunit": "^0.12.1", + "phpunit/phpunit": "^7.1 || ^8.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "OndraM\\CiDetector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ondřej Machulda", + "email": "ondrej.machulda@gmail.com" + } + ], + "description": "Detect continuous integration environment and provide unified access to properties of current build", + "keywords": [ + "CircleCI", + "Codeship", + "Wercker", + "adapter", + "appveyor", + "aws", + "aws codebuild", + "bamboo", + "bitbucket", + "buddy", + "ci-info", + "codebuild", + "continuous integration", + "continuousphp", + "drone", + "github", + "gitlab", + "interface", + "jenkins", + "teamcity", + "travis" + ], + "support": { + "issues": "https://github.com/OndraM/ci-detector/issues", + "source": "https://github.com/OndraM/ci-detector/tree/main" + }, + "time": "2020-09-04T11:21:14+00:00" + }, + { + "name": "openlss/lib-array2xml", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/nullivex/lib-array2xml.git", + "reference": "a91f18a8dfc69ffabe5f9b068bc39bb202c81d90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nullivex/lib-array2xml/zipball/a91f18a8dfc69ffabe5f9b068bc39bb202c81d90", + "reference": "a91f18a8dfc69ffabe5f9b068bc39bb202c81d90", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "type": "library", + "autoload": { + "psr-0": { + "LSS": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Bryan Tong", + "email": "bryan@nullivex.com", + "homepage": "https://www.nullivex.com" + }, + { + "name": "Tony Butler", + "email": "spudz76@gmail.com", + "homepage": "https://www.nullivex.com" + } + ], + "description": "Array2XML conversion library credit to lalit.org", + "homepage": "https://www.nullivex.com", + "keywords": [ + "array", + "array conversion", + "xml", + "xml conversion" + ], + "support": { + "issues": "https://github.com/nullivex/lib-array2xml/issues", + "source": "https://github.com/nullivex/lib-array2xml/tree/master" + }, + "time": "2019-03-29T20:06:56+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "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/master" + }, + "time": "2020-06-27T14:33:11+00:00" + }, + { + "name": "phar-io/version", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "bae7c545bef187884426f042434e561ab1ddb182" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182", + "reference": "bae7c545bef187884426f042434e561ab1ddb182", + "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.1.0" + }, + "time": "2021-02-23T14:00:09+00:00" + }, + { + "name": "php-http/client-common", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/client-common.git", + "reference": "e37e46c610c87519753135fb893111798c69076a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/client-common/zipball/e37e46c610c87519753135fb893111798c69076a", + "reference": "e37e46c610c87519753135fb893111798c69076a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/httplug": "^2.0", + "php-http/message": "^1.6", + "php-http/message-factory": "^1.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0", + "symfony/options-resolver": "^2.6 || ^3.4.20 || ~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0", + "symfony/polyfill-php80": "^1.17" + }, + "require-dev": { + "doctrine/instantiator": "^1.1", + "guzzlehttp/psr7": "^1.4", + "nyholm/psr7": "^1.2", + "phpspec/phpspec": "^5.1 || ^6.0", + "phpspec/prophecy": "^1.10.2", + "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3" + }, + "suggest": { + "ext-json": "To detect JSON responses with the ContentTypePlugin", + "ext-libxml": "To detect XML responses with the ContentTypePlugin", + "php-http/cache-plugin": "PSR-6 Cache plugin", + "php-http/logger-plugin": "PSR-3 Logger plugin", + "php-http/stopwatch-plugin": "Symfony Stopwatch plugin" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Client\\Common\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Common HTTP Client implementations and tools for HTTPlug", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "common", + "http", + "httplug" + ], + "support": { + "issues": "https://github.com/php-http/client-common/issues", + "source": "https://github.com/php-http/client-common/tree/2.3.0" + }, + "time": "2020-07-21T10:04:13+00:00" + }, + { + "name": "php-http/discovery", + "version": "1.13.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "788f72d64c43dc361e7fcc7464c3d947c64984a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/788f72d64c43dc361e7fcc7464c3d947c64984a7", + "reference": "788f72d64c43dc361e7fcc7464c3d947c64984a7", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0" + }, + "require-dev": { + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1", + "puli/composer-plugin": "1.0.0-beta10" + }, + "suggest": { + "php-http/message": "Allow to use Guzzle, Diactoros or Slim Framework factories", + "puli/composer-plugin": "Sets up Puli which is recommended for Discovery to work. Check http://docs.php-http.org/en/latest/discovery.html for more details." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds installed HTTPlug implementations and PSR-7 message factories", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.13.0" + }, + "time": "2020-11-27T14:49:42+00:00" + }, + { + "name": "php-http/httplug", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/httplug.git", + "reference": "191a0a1b41ed026b717421931f8d3bd2514ffbf9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/httplug/zipball/191a0a1b41ed026b717421931f8d3bd2514ffbf9", + "reference": "191a0a1b41ed026b717421931f8d3bd2514ffbf9", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/promise": "^1.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.1", + "phpspec/phpspec": "^5.1 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http" + ], + "support": { + "issues": "https://github.com/php-http/httplug/issues", + "source": "https://github.com/php-http/httplug/tree/master" + }, + "time": "2020-07-13T15:43:23+00:00" + }, + { + "name": "php-http/message", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/message.git", + "reference": "fb0dbce7355cad4f4f6a225f537c34d013571f29" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message/zipball/fb0dbce7355cad4f4f6a225f537c34d013571f29", + "reference": "fb0dbce7355cad4f4f6a225f537c34d013571f29", + "shasum": "" + }, + "require": { + "clue/stream-filter": "^1.5", + "php": "^7.1 || ^8.0", + "php-http/message-factory": "^1.0.2", + "psr/http-message": "^1.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.6", + "ext-zlib": "*", + "guzzlehttp/psr7": "^1.0", + "laminas/laminas-diactoros": "^2.0", + "phpspec/phpspec": "^5.1 || ^6.3", + "slim/slim": "^3.0" + }, + "suggest": { + "ext-zlib": "Used with compressor/decompressor streams", + "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", + "laminas/laminas-diactoros": "Used with Diactoros Factories", + "slim/slim": "Used with Slim Framework PSR-7 implementation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Message\\": "src/" + }, + "files": [ + "src/filters.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "HTTP Message related tools", + "homepage": "http://php-http.org", + "keywords": [ + "http", + "message", + "psr-7" + ], + "support": { + "issues": "https://github.com/php-http/message/issues", + "source": "https://github.com/php-http/message/tree/1.11.0" + }, + "time": "2021-02-01T08:54:58+00:00" + }, + { + "name": "php-http/message-factory", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/message-factory.git", + "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message-factory/zipball/a478cb11f66a6ac48d8954216cfed9aa06a501a1", + "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Factory interfaces for PSR-7 HTTP Message", + "homepage": "http://php-http.org", + "keywords": [ + "factory", + "http", + "message", + "stream", + "uri" + ], + "support": { + "issues": "https://github.com/php-http/message-factory/issues", + "source": "https://github.com/php-http/message-factory/tree/master" + }, + "time": "2015-12-19T14:08:53+00:00" + }, + { + "name": "php-http/mock-client", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/mock-client.git", + "reference": "1f89dc2dcfcf7c2aa69a2045fd05d67c52f8b612" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/mock-client/zipball/1f89dc2dcfcf7c2aa69a2045fd05d67c52f8b612", + "reference": "1f89dc2dcfcf7c2aa69a2045fd05d67c52f8b612", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/client-common": "^1.9 || ^2.0", + "php-http/discovery": "^1.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0", + "symfony/polyfill-php80": "^1.17" + }, + "provide": { + "php-http/async-client-implementation": "1.0", + "php-http/client-implementation": "1.0" + }, + "require-dev": { + "phpspec/phpspec": "^5.1 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Mock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "David de Boer", + "email": "david@ddeboer.nl" + } + ], + "description": "Mock HTTP client", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http", + "mock", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/mock-client/issues", + "source": "https://github.com/php-http/mock-client/tree/1.4.1" + }, + "time": "2020-07-14T08:01:01+00:00" + }, + { + "name": "php-http/promise", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/promise.git", + "reference": "4c4c1f9b7289a2ec57cde7f1e9762a5789506f88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/promise/zipball/4c4c1f9b7289a2ec57cde7f1e9762a5789506f88", + "reference": "4c4c1f9b7289a2ec57cde7f1e9762a5789506f88", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2", + "phpspec/phpspec": "^5.1.2 || ^6.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/php-http/promise/issues", + "source": "https://github.com/php-http/promise/tree/1.1.0" + }, + "time": "2020-07-07T09:29:14+00:00" + }, + { + "name": "phpbench/container", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/phpbench/container.git", + "reference": "def10824b6009d31028fa8dc9f73f4b26b234a67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpbench/container/zipball/def10824b6009d31028fa8dc9f73f4b26b234a67", + "reference": "def10824b6009d31028fa8dc9f73f4b26b234a67", + "shasum": "" + }, + "require": { + "psr/container": "^1.0", + "symfony/options-resolver": "^4.2 || ^5.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "phpstan/phpstan": "^0.12.52", + "phpunit/phpunit": "^8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpBench\\DependencyInjection\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Leech", + "email": "daniel@dantleech.com" + } + ], + "description": "Simple, configurable, service container.", + "support": { + "issues": "https://github.com/phpbench/container/issues", + "source": "https://github.com/phpbench/container/tree/2.1.0" + }, + "time": "2021-04-04T07:23:17+00:00" + }, + { + "name": "phpbench/dom", + "version": "0.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpbench/dom.git", + "reference": "a126b32e83d0541f3c89befa1b166ba32d0048ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpbench/dom/zipball/a126b32e83d0541f3c89befa1b166ba32d0048ab", + "reference": "a126b32e83d0541f3c89befa1b166ba32d0048ab", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": "^7.2|^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.0|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.3-dev" + } + }, + "autoload": { + "psr-4": { + "PhpBench\\Dom\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Leech", + "email": "daniel@dantleech.com" + } + ], + "description": "DOM wrapper to simplify working with the PHP DOM implementation", + "support": { + "issues": "https://github.com/phpbench/dom/issues", + "source": "https://github.com/phpbench/dom/tree/0.3.0" + }, + "time": "2020-10-25T08:41:08+00:00" + }, + { + "name": "phpbench/phpbench", + "version": "1.0.0-beta1", + "source": { + "type": "git", + "url": "https://github.com/phpbench/phpbench.git", + "reference": "d3262826d103a4e3ae728e21fc79b05f0fb8f0fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpbench/phpbench/zipball/d3262826d103a4e3ae728e21fc79b05f0fb8f0fd", + "reference": "d3262826d103a4e3ae728e21fc79b05f0fb8f0fd", + "shasum": "" + }, + "require": { + "beberlei/assert": "^2.4 || ^3.0", + "doctrine/annotations": "^1.2.7", + "ext-dom": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "ext-tokenizer": "*", + "php": "^7.2 || ^8.0", + "phpbench/container": "^2.1", + "phpbench/dom": "~0.3.0", + "psr/log": "^1.1", + "seld/jsonlint": "^1.1", + "symfony/console": "^4.2 || ^5.0", + "symfony/filesystem": "^4.2 || ^5.0", + "symfony/finder": "^4.2 || ^5.0", + "symfony/options-resolver": "^4.2 || ^5.0", + "symfony/process": "^4.2 || ^5.0", + "webmozart/path-util": "^2.3" + }, + "require-dev": { + "dantleech/invoke": "^1.2", + "friendsofphp/php-cs-fixer": "^2.13.1", + "jangregor/phpstan-prophecy": "^0.8.1", + "padraic/phar-updater": "^1.0", + "phpspec/prophecy": "^1.12", + "phpstan/phpstan": "^0.12.7", + "phpunit/phpunit": "^8.5.8 || ^9.0", + "symfony/error-handler": "^5.2", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "ext-xdebug": "For Xdebug profiling extension." + }, + "bin": [ + "bin/phpbench" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpBench\\": "lib/", + "PhpBench\\Extensions\\XDebug\\": "extensions/xdebug/lib/", + "PhpBench\\Extensions\\Reports\\": "extensions/reports/lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Leech", + "email": "daniel@dantleech.com" + } + ], + "description": "PHP Benchmarking Framework", + "support": { + "issues": "https://github.com/phpbench/phpbench/issues", + "source": "https://github.com/phpbench/phpbench/tree/1.0.0-beta1" + }, + "time": "2021-04-13T15:31:36+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.2.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master" + }, + "time": "2020-09-03T19:13:55+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0" + }, + "time": "2020-09-17T18:55:26+00:00" + }, + { + "name": "phpspec/php-diff", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/phpspec/php-diff.git", + "reference": "fc1156187f9f6c8395886fe85ed88a0a245d72e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/php-diff/zipball/fc1156187f9f6c8395886fe85ed88a0a245d72e9", + "reference": "fc1156187f9f6c8395886fe85ed88a0a245d72e9", + "shasum": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Diff": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Chris Boulton", + "homepage": "http://github.com/chrisboulton" + } + ], + "description": "A comprehensive library for generating differences between two hashable objects (strings or arrays).", + "support": { + "source": "https://github.com/phpspec/php-diff/tree/v1.1.3" + }, + "time": "2020-09-18T13:47:07+00:00" + }, + { + "name": "phpspec/phpspec", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/phpspec/phpspec.git", + "reference": "26e814250e76711235a30b6f1a7695181934f230" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/phpspec/zipball/26e814250e76711235a30b6f1a7695181934f230", + "reference": "26e814250e76711235a30b6f1a7695181934f230", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.5", + "ext-tokenizer": "*", + "php": "^7.3 || 8.0.*", + "phpspec/php-diff": "^1.0.0", + "phpspec/prophecy": "^1.9", + "sebastian/exporter": "^3.0 || ^4.0", + "symfony/console": "^3.4 || ^4.4 || ^5.0", + "symfony/event-dispatcher": "^3.4 || ^4.4 || ^5.0", + "symfony/finder": "^3.4 || ^4.4 || ^5.0", + "symfony/process": "^3.4 || ^4.4 || ^5.0", + "symfony/yaml": "^3.4 || ^4.4 || ^5.0" + }, + "conflict": { + "sebastian/comparator": "<1.2.4" + }, + "require-dev": { + "behat/behat": "^3.3", + "phpunit/phpunit": "^8.0 || ^9.0", + "symfony/filesystem": "^3.4 || ^4.0 || ^5.0" + }, + "suggest": { + "phpspec/nyan-formatters": "Adds Nyan formatters" + }, + "bin": [ + "bin/phpspec" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "PhpSpec": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "homepage": "http://marcelloduarte.net/" + }, + { + "name": "Ciaran McNulty", + "homepage": "https://ciaranmcnulty.com/" + } + ], + "description": "Specification-oriented BDD framework for PHP 7.1+", + "homepage": "http://phpspec.net/", + "keywords": [ + "BDD", + "SpecBDD", + "TDD", + "spec", + "specification", + "testing", + "tests" + ], + "support": { + "issues": "https://github.com/phpspec/phpspec/issues", + "source": "https://github.com/phpspec/phpspec/tree/7.0.1" + }, + "time": "2020-12-29T14:51:04+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "1.13.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea", + "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2", + "php": "^7.2 || ~8.0, <8.1", + "phpdocumentor/reflection-docblock": "^5.2", + "sebastian/comparator": "^3.0 || ^4.0", + "sebastian/recursion-context": "^3.0 || ^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^6.0", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/1.13.0" + }, + "time": "2021-03-17T13:42:18+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "0.5.4", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "e352d065af1ae9b41c12d1dfd309e90f7b1f55c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/e352d065af1ae9b41c12d1dfd309e90f7b1f55c9", + "reference": "e352d065af1ae9b41c12d1dfd309e90f7b1f55c9", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phing/phing": "^2.16.3", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12.60", + "phpstan/phpstan-strict-rules": "^0.12.5", + "phpunit/phpunit": "^7.5.20", + "symfony/process": "^5.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.5-dev" + } + }, + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/0.5.4" + }, + "time": "2021-04-03T14:46:19+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "f6293e1b30a2354e8428e004689671b83871edde" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f6293e1b30a2354e8428e004689671b83871edde", + "reference": "f6293e1b30a2354e8428e004689671b83871edde", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.10.2", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "*", + "ext-xdebug": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-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", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-03-28T07:26:59+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/aa4be8575f26070b100fccb67faabb28f21f66f8", + "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "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", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:57:25+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.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", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "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": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.5.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "c73c6737305e779771147af66c96ca6a7ed8a741" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c73c6737305e779771147af66c96ca6a7ed8a741", + "reference": "c73c6737305e779771147af66c96ca6a7ed8a741", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.1", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpspec/prophecy": "^1.12.1", + "phpunit/php-code-coverage": "^9.2.3", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.5", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.3", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^2.3", + "sebastian/version": "^3.0.2" + }, + "require-dev": { + "ext-pdo": "*", + "phpspec/prophecy-phpunit": "^2.0.1" + }, + "suggest": { + "ext-soap": "*", + "ext-xdebug": "*" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.5-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ], + "files": [ + "src/Framework/Assert/Functions.php" + ] + }, + "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", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.4" + }, + "funding": [ + { + "url": "https://phpunit.de/donate.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-03-23T07:16:29+00:00" + }, + { + "name": "psalm/plugin-phpunit", + "version": "0.15.1", + "source": { + "type": "git", + "url": "https://github.com/psalm/psalm-plugin-phpunit.git", + "reference": "30ca25ce069bf4943c36e59b7df6954f6af05e64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/psalm/psalm-plugin-phpunit/zipball/30ca25ce069bf4943c36e59b7df6954f6af05e64", + "reference": "30ca25ce069bf4943c36e59b7df6954f6af05e64", + "shasum": "" + }, + "require": { + "composer/package-versions-deprecated": "^1.10", + "composer/semver": "^1.4 || ^2.0 || ^3.0", + "ext-simplexml": "*", + "php": "^7.1 || ^8.0", + "vimeo/psalm": "dev-master || dev-4.x || ^4.0" + }, + "conflict": { + "phpunit/phpunit": "<7.5" + }, + "require-dev": { + "codeception/codeception": "^4.0.3", + "php": "^7.3 || ^8.0", + "phpunit/phpunit": "^7.5 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.3.1", + "weirdan/codeception-psalm-module": "^0.11.0", + "weirdan/prophecy-shim": "^1.0 || ^2.0" + }, + "type": "psalm-plugin", + "extra": { + "psalm": { + "pluginClass": "Psalm\\PhpUnitPlugin\\Plugin" + } + }, + "autoload": { + "psr-4": { + "Psalm\\PhpUnitPlugin\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Brown", + "email": "github@muglug.com" + } + ], + "description": "Psalm plugin for PHPUnit", + "support": { + "issues": "https://github.com/psalm/psalm-plugin-phpunit/issues", + "source": "https://github.com/psalm/psalm-plugin-phpunit/tree/0.15.1" + }, + "time": "2021-01-23T00:19:07+00:00" + }, + { + "name": "psr/cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/master" + }, + "time": "2016-08-06T20:24:11+00:00" + }, + { + "name": "psr/container", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.1" + }, + "time": "2021-03-05T17:36:06+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client/tree/master" + }, + "time": "2020-06-29T06:28:15+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/master" + }, + "time": "2019-04-30T12:38:16+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/log", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.3" + }, + "time": "2020-03-23T09:12:05+00:00" + }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/master" + }, + "time": "2017-10-23T01:57:42+00:00" + }, + { + "name": "roave/infection-static-analysis-plugin", + "version": "1.7.1", + "source": { + "type": "git", + "url": "https://github.com/Roave/infection-static-analysis-plugin.git", + "reference": "36d171236fa44b485538c88f56fd1f536b903036" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Roave/infection-static-analysis-plugin/zipball/36d171236fa44b485538c88f56fd1f536b903036", + "reference": "36d171236fa44b485538c88f56fd1f536b903036", + "shasum": "" + }, + "require": { + "infection/infection": "0.20.2", + "ocramius/package-versions": "^1.9.0 || ^2.0.0", + "php": "~7.4.7|~8.0.0", + "vimeo/psalm": "^4.3.2" + }, + "require-dev": { + "doctrine/coding-standard": "^8.2.0", + "phpunit/phpunit": "^9.5.0" + }, + "bin": [ + "bin/roave-infection-static-analysis-plugin" + ], + "type": "library", + "autoload": { + "psr-4": { + "Roave\\InfectionStaticAnalysis\\": "src/Roave/InfectionStaticAnalysis" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Static analysis on top of mutation testing - prevents escaped mutants from being invalid according to static analysis", + "support": { + "issues": "https://github.com/Roave/infection-static-analysis-plugin/issues", + "source": "https://github.com/Roave/infection-static-analysis-plugin/tree/1.7.1" + }, + "time": "2021-03-04T16:33:09+00:00" + }, + { + "name": "sanmai/pipeline", + "version": "v5.1.0", + "source": { + "type": "git", + "url": "https://github.com/sanmai/pipeline.git", + "reference": "f935e10ddcb758c89829e7b69cfb1dc2b2638518" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sanmai/pipeline/zipball/f935e10ddcb758c89829e7b69cfb1dc2b2638518", + "reference": "f935e10ddcb758c89829e7b69cfb1dc2b2638518", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.8", + "friendsofphp/php-cs-fixer": "^2.16", + "infection/infection": ">=0.10.5", + "league/pipeline": "^1.0 || ^0.3", + "phan/phan": "^1.1 || ^2.0 || ^3.0", + "php-coveralls/php-coveralls": "^2.4.1", + "phpstan/phpstan": ">=0.10", + "phpunit/phpunit": "^7.4 || ^8.1 || ^9.4", + "vimeo/psalm": "^2.0 || ^3.0 || ^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "v5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Pipeline\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Alexey Kopytko", + "email": "alexey@kopytko.com" + } + ], + "description": "General-purpose collections pipeline", + "support": { + "issues": "https://github.com/sanmai/pipeline/issues", + "source": "https://github.com/sanmai/pipeline/tree/v5.1.0" + }, + "funding": [ + { + "url": "https://github.com/sanmai", + "type": "github" + } + ], + "time": "2020-10-25T15:20:56+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.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", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:08:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", + "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.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": "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", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:49:45+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.7", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.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", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:52:27+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.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", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:10:38+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "388b6ced16caa751030f6a69e588299fa09200ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac", + "reference": "388b6ced16caa751030f6a69e588299fa09200ac", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-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": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:52:38+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d89cc98761b8cb5a1a235a6b703ae50d34080e65", + "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.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": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:24:23+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "a90ccbddffa067b51f574dea6eb25d5680839455" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/a90ccbddffa067b51f574dea6eb25d5680839455", + "reference": "a90ccbddffa067b51f574dea6eb25d5680839455", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "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": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:55:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.6", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.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", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-28T06:42:11+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "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", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.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", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.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": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:17:30+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.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 a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:45:17+00:00" + }, + { + "name": "sebastian/type", + "version": "2.3.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/81cd61ab7bbf2de744aba0ea61fae32f721df3d2", + "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3-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", + "source": "https://github.com/sebastianbergmann/type/tree/2.3.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:18:59+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library 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", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "seld/jsonlint", + "version": "1.8.3", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "9ad6ce79c342fbd44df10ea95511a1b24dee5b57" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9ad6ce79c342fbd44df10ea95511a1b24dee5b57", + "reference": "9ad6ce79c342fbd44df10ea95511a1b24dee5b57", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "support": { + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.8.3" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2020-11-11T09:19:24+00:00" + }, + { + "name": "slevomat/coding-standard", + "version": "7.0.3", + "source": { + "type": "git", + "url": "https://github.com/slevomat/coding-standard.git", + "reference": "ce75dc28e6c4c9850844bdfd3b5641973ad6e721" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/ce75dc28e6c4c9850844bdfd3b5641973ad6e721", + "reference": "ce75dc28e6c4c9850844bdfd3b5641973ad6e721", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", + "php": "^7.1 || ^8.0", + "phpstan/phpdoc-parser": "0.5.1 - 0.5.4", + "squizlabs/php_codesniffer": "^3.6.0" + }, + "require-dev": { + "phing/phing": "2.16.4", + "php-parallel-lint/php-parallel-lint": "1.3.0", + "phpstan/phpstan": "0.12.83", + "phpstan/phpstan-deprecation-rules": "0.12.6", + "phpstan/phpstan-phpunit": "0.12.18", + "phpstan/phpstan-strict-rules": "0.12.9", + "phpunit/phpunit": "7.5.20|8.5.5|9.5.4" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "SlevomatCodingStandard\\": "SlevomatCodingStandard" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "support": { + "issues": "https://github.com/slevomat/coding-standard/issues", + "source": "https://github.com/slevomat/coding-standard/tree/7.0.3" + }, + "funding": [ + { + "url": "https://github.com/kukulich", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", + "type": "tidelift" + } + ], + "time": "2021-04-16T12:07:02+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.6.0", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "ffced0d2c8fa8e6cdc4d695a743271fab6c38625" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ffced0d2c8fa8e6cdc4d695a743271fab6c38625", + "reference": "ffced0d2c8fa8e6cdc4d695a743271fab6c38625", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards" + ], + "support": { + "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + }, + "time": "2021-04-09T00:54:41+00:00" + }, + { + "name": "symfony/console", + "version": "v5.2.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/35f039df40a3b335ebf310f244cb242b3a83ac8d", + "reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.8", + "symfony/polyfill-php80": "^1.15", + "symfony/service-contracts": "^1.1|^2", + "symfony/string": "^5.1" + }, + "conflict": { + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "^4.4|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/event-dispatcher": "^4.4|^5.0", + "symfony/lock": "^4.4|^5.0", + "symfony/process": "^4.4|^5.0", + "symfony/var-dumper": "^4.4|^5.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v5.2.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-03-28T09:42:18+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5fa56b4074d1ae755beb55617ddafe6f5d78f665", + "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/master" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-09-07T11:33:47+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v5.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "d08d6ec121a425897951900ab692b612a61d6240" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d08d6ec121a425897951900ab692b612a61d6240", + "reference": "d08d6ec121a425897951900ab692b612a61d6240", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", + "symfony/event-dispatcher-contracts": "^2", + "symfony/polyfill-php80": "^1.15" + }, + "conflict": { + "symfony/dependency-injection": "<4.4" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "^4.4|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/error-handler": "^4.4|^5.0", + "symfony/expression-language": "^4.4|^5.0", + "symfony/http-foundation": "^4.4|^5.0", + "symfony/service-contracts": "^1.1|^2", + "symfony/stopwatch": "^4.4|^5.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v5.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-02-18T17:12:37+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "0ba7d54483095a198fa51781bc608d17e84dffa2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/0ba7d54483095a198fa51781bc608d17e84dffa2", + "reference": "0ba7d54483095a198fa51781bc608d17e84dffa2", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/event-dispatcher": "^1" + }, + "suggest": { + "symfony/event-dispatcher-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-09-07T11:33:47+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v5.2.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "8c86a82f51658188119e62cff0a050a12d09836f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/8c86a82f51658188119e62cff0a050a12d09836f", + "reference": "8c86a82f51658188119e62cff0a050a12d09836f", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v5.2.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-03-28T14:30:26+00:00" + }, + { + "name": "symfony/finder", + "version": "v5.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "0d639a0943822626290d169965804f79400e6a04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/0d639a0943822626290d169965804f79400e6a04", + "reference": "0d639a0943822626290d169965804f79400e6a04", + "shasum": "" + }, + "require": { + "php": ">=7.2.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v5.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-02-15T18:55:04+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v5.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "5d0f633f9bbfcf7ec642a2b5037268e61b0a62ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/5d0f633f9bbfcf7ec642a2b5037268e61b0a62ce", + "reference": "5d0f633f9bbfcf7ec642a2b5037268e61b0a62ce", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", + "symfony/polyfill-php73": "~1.0", + "symfony/polyfill-php80": "^1.15" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v5.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-27T12:56:27+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-07T16:49:33+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5601e09b69f26c1828b13b6bb87cb07cddba3170", + "reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-22T09:19:47+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/43a0283138253ed1d48d352ab6d0bdb3f809f248", + "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-22T09:19:47+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "5232de97ee3b75b0360528dae24e73db49566ab1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1", + "reference": "5232de97ee3b75b0360528dae24e73db49566ab1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-22T09:19:47+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-07T16:49:33+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91", + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-07T16:49:33+00:00" + }, + { + "name": "symfony/process", + "version": "v5.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/313a38f09c77fbcdc1d223e57d368cea76a2fd2f", + "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.15" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v5.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-27T10:15:41+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1", + "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.0" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/master" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-09-07T11:33:47+00:00" + }, + { + "name": "symfony/string", + "version": "v5.2.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", + "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "~1.15" + }, + "require-dev": { + "symfony/error-handler": "^4.4|^5.0", + "symfony/http-client": "^4.4|^5.0", + "symfony/translation-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.4|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "files": [ + "Resources/functions.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v5.2.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-03-17T17:12:15+00:00" + }, + { + "name": "symfony/yaml", + "version": "v4.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "3871c720871029f008928244e56cf43497da7e9d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/3871c720871029f008928244e56cf43497da7e9d", + "reference": "3871c720871029f008928244e56cf43497da7e9d", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/console": "<3.4" + }, + "require-dev": { + "symfony/console": "^3.4|^4.0|^5.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v4.4.21" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-03-05T17:58:50+00:00" + }, + { + "name": "thecodingmachine/safe", + "version": "v1.3.3", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/a8ab0876305a4cdaef31b2350fcb9811b5608dbc", + "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "phpstan/phpstan": "^0.12", + "squizlabs/php_codesniffer": "^3.2", + "thecodingmachine/phpstan-strict-rules": "^0.12" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.1-dev" + } + }, + "autoload": { + "psr-4": { + "Safe\\": [ + "lib/", + "deprecated/", + "generated/" + ] + }, + "files": [ + "deprecated/apc.php", + "deprecated/libevent.php", + "deprecated/mssql.php", + "deprecated/stats.php", + "lib/special_cases.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/ingres-ii.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/msql.php", + "generated/mysql.php", + "generated/mysqli.php", + "generated/mysqlndMs.php", + "generated/mysqlndQc.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/password.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pdf.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/simplexml.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v1.3.3" + }, + "time": "2020-10-28T17:51:34+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "75a63c33a8577608444246075ea0af0d052e452a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", + "reference": "75a63c33a8577608444246075ea0af0d052e452a", + "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/master" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2020-07-12T23:59:07+00:00" + }, + { + "name": "vimeo/psalm", + "version": "4.7.0", + "source": { + "type": "git", + "url": "https://github.com/vimeo/psalm.git", + "reference": "d4377c0baf3ffbf0b1ec6998e8d1be2a40971005" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/d4377c0baf3ffbf0b1ec6998e8d1be2a40971005", + "reference": "d4377c0baf3ffbf0b1ec6998e8d1be2a40971005", + "shasum": "" + }, + "require": { + "amphp/amp": "^2.4.2", + "amphp/byte-stream": "^1.5", + "composer/package-versions-deprecated": "^1.8.0", + "composer/semver": "^1.4 || ^2.0 || ^3.0", + "composer/xdebug-handler": "^1.1", + "dnoegel/php-xdg-base-dir": "^0.1.1", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-tokenizer": "*", + "felixfbecker/advanced-json-rpc": "^3.0.3", + "felixfbecker/language-server-protocol": "^1.5", + "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", + "nikic/php-parser": "^4.10.1", + "openlss/lib-array2xml": "^1.0", + "php": "^7.1|^8", + "sebastian/diff": "^3.0 || ^4.0", + "symfony/console": "^3.4.17 || ^4.1.6 || ^5.0", + "webmozart/path-util": "^2.3" + }, + "provide": { + "psalm/psalm": "self.version" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2", + "brianium/paratest": "^4.0||^6.0", + "ext-curl": "*", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpdocumentor/reflection-docblock": "^5", + "phpmyadmin/sql-parser": "5.1.0||dev-master", + "phpspec/prophecy": ">=1.9.0", + "phpunit/phpunit": "^9.0", + "psalm/plugin-phpunit": "^0.13", + "slevomat/coding-standard": "^6.3.11", + "squizlabs/php_codesniffer": "^3.5", + "symfony/process": "^4.3", + "weirdan/phpunit-appveyor-reporter": "^1.0.0", + "weirdan/prophecy-shim": "^1.0 || ^2.0" + }, + "suggest": { + "ext-igbinary": "^2.0.5" + }, + "bin": [ + "psalm", + "psalm-language-server", + "psalm-plugin", + "psalm-refactor", + "psalter" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev", + "dev-3.x": "3.x-dev", + "dev-2.x": "2.x-dev", + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psalm\\": "src/Psalm/" + }, + "files": [ + "src/functions.php", + "src/spl_object_id.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matthew Brown" + } + ], + "description": "A static analysis tool for finding errors in PHP applications", + "keywords": [ + "code", + "inspection", + "php" + ], + "support": { + "issues": "https://github.com/vimeo/psalm/issues", + "source": "https://github.com/vimeo/psalm/tree/4.7.0" + }, + "time": "2021-03-29T03:54:38+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.10.0" + }, + "time": "2021-03-09T10:59:23+00:00" + }, + { + "name": "webmozart/path-util", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/path-util.git", + "reference": "d939f7edc24c9a1bb9c0dee5cb05d8e859490725" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/path-util/zipball/d939f7edc24c9a1bb9c0dee5cb05d8e859490725", + "reference": "d939f7edc24c9a1bb9c0dee5cb05d8e859490725", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "webmozart/assert": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\PathUtil\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "A robust cross-platform utility for normalizing, comparing and modifying file paths.", + "support": { + "issues": "https://github.com/webmozart/path-util/issues", + "source": "https://github.com/webmozart/path-util/tree/2.3.0" + }, + "time": "2015-12-17T08:42:14+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": { + "phpbench/phpbench": 10 + }, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.0", + "ext-bcmath": "*", + "ext-json": "*" + }, + "platform-dev": { + "ext-gmp": "*", + "ext-intl": "*" + }, + "plugin-api-version": "2.0.0" +} diff --git a/doc/concept.rst b/doc/concept.rst index ee35160ee..e2bbd9194 100644 --- a/doc/concept.rst +++ b/doc/concept.rst @@ -5,6 +5,33 @@ This section introduces the concept and basic features of the library .. _immutability: +Type Safety +----------- + +This library abstracts concepts around `Money`, although with minimal runtime validation. +We attempt to leverage PHP 8's type system as much as possible, but sometimes you will +encounter `string` parameters that are documented as being `numeric-string`: should you +encounter these, it means that the contained value **must** be a `string` that passes +an `assert(is_numeric())` check. + +Specifically, be aware that `numeric-string` is used in order to guarantee that large numeric +values (larger than `PHP_INT_MAX` or smaller than `PHP_INT_MIN`), as well as precise fractional +values are not approximated unless requested to do so: cast a `numeric-string` to an `int` or `float` +at your own risk. + +It is **strongly advised** that you use a type-checker when interacting with this library. + +Compatible type-checkers are: + +- https://github.com/vimeo/psalm +- https://github.com/phpstan/phpstan + +.. warning:: + If you fail to guarantee type-safety when interacting with this library, especially around + `numeric-string` passed as parameter to methods of its API, then these values will likely + be accepted, producing late production crashes. Make sure you run a type-checker! + + Immutability ------------ @@ -43,37 +70,6 @@ The correct way of doing operations is: $jimPrice->equals(Money::EUR(2000)); // true -Integer Limit -------------- - -Although in real life it is highly unprobable, you might have to deal with money values greater than -the integer limit of your system (``PHP_INT_MAX`` constant represents the maximum integer value). - -In order to bypass this limit, we introduced `Calculators`. Based on your environment, Money automatically -picks the best internally and globally. The following implementations are available: - -- BC Math (requires `bcmath` extension) -- GMP (requires `gmp` extension) -- Plain integer - -Calculators are checked for availability in the order above. If no suitable Calculator is found -Money silently falls back to the integer implementation. - -Because of PHP's integer limit, money values are stored as string internally and -``Money::getAmount`` also returns string. - -.. code-block:: php - - use Money\Currency; - use Money\Money; - - $hugeAmount = new Money('12345678901234567890', new Currency('USD')); - - -.. note:: - Remember, because of the integer limit in PHP, you should inject a string that represents your huge amount. - - JSON ---- diff --git a/infection.json.dist b/infection.json.dist new file mode 100644 index 000000000..eb3851006 --- /dev/null +++ b/infection.json.dist @@ -0,0 +1,16 @@ +{ + "source": { + "directories": [ + "src" + ] + }, + "logs": { + "text": "php://stderr", + "badge": { + "branch": "master" + }, + "github": true + }, + "minMsi": 78, + "minCoveredMsi": 93 +} diff --git a/phpbench.json b/phpbench.json new file mode 100644 index 000000000..2b3078660 --- /dev/null +++ b/phpbench.json @@ -0,0 +1,4 @@ +{ + "runner.bootstrap": "vendor/autoload.php", + "runner.path": "benchmark" +} diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 000000000..f5bdc25e0 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,32 @@ + + + + + + + + + + + + + ./src + ./static-analysis + ./tests + + + + + + + + + + + + + + diff --git a/phpspec.ci.yml b/phpspec.ci.yml deleted file mode 100644 index 025c767b3..000000000 --- a/phpspec.ci.yml +++ /dev/null @@ -1,11 +0,0 @@ -suites: - money_suite: - namespace: Money - psr4_prefix: Money -formatter.name: pretty -extensions: - FriendsOfPhpSpec\PhpSpec\CodeCoverage\CodeCoverageExtension: - format: - - clover - output: - clover: build/spec_coverage.xml diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon deleted file mode 100644 index da9210d2b..000000000 --- a/phpstan-baseline.neon +++ /dev/null @@ -1,332 +0,0 @@ -parameters: - ignoreErrors: - - - message: "#^Property Money\\\\Calculator\\\\BcMathCalculator\\:\\:\\$scale \\(string\\) does not accept int\\.$#" - count: 1 - path: src/Calculator/BcMathCalculator.php - - - - message: "#^Parameter \\#3 \\$scale of function bccomp expects int, string given\\.$#" - count: 1 - path: src/Calculator/BcMathCalculator.php - - - - message: "#^Parameter \\#3 \\$scale of function bcadd expects int, string given\\.$#" - count: 1 - path: src/Calculator/BcMathCalculator.php - - - - message: "#^PHPDoc tag @param has invalid value \\(\\$amount\\)\\: Unexpected token \"\\$amount\", expected type at offset 46$#" - count: 1 - path: src/Calculator/BcMathCalculator.php - - - - message: "#^PHPDoc tag @param has invalid value \\(\\$subtrahend\\)\\: Unexpected token \"\\$subtrahend\", expected type at offset 68$#" - count: 1 - path: src/Calculator/BcMathCalculator.php - - - - message: "#^Parameter \\#3 \\$scale of function bcsub expects int, string given\\.$#" - count: 1 - path: src/Calculator/BcMathCalculator.php - - - - message: "#^Parameter \\#3 \\$scale of function bcmul expects int, string given\\.$#" - count: 2 - path: src/Calculator/BcMathCalculator.php - - - - message: "#^Method Money\\\\Calculator\\\\BcMathCalculator\\:\\:divide\\(\\) should return string but returns string\\|null\\.$#" - count: 1 - path: src/Calculator/BcMathCalculator.php - - - - message: "#^Parameter \\#3 \\$scale of function bcdiv expects int, string given\\.$#" - count: 2 - path: src/Calculator/BcMathCalculator.php - - - - message: "#^Parameter \\#1 \\$number of method Money\\\\Calculator\\\\BcMathCalculator\\:\\:floor\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/Calculator/BcMathCalculator.php - - - - message: "#^Parameter \\#2 \\$right_operand of function bcdiv expects string, float\\|int\\|string given\\.$#" - count: 1 - path: src/Calculator/BcMathCalculator.php - - - - message: "#^Parameter \\#2 \\$right_operand of function bcmul expects string, float\\|int\\|string given\\.$#" - count: 1 - path: src/Calculator/BcMathCalculator.php - - - - message: "#^Method Money\\\\Calculator\\\\BcMathCalculator\\:\\:mod\\(\\) should return string but returns string\\|null\\.$#" - count: 1 - path: src/Calculator/BcMathCalculator.php - - - - message: "#^Parameter \\#2 \\$right_operand of function bcmod expects string, float\\|int\\|string given\\.$#" - count: 1 - path: src/Calculator/BcMathCalculator.php - - - - message: "#^Property Money\\\\Calculator\\\\GmpCalculator\\:\\:\\$scale \\(string\\) does not accept int\\.$#" - count: 1 - path: src/Calculator/GmpCalculator.php - - - - message: "#^Parameter \\#2 \\$pad_length of function str_pad expects int, string given\\.$#" - count: 2 - path: src/Calculator/GmpCalculator.php - - - - message: "#^PHPDoc tag @param has invalid value \\(\\$number\\)\\: Unexpected token \"\\$number\", expected type at offset 18$#" - count: 1 - path: src/Calculator/GmpCalculator.php - - - - message: "#^Parameter \\#1 \\$number of method Money\\\\Calculator\\\\GmpCalculator\\:\\:absolute\\(\\) expects string, float\\|int\\|string given\\.$#" - count: 1 - path: src/Calculator/GmpCalculator.php - - - - message: "#^Method Money\\\\Calculator\\\\GmpCalculator\\:\\:it_divides_bug538\\(\\) has no return typehint specified\\.$#" - count: 1 - path: src/Calculator/GmpCalculator.php - - - - message: "#^Call to an undefined method Money\\\\Calculator\\\\GmpCalculator\\:\\:assertSame\\(\\)\\.$#" - count: 1 - path: src/Calculator/GmpCalculator.php - - - - message: "#^Call to an undefined method Money\\\\Calculator\\\\GmpCalculator\\:\\:getCalculator\\(\\)\\.$#" - count: 1 - path: src/Calculator/GmpCalculator.php - - - - message: "#^Binary operation \"\\+\" between string and string results in an error\\.$#" - count: 1 - path: src/Calculator/PhpCalculator.php - - - - message: "#^Binary operation \"\\-\" between string and string results in an error\\.$#" - count: 1 - path: src/Calculator/PhpCalculator.php - - - - message: "#^Binary operation \"\\*\" between string and float\\|int\\|string results in an error\\.$#" - count: 2 - path: src/Calculator/PhpCalculator.php - - - - message: "#^Parameter \\#1 \\$amount of method Money\\\\Calculator\\\\PhpCalculator\\:\\:assertIntegerBounds\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: src/Calculator/PhpCalculator.php - - - - message: "#^Parameter \\#1 \\$amount of method Money\\\\Calculator\\\\PhpCalculator\\:\\:castInteger\\(\\) expects int, float\\|int given\\.$#" - count: 2 - path: src/Calculator/PhpCalculator.php - - - - message: "#^Parameter \\#1 \\$number of function ceil expects float, string given\\.$#" - count: 2 - path: src/Calculator/PhpCalculator.php - - - - message: "#^Parameter \\#1 \\$amount of method Money\\\\Calculator\\\\PhpCalculator\\:\\:castInteger\\(\\) expects int, float given\\.$#" - count: 6 - path: src/Calculator/PhpCalculator.php - - - - message: "#^Parameter \\#1 \\$number of function floor expects float, string given\\.$#" - count: 2 - path: src/Calculator/PhpCalculator.php - - - - message: "#^Parameter \\#1 \\$amount of method Money\\\\Calculator\\\\PhpCalculator\\:\\:assertIntegerBounds\\(\\) expects int, string given\\.$#" - count: 1 - path: src/Calculator/PhpCalculator.php - - - - message: "#^Parameter \\#1 \\$number of function round expects float, string given\\.$#" - count: 2 - path: src/Calculator/PhpCalculator.php - - - - message: "#^Parameter \\#1 \\$number of function round expects float, float\\|int\\|string given\\.$#" - count: 1 - path: src/Calculator/PhpCalculator.php - - - - message: "#^Method Money\\\\Calculator\\\\PhpCalculator\\:\\:assertIntegerBounds\\(\\) has no return typehint specified\\.$#" - count: 1 - path: src/Calculator/PhpCalculator.php - - - - message: "#^Method Money\\\\Calculator\\\\PhpCalculator\\:\\:assertInteger\\(\\) has no return typehint specified\\.$#" - count: 1 - path: src/Calculator/PhpCalculator.php - - - - message: "#^Strict comparison using \\=\\=\\= between int and false will always evaluate to false\\.$#" - count: 1 - path: src/Calculator/PhpCalculator.php - - - - message: "#^Interface Money\\\\Currencies extends generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#" - count: 1 - path: src/Currencies.php - - - - message: "#^Strict comparison using \\=\\=\\= between false and true will always evaluate to false\\.$#" - count: 1 - path: src/Currencies/AggregateCurrencies.php - - - - message: "#^Parameter \\#1 \\$it of method AppendIterator\\:\\:append\\(\\) expects Iterator, Traversable\\ given\\.$#" - count: 1 - path: src/Currencies/AggregateCurrencies.php - - - - message: "#^Parameter \\#1 \\$it of class CallbackFilterIterator constructor expects Iterator, Traversable\\ given\\.$#" - count: 1 - path: src/Currencies/CachedCurrencies.php - - - - message: "#^Parameter \\#3 \\$conversionRatio of class Money\\\\CurrencyPair constructor expects float, string given\\.$#" - count: 1 - path: src/Exchange/ExchangerExchange.php - - - - message: "#^Method Money\\\\Exchange\\\\IndirectExchange\\:\\:registerCalculator\\(\\) has no return typehint specified\\.$#" - count: 1 - path: src/Exchange/IndirectExchange.php - - - - message: "#^Cannot instantiate interface Money\\\\Calculator\\.$#" - count: 1 - path: src/Exchange/IndirectExchange.php - - - - message: "#^Parameter \\#3 \\$conversionRatio of class Money\\\\CurrencyPair constructor expects float, string given\\.$#" - count: 1 - path: src/Exchange/SwapExchange.php - - - - message: "#^Strict comparison using \\=\\=\\= between false and true will always evaluate to false\\.$#" - count: 1 - path: src/Formatter/AggregateMoneyFormatter.php - - - - message: "#^Parameter \\#3 \\$length of function substr expects int, int\\|false given\\.$#" - count: 1 - path: src/Formatter/BitcoinMoneyFormatter.php - - - - message: "#^Parameter \\#1 \\$num of method NumberFormatter\\:\\:formatCurrency\\(\\) expects float, string given\\.$#" - count: 1 - path: src/Formatter/IntlMoneyFormatter.php - - - - message: "#^Method Money\\\\Money\\:\\:assertSameCurrency\\(\\) has no return typehint specified\\.$#" - count: 1 - path: src/Money.php - - - - message: "#^PHPDoc tag @param for parameter \\$addends with type array\\ is incompatible with native type Money\\\\Money\\.$#" - count: 1 - path: src/Money.php - - - - message: "#^PHPDoc tag @param for parameter \\$subtrahends with type array\\ is incompatible with native type Money\\\\Money\\.$#" - count: 1 - path: src/Money.php - - - - message: "#^Method Money\\\\Money\\:\\:assertOperand\\(\\) has no return typehint specified\\.$#" - count: 1 - path: src/Money.php - - - - message: "#^Call to function is_object\\(\\) with string will always evaluate to false\\.$#" - count: 1 - path: src/Money.php - - - - message: "#^Method Money\\\\Money\\:\\:assertRoundingMode\\(\\) has no return typehint specified\\.$#" - count: 1 - path: src/Money.php - - - - message: "#^Binary operation \"\\*\" between \\(float\\|int\\) and string results in an error\\.$#" - count: 1 - path: src/Money.php - - - - message: "#^Parameter \\#2 \\$b of method Money\\\\Calculator\\:\\:compare\\(\\) expects string, int given\\.$#" - count: 3 - path: src/Money.php - - - - message: "#^Method Money\\\\Money\\:\\:registerCalculator\\(\\) has no return typehint specified\\.$#" - count: 1 - path: src/Money.php - - - - message: "#^Cannot instantiate interface Money\\\\Calculator\\.$#" - count: 1 - path: src/Money.php - - - - message: "#^Method Money\\\\Number\\:\\:fromString\\(\\) has parameter \\$number with no typehint specified\\.$#" - count: 1 - path: src/Number.php - - - - message: "#^PHPDoc tag @param has invalid value \\(\\$number\\)\\: Unexpected token \"\\$number\", expected type at offset 18$#" - count: 1 - path: src/Number.php - - - - message: "#^Strict comparison using \\=\\=\\= between true and false will always evaluate to false\\.$#" - count: 1 - path: src/Number.php - - - - message: "#^Parameter \\#1 \\$integerPart of class Money\\\\Number constructor expects string, int given\\.$#" - count: 1 - path: src/Number.php - - - - message: "#^Unreachable statement \\- code above always terminates\\.$#" - count: 1 - path: src/Number.php - - - - message: "#^Binary operation \"\\+\" between int and 1\\|string results in an error\\.$#" - count: 1 - path: src/Number.php - - - - message: "#^Method Money\\\\PHPUnit\\\\Comparator\\:\\:assertEquals\\(\\) has no return typehint specified\\.$#" - count: 1 - path: src/PHPUnit/Comparator.php - - - - message: "#^Strict comparison using \\=\\=\\= between false and true will always evaluate to false\\.$#" - count: 1 - path: src/Parser/AggregateMoneyParser.php - - - - message: "#^Strict comparison using \\=\\=\\= between true and false will always evaluate to false\\.$#" - count: 1 - path: src/Parser/BitcoinMoneyParser.php - - - - message: "#^Strict comparison using \\=\\=\\= between false and float will always evaluate to false\\.$#" - count: 1 - path: src/Parser/IntlMoneyParser.php - diff --git a/phpstan.neon.dist b/phpstan.neon.dist deleted file mode 100644 index 2186b5aa9..000000000 --- a/phpstan.neon.dist +++ /dev/null @@ -1,8 +0,0 @@ -includes: - - phpstan-baseline.neon - -parameters: - level: max - checkMissingIterableValueType: false - paths: - - src diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7e37f9f5a..b4d5ed767 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,22 +1,24 @@ - - + + tests/ - - + + src - - - - - - - - - - + + diff --git a/psalm.xml b/psalm.xml index 42b82d7e7..f70033d4c 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,51 +1,67 @@ - + + + + + + - + + + - - - + + + + + + + - - - - - + + + + + + - - - - + + + + + + - - - - - - - - - - + + + + + + + - --> + + + + diff --git a/resources/generate-money-factory.php b/resources/generate-money-factory.php index bc82b3fd0..3323da4ef 100644 --- a/resources/generate-money-factory.php +++ b/resources/generate-money-factory.php @@ -3,16 +3,24 @@ require __DIR__.'/../vendor/autoload.php'; use Money\Currencies; +use Money\Currency; -$buffer = << - * \$fiveDollar = Money::USD(500); + * $fiveDollar = Money::USD(500); * * - * @param string \$method - * @param array \$arguments + * @param string $method + * @param array $arguments + * @psalm-param empty $arguments * - * @return Money + * @throws InvalidArgumentException If amount is not integer(ish). * - * @throws \InvalidArgumentException If amount is not integer(ish) + * @psalm-pure */ - public static function __callStatic(\$method, \$arguments) + public static function __callStatic(string $method, array $arguments): Money { - return new Money(\$arguments[0], new Currency(\$method)); + return new Money($arguments[0], new Currency($method)); } } PHP; -$methodBuffer = ''; + $methodBuffer = ''; -$currencies = new Currencies\AggregateCurrencies([ - new Currencies\ISOCurrencies(), - new Currencies\BitcoinCurrencies(), -]); + $currencies = iterator_to_array(new Currencies\AggregateCurrencies([ + new Currencies\ISOCurrencies(), + new Currencies\BitcoinCurrencies(), + ])); -$currencies = iterator_to_array($currencies); + usort($currencies, static fn (Currency $a, Currency $b): int => strcmp($a->getCode(), $b->getCode())); -usort($currencies, function (\Money\Currency $a, \Money\Currency $b) { - return strcmp($a->getCode(), $b->getCode()); -}); - -/** @var \Money\Currency[] $currencies */ -foreach ($currencies as $currency) { - $methodBuffer .= sprintf(" * @method static Money %s(string|int \$amount)\n", $currency->getCode()); -} + /** @var Currency[] $currencies */ + foreach ($currencies as $currency) { + $methodBuffer .= sprintf(" * @method static Money %s(numeric-string|int \$amount)\n", $currency->getCode()); + } -$buffer = str_replace('PHPDOC', rtrim($methodBuffer), $buffer); + $buffer = str_replace('PHPDOC', rtrim($methodBuffer), $buffer); -file_put_contents(__DIR__.'/../src/MoneyFactory.php', $buffer); + file_put_contents(__DIR__.'/../src/MoneyFactory.php', $buffer); +})(); diff --git a/spec/.php_cs.dist b/spec/.php_cs.dist deleted file mode 100644 index 599607ef3..000000000 --- a/spec/.php_cs.dist +++ /dev/null @@ -1,15 +0,0 @@ -in(__DIR__) -; - -return PhpCsFixer\Config::create() - ->setRules([ - '@Symfony' => true, - 'array_syntax' => ['syntax' => 'short'], - 'yoda_style' => false, - 'visibility_required' => [], - ]) - ->setFinder($finder) -; diff --git a/spec/Calculator/BcMathCalculatorSpec.php b/spec/Calculator/BcMathCalculatorSpec.php index 4e06bb52d..adb16ddcd 100644 --- a/spec/Calculator/BcMathCalculatorSpec.php +++ b/spec/Calculator/BcMathCalculatorSpec.php @@ -1,5 +1,7 @@ shouldHaveType(BcMathCalculator::class); } diff --git a/spec/Calculator/CalculatorBehavior.php b/spec/Calculator/CalculatorBehavior.php index 5a0ac1741..e19df648b 100644 --- a/spec/Calculator/CalculatorBehavior.php +++ b/spec/Calculator/CalculatorBehavior.php @@ -1,9 +1,13 @@ shouldImplement(Calculator::class); } - function it_compares_two_values() + public function it_compares_two_values(): void { $this->compare(2, 1)->shouldReturn(1); $this->compare(1, 2)->shouldReturn(-1); $this->compare(1, 1)->shouldReturn(0); } - function it_adds_two_values() + public function it_adds_two_values(): void { $this->add(rand(-100, 100), rand(-100, 100))->shouldBeString(); } - function it_subtracts_a_value_from_another() + public function it_subtracts_a_value_from_another(): void { $this->subtract(rand(-100, 100), rand(-100, 100))->shouldBeString(); } - function it_multiplies_a_value_by_another() + public function it_multiplies_a_value_by_another(): void { $this->multiply(rand(-100, 100), rand(-100, 100))->shouldBeString(); } - function it_divides_a_value_by_another() + public function it_divides_a_value_by_another(): void { $this->divide(rand(-100, 100), rand(1, 100))->shouldBeString(); } - function it_ceils_a_value() + public function it_ceils_a_value(): void { $this->ceil(rand(-100, 100) / 100)->shouldBeString(); } - function it_floors_a_value() + public function it_floors_a_value(): void { $this->floor(rand(-100, 100) / 100)->shouldBeString(); } - function it_calculates_the_absolute_value() + public function it_calculates_the_absolute_value(): void { $result = $this->absolute(rand(1, 100)); @@ -66,20 +70,21 @@ function it_calculates_the_absolute_value() $result->shouldBeString(); } - function it_shares_a_value() + public function it_shares_a_value(): void { - $this->share(10, 2, 4)->shouldBeString(); + $this->share('10', '2', '4')->shouldBeString(); } - function it_calculates_the_modulus() + public function it_calculates_the_modulus(): void { - $this->mod(11, 5)->shouldBeString(); + $this->mod('11', '5')->shouldBeString(); } - public function getMatchers() + /** {@inheritDoc} */ + public function getMatchers(): array { return [ - 'beGreaterThanZero' => function ($subject) { + 'beGreaterThanZero' => static function ($subject) { return $subject > 0; }, ]; diff --git a/spec/Calculator/GmpCalculatorSpec.php b/spec/Calculator/GmpCalculatorSpec.php index 6097d301c..c17a846fd 100644 --- a/spec/Calculator/GmpCalculatorSpec.php +++ b/spec/Calculator/GmpCalculatorSpec.php @@ -1,5 +1,7 @@ shouldHaveType(GmpCalculator::class); } diff --git a/spec/Calculator/PhpCalculatorSpec.php b/spec/Calculator/PhpCalculatorSpec.php deleted file mode 100644 index da35688b1..000000000 --- a/spec/Calculator/PhpCalculatorSpec.php +++ /dev/null @@ -1,31 +0,0 @@ -shouldHaveType(PhpCalculator::class); - } - - function it_throws_an_exception_when_overflown() - { - $this->shouldThrow(\OverflowException::class)->duringMultiply(PHP_INT_MAX, 2); - } - - function it_throws_an_exception_when_underflown() - { - $this->shouldThrow(\UnderflowException::class)->duringMultiply(~PHP_INT_MAX, 2); - } - - function throws_an_exception_when_the_result_is_not_integer() - { - $this->shouldThrow(\UnexpectedValueException::class)->duringAdd(PHP_INT_MAX, 1); - } -} diff --git a/spec/ConverterSpec.php b/spec/ConverterSpec.php index a0679ab60..d674f26cd 100644 --- a/spec/ConverterSpec.php +++ b/spec/ConverterSpec.php @@ -1,5 +1,7 @@ beConstructedWith($currencies, $exchange); } - function it_is_initializable() + public function it_is_initializable(): void { $this->shouldHaveType(Converter::class); } - function it_converts_to_a_different_currency(Currencies $currencies, Exchange $exchange) + public function it_converts_to_a_different_currency(Currencies $currencies, Exchange $exchange): void { - $baseCurrency = new Currency($baseCurrencyCode = 'ABC'); + $baseCurrency = new Currency($baseCurrencyCode = 'ABC'); $counterCurrency = new Currency($counterCurrencyCode = 'XYZ'); - $pair = new CurrencyPair($baseCurrency, $counterCurrency, 0.5); + $pair = new CurrencyPair($baseCurrency, $counterCurrency, '0.5'); $currencies->subunitFor($baseCurrency)->willReturn(100); $currencies->subunitFor($counterCurrency)->willReturn(100); @@ -43,11 +47,11 @@ function it_converts_to_a_different_currency(Currencies $currencies, Exchange $e $money->getCurrency()->getCode()->shouldBe($counterCurrencyCode); } - function it_converts_using_rounding_modes(Currencies $currencies, Exchange $exchange) + public function it_converts_using_rounding_modes(Currencies $currencies, Exchange $exchange): void { - $baseCurrency = new Currency('EUR'); + $baseCurrency = new Currency('EUR'); $counterCurrency = new Currency('USD'); - $pair = new CurrencyPair($baseCurrency, $counterCurrency, 1.25); + $pair = new CurrencyPair($baseCurrency, $counterCurrency, '1.25'); $currencies->subunitFor($baseCurrency)->willReturn(2); $currencies->subunitFor($counterCurrency)->willReturn(2); diff --git a/spec/Currencies/AggregateCurrenciesSpec.php b/spec/Currencies/AggregateCurrenciesSpec.php deleted file mode 100644 index bde455514..000000000 --- a/spec/Currencies/AggregateCurrenciesSpec.php +++ /dev/null @@ -1,90 +0,0 @@ -beConstructedWith([ - $currencies, - $otherCurrencies, - ]); - } - - function it_is_initializable() - { - $this->shouldHaveType(AggregateCurrencies::class); - } - - function it_is_a_currency_repository() - { - $this->shouldImplement(Currencies::class); - } - - function it_throws_an_exception_when_invalid_currency_repository_is_passed() - { - $this->beConstructedWith(['currencies']); - - $this->shouldThrow(\InvalidArgumentException::class)->duringInstantiation(); - } - - function it_contains_currencies(Currencies $currencies, Currencies $otherCurrencies) - { - $currency = new Currency('EUR'); - - $currencies->contains($currency)->willReturn(false); - $otherCurrencies->contains($currency)->willReturn(true); - - $this->contains($currency)->shouldReturn(true); - } - - function it_might_not_contain_currencies(Currencies $currencies, Currencies $otherCurrencies) - { - $currency = new Currency('EUR'); - - $currencies->contains($currency)->willReturn(false); - $otherCurrencies->contains($currency)->willReturn(false); - - $this->contains($currency)->shouldReturn(false); - } - - function it_provides_subunit(Currencies $currencies, Currencies $otherCurrencies) - { - $currency = new Currency('EUR'); - - $currencies->contains($currency)->willReturn(false); - $otherCurrencies->contains($currency)->willReturn(true); - $otherCurrencies->subunitFor($currency)->willReturn(2); - - $this->subunitFor($currency)->shouldReturn(2); - } - - function it_throws_an_exception_when_providing_subunit_and_currency_is_unknown(Currencies $currencies, Currencies $otherCurrencies) - { - $currency = new Currency('XXXX'); - - $currencies->contains($currency)->willReturn(false); - $otherCurrencies->contains($currency)->willReturn(false); - - $this->shouldThrow(UnknownCurrencyException::class)->duringSubunitFor($currency); - } - - function it_is_iterable(Currencies $currencies, Currencies $otherCurrencies) - { - $currencies->getIterator()->willReturn(new \ArrayIterator([new Currency('EUR')])); - $otherCurrencies->getIterator()->willReturn(new \ArrayIterator([new Currency('USD')])); - - $this->getIterator()->shouldReturnAnInstanceOf(\Traversable::class); - $this->getIterator()->shouldHaveCurrency('EUR'); - $this->getIterator()->shouldHaveCurrency('USD'); - } -} diff --git a/spec/Currencies/BitcoinCurrenciesSpec.php b/spec/Currencies/BitcoinCurrenciesSpec.php index 80c7c4c2a..85aed918f 100644 --- a/spec/Currencies/BitcoinCurrenciesSpec.php +++ b/spec/Currencies/BitcoinCurrenciesSpec.php @@ -1,5 +1,7 @@ shouldHaveType(BitcoinCurrencies::class); } - function it_is_a_currency_repository() + public function it_is_a_currency_repository(): void { $this->shouldImplement(Currencies::class); } - function it_contains_bitcoin() + public function it_contains_bitcoin(): void { $this->contains(new Currency('XBT'))->shouldReturn(true); $this->contains(new Currency('EUR'))->shouldReturn(false); } - function it_is_iterable() + public function it_is_iterable(): void { $this->getIterator()->shouldHaveCurrency('XBT'); } diff --git a/spec/Currencies/CachedCurrenciesSpec.php b/spec/Currencies/CachedCurrenciesSpec.php deleted file mode 100644 index c669eec7a..000000000 --- a/spec/Currencies/CachedCurrenciesSpec.php +++ /dev/null @@ -1,82 +0,0 @@ -beConstructedWith($currencies, $pool); - } - - function it_is_initializable() - { - $this->shouldHaveType(CachedCurrencies::class); - } - - function it_is_a_currency_repository() - { - $this->shouldImplement(Currencies::class); - } - - function it_check_currencies_using_the_delegated_ones( - CacheItemInterface $item, - CacheItemPoolInterface $pool, - Currencies $currencies - ) { - $item->isHit()->willReturn(false); - $item->set(true)->shouldBeCalled(); - $item->get()->willReturn(true); - - $pool->getItem('currency|availability|EUR')->willReturn($item); - $pool->save($item)->shouldBeCalled(); - - $currency = new Currency('EUR'); - - $currencies->contains($currency)->willReturn(true); - - $this->contains($currency)->shouldReturn(true); - } - - function it_checks_currencies_from_the_cache( - CacheItemInterface $item, - CacheItemPoolInterface $pool, - Currencies $currencies - ) { - $item->isHit()->willReturn(true); - $item->set(true)->shouldNotBeCalled(); - $item->get()->willReturn(true); - - $pool->getItem('currency|availability|EUR')->willReturn($item); - $pool->save($item)->shouldNotBeCalled(); - - $currency = new Currency('EUR'); - - $currencies->contains($currency)->shouldNotBeCalled(); - - $this->contains($currency)->shouldReturn(true); - } - - function it_is_iterable( - CacheItemInterface $item, - CacheItemPoolInterface $pool, - Currencies $currencies - ) { - $item->set(true)->shouldBeCalled(); - $pool->save($item)->shouldBeCalled(); - - $pool->getItem('currency|availability|EUR')->willReturn($item); - $currencies->getIterator()->willReturn(new \ArrayIterator([new Currency('EUR')])); - - $this->getIterator()->shouldHaveCurrency('EUR'); - } -} diff --git a/spec/Currencies/CurrencyListSpec.php b/spec/Currencies/CurrencyListSpec.php index f84584d84..1bc814acc 100644 --- a/spec/Currencies/CurrencyListSpec.php +++ b/spec/Currencies/CurrencyListSpec.php @@ -1,5 +1,7 @@ beConstructedWith([ 'MY1' => 2, @@ -20,27 +22,27 @@ function let() ]); } - function it_is_a_currency_repository() + public function it_is_a_currency_repository(): void { $this->shouldImplement(Currencies::class); } - function it_is_initializable() + public function it_is_initializable(): void { $this->shouldHaveType(CurrencyList::class); } - function it_contains_custom_currency() + public function it_contains_custom_currency(): void { $this->contains(new Currency('MY1'))->shouldReturn(true); } - function it_does_not_contain_currency() + public function it_does_not_contain_currency(): void { $this->contains(new Currency('EUR'))->shouldReturn(false); } - function it_is_iterable() + public function it_is_iterable(): void { $this->getIterator()->shouldHaveCurrency('MY1'); } diff --git a/spec/Currencies/ISOCurrenciesSpec.php b/spec/Currencies/ISOCurrenciesSpec.php index a38e171e2..331af8634 100644 --- a/spec/Currencies/ISOCurrenciesSpec.php +++ b/spec/Currencies/ISOCurrenciesSpec.php @@ -1,5 +1,7 @@ shouldHaveType(ISOCurrencies::class); } - function it_is_a_currency_repository() + public function it_is_a_currency_repository(): void { $this->shouldImplement(Currencies::class); } diff --git a/spec/Currencies/Matchers.php b/spec/Currencies/Matchers.php index 6dfdf582a..84abcf86e 100644 --- a/spec/Currencies/Matchers.php +++ b/spec/Currencies/Matchers.php @@ -1,20 +1,25 @@ */ + public function getMatchers(): array { return [ - 'haveCurrency' => function ($subject, $value) { - /** @var Currency $currency */ + 'haveCurrency' => static function (mixed $subject, mixed $value): bool { + assert(is_array($subject)); + foreach ($subject as $currency) { + assert($currency instanceof Currency); if ($currency->getCode() === $value) { return true; } diff --git a/spec/CurrencyPairSpec.php b/spec/CurrencyPairSpec.php index 8a95629d4..ae33466f3 100644 --- a/spec/CurrencyPairSpec.php +++ b/spec/CurrencyPairSpec.php @@ -1,61 +1,51 @@ beConstructedWith(new Currency('EUR'), new Currency('USD'), 1.250000); + $this->beConstructedWith(new Currency('EUR'), new Currency('USD'), '1.250000'); } - function it_is_initializable() + public function it_is_initializable(): void { $this->shouldHaveType(CurrencyPair::class); } - function it_is_json_serializable() + public function it_is_json_serializable(): void { - $this->shouldImplement(\JsonSerializable::class); + $this->shouldImplement(JsonSerializable::class); } - function it_has_currencies_and_ratio() + public function it_has_currencies_and_ratio(): void { - $this->beConstructedWith($base = new Currency('EUR'), $counter = new Currency('USD'), $ratio = 1.0); + $this->beConstructedWith($base = new Currency('EUR'), $counter = new Currency('USD'), $ratio = '1.0'); $this->getBaseCurrency()->shouldReturn($base); $this->getCounterCurrency()->shouldReturn($counter); $this->getConversionRatio()->shouldReturn($ratio); } - function it_throws_an_exception_when_ratio_is_not_numeric() - { - $this->beConstructedWith(new Currency('EUR'), new Currency('USD'), 'NON_NUMERIC'); - - $this->shouldThrow(\InvalidArgumentException::class)->duringInstantiation(); - } - - function it_equals_to_another_currency_pair() + public function it_equals_to_another_currency_pair(): void { - $this->equals(new CurrencyPair(new Currency('GBP'), new Currency('USD'), 1.250000))->shouldReturn(false); - $this->equals(new CurrencyPair(new Currency('EUR'), new Currency('GBP'), 1.250000))->shouldReturn(false); - $this->equals(new CurrencyPair(new Currency('EUR'), new Currency('USD'), 1.5000))->shouldReturn(false); - $this->equals(new CurrencyPair(new Currency('EUR'), new Currency('USD'), 1.250000))->shouldReturn(true); - } - - function it_parses_an_iso_string() - { - $pair = $this->createFromIso('EUR/USD 1.250000'); - - $this->equals($pair)->shouldReturn(true); + $this->equals(new CurrencyPair(new Currency('GBP'), new Currency('USD'), '1.250000'))->shouldReturn(false); + $this->equals(new CurrencyPair(new Currency('EUR'), new Currency('GBP'), '1.250000'))->shouldReturn(false); + $this->equals(new CurrencyPair(new Currency('EUR'), new Currency('USD'), '1.5000'))->shouldReturn(false); + $this->equals(new CurrencyPair(new Currency('EUR'), new Currency('USD'), '1.250000'))->shouldReturn(true); } - function it_throws_an_exception_when_iso_string_cannot_be_parsed() + public function it_throws_an_exception_when_iso_string_cannot_be_parsed(): void { - $this->shouldThrow(\InvalidArgumentException::class)->duringCreateFromIso('1.250000'); + $this->shouldThrow(InvalidArgumentException::class)->duringCreateFromIso('1.250000'); } } diff --git a/spec/CurrencySpec.php b/spec/CurrencySpec.php index aaeaf0c1b..b492b26fb 100644 --- a/spec/CurrencySpec.php +++ b/spec/CurrencySpec.php @@ -1,40 +1,36 @@ beConstructedWith('EUR'); } - function it_is_initializable() + public function it_is_initializable(): void { $this->shouldHaveType(Currency::class); } - function it_is_json_serializable() + public function it_is_json_serializable(): void { - $this->shouldImplement(\JsonSerializable::class); - } - - function it_throws_an_exception_when_code_is_not_string() - { - $this->beConstructedWith(123); - - $this->shouldThrow(\InvalidArgumentException::class)->duringInstantiation(); + $this->shouldImplement(JsonSerializable::class); } - function it_has_a_code() + public function it_has_a_code(): void { $this->getCode()->shouldReturn('EUR'); } - function it_equals_to_a_currency_with_the_same_code() + public function it_equals_to_a_currency_with_the_same_code(): void { $this->equals(new Currency('EUR'))->shouldReturn(true); $this->equals(new Currency('USD'))->shouldReturn(false); diff --git a/spec/Exception/FormatterExceptionSpec.php b/spec/Exception/FormatterExceptionSpec.php index f97849739..b105b3394 100644 --- a/spec/Exception/FormatterExceptionSpec.php +++ b/spec/Exception/FormatterExceptionSpec.php @@ -1,25 +1,28 @@ shouldHaveType(FormatterException::class); } - function it_is_an_exception() + public function it_is_an_exception(): void { $this->shouldHaveType(Exception::class); } - function it_is_a_runtime_exception() + public function it_is_a_runtime_exception(): void { - $this->shouldHaveType(\RuntimeException::class); + $this->shouldHaveType(RuntimeException::class); } } diff --git a/spec/Exception/ParserExceptionSpec.php b/spec/Exception/ParserExceptionSpec.php index 0a3aa2023..b4d7aab5d 100644 --- a/spec/Exception/ParserExceptionSpec.php +++ b/spec/Exception/ParserExceptionSpec.php @@ -1,25 +1,28 @@ shouldHaveType(ParserException::class); } - function it_is_an_exception() + public function it_is_an_exception(): void { $this->shouldHaveType(Exception::class); } - function it_is_a_runtime_exception() + public function it_is_a_runtime_exception(): void { - $this->shouldHaveType(\RuntimeException::class); + $this->shouldHaveType(RuntimeException::class); } } diff --git a/spec/Exception/UnknownCurrencyExceptionSpec.php b/spec/Exception/UnknownCurrencyExceptionSpec.php index c80c6aef3..9170d40e3 100644 --- a/spec/Exception/UnknownCurrencyExceptionSpec.php +++ b/spec/Exception/UnknownCurrencyExceptionSpec.php @@ -1,25 +1,28 @@ shouldHaveType(UnknownCurrencyException::class); } - function it_is_an_exception() + public function it_is_an_exception(): void { $this->shouldHaveType(Exception::class); } - function it_is_a_domain_exception() + public function it_is_a_domain_exception(): void { - $this->shouldHaveType(\DomainException::class); + $this->shouldHaveType(DomainException::class); } } diff --git a/spec/Exception/UnresolvableCurrencyPairExceptionSpec.php b/spec/Exception/UnresolvableCurrencyPairExceptionSpec.php index ce8668552..a559bee47 100644 --- a/spec/Exception/UnresolvableCurrencyPairExceptionSpec.php +++ b/spec/Exception/UnresolvableCurrencyPairExceptionSpec.php @@ -1,7 +1,10 @@ shouldHaveType(UnresolvableCurrencyPairException::class); } - function it_is_an_exception() + public function it_is_an_exception(): void { $this->shouldHaveType(Exception::class); } - function it_is_an_invalid_argument_exception() + public function it_is_an_invalid_argument_exception(): void { - $this->shouldHaveType(\InvalidArgumentException::class); + $this->shouldHaveType(InvalidArgumentException::class); } - function it_accepts_a_currency_pair() + public function it_accepts_a_currency_pair(): void { $this->createFromCurrencies(new Currency('EUR'), new Currency('USD')) ->shouldHaveType(UnresolvableCurrencyPairException::class); diff --git a/spec/Exchange/ExchangerExchangeSpec.php b/spec/Exchange/ExchangerExchangeSpec.php deleted file mode 100644 index a15a5511c..000000000 --- a/spec/Exchange/ExchangerExchangeSpec.php +++ /dev/null @@ -1,59 +0,0 @@ -beConstructedWith($exchanger); - } - - function it_is_initializable() - { - $this->shouldHaveType(ExchangerExchange::class); - } - - function it_is_an_exchange() - { - $this->shouldImplement(Exchange::class); - } - - function it_exchanges_currencies(Exchanger $exchanger, ExchangeRate $exchangeRate) - { - $exchangeRate->getValue()->willReturn('1.0'); - - $query = new ExchangeRateQuery(new ExchangerCurrencyPair('EUR', 'USD')); - $exchanger->getExchangeRate($query)->willReturn($exchangeRate); - - $currencyPair = $this->quote($base = new Currency('EUR'), $counter = new Currency('USD')); - - $currencyPair->shouldHaveType(CurrencyPair::class); - $currencyPair->getBaseCurrency()->shouldReturn($base); - $currencyPair->getCounterCurrency()->shouldReturn($counter); - $currencyPair->getConversionRatio()->shouldReturn(1.0); - } - - function it_throws_an_exception_when_cannot_exchange_currencies(Exchanger $exchanger) - { - $query = new ExchangeRateQuery(new ExchangerCurrencyPair('EUR', 'XYZ')); - $exchanger->getExchangeRate($query)->willThrow(Exception::class); - - $this->shouldThrow(UnresolvableCurrencyPairException::class) - ->duringQuote(new Currency('EUR'), new Currency('XYZ')); - } -} diff --git a/spec/Exchange/FixedExchangeSpec.php b/spec/Exchange/FixedExchangeSpec.php index cc879fe75..d4c82ac22 100644 --- a/spec/Exchange/FixedExchangeSpec.php +++ b/spec/Exchange/FixedExchangeSpec.php @@ -1,5 +1,7 @@ beConstructedWith([ - 'EUR' => [ - 'USD' => 1.25, - ], + 'EUR' => ['USD' => '1.25'], ]); } - function it_is_initializable() + public function it_is_initializable(): void { $this->shouldHaveType(FixedExchange::class); } - function it_is_an_exchange() + public function it_is_an_exchange(): void { $this->shouldImplement(Exchange::class); } - function it_exchanges_currencies() + public function it_exchanges_currencies(): void { - $baseCurrency = new Currency('EUR'); + $baseCurrency = new Currency('EUR'); $counterCurrency = new Currency('USD'); $currencyPair = $this->quote($baseCurrency, $counterCurrency); @@ -40,10 +40,10 @@ function it_exchanges_currencies() $currencyPair->shouldHaveType(CurrencyPair::class); $currencyPair->getBaseCurrency()->shouldReturn($baseCurrency); $currencyPair->getCounterCurrency()->shouldReturn($counterCurrency); - $currencyPair->getConversionRatio()->shouldReturn(1.25); + $currencyPair->getConversionRatio()->shouldReturn('1.25'); } - function it_cannot_exchange_currencies() + public function it_cannot_exchange_currencies(): void { $this->shouldThrow(UnresolvableCurrencyPairException::class) ->duringQuote(new Currency('USD'), new Currency('EUR')); diff --git a/spec/Exchange/ReversedCurrenciesExchangeSpec.php b/spec/Exchange/ReversedCurrenciesExchangeSpec.php deleted file mode 100644 index cf1637449..000000000 --- a/spec/Exchange/ReversedCurrenciesExchangeSpec.php +++ /dev/null @@ -1,69 +0,0 @@ -beConstructedWith($exchange); - } - - function it_is_initializable() - { - $this->shouldHaveType(ReversedCurrenciesExchange::class); - } - - function it_is_an_exchange() - { - $this->shouldImplement(Exchange::class); - } - - function it_exchanges_currencies(Exchange $exchange) - { - $baseCurrency = new Currency('EUR'); - $counterCurrency = new Currency('USD'); - $currencyPair = new CurrencyPair($baseCurrency, $counterCurrency, 1.25); - - $exchange->quote($baseCurrency, $counterCurrency)->willReturn($currencyPair); - - $this->quote($baseCurrency, $counterCurrency)->shouldreturn($currencyPair); - } - - function it_exchanges_reversed_currencies_when_the_original_pair_is_not_found(Exchange $exchange) - { - $baseCurrency = new Currency('USD'); - $counterCurrency = new Currency('EUR'); - $conversionRatio = 1.25; - $currencyPair = new CurrencyPair($counterCurrency, $baseCurrency, $conversionRatio); - - $exchange->quote($baseCurrency, $counterCurrency)->willThrow(UnresolvableCurrencyPairException::class); - $exchange->quote($counterCurrency, $baseCurrency)->willReturn($currencyPair); - - $currencyPair = $this->quote($baseCurrency, $counterCurrency); - - $currencyPair->shouldHaveType(CurrencyPair::class); - $currencyPair->getBaseCurrency()->shouldReturn($baseCurrency); - $currencyPair->getCounterCurrency()->shouldReturn($counterCurrency); - $currencyPair->getConversionRatio()->shouldReturn(1 / $conversionRatio); - } - - function it_throws_an_exception_when_neither_the_original_nor_the_reversed_currency_pair_can_be_resolved(Exchange $exchange) - { - $baseCurrency = new Currency('USD'); - $counterCurrency = new Currency('EUR'); - - // Exceptions are not matched based on identity, but instance and properties - $exchange->quote($baseCurrency, $counterCurrency)->willThrow(UnresolvableCurrencyPairException::createFromCurrencies($baseCurrency, $counterCurrency)); - $exchange->quote($counterCurrency, $baseCurrency)->willThrow(UnresolvableCurrencyPairException::createFromCurrencies($counterCurrency, $baseCurrency)); - - $this->shouldThrow(UnresolvableCurrencyPairException::createFromCurrencies($baseCurrency, $counterCurrency))->duringQuote($baseCurrency, $counterCurrency); - } -} diff --git a/spec/Exchange/SwapExchangeSpec.php b/spec/Exchange/SwapExchangeSpec.php deleted file mode 100644 index 28d0de934..000000000 --- a/spec/Exchange/SwapExchangeSpec.php +++ /dev/null @@ -1,53 +0,0 @@ -beConstructedWith($swap); - } - - function it_is_initializable() - { - $this->shouldHaveType(SwapExchange::class); - } - - function it_is_an_exchange() - { - $this->shouldImplement(Exchange::class); - } - - function it_exchanges_currencies(Swap $swap, ExchangeRate $exchangeRate) - { - $exchangeRate->getValue()->willReturn(1.0); - - $swap->latest('EUR/USD')->willReturn($exchangeRate); - - $currencyPair = $this->quote($base = new Currency('EUR'), $counter = new Currency('USD')); - - $currencyPair->shouldHaveType(CurrencyPair::class); - $currencyPair->getBaseCurrency()->shouldReturn($base); - $currencyPair->getCounterCurrency()->shouldReturn($counter); - $currencyPair->getConversionRatio()->shouldReturn(1.0); - } - - function it_throws_an_exception_when_cannot_exchange_currencies(Swap $swap) - { - $swap->latest('EUR/XYZ')->willThrow(Exception::class); - - $this->shouldThrow(UnresolvableCurrencyPairException::class) - ->duringQuote(new Currency('EUR'), new Currency('XYZ')); - } -} diff --git a/spec/Formatter/AggregateMoneyFormatterSpec.php b/spec/Formatter/AggregateMoneyFormatterSpec.php deleted file mode 100644 index 980c652f7..000000000 --- a/spec/Formatter/AggregateMoneyFormatterSpec.php +++ /dev/null @@ -1,60 +0,0 @@ -beConstructedWith([ - 'EUR' => $moneyFormatter, - ]); - } - - function it_is_initializable() - { - $this->shouldHaveType(AggregateMoneyFormatter::class); - } - - function it_is_a_money_formatter() - { - $this->shouldImplement(MoneyFormatter::class); - } - - function it_formats_money(MoneyFormatter $moneyFormatter) - { - $money = new Money(1, new Currency('EUR')); - - $moneyFormatter->format($money)->willReturn('€1.00'); - - $this->format($money)->shouldReturn('€1.00'); - } - - function it_throws_an_exception_when_no_formatter_for_currency_is_found() - { - $money = new Money(1, new Currency('USD')); - - $this->shouldThrow(FormatterException::class)->duringFormat($money); - } - - function it_uses_default_formatter_when_no_specific_one_is_found(MoneyFormatter $moneyFormatter, MoneyFormatter $moneyFormatter2) - { - $this->beConstructedWith([ - 'USD' => $moneyFormatter, - '*' => $moneyFormatter2, - ]); - - $money = new Money(1, new Currency('EUR')); - - $moneyFormatter2->format($money)->willReturn('€1.00'); - - $this->format($money)->shouldReturn('€1.00'); - } -} diff --git a/spec/Formatter/BitcoinMoneyFormatterSpec.php b/spec/Formatter/BitcoinMoneyFormatterSpec.php index 45316c2dc..463373879 100644 --- a/spec/Formatter/BitcoinMoneyFormatterSpec.php +++ b/spec/Formatter/BitcoinMoneyFormatterSpec.php @@ -1,5 +1,7 @@ beConstructedWith(2, $bitcoinCurrencies); } - function it_is_initializable() + public function it_is_initializable(): void { $this->shouldHaveType(BitcoinMoneyFormatter::class); } - function it_is_a_money_formatter() + public function it_is_a_money_formatter(): void { $this->shouldImplement(MoneyFormatter::class); } - function it_formats_money(Currencies $bitcoinCurrencies) + public function it_formats_money(Currencies $bitcoinCurrencies): void { $this->beConstructedWith(1, $bitcoinCurrencies); $currency = new Currency('XBT'); - $money = new Money(1000000, $currency); + $money = new Money(1000000, $currency); $bitcoinCurrencies->subunitFor($currency)->willReturn(8); @@ -42,7 +44,7 @@ function it_formats_money(Currencies $bitcoinCurrencies) $formatted->shouldContain(Currencies\BitcoinCurrencies::SYMBOL); } - function it_throws_an_exception_when_currency_is_not_bitcoin() + public function it_throws_an_exception_when_currency_is_not_bitcoin(): void { $money = new Money(5, new Currency('USD')); diff --git a/spec/Formatter/DecimalMoneyFormatterSpec.php b/spec/Formatter/DecimalMoneyFormatterSpec.php index 28ca50d0b..c42fe23bd 100644 --- a/spec/Formatter/DecimalMoneyFormatterSpec.php +++ b/spec/Formatter/DecimalMoneyFormatterSpec.php @@ -1,5 +1,7 @@ beConstructedWith($currencies); } - function it_is_initializable() + public function it_is_initializable(): void { $this->shouldHaveType(DecimalMoneyFormatter::class); } - function it_is_a_money_formatter() + public function it_is_a_money_formatter(): void { $this->shouldImplement(MoneyFormatter::class); } - function it_formats_money(Currencies $currencies) + public function it_formats_money(Currencies $currencies): void { $money = new Money(100, new Currency('EUR')); diff --git a/spec/Formatter/IntlMoneyFormatterSpec.php b/spec/Formatter/IntlMoneyFormatterSpec.php index 3748a9d3b..202b96bbc 100644 --- a/spec/Formatter/IntlMoneyFormatterSpec.php +++ b/spec/Formatter/IntlMoneyFormatterSpec.php @@ -1,5 +1,7 @@ beConstructedWith($numberFormatter, $currencies); } - function it_is_initializable() + public function it_is_initializable(): void { $this->shouldHaveType(IntlMoneyFormatter::class); } - function it_is_a_money_formatter() + public function it_is_a_money_formatter(): void { $this->shouldImplement(MoneyFormatter::class); } - function it_formats_money(\NumberFormatter $numberFormatter, Currencies $currencies) + public function it_formats_money(NumberFormatter $numberFormatter, Currencies $currencies): void { $money = new Money(1, new Currency('EUR')); diff --git a/spec/MoneySpec.php b/spec/MoneySpec.php index f63b16a8c..1cce94f9d 100644 --- a/spec/MoneySpec.php +++ b/spec/MoneySpec.php @@ -1,46 +1,50 @@ setAccessible(true); - $reflection->setValue(null, $calculator->getWrappedObject()); - $this->beConstructedWith(self::AMOUNT, new Currency(self::CURRENCY)); } - function it_is_initializable() + public function it_is_initializable(): void { $this->shouldHaveType(Money::class); } - function it_is_json_serializable() + public function it_is_json_serializable(): void { - $this->shouldImplement(\JsonSerializable::class); + $this->shouldImplement(JsonSerializable::class); } - function it_has_an_amount() + public function it_has_an_amount(): void { $this->getAmount()->shouldBeLike(self::AMOUNT); } - function it_has_a_currency() + public function it_has_a_currency(): void { $currency = $this->getCurrency(); @@ -48,356 +52,72 @@ function it_has_a_currency() $currency->equals(new Currency(self::CURRENCY))->shouldReturn(true); } - function it_throws_an_exception_when_amount_is_not_numeric() + public function it_throws_an_exception_when_amount_is_not_numeric(): void { $this->beConstructedWith('ONE', new Currency(self::CURRENCY)); - $this->shouldThrow(\InvalidArgumentException::class)->duringInstantiation(); + $this->shouldThrow(InvalidArgumentException::class)->duringInstantiation(); } - function it_constructs_integer() + public function it_constructs_integer(): void { $this->beConstructedWith(5, new Currency(self::CURRENCY)); } - function it_constructs_string() + public function it_constructs_string(): void { $this->beConstructedWith('5', new Currency(self::CURRENCY)); } - function it_constructs_integer_with_decimals_of_zero() + public function it_constructs_integer_with_decimals_of_zero(): void { $this->beConstructedWith('5.00', new Currency(self::CURRENCY)); } - function it_constructs_integer_with_plus() + public function it_constructs_integer_with_plus(): void { $this->beConstructedWith('+500', new Currency(self::CURRENCY)); - $this->shouldNotThrow(\InvalidArgumentException::class)->duringInstantiation(); + $this->shouldNotThrow(InvalidArgumentException::class)->duringInstantiation(); } - function it_tests_currency_equality() + public function it_tests_currency_equality(): void { $this->isSameCurrency(new Money(self::AMOUNT, new Currency(self::CURRENCY)))->shouldReturn(true); $this->isSameCurrency(new Money(self::AMOUNT, new Currency(self::OTHER_CURRENCY)))->shouldReturn(false); } - function it_equals_to_another_money() + public function it_equals_to_another_money(): void { $this->equals(new Money(self::AMOUNT, new Currency(self::CURRENCY)))->shouldReturn(true); } - function it_compares_two_amounts(Calculator $calculator) - { - $calculator->compare((string) self::AMOUNT, (string) self::AMOUNT)->willReturn(0); - $money = new Money(self::AMOUNT, new Currency(self::CURRENCY)); - - $this->compare($money)->shouldReturn(0); - $this->greaterThan($money)->shouldReturn(false); - $this->greaterThanOrEqual($money)->shouldReturn(true); - $this->lessThan($money)->shouldReturn(false); - $this->lessThanOrEqual($money)->shouldReturn(true); - } - - function it_throws_an_exception_when_currency_is_different_during_comparison(Calculator $calculator) - { - $calculator->compare(Argument::type('string'), Argument::type('string'))->shouldNotBeCalled(); - - $money = new Money(self::AMOUNT + 1, new Currency(self::OTHER_CURRENCY)); - - $this->shouldThrow(\InvalidArgumentException::class)->duringCompare($money); - $this->shouldThrow(\InvalidArgumentException::class)->duringGreaterThan($money); - $this->shouldThrow(\InvalidArgumentException::class)->duringGreaterThanOrEqual($money); - $this->shouldThrow(\InvalidArgumentException::class)->duringLessThan($money); - $this->shouldThrow(\InvalidArgumentException::class)->duringLessThanOrEqual($money); - } - - function it_adds_an_other_money(Calculator $calculator) - { - $result = self::AMOUNT + self::OTHER_AMOUNT; - $calculator->add((string) self::AMOUNT, (string) self::OTHER_AMOUNT)->willReturn((string) $result); - $money = $this->add(new Money(self::OTHER_AMOUNT, new Currency(self::CURRENCY))); - - $money->shouldHaveType(Money::class); - $money->getAmount()->shouldBe((string) $result); - } - - function it_adds_other_money_values(Calculator $calculator) - { - $result = self::AMOUNT + self::OTHER_AMOUNT + self::AMOUNT + self::OTHER_AMOUNT; - - $calculator->add((string) self::AMOUNT, (string) self::OTHER_AMOUNT)->willReturn((string) (self::AMOUNT + self::OTHER_AMOUNT)); - $calculator->add((string) (self::AMOUNT + self::OTHER_AMOUNT), (string) self::AMOUNT)->willReturn((string) (self::AMOUNT + self::OTHER_AMOUNT + self::AMOUNT)); - $calculator->add((string) (self::AMOUNT + self::OTHER_AMOUNT + self::AMOUNT), (string) self::OTHER_AMOUNT)->willReturn((string) $result); - $money = $this->add( - new Money(self::OTHER_AMOUNT, new Currency(self::CURRENCY)), - new Money(self::AMOUNT, new Currency(self::CURRENCY)), - new Money(self::OTHER_AMOUNT, new Currency(self::CURRENCY)) - ); - - $money->shouldHaveType(Money::class); - $money->getAmount()->shouldBe((string) $result); - } - - function it_returns_the_same_money_when_no_addends_are_provided() + public function it_returns_the_same_money_when_no_addends_are_provided(): void { $money = $this->add(); $money->getAmount()->shouldBe($this->getAmount()); } - function it_throws_an_exception_when_currency_is_different_during_addition(Calculator $calculator) - { - $calculator->add((string) self::AMOUNT, (string) self::AMOUNT)->shouldNotBeCalled(); - - $this->shouldThrow(\InvalidArgumentException::class)->duringAdd(new Money(self::AMOUNT, new Currency(self::OTHER_CURRENCY))); - } - - function it_subtracts_an_other_money(Calculator $calculator) - { - $result = self::AMOUNT - self::OTHER_AMOUNT; - - $calculator->subtract((string) self::AMOUNT, (string) self::OTHER_AMOUNT)->willReturn((string) $result); - $money = $this->subtract(new Money(self::OTHER_AMOUNT, new Currency(self::CURRENCY))); - - $money->shouldHaveType(Money::class); - $money->getAmount()->shouldBe((string) $result); - } - - function it_subtracts_other_money_values(Calculator $calculator) - { - $this->beConstructedWith(self::AMOUNT + self::OTHER_AMOUNT + self::AMOUNT + self::OTHER_AMOUNT, new Currency(self::CURRENCY)); - - $calculator->subtract((string) (self::AMOUNT + self::OTHER_AMOUNT + self::AMOUNT + self::OTHER_AMOUNT), (string) self::OTHER_AMOUNT)->willReturn((string) (self::AMOUNT + self::OTHER_AMOUNT + self::AMOUNT)); - $calculator->subtract((string) (self::AMOUNT + self::OTHER_AMOUNT + self::AMOUNT), (string) self::AMOUNT)->willReturn((string) (self::AMOUNT + self::OTHER_AMOUNT)); - $calculator->subtract((string) (self::AMOUNT + self::OTHER_AMOUNT), (string) self::OTHER_AMOUNT)->willReturn((string) self::AMOUNT); - $money = $this->subtract( - new Money(self::OTHER_AMOUNT, new Currency(self::CURRENCY)), - new Money(self::AMOUNT, new Currency(self::CURRENCY)), - new Money(self::OTHER_AMOUNT, new Currency(self::CURRENCY)) - ); - - $money->shouldHaveType(Money::class); - $money->getAmount()->shouldBe((string) self::AMOUNT); - } - - function it_returns_the_same_money_when_no_subtrahends_are_provided() + public function it_returns_the_same_money_when_no_subtrahends_are_provided(): void { $money = $this->subtract(); $money->getAmount()->shouldBe($this->getAmount()); } - function it_throws_an_exception_if_currency_is_different_during_subtractition(Calculator $calculator) - { - $calculator->subtract((string) self::AMOUNT, (string) self::AMOUNT)->shouldNotBeCalled(); - - $this->shouldThrow(\InvalidArgumentException::class)->duringSubtract(new Money(self::AMOUNT, new Currency(self::OTHER_CURRENCY))); - } - - function it_multiplies_the_amount(Calculator $calculator) - { - $this->beConstructedWith(1, new Currency(self::CURRENCY)); - - $calculator->multiply('1', 5)->willReturn(5); - $calculator->round(5, Money::ROUND_HALF_UP)->willReturn(5); - - $money = $this->multiply(5); - - $money->shouldHaveType(Money::class); - $money->getAmount()->shouldBe('5'); - } - - public function it_throws_an_exception_when_operand_is_invalid_during_multiplication(Calculator $calculator) - { - $calculator->multiply(Argument::type('string'), Argument::type('numeric'))->shouldNotBeCalled(); - $calculator->round(Argument::type('string'), Argument::type('integer'))->shouldNotBeCalled(); - - $this->shouldThrow(\InvalidArgumentException::class)->duringMultiply('INVALID_OPERAND'); - } - - public function it_throws_an_exception_when_rounding_mode_is_invalid_during_multiplication(Calculator $calculator) - { - $calculator->multiply(Argument::type('string'), Argument::type('numeric'))->shouldNotBeCalled(); - $calculator->round(Argument::type('string'), Argument::type('integer'))->shouldNotBeCalled(); - - $this->shouldThrow(\InvalidArgumentException::class)->duringMultiply(1.0, 'INVALID_ROUNDING_MODE'); - } - - function it_divides_the_amount(Calculator $calculator) + public function it_throws_an_exception_when_allocation_target_is_empty(): void { - $this->beConstructedWith(4, new Currency(self::CURRENCY)); - - $calculator->compare((string) (1 / 2), '0')->willReturn(1 / 2 > 1); - $calculator->divide('4', 1 / 2)->willReturn(2); - $calculator->round(2, Money::ROUND_HALF_UP)->willReturn(2); - - $money = $this->divide(1 / 2, Money::ROUND_HALF_UP); - - $money->shouldHaveType(Money::class); - $money->getAmount()->shouldBeLike(2); - } - - public function it_throws_an_exception_when_operand_is_invalid_during_division(Calculator $calculator) - { - $calculator->compare(Argument::type('string'), Argument::type('string'))->shouldNotBeCalled(); - $calculator->divide(Argument::type('string'), Argument::type('numeric'))->shouldNotBeCalled(); - $calculator->round(Argument::type('string'), Argument::type('integer'))->shouldNotBeCalled(); - - $this->shouldThrow(\InvalidArgumentException::class)->duringDivide('INVALID_OPERAND'); - } - - public function it_throws_an_exception_when_rounding_mode_is_invalid_during_division(Calculator $calculator) - { - $calculator->compare('1.0', '0')->shouldNotBeCalled(); - $calculator->divide(Argument::type('string'), Argument::type('numeric'))->shouldNotBeCalled(); - $calculator->round(Argument::type('string'), Argument::type('integer'))->shouldNotBeCalled(); - - $this->shouldThrow(\InvalidArgumentException::class)->duringDivide(1.0, 'INVALID_ROUNDING_MODE'); - } - - function it_throws_an_exception_when_divisor_is_zero(Calculator $calculator) - { - $calculator->compare(0, '0')->willThrow(\InvalidArgumentException::class); - $calculator->divide(Argument::type('string'), Argument::type('numeric'))->shouldNotBeCalled(); - $calculator->round(Argument::type('string'), Argument::type('integer'))->shouldNotBeCalled(); - - $this->shouldThrow(\InvalidArgumentException::class)->duringDivide(0); - } - - function it_allocates_amount(Calculator $calculator) - { - $this->beConstructedWith(100, new Currency(self::CURRENCY)); - - $calculator->share(Argument::type('numeric'), Argument::type('int'), Argument::type('int'))->will(function ($args) { - return (int) floor($args[0] * $args[1] / $args[2]); - }); - - $calculator->subtract(Argument::type('numeric'), Argument::type('numeric'))->will(function ($args) { - return (string) $args[0] - $args[1]; - }); - - $calculator->add(Argument::type('numeric'), Argument::type('numeric'))->will(function ($args) { - return (string) ($args[0] + $args[1]); - }); - - $calculator->compare(Argument::type('numeric'), Argument::type('numeric'))->will(function ($args) { - return ($args[0] < $args[1]) ? -1 : (($args[0] > $args[1]) ? 1 : 0); - }); - - $calculator->absolute(Argument::type('numeric'))->will(function ($args) { - return ltrim($args[0], '-'); - }); - - $calculator->multiply(Argument::type('numeric'), Argument::type('int'))->will(function ($args) { - return (string) $args[0] * $args[1]; - }); - - $allocated = $this->allocate([1, 1, 1]); - $allocated->shouldBeArray(); - $allocated->shouldEqualAllocation([34, 33, 33]); - } - - function it_allocates_amount_to_n_targets(Calculator $calculator) - { - $this->beConstructedWith(15, new Currency(self::CURRENCY)); - - $calculator->share(Argument::type('numeric'), Argument::type('int'), Argument::type('int'))->will(function ($args) { - return (int) floor($args[0] * $args[1] / $args[2]); - }); - - $calculator->subtract(Argument::type('numeric'), Argument::type('numeric'))->will(function ($args) { - return (string)($args[0] - $args[1]); - }); - - $calculator->add(Argument::type('numeric'), Argument::type('numeric'))->will(function ($args) { - return (string)($args[0] + $args[1]); - }); - - $calculator->compare(Argument::type('numeric'), Argument::type('numeric'))->will(function ($args) { - return ($args[0] < $args[1]) ? -1 : (($args[0] > $args[1]) ? 1 : 0); - }); - - $allocated = $this->allocateTo(2); - $allocated->shouldBeArray(); - - $allocated->shouldEqualAllocation([8, 7]); - } - - function it_throws_an_exception_when_allocation_target_is_not_integer() - { - $this->shouldThrow(\InvalidArgumentException::class)->duringAllocateTo('two'); - } - - function it_throws_an_exception_when_allocation_target_is_empty() - { - $this->shouldThrow(\InvalidArgumentException::class)->duringAllocate([]); - } - - function it_throws_an_exception_when_allocation_ratio_is_negative() - { - $this->shouldThrow(\InvalidArgumentException::class)->duringAllocate([-1]); - } - - function it_throws_an_exception_when_allocation_total_is_zero() - { - $this->shouldThrow(\InvalidArgumentException::class)->duringAllocate([0, 0]); - } - - function it_throws_an_exception_when_allocate_to_target_is_less_than_or_equals_zero() - { - $this->shouldThrow(\InvalidArgumentException::class)->duringAllocateTo(-1); - } - - function it_has_comparators(Calculator $calculator) - { - $this->beConstructedWith(1, new Currency(self::CURRENCY)); - - $calculator->compare(Argument::type('numeric'), Argument::type('int'))->will(function ($args) { - return ($args[0] < $args[1]) ? -1 : (($args[0] > $args[1]) ? 1 : 0); - }); - - $this->isZero()->shouldReturn(false); - $this->isPositive()->shouldReturn(true); - $this->isNegative()->shouldReturn(false); - } - - function it_calculates_the_absolute_amount(Calculator $calculator) - { - $this->beConstructedWith(-1, new Currency(self::CURRENCY)); - - $calculator->absolute(-1)->willReturn(1); - - $money = $this->absolute(); - - $money->shouldHaveType(Money::class); - $money->getAmount()->shouldBeLike(1); - } - - function it_calculates_a_modulus_with_an_other_money(Calculator $calculator) - { - $result = self::AMOUNT % self::OTHER_AMOUNT; - $calculator->mod((string) self::AMOUNT, (string) self::OTHER_AMOUNT)->willReturn((string) $result); - $money = $this->mod(new Money(self::OTHER_AMOUNT, new Currency(self::CURRENCY))); - - $money->shouldHaveType(Money::class); - $money->getAmount()->shouldBe((string) $result); - } - - function it_throws_an_exception_when_currency_is_different_during_modulus(Calculator $calculator) - { - $calculator->mod((string) self::AMOUNT, (string) self::AMOUNT)->shouldNotBeCalled(); - - $this->shouldThrow(\InvalidArgumentException::class)->duringMod(new Money(self::AMOUNT, new Currency(self::OTHER_CURRENCY))); + $this->shouldThrow(InvalidArgumentException::class)->duringAllocate([]); } - public function getMatchers() + /** {@inheritDoc} */ + public function getMatchers(): array { return [ 'equalAllocation' => function ($subject, $value) { - /** @var Money $money */ foreach ($subject as $key => $money) { + assert($money instanceof Money); $compareTo = new Money($value[$key], $money->getCurrency()); if ($money->equals($compareTo) === false) { return false; diff --git a/spec/NumberSpec.php b/spec/NumberSpec.php index 8813f17ca..d2a90263e 100644 --- a/spec/NumberSpec.php +++ b/spec/NumberSpec.php @@ -1,44 +1,38 @@ beConstructedWith('1'); } - function it_is_initializable() + public function it_is_initializable(): void { $this->shouldHaveType(Number::class); } - function it_throws_an_exception_when_number_is_invalid() + public function it_throws_an_exception_when_number_is_invalid(): void { $this->beConstructedWith('ONE'); - $this->shouldThrow(\InvalidArgumentException::class)->duringInstantiation(); + $this->shouldThrow(InvalidArgumentException::class)->duringInstantiation(); } - function it_creates_a_number_from_float() + public function it_creates_a_number_from_float(): void { $number = $this->fromFloat(1.1); $number->shouldHaveType(Number::class); $number->__toString()->shouldReturn('1.1'); } - - function it_throws_an_exception_when_number_is_not_float_during_creation_from_float() - { - $this->shouldThrow(\InvalidArgumentException::class)->duringFromFloat(1); - } - - function it_throws_an_exception_when_number_is_not_numeric_during_creation_from_number() - { - $this->shouldThrow(\InvalidArgumentException::class)->duringFromNumber(false); - } } diff --git a/spec/Parser/AggregateMoneyParserSpec.php b/spec/Parser/AggregateMoneyParserSpec.php deleted file mode 100644 index a991f3aa9..000000000 --- a/spec/Parser/AggregateMoneyParserSpec.php +++ /dev/null @@ -1,44 +0,0 @@ -beConstructedWith([$moneyParser]); - } - - function it_is_initializable() - { - $this->shouldHaveType(AggregateMoneyParser::class); - } - - function it_is_a_money_parser() - { - $this->shouldImplement(MoneyParser::class); - } - - function it_parses_money(MoneyParser $moneyParser) - { - $money = new Money(10000, new Currency('EUR')); - - $moneyParser->parse('€ 100', null)->willReturn($money); - - $this->parse('€ 100', null)->shouldReturn($money); - } - - function it_throws_an_exception_when_money_cannot_be_parsed(MoneyParser $moneyParser) - { - $moneyParser->parse('INVALID', null)->willThrow(ParserException::class); - - $this->shouldThrow(ParserException::class)->duringParse('INVALID', null); - } -} diff --git a/spec/Parser/BitcoinMoneyParserSpec.php b/spec/Parser/BitcoinMoneyParserSpec.php index e92b41e92..9cee0e610 100644 --- a/spec/Parser/BitcoinMoneyParserSpec.php +++ b/spec/Parser/BitcoinMoneyParserSpec.php @@ -1,5 +1,7 @@ beConstructedWith(2); } - function it_is_initializable() + public function it_is_initializable(): void { $this->shouldHaveType(BitcoinMoneyParser::class); } - function it_is_a_money_parser() + public function it_is_a_money_parser(): void { $this->shouldImplement(MoneyParser::class); } - function it_parses_money() + public function it_parses_money(): void { $money = $this->parse('Ƀ1000.00'); @@ -34,12 +36,12 @@ function it_parses_money() $money->getCurrency()->getCode()->shouldReturn(BitcoinCurrencies::CODE); } - function it_does_not_parse_a_different_currency() + public function it_does_not_parse_a_different_currency(): void { $this->shouldThrow(ParserException::class)->duringParse('€1.00'); } - function it_does_not_parse_an_invalid_value() + public function it_does_not_parse_an_invalid_value(): void { $this->shouldThrow(ParserException::class)->duringParse(true); } diff --git a/spec/Parser/DecimalMoneyParserSpec.php b/spec/Parser/DecimalMoneyParserSpec.php index 36ad47da5..166c8adb9 100644 --- a/spec/Parser/DecimalMoneyParserSpec.php +++ b/spec/Parser/DecimalMoneyParserSpec.php @@ -1,5 +1,7 @@ beConstructedWith($currencies); } - function it_is_initializable() + public function it_is_initializable(): void { $this->shouldHaveType(DecimalMoneyParser::class); } - function it_is_a_money_parser() + public function it_is_a_money_parser(): void { $this->shouldImplement(MoneyParser::class); } - public function it_parses_money(Currencies $currencies) + public function it_parses_money(Currencies $currencies): void { $currencies->subunitFor(Argument::type(Currency::class))->willReturn(2); - $money = $this->parse('1.00', 'EUR'); + $money = $this->parse('1.00', new Currency('EUR')); $money->shouldHaveType(Money::class); $money->getAmount()->shouldReturn('100'); $money->getCurrency()->getCode()->shouldReturn('EUR'); } - function it_throws_an_exception_when_there_is_no_currency() + public function it_throws_an_exception_when_there_is_no_currency(): void { $this->shouldThrow(ParserException::class)->duringParse('100'); } - function it_throws_an_exception_when_money_includes_currency_symbol() + public function it_throws_an_exception_when_money_includes_currency_symbol(): void { - $this->shouldThrow(ParserException::class)->duringParse('€ 100', 'EUR'); + $this->shouldThrow(ParserException::class)->duringParse('€ 100', new Currency('EUR')); } - function it_throws_an_exception_when_money_is_not_a_valid_decimal() + public function it_throws_an_exception_when_money_is_not_a_valid_decimal(): void { - $this->shouldThrow(ParserException::class)->duringParse('INVALID', 'EUR'); + $this->shouldThrow(ParserException::class)->duringParse('INVALID', new Currency('EUR')); } } diff --git a/spec/Parser/IntlMoneyParserSpec.php b/spec/Parser/IntlMoneyParserSpec.php index 19f4cda64..17ee44f4a 100644 --- a/spec/Parser/IntlMoneyParserSpec.php +++ b/spec/Parser/IntlMoneyParserSpec.php @@ -1,5 +1,7 @@ beConstructedWith($numberFormatter, $currencies); } - function it_is_initializable() + public function it_is_initializable(): void { $this->shouldHaveType(IntlMoneyParser::class); } - function it_is_a_money_parser() + public function it_is_a_money_parser(): void { $this->shouldImplement(MoneyParser::class); } - function it_parses_money(\NumberFormatter $numberFormatter, Currencies $currencies) + public function it_parses_money(NumberFormatter $numberFormatter, Currencies $currencies): void { $currencyString = null; @@ -36,14 +39,14 @@ function it_parses_money(\NumberFormatter $numberFormatter, Currencies $currenci $currencies->subunitFor(Argument::type(Currency::class))->willReturn(2); $currency = new Currency('EUR'); - $money = $this->parse('€1.00', $currency); + $money = $this->parse('€1.00', $currency); $money->shouldHaveType(Money::class); $money->getAmount()->shouldReturn('100'); $money->getCurrency()->getCode()->shouldReturn('EUR'); } - function it_throws_an_exception_when_money_cannot_be_parsed(\NumberFormatter $numberFormatter) + public function it_throws_an_exception_when_money_cannot_be_parsed(NumberFormatter $numberFormatter): void { $currencyString = null; diff --git a/src/Calculator.php b/src/Calculator.php index 4f2f019a4..e61daf8a9 100644 --- a/src/Calculator.php +++ b/src/Calculator.php @@ -1,127 +1,152 @@ + * @internal the calculator component is an internal detail of this library: it is only supposed to be replaced if + * your system requires a custom architecture for operating on large numbers. */ interface Calculator { - /** - * Returns whether the calculator is supported in - * the current server environment. - * - * @return bool - */ - public static function supported(); - /** * Compare a to b. * - * @param string $a - * @param string $b + * Retrieves a negative value if $a < $b. + * Retrieves a positive value if $a > $b. + * Retrieves zero if $a == $b + * + * @psalm-param numeric-string $a + * @psalm-param numeric-string $b * - * @return int + * @psalm-pure */ - public function compare($a, $b); + public static function compare(string $a, string $b): int; /** * Add added to amount. * - * @param string $amount - * @param string $addend + * @psalm-param numeric-string $amount + * @psalm-param numeric-string $addend + * + * @psalm-return numeric-string * - * @return string + * @psalm-pure */ - public function add($amount, $addend); + public static function add(string $amount, string $addend): string; /** * Subtract subtrahend from amount. * - * @param string $amount - * @param string $subtrahend + * @psalm-param numeric-string $amount + * @psalm-param numeric-string $subtrahend * - * @return string + * @psalm-return numeric-string + * + * @psalm-pure */ - public function subtract($amount, $subtrahend); + public static function subtract(string $amount, string $subtrahend): string; /** * Multiply amount with multiplier. * - * @param string $amount - * @param int|float|string $multiplier + * @psalm-param numeric-string $amount + * @psalm-param numeric-string $multiplier + * + * @psalm-return numeric-string * - * @return string + * @psalm-pure */ - public function multiply($amount, $multiplier); + public static function multiply(string $amount, string $multiplier): string; /** * Divide amount with divisor. * - * @param string $amount - * @param int|float|string $divisor + * @psalm-param numeric-string $amount + * @psalm-param numeric-string $divisor * - * @return string + * @psalm-return numeric-string + * + * @throws InvalidArgumentException when $divisor is zero. + * + * @psalm-pure */ - public function divide($amount, $divisor); + public static function divide(string $amount, string $divisor): string; /** * Round number to following integer. * - * @param string $number + * @psalm-param numeric-string $number + * + * @psalm-return numeric-string * - * @return string + * @psalm-pure */ - public function ceil($number); + public static function ceil(string $number): string; /** * Round number to preceding integer. * - * @param string $number + * @psalm-param numeric-string $number * - * @return string + * @psalm-return numeric-string + * + * @psalm-pure */ - public function floor($number); + public static function floor(string $number): string; /** * Returns the absolute value of the number. * - * @param string $number + * @psalm-param numeric-string $number + * + * @psalm-return numeric-string * - * @return string + * @psalm-pure */ - public function absolute($number); + public static function absolute(string $number): string; /** * Round number, use rounding mode for tie-breaker. * - * @param int|float|string $number - * @param int $roundingMode + * @psalm-param numeric-string $number + * @psalm-param Money::ROUND_* $roundingMode * - * @return string + * @psalm-return numeric-string + * + * @psalm-pure */ - public function round($number, $roundingMode); + public static function round(string $number, int $roundingMode): string; /** * Share amount among ratio / total portions. * - * @param string $amount - * @param int|float|string $ratio - * @param int|float|string $total + * @psalm-param numeric-string $amount + * @psalm-param numeric-string $ratio + * @psalm-param numeric-string $total + * + * @psalm-return numeric-string * - * @return string + * @psalm-pure */ - public function share($amount, $ratio, $total); + public static function share(string $amount, string $ratio, string $total): string; /** * Get the modulus of an amount. * - * @param string $amount - * @param int|float|string $divisor + * @psalm-param numeric-string $amount + * @psalm-param numeric-string $divisor + * + * @psalm-return numeric-string + * + * @throws InvalidArgumentException when $divisor is zero. * - * @return string + * @psalm-pure */ - public function mod($amount, $divisor); + public static function mod(string $amount, string $divisor): string; } diff --git a/src/Calculator/BcMathCalculator.php b/src/Calculator/BcMathCalculator.php index 458ab70bf..2d04464ef 100644 --- a/src/Calculator/BcMathCalculator.php +++ b/src/Calculator/BcMathCalculator.php @@ -1,241 +1,220 @@ - */ +use function bcadd; +use function bccomp; +use function bcdiv; +use function bcmod; +use function bcmul; +use function bcsub; +use function ltrim; + final class BcMathCalculator implements Calculator { - /** - * @var string - */ - private $scale; + private const SCALE = 14; - /** - * @param int $scale - */ - public function __construct($scale = 14) + /** @psalm-pure */ + public static function compare(string $a, string $b): int { - $this->scale = $scale; + return bccomp($a, $b, self::SCALE); } - /** - * {@inheritdoc} - */ - public static function supported() + /** @psalm-pure */ + public static function add(string $amount, string $addend): string { - return extension_loaded('bcmath'); + return bcadd($amount, $addend, self::SCALE); } - /** - * {@inheritdoc} - */ - public function compare($a, $b) + /** @psalm-pure */ + public static function subtract(string $amount, string $subtrahend): string { - return bccomp($a, $b, $this->scale); + return bcsub($amount, $subtrahend, self::SCALE); } - /** - * {@inheritdoc} - */ - public function add($amount, $addend) - { - return (string) Number::fromString(bcadd($amount, $addend, $this->scale)); - } - - /** - * {@inheritdoc} - * - * @param $amount - * @param $subtrahend - * - * @return string - */ - public function subtract($amount, $subtrahend) + /** @psalm-pure */ + public static function multiply(string $amount, string $multiplier): string { - return (string) Number::fromString(bcsub($amount, $subtrahend, $this->scale)); + return bcmul($amount, $multiplier, self::SCALE); } - /** - * {@inheritdoc} - */ - public function multiply($amount, $multiplier) + /** @psalm-pure */ + public static function divide(string $amount, string $divisor): string { - $multiplier = Number::fromNumber($multiplier); - - return bcmul($amount, (string) $multiplier, $this->scale); - } - - /** - * {@inheritdoc} - */ - public function divide($amount, $divisor) - { - $divisor = Number::fromNumber($divisor); + if (bccomp($divisor, '0', self::SCALE) === 0) { + throw InvalidArgumentException::divisionByZero(); + } - return bcdiv($amount, (string) $divisor, $this->scale); + return bcdiv($amount, $divisor, self::SCALE); } - /** - * {@inheritdoc} - */ - public function ceil($number) + /** @psalm-pure */ + public static function ceil(string $number): string { - $number = Number::fromNumber($number); + $number = Number::fromString($number); if ($number->isInteger()) { - return (string) $number; + return $number->__toString(); } if ($number->isNegative()) { - return bcadd((string) $number, '0', 0); + return bcadd($number->__toString(), '0', 0); } - return bcadd((string) $number, '1', 0); + return bcadd($number->__toString(), '1', 0); } - /** - * {@inheritdoc} - */ - public function floor($number) + /** @psalm-pure */ + public static function floor(string $number): string { - $number = Number::fromNumber($number); + $number = Number::fromString($number); if ($number->isInteger()) { - return (string) $number; + return $number->__toString(); } if ($number->isNegative()) { - return bcadd((string) $number, '-1', 0); + return bcadd($number->__toString(), '-1', 0); } - return bcadd($number, '0', 0); + return bcadd($number->__toString(), '0', 0); } /** - * {@inheritdoc} + * @psalm-suppress MoreSpecificReturnType we know that trimming `-` produces a positive numeric-string here + * @psalm-suppress LessSpecificReturnStatement we know that trimming `-` produces a positive numeric-string here + * @psalm-pure */ - public function absolute($number) + public static function absolute(string $number): string { return ltrim($number, '-'); } /** - * {@inheritdoc} + * @psalm-param Money::ROUND_* $roundingMode + * + * @psalm-return numeric-string + * + * @psalm-pure */ - public function round($number, $roundingMode) + public static function round(string $number, int $roundingMode): string { - $number = Number::fromNumber($number); + $number = Number::fromString($number); if ($number->isInteger()) { - return (string) $number; + return $number->__toString(); } if ($number->isHalf() === false) { - return $this->roundDigit($number); + return self::roundDigit($number); } - if (Money::ROUND_HALF_UP === $roundingMode) { + if ($roundingMode === Money::ROUND_HALF_UP) { return bcadd( - (string) $number, + $number->__toString(), $number->getIntegerRoundingMultiplier(), 0 ); } - if (Money::ROUND_HALF_DOWN === $roundingMode) { - return bcadd((string) $number, '0', 0); + if ($roundingMode === Money::ROUND_HALF_DOWN) { + return bcadd($number->__toString(), '0', 0); } - if (Money::ROUND_HALF_EVEN === $roundingMode) { + if ($roundingMode === Money::ROUND_HALF_EVEN) { if ($number->isCurrentEven()) { - return bcadd((string) $number, '0', 0); + return bcadd($number->__toString(), '0', 0); } return bcadd( - (string) $number, + $number->__toString(), $number->getIntegerRoundingMultiplier(), 0 ); } - if (Money::ROUND_HALF_ODD === $roundingMode) { + if ($roundingMode === Money::ROUND_HALF_ODD) { if ($number->isCurrentEven()) { return bcadd( - (string) $number, + $number->__toString(), $number->getIntegerRoundingMultiplier(), 0 ); } - return bcadd((string) $number, '0', 0); + return bcadd($number->__toString(), '0', 0); } - if (Money::ROUND_HALF_POSITIVE_INFINITY === $roundingMode) { + if ($roundingMode === Money::ROUND_HALF_POSITIVE_INFINITY) { if ($number->isNegative()) { - return bcadd((string) $number, '0', 0); + return bcadd($number->__toString(), '0', 0); } return bcadd( - (string) $number, + $number->__toString(), $number->getIntegerRoundingMultiplier(), 0 ); } - if (Money::ROUND_HALF_NEGATIVE_INFINITY === $roundingMode) { + if ($roundingMode === Money::ROUND_HALF_NEGATIVE_INFINITY) { if ($number->isNegative()) { return bcadd( - (string) $number, + $number->__toString(), $number->getIntegerRoundingMultiplier(), 0 ); } return bcadd( - (string) $number, + $number->__toString(), '0', 0 ); } - throw new \InvalidArgumentException('Unknown rounding mode'); + throw new CoreInvalidArgumentException('Unknown rounding mode'); } /** - * @return string + * @psalm-return numeric-string + * + * @psalm-pure */ - private function roundDigit(Number $number) + private static function roundDigit(Number $number): string { if ($number->isCloserToNext()) { return bcadd( - (string) $number, + $number->__toString(), $number->getIntegerRoundingMultiplier(), 0 ); } - return bcadd((string) $number, '0', 0); + return bcadd($number->__toString(), '0', 0); } - /** - * {@inheritdoc} - */ - public function share($amount, $ratio, $total) + /** @psalm-pure */ + public static function share(string $amount, string $ratio, string $total): string { - return $this->floor(bcdiv(bcmul($amount, $ratio, $this->scale), $total, $this->scale)); + return self::floor(bcdiv(bcmul($amount, $ratio, self::SCALE), $total, self::SCALE)); } - /** - * {@inheritdoc} - */ - public function mod($amount, $divisor) + /** @psalm-pure */ + public static function mod(string $amount, string $divisor): string { - return bcmod($amount, $divisor); + if (bccomp($divisor, '0') === 0) { + throw InvalidArgumentException::moduloByZero(); + } + + return bcmod($amount, $divisor) ?? '0'; } } diff --git a/src/Calculator/GmpCalculator.php b/src/Calculator/GmpCalculator.php index ad6738ac2..f446fdc03 100644 --- a/src/Calculator/GmpCalculator.php +++ b/src/Calculator/GmpCalculator.php @@ -1,44 +1,49 @@ + * @psalm-immutable + * + * Important: the {@see GmpCalculator} is not optimized for decimal operations, as GMP + * is designed to operate on large integers. Consider using this only if your + * system does not have `ext-bcmath` installed. */ final class GmpCalculator implements Calculator { - /** - * @var string - */ - private $scale; - - /** - * @param int $scale - */ - public function __construct($scale = 14) - { - $this->scale = $scale; - } + private const SCALE = 14; - /** - * {@inheritdoc} - */ - public static function supported() + /** @psalm-pure */ + public static function compare(string $a, string $b): int { - return extension_loaded('gmp'); - } - - /** - * {@inheritdoc} - */ - public function compare($a, $b) - { - $aNum = Number::fromNumber($a); - $bNum = Number::fromNumber($b); + $aNum = Number::fromString($a); + $bNum = Number::fromString($b); if ($aNum->isDecimal() || $bNum->isDecimal()) { $integersCompared = gmp_cmp($aNum->getIntegerPart(), $bNum->getIntegerPart()); @@ -55,31 +60,25 @@ public function compare($a, $b) return gmp_cmp($a, $b); } - /** - * {@inheritdoc} - */ - public function add($amount, $addend) + /** @psalm-pure */ + public static function add(string $amount, string $addend): string { return gmp_strval(gmp_add($amount, $addend)); } - /** - * {@inheritdoc} - */ - public function subtract($amount, $subtrahend) + /** @psalm-pure */ + public static function subtract(string $amount, string $subtrahend): string { return gmp_strval(gmp_sub($amount, $subtrahend)); } - /** - * {@inheritdoc} - */ - public function multiply($amount, $multiplier) + /** @psalm-pure */ + public static function multiply(string $amount, string $multiplier): string { - $multiplier = Number::fromNumber($multiplier); + $multiplier = Number::fromString($multiplier); if ($multiplier->isDecimal()) { - $decimalPlaces = strlen($multiplier->getFractionalPart()); + $decimalPlaces = strlen($multiplier->getFractionalPart()); $multiplierBase = $multiplier->getIntegerPart(); if ($multiplierBase) { @@ -90,42 +89,50 @@ public function multiply($amount, $multiplier) $resultBase = gmp_strval(gmp_mul(gmp_init($amount), gmp_init($multiplierBase))); - if ('0' === $resultBase) { + if ($resultBase === '0') { return '0'; } - $result = substr($resultBase, $decimalPlaces * -1); + $result = substr($resultBase, $decimalPlaces * -1); $resultLength = strlen($result); if ($decimalPlaces > $resultLength) { - return '0.'.str_pad('', $decimalPlaces - $resultLength, '0').$result; + /** @psalm-var numeric-string $finalResult */ + $finalResult = '0.' . str_pad('', $decimalPlaces - $resultLength, '0') . $result; + + return $finalResult; } - return substr($resultBase, 0, $decimalPlaces * -1).'.'.$result; + /** @psalm-var numeric-string $finalResult */ + $finalResult = substr($resultBase, 0, $decimalPlaces * -1) . '.' . $result; + + return $finalResult; } return gmp_strval(gmp_mul(gmp_init($amount), gmp_init((string) $multiplier))); } - /** - * {@inheritdoc} - */ - public function divide($amount, $divisor) + /** @psalm-pure */ + public static function divide(string $amount, string $divisor): string { - $divisor = Number::fromNumber($divisor); + if (self::compare($divisor, '0') === 0) { + throw InvalidArgumentException::moduloByZero(); + } + + $divisor = Number::fromString($divisor); if ($divisor->isDecimal()) { $decimalPlaces = strlen($divisor->getFractionalPart()); if ($divisor->getIntegerPart()) { - $divisor = new Number($divisor->getIntegerPart().$divisor->getFractionalPart()); + $divisor = new Number($divisor->getIntegerPart() . $divisor->getFractionalPart()); } else { $divisor = new Number(ltrim($divisor->getFractionalPart(), '0')); } - $amount = gmp_strval(gmp_mul(gmp_init($amount), gmp_init('1'.str_pad('', $decimalPlaces, '0')))); + $amount = gmp_strval(gmp_mul(gmp_init($amount), gmp_init('1' . str_pad('', $decimalPlaces, '0')))); } - list($integer, $remainder) = gmp_div_qr(gmp_init($amount), gmp_init((string) $divisor)); + [$integer, $remainder] = gmp_div_qr(gmp_init($amount), gmp_init((string) $divisor)); if (gmp_cmp($remainder, '0') === 0) { return gmp_strval($integer); @@ -133,7 +140,7 @@ public function divide($amount, $divisor) $divisionOfRemainder = gmp_strval( gmp_div_q( - gmp_mul($remainder, gmp_init('1'.str_pad('', $this->scale, '0'))), + gmp_mul($remainder, gmp_init('1' . str_pad('', self::SCALE, '0'))), gmp_init((string) $divisor), GMP_ROUND_MINUSINF ) @@ -143,167 +150,172 @@ public function divide($amount, $divisor) $divisionOfRemainder = substr($divisionOfRemainder, 1); } - return gmp_strval($integer).'.'.str_pad($divisionOfRemainder, $this->scale, '0', STR_PAD_LEFT); + /** @psalm-var numeric-string $finalResult */ + $finalResult = gmp_strval($integer) . '.' . str_pad($divisionOfRemainder, self::SCALE, '0', STR_PAD_LEFT); + + return $finalResult; } - /** - * {@inheritdoc} - */ - public function ceil($number) + /** @psalm-pure */ + public static function ceil(string $number): string { - $number = Number::fromNumber($number); + $number = Number::fromString($number); if ($number->isInteger()) { - return (string) $number; + return $number->__toString(); } if ($number->isNegative()) { - return $this->add($number->getIntegerPart(), '0'); + return self::add($number->getIntegerPart(), '0'); } - return $this->add($number->getIntegerPart(), '1'); + return self::add($number->getIntegerPart(), '1'); } - /** - * {@inheritdoc} - */ - public function floor($number) + /** @psalm-pure */ + public static function floor(string $number): string { - $number = Number::fromNumber($number); + $number = Number::fromString($number); if ($number->isInteger()) { - return (string) $number; + return $number->__toString(); } if ($number->isNegative()) { - return $this->add($number->getIntegerPart(), '-1'); + return self::add($number->getIntegerPart(), '-1'); } - return $this->add($number->getIntegerPart(), '0'); + return self::add($number->getIntegerPart(), '0'); } /** - * {@inheritdoc} + * @psalm-suppress MoreSpecificReturnType we know that trimming `-` produces a positive numeric-string here + * @psalm-suppress LessSpecificReturnStatement we know that trimming `-` produces a positive numeric-string here + * @psalm-pure */ - public function absolute($number) + public static function absolute(string $number): string { return ltrim($number, '-'); } /** - * {@inheritdoc} + * @psalm-param Money::ROUND_* $roundingMode + * + * @psalm-return numeric-string + * + * @psalm-pure */ - public function round($number, $roundingMode) + public static function round(string $number, int $roundingMode): string { - $number = Number::fromNumber($number); + $number = Number::fromString($number); if ($number->isInteger()) { - return (string) $number; + return $number->__toString(); } if ($number->isHalf() === false) { - return $this->roundDigit($number); + return self::roundDigit($number); } - if (Money::ROUND_HALF_UP === $roundingMode) { - return $this->add( + if ($roundingMode === Money::ROUND_HALF_UP) { + return self::add( $number->getIntegerPart(), $number->getIntegerRoundingMultiplier() ); } - if (Money::ROUND_HALF_DOWN === $roundingMode) { - return $this->add($number->getIntegerPart(), '0'); + if ($roundingMode === Money::ROUND_HALF_DOWN) { + return self::add($number->getIntegerPart(), '0'); } - if (Money::ROUND_HALF_EVEN === $roundingMode) { + if ($roundingMode === Money::ROUND_HALF_EVEN) { if ($number->isCurrentEven()) { - return $this->add($number->getIntegerPart(), '0'); + return self::add($number->getIntegerPart(), '0'); } - return $this->add( + return self::add( $number->getIntegerPart(), $number->getIntegerRoundingMultiplier() ); } - if (Money::ROUND_HALF_ODD === $roundingMode) { + if ($roundingMode === Money::ROUND_HALF_ODD) { if ($number->isCurrentEven()) { - return $this->add( + return self::add( $number->getIntegerPart(), $number->getIntegerRoundingMultiplier() ); } - return $this->add($number->getIntegerPart(), '0'); + return self::add($number->getIntegerPart(), '0'); } - if (Money::ROUND_HALF_POSITIVE_INFINITY === $roundingMode) { + if ($roundingMode === Money::ROUND_HALF_POSITIVE_INFINITY) { if ($number->isNegative()) { - return $this->add( + return self::add( $number->getIntegerPart(), '0' ); } - return $this->add( + return self::add( $number->getIntegerPart(), $number->getIntegerRoundingMultiplier() ); } - if (Money::ROUND_HALF_NEGATIVE_INFINITY === $roundingMode) { + if ($roundingMode === Money::ROUND_HALF_NEGATIVE_INFINITY) { if ($number->isNegative()) { - return $this->add( + return self::add( $number->getIntegerPart(), $number->getIntegerRoundingMultiplier() ); } - return $this->add( + return self::add( $number->getIntegerPart(), '0' ); } - throw new \InvalidArgumentException('Unknown rounding mode'); + throw new CoreInvalidArgumentException('Unknown rounding mode'); } /** - * @param $number + * @psalm-return numeric-string * - * @return string + * @psalm-pure */ - private function roundDigit(Number $number) + private static function roundDigit(Number $number): string { if ($number->isCloserToNext()) { - return $this->add( + return self::add( $number->getIntegerPart(), $number->getIntegerRoundingMultiplier() ); } - return $this->add($number->getIntegerPart(), '0'); + return self::add($number->getIntegerPart(), '0'); } - /** - * {@inheritdoc} - */ - public function share($amount, $ratio, $total) + /** @psalm-pure */ + public static function share(string $amount, string $ratio, string $total): string { - return $this->floor($this->divide($this->multiply($amount, $ratio), $total)); + return self::floor(self::divide(self::multiply($amount, $ratio), $total)); } - /** - * {@inheritdoc} - */ - public function mod($amount, $divisor) + /** @psalm-pure */ + public static function mod(string $amount, string $divisor): string { + if (self::compare($divisor, '0') === 0) { + throw InvalidArgumentException::moduloByZero(); + } + // gmp_mod() only calculates non-negative integers, so we use absolutes - $remainder = gmp_mod($this->absolute($amount), $this->absolute($divisor)); + $remainder = gmp_mod(self::absolute($amount), self::absolute($divisor)); // If the amount was negative, we negate the result of the modulus operation - $amount = Number::fromNumber($amount); + $amount = Number::fromString($amount); if ($amount->isNegative()) { $remainder = gmp_neg($remainder); @@ -311,12 +323,4 @@ public function mod($amount, $divisor) return gmp_strval($remainder); } - - /** - * @test - */ - public function it_divides_bug538() - { - $this->assertSame('-4.54545454545455', $this->getCalculator()->divide('-500', 110)); - } } diff --git a/src/Calculator/PhpCalculator.php b/src/Calculator/PhpCalculator.php deleted file mode 100644 index 6713a1808..000000000 --- a/src/Calculator/PhpCalculator.php +++ /dev/null @@ -1,197 +0,0 @@ - - */ -final class PhpCalculator implements Calculator -{ - /** - * {@inheritdoc} - */ - public static function supported() - { - return true; - } - - /** - * {@inheritdoc} - */ - public function compare($a, $b) - { - return ($a < $b) ? -1 : (($a > $b) ? 1 : 0); - } - - /** - * {@inheritdoc} - */ - public function add($amount, $addend) - { - $result = $amount + $addend; - - $this->assertInteger($result); - - return (string) $result; - } - - /** - * {@inheritdoc} - */ - public function subtract($amount, $subtrahend) - { - $result = $amount - $subtrahend; - - $this->assertInteger($result); - - return (string) $result; - } - - /** - * {@inheritdoc} - */ - public function multiply($amount, $multiplier) - { - $result = $amount * $multiplier; - - $this->assertIntegerBounds($result); - - return (string) Number::fromNumber($result); - } - - /** - * {@inheritdoc} - */ - public function divide($amount, $divisor) - { - $result = $amount / $divisor; - - $this->assertIntegerBounds($result); - - return (string) Number::fromNumber($result); - } - - /** - * {@inheritdoc} - */ - public function ceil($number) - { - return $this->castInteger(ceil($number)); - } - - /** - * {@inheritdoc} - */ - public function floor($number) - { - return $this->castInteger(floor($number)); - } - - /** - * {@inheritdoc} - */ - public function absolute($number) - { - $result = ltrim($number, '-'); - - $this->assertIntegerBounds($result); - - return (string) $result; - } - - /** - * {@inheritdoc} - */ - public function round($number, $roundingMode) - { - if (Money::ROUND_HALF_POSITIVE_INFINITY === $roundingMode) { - $number = Number::fromNumber($number); - - if ($number->isHalf()) { - return $this->castInteger(ceil((string) $number)); - } - - return $this->castInteger(round((string) $number, 0, Money::ROUND_HALF_UP)); - } - - if (Money::ROUND_HALF_NEGATIVE_INFINITY === $roundingMode) { - $number = Number::fromNumber($number); - - if ($number->isHalf()) { - return $this->castInteger(floor((string) $number)); - } - - return $this->castInteger(round((string) $number, 0, Money::ROUND_HALF_DOWN)); - } - - return $this->castInteger(round($number, 0, $roundingMode)); - } - - /** - * {@inheritdoc} - */ - public function share($amount, $ratio, $total) - { - return $this->castInteger(floor($amount * $ratio / $total)); - } - - /** - * {@inheritdoc} - */ - public function mod($amount, $divisor) - { - $result = $amount % $divisor; - - $this->assertIntegerBounds($result); - - return (string) $result; - } - - /** - * Asserts that an integer value didn't become something else - * (after some arithmetic operation). - * - * @param int $amount - * - * @throws \OverflowException If integer overflow occured - * @throws \UnderflowException If integer underflow occured - */ - private function assertIntegerBounds($amount) - { - if ($amount > PHP_INT_MAX) { - throw new \OverflowException('You overflowed the maximum allowed integer (PHP_INT_MAX)'); - } elseif ($amount < ~PHP_INT_MAX) { - throw new \UnderflowException('You underflowed the minimum allowed integer (PHP_INT_MAX)'); - } - } - - /** - * Casts an amount to integer ensuring that an overflow/underflow did not occur. - * - * @param int $amount - * - * @return string - */ - private function castInteger($amount) - { - $this->assertIntegerBounds($amount); - - return (string) intval($amount); - } - - /** - * Asserts that integer remains integer after arithmetic operations. - * - * @param int $amount - */ - private function assertInteger($amount) - { - if (filter_var($amount, FILTER_VALIDATE_INT) === false) { - throw new \UnexpectedValueException('The result of arithmetic operation is not an integer'); - } - } -} diff --git a/src/Converter.php b/src/Converter.php index d616ae6ed..3eb17f518 100644 --- a/src/Converter.php +++ b/src/Converter.php @@ -1,45 +1,36 @@ */ final class Converter { - /** - * @var Currencies - */ - private $currencies; + private Currencies $currencies; - /** - * @var Exchange - */ - private $exchange; + private Exchange $exchange; public function __construct(Currencies $currencies, Exchange $exchange) { $this->currencies = $currencies; - $this->exchange = $exchange; + $this->exchange = $exchange; } - /** - * @param int $roundingMode - * - * @return Money - */ - public function convert(Money $money, Currency $counterCurrency, $roundingMode = Money::ROUND_HALF_UP) + public function convert(Money $money, Currency $counterCurrency, int $roundingMode = Money::ROUND_HALF_UP): Money { $baseCurrency = $money->getCurrency(); - $ratio = $this->exchange->quote($baseCurrency, $counterCurrency)->getConversionRatio(); + $ratio = $this->exchange->quote($baseCurrency, $counterCurrency)->getConversionRatio(); - $baseCurrencySubunit = $this->currencies->subunitFor($baseCurrency); + $baseCurrencySubunit = $this->currencies->subunitFor($baseCurrency); $counterCurrencySubunit = $this->currencies->subunitFor($counterCurrency); - $subunitDifference = $baseCurrencySubunit - $counterCurrencySubunit; + $subunitDifference = $baseCurrencySubunit - $counterCurrencySubunit; - $ratio = (string) Number::fromFloat($ratio)->base10($subunitDifference); + $ratio = Number::fromString($ratio) + ->base10($subunitDifference) + ->__toString(); $counterValue = $money->multiply($ratio, $roundingMode); diff --git a/src/Currencies.php b/src/Currencies.php index 5c12f5c8e..b9b70527d 100644 --- a/src/Currencies.php +++ b/src/Currencies.php @@ -1,29 +1,32 @@ */ - public function subunitFor(Currency $currency); + public function getIterator(): Traversable; } diff --git a/src/Currencies/AggregateCurrencies.php b/src/Currencies/AggregateCurrencies.php index 5a4fc44fb..a4f946ddb 100644 --- a/src/Currencies/AggregateCurrencies.php +++ b/src/Currencies/AggregateCurrencies.php @@ -1,41 +1,32 @@ */ final class AggregateCurrencies implements Currencies { - /** - * @var Currencies[] - */ - private $currencies; + /** @var Currencies[] */ + private array $currencies; /** * @param Currencies[] $currencies */ public function __construct(array $currencies) { - foreach ($currencies as $c) { - if (false === $c instanceof Currencies) { - throw new \InvalidArgumentException('All currency repositories must implement '.Currencies::class); - } - } - $this->currencies = $currencies; } - /** - * {@inheritdoc} - */ - public function contains(Currency $currency) + public function contains(Currency $currency): bool { foreach ($this->currencies as $currencies) { if ($currencies->contains($currency)) { @@ -46,10 +37,7 @@ public function contains(Currency $currency) return false; } - /** - * {@inheritdoc} - */ - public function subunitFor(Currency $currency) + public function subunitFor(Currency $currency): int { foreach ($this->currencies as $currencies) { if ($currencies->contains($currency)) { @@ -57,15 +45,14 @@ public function subunitFor(Currency $currency) } } - throw new UnknownCurrencyException('Cannot find currency '.$currency->getCode()); + throw new UnknownCurrencyException('Cannot find currency ' . $currency->getCode()); } - /** - * {@inheritdoc} - */ - public function getIterator() + /** {@inheritDoc} */ + public function getIterator(): Traversable { - $iterator = new \AppendIterator(); + /** @psalm-var AppendIterator&Traversable $iterator */ + $iterator = new AppendIterator(); foreach ($this->currencies as $currencies) { $iterator->append($currencies->getIterator()); diff --git a/src/Currencies/BitcoinCurrencies.php b/src/Currencies/BitcoinCurrencies.php index 711244687..87d73f94a 100644 --- a/src/Currencies/BitcoinCurrencies.php +++ b/src/Currencies/BitcoinCurrencies.php @@ -1,45 +1,38 @@ - */ final class BitcoinCurrencies implements Currencies { - const CODE = 'XBT'; + public const CODE = 'XBT'; - const SYMBOL = "\xC9\x83"; + public const SYMBOL = "\xC9\x83"; - /** - * {@inheritdoc} - */ - public function contains(Currency $currency) + public function contains(Currency $currency): bool { - return self::CODE === $currency->getCode(); + return $currency->getCode() === self::CODE; } - /** - * {@inheritdoc} - */ - public function subunitFor(Currency $currency) + public function subunitFor(Currency $currency): int { if ($currency->getCode() !== self::CODE) { - throw new UnknownCurrencyException($currency->getCode().' is not bitcoin and is not supported by this currency repository'); + throw new UnknownCurrencyException($currency->getCode() . ' is not bitcoin and is not supported by this currency repository'); } return 8; } - /** - * {@inheritdoc} - */ - public function getIterator() + /** {@inheritDoc} */ + public function getIterator(): Traversable { - return new \ArrayIterator([new Currency(self::CODE)]); + return new ArrayIterator([new Currency(self::CODE)]); } } diff --git a/src/Currencies/CachedCurrencies.php b/src/Currencies/CachedCurrencies.php index a0bdb8180..81d99416d 100644 --- a/src/Currencies/CachedCurrencies.php +++ b/src/Currencies/CachedCurrencies.php @@ -1,88 +1,79 @@ */ final class CachedCurrencies implements Currencies { - /** - * @var Currencies - */ - private $currencies; + private Currencies $currencies; - /** - * @var CacheItemPoolInterface - */ - private $pool; + private CacheItemPoolInterface $pool; public function __construct(Currencies $currencies, CacheItemPoolInterface $pool) { $this->currencies = $currencies; - $this->pool = $pool; + $this->pool = $pool; } - /** - * {@inheritdoc} - */ - public function contains(Currency $currency) + public function contains(Currency $currency): bool { - $item = $this->pool->getItem('currency|availability|'.$currency->getCode()); + $item = $this->pool->getItem('currency|availability|' . $currency->getCode()); - if (false === $item->isHit()) { + if ($item->isHit() === false) { $item->set($this->currencies->contains($currency)); - if ($item instanceof TaggableItemInterface) { - $item->addTag('currency.availability'); + if ($item instanceof TaggableCacheItemInterface) { + $item->setTags(['currency.availability']); } $this->pool->save($item); } - return $item->get(); + return (bool) $item->get(); } - /** - * {@inheritdoc} - */ - public function subunitFor(Currency $currency) + public function subunitFor(Currency $currency): int { - $item = $this->pool->getItem('currency|subunit|'.$currency->getCode()); + $item = $this->pool->getItem('currency|subunit|' . $currency->getCode()); - if (false === $item->isHit()) { + if ($item->isHit() === false) { $item->set($this->currencies->subunitFor($currency)); - if ($item instanceof TaggableItemInterface) { - $item->addTag('currency.subunit'); + if ($item instanceof TaggableCacheItemInterface) { + $item->setTags(['currency.subunit']); } $this->pool->save($item); } - return $item->get(); + return (int) $item->get(); } - /** - * {@inheritdoc} - */ - public function getIterator() + /** {@inheritDoc} */ + public function getIterator(): Traversable { - return new \CallbackFilterIterator( - $this->currencies->getIterator(), - function (Currency $currency) { - $item = $this->pool->getItem('currency|availability|'.$currency->getCode()); + return new CallbackFilterIterator( + new ArrayIterator(iterator_to_array($this->currencies->getIterator())), + function (Currency $currency): bool { + $item = $this->pool->getItem('currency|availability|' . $currency->getCode()); $item->set(true); - if ($item instanceof TaggableItemInterface) { - $item->addTag('currency.availability'); + if ($item instanceof TaggableCacheItemInterface) { + $item->setTags(['currency.availability']); } $this->pool->save($item); diff --git a/src/Currencies/CurrencyList.php b/src/Currencies/CurrencyList.php index 9fb240514..8c2f37bed 100644 --- a/src/Currencies/CurrencyList.php +++ b/src/Currencies/CurrencyList.php @@ -1,68 +1,56 @@ */ final class CurrencyList implements Currencies { /** - * Map of currencies indexed by code. + * Map of currencies and their sub-units indexed by code. * - * @var array + * @psalm-var array */ - private $currencies; + private array $currencies; + /** @psalm-param array $currencies */ public function __construct(array $currencies) { - foreach ($currencies as $currencyCode => $subunit) { - if (empty($currencyCode) || !is_string($currencyCode)) { - throw new \InvalidArgumentException(sprintf('Currency code must be a string and not empty. "%s" given', $currencyCode)); - } - - if (!is_int($subunit) || $subunit < 0) { - throw new \InvalidArgumentException(sprintf('Currency %s does not have a valid minor unit. Must be a positive integer.', $currencyCode)); - } - } - $this->currencies = $currencies; } - /** - * {@inheritdoc} - */ - public function contains(Currency $currency) + public function contains(Currency $currency): bool { return isset($this->currencies[$currency->getCode()]); } - /** - * {@inheritdoc} - */ - public function subunitFor(Currency $currency) + public function subunitFor(Currency $currency): int { - if (!$this->contains($currency)) { - throw new UnknownCurrencyException('Cannot find currency '.$currency->getCode()); + if (! $this->contains($currency)) { + throw new UnknownCurrencyException('Cannot find currency ' . $currency->getCode()); } return $this->currencies[$currency->getCode()]; } - /** - * {@inheritdoc} - */ - public function getIterator() + /** {@inheritDoc} */ + public function getIterator(): Traversable { - return new \ArrayIterator( + return new ArrayIterator( array_map( - function ($code) { + static function ($code) { return new Currency($code); }, array_keys($this->currencies) diff --git a/src/Currencies/ISOCurrencies.php b/src/Currencies/ISOCurrencies.php index ed16b7b14..4e17e9e94 100644 --- a/src/Currencies/ISOCurrencies.php +++ b/src/Currencies/ISOCurrencies.php @@ -1,40 +1,46 @@ |null */ - private static $currencies; + private static ?array $currencies = null; - /** - * {@inheritdoc} - */ - public function contains(Currency $currency) + public function contains(Currency $currency): bool { return isset($this->getCurrencies()[$currency->getCode()]); } - /** - * {@inheritdoc} - */ - public function subunitFor(Currency $currency) + public function subunitFor(Currency $currency): int { - if (!$this->contains($currency)) { - throw new UnknownCurrencyException('Cannot find ISO currency '.$currency->getCode()); + if (! $this->contains($currency)) { + throw new UnknownCurrencyException('Cannot find ISO currency ' . $currency->getCode()); } return $this->getCurrencies()[$currency->getCode()]['minorUnit']; @@ -43,27 +49,25 @@ public function subunitFor(Currency $currency) /** * Returns the numeric code for a currency. * - * @return int - * - * @throws UnknownCurrencyException If currency is not available in the current context + * @throws UnknownCurrencyException If currency is not available in the current context. */ - public function numericCodeFor(Currency $currency) + public function numericCodeFor(Currency $currency): int { - if (!$this->contains($currency)) { - throw new UnknownCurrencyException('Cannot find ISO currency '.$currency->getCode()); + if (! $this->contains($currency)) { + throw new UnknownCurrencyException('Cannot find ISO currency ' . $currency->getCode()); } return $this->getCurrencies()[$currency->getCode()]['numericCode']; } /** - * @return \Traversable + * @psalm-return Traversable */ - public function getIterator() + public function getIterator(): Traversable { - return new \ArrayIterator( + return new ArrayIterator( array_map( - function ($code) { + static function ($code) { return new Currency($code); }, array_keys($this->getCurrencies()) @@ -74,11 +78,16 @@ function ($code) { /** * Returns a map of known currencies indexed by code. * - * @return array + * @psalm-return non-empty-array */ - private function getCurrencies() + private function getCurrencies(): array { - if (null === self::$currencies) { + if (self::$currencies === null) { self::$currencies = $this->loadCurrencies(); } @@ -86,16 +95,24 @@ private function getCurrencies() } /** - * @return array + * @psalm-return non-empty-array + * + * @psalm-suppress MixedInferredReturnType `include` cannot be inferred here + * @psalm-suppress MixedReturnStatement `include` cannot be inferred here */ - private function loadCurrencies() + private function loadCurrencies(): array { - $file = __DIR__.'/../../resources/currency.php'; + $file = __DIR__ . '/../../resources/currency.php'; - if (file_exists($file)) { + if (is_file($file)) { return require $file; } - throw new \RuntimeException('Failed to load currency ISO codes.'); + throw new RuntimeException('Failed to load currency ISO codes.'); } } diff --git a/src/Currency.php b/src/Currency.php index c81f23687..537026040 100644 --- a/src/Currency.php +++ b/src/Currency.php @@ -1,75 +1,52 @@ code = $code; } /** * Returns the currency code. * - * @return string + * @psalm-return non-empty-string */ - public function getCode() + public function getCode(): string { return $this->code; } /** * Checks whether this currency is the same as an other. - * - * @return bool */ - public function equals(Currency $other) + public function equals(Currency $other): bool { return $this->code === $other->code; } - /** - * Checks whether this currency is available in the passed context. - * - * @return bool - */ - public function isAvailableWithin(Currencies $currencies) - { - return $currencies->contains($this); - } - - /** - * @return string - */ - public function __toString() + public function __toString(): string { return $this->code; } diff --git a/src/CurrencyPair.php b/src/CurrencyPair.php index 6e7eba927..324cdebc7 100644 --- a/src/CurrencyPair.php +++ b/src/CurrencyPair.php @@ -1,49 +1,45 @@ counterCurrency = $counterCurrency; - $this->baseCurrency = $baseCurrency; - $this->conversionRatio = (float) $conversionRatio; + $this->baseCurrency = $baseCurrency; + $this->conversionRatio = $conversionRatio; } /** @@ -51,41 +47,39 @@ public function __construct(Currency $baseCurrency, Currency $counterCurrency, $ * * @param string $iso String representation of the form "EUR/USD 1.2500" * - * @return CurrencyPair - * - * @throws \InvalidArgumentException Format of $iso is invalid + * @throws InvalidArgumentException Format of $iso is invalid. */ - public static function createFromIso($iso) + public static function createFromIso(string $iso): CurrencyPair { $currency = '([A-Z]{2,3})'; - $ratio = "([0-9]*\.?[0-9]+)"; // @see http://www.regular-expressions.info/floatingpoint.html - $pattern = '#'.$currency.'/'.$currency.' '.$ratio.'#'; + $ratio = '([0-9]*\.?[0-9]+)'; // @see http://www.regular-expressions.info/floatingpoint.html + $pattern = '#' . $currency . '/' . $currency . ' ' . $ratio . '#'; $matches = []; - if (!preg_match($pattern, $iso, $matches)) { - throw new \InvalidArgumentException(sprintf('Cannot create currency pair from ISO string "%s", format of string is invalid', $iso)); + if (! preg_match($pattern, $iso, $matches)) { + throw new InvalidArgumentException(sprintf('Cannot create currency pair from ISO string "%s", format of string is invalid', $iso)); } + assert(! empty($matches[1])); + assert(is_numeric($matches[2])); + assert(is_numeric($matches[3])); + return new self(new Currency($matches[1]), new Currency($matches[2]), $matches[3]); } /** * Returns the counter currency. - * - * @return Currency */ - public function getCounterCurrency() + public function getCounterCurrency(): Currency { return $this->counterCurrency; } /** * Returns the base currency. - * - * @return Currency */ - public function getBaseCurrency() + public function getBaseCurrency(): Currency { return $this->baseCurrency; } @@ -93,25 +87,21 @@ public function getBaseCurrency() /** * Returns the conversion ratio. * - * @return float + * @psalm-return numeric-string */ - public function getConversionRatio() + public function getConversionRatio(): string { return $this->conversionRatio; } /** * Checks if an other CurrencyPair has the same parameters as this. - * - * @return bool */ - public function equals(CurrencyPair $other) + public function equals(CurrencyPair $other): bool { - return - $this->baseCurrency->equals($other->baseCurrency) + return $this->baseCurrency->equals($other->baseCurrency) && $this->counterCurrency->equals($other->counterCurrency) - && $this->conversionRatio === $other->conversionRatio - ; + && $this->conversionRatio === $other->conversionRatio; } /** diff --git a/src/Exception.php b/src/Exception.php index 1c5cbdabd..eea280a24 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -1,11 +1,11 @@ */ interface Exception { diff --git a/src/Exception/FormatterException.php b/src/Exception/FormatterException.php index fc3340503..7562e0768 100644 --- a/src/Exception/FormatterException.php +++ b/src/Exception/FormatterException.php @@ -1,14 +1,15 @@ */ -final class FormatterException extends \RuntimeException implements Exception +final class FormatterException extends RuntimeException implements Exception { } diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php new file mode 100644 index 000000000..2c8a13341 --- /dev/null +++ b/src/Exception/InvalidArgumentException.php @@ -0,0 +1,23 @@ + */ -final class ParserException extends \RuntimeException implements Exception +final class ParserException extends RuntimeException implements Exception { } diff --git a/src/Exception/UnknownCurrencyException.php b/src/Exception/UnknownCurrencyException.php index ea418330e..3c3d7187b 100644 --- a/src/Exception/UnknownCurrencyException.php +++ b/src/Exception/UnknownCurrencyException.php @@ -1,14 +1,15 @@ */ -final class UnknownCurrencyException extends \DomainException implements Exception +final class UnknownCurrencyException extends DomainException implements Exception { } diff --git a/src/Exception/UnresolvableCurrencyPairException.php b/src/Exception/UnresolvableCurrencyPairException.php index f48f50ff0..1a35efafc 100644 --- a/src/Exception/UnresolvableCurrencyPairException.php +++ b/src/Exception/UnresolvableCurrencyPairException.php @@ -1,23 +1,24 @@ */ -final class UnresolvableCurrencyPairException extends \InvalidArgumentException implements Exception +final class UnresolvableCurrencyPairException extends InvalidArgumentException implements Exception { /** * Creates an exception from Currency objects. - * - * @return UnresolvableCurrencyPairException */ - public static function createFromCurrencies(Currency $baseCurrency, Currency $counterCurrency) + public static function createFromCurrencies(Currency $baseCurrency, Currency $counterCurrency): UnresolvableCurrencyPairException { $message = sprintf( 'Cannot resolve a currency pair for currencies: %s/%s', diff --git a/src/Exchange.php b/src/Exchange.php index 972addb05..5c559d918 100644 --- a/src/Exchange.php +++ b/src/Exchange.php @@ -1,22 +1,20 @@ */ interface Exchange { /** * Returns a currency pair for the passed currencies with the rate coming from a third-party source. * - * @return CurrencyPair - * - * @throws UnresolvableCurrencyPairException When there is no currency pair (rate) available for the given currencies + * @throws UnresolvableCurrencyPairException When there is no currency pair (rate) available for the given currencies. */ - public function quote(Currency $baseCurrency, Currency $counterCurrency); + public function quote(Currency $baseCurrency, Currency $counterCurrency): CurrencyPair; } diff --git a/src/Exchange/ExchangerExchange.php b/src/Exchange/ExchangerExchange.php index ec3633fbb..9d91992cb 100644 --- a/src/Exchange/ExchangerExchange.php +++ b/src/Exchange/ExchangerExchange.php @@ -1,5 +1,7 @@ */ final class ExchangerExchange implements Exchange { - /** - * @var ExchangeRateProvider - */ - private $exchanger; + private ExchangeRateProvider $exchanger; public function __construct(ExchangeRateProvider $exchanger) { $this->exchanger = $exchanger; } - /** - * {@inheritdoc} - */ - public function quote(Currency $baseCurrency, Currency $counterCurrency) + public function quote(Currency $baseCurrency, Currency $counterCurrency): CurrencyPair { try { $query = new ExchangeRateQuery( new ExchangerCurrencyPair($baseCurrency->getCode(), $counterCurrency->getCode()) ); - $rate = $this->exchanger->getExchangeRate($query); - } catch (ExchangerException $e) { + $rate = $this->exchanger->getExchangeRate($query); + } catch (ExchangerException) { throw UnresolvableCurrencyPairException::createFromCurrencies($baseCurrency, $counterCurrency); } - return new CurrencyPair($baseCurrency, $counterCurrency, $rate->getValue()); + $rateValue = sprintf('%.14F', $rate->getValue()); + + assert(is_numeric($rateValue)); + + return new CurrencyPair($baseCurrency, $counterCurrency, $rateValue); } } diff --git a/src/Exchange/FixedExchange.php b/src/Exchange/FixedExchange.php index 7253222c2..204c5efbb 100644 --- a/src/Exchange/FixedExchange.php +++ b/src/Exchange/FixedExchange.php @@ -1,5 +1,7 @@ */ final class FixedExchange implements Exchange { - /** - * @var array - */ - private $list; + /** @psalm-var array> */ + private array $list; + /** @psalm-param array> $list */ public function __construct(array $list) { $this->list = $list; } - /** - * {@inheritdoc} - */ - public function quote(Currency $baseCurrency, Currency $counterCurrency) + public function quote(Currency $baseCurrency, Currency $counterCurrency): CurrencyPair { if (isset($this->list[$baseCurrency->getCode()][$counterCurrency->getCode()])) { return new CurrencyPair( diff --git a/src/Exchange/IndirectExchange.php b/src/Exchange/IndirectExchange.php index 6811c4d85..08bdddcaa 100644 --- a/src/Exchange/IndirectExchange.php +++ b/src/Exchange/IndirectExchange.php @@ -1,77 +1,65 @@ */ final class IndirectExchange implements Exchange { /** * @var Calculator + * @psalm-var class-string */ - private static $calculator; + private static string $calculator = BcMathCalculator::class; - /** - * @var array - */ - private static $calculators = [ - BcMathCalculator::class, - GmpCalculator::class, - PhpCalculator::class, - ]; + private Currencies $currencies; - /** - * @var Currencies - */ - private $currencies; - - /** - * @var Exchange - */ - private $exchange; + private Exchange $exchange; public function __construct(Exchange $exchange, Currencies $currencies) { - $this->exchange = $exchange; + $this->exchange = $exchange; $this->currencies = $currencies; } - /** - * @param string $calculator - */ - public static function registerCalculator($calculator) + /** @psalm-param class-string $calculator */ + public static function registerCalculator(string $calculator): void { - if (is_a($calculator, Calculator::class, true) === false) { - throw new \InvalidArgumentException('Calculator must implement '.Calculator::class); - } - - array_unshift(self::$calculators, $calculator); + self::$calculator = $calculator; } - /** - * {@inheritdoc} - */ - public function quote(Currency $baseCurrency, Currency $counterCurrency) + public function quote(Currency $baseCurrency, Currency $counterCurrency): CurrencyPair { try { return $this->exchange->quote($baseCurrency, $counterCurrency); - } catch (UnresolvableCurrencyPairException $exception) { - $rate = array_reduce($this->getConversions($baseCurrency, $counterCurrency), function ($carry, CurrencyPair $pair) { - return static::getCalculator()->multiply($carry, $pair->getConversionRatio()); - }, '1.0'); + } catch (UnresolvableCurrencyPairException) { + $rate = array_reduce( + $this->getConversions($baseCurrency, $counterCurrency), + /** + * @psalm-param numeric-string $carry + * + * @psalm-return numeric-string + */ + static function (string $carry, CurrencyPair $pair) { + return self::$calculator::multiply($carry, $pair->getConversionRatio()); + }, + '1.0' + ); return new CurrencyPair($baseCurrency, $counterCurrency, $rate); } @@ -82,48 +70,48 @@ public function quote(Currency $baseCurrency, Currency $counterCurrency) * * @throws UnresolvableCurrencyPairException */ - private function getConversions(Currency $baseCurrency, Currency $counterCurrency) + private function getConversions(Currency $baseCurrency, Currency $counterCurrency): array { - $startNode = $this->initializeNode($baseCurrency); + $startNode = new IndirectExchangeQueuedItem($baseCurrency); $startNode->discovered = true; + /** @psalm-var array $nodes */ $nodes = [$baseCurrency->getCode() => $startNode]; - $frontier = new \SplQueue(); + /** @psam-var SplQueue $frontier */ + $frontier = new SplQueue(); $frontier->enqueue($startNode); while ($frontier->count()) { - /** @var \stdClass $currentNode */ - $currentNode = $frontier->dequeue(); - - /** @var Currency $currentCurrency */ + /** @psalm-var IndirectExchangeQueuedItem $currentNode */ + $currentNode = $frontier->dequeue(); $currentCurrency = $currentNode->currency; if ($currentCurrency->equals($counterCurrency)) { return $this->reconstructConversionChain($nodes, $currentNode); } - /** @var Currency $candidateCurrency */ foreach ($this->currencies as $candidateCurrency) { - if (!isset($nodes[$candidateCurrency->getCode()])) { - $nodes[$candidateCurrency->getCode()] = $this->initializeNode($candidateCurrency); + if (! isset($nodes[$candidateCurrency->getCode()])) { + $nodes[$candidateCurrency->getCode()] = new IndirectExchangeQueuedItem($candidateCurrency); } - /** @var \stdClass $node */ $node = $nodes[$candidateCurrency->getCode()]; - if (!$node->discovered) { - try { - // Check if the candidate is a neighbor. This will throw an exception if it isn't. - $this->exchange->quote($currentCurrency, $candidateCurrency); + if ($node->discovered) { + continue; + } - $node->discovered = true; - $node->parent = $currentNode; + try { + // Check if the candidate is a neighbor. This will throw an exception if it isn't. + $this->exchange->quote($currentCurrency, $candidateCurrency); - $frontier->enqueue($node); - } catch (UnresolvableCurrencyPairException $exception) { - // Not a neighbor. Move on. - } + $node->discovered = true; + $node->parent = $currentNode; + + $frontier->enqueue($node); + } catch (UnresolvableCurrencyPairException $exception) { + // Not a neighbor. Move on. } } } @@ -132,64 +120,22 @@ private function getConversions(Currency $baseCurrency, Currency $counterCurrenc } /** - * @return \stdClass - */ - private function initializeNode(Currency $currency) - { - $node = new \stdClass(); - - $node->currency = $currency; - $node->discovered = false; - $node->parent = null; - - return $node; - } - - /** + * @psalm-param array $currencies + * * @return CurrencyPair[] + * @psalm-return list */ - private function reconstructConversionChain(array $currencies, \stdClass $goalNode) + private function reconstructConversionChain(array $currencies, IndirectExchangeQueuedItem $goalNode): array { - $current = $goalNode; + $current = $goalNode; $conversions = []; while ($current->parent) { - $previous = $currencies[$current->parent->currency->getCode()]; + $previous = $currencies[$current->parent->currency->getCode()]; $conversions[] = $this->exchange->quote($previous->currency, $current->currency); - $current = $previous; + $current = $previous; } return array_reverse($conversions); } - - /** - * @return Calculator - */ - private function getCalculator() - { - if (null === self::$calculator) { - self::$calculator = self::initializeCalculator(); - } - - return self::$calculator; - } - - /** - * @return Calculator - * - * @throws \RuntimeException If cannot find calculator for money calculations - */ - private static function initializeCalculator() - { - $calculators = self::$calculators; - - foreach ($calculators as $calculator) { - /** @var Calculator $calculator */ - if ($calculator::supported()) { - return new $calculator(); - } - } - - throw new \RuntimeException('Cannot find calculator for money calculations'); - } } diff --git a/src/Exchange/IndirectExchangeQueuedItem.php b/src/Exchange/IndirectExchangeQueuedItem.php new file mode 100644 index 000000000..7656c88e9 --- /dev/null +++ b/src/Exchange/IndirectExchangeQueuedItem.php @@ -0,0 +1,20 @@ +currency = $currency; + } +} diff --git a/src/Exchange/ReversedCurrenciesExchange.php b/src/Exchange/ReversedCurrenciesExchange.php index 8f1205ec0..50c44fb24 100644 --- a/src/Exchange/ReversedCurrenciesExchange.php +++ b/src/Exchange/ReversedCurrenciesExchange.php @@ -1,5 +1,7 @@ */ final class ReversedCurrenciesExchange implements Exchange { - /** - * @var Exchange - */ - private $exchange; + private Exchange $exchange; public function __construct(Exchange $exchange) { $this->exchange = $exchange; } - /** - * {@inheritdoc} - */ - public function quote(Currency $baseCurrency, Currency $counterCurrency) + public function quote(Currency $baseCurrency, Currency $counterCurrency): CurrencyPair { try { return $this->exchange->quote($baseCurrency, $counterCurrency); @@ -37,8 +31,12 @@ public function quote(Currency $baseCurrency, Currency $counterCurrency) try { $currencyPair = $this->exchange->quote($counterCurrency, $baseCurrency); - return new CurrencyPair($baseCurrency, $counterCurrency, 1 / $currencyPair->getConversionRatio()); - } catch (UnresolvableCurrencyPairException $inversedException) { + return new CurrencyPair( + $baseCurrency, + $counterCurrency, + (string) (1.0 / (float) $currencyPair->getConversionRatio()) + ); + } catch (UnresolvableCurrencyPairException) { throw $exception; } } diff --git a/src/Exchange/SwapExchange.php b/src/Exchange/SwapExchange.php index 4c75e7eeb..08f8c3e39 100644 --- a/src/Exchange/SwapExchange.php +++ b/src/Exchange/SwapExchange.php @@ -1,5 +1,7 @@ */ final class SwapExchange implements Exchange { - /** - * @var Swap - */ - private $swap; + private Swap $swap; public function __construct(Swap $swap) { $this->swap = $swap; } - /** - * {@inheritdoc} - */ - public function quote(Currency $baseCurrency, Currency $counterCurrency) + public function quote(Currency $baseCurrency, Currency $counterCurrency): CurrencyPair { try { - $rate = $this->swap->latest($baseCurrency->getCode().'/'.$counterCurrency->getCode()); - } catch (ExchangerException $e) { + $rate = $this->swap->latest($baseCurrency->getCode() . '/' . $counterCurrency->getCode()); + } catch (ExchangerException) { throw UnresolvableCurrencyPairException::createFromCurrencies($baseCurrency, $counterCurrency); } - return new CurrencyPair($baseCurrency, $counterCurrency, $rate->getValue()); + return new CurrencyPair($baseCurrency, $counterCurrency, (string) $rate->getValue()); } } diff --git a/src/Formatter/AggregateMoneyFormatter.php b/src/Formatter/AggregateMoneyFormatter.php index 5b912e7f8..3eb6c951c 100644 --- a/src/Formatter/AggregateMoneyFormatter.php +++ b/src/Formatter/AggregateMoneyFormatter.php @@ -1,5 +1,7 @@ */ final class AggregateMoneyFormatter implements MoneyFormatter { /** - * @var MoneyFormatter[] + * @var MoneyFormatter[] indexed by currency code + * @psalm-var non-empty-array indexed by currency code */ - private $formatters = []; + private array $formatters; /** - * @param MoneyFormatter[] $formatters + * @param MoneyFormatter[] $formatters indexed by currency code + * @psalm-param non-empty-array $formatters indexed by currency code */ public function __construct(array $formatters) { - if (empty($formatters)) { - throw new \InvalidArgumentException(sprintf('Initialize an empty %s is not possible', self::class)); - } - - foreach ($formatters as $currencyCode => $formatter) { - if (false === $formatter instanceof MoneyFormatter) { - throw new \InvalidArgumentException('All formatters must implement '.MoneyFormatter::class); - } - - $this->formatters[$currencyCode] = $formatter; - } + $this->formatters = $formatters; } - /** - * {@inheritdoc} - */ - public function format(Money $money) + public function format(Money $money): string { $currencyCode = $money->getCurrency()->getCode(); @@ -51,6 +40,6 @@ public function format(Money $money) return $this->formatters['*']->format($money); } - throw new FormatterException('No formatter found for currency '.$currencyCode); + throw new FormatterException('No formatter found for currency ' . $currencyCode); } } diff --git a/src/Formatter/BitcoinMoneyFormatter.php b/src/Formatter/BitcoinMoneyFormatter.php index 4979ab93e..7eca31c61 100644 --- a/src/Formatter/BitcoinMoneyFormatter.php +++ b/src/Formatter/BitcoinMoneyFormatter.php @@ -1,5 +1,7 @@ */ final class BitcoinMoneyFormatter implements MoneyFormatter { - /** - * @var int - */ - private $fractionDigits; - - /** - * @var Currencies - */ - private $currencies; - - /** - * @param int $fractionDigits - */ - public function __construct($fractionDigits, Currencies $currencies) + private int $fractionDigits; + + private Currencies $currencies; + + public function __construct(int $fractionDigits, Currencies $currencies) { $this->fractionDigits = $fractionDigits; - $this->currencies = $currencies; + $this->currencies = $currencies; } - /** - * {@inheritdoc} - */ - public function format(Money $money) + public function format(Money $money): string { - if (BitcoinCurrencies::CODE !== $money->getCurrency()->getCode()) { + if ($money->getCurrency()->getCode() !== BitcoinCurrencies::CODE) { throw new FormatterException('Bitcoin Formatter can only format Bitcoin currency'); } $valueBase = $money->getAmount(); - $negative = false; + $negative = false; - if ('-' === $valueBase[0]) { - $negative = true; + if ($valueBase[0] === '-') { + $negative = true; $valueBase = substr($valueBase, 1); } - $subunit = $this->currencies->subunitFor($money->getCurrency()); - $valueBase = Number::roundMoneyValue($valueBase, $this->fractionDigits, $subunit); + $subunit = $this->currencies->subunitFor($money->getCurrency()); + $valueBase = Number::roundMoneyValue($valueBase, $this->fractionDigits, $subunit); $valueLength = strlen($valueBase); if ($valueLength > $subunit) { @@ -64,22 +57,22 @@ public function format(Money $money) $formatted .= substr($valueBase, $valueLength - $subunit); } } else { - $formatted = '0.'.str_pad('', $subunit - $valueLength, '0').$valueBase; + $formatted = '0.' . str_pad('', $subunit - $valueLength, '0') . $valueBase; } if ($this->fractionDigits === 0) { - $formatted = substr($formatted, 0, strpos($formatted, '.')); + $formatted = substr($formatted, 0, (int) strpos($formatted, '.')); } elseif ($this->fractionDigits > $subunit) { $formatted .= str_pad('', $this->fractionDigits - $subunit, '0'); } elseif ($this->fractionDigits < $subunit) { - $lastDigit = strpos($formatted, '.') + $this->fractionDigits + 1; + $lastDigit = (int) strpos($formatted, '.') + $this->fractionDigits + 1; $formatted = substr($formatted, 0, $lastDigit); } - $formatted = BitcoinCurrencies::SYMBOL.$formatted; + $formatted = BitcoinCurrencies::SYMBOL . $formatted; - if (true === $negative) { - $formatted = '-'.$formatted; + if ($negative) { + $formatted = '-' . $formatted; } return $formatted; diff --git a/src/Formatter/DecimalMoneyFormatter.php b/src/Formatter/DecimalMoneyFormatter.php index 84465fa84..db57bb6b0 100644 --- a/src/Formatter/DecimalMoneyFormatter.php +++ b/src/Formatter/DecimalMoneyFormatter.php @@ -1,59 +1,59 @@ */ final class DecimalMoneyFormatter implements MoneyFormatter { - /** - * @var Currencies - */ - private $currencies; + private Currencies $currencies; public function __construct(Currencies $currencies) { $this->currencies = $currencies; } - /** - * {@inheritdoc} - */ - public function format(Money $money) + public function format(Money $money): string { $valueBase = $money->getAmount(); - $negative = false; + $negative = $valueBase[0] === '-'; - if ($valueBase[0] === '-') { - $negative = true; + if ($negative) { $valueBase = substr($valueBase, 1); } - $subunit = $this->currencies->subunitFor($money->getCurrency()); + $subunit = $this->currencies->subunitFor($money->getCurrency()); $valueLength = strlen($valueBase); if ($valueLength > $subunit) { - $formatted = substr($valueBase, 0, $valueLength - $subunit); + $formatted = substr($valueBase, 0, $valueLength - $subunit); $decimalDigits = substr($valueBase, $valueLength - $subunit); if (strlen($decimalDigits) > 0) { - $formatted .= '.'.$decimalDigits; + $formatted .= '.' . $decimalDigits; } } else { - $formatted = '0.'.str_pad('', $subunit - $valueLength, '0').$valueBase; + $formatted = '0.' . str_pad('', $subunit - $valueLength, '0') . $valueBase; } - if ($negative === true) { - $formatted = '-'.$formatted; + if ($negative) { + return '-' . $formatted; } + assert(! empty($formatted)); + return $formatted; } } diff --git a/src/Formatter/IntlLocalizedDecimalFormatter.php b/src/Formatter/IntlLocalizedDecimalFormatter.php index 0c550d768..efcccccfa 100644 --- a/src/Formatter/IntlLocalizedDecimalFormatter.php +++ b/src/Formatter/IntlLocalizedDecimalFormatter.php @@ -1,65 +1,66 @@ */ final class IntlLocalizedDecimalFormatter implements MoneyFormatter { - /** - * @var \NumberFormatter - */ - private $formatter; + private NumberFormatter $formatter; - /** - * @var Currencies - */ - private $currencies; + private Currencies $currencies; - public function __construct(\NumberFormatter $formatter, Currencies $currencies) + public function __construct(NumberFormatter $formatter, Currencies $currencies) { - $this->formatter = $formatter; + $this->formatter = $formatter; $this->currencies = $currencies; } - /** - * {@inheritdoc} - */ - public function format(Money $money) + public function format(Money $money): string { $valueBase = $money->getAmount(); - $negative = false; + $negative = $valueBase[0] === '-'; - if ($valueBase[0] === '-') { - $negative = true; + if ($negative) { $valueBase = substr($valueBase, 1); } - $subunit = $this->currencies->subunitFor($money->getCurrency()); + $subunit = $this->currencies->subunitFor($money->getCurrency()); $valueLength = strlen($valueBase); if ($valueLength > $subunit) { - $formatted = substr($valueBase, 0, $valueLength - $subunit); + $formatted = substr($valueBase, 0, $valueLength - $subunit); $decimalDigits = substr($valueBase, $valueLength - $subunit); if (strlen($decimalDigits) > 0) { - $formatted .= '.'.$decimalDigits; + $formatted .= '.' . $decimalDigits; } } else { - $formatted = '0.'.str_pad('', $subunit - $valueLength, '0').$valueBase; + $formatted = '0.' . str_pad('', $subunit - $valueLength, '0') . $valueBase; } - if ($negative === true) { - $formatted = '-'.$formatted; + if ($negative) { + $formatted = '-' . $formatted; } - return $this->formatter->format($formatted); + $formatted = $this->formatter->format((float) $formatted); + + assert(is_string($formatted) && ! empty($formatted)); + + return $formatted; } } diff --git a/src/Formatter/IntlMoneyFormatter.php b/src/Formatter/IntlMoneyFormatter.php index 3f0c7d1bd..660b17f3f 100644 --- a/src/Formatter/IntlMoneyFormatter.php +++ b/src/Formatter/IntlMoneyFormatter.php @@ -1,65 +1,65 @@ */ final class IntlMoneyFormatter implements MoneyFormatter { - /** - * @var \NumberFormatter - */ - private $formatter; + private NumberFormatter $formatter; - /** - * @var Currencies - */ - private $currencies; + private Currencies $currencies; - public function __construct(\NumberFormatter $formatter, Currencies $currencies) + public function __construct(NumberFormatter $formatter, Currencies $currencies) { - $this->formatter = $formatter; + $this->formatter = $formatter; $this->currencies = $currencies; } - /** - * {@inheritdoc} - */ - public function format(Money $money) + public function format(Money $money): string { $valueBase = $money->getAmount(); - $negative = false; + $negative = $valueBase[0] === '-'; - if ($valueBase[0] === '-') { - $negative = true; + if ($negative) { $valueBase = substr($valueBase, 1); } - $subunit = $this->currencies->subunitFor($money->getCurrency()); + $subunit = $this->currencies->subunitFor($money->getCurrency()); $valueLength = strlen($valueBase); if ($valueLength > $subunit) { - $formatted = substr($valueBase, 0, $valueLength - $subunit); + $formatted = substr($valueBase, 0, $valueLength - $subunit); $decimalDigits = substr($valueBase, $valueLength - $subunit); if (strlen($decimalDigits) > 0) { - $formatted .= '.'.$decimalDigits; + $formatted .= '.' . $decimalDigits; } } else { - $formatted = '0.'.str_pad('', $subunit - $valueLength, '0').$valueBase; + $formatted = '0.' . str_pad('', $subunit - $valueLength, '0') . $valueBase; } - if ($negative === true) { - $formatted = '-'.$formatted; + if ($negative) { + $formatted = '-' . $formatted; } - return $this->formatter->formatCurrency($formatted, $money->getCurrency()->getCode()); + $formatted = $this->formatter->formatCurrency((float) $formatted, $money->getCurrency()->getCode()); + + assert(! empty($formatted)); + + return $formatted; } } diff --git a/src/Money.php b/src/Money.php index 891660228..d3252e9d4 100644 --- a/src/Money.php +++ b/src/Money.php @@ -1,180 +1,158 @@ */ - private static $calculator; - - /** - * @var array - */ - private static $calculators = [ - BcMathCalculator::class, - GmpCalculator::class, - PhpCalculator::class, - ]; + private static string $calculator = BcMathCalculator::class; /** * @param int|string $amount Amount, expressed in the smallest units of $currency (eg cents) + * @psalm-param int|numeric-string $amount * - * @throws \InvalidArgumentException If amount is not integer + * @throws InvalidArgumentException If amount is not integer. */ - public function __construct($amount, Currency $currency) + public function __construct(int|string $amount, Currency $currency) { + $this->currency = $currency; + if (filter_var($amount, FILTER_VALIDATE_INT) === false) { - $numberFromString = Number::fromString($amount); - if (!$numberFromString->isInteger()) { - throw new \InvalidArgumentException('Amount must be an integer(ish) value'); + $numberFromString = Number::fromString((string) $amount); + if (! $numberFromString->isInteger()) { + throw new InvalidArgumentException('Amount must be an integer(ish) value'); } - $amount = $numberFromString->getIntegerPart(); + $this->amount = $numberFromString->getIntegerPart(); + + return; } $this->amount = (string) $amount; - $this->currency = $currency; - } - - /** - * Returns a new Money instance based on the current one using the Currency. - * - * @param int|string $amount - * - * @return Money - * - * @throws \InvalidArgumentException If amount is not integer - */ - private function newInstance($amount) - { - return new self($amount, $this->currency); } /** * Checks whether a Money has the same Currency as this. - * - * @return bool */ - public function isSameCurrency(Money $other) + public function isSameCurrency(Money $other): bool { - return $this->currency->equals($other->currency); + // Note: non-strict equality is intentional here, since `Currency` is `final` and reliable. + return $this->currency == $other->currency; } /** - * Asserts that a Money has the same currency as this. - * - * @throws \InvalidArgumentException If $other has a different currency + * Checks whether the value represented by this object equals to the other. */ - private function assertSameCurrency(Money $other) + public function equals(Money $other): bool { - if (!$this->isSameCurrency($other)) { - throw new \InvalidArgumentException('Currencies must be identical'); + // Note: non-strict equality is intentional here, since `Currency` is `final` and reliable. + if ($this->currency != $other->currency) { + return false; } - } - /** - * Checks whether the value represented by this object equals to the other. - * - * @return bool - */ - public function equals(Money $other) - { - return $this->isSameCurrency($other) && $this->amount === $other->amount; + if ($this->amount === $other->amount) { + return true; + } + + // @TODO do we want Money instance to be byte-equivalent when trailing zeroes exist? Very expensive! + // Assumption: Money#equals() is called **less** than other number-based comparisons, and probably + // only within test suites. Therefore, using complex normalization here is acceptable waste of performance. + return $this->compare($other) === 0; } /** * Returns an integer less than, equal to, or greater than zero * if the value of this object is considered to be respectively * less than, equal to, or greater than the other. - * - * @return int */ - public function compare(Money $other) + public function compare(Money $other): int { - $this->assertSameCurrency($other); + // Note: non-strict equality is intentional here, since `Currency` is `final` and reliable. + if ($this->currency != $other->currency) { + throw new InvalidArgumentException('Currencies must be identical'); + } - return $this->getCalculator()->compare($this->amount, $other->amount); + return self::$calculator::compare($this->amount, $other->amount); } /** * Checks whether the value represented by this object is greater than the other. - * - * @return bool */ - public function greaterThan(Money $other) + public function greaterThan(Money $other): bool { return $this->compare($other) > 0; } - /** - * @param \Money\Money $other - * - * @return bool - */ - public function greaterThanOrEqual(Money $other) + public function greaterThanOrEqual(Money $other): bool { return $this->compare($other) >= 0; } /** * Checks whether the value represented by this object is less than the other. - * - * @return bool */ - public function lessThan(Money $other) + public function lessThan(Money $other): bool { return $this->compare($other) < 0; } - /** - * @param \Money\Money $other - * - * @return bool - */ - public function lessThanOrEqual(Money $other) + public function lessThanOrEqual(Money $other): bool { return $this->compare($other) <= 0; } @@ -182,19 +160,17 @@ public function lessThanOrEqual(Money $other) /** * Returns the value represented by this object. * - * @return string + * @psalm-return numeric-string */ - public function getAmount() + public function getAmount(): string { return $this->amount; } /** * Returns the currency of this object. - * - * @return Currency */ - public function getCurrency() + public function getCurrency(): Currency { return $this->currency; } @@ -204,18 +180,18 @@ public function getCurrency() * the sum of this and an other Money object. * * @param Money[] $addends - * - * @return Money */ - public function add(Money ...$addends) + public function add(Money ...$addends): Money { $amount = $this->amount; - $calculator = $this->getCalculator(); foreach ($addends as $addend) { - $this->assertSameCurrency($addend); + // Note: non-strict equality is intentional here, since `Currency` is `final` and reliable. + if ($this->currency != $addend->currency) { + throw new InvalidArgumentException('Currencies must be identical'); + } - $amount = $calculator->add($amount, $addend->amount); + $amount = self::$calculator::add($amount, $addend->amount); } return new self($amount, $this->currency); @@ -227,158 +203,116 @@ public function add(Money ...$addends) * * @param Money[] $subtrahends * - * @return Money - * * @psalm-pure */ - public function subtract(Money ...$subtrahends) + public function subtract(Money ...$subtrahends): Money { $amount = $this->amount; - $calculator = $this->getCalculator(); foreach ($subtrahends as $subtrahend) { - $this->assertSameCurrency($subtrahend); + // Note: non-strict equality is intentional here, since `Currency` is `final` and reliable. + if ($this->currency != $subtrahend->currency) { + throw new InvalidArgumentException('Currencies must be identical'); + } - $amount = $calculator->subtract($amount, $subtrahend->amount); + $amount = self::$calculator::subtract($amount, $subtrahend->amount); } return new self($amount, $this->currency); } - /** - * Asserts that the operand is integer or float. - * - * @param float|int|string $operand - * - * @throws \InvalidArgumentException If $operand is neither integer nor float - */ - private function assertOperand($operand) - { - if (!is_numeric($operand)) { - throw new \InvalidArgumentException(sprintf('Operand should be a numeric value, "%s" given.', is_object($operand) ? get_class($operand) : gettype($operand))); - } - } - - /** - * Asserts that rounding mode is a valid integer value. - * - * @param int $roundingMode - * - * @throws \InvalidArgumentException If $roundingMode is not valid - */ - private function assertRoundingMode($roundingMode) - { - if (!in_array( - $roundingMode, [ - self::ROUND_HALF_DOWN, self::ROUND_HALF_EVEN, self::ROUND_HALF_ODD, - self::ROUND_HALF_UP, self::ROUND_UP, self::ROUND_DOWN, - self::ROUND_HALF_POSITIVE_INFINITY, self::ROUND_HALF_NEGATIVE_INFINITY, - ], true - )) { - throw new \InvalidArgumentException('Rounding mode should be Money::ROUND_HALF_DOWN | '.'Money::ROUND_HALF_EVEN | Money::ROUND_HALF_ODD | '.'Money::ROUND_HALF_UP | Money::ROUND_UP | Money::ROUND_DOWN'.'Money::ROUND_HALF_POSITIVE_INFINITY | Money::ROUND_HALF_NEGATIVE_INFINITY'); - } - } - /** * Returns a new Money object that represents * the multiplied value by the given factor. * - * @param float|int|string $multiplier - * @param int $roundingMode - * - * @return Money + * @psalm-param numeric-string $multiplier + * @psalm-param self::ROUND_* $roundingMode */ - public function multiply($multiplier, $roundingMode = self::ROUND_HALF_UP) + public function multiply(string $multiplier, int $roundingMode = self::ROUND_HALF_UP): Money { - $this->assertOperand($multiplier); - $this->assertRoundingMode($roundingMode); + $product = $this->round(self::$calculator::multiply($this->amount, $multiplier), $roundingMode); - $product = $this->round($this->getCalculator()->multiply($this->amount, $multiplier), $roundingMode); - - return $this->newInstance($product); + return new self($product, $this->currency); } /** * Returns a new Money object that represents * the divided value by the given factor. * - * @param float|int|string $divisor - * @param int $roundingMode - * - * @return Money + * @psalm-param numeric-string $divisor + * @psalm-param self::ROUND_* $roundingMode */ - public function divide($divisor, $roundingMode = self::ROUND_HALF_UP) + public function divide(string $divisor, int $roundingMode = self::ROUND_HALF_UP): Money { - $this->assertOperand($divisor); - $this->assertRoundingMode($roundingMode); - - $divisor = (string) Number::fromNumber($divisor); - - if ($this->getCalculator()->compare($divisor, '0') === 0) { - throw new \InvalidArgumentException('Division by zero'); - } - - $quotient = $this->round($this->getCalculator()->divide($this->amount, $divisor), $roundingMode); + $quotient = $this->round(self::$calculator::divide($this->amount, $divisor), $roundingMode); - return $this->newInstance($quotient); + return new self($quotient, $this->currency); } /** * Returns a new Money object that represents * the remainder after dividing the value by * the given factor. - * - * @return Money */ - public function mod(Money $divisor) + public function mod(Money $divisor): Money { - $this->assertSameCurrency($divisor); + // Note: non-strict equality is intentional here, since `Currency` is `final` and reliable. + if ($this->currency != $divisor->currency) { + throw new InvalidArgumentException('Currencies must be identical'); + } - return new self($this->getCalculator()->mod($this->amount, $divisor->amount), $this->currency); + return new self(self::$calculator::mod($this->amount, $divisor->amount), $this->currency); } /** * Allocate the money according to a list of ratios. * + * @psalm-param TRatios $ratios + * * @return Money[] + * @psalm-return ( + * TRatios is list + * ? non-empty-list + * : non-empty-array + * ) + * + * @template TRatios as non-empty-array */ - public function allocate(array $ratios) + public function allocate(array $ratios): array { - if (count($ratios) === 0) { - throw new \InvalidArgumentException('Cannot allocate to none, ratios cannot be an empty array'); - } - $remainder = $this->amount; - $results = []; - $total = array_sum($ratios); + $results = []; + $total = array_sum($ratios); if ($total <= 0) { - throw new \InvalidArgumentException('Cannot allocate to none, sum of ratios must be greater than zero'); + throw new InvalidArgumentException('Cannot allocate to none, sum of ratios must be greater than zero'); } foreach ($ratios as $key => $ratio) { if ($ratio < 0) { - throw new \InvalidArgumentException('Cannot allocate to none, ratio must be zero or positive'); + throw new InvalidArgumentException('Cannot allocate to none, ratio must be zero or positive'); } - $share = $this->getCalculator()->share($this->amount, $ratio, $total); - $results[$key] = $this->newInstance($share); - $remainder = $this->getCalculator()->subtract($remainder, $share); + + $share = self::$calculator::share($this->amount, (string) $ratio, (string) $total); + $results[$key] = new self($share, $this->currency); + $remainder = self::$calculator::subtract($remainder, $share); } - if ($this->getCalculator()->compare($remainder, '0') === 0) { + if (self::$calculator::compare($remainder, '0') === 0) { return $results; } - $fractions = array_map(function ($ratio) use ($total) { - $share = ($ratio / $total) * $this->amount; + $amount = $this->amount; + $fractions = array_map(static function (float|int $ratio) use ($total, $amount) { + $share = (float) $ratio / $total * (float) $amount; return $share - floor($share); }, $ratios); - while ($this->getCalculator()->compare($remainder, '0') > 0) { - $index = !empty($fractions) ? array_keys($fractions, max($fractions))[0] : 0; - $results[$index]->amount = $this->getCalculator()->add($results[$index]->amount, '1'); - $remainder = $this->getCalculator()->subtract($remainder, '1'); + while (self::$calculator::compare($remainder, '0') > 0) { + $index = ! empty($fractions) ? array_keys($fractions, max($fractions))[0] : 0; + $results[$index] = new self(self::$calculator::add($results[$index]->amount, '1'), $results[$index]->currency); + $remainder = self::$calculator::subtract($remainder, '1'); unset($fractions[$index]); } @@ -388,102 +322,85 @@ public function allocate(array $ratios) /** * Allocate the money among N targets. * - * @param int $n + * @psalm-param positive-int $n * * @return Money[] + * @psalm-return non-empty-list * - * @throws \InvalidArgumentException If number of targets is not an integer + * @throws InvalidArgumentException If number of targets is not an integer. */ - public function allocateTo($n) + public function allocateTo(int $n): array { - if (!is_int($n)) { - throw new \InvalidArgumentException('Number of targets must be an integer'); - } - - if ($n <= 0) { - throw new \InvalidArgumentException('Cannot allocate to none, target must be greater than zero'); - } - return $this->allocate(array_fill(0, $n, 1)); } /** - * @return string + * @throws InvalidArgumentException if the given $money is zero. */ - public function ratioOf(Money $money) + public function ratioOf(Money $money): string { if ($money->isZero()) { - throw new \InvalidArgumentException('Cannot calculate a ratio of zero'); + throw new InvalidArgumentException('Cannot calculate a ratio of zero'); } - return $this->getCalculator()->divide($this->amount, $money->amount); + return self::$calculator::divide($this->amount, $money->amount); } /** - * @param string $amount - * @param int $rounding_mode + * @psalm-param numeric-string $amount + * @psalm-param self::ROUND_* $rounding_mode * - * @return string + * @psalm-return numeric-string */ - private function round($amount, $rounding_mode) + private function round(string $amount, int $rounding_mode): string { - $this->assertRoundingMode($rounding_mode); - if ($rounding_mode === self::ROUND_UP) { - return $this->getCalculator()->ceil($amount); + return self::$calculator::ceil($amount); } if ($rounding_mode === self::ROUND_DOWN) { - return $this->getCalculator()->floor($amount); + return self::$calculator::floor($amount); } - return $this->getCalculator()->round($amount, $rounding_mode); + return self::$calculator::round($amount, $rounding_mode); } - /** - * @return Money - */ - public function absolute() + public function absolute(): Money { - return $this->newInstance($this->getCalculator()->absolute($this->amount)); + return new self( + self::$calculator::absolute($this->amount), + $this->currency + ); } - /** - * @return Money - */ - public function negative() + public function negative(): Money { - return $this->newInstance(0)->subtract($this); + return (new self(0, $this->currency)) + ->subtract($this); } /** * Checks if the value represented by this object is zero. - * - * @return bool */ - public function isZero() + public function isZero(): bool { - return $this->getCalculator()->compare($this->amount, 0) === 0; + return self::$calculator::compare($this->amount, '0') === 0; } /** * Checks if the value represented by this object is positive. - * - * @return bool */ - public function isPositive() + public function isPositive(): bool { - return $this->getCalculator()->compare($this->amount, 0) > 0; + return self::$calculator::compare($this->amount, '0') > 0; } /** * Checks if the value represented by this object is negative. - * - * @return bool */ - public function isNegative() + public function isNegative(): bool { - return $this->getCalculator()->compare($this->amount, 0) < 0; + return self::$calculator::compare($this->amount, '0') < 0; } /** @@ -503,18 +420,18 @@ public function jsonSerialize() * @param Money $first * @param Money ...$collection * - * @return Money - * * @psalm-pure */ - public static function min(self $first, self ...$collection) + public static function min(self $first, self ...$collection): Money { $min = $first; foreach ($collection as $money) { - if ($money->lessThan($min)) { - $min = $money; + if (! $money->lessThan($min)) { + continue; } + + $min = $money; } return $min; @@ -524,89 +441,38 @@ public static function min(self $first, self ...$collection) * @param Money $first * @param Money ...$collection * - * @return Money - * * @psalm-pure */ - public static function max(self $first, self ...$collection) + public static function max(self $first, self ...$collection): Money { $max = $first; foreach ($collection as $money) { - if ($money->greaterThan($max)) { - $max = $money; + if (! $money->greaterThan($max)) { + continue; } + + $max = $money; } return $max; } - /** - * @param Money $first - * @param Money ...$collection - * - * @return Money - * - * @psalm-pure - */ - public static function sum(self $first, self ...$collection) + /** @psalm-pure */ + public static function sum(self $first, self ...$collection): Money { return $first->add(...$collection); } - /** - * @param Money $first - * @param Money ...$collection - * - * @return Money - * - * @psalm-pure - */ - public static function avg(self $first, self ...$collection) - { - return $first->add(...$collection)->divide(func_num_args()); - } - - /** - * @param string $calculator - */ - public static function registerCalculator($calculator) - { - if (is_a($calculator, Calculator::class, true) === false) { - throw new \InvalidArgumentException('Calculator must implement '.Calculator::class); - } - - array_unshift(self::$calculators, $calculator); - } - - /** - * @return Calculator - * - * @throws \RuntimeException If cannot find calculator for money calculations - */ - private static function initializeCalculator() + /** @psalm-pure */ + public static function avg(self $first, self ...$collection): Money { - $calculators = self::$calculators; - - foreach ($calculators as $calculator) { - /** @var Calculator $calculator */ - if ($calculator::supported()) { - return new $calculator(); - } - } - - throw new \RuntimeException('Cannot find calculator for money calculations'); + return $first->add(...$collection)->divide((string) (count($collection) + 1)); } - /** - * @return Calculator - */ - private function getCalculator() + /** @psalm-param class-string $calculator */ + public static function registerCalculator(string $calculator): void { - if (null === self::$calculator) { - self::$calculator = self::initializeCalculator(); - } - - return self::$calculator; + self::$calculator = $calculator; } } diff --git a/src/MoneyFactory.php b/src/MoneyFactory.php index 3a8bce220..19a9fffdf 100644 --- a/src/MoneyFactory.php +++ b/src/MoneyFactory.php @@ -1,189 +1,194 @@ * - * @param string $method - * @param array $arguments + * @param array $arguments + * @psalm-param non-empty-string $method + * @psalm-param array{numeric-string|int} $arguments * - * @return Money + * @throws InvalidArgumentException If amount is not integer(ish). * - * @throws \InvalidArgumentException If amount is not integer(ish) + * @psalm-pure */ - public static function __callStatic($method, $arguments) + public static function __callStatic(string $method, array $arguments): Money { return new Money($arguments[0], new Currency($method)); } diff --git a/src/MoneyFormatter.php b/src/MoneyFormatter.php index 60ebc6457..7d1146421 100644 --- a/src/MoneyFormatter.php +++ b/src/MoneyFormatter.php @@ -1,20 +1,20 @@ */ interface MoneyFormatter { /** * Formats a Money object as string. * - * @return string + * @psalm-return non-empty-string * * Exception\FormatterException */ - public function format(Money $money); + public function format(Money $money): string; } diff --git a/src/MoneyParser.php b/src/MoneyParser.php index 1a5a6339b..b61bd6afb 100644 --- a/src/MoneyParser.php +++ b/src/MoneyParser.php @@ -1,23 +1,18 @@ */ interface MoneyParser { /** * Parses a string into a Money object (including currency). * - * @param string $money - * @param Currency|string|null $forceCurrency - * - * @return Money - * * @throws Exception\ParserException */ - public function parse($money, $forceCurrency = null); + public function parse(string $money, Currency|null $forceCurrency = null): Money; } diff --git a/src/Number.php b/src/Number.php index b6caa09f1..c34ce11a3 100644 --- a/src/Number.php +++ b/src/Number.php @@ -1,135 +1,100 @@ + * @internal this is an internal utility of the library, and may vary at any time. It is mostly used to internally validate + * that a number is represented at digits, but by improving type system integration, we may be able to completely + * get rid of it. + * + * @psalm-immutable */ final class Number { - /** - * @var string - */ - private $integerPart; - - /** - * @var string - */ - private $fractionalPart; + /** @psalm-var numeric-string */ + private string $integerPart; - /** - * @var array - */ - private static $numbers = [0 => 1, 1 => 1, 2 => 1, 3 => 1, 4 => 1, 5 => 1, 6 => 1, 7 => 1, 8 => 1, 9 => 1]; + /** @psalm-var numeric-string|'' */ + private string $fractionalPart; + private const NUMBERS = [0 => 1, 1 => 1, 2 => 1, 3 => 1, 4 => 1, 5 => 1, 6 => 1, 7 => 1, 8 => 1, 9 => 1]; - /** - * @param string $integerPart - * @param string $fractionalPart - */ - public function __construct($integerPart, $fractionalPart = '') + public function __construct(string $integerPart, string $fractionalPart = '') { - if ('' === $integerPart && '' === $fractionalPart) { - throw new \InvalidArgumentException('Empty number is invalid'); + if ($integerPart === '' && $fractionalPart === '') { + throw new InvalidArgumentException('Empty number is invalid'); } - $this->integerPart = $this->parseIntegerPart((string) $integerPart); - $this->fractionalPart = $this->parseFractionalPart((string) $fractionalPart); + $this->integerPart = self::parseIntegerPart($integerPart); + $this->fractionalPart = self::parseFractionalPart($fractionalPart); } - /** - * @param $number - * - * @return self - */ - public static function fromString($number) + /** @psalm-pure */ + public static function fromString(string $number): self { - $decimalSeparatorPosition = strpos($number, '.'); - if ($decimalSeparatorPosition === false) { - return new self($number, ''); - } + $portions = explode('.', $number, 2); return new self( - substr($number, 0, $decimalSeparatorPosition), - rtrim(substr($number, $decimalSeparatorPosition + 1), '0') + $portions[0], + rtrim($portions[1] ?? '', '0') ); } - /** - * @param float $number - * - * @return self - */ - public static function fromFloat($number) + /** @psalm-pure */ + public static function fromFloat(float $number): self { - if (is_float($number) === false) { - throw new \InvalidArgumentException('Floating point value expected'); - } - return self::fromString(sprintf('%.14F', $number)); } - /** - * @param float|int|string $number - * - * @return self - */ - public static function fromNumber($number) + /** @psalm-pure */ + public static function fromNumber(int|string $number): self { - if (is_float($number)) { - return self::fromString(sprintf('%.14F', $number)); - } - if (is_int($number)) { - return new self($number); - } - - if (is_string($number)) { - return self::fromString($number); + return new self((string) $number); } - throw new \InvalidArgumentException('Valid numeric value expected'); + return self::fromString($number); } - /** - * @return bool - */ - public function isDecimal() + public function isDecimal(): bool { return $this->fractionalPart !== ''; } - /** - * @return bool - */ - public function isInteger() + public function isInteger(): bool { return $this->fractionalPart === ''; } - /** - * @return bool - */ - public function isHalf() + public function isHalf(): bool { return $this->fractionalPart === '5'; } - /** - * @return bool - */ - public function isCurrentEven() + public function isCurrentEven(): bool { - $lastIntegerPartNumber = $this->integerPart[strlen($this->integerPart) - 1]; + $lastIntegerPartNumber = (int) $this->integerPart[strlen($this->integerPart) - 1]; return $lastIntegerPartNumber % 2 === 0; } - /** - * @return bool - */ - public function isCloserToNext() + public function isCloserToNext(): bool { if ($this->fractionalPart === '') { return false; @@ -138,46 +103,36 @@ public function isCloserToNext() return $this->fractionalPart[0] >= 5; } - /** - * @return string - */ - public function __toString() + /** @psalm-return numeric-string */ + public function __toString(): string { if ($this->fractionalPart === '') { return $this->integerPart; } - return $this->integerPart.'.'.$this->fractionalPart; + /** @psalm-suppress LessSpecificReturnStatement this operation is guaranteed to pruduce a numeric-string, but inference can't understand it */ + return $this->integerPart . '.' . $this->fractionalPart; } - /** - * @return bool - */ - public function isNegative() + public function isNegative(): bool { return $this->integerPart[0] === '-'; } - /** - * @return string - */ - public function getIntegerPart() + /** @psalm-return numeric-string */ + public function getIntegerPart(): string { return $this->integerPart; } - /** - * @return string - */ - public function getFractionalPart() + /** @psalm-return numeric-string|'' */ + public function getFractionalPart(): string { return $this->fractionalPart; } - /** - * @return string - */ - public function getIntegerRoundingMultiplier() + /** @psalm-return numeric-string */ + public function getIntegerRoundingMultiplier(): string { if ($this->integerPart[0] === '-') { return '-1'; @@ -186,78 +141,79 @@ public function getIntegerRoundingMultiplier() return '1'; } - /** - * @param int $number - * - * @return self - */ - public function base10($number) + public function base10(int $number): self { - if (!is_int($number)) { - throw new \InvalidArgumentException('Expecting integer'); - } - - if ($this->integerPart === '0' && !$this->fractionalPart) { + if ($this->integerPart === '0' && ! $this->fractionalPart) { return $this; } - $sign = ''; + $sign = ''; $integerPart = $this->integerPart; if ($integerPart[0] === '-') { - $sign = '-'; + $sign = '-'; $integerPart = substr($integerPart, 1); } if ($number >= 0) { - $integerPart = ltrim($integerPart, '0'); + $integerPart = ltrim($integerPart, '0'); $lengthIntegerPart = strlen($integerPart); - $integers = $lengthIntegerPart - min($number, $lengthIntegerPart); - $zeroPad = $number - min($number, $lengthIntegerPart); + $integers = $lengthIntegerPart - min($number, $lengthIntegerPart); + $zeroPad = $number - min($number, $lengthIntegerPart); return new self( - $sign.substr($integerPart, 0, $integers), - rtrim(str_pad('', $zeroPad, '0').substr($integerPart, $integers).$this->fractionalPart, '0') + $sign . substr($integerPart, 0, $integers), + rtrim(str_pad('', $zeroPad, '0') . substr($integerPart, $integers) . $this->fractionalPart, '0') ); } - $number = abs($number); + $number = abs($number); $lengthFractionalPart = strlen($this->fractionalPart); - $fractions = $lengthFractionalPart - min($number, $lengthFractionalPart); - $zeroPad = $number - min($number, $lengthFractionalPart); + $fractions = $lengthFractionalPart - min($number, $lengthFractionalPart); + $zeroPad = $number - min($number, $lengthFractionalPart); return new self( - $sign.ltrim($integerPart.substr($this->fractionalPart, 0, $lengthFractionalPart - $fractions).str_pad('', $zeroPad, '0'), '0'), + $sign . ltrim($integerPart . substr($this->fractionalPart, 0, $lengthFractionalPart - $fractions) . str_pad('', $zeroPad, '0'), '0'), substr($this->fractionalPart, $lengthFractionalPart - $fractions) ); } /** - * @param string $number + * @psalm-return numeric-string * - * @return string + * @psalm-pure + * + * @psalm-suppress MoreSpecificReturnType this operation is guaranteed to pruduce a numeric-string, but inference can't understand it + * @psalm-suppress LessSpecificReturnStatement this operation is guaranteed to pruduce a numeric-string, but inference can't understand it */ - private static function parseIntegerPart($number) + private static function parseIntegerPart(string $number): string { - if ('' === $number || '0' === $number) { + if ($number === '' || $number === '0') { return '0'; } - if ('-' === $number) { + if ($number === '-') { return '-0'; } + // Happy path performance optimization: number can be used as-is if it is within + // the platform's integer capabilities. + if ($number === (string) (int) $number) { + return $number; + } + $nonZero = false; for ($position = 0, $characters = strlen($number); $position < $characters; ++$position) { $digit = $number[$position]; - if (!isset(static::$numbers[$digit]) && !(0 === $position && '-' === $digit)) { - throw new \InvalidArgumentException(sprintf('Invalid integer part %1$s. Invalid digit %2$s found', $number, $digit)); + /** @psalm-suppress InvalidArrayOffset we are, on purpose, checking if the digit is valid against a fixed structure */ + if (! isset(self::NUMBERS[$digit]) && ! ($position === 0 && $digit === '-')) { + throw new InvalidArgumentException(sprintf('Invalid integer part %1$s. Invalid digit %2$s found', $number, $digit)); } - if (false === $nonZero && '0' === $digit) { - throw new \InvalidArgumentException('Leading zeros are not allowed'); + if ($nonZero === false && $digit === '0') { + throw new InvalidArgumentException('Leading zeros are not allowed'); } $nonZero = true; @@ -267,20 +223,30 @@ private static function parseIntegerPart($number) } /** - * @param string $number + * @psalm-return numeric-string|'' * - * @return string + * @psalm-pure */ - private static function parseFractionalPart($number) + private static function parseFractionalPart(string $number): string { - if ('' === $number) { + if ($number === '') { + return $number; + } + + $intFraction = (int) $number; + + // Happy path performance optimization: number can be used as-is if it is within + // the platform's integer capabilities, and it starts with zeroes only. + if ($intFraction > 0 && ltrim($number, '0') === (string) $intFraction) { return $number; } for ($position = 0, $characters = strlen($number); $position < $characters; ++$position) { $digit = $number[$position]; - if (!isset(static::$numbers[$digit])) { - throw new \InvalidArgumentException(sprintf('Invalid fractional part %1$s. Invalid digit %2$s found', $number, $digit)); + + /** @psalm-suppress InvalidArrayOffset we are, on purpose, checking if the digit is valid against a fixed structure */ + if (! isset(self::NUMBERS[$digit])) { + throw new InvalidArgumentException(sprintf('Invalid fractional part %1$s. Invalid digit %2$s found', $number, $digit)); } } @@ -288,19 +254,17 @@ private static function parseFractionalPart($number) } /** - * @param string $moneyValue - * @param int $targetDigits - * @param int $havingDigits - * - * @return string + * @psalm-pure + * @psalm-suppress InvalidOperand string and integers get concatenated here - that is by design, as we're computing remainders */ - public static function roundMoneyValue($moneyValue, $targetDigits, $havingDigits) + public static function roundMoneyValue(string $moneyValue, int $targetDigits, int $havingDigits): string { $valueLength = strlen($moneyValue); $shouldRound = $targetDigits < $havingDigits && $valueLength - $havingDigits + $targetDigits > 0; if ($shouldRound && $moneyValue[$valueLength - $havingDigits + $targetDigits] >= 5) { $position = $valueLength - $havingDigits + $targetDigits; + /** @psalm-var positive-int|0 $addend */ $addend = 1; while ($position > 0) { @@ -308,15 +272,16 @@ public static function roundMoneyValue($moneyValue, $targetDigits, $havingDigits if ($newValue >= 10) { $moneyValue[$position - 1] = $newValue[1]; + /** @psalm-var numeric-string $addend */ $addend = $newValue[0]; --$position; if ($position === 0) { - $moneyValue = $addend.$moneyValue; + $moneyValue = $addend . $moneyValue; } } else { if ($moneyValue[$position - 1] === '-') { $moneyValue[$position - 1] = $newValue[0]; - $moneyValue = '-'.$moneyValue; + $moneyValue = '-' . $moneyValue; } else { $moneyValue[$position - 1] = $newValue[0]; } diff --git a/src/PHPUnit/Comparator.php b/src/PHPUnit/Comparator.php index 4d30121af..28a5d4ea0 100644 --- a/src/PHPUnit/Comparator.php +++ b/src/PHPUnit/Comparator.php @@ -1,5 +1,7 @@ register(new \Money\PHPUnit\Comparator()); + * + * @internal do not use within your sources: this comparator is only to be used within the test suite of this library + * + * @psalm-suppress PropertyNotSetInConstructor the parent implementation includes factories that cannot be initialized here */ final class Comparator extends \SebastianBergmann\Comparator\Comparator { - /** - * @var IntlMoneyFormatter - */ - private $formatter; + private IntlMoneyFormatter $formatter; public function __construct() { @@ -32,31 +38,28 @@ public function __construct() new BitcoinCurrencies(), ]); - $numberFormatter = new \NumberFormatter('en_US', \NumberFormatter::CURRENCY); + $numberFormatter = new NumberFormatter('en_US', NumberFormatter::CURRENCY); $this->formatter = new IntlMoneyFormatter($numberFormatter, $currencies); } + /** {@inheritDoc} */ public function accepts($expected, $actual) { return $expected instanceof Money && $actual instanceof Money; } - /** - * @param Money $expected - * @param Money $actual - * @param float $delta - * @param bool $canonicalize - * @param bool $ignoreCase - */ + /** {@inheritDoc} */ public function assertEquals( $expected, $actual, $delta = 0.0, $canonicalize = false, - $ignoreCase = false, - array &$processed = [] - ) { - if (!$expected->equals($actual)) { + $ignoreCase = false + ): void { + assert($expected instanceof Money); + assert($actual instanceof Money); + + if (! $expected->equals($actual)) { throw new ComparisonFailure($expected, $actual, $this->formatter->format($expected), $this->formatter->format($actual), false, 'Failed asserting that two Money objects are equal.'); } } diff --git a/src/Parser/AggregateMoneyParser.php b/src/Parser/AggregateMoneyParser.php index 8e2a776fd..7c4515e1c 100644 --- a/src/Parser/AggregateMoneyParser.php +++ b/src/Parser/AggregateMoneyParser.php @@ -1,51 +1,38 @@ */ final class AggregateMoneyParser implements MoneyParser { /** * @var MoneyParser[] + * @psalm-var non-empty-array */ - private $parsers = []; + private array $parsers; /** * @param MoneyParser[] $parsers + * @psalm-param non-empty-array $parsers */ public function __construct(array $parsers) { - if (empty($parsers)) { - throw new \InvalidArgumentException(sprintf('Initialize an empty %s is not possible', self::class)); - } - - foreach ($parsers as $parser) { - if (false === $parser instanceof MoneyParser) { - throw new \InvalidArgumentException('All parsers must implement '.MoneyParser::class); - } - - $this->parsers[] = $parser; - } + $this->parsers = $parsers; } - /** - * {@inheritdoc} - */ - public function parse($money, $forceCurrency = null) + public function parse(string $money, Currency|null $forceCurrency = null): Money { - if ($forceCurrency !== null && !$forceCurrency instanceof Currency) { - @trigger_error('Passing a currency as string is deprecated since 3.1 and will be removed in 4.0. Please pass a '.Currency::class.' instance instead.', E_USER_DEPRECATED); - $forceCurrency = new Currency($forceCurrency); - } - foreach ($this->parsers as $parser) { try { return $parser->parse($money, $forceCurrency); diff --git a/src/Parser/BitcoinMoneyParser.php b/src/Parser/BitcoinMoneyParser.php index 320456d49..d288c13f3 100644 --- a/src/Parser/BitcoinMoneyParser.php +++ b/src/Parser/BitcoinMoneyParser.php @@ -1,5 +1,7 @@ */ final class BitcoinMoneyParser implements MoneyParser { - /** - * @var int - */ - private $fractionDigits; + private int $fractionDigits; - /** - * @param int $fractionDigits - */ - public function __construct($fractionDigits) + public function __construct(int $fractionDigits) { $this->fractionDigits = $fractionDigits; } - /** - * {@inheritdoc} - */ - public function parse($money, $forceCurrency = null) + public function parse(string $money, Currency|null $forceCurrency = null): Money { - if (is_string($money) === false) { - throw new ParserException('Formatted raw money should be string, e.g. $1.00'); - } - if (strpos($money, BitcoinCurrencies::SYMBOL) === false) { throw new ParserException('Value cannot be parsed as Bitcoin'); } - if ($forceCurrency === null) { - $forceCurrency = new Currency(BitcoinCurrencies::CODE); - } - - /* - * This conversion is only required whilst currency can be either a string or a - * Currency object. - */ - $currency = $forceCurrency; - if (!$currency instanceof Currency) { - @trigger_error('Passing a currency as string is deprecated since 3.1 and will be removed in 4.0. Please pass a '.Currency::class.' instance instead.', E_USER_DEPRECATED); - $currency = new Currency($currency); - } - - $decimal = str_replace(BitcoinCurrencies::SYMBOL, '', $money); + $currency = $forceCurrency ?? new Currency(BitcoinCurrencies::CODE); + $decimal = str_replace(BitcoinCurrencies::SYMBOL, '', $money); $decimalSeparator = strpos($decimal, '.'); - if (false !== $decimalSeparator) { - $decimal = rtrim($decimal, '0'); + if ($decimalSeparator !== false) { + $decimal = rtrim($decimal, '0'); $lengthDecimal = strlen($decimal); - $decimal = str_replace('.', '', $decimal); - $decimal .= str_pad('', ($lengthDecimal - $decimalSeparator - $this->fractionDigits - 1) * -1, '0'); + $decimal = str_replace('.', '', $decimal); + $decimal .= str_pad('', ($lengthDecimal - $decimalSeparator - $this->fractionDigits - 1) * -1, '0'); } else { $decimal .= str_pad('', $this->fractionDigits, '0'); } if (substr($decimal, 0, 1) === '-') { - $decimal = '-'.ltrim(substr($decimal, 1), '0'); + $decimal = '-' . ltrim(substr($decimal, 1), '0'); } else { $decimal = ltrim($decimal, '0'); } - if ('' === $decimal) { + if ($decimal === '') { $decimal = '0'; } + /** @psalm-var numeric-string $decimal */ return new Money($decimal, $currency); } } diff --git a/src/Parser/DecimalMoneyParser.php b/src/Parser/DecimalMoneyParser.php index 707c54ee9..b89399381 100644 --- a/src/Parser/DecimalMoneyParser.php +++ b/src/Parser/DecimalMoneyParser.php @@ -1,5 +1,7 @@ */ final class DecimalMoneyParser implements MoneyParser { - const DECIMAL_PATTERN = '/^(?P-)?(?P0|[1-9]\d*)?\.?(?P\d+)?$/'; + public const DECIMAL_PATTERN = '/^(?P-)?(?P0|[1-9]\d*)?\.?(?P\d+)?$/'; - /** - * @var Currencies - */ - private $currencies; + private Currencies $currencies; public function __construct(Currencies $currencies) { $this->currencies = $currencies; } - /** - * {@inheritdoc} - */ - public function parse($money, $forceCurrency = null) + public function parse(string $money, Currency|null $forceCurrency = null): Money { - if (!is_string($money)) { - throw new ParserException('Formatted raw money should be string, e.g. 1.00'); - } - - if (null === $forceCurrency) { + if ($forceCurrency === null) { throw new ParserException('DecimalMoneyParser cannot parse currency symbols. Use forceCurrency argument'); } - /* - * This conversion is only required whilst currency can be either a string or a - * Currency object. - */ - $currency = $forceCurrency; - if (!$currency instanceof Currency) { - @trigger_error('Passing a currency as string is deprecated since 3.1 and will be removed in 4.0. Please pass a '.Currency::class.' instance instead.', E_USER_DEPRECATED); - $currency = new Currency($currency); - } - $decimal = trim($money); if ($decimal === '') { - return new Money(0, $currency); + return new Money(0, $forceCurrency); } - $subunit = $this->currencies->subunitFor($currency); - - if (!preg_match(self::DECIMAL_PATTERN, $decimal, $matches) || !isset($matches['digits'])) { + if (! preg_match(self::DECIMAL_PATTERN, $decimal, $matches) || ! isset($matches['digits'])) { throw new ParserException(sprintf('Cannot parse "%s" to Money.', $decimal)); } @@ -68,13 +54,15 @@ public function parse($money, $forceCurrency = null) $decimal = $matches['digits']; if ($negative) { - $decimal = '-'.$decimal; + $decimal = '-' . $decimal; } + $subunit = $this->currencies->subunitFor($forceCurrency); + if (isset($matches['fraction'])) { $fractionDigits = strlen($matches['fraction']); - $decimal .= $matches['fraction']; - $decimal = Number::roundMoneyValue($decimal, $subunit, $fractionDigits); + $decimal .= $matches['fraction']; + $decimal = Number::roundMoneyValue($decimal, $subunit, $fractionDigits); if ($fractionDigits > $subunit) { $decimal = substr($decimal, 0, $subunit - $fractionDigits); @@ -86,7 +74,7 @@ public function parse($money, $forceCurrency = null) } if ($negative) { - $decimal = '-'.ltrim(substr($decimal, 1), '0'); + $decimal = '-' . ltrim(substr($decimal, 1), '0'); } else { $decimal = ltrim($decimal, '0'); } @@ -95,6 +83,7 @@ public function parse($money, $forceCurrency = null) $decimal = '0'; } - return new Money($decimal, $currency); + /** @psalm-var numeric-string $decimal */ + return new Money($decimal, $forceCurrency); } } diff --git a/src/Parser/IntlLocalizedDecimalParser.php b/src/Parser/IntlLocalizedDecimalParser.php index 20ef807c3..da95acb92 100644 --- a/src/Parser/IntlLocalizedDecimalParser.php +++ b/src/Parser/IntlLocalizedDecimalParser.php @@ -1,5 +1,7 @@ */ final class IntlLocalizedDecimalParser implements MoneyParser { - /** - * @var \NumberFormatter - */ - private $formatter; + private NumberFormatter $formatter; - /** - * @var Currencies - */ - private $currencies; + private Currencies $currencies; - public function __construct(\NumberFormatter $formatter, Currencies $currencies) + public function __construct(NumberFormatter $formatter, Currencies $currencies) { - $this->formatter = $formatter; + $this->formatter = $formatter; $this->currencies = $currencies; } - /** - * {@inheritdoc} - */ - public function parse($money, $forceCurrency = null) + public function parse(string $money, Currency|null $forceCurrency = null): Money { - if (!is_string($money)) { - throw new ParserException('Formatted raw money should be string, e.g. $1.00'); - } - - if (null === $forceCurrency) { + if ($forceCurrency === null) { throw new ParserException('IntlLocalizedDecimalParser cannot parse currency symbols. Use forceCurrency argument'); } $decimal = $this->formatter->parse($money); - if (false === $decimal) { - throw new ParserException('Cannot parse '.$money.' to Money. '.$this->formatter->getErrorMessage()); - } - - /* - * This conversion is only required whilst currency can be either a string or a - * Currency object. - */ - if (!$forceCurrency instanceof Currency) { - @trigger_error('Passing a currency as string is deprecated since 3.1 and will be removed in 4.0. Please pass a '.Currency::class.' instance instead.', E_USER_DEPRECATED); - $forceCurrency = new Currency($forceCurrency); + if ($decimal === false) { + throw new ParserException('Cannot parse ' . $money . ' to Money. ' . $this->formatter->getErrorMessage()); } - $decimal = (string) $decimal; - $subunit = $this->currencies->subunitFor($forceCurrency); + $decimal = (string) $decimal; + $subunit = $this->currencies->subunitFor($forceCurrency); $decimalPosition = strpos($decimal, '.'); - if (false !== $decimalPosition) { - $decimalLength = strlen($decimal); + if ($decimalPosition !== false) { + $decimalLength = strlen($decimal); $fractionDigits = $decimalLength - $decimalPosition - 1; - $decimal = str_replace('.', '', $decimal); - $decimal = Number::roundMoneyValue($decimal, $subunit, $fractionDigits); + $decimal = str_replace('.', '', $decimal); + $decimal = Number::roundMoneyValue($decimal, $subunit, $fractionDigits); if ($fractionDigits > $subunit) { $decimal = substr($decimal, 0, $decimalPosition + $subunit); @@ -79,16 +65,17 @@ public function parse($money, $forceCurrency = null) $decimal .= str_pad('', $subunit, '0'); } - if ('-' === $decimal[0]) { - $decimal = '-'.ltrim(substr($decimal, 1), '0'); + if ($decimal[0] === '-') { + $decimal = '-' . ltrim(substr($decimal, 1), '0'); } else { $decimal = ltrim($decimal, '0'); } - if ('' === $decimal) { + if ($decimal === '') { $decimal = '0'; } + /** @psalm-var numeric-string $decimal */ return new Money($decimal, $forceCurrency); } } diff --git a/src/Parser/IntlMoneyParser.php b/src/Parser/IntlMoneyParser.php index 8bc10083f..9b7130b87 100644 --- a/src/Parser/IntlMoneyParser.php +++ b/src/Parser/IntlMoneyParser.php @@ -1,5 +1,7 @@ */ final class IntlMoneyParser implements MoneyParser { - /** - * @var \NumberFormatter - */ - private $formatter; + private NumberFormatter $formatter; - /** - * @var Currencies - */ - private $currencies; + private Currencies $currencies; - public function __construct(\NumberFormatter $formatter, Currencies $currencies) + public function __construct(NumberFormatter $formatter, Currencies $currencies) { - $this->formatter = $formatter; + $this->formatter = $formatter; $this->currencies = $currencies; } - /** - * {@inheritdoc} - */ - public function parse($money, $forceCurrency = null) + public function parse(string $money, Currency|null $forceCurrency = null): Money { - if (!is_string($money)) { - throw new ParserException('Formatted raw money should be string, e.g. $1.00'); - } - $currency = null; - $decimal = $this->formatter->parseCurrency($money, $currency); + $decimal = $this->formatter->parseCurrency($money, $currency); - if (false === $decimal) { - throw new ParserException('Cannot parse '.$money.' to Money. '.$this->formatter->getErrorMessage()); + if ($decimal === false) { + throw new ParserException('Cannot parse ' . $money . ' to Money. ' . $this->formatter->getErrorMessage()); } - if (null !== $forceCurrency) { - $currency = $forceCurrency; - } else { - $currency = new Currency($currency); - } + if ($forceCurrency === null) { + assert(! empty($currency)); - /* - * This conversion is only required whilst currency can be either a string or a - * Currency object. - */ - if (!$currency instanceof Currency) { - @trigger_error('Passing a currency as string is deprecated since 3.1 and will be removed in 4.0. Please pass a '.Currency::class.' instance instead.', E_USER_DEPRECATED); - $currency = new Currency($currency); + $forceCurrency = new Currency($currency); } - $decimal = (string) $decimal; - $subunit = $this->currencies->subunitFor($currency); + $decimal = (string) $decimal; + $subunit = $this->currencies->subunitFor($forceCurrency); $decimalPosition = strpos($decimal, '.'); - if (false !== $decimalPosition) { - $decimalLength = strlen($decimal); + if ($decimalPosition !== false) { + $decimalLength = strlen($decimal); $fractionDigits = $decimalLength - $decimalPosition - 1; - $decimal = str_replace('.', '', $decimal); - $decimal = Number::roundMoneyValue($decimal, $subunit, $fractionDigits); + $decimal = str_replace('.', '', $decimal); + $decimal = Number::roundMoneyValue($decimal, $subunit, $fractionDigits); if ($fractionDigits > $subunit) { $decimal = substr($decimal, 0, $decimalPosition + $subunit); @@ -82,16 +69,17 @@ public function parse($money, $forceCurrency = null) $decimal .= str_pad('', $subunit, '0'); } - if ('-' === $decimal[0]) { - $decimal = '-'.ltrim(substr($decimal, 1), '0'); + if ($decimal[0] === '-') { + $decimal = '-' . ltrim(substr($decimal, 1), '0'); } else { $decimal = ltrim($decimal, '0'); } - if ('' === $decimal) { + if ($decimal === '') { $decimal = '0'; } - return new Money($decimal, $currency); + /** @psalm-var numeric-string $decimal */ + return new Money($decimal, $forceCurrency); } } diff --git a/static-analysis/money-is-pure.php b/static-analysis/money-is-pure.php index 27047fcb5..6c18aebed 100644 --- a/static-analysis/money-is-pure.php +++ b/static-analysis/money-is-pure.php @@ -1,5 +1,7 @@ , Money}> */ + public function sumExamples(): array { return [ [[Money::EUR(5), Money::EUR(10), Money::EUR(15)], Money::EUR(30)], @@ -15,7 +18,8 @@ public function sumExamples() ]; } - public function minExamples() + /** @psalm-return non-empty-list, Money}> */ + public function minExamples(): array { return [ [[Money::EUR(5), Money::EUR(10), Money::EUR(15)], Money::EUR(5)], @@ -24,7 +28,8 @@ public function minExamples() ]; } - public function maxExamples() + /** @psalm-return non-empty-list, Money}> */ + public function maxExamples(): array { return [ [[Money::EUR(5), Money::EUR(10), Money::EUR(15)], Money::EUR(15)], @@ -33,7 +38,8 @@ public function maxExamples() ]; } - public function avgExamples() + /** @psalm-return non-empty-list, Money}> */ + public function avgExamples(): array { return [ [[Money::EUR(5), Money::EUR(10), Money::EUR(15)], Money::EUR(10)], diff --git a/tests/Calculator/BcMathCalculatorTest.php b/tests/Calculator/BcMathCalculatorTest.php index 327b1a039..1659ba42f 100644 --- a/tests/Calculator/BcMathCalculatorTest.php +++ b/tests/Calculator/BcMathCalculatorTest.php @@ -1,75 +1,132 @@ + */ + protected function getCalculator(): string { - return new BcMathCalculator(); + return BcMathCalculator::class; } - public function setUp() + public function setUp(): void { - $this->defaultScale = ini_get('bcmath.scale'); + $this->defaultScale = (int) ini_get('bcmath.scale'); } - public function tearDown() + public function tearDown(): void { bcscale($this->defaultScale); } /** + * @psalm-param positive-int $value1 + * @psalm-param positive-int $value2 + * @psalm-param numeric-string $expected + * * @dataProvider additionExamples * @test */ - public function it_adds_two_values_with_scale_set($value1, $value2, $expected) + public function itAddsTwoValuesWithScaleSet(int $value1, int $value2, string $expected): void { bcscale(1); - $this->assertEquals($expected, $this->getCalculator()->add($value1, $value2)); + self::assertEqualNumber($expected, $this->getCalculator()::add((string) $value1, (string) $value2)); } /** + * @psalm-param positive-int $value1 + * @psalm-param positive-int $value2 + * @psalm-param numeric-string $expected + * * @dataProvider subtractionExamples * @test */ - public function it_subtracts_a_value_from_another_with_scale_set($value1, $value2, $expected) + public function itSubtractsAValueFromAnotherWithScaleSet(int $value1, int $value2, string $expected): void { bcscale(1); - $this->assertEquals($expected, $this->getCalculator()->subtract($value1, $value2)); + self::assertEqualNumber($expected, $this->getCalculator()::subtract((string) $value1, (string) $value2)); } /** * @test */ - public function it_compares_numbers_close_to_zero() + public function itComparesNumbersCloseToZero(): void { - $this->assertEquals(1, $this->getCalculator()->compare('1', '0.0005')); - $this->assertEquals(1, $this->getCalculator()->compare('1', '0.000000000000000000000000005')); + self::assertEquals(1, $this->getCalculator()::compare('1', '0.0005')); + self::assertEquals(1, $this->getCalculator()::compare('1', '0.000000000000000000000000005')); } /** * @test */ - public function it_uses_scale_for_add() + public function itUsesScaleForAdd(): void { - $this->assertEquals('0.00130154000000', $this->getCalculator()->add('0.00125148', '0.00005006')); + self::assertEquals('0.00130154000000', $this->getCalculator()::add('0.00125148', '0.00005006')); } /** * @test */ - public function it_uses_scale_for_subtract() + public function itUsesScaleForSubtract(): void + { + self::assertEqualNumber('0.00120142', $this->getCalculator()::subtract('0.00125148', '0.00005006')); + } + + /** @test */ + public function itRefusesToDivideByZeroWhenDivisorIsTooSmallToCompare(): void + { + $calculator = $this->getCalculator(); + + $this->expectException(InvalidArgumentException::class); + + $calculator::divide('1', '0.0000000000000000000000000000000000000000001'); + } + + /** @test */ + public function itRefusesToModuloByZeroWhenDivisorIsTooSmallToCompare(): void + { + $calculator = $this->getCalculator(); + + $this->expectException(InvalidArgumentException::class); + + $calculator::mod('1', '0.0000000000000000000000000000000000000000001'); + } + + /** + * @psalm-return non-empty-list + */ + public function compareLessExamples(): array { - $this->assertEquals('0.00120142', $this->getCalculator()->subtract('0.00125148', '0.00005006')); + return array_merge( + parent::compareLessExamples(), + [ + // Slightly below PHP_INT_MIN on 64 bit systems (does not work with the PhpCalculator) + ['-9223372036854775810', '-9223372036854775809', -1], + ] + ); } } diff --git a/tests/Calculator/CalculatorTestCase.php b/tests/Calculator/CalculatorTestCase.php index 085df1a78..5c15e2e17 100644 --- a/tests/Calculator/CalculatorTestCase.php +++ b/tests/Calculator/CalculatorTestCase.php @@ -1,144 +1,256 @@ */ - abstract protected function getCalculator(); + abstract protected function getCalculator(): string; /** + * @psalm-param positive-int $value1 + * @psalm-param positive-int $value2 + * @psalm-param numeric-string $expected + * * @dataProvider additionExamples * @test */ - public function it_adds_two_values($value1, $value2, $expected) + public function itAddsTwoValues(int $value1, int $value2, string $expected): void { - $this->assertEquals($expected, $this->getCalculator()->add($value1, $value2)); + self::assertEqualNumber($expected, $this->getCalculator()::add((string) $value1, (string) $value2)); } /** + * @psalm-param positive-int $value1 + * @psalm-param positive-int $value2 + * @psalm-param numeric-string $expected + * * @dataProvider subtractionExamples * @test */ - public function it_subtracts_a_value_from_another($value1, $value2, $expected) + public function itSubtractsAValueFromAnother(int $value1, int $value2, string $expected): void { - $this->assertEquals($expected, $this->getCalculator()->subtract($value1, $value2)); + self::assertEqualNumber($expected, $this->getCalculator()::subtract((string) $value1, (string) $value2)); } /** + * @psalm-param positive-int|numeric-string $value1 + * @psalm-param float $value2 + * @psalm-param numeric-string $expected + * * @dataProvider multiplicationExamples * @test */ - public function it_multiplies_a_value_by_another($value1, $value2, $expected) + public function itMultipliesAValueByAnother(int|string $value1, float $value2, string $expected): void { - $this->assertEquals($expected, $this->getCalculator()->multiply($value1, $value2)); + self::assertEqualNumber($expected, $this->getCalculator()::multiply((string) $value1, (string) $value2)); } /** + * @psalm-param positive-int|numeric-string $value1 + * @psalm-param positive-int|float $value2 + * @psalm-param numeric-string $expected + * * @dataProvider divisionExamples * @test */ - public function it_divides_a_value_by_another($value1, $value2, $expected) + public function itDividesAValueByAnother(int|string $value1, int|float $value2, string $expected): void { - $result = $this->getCalculator()->divide($value1, $value2); - $this->assertEquals(substr($expected, 0, 12), substr($result, 0, 12)); + $expectedNumericString = substr($expected, 0, 12); + $resultNumericString = substr( + $this->getCalculator()::divide((string) $value1, (string) $value2), + 0, + 12 + ); + + self::assertIsNumeric($expectedNumericString); + self::assertIsNumeric($resultNumericString); + self::assertEqualNumber($expectedNumericString, $resultNumericString); } /** + * @psalm-param positive-int $value1 + * @psalm-param positive-int|float $value2 + * @psalm-param numeric-string $expected + * * @dataProvider divisionExactExamples * @test */ - public function it_divides_a_value_by_another_exact($value1, $value2, $expected) + public function itDividesAValueByAnotherExact(int $value1, int|float $value2, string $expected): void { - $this->assertEquals($expected, $this->getCalculator()->divide($value1, $value2)); + self::assertEqualNumber($expected, $this->getCalculator()::divide((string) $value1, (string) $value2)); } /** + * @psalm-param float $value + * @psalm-param numeric-string $expected + * * @dataProvider ceilExamples * @test */ - public function it_ceils_a_value($value, $expected) + public function itCeilsAValue(float $value, string $expected): void { - $this->assertEquals($expected, $this->getCalculator()->ceil($value)); + self::assertEquals($expected, $this->getCalculator()::ceil((string) $value)); } /** + * @psalm-param float $value + * @psalm-param numeric-string $expected + * * @dataProvider floorExamples * @test */ - public function it_floors_a_value($value, $expected) + public function itFloorsAValue(float $value, string $expected): void { - $this->assertEquals($expected, $this->getCalculator()->floor($value)); + self::assertEquals($expected, $this->getCalculator()::floor((string) $value)); } /** + * @psalm-param int $value + * @psalm-param numeric-string $expected + * * @dataProvider absoluteExamples * @test */ - public function it_calculates_the_absolute_value($value, $expected) + public function itCalculatesTheAbsoluteValue(int $value, string $expected): void { - $this->assertEquals($expected, $this->getCalculator()->absolute($value)); + self::assertEquals($expected, $this->getCalculator()::absolute((string) $value)); } /** + * @psalm-param int $value + * @psalm-param int $ratio + * @psalm-param int $total + * @psalm-param numeric-string $expected + * * @dataProvider shareExamples * @test */ - public function it_shares_a_value($value, $ratio, $total, $expected) + public function itSharesAValue(int $value, int $ratio, int $total, string $expected): void { - $this->assertEquals($expected, $this->getCalculator()->share($value, $ratio, $total)); + self::assertEquals($expected, $this->getCalculator()::share((string) $value, (string) $ratio, (string) $total)); } /** - * @dataProvider roundExamples + * @psalm-param float|int|numeric-string $value + * @psalm-param Money::ROUND_* $mode + * @psalm-param numeric-string $expected + * + * @dataProvider roundingExamples * @test */ - public function it_rounds_a_value($value, $mode, $expected) + public function itRoundsAValue(float|int|string $value, int $mode, string $expected): void { - $this->assertEquals($expected, $this->getCalculator()->round($value, $mode)); + self::assertEquals($expected, $this->getCalculator()::round((string) $value, $mode)); } /** + * @psalm-param int|numeric-string $left + * @psalm-param int|numeric-string $right + * * @dataProvider compareLessExamples * @test */ - public function it_compares_values_less($left, $right) + public function itComparesValuesLess(int|string $left, int|string $right): void { // Compare with both orders. One must return a value less than zero, // the other must return a value greater than zero. - $this->assertLessThan(0, $this->getCalculator()->compare($left, $right)); - $this->assertGreaterThan(0, $this->getCalculator()->compare($right, $left)); + self::assertLessThan(0, $this->getCalculator()::compare((string) $left, (string) $right)); + self::assertGreaterThan(0, $this->getCalculator()::compare((string) $right, (string) $left)); } /** + * @psalm-param int|numeric-string $left + * @psalm-param int|numeric-string $right + * * @dataProvider compareEqualExamples * @test */ - public function it_compares_values($left, $right) + public function itComparesValues(int|string $left, int|string $right): void { // Compare with both orders, both must return zero. - $this->assertEquals(0, $this->getCalculator()->compare($left, $right)); - $this->assertEquals(0, $this->getCalculator()->compare($right, $left)); + self::assertEquals(0, $this->getCalculator()::compare((string) $left, (string) $right)); + self::assertEquals(0, $this->getCalculator()::compare((string) $left, (string) $right)); } /** + * @psalm-param int $left + * @psalm-param int $right + * @psalm-param numeric-string $expected + * * @dataProvider modExamples * @test */ - public function it_calculates_the_modulus_of_a_value($left, $right, $expected) + public function itCalculatesTheModulusOfAValue(int $left, int $right, string $expected): void + { + self::assertEquals($expected, $this->getCalculator()::mod((string) $left, (string) $right)); + } + + /** @test */ + public function itRefusesToDivideByZero(): void + { + $calculator = $this->getCalculator(); + + $this->expectException(InvalidArgumentException::class); + + $calculator::divide('1', '0'); + } + + /** @test */ + public function itRefusesToDivideByNegativeZero(): void + { + $calculator = $this->getCalculator(); + + $this->expectException(InvalidArgumentException::class); + + $calculator::divide('1', '-0'); + } + + /** @test */ + public function itRefusesToModuloByZero(): void + { + $calculator = $this->getCalculator(); + + $this->expectException(InvalidArgumentException::class); + + $calculator::mod('1', '0'); + } + + /** @test */ + public function itRefusesToModuloByNegativeZero(): void { - $this->assertEquals($expected, $this->getCalculator()->mod($left, $right)); + $calculator = $this->getCalculator(); + + $this->expectException(InvalidArgumentException::class); + + $calculator::mod('1', '-0'); } - public function additionExamples() + /** + * @psalm-return non-empty-list + */ + public function additionExamples(): array { return [ [1, 1, '2'], @@ -146,7 +258,14 @@ public function additionExamples() ]; } - public function subtractionExamples() + /** + * @psalm-return non-empty-list + */ + public function subtractionExamples(): array { return [ [1, 1, '0'], @@ -154,7 +273,14 @@ public function subtractionExamples() ]; } - public function multiplicationExamples() + /** + * @psalm-return non-empty-list + */ + public function multiplicationExamples(): array { return [ [1, 1.5, '1.5'], @@ -170,7 +296,14 @@ public function multiplicationExamples() ]; } - public function divisionExamples() + /** + * @psalm-return non-empty-list + */ + public function divisionExamples(): array { return [ [6, 3, '2'], @@ -187,7 +320,14 @@ public function divisionExamples() ]; } - public function divisionExactExamples() + /** + * @psalm-return non-empty-list + */ + public function divisionExactExamples(): array { return [ [6, 3, '2'], @@ -200,7 +340,13 @@ public function divisionExactExamples() ]; } - public function ceilExamples() + /** + * @psalm-return non-empty-list + */ + public function ceilExamples(): array { return [ [1.2, '2'], @@ -209,7 +355,13 @@ public function ceilExamples() ]; } - public function floorExamples() + /** + * @psalm-return non-empty-list + */ + public function floorExamples(): array { return [ [2.7, '2'], @@ -218,7 +370,13 @@ public function floorExamples() ]; } - public function absoluteExamples() + /** + * @psalm-return non-empty-list + */ + public function absoluteExamples(): array { return [ [2, '2'], @@ -226,14 +384,28 @@ public function absoluteExamples() ]; } - public function shareExamples() + /** + * @psalm-return non-empty-list + */ + public function shareExamples(): array { return [ [10, 2, 4, '5'], ]; } - public function compareLessExamples() + /** + * @psalm-return non-empty-list + */ + public function compareLessExamples(): array { return [ [0, 1], @@ -241,10 +413,18 @@ public function compareLessExamples() ['0.0005', '1'], ['0.000000000000000000000000005', '1'], ['-1000', '1000', -1], + // Slightly above PHP_INT_MAX on 64 bit systems + ['9223372036854775808', '9223372036854775809', -1], ]; } - public function compareEqualExamples() + /** + * @psalm-return non-empty-list + */ + public function compareEqualExamples(): array { return [ [1, 1], @@ -253,7 +433,14 @@ public function compareEqualExamples() ]; } - public function modExamples() + /** + * @psalm-return non-empty-list + */ + public function modExamples(): array { return [ [11, 5, '1'], @@ -265,4 +452,31 @@ public function modExamples() [13, -5, '3'], ]; } + + /** + * Fixed point precision operations sometimes retrieve trailing zeroes due to higher precision than requested: + * this is acceptable for us, and we are OK with ignoring trailing zero fractional digits during test comparisons. + * + * @psalm-param numeric-string $expected + * @psalm-param numeric-string $result + */ + final protected static function assertEqualNumber(string $expected, string $result): void + { + $normalizedExpected = $expected; + $normalizedResult = $result; + + // Thank you, Murica -.- + if ($normalizedExpected[0] === '.') { + $normalizedExpected = '0' . $normalizedExpected; + } + + if ($normalizedResult[0] === '.') { + $normalizedResult = '0' . $normalizedResult; + } + + $normalizedExpected = rtrim(preg_replace('/^(\d+\.\d*?[1-9]*)0+$/', '$1', $normalizedExpected), '.'); + $normalizedResult = rtrim(preg_replace('/^(\d+\.\d*?[1-9]*)0+$/', '$1', $normalizedResult), '.'); + + self::assertEquals($normalizedExpected, $normalizedResult); + } } diff --git a/tests/Calculator/GmpCalculatorTest.php b/tests/Calculator/GmpCalculatorTest.php index e10802e33..bc00ba33c 100644 --- a/tests/Calculator/GmpCalculatorTest.php +++ b/tests/Calculator/GmpCalculatorTest.php @@ -1,40 +1,74 @@ + */ + protected function getCalculator(): string { - return new GmpCalculator(); + return GmpCalculator::class; } /** * @test */ - public function it_multiplies_zero() + public function itMultipliesZero(): void { - $this->assertSame('0', $this->getCalculator()->multiply('0', '0.8')); + self::assertSame('0', $this->getCalculator()::multiply('0', '0.8')); } /** * @test */ - public function it_floors_zero() + public function itFloorsZero(): void { - $this->assertSame('0', $this->getCalculator()->floor('0')); + self::assertSame('0', $this->getCalculator()::floor('0')); } /** * @test */ - public function it_compares_zero_with_fraction() + public function itComparesZeroWithFraction(): void + { + self::assertSame(1, $this->getCalculator()::compare('0.5', '0')); + } + + /** + * @test + */ + public function it_divides_bug538(): void + { + self::assertSame('-4.54545454545455', $this->getCalculator()::divide('-500', '110')); + } + + /** + * @psalm-return non-empty-list + */ + public function compareLessExamples(): array { - $this->assertSame(1, $this->getCalculator()->compare('0.5', '0')); + return array_merge( + parent::compareLessExamples(), + [ + // Slightly below PHP_INT_MIN on 64 bit systems (does not work with the PhpCalculator) + ['-9223372036854775810', '-9223372036854775809', -1], + ] + ); } } diff --git a/tests/Calculator/LocaleAwareBcMathCalculatorTest.php b/tests/Calculator/LocaleAwareBcMathCalculatorTest.php index cf088cd22..a31d4641e 100644 --- a/tests/Calculator/LocaleAwareBcMathCalculatorTest.php +++ b/tests/Calculator/LocaleAwareBcMathCalculatorTest.php @@ -1,10 +1,15 @@ setLocale(LC_ALL, 'ru_RU.UTF-8'); - } -} diff --git a/tests/Calculator/PhpCalculatorTest.php b/tests/Calculator/PhpCalculatorTest.php deleted file mode 100644 index 5c88cb214..000000000 --- a/tests/Calculator/PhpCalculatorTest.php +++ /dev/null @@ -1,13 +0,0 @@ -comparator = new Comparator(); } @@ -21,50 +22,57 @@ protected function setUp() /** * @test */ - public function it_accepts_only_money() + public function itAcceptsOnlyMoney(): void { $money_a = Money::EUR(1); $money_b = Money::EUR(2); - $this->assertFalse($this->comparator->accepts($money_a, false)); - $this->assertFalse($this->comparator->accepts(false, $money_a)); - $this->assertTrue($this->comparator->accepts($money_a, $money_b)); + self::assertFalse($this->comparator->accepts($money_a, false)); + self::assertFalse($this->comparator->accepts(false, $money_a)); + self::assertTrue($this->comparator->accepts($money_a, $money_b)); } /** * @test */ - public function it_compares_unequal_values() + public function itComparesUnequalValues(): void { $money_a = Money::EUR(1); $money_b = Money::USD(1); try { $this->comparator->assertEquals($money_a, $money_b); - } catch (\SebastianBergmann\Comparator\ComparisonFailure $e) { - $this->assertEquals('Failed asserting that two Money objects are equal.', $e->getMessage()); - $this->assertContains( + } catch (ComparisonFailure $e) { + self::assertSame('Failed asserting that two Money objects are equal.', $e->getMessage()); + self::assertStringContainsString( '--- Expected +++ Actual @@ @@ -€0.01 -+$0.01', $e->getDiff() ++$0.01', + $e->getDiff() ); return; } - $this->fail('ComparisonFailure should have been thrown.'); + self::fail('ComparisonFailure should have been thrown.'); } /** * @test */ - public function it_compares_equal_values() + public function itComparesEqualValues(): void { $money_a = Money::EUR(1); $money_b = Money::EUR(1); - $this->assertNull($this->comparator->assertEquals($money_a, $money_b)); + $this->comparator->assertEquals($money_a, $money_b); + + self::assertEquals( + $money_a, + $money_b, + 'This is only here to increment the assertion counter, since we are testing an assertion' + ); } } diff --git a/tests/ConverterTest.php b/tests/ConverterTest.php index fb4aca565..aff06f279 100644 --- a/tests/ConverterTest.php +++ b/tests/ConverterTest.php @@ -1,5 +1,7 @@ prophesize(Currencies::class); + self::assertIsNumeric($numericRatio); - /** @var Exchange|ObjectProphecy $exchange */ - $exchange = $this->prophesize(Exchange::class); + $pair = new CurrencyPair($baseCurrency, $counterCurrency, $numericRatio); + $currencies = $this->createMock(Currencies::class); + $exchange = $this->createMock(Exchange::class); + $converter = new Converter($currencies, $exchange); - $converter = new Converter($currencies->reveal(), $exchange->reveal()); + $currencies->method('subunitFor') + ->with(self::logicalOr(self::equalTo($baseCurrency), self::equalTo($counterCurrency))) + ->willReturnCallback( + static fn (Currency $currency): int => $currency->equals($baseCurrency) ? $subunitBase : $subunitCounter + ); - $currencies->subunitFor($baseCurrency)->willReturn($subunitBase); - $currencies->subunitFor($counterCurrency)->willReturn($subunitCounter); - - $exchange->quote($baseCurrency, $counterCurrency)->willReturn($pair); + $exchange->method('quote') + ->with(self::equalTo($baseCurrency), self::equalTo($counterCurrency)) + ->willReturn($pair); $money = $converter->convert( new Money($amount, new Currency($baseCurrencyCode)), $counterCurrency ); - $this->assertInstanceOf(Money::class, $money); - $this->assertEquals($expectedAmount, $money->getAmount()); - $this->assertEquals($counterCurrencyCode, $money->getCurrency()->getCode()); + self::assertEquals($expectedAmount, $money->getAmount()); + self::assertEquals($counterCurrencyCode, $money->getCurrency()->getCode()); } /** + * @psalm-param non-empty-string $baseCurrencyCode + * @psalm-param non-empty-string $counterCurrencyCode + * @psalm-param positive-int|0 $subunitBase + * @psalm-param positive-int|0 $subunitCounter + * @psalm-param int|float $ratio + * @psalm-param positive-int|numeric-string $amount + * @psalm-param positive-int|0 $expectedAmount + * * @dataProvider convertExamples * @test */ - public function it_converts_to_a_different_currency_when_decimal_separator_is_comma( + public function itConvertsToADifferentCurrencyWhenDecimalSeparatorIsComma( $baseCurrencyCode, $counterCurrencyCode, $subunitBase, @@ -65,10 +90,10 @@ public function it_converts_to_a_different_currency_when_decimal_separator_is_co $ratio, $amount, $expectedAmount - ) { + ): void { $this->setLocale(LC_ALL, 'ru_RU.UTF-8'); - $this->it_converts_to_a_different_currency( + $this->itConvertsToADifferentCurrency( $baseCurrencyCode, $counterCurrencyCode, $subunitBase, @@ -79,7 +104,18 @@ public function it_converts_to_a_different_currency_when_decimal_separator_is_co ); } - public function convertExamples() + /** + * @psalm-return non-empty-list + */ + public function convertExamples(): array { return [ ['USD', 'JPY', 2, 0, 101, 100, 101], diff --git a/tests/Currencies/AggregateCurrenciesTest.php b/tests/Currencies/AggregateCurrenciesTest.php new file mode 100644 index 000000000..4ebe1a2c0 --- /dev/null +++ b/tests/Currencies/AggregateCurrenciesTest.php @@ -0,0 +1,157 @@ +createMock(Currencies::class); + $otherCurrencies = $this->createMock(Currencies::class); + $currency = new Currency('EUR'); + + $currencies->method('contains') + ->with(self::equalTo($currency)) + ->willReturn(false); + $otherCurrencies->method('contains') + ->with(self::equalTo($currency)) + ->willReturn(true); + + self::assertTrue( + (new AggregateCurrencies([$currencies, $otherCurrencies])) + ->contains($currency) + ); + } + + /** @test */ + public function it_might_not_contain_currencies(): void + { + $currencies = $this->createMock(Currencies::class); + $otherCurrencies = $this->createMock(Currencies::class); + $currency = new Currency('EUR'); + + $currencies->method('contains') + ->with(self::equalTo($currency)) + ->willReturn(false); + $otherCurrencies->method('contains') + ->with(self::equalTo($currency)) + ->willReturn(false); + + self::assertFalse( + (new AggregateCurrencies([$currencies, $otherCurrencies])) + ->contains($currency) + ); + } + + /** @test */ + public function it_provides_subunit(): void + { + $currencies = $this->createMock(Currencies::class); + $otherCurrencies = $this->createMock(Currencies::class); + $currency = new Currency('EUR'); + + $currencies->method('contains') + ->with(self::equalTo($currency)) + ->willReturn(false); + $otherCurrencies->method('contains') + ->with(self::equalTo($currency)) + ->willReturn(true); + $currencies->expects(self::never()) + ->method('subunitFor'); + $otherCurrencies->method('subunitFor') + ->with(self::equalTo($currency)) + ->willReturn(2); + + self::assertSame( + 2, + (new AggregateCurrencies([$currencies, $otherCurrencies])) + ->subunitFor($currency) + ); + } + + /** @test */ + public function it_throws_an_exception_when_providing_subunit_and_currency_is_unknown(): void + { + $currencies = $this->createMock(Currencies::class); + $otherCurrencies = $this->createMock(Currencies::class); + $currency = new Currency('EUR'); + + $currencies->method('contains') + ->with(self::equalTo($currency)) + ->willReturn(false); + $otherCurrencies->method('contains') + ->with(self::equalTo($currency)) + ->willReturn(false); + $currencies->expects(self::never()) + ->method('subunitFor'); + $otherCurrencies->expects(self::never()) + ->method('subunitFor'); + + $aggregate = new AggregateCurrencies([$currencies, $otherCurrencies]); + + $this->expectException(UnknownCurrencyException::class); + $aggregate->subunitFor($currency); + } + + /** @test */ + public function it_is_iterable(): void + { + $currencies = $this->createMock(Currencies::class); + $otherCurrencies = $this->createMock(Currencies::class); + + $currencies->method('getIterator') + ->willReturn(new ArrayIterator([new Currency('EUR')])); + $otherCurrencies->method('getIterator') + ->willReturn(new ArrayIterator([new Currency('USD')])); + + self::assertEquals( + [ + new Currency('EUR'), + new Currency('USD'), + ], + iterator_to_array(new AggregateCurrencies([$currencies, $otherCurrencies]), false) + ); + } + + /** @test */ + public function it_can_operate_be_rewinded_and_reused(): void + { + $currencies = $this->createMock(Currencies::class); + $otherCurrencies = $this->createMock(Currencies::class); + + $currencies->method('getIterator') + ->willReturn(new ArrayIterator([new Currency('EUR')])); + $otherCurrencies->method('getIterator') + ->willReturn(new ArrayIterator([new Currency('USD')])); + + $expectedCurrencies = [ + new Currency('EUR'), + new Currency('USD'), + ]; + $iterator = (new AggregateCurrencies([$currencies, $otherCurrencies])) + ->getIterator(); + + self::assertEquals( + $expectedCurrencies, + iterator_to_array($iterator, false) + ); + self::assertEquals( + $expectedCurrencies, + iterator_to_array($iterator, false), + 'Can re-use the previous iteration' + ); + } +} diff --git a/tests/Currencies/CachedCurrenciesTest.php b/tests/Currencies/CachedCurrenciesTest.php new file mode 100644 index 000000000..38ce1de71 --- /dev/null +++ b/tests/Currencies/CachedCurrenciesTest.php @@ -0,0 +1,125 @@ +createMock(CacheItemInterface::class); + $cache = $this->createMock(CacheItemPoolInterface::class); + $wrappedCurrencies = $this->createMock(Currencies::class); + + $miss->method('isHit') + ->willReturn(false); + $miss->expects(self::once()) + ->method('set') + ->with(true); + $miss->method('get') + ->willReturn(true); + + $cache->method('getItem') + ->with('currency|availability|EUR') + ->willReturn($miss); + $cache->expects(self::once()) + ->method('save') + ->with($miss); + + $wrappedCurrencies->method('contains') + ->with(self::equalTo($currency)) + ->willReturn(true); + + self::assertTrue( + (new CachedCurrencies($wrappedCurrencies, $cache)) + ->contains($currency) + ); + } + + /** @test */ + public function it_checks_currencies_from_the_cache(): void + { + $currency = new Currency('EUR'); + + $hit = $this->createMock(CacheItemInterface::class); + $cache = $this->createMock(CacheItemPoolInterface::class); + $wrappedCurrencies = $this->createMock(Currencies::class); + + $hit->method('isHit') + ->willReturn(true); + $hit->expects(self::never()) + ->method('set'); + $hit->method('get') + ->willReturn(true); + + $cache->method('getItem') + ->with('currency|availability|EUR') + ->willReturn($hit); + $cache->expects(self::never()) + ->method('save'); + + $wrappedCurrencies->expects(self::never()) + ->method('contains'); + + self::assertTrue( + (new CachedCurrencies($wrappedCurrencies, $cache)) + ->contains($currency) + ); + } + + /** @test */ + public function it_is_iterable(): void + { + $refreshed1 = $this->createMock(CacheItemInterface::class); + $refreshed2 = $this->createMock(CacheItemInterface::class); + $cache = $this->createMock(CacheItemPoolInterface::class); + $wrappedCurrencies = $this->createMock(Currencies::class); + + $refreshed1->expects(self::once()) + ->method('set') + ->with(true); + $refreshed2->expects(self::once()) + ->method('set') + ->with(true); + + $cache->method('getItem') + ->willReturnMap([ + ['currency|availability|EUR', $refreshed1], + ['currency|availability|USD', $refreshed2], + ]); + + $cache->expects(self::exactly(2)) + ->method('save') + ->with(self::logicalOr($refreshed1, $refreshed2)); + + $wrappedCurrencies->expects(self::once()) + ->method('getIterator') + ->willReturn(new ArrayIterator([ + new Currency('EUR'), + new Currency('USD'), + ])); + + self::assertEquals( + [ + new Currency('EUR'), + new Currency('USD'), + ], + iterator_to_array(new CachedCurrencies($wrappedCurrencies, $cache)) + ); + } +} diff --git a/tests/Currencies/CurrencyListTest.php b/tests/Currencies/CurrencyListTest.php index 867f29909..1d6e073a1 100644 --- a/tests/Currencies/CurrencyListTest.php +++ b/tests/Currencies/CurrencyListTest.php @@ -1,5 +1,7 @@ 2, 'MY2' => 0, 'MY3' => 1, ]; /** + * @psalm-param non-empty-string $currency + * * @dataProvider currencyCodeExamples * @test */ - public function it_has_currencies($currency) + public function itHasCurrencies(string $currency): void { - $currencies = new CurrencyList(self::$correctCurrencies); + $currencies = new CurrencyList(self::CORRECT_CURRENCIES); - $this->assertTrue($currencies->contains(new Currency($currency))); + self::assertTrue($currencies->contains(new Currency($currency))); } /** + * @psalm-param non-empty-string $currency + * * @dataProvider currencyCodeExamples * @test */ - public function it_provides_subunit($currency) + public function itProvidesSubunit(string $currency): void { - $currencies = new CurrencyList(self::$correctCurrencies); + $currencies = new CurrencyList(self::CORRECT_CURRENCIES); - $this->assertInternalType('int', $currencies->subunitFor(new Currency($currency))); + self::assertIsInt($currencies->subunitFor(new Currency($currency))); } /** * @test */ - public function it_throws_an_exception_when_providing_subunit_and_currency_is_unknown() + public function itThrowsAnExceptionWhenProvidingSubunitAndCurrencyIsUnknown(): void { - $currencies = new CurrencyList(self::$correctCurrencies); + $currencies = new CurrencyList(self::CORRECT_CURRENCIES); $this->expectException(UnknownCurrencyException::class); @@ -52,45 +62,22 @@ public function it_throws_an_exception_when_providing_subunit_and_currency_is_un /** * @test */ - public function it_is_iterable() + public function itIsIterable(): void { - $currencies = new CurrencyList(self::$correctCurrencies); + $currencies = new CurrencyList(self::CORRECT_CURRENCIES); $iterator = $currencies->getIterator(); - $this->assertContainsOnlyInstancesOf(Currency::class, $iterator); - } - - /** - * @dataProvider invalidInstantiation - * @test - */ - public function it_does_not_initialize_if_array_is_invalid(array $currencies) - { - $this->expectException(\InvalidArgumentException::class); - - new CurrencyList($currencies); + self::assertContainsOnlyInstancesOf(Currency::class, $iterator); } - public function currencyCodeExamples() + /** @psalm-return non-empty-list */ + public function currencyCodeExamples(): array { - $currencies = array_keys(self::$correctCurrencies); + $currencies = array_keys(self::CORRECT_CURRENCIES); - return array_map(function ($currency) { + return array_map(static function ($currency) { return [$currency]; }, $currencies); } - - public function invalidInstantiation() - { - return [ - [[1 => 2]], - [['' => 2]], - [['OWO' => []]], - [['OWO' => null]], - [['OWO' => '']], - [['OWO' => -2]], - [['OWO' => 2.1]], - ]; - } } diff --git a/tests/Currencies/ISOCurrenciesTest.php b/tests/Currencies/ISOCurrenciesTest.php index 819f01486..856862b2c 100644 --- a/tests/Currencies/ISOCurrenciesTest.php +++ b/tests/Currencies/ISOCurrenciesTest.php @@ -1,5 +1,7 @@ assertTrue($currencies->contains(new Currency($currency))); + self::assertTrue($currencies->contains(new Currency($currency))); } /** + * @psalm-param non-empty-string $currency + * * @dataProvider currencyCodeExamples * @test */ - public function it_provides_subunit($currency) + public function itProvidesSubunit(string $currency): void { $currencies = new ISOCurrencies(); - $this->assertInternalType('int', $currencies->subunitFor(new Currency($currency))); + self::assertIsInt($currencies->subunitFor(new Currency($currency))); } /** * @test */ - public function it_throws_an_exception_when_providing_subunit_and_currency_is_unknown() + public function itThrowsAnExceptionWhenProvidingSubunitAndCurrencyIsUnknown(): void { $this->expectException(UnknownCurrencyException::class); @@ -44,20 +54,22 @@ public function it_throws_an_exception_when_providing_subunit_and_currency_is_un } /** + * @psalm-param non-empty-string $currency + * * @dataProvider currencyCodeExamples * @test */ - public function it_provides_numeric_code($currency) + public function itProvidesNumericCode(string $currency): void { $currencies = new ISOCurrencies(); - $this->assertInternalType('int', $currencies->numericCodeFor(new Currency($currency))); + self::assertIsInt($currencies->numericCodeFor(new Currency($currency))); } /** * @test */ - public function it_throws_an_exception_when_providing_numeric_code_and_currency_is_unknown() + public function itThrowsAnExceptionWhenProvidingNumericCodeAndCurrencyIsUnknown(): void { $this->expectException(UnknownCurrencyException::class); @@ -69,22 +81,25 @@ public function it_throws_an_exception_when_providing_numeric_code_and_currency_ /** * @test */ - public function it_is_iterable() + public function itIsIterable(): void { $currencies = new ISOCurrencies(); $iterator = $currencies->getIterator(); - $this->assertContainsOnlyInstancesOf(Currency::class, $iterator); + self::assertContainsOnlyInstancesOf(Currency::class, $iterator); } - public function currencyCodeExamples() + /** + * @psalm-return non-empty-list + */ + public function currencyCodeExamples(): array { - $currencies = require __DIR__.'/../../resources/currency.php'; - $currencies = array_keys($currencies); + /** @psalm-var non-empty-array $currencies */ + $currencies = require __DIR__ . '/../../resources/currency.php'; - return array_map(function ($currency) { + return array_map(static function (string $currency) { return [$currency]; - }, $currencies); + }, array_keys($currencies)); } } diff --git a/tests/CurrencyPairTest.php b/tests/CurrencyPairTest.php index 84b37c50a..bba68ce93 100644 --- a/tests/CurrencyPairTest.php +++ b/tests/CurrencyPairTest.php @@ -1,21 +1,69 @@ equals(new CurrencyPair( + new Currency('GBP'), + new Currency('USD'), + '1.250000' + ))); + + self::assertFalse($pair->equals(new CurrencyPair( + new Currency('EUR'), + new Currency('GBP'), + '1.250000' + ))); + + self::assertFalse($pair->equals(new CurrencyPair( + new Currency('EUR'), + new Currency('USD'), + '1.5000' + ))); - $this->assertEquals($expectedJson, $actualJson); + self::assertTrue($pair->equals(new CurrencyPair( + new Currency('EUR'), + new Currency('USD'), + '1.250000' + ))); } } diff --git a/tests/CurrencyTest.php b/tests/CurrencyTest.php index 12dc0632e..e654634bc 100644 --- a/tests/CurrencyTest.php +++ b/tests/CurrencyTest.php @@ -1,17 +1,22 @@ assertEquals('"USD"', json_encode(new Currency('USD'))); + self::assertEquals('"USD"', json_encode(new Currency('USD'))); } } diff --git a/tests/Exchange/ExchangerExchangeTest.php b/tests/Exchange/ExchangerExchangeTest.php new file mode 100644 index 000000000..8e0d136ed --- /dev/null +++ b/tests/Exchange/ExchangerExchangeTest.php @@ -0,0 +1,55 @@ +createMock(ExchangeRate::class); + $exchangeRates = $this->createMock(ExchangeRateProvider::class); + + $exchangeRate->method('getValue') + ->willReturn(1.12); + $exchangeRates->method('getExchangeRate') + ->with(self::equalTo(new ExchangeRateQuery(new ExchangerCurrencyPair('EUR', 'USD')))) + ->willReturn($exchangeRate); + + $base = new Currency('EUR'); + $counter = new Currency('USD'); + $currencyPair = (new ExchangerExchange($exchangeRates)) + ->quote($base, $counter); + + self::assertEquals($base, $currencyPair->getBaseCurrency()); + self::assertEquals($counter, $currencyPair->getCounterCurrency()); + self::assertEquals('1.12000000000000', $currencyPair->getConversionRatio()); + } + + /** @test */ + public function it_throws_an_exception_when_cannot_exchange_currencies(): void + { + $exchangeRates = $this->createMock(ExchangeRateProvider::class); + + $exchangeRates->method('getExchangeRate') + ->willThrowException(new Exception()); + + $exchanger = new ExchangerExchange($exchangeRates); + + $this->expectException(UnresolvableCurrencyPairException::class); + $exchanger->quote(new Currency('EUR'), new Currency('XYZ')); + } +} diff --git a/tests/Exchange/IndirectExchangeTest.php b/tests/Exchange/IndirectExchangeTest.php index 9907c9f5d..ad30c794a 100644 --- a/tests/Exchange/IndirectExchangeTest.php +++ b/tests/Exchange/IndirectExchangeTest.php @@ -1,5 +1,7 @@ createExchange(); $pair = $exchange->quote(new Currency('USD'), new Currency('AOA')); // USD => EUR => AOA - $this->assertEquals('USD', $pair->getBaseCurrency()->getCode()); - $this->assertEquals('AOA', $pair->getCounterCurrency()->getCode()); - $this->assertEquals(12, $pair->getConversionRatio()); + self::assertEquals('USD', $pair->getBaseCurrency()->getCode()); + self::assertEquals('AOA', $pair->getCounterCurrency()->getCode()); + self::assertEquals(12, $pair->getConversionRatio()); } - private function createExchange() + private function createExchange(): IndirectExchange { $baseExchange = new FixedExchange([ 'USD' => [ - 'AFN' => 2, - 'EUR' => 4, + 'AFN' => '2', + 'EUR' => '4', ], 'AFN' => [ - 'DZD' => 12, - 'EUR' => 8, - ], - 'EUR' => [ - 'AOA' => 3, + 'DZD' => '12', + 'EUR' => '8', ], + 'EUR' => ['AOA' => '3'], 'DZD' => [ - 'AOA' => 5, - 'USD' => 2, - ], - 'ARS' => [ - 'AOA' => 2, + 'AOA' => '5', + 'USD' => '2', ], + 'ARS' => ['AOA' => '2'], ]); return new IndirectExchange($baseExchange, new ISOCurrencies()); @@ -55,21 +54,21 @@ private function createExchange() /** * @test */ - public function it_calculates_adjacent_nodes() + public function itCalculatesAdjacentNodes(): void { $exchange = $this->createExchange(); $pair = $exchange->quote(new Currency('USD'), new Currency('EUR')); - $this->assertEquals('USD', $pair->getBaseCurrency()->getCode()); - $this->assertEquals('EUR', $pair->getCounterCurrency()->getCode()); - $this->assertEquals(4, $pair->getConversionRatio()); + self::assertEquals('USD', $pair->getBaseCurrency()->getCode()); + self::assertEquals('EUR', $pair->getCounterCurrency()->getCode()); + self::assertEquals(4, $pair->getConversionRatio()); } /** * @test */ - public function it_throws_when_no_chain_is_found() + public function itThrowsWhenNoChainIsFound(): void { $exchange = $this->createExchange(); diff --git a/tests/Exchange/ReversedCurrenciesExchangeTest.php b/tests/Exchange/ReversedCurrenciesExchangeTest.php new file mode 100644 index 000000000..11f6bcbe5 --- /dev/null +++ b/tests/Exchange/ReversedCurrenciesExchangeTest.php @@ -0,0 +1,84 @@ +createMock(Exchange::class); + + $wrappedExchange->method('quote') + ->with(self::equalTo($base), self::equalTo($counter)) + ->willReturn(new CurrencyPair($base, $counter, '1.25')); + + self::assertEquals( + new CurrencyPair($base, $counter, '1.25'), + (new ReversedCurrenciesExchange($wrappedExchange)) + ->quote($base, $counter) + ); + } + + /** @test */ + public function it_exchanges_reversed_currencies_when_the_original_pair_is_not_found(): void + { + $base = new Currency('EUR'); + $counter = new Currency('USD'); + $wrappedExchange = $this->createMock(Exchange::class); + + $wrappedExchange->method('quote') + ->willReturnCallback(static function (Currency $givenBase, Currency $givenCounter) use ($base + ): CurrencyPair { + if ($givenBase->equals($base)) { + throw new UnresolvableCurrencyPairException(); + } + + return new CurrencyPair($givenBase, $givenCounter, '1.25'); + }); + + self::assertEquals( + new CurrencyPair($base, $counter, '0.8'), + (new ReversedCurrenciesExchange($wrappedExchange)) + ->quote($base, $counter) + ); + } + + /** @test */ + public function it_throws_an_exception_when_neither_the_original_nor_the_reversed_currency_pair_can_be_resolved(): void + { + $exception1 = new UnresolvableCurrencyPairException('first thrown'); + $exception2 = new UnresolvableCurrencyPairException('second thrown'); + $base = new Currency('EUR'); + $counter = new Currency('USD'); + $wrappedExchange = $this->createMock(Exchange::class); + + $wrappedExchange->method('quote') + ->willReturnCallback(static function (Currency $givenBase) use ($exception2, $exception1, $base + ): CurrencyPair { + if ($givenBase->equals($base)) { + throw $exception1; + } + + throw $exception2; + }); + + $exchanger = new ReversedCurrenciesExchange($wrappedExchange); + + $this->expectExceptionObject($exception1); + + $exchanger->quote($base, $counter); + } +} diff --git a/tests/Exchange/SwapExchangeTest.php b/tests/Exchange/SwapExchangeTest.php new file mode 100644 index 000000000..f3fed1eec --- /dev/null +++ b/tests/Exchange/SwapExchangeTest.php @@ -0,0 +1,64 @@ +createMock(Swap::class); + $exchangeRate = $this->createMock(ExchangeRate::class); + + $exchangeRate->method('getValue') + ->willReturn(1.25); + + $swapExchange->expects(self::once()) + ->method('latest') + ->with('EUR/USD') + ->willReturn($exchangeRate); + + self::assertEquals( + new CurrencyPair($base, $counter, '1.25'), + (new SwapExchange($swapExchange)) + ->quote($base, $counter) + ); + } + + /** @test */ + public function it_throws_an_exception_when_cannot_exchange_currencies(): void + { + $base = new Currency('EUR'); + $counter = new Currency('USD'); + $swapExchange = $this->createMock(Swap::class); + $exchangeRate = $this->createMock(ExchangeRate::class); + + $exchangeRate->method('getValue') + ->willReturn(1.25); + + $swapExchange->expects(self::once()) + ->method('latest') + ->with('EUR/USD') + ->willThrowException(new ExchangerException()); + + $exchanger = new SwapExchange($swapExchange); + + $this->expectException(UnresolvableCurrencyPairException::class); + + $exchanger->quote($base, $counter); + } +} diff --git a/tests/Formatter/AggregateMoneyFormatterTest.php b/tests/Formatter/AggregateMoneyFormatterTest.php index db51dcc14..fc8904959 100644 --- a/tests/Formatter/AggregateMoneyFormatterTest.php +++ b/tests/Formatter/AggregateMoneyFormatterTest.php @@ -1,20 +1,80 @@ createMock(MoneyFormatter::class); + + $eurFormatter->method('format') + ->with(self::equalTo($money)) + ->willReturn('FIRST'); + + self::assertEquals( + 'FIRST', + (new AggregateMoneyFormatter(['EUR' => $eurFormatter])) + ->format($money) + ); + } + + /** @test */ + public function it_throws_an_exception_when_no_formatter_for_currency_is_found(): void + { + $eurFormatter = $this->createMock(MoneyFormatter::class); + + $eurFormatter->expects(self::never()) + ->method('format'); + + $formatter = new AggregateMoneyFormatter(['EUR' => $eurFormatter]); + + $this->expectException(FormatterException::class); + $formatter->format(new Money(1, new Currency('USD'))); + } + + /** @test */ + public function it_uses_default_formatter_when_no_specific_one_is_found(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Initialize an empty Money\\Formatter\\AggregateMoneyFormatter is not possible'); + $eur = new Money(1, new Currency('EUR')); + $usd = new Money(1, new Currency('USD')); + $other = new Money(1, new Currency('CZK')); + $eurFormatter = $this->createMock(MoneyFormatter::class); + $usdFormatter = $this->createMock(MoneyFormatter::class); + $defaultFormatter = $this->createMock(MoneyFormatter::class); + + $eurFormatter->method('format') + ->with(self::equalTo($eur)) + ->willReturn('EUR_FORMATTER'); + + $usdFormatter->method('format') + ->with(self::equalTo($usd)) + ->willReturn('USD_FORMATTER'); + + $defaultFormatter->method('format') + ->with(self::equalTo($other)) + ->willReturn('OTHER_FORMATTER'); + + $formatter = new AggregateMoneyFormatter([ + 'EUR' => $eurFormatter, + 'USD' => $usdFormatter, + '*' => $defaultFormatter, + ]); - new AggregateMoneyFormatter([]); + self::assertEquals('EUR_FORMATTER', $formatter->format($eur)); + self::assertEquals('USD_FORMATTER', $formatter->format($usd)); + self::assertEquals('OTHER_FORMATTER', $formatter->format($other)); } } diff --git a/tests/Formatter/BitcoinMoneyFormatterTest.php b/tests/Formatter/BitcoinMoneyFormatterTest.php index 15c7f5716..85be82c0a 100644 --- a/tests/Formatter/BitcoinMoneyFormatterTest.php +++ b/tests/Formatter/BitcoinMoneyFormatterTest.php @@ -1,5 +1,7 @@ prophesize(Currencies::class); - - $formatter = new BitcoinMoneyFormatter($fractionDigits, $currencies->reveal()); - - $currency = new Currency('XBT'); - $money = new Money($value, $currency); - - $currencies->subunitFor($currency)->willReturn(8); - - $this->assertSame($formatted, $formatter->format($money)); + $currencies = $this->createMock(Currencies::class); + $currency = new Currency('XBT'); + + $currencies->method('subunitFor') + ->with(self::equalTo($currency)) + ->willReturn(8); + + self::assertSame( + $formatted, + (new BitcoinMoneyFormatter($fractionDigits, $currencies)) + ->format(new Money($value, $currency)) + ); } - public function bitcoinExamples() + /** + * @psalm-return non-empty-list + */ + public function bitcoinExamples(): array { return [ [100000000000, "\xC9\x831000.00", 2], diff --git a/tests/Formatter/DecimalMoneyFormatterTest.php b/tests/Formatter/DecimalMoneyFormatterTest.php index e67c0dd62..75d01d91d 100644 --- a/tests/Formatter/DecimalMoneyFormatterTest.php +++ b/tests/Formatter/DecimalMoneyFormatterTest.php @@ -1,5 +1,7 @@ prophesize(Currencies::class); + $currencies = $this->createMock(Currencies::class); - $currencies->subunitFor(Argument::allOf( - Argument::type(Currency::class), - Argument::which('getCode', $currency) - ))->willReturn($subunit); + $currencies->method('subunitFor') + ->with(self::callback(static fn (Currency $givenCurrency): bool => $currency === $givenCurrency->getCode())) + ->willReturn($subunit); - $moneyFormatter = new DecimalMoneyFormatter($currencies->reveal()); - $this->assertSame($result, $moneyFormatter->format($money)); + $moneyFormatter = new DecimalMoneyFormatter($currencies); + self::assertSame($result, $moneyFormatter->format($money)); } + /** + * @psalm-return non-empty-list + */ public static function moneyExamples() { return [ diff --git a/tests/Formatter/IntlLocalizedDecimalFormatterTest.php b/tests/Formatter/IntlLocalizedDecimalFormatterTest.php index e20528907..103a6ec44 100644 --- a/tests/Formatter/IntlLocalizedDecimalFormatterTest.php +++ b/tests/Formatter/IntlLocalizedDecimalFormatterTest.php @@ -1,62 +1,79 @@ setAttribute(\NumberFormatter::FRACTION_DIGITS, $fractionDigits); + $numberFormatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $fractionDigits); - $currencies = $this->prophesize(Currencies::class); + $currencies = $this->createMock(Currencies::class); - $currencies->subunitFor(Argument::allOf( - Argument::type(Currency::class), - Argument::which('getCode', $currency) - ))->willReturn($subunit); + $currencies->method('subunitFor') + ->with(self::callback(static fn (Currency $givenCurrency): bool => $currency === $givenCurrency->getCode())) + ->willReturn($subunit); - $moneyFormatter = new IntlLocalizedDecimalFormatter($numberFormatter, $currencies->reveal()); - $this->assertSame($result, $moneyFormatter->format($money)); + $moneyFormatter = new IntlLocalizedDecimalFormatter($numberFormatter, $currencies); + self::assertSame($result, $moneyFormatter->format($money)); } - public static function moneyExamples() + /** + * @psalm-return non-empty-list + */ + public static function moneyExamples(): array { return [ - [5005, 'USD', 2, '50', \NumberFormatter::DECIMAL, 0], - [100, 'USD', 2, '1.00', \NumberFormatter::DECIMAL, 2], - [41, 'USD', 2, '0.41', \NumberFormatter::DECIMAL, 2], - [5, 'USD', 2, '0.05', \NumberFormatter::DECIMAL, 2], - [5, 'USD', 2, '0.050', \NumberFormatter::DECIMAL, 3], - [35, 'USD', 2, '0.350', \NumberFormatter::DECIMAL, 3], - [135, 'USD', 2, '1.350', \NumberFormatter::DECIMAL, 3], - [6135, 'USD', 2, '61.350', \NumberFormatter::DECIMAL, 3], - [-6135, 'USD', 2, '-61.350', \NumberFormatter::DECIMAL, 3], - [-6152, 'USD', 2, '-61.5', \NumberFormatter::DECIMAL, 1], - [5, 'EUR', 2, '0.05', \NumberFormatter::DECIMAL, 2], - [50, 'EUR', 2, '0.50', \NumberFormatter::DECIMAL, 2], - [500, 'EUR', 2, '5.00', \NumberFormatter::DECIMAL, 2], - [5, 'EUR', 2, '0.05', \NumberFormatter::DECIMAL, 2], - [50, 'EUR', 2, '0.50', \NumberFormatter::DECIMAL, 2], - [500, 'EUR', 2, '5.00', \NumberFormatter::DECIMAL, 2], - [5, 'EUR', 2, '0', \NumberFormatter::DECIMAL, 0], - [50, 'EUR', 2, '0', \NumberFormatter::DECIMAL, 0], - [500, 'EUR', 2, '5', \NumberFormatter::DECIMAL, 0], - [5055, 'USD', 2, '51', \NumberFormatter::DECIMAL, 0], + [5005, 'USD', 2, '50', NumberFormatter::DECIMAL, 0], + [100, 'USD', 2, '1.00', NumberFormatter::DECIMAL, 2], + [41, 'USD', 2, '0.41', NumberFormatter::DECIMAL, 2], + [5, 'USD', 2, '0.05', NumberFormatter::DECIMAL, 2], + [5, 'USD', 2, '0.050', NumberFormatter::DECIMAL, 3], + [35, 'USD', 2, '0.350', NumberFormatter::DECIMAL, 3], + [135, 'USD', 2, '1.350', NumberFormatter::DECIMAL, 3], + [6135, 'USD', 2, '61.350', NumberFormatter::DECIMAL, 3], + [-6135, 'USD', 2, '-61.350', NumberFormatter::DECIMAL, 3], + [-6152, 'USD', 2, '-61.5', NumberFormatter::DECIMAL, 1], + [5, 'EUR', 2, '0.05', NumberFormatter::DECIMAL, 2], + [50, 'EUR', 2, '0.50', NumberFormatter::DECIMAL, 2], + [500, 'EUR', 2, '5.00', NumberFormatter::DECIMAL, 2], + [5, 'EUR', 2, '0.05', NumberFormatter::DECIMAL, 2], + [50, 'EUR', 2, '0.50', NumberFormatter::DECIMAL, 2], + [500, 'EUR', 2, '5.00', NumberFormatter::DECIMAL, 2], + [5, 'EUR', 2, '0', NumberFormatter::DECIMAL, 0], + [50, 'EUR', 2, '0', NumberFormatter::DECIMAL, 0], + [500, 'EUR', 2, '5', NumberFormatter::DECIMAL, 0], + [5055, 'USD', 2, '51', NumberFormatter::DECIMAL, 0], ]; } } diff --git a/tests/Formatter/IntlMoneyFormatterTest.php b/tests/Formatter/IntlMoneyFormatterTest.php index c6ecd86d7..574d589bf 100644 --- a/tests/Formatter/IntlMoneyFormatterTest.php +++ b/tests/Formatter/IntlMoneyFormatterTest.php @@ -1,67 +1,86 @@ setPattern('¤#,##0.00;-¤#,##0.00'); } - $numberFormatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $fractionDigits); + $numberFormatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $fractionDigits); - $currencies = $this->prophesize(Currencies::class); + $currencies = $this->createMock(Currencies::class); - $currencies->subunitFor(Argument::allOf( - Argument::type(Currency::class), - Argument::which('getCode', $currency) - ))->willReturn($subunit); + $currencies->method('subunitFor') + ->with(self::callback(static fn (Currency $givenCurrency): bool => $currency === $givenCurrency->getCode())) + ->willReturn($subunit); - $moneyFormatter = new IntlMoneyFormatter($numberFormatter, $currencies->reveal()); - $this->assertSame($result, $moneyFormatter->format($money)); + $moneyFormatter = new IntlMoneyFormatter($numberFormatter, $currencies); + self::assertSame($result, $moneyFormatter->format($money)); } - public static function moneyExamples() + /** + * @psalm-return non-empty-list + */ + public static function moneyExamples(): array { return [ - [5005, 'USD', 2, '$50', \NumberFormatter::CURRENCY, true, 0], - [100, 'USD', 2, '$1.00', \NumberFormatter::CURRENCY, true, 2], - [41, 'USD', 2, '$0.41', \NumberFormatter::CURRENCY, true, 2], - [5, 'USD', 2, '$0.05', \NumberFormatter::CURRENCY, true, 2], - [5, 'USD', 2, '$0.050', \NumberFormatter::CURRENCY, true, 3], - [35, 'USD', 2, '$0.350', \NumberFormatter::CURRENCY, true, 3], - [135, 'USD', 2, '$1.350', \NumberFormatter::CURRENCY, true, 3], - [6135, 'USD', 2, '$61.350', \NumberFormatter::CURRENCY, true, 3], - [-6135, 'USD', 2, '-$61.350', \NumberFormatter::CURRENCY, true, 3], - [-6152, 'USD', 2, '-$61.5', \NumberFormatter::CURRENCY, true, 1], - [5, 'EUR', 2, '€0.05', \NumberFormatter::CURRENCY, true, 2], - [50, 'EUR', 2, '€0.50', \NumberFormatter::CURRENCY, true, 2], - [500, 'EUR', 2, '€5.00', \NumberFormatter::CURRENCY, true, 2], - [5, 'EUR', 2, '€0.05', \NumberFormatter::DECIMAL, true, 2], - [50, 'EUR', 2, '€0.50', \NumberFormatter::DECIMAL, true, 2], - [500, 'EUR', 2, '€5.00', \NumberFormatter::DECIMAL, true, 2], - [5, 'EUR', 2, '0', \NumberFormatter::DECIMAL, false, 0], - [50, 'EUR', 2, '0', \NumberFormatter::DECIMAL, false, 0], - [500, 'EUR', 2, '5', \NumberFormatter::DECIMAL, false, 0], - [5, 'EUR', 2, '5%', \NumberFormatter::PERCENT, false, 0], - [5055, 'USD', 2, '$51', \NumberFormatter::CURRENCY, true, 0], + [5005, 'USD', 2, '$50', NumberFormatter::CURRENCY, true, 0], + [100, 'USD', 2, '$1.00', NumberFormatter::CURRENCY, true, 2], + [41, 'USD', 2, '$0.41', NumberFormatter::CURRENCY, true, 2], + [5, 'USD', 2, '$0.05', NumberFormatter::CURRENCY, true, 2], + [5, 'USD', 2, '$0.050', NumberFormatter::CURRENCY, true, 3], + [35, 'USD', 2, '$0.350', NumberFormatter::CURRENCY, true, 3], + [135, 'USD', 2, '$1.350', NumberFormatter::CURRENCY, true, 3], + [6135, 'USD', 2, '$61.350', NumberFormatter::CURRENCY, true, 3], + [-6135, 'USD', 2, '-$61.350', NumberFormatter::CURRENCY, true, 3], + [-6152, 'USD', 2, '-$61.5', NumberFormatter::CURRENCY, true, 1], + [5, 'EUR', 2, '€0.05', NumberFormatter::CURRENCY, true, 2], + [50, 'EUR', 2, '€0.50', NumberFormatter::CURRENCY, true, 2], + [500, 'EUR', 2, '€5.00', NumberFormatter::CURRENCY, true, 2], + [5, 'EUR', 2, '€0.05', NumberFormatter::DECIMAL, true, 2], + [50, 'EUR', 2, '€0.50', NumberFormatter::DECIMAL, true, 2], + [500, 'EUR', 2, '€5.00', NumberFormatter::DECIMAL, true, 2], + [5, 'EUR', 2, '0', NumberFormatter::DECIMAL, false, 0], + [50, 'EUR', 2, '0', NumberFormatter::DECIMAL, false, 0], + [500, 'EUR', 2, '5', NumberFormatter::DECIMAL, false, 0], + [5, 'EUR', 2, '5%', NumberFormatter::PERCENT, false, 0], + [5055, 'USD', 2, '$51', NumberFormatter::CURRENCY, true, 0], ]; } } diff --git a/tests/MoneyFactoryTest.php b/tests/MoneyFactoryTest.php index 9ad448a4b..0d9524355 100644 --- a/tests/MoneyFactoryTest.php +++ b/tests/MoneyFactoryTest.php @@ -1,5 +1,7 @@ getCode(); + $code = $currency->getCode(); $money = Money::{$code}(20); - $this->assertInstanceOf(Money::class, $money); - $this->assertEquals(new Money(20, $currency), $money); + self::assertInstanceOf(Money::class, $money); + self::assertEquals(new Money(20, $currency), $money); } - public function currencyExamples() + /** @psalm-return list */ + public function currencyExamples(): array { $currencies = new AggregateCurrencies([ new ISOCurrencies(), diff --git a/tests/MoneyTest.php b/tests/MoneyTest.php index 55ddc66d6..19553fb63 100644 --- a/tests/MoneyTest.php +++ b/tests/MoneyTest.php @@ -1,129 +1,144 @@ assertEquals($equality, $money->equals(new Money($amount, $currency))); + self::assertEquals($equality, $money->equals(new Money($amount, $currency))); + } + + /** @test */ + public function it_can_compare_currency(): void + { + $money1 = new Money(self::AMOUNT, new Currency('USD')); + $money2 = new Money(self::AMOUNT, new Currency('USD')); + $money3 = new Money(self::AMOUNT, new Currency('EUR')); + + self::assertTrue($money1->isSameCurrency($money2)); + self::assertTrue($money2->isSameCurrency($money1)); + self::assertFalse($money1->isSameCurrency($money3)); + self::assertFalse($money3->isSameCurrency($money1)); } /** * @dataProvider comparisonExamples * @test */ - public function it_compares_two_amounts($other, $result) + public function itComparesTwoAmounts(int $other, int $result): void { $money = new Money(self::AMOUNT, new Currency(self::CURRENCY)); $other = new Money($other, new Currency(self::CURRENCY)); - $this->assertEquals($result, $money->compare($other)); - $this->assertEquals(1 === $result, $money->greaterThan($other)); - $this->assertEquals(0 <= $result, $money->greaterThanOrEqual($other)); - $this->assertEquals(-1 === $result, $money->lessThan($other)); - $this->assertEquals(0 >= $result, $money->lessThanOrEqual($other)); + self::assertEquals($result, $money->compare($other)); + self::assertEquals($result === 1, $money->greaterThan($other)); + self::assertEquals(0 <= $result, $money->greaterThanOrEqual($other)); + self::assertEquals($result === -1, $money->lessThan($other)); + self::assertEquals(0 >= $result, $money->lessThanOrEqual($other)); if ($result === 0) { - $this->assertEquals($money, $other); + self::assertEquals($money, $other); } else { - $this->assertNotEquals($money, $other); + self::assertNotEquals($money, $other); } } /** - * @dataProvider roundExamples + * @psalm-param numeric-string $multiplier + * @psalm-param Money::ROUND_* $roundingMode + * @psalm-param numeric-string $result + * + * @dataProvider roundingExamples * @test */ - public function it_multiplies_the_amount($multiplier, $roundingMode, $result) + public function itMultipliesTheAmount(string $multiplier, int $roundingMode, string $result): void { $money = new Money(1, new Currency(self::CURRENCY)); $money = $money->multiply($multiplier, $roundingMode); - $this->assertInstanceOf(Money::class, $money); - $this->assertEquals((string) $result, $money->getAmount()); + self::assertInstanceOf(Money::class, $money); + self::assertEquals($result, $money->getAmount()); } /** * @test */ - public function it_multiplies_the_amount_with_locale_that_uses_comma_separator() + public function itMultipliesTheAmountWithLocaleThatUsesCommaSeparator(): void { $this->setLocale(LC_ALL, 'es_ES.utf8'); $money = new Money(100, new Currency(self::CURRENCY)); - $money = $money->multiply(10 / 100); + $money = $money->multiply('0.1'); - $this->assertInstanceOf(Money::class, $money); - $this->assertEquals(10, $money->getAmount()); + self::assertInstanceOf(Money::class, $money); + self::assertEquals('10', $money->getAmount()); } /** - * @dataProvider invalidOperandExamples + * @psalm-param numeric-string $divisor + * @psalm-param Money::ROUND_* $roundingMode + * @psalm-param numeric-string $result + * + * @dataProvider roundingExamples * @test */ - public function it_throws_an_exception_when_operand_is_invalid_during_multiplication($operand) - { - $this->expectException(\InvalidArgumentException::class); - - $money = new Money(1, new Currency(self::CURRENCY)); - - $money->multiply($operand); - } - - /** - * @dataProvider roundExamples - */ - public function it_divides_the_amount($divisor, $roundingMode, $result) - { - $money = new Money(1, new Currency(self::CURRENCY)); - - $money = $money->divide(1 / $divisor, $roundingMode); - - $this->assertInstanceOf(Money::class, $money); - $this->assertEquals((string) $result, $money->getAmount()); - } - - /** - * @dataProvider invalidOperandExamples - * @test - */ - public function it_throws_an_exception_when_operand_is_invalid_during_division($operand) - { - $this->expectException(\InvalidArgumentException::class); - - $money = new Money(1, new Currency(self::CURRENCY)); - - $money->divide($operand); + public function it_divides_the_amount(string $divisor, int $roundingMode, string $result): void + { + self::assertEquals( + $result, + (new Money(1, new Currency(self::CURRENCY))) + ->multiply($divisor, $roundingMode) + ->multiply($divisor, $roundingMode) + ->divide($divisor, $roundingMode) + ->getAmount(), + 'Our dataset does not contain a lot of data around divisions: we abuse multiplication to verify inverse function properties' + ); } /** + * @psalm-param int $amount + * @psalm-param non-empty-array $ratios + * @psalm-param non-empty-array $results + * * @dataProvider allocationExamples * @test */ - public function it_allocates_amount($amount, $ratios, $results) + public function itAllocatesAmount(int $amount, array $ratios, array $results): void { $money = new Money($amount, new Currency(self::CURRENCY)); @@ -132,15 +147,39 @@ public function it_allocates_amount($amount, $ratios, $results) foreach ($allocated as $key => $money) { $compareTo = new Money($results[$key], $money->getCurrency()); - $this->assertEquals($money, $compareTo); + self::assertTrue($money->equals($compareTo)); } } + /** @test */ + public function it_throws_an_exception_when_allocation_ratio_is_negative(): void + { + $money = new Money(100, new Currency(self::CURRENCY)); + + $this->expectException(InvalidArgumentException::class); + /** @psalm-suppress UnusedMethodCall this method throws, but is also considered pure. It's unused by design. */ + $money->allocate([-1]); + } + + /** @test */ + public function it_throws_an_exception_when_allocation_total_is_zero(): void + { + $money = new Money(100, new Currency(self::CURRENCY)); + + $this->expectException(InvalidArgumentException::class); + /** @psalm-suppress UnusedMethodCall this method throws, but is also considered pure. It's unused by design. */ + $money->allocate([0, 0]); + } + /** + * @psalm-param positive-int $amount + * @psalm-param positive-int $target + * @psalm-param non-empty-list $results + * * @dataProvider allocationTargetExamples * @test */ - public function it_allocates_amount_to_n_targets($amount, $target, $results) + public function itAllocatesAmountToNTargets(int $amount, int $target, array $results): void { $money = new Money($amount, new Currency(self::CURRENCY)); @@ -149,75 +188,87 @@ public function it_allocates_amount_to_n_targets($amount, $target, $results) foreach ($allocated as $key => $money) { $compareTo = new Money($results[$key], $money->getCurrency()); - $this->assertEquals($money, $compareTo); + self::assertTrue($money->equals($compareTo)); } } /** + * @psalm-param int|numeric-string $amount + * * @dataProvider comparatorExamples * @test */ - public function it_has_comparators($amount, $isZero, $isPositive, $isNegative) + public function itHasComparators(int|string $amount, bool $isZero, bool $isPositive, bool $isNegative): void { $money = new Money($amount, new Currency(self::CURRENCY)); - $this->assertEquals($isZero, $money->isZero()); - $this->assertEquals($isPositive, $money->isPositive()); - $this->assertEquals($isNegative, $money->isNegative()); + self::assertEquals($isZero, $money->isZero()); + self::assertEquals($isPositive, $money->isPositive()); + self::assertEquals($isNegative, $money->isNegative()); } /** + * @psalm-param int|numeric-string $amount + * @psalm-param positive-int|0 $result + * * @dataProvider absoluteExamples * @test */ - public function it_calculates_the_absolute_amount($amount, $result) + public function itCalculatesTheAbsoluteAmount($amount, $result): void { $money = new Money($amount, new Currency(self::CURRENCY)); $money = $money->absolute(); - $this->assertEquals($result, $money->getAmount()); + self::assertEquals($result, $money->getAmount()); } /** + * @psalm-param int|numeric-string $amount + * @psalm-param int $result + * * @dataProvider negativeExamples * @test */ - public function it_calculates_the_negative_amount($amount, $result) + public function itCalculatesTheNegativeAmount($amount, $result): void { $money = new Money($amount, new Currency(self::CURRENCY)); $money = $money->negative(); - $this->assertEquals($result, $money->getAmount()); + self::assertEquals($result, $money->getAmount()); } /** + * @psalm-param positive-int $left + * @psalm-param positive-int $right + * @psalm-param numeric-string $expected + * * @dataProvider modExamples * @test */ - public function it_calculates_the_modulus_of_an_amount($left, $right, $expected) + public function itCalculatesTheModulusOfAnAmount($left, $right, $expected): void { - $money = new Money($left, new Currency(self::CURRENCY)); + $money = new Money($left, new Currency(self::CURRENCY)); $rightMoney = new Money($right, new Currency(self::CURRENCY)); $money = $money->mod($rightMoney); - $this->assertInstanceOf(Money::class, $money); - $this->assertEquals($expected, $money->getAmount()); + self::assertInstanceOf(Money::class, $money); + self::assertEquals($expected, $money->getAmount()); } /** * @test */ - public function it_converts_to_json() + public function itConvertsToJson(): void { - $this->assertEquals( + self::assertEquals( '{"amount":"350","currency":"EUR"}', json_encode(Money::EUR(350)) ); - $this->assertEquals( + self::assertEquals( ['amount' => '350', 'currency' => 'EUR'], Money::EUR(350)->jsonSerialize() ); @@ -226,88 +277,97 @@ public function it_converts_to_json() /** * @test */ - public function it_supports_max_int() + public function itSupportsMaxInt(): void { $one = new Money(1, new Currency('EUR')); - $this->assertInstanceOf(Money::class, new Money(PHP_INT_MAX, new Currency('EUR'))); - $this->assertInstanceOf(Money::class, (new Money(PHP_INT_MAX, new Currency('EUR')))->add($one)); - $this->assertInstanceOf(Money::class, (new Money(PHP_INT_MAX, new Currency('EUR')))->subtract($one)); + self::assertInstanceOf(Money::class, new Money(PHP_INT_MAX, new Currency('EUR'))); + self::assertInstanceOf(Money::class, (new Money(PHP_INT_MAX, new Currency('EUR')))->add($one)); + self::assertInstanceOf(Money::class, (new Money(PHP_INT_MAX, new Currency('EUR')))->subtract($one)); } /** * @test */ - public function it_returns_ratio_of() + public function itReturnsRatioOf(): void { $currency = new Currency('EUR'); - $zero = new Money(0, $currency); - $three = new Money(3, $currency); - $six = new Money(6, $currency); + $zero = new Money(0, $currency); + $three = new Money(3, $currency); + $six = new Money(6, $currency); - $this->assertEquals(0, $zero->ratioOf($six)); - $this->assertEquals(0.5, $three->ratioOf($six)); - $this->assertEquals(1, $three->ratioOf($three)); - $this->assertEquals(2, $six->ratioOf($three)); + self::assertEquals(0, $zero->ratioOf($six)); + self::assertEquals(0.5, $three->ratioOf($six)); + self::assertEquals(1, $three->ratioOf($three)); + self::assertEquals(2, $six->ratioOf($three)); } /** * @test */ - public function it_throws_when_calculating_ratio_of_zero() + public function itThrowsWhenCalculatingRatioOfZero(): void { - $this->expectException(\InvalidArgumentException::class); - $currency = new Currency('EUR'); - $zero = new Money(0, $currency); - $six = new Money(6, $currency); + $zero = new Money(0, $currency); + $six = new Money(6, $currency); + + $this->expectException(InvalidArgumentException::class); + /** @psalm-suppress UnusedMethodCall this method throws, but is also considered pure. It's unused by design. */ $six->ratioOf($zero); } /** + * @psalm-param non-empty-list $values + * * @dataProvider sumExamples * @test */ - public function it_calculates_sum($values, $sum) + public function itCalculatesSum(array $values, Money $sum): void { - $this->assertEquals($sum, Money::sum(...$values)); + self::assertEquals($sum, Money::sum(...$values)); } /** + * @psalm-param non-empty-list $values + * * @dataProvider minExamples * @test */ - public function it_calculates_min($values, $min) + public function itCalculatesMin(array $values, Money $min): void { - $this->assertEquals($min, Money::min(...$values)); + self::assertEquals($min, Money::min(...$values)); } /** + * @psalm-param non-empty-list $values + * * @dataProvider maxExamples * @test */ - public function it_calculates_max($values, $max) + public function itCalculatesMax(array $values, Money $max): void { - $this->assertEquals($max, Money::max(...$values)); + self::assertEquals($max, Money::max(...$values)); } /** + * @psalm-param non-empty-list $values + * * @dataProvider avgExamples * @test */ - public function it_calculates_avg($values, $avg) + public function itCalculatesAvg(array $values, Money $avg): void { - $this->assertEquals($avg, Money::avg(...$values)); + self::assertEquals($avg, Money::avg(...$values)); } /** * @test * @requires PHP 7.0 */ - public function it_throws_when_calculating_min_with_zero_arguments() + public function itThrowsWhenCalculatingMinWithZeroArguments(): void { - $this->expectException(\Throwable::class); + $this->expectException(Throwable::class); Money::min(...[]); } @@ -315,9 +375,9 @@ public function it_throws_when_calculating_min_with_zero_arguments() * @test * @requires PHP 7.0 */ - public function it_throws_when_calculating_max_with_zero_arguments() + public function itThrowsWhenCalculatingMaxWithZeroArguments(): void { - $this->expectException(\Throwable::class); + $this->expectException(Throwable::class); Money::max(...[]); } @@ -325,9 +385,9 @@ public function it_throws_when_calculating_max_with_zero_arguments() * @test * @requires PHP 7.0 */ - public function it_throws_when_calculating_sum_with_zero_arguments() + public function itThrowsWhenCalculatingSumWithZeroArguments(): void { - $this->expectException(\Throwable::class); + $this->expectException(Throwable::class); Money::sum(...[]); } @@ -335,25 +395,37 @@ public function it_throws_when_calculating_sum_with_zero_arguments() * @test * @requires PHP 7.0 */ - public function it_throws_when_calculating_avg_with_zero_arguments() + public function itThrowsWhenCalculatingAvgWithZeroArguments(): void { - $this->expectException(\Throwable::class); + $this->expectException(Throwable::class); Money::avg(...[]); } - public function equalityExamples() + /** + * @psalm-return non-empty-list + */ + public function equalityExamples(): array { return [ - [self::AMOUNT, new Currency(self::CURRENCY), true], - [self::AMOUNT + 1, new Currency(self::CURRENCY), false], - [self::AMOUNT, new Currency(self::OTHER_CURRENCY), false], - [self::AMOUNT + 1, new Currency(self::OTHER_CURRENCY), false], - [(string) self::AMOUNT, new Currency(self::CURRENCY), true], - [((string) self::AMOUNT).'.000', new Currency(self::CURRENCY), true], + [10, new Currency(self::CURRENCY), true], + [10, new Currency(self::OTHER_CURRENCY), false], + [11, new Currency(self::OTHER_CURRENCY), false], + ['10', new Currency(self::CURRENCY), true], + ['10.000', new Currency(self::CURRENCY), true], ]; } - public function comparisonExamples() + /** + * @psalm-return non-empty-list + */ + public function comparisonExamples(): array { return [ [self::AMOUNT, 0], @@ -362,18 +434,17 @@ public function comparisonExamples() ]; } - public function invalidOperandExamples() - { - return [ - [[]], - [false], - ['operand'], - [null], - [new \stdClass()], - ]; - } - - public function allocationExamples() + /** + * @psalm-return non-empty-list, + * non-empty-array + * }> + * + * @psalm-suppress LessSpecificReturnStatement type inference for `array` fails to find non-empty-array for the last item + * @psalm-suppress MoreSpecificReturnType type inference for `array` fails to find non-empty-array for the last item + */ + public function allocationExamples(): array { return [ [100, [1, 1, 1], [34, 33, 33]], @@ -396,7 +467,14 @@ public function allocationExamples() ]; } - public function allocationTargetExamples() + /** + * @psalm-return non-empty-list + * }> + */ + public function allocationTargetExamples(): array { return [ [15, 2, [8, 7]], @@ -406,7 +484,15 @@ public function allocationTargetExamples() ]; } - public function comparatorExamples() + /** + * @psalm-return non-empty-list + */ + public function comparatorExamples(): array { return [ [1, false, true, false], @@ -418,7 +504,13 @@ public function comparatorExamples() ]; } - public function absoluteExamples() + /** + * @psalm-return non-empty-list + */ + public function absoluteExamples(): array { return [ [1, 1], @@ -430,7 +522,13 @@ public function absoluteExamples() ]; } - public function negativeExamples() + /** + * @psalm-return non-empty-list + */ + public function negativeExamples(): array { return [ [1, -1], @@ -442,7 +540,14 @@ public function negativeExamples() ]; } - public function modExamples() + /** + * @psalm-return non-empty-list + */ + public function modExamples(): array { return [ [11, 5, '1'], diff --git a/tests/NumberTest.php b/tests/NumberTest.php index e933e6b43..c4a4e9468 100644 --- a/tests/NumberTest.php +++ b/tests/NumberTest.php @@ -1,63 +1,106 @@ assertSame($decimal, $number->isDecimal()); - $this->assertSame($half, $number->isHalf()); - $this->assertSame($currentEven, $number->isCurrentEven()); - $this->assertSame($negative, $number->isNegative()); - $this->assertSame($integerPart, $number->getIntegerPart()); - $this->assertSame($fractionalPart, $number->getFractionalPart()); - $this->assertSame($negative ? '-1' : '1', $number->getIntegerRoundingMultiplier()); + self::assertSame($decimal, $number->isDecimal()); + self::assertSame($half, $number->isHalf()); + self::assertSame($currentEven, $number->isCurrentEven()); + self::assertSame($negative, $number->isNegative()); + self::assertSame($integerPart, $number->getIntegerPart()); + self::assertSame($fractionalPart, $number->getFractionalPart()); + self::assertSame($negative ? '-1' : '1', $number->getIntegerRoundingMultiplier()); } /** * @dataProvider invalidNumberExamples * @test */ - public function it_fails_parsing_invalid_numbers($number) + public function itFailsParsingInvalidNumbers(string $number): void { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); Number::fromString($number); } /** + * @psalm-param numeric-string $numberString + * @psalm-param numeric-string $expectedResult + * * @dataProvider base10Examples * @test */ - public function base10($numberString, $baseNumber, $expectedResult) + public function base10(string $numberString, int $baseNumber, string $expectedResult): void { $number = Number::fromString($numberString); - $this->assertSame($expectedResult, (string) $number->base10($baseNumber)); + self::assertSame($expectedResult, (string) $number->base10($baseNumber)); } /** + * @psalm-param int|numeric-string $number + * * @dataProvider numericExamples * @test */ - public function it_creates_a_number_from_a_numeric_value($number) + public function itCreatesANumberFromANumericValue(int|string $number): void { $number = Number::fromNumber($number); - $this->assertInstanceOf(Number::class, $number); + self::assertInstanceOf(Number::class, $number); + } + + /** @test */ + public function itCreatesANumberFromAFloatingPointValue(): void + { + self::assertEquals( + Number::fromString('123.456789'), + Number::fromFloat(123.456789) + ); } - public function numberExamples() + /** + * @psalm-return non-empty-list + * + * @psalm-suppress LessSpecificReturnStatement the {@see PHP_INT_MAX} operations below cannot be inferred to numeric-string + * @psalm-suppress MoreSpecificReturnType the {@see PHP_INT_MAX} operations below cannot be inferred to numeric-string + * @psalm-suppress InvalidOperand concatenation of {@see PHP_INT_MAX} is disallowed by type checker, but valid in this scenario + */ + public function numberExamples(): array { return [ ['0', false, false, true, false, '0', ''], @@ -85,35 +128,36 @@ public function numberExamples() [(string) PHP_INT_MAX, false, false, false, false, (string) PHP_INT_MAX, ''], [(string) -PHP_INT_MAX, false, false, false, true, (string) -PHP_INT_MAX, ''], [ - PHP_INT_MAX.PHP_INT_MAX.PHP_INT_MAX, + PHP_INT_MAX . PHP_INT_MAX . PHP_INT_MAX, false, false, false, false, - PHP_INT_MAX.PHP_INT_MAX.PHP_INT_MAX, + PHP_INT_MAX . PHP_INT_MAX . PHP_INT_MAX, '', ], [ - -PHP_INT_MAX.PHP_INT_MAX.PHP_INT_MAX, + -PHP_INT_MAX . PHP_INT_MAX . PHP_INT_MAX, false, false, false, true, - -PHP_INT_MAX.PHP_INT_MAX.PHP_INT_MAX, + -PHP_INT_MAX . PHP_INT_MAX . PHP_INT_MAX, '', ], [ - substr(PHP_INT_MAX, 0, strlen((string) PHP_INT_MAX) - 1).str_repeat('0', strlen((string) PHP_INT_MAX) - 1).PHP_INT_MAX, + substr((string) PHP_INT_MAX, 0, strlen((string) PHP_INT_MAX) - 1) . str_repeat('0', strlen((string) PHP_INT_MAX) - 1) . PHP_INT_MAX, false, false, false, false, - substr(PHP_INT_MAX, 0, strlen((string) PHP_INT_MAX) - 1).str_repeat('0', strlen((string) PHP_INT_MAX) - 1).PHP_INT_MAX, + substr((string) PHP_INT_MAX, 0, strlen((string) PHP_INT_MAX) - 1) . str_repeat('0', strlen((string) PHP_INT_MAX) - 1) . PHP_INT_MAX, '', ], ]; } + /** @psalm-return non-empty-list */ public function invalidNumberExamples() { return [ @@ -126,9 +170,19 @@ public function invalidNumberExamples() ['-123456789012345678.-13456'], ['+123456789'], ['+123456789012345678.+13456'], + ['123.456.789'], + ['123.456z'], + ['123z'], ]; } + /** + * @psalm-return non-empty-list + */ public function base10Examples() { return [ @@ -149,13 +203,12 @@ public function base10Examples() ]; } - public function numericExamples() + /** @psalm-return non-empty-list */ + public function numericExamples(): array { return [ [1], [-1], - [1.0], - [-1.0], ['1'], ['-1'], ['1.0'], diff --git a/tests/Parser/AggregateMoneyParserTest.php b/tests/Parser/AggregateMoneyParserTest.php index d53312655..1ecb00917 100644 --- a/tests/Parser/AggregateMoneyParserTest.php +++ b/tests/Parser/AggregateMoneyParserTest.php @@ -1,20 +1,83 @@ createMock(MoneyParser::class); + + $wrappedParser->method('parse') + ->with('€ 100') + ->willReturn($money); + + self::assertEquals( + $money, + (new AggregateMoneyParser([$wrappedParser])) + ->parse('€ 100') + ); + } + + /** @test */ + public function it_throws_an_exception_when_money_cannot_be_parsed(): void + { + $wrappedParser1 = $this->createMock(MoneyParser::class); + $wrappedParser2 = $this->createMock(MoneyParser::class); + + $wrappedParser1->expects(self::once()) + ->method('parse') + ->with('€ 100') + ->willThrowException(new ParserException()); + + $wrappedParser2->expects(self::once()) + ->method('parse') + ->with('€ 100') + ->willThrowException(new ParserException()); + + $parser = new AggregateMoneyParser([$wrappedParser1, $wrappedParser2]); + + $this->expectException(ParserException::class); + $parser->parse('€ 100'); + } + + /** @test */ + public function it_will_retrieve_parser_result_from_first_successful_parser(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Initialize an empty Money\\Parser\\AggregateMoneyParser is not possible'); + $money = new Money(10000, new Currency('EUR')); + $wrappedParser1 = $this->createMock(MoneyParser::class); + $wrappedParser2 = $this->createMock(MoneyParser::class); + $wrappedParser3 = $this->createMock(MoneyParser::class); + + $wrappedParser1->expects(self::once()) + ->method('parse') + ->with('€ 100') + ->willThrowException(new ParserException()); + + $wrappedParser2->expects(self::once()) + ->method('parse') + ->with('€ 100') + ->willReturn($money); + + $wrappedParser3->expects(self::never()) + ->method('parse'); - new AggregateMoneyParser([]); + self::assertEquals( + $money, + (new AggregateMoneyParser([$wrappedParser1, $wrappedParser2, $wrappedParser3])) + ->parse('€ 100') + ); } } diff --git a/tests/Parser/BitcoinMoneyParserTest.php b/tests/Parser/BitcoinMoneyParserTest.php index a01696c54..dbd8f7ffe 100644 --- a/tests/Parser/BitcoinMoneyParserTest.php +++ b/tests/Parser/BitcoinMoneyParserTest.php @@ -1,5 +1,7 @@ parse($string); - $this->assertInstanceOf(Money::class, $money); - $this->assertEquals($units, $money->getAmount()); - $this->assertEquals(BitcoinCurrencies::CODE, $money->getCurrency()->getCode()); + self::assertInstanceOf(Money::class, $money); + self::assertEquals($units, $money->getAmount()); + self::assertEquals(BitcoinCurrencies::CODE, $money->getCurrency()->getCode()); } /** * @test */ - public function force_currency_works() + public function forceCurrencyWorks(): void { $moneyParser = new BitcoinMoneyParser(2); $money = $moneyParser->parse("\xC9\x830.25", new Currency('ETH')); - $this->assertInstanceOf(Money::class, $money); - $this->assertEquals('25', $money->getAmount()); - $this->assertEquals('ETH', $money->getCurrency()->getCode()); + self::assertInstanceOf(Money::class, $money); + self::assertEquals('25', $money->getAmount()); + self::assertEquals('ETH', $money->getCurrency()->getCode()); } - public function bitcoinExamples() + /** + * @psalm-return non-empty-list + */ + public function bitcoinExamples(): array { return [ ["\xC9\x831000.00", 100000], diff --git a/tests/Parser/DecimalMoneyParserTest.php b/tests/Parser/DecimalMoneyParserTest.php index 4a4ff06f8..68be4e858 100644 --- a/tests/Parser/DecimalMoneyParserTest.php +++ b/tests/Parser/DecimalMoneyParserTest.php @@ -1,5 +1,7 @@ prophesize(Currencies::class); + $currencies = $this->createMock(Currencies::class); - $currencies->subunitFor(Argument::allOf( - Argument::type(Currency::class), - Argument::which('getCode', $currency) - ))->willReturn($subunit); + $currencies->method('subunitFor') + ->with(self::callback(static fn (Currency $givenCurrency): bool => $currency === $givenCurrency->getCode())) + ->willReturn($subunit); - $parser = new DecimalMoneyParser($currencies->reveal()); + $parser = new DecimalMoneyParser($currencies); - $this->assertEquals($result, $parser->parse($decimal, new Currency($currency))->getAmount()); + self::assertEquals($result, $parser->parse($decimal, new Currency($currency))->getAmount()); } /** + * @psalm-param non-empty-string $input + * * @dataProvider invalidMoneyExamples * @test */ - public function it_throws_an_exception_upon_invalid_inputs($input) + public function itThrowsAnExceptionUponInvalidInputs($input): void { - $currencies = $this->prophesize(Currencies::class); + $currencies = $this->createMock(Currencies::class); - $currencies->subunitFor(Argument::allOf( - Argument::type(Currency::class), - Argument::which('getCode', 'USD') - ))->willReturn(2); + $currencies->method('subunitFor') + ->with(self::callback(static fn (Currency $givenCurrency): bool => 'USD' === $givenCurrency->getCode())) + ->willReturn(2); - $parser = new DecimalMoneyParser($currencies->reveal()); + $parser = new DecimalMoneyParser($currencies); $this->expectException(ParserException::class); - $parser->parse($input, new Currency('USD'))->getAmount(); + $parser->parse($input, new Currency('USD')); } /** - * @group legacy - * @expectedDeprecation Passing a currency as string is deprecated since 3.1 and will be removed in 4.0. Please pass a Money\Currency instance instead. - * @test + * @psalm-return non-empty-list */ - public function it_accepts_only_a_currency_object() - { - $currencies = $this->prophesize(Currencies::class); - - $currencies->subunitFor(Argument::allOf( - Argument::type(Currency::class), - Argument::which('getCode', 'USD') - ))->willReturn(2); - - $parser = new DecimalMoneyParser($currencies->reveal()); - - $parser->parse('1.0', 'USD')->getAmount(); - } - - public function formattedMoneyExamples() + public function formattedMoneyExamples(): array { return [ ['1000.50', 'USD', 2, 100050], @@ -123,6 +118,7 @@ public function formattedMoneyExamples() ]; } + /** @psalm-return non-empty-list */ public static function invalidMoneyExamples() { return [ diff --git a/tests/Parser/IntlLocalizedDecimalParserTest.php b/tests/Parser/IntlLocalizedDecimalParserTest.php index 4ba733868..f57427a06 100644 --- a/tests/Parser/IntlLocalizedDecimalParserTest.php +++ b/tests/Parser/IntlLocalizedDecimalParserTest.php @@ -1,5 +1,7 @@ prophesize(Currencies::class); + $currencies = $this->createMock(Currencies::class); - $currencies->subunitFor(Argument::allOf( - Argument::type(Currency::class), - Argument::which('getCode', 'USD') - ))->willReturn(2); + $currencies->method('subunitFor') + ->with(self::callback(static fn (Currency $givenCurrency): bool => 'USD' === $givenCurrency->getCode())) + ->willReturn(2); $currencyCode = 'USD'; - $currency = new Currency($currencyCode); + $currency = new Currency($currencyCode); - $parser = new IntlLocalizedDecimalParser($formatter, $currencies->reveal()); - $this->assertEquals($units, $parser->parse($string, $currency)->getAmount()); + $parser = new IntlLocalizedDecimalParser($formatter, $currencies); + self::assertEquals($units, $parser->parse($string, $currency)->getAmount()); } /** * @test */ - public function it_cannot_convert_string_to_units() + public function itCannotConvertStringToUnits(): void { - $formatter = new \NumberFormatter('en_US', \NumberFormatter::DECIMAL); + $formatter = new NumberFormatter('en_US', NumberFormatter::DECIMAL); $currency = new Currency('USD'); - $parser = new IntlLocalizedDecimalParser($formatter, new ISOCurrencies()); + $parser = new IntlLocalizedDecimalParser($formatter, new ISOCurrencies()); $this->expectException(ParserException::class); $parser->parse('THIS_IS_NOT_CONVERTABLE_TO_UNIT', $currency); @@ -52,69 +57,67 @@ public function it_cannot_convert_string_to_units() /** * @test */ - public function it_works_with_all_kinds_of_locales() + public function itWorksWithAllKindsOfLocales(): void { - $formatter = new \NumberFormatter('en_CA', \NumberFormatter::DECIMAL); + $formatter = new NumberFormatter('en_CA', NumberFormatter::DECIMAL); $parser = new IntlLocalizedDecimalParser($formatter, new ISOCurrencies()); - $money = $parser->parse('1000.00', new Currency('CAD')); + $money = $parser->parse('1000.00', new Currency('CAD')); - $this->assertTrue(Money::CAD(100000)->equals($money)); + self::assertTrue(Money::CAD(100000)->equals($money)); } /** * @test */ - public function it_accepts_a_forced_currency() + public function itAcceptsAForcedCurrency(): void { - $formatter = new \NumberFormatter('en_US', \NumberFormatter::DECIMAL); + $formatter = new NumberFormatter('en_US', NumberFormatter::DECIMAL); $currency = new Currency('CAD'); - $parser = new IntlLocalizedDecimalParser($formatter, new ISOCurrencies()); - $money = $parser->parse('1000.00', $currency); + $parser = new IntlLocalizedDecimalParser($formatter, new ISOCurrencies()); + $money = $parser->parse('1000.00', $currency); - $this->assertSame('100000', $money->getAmount()); - $this->assertSame('CAD', $money->getCurrency()->getCode()); + self::assertSame('100000', $money->getAmount()); + self::assertSame('CAD', $money->getCurrency()->getCode()); } /** * @test */ - public function it_supports_fraction_digits() - { - $formatter = new \NumberFormatter('en_US', \NumberFormatter::DECIMAL); - $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, 3); - - $parser = new IntlLocalizedDecimalParser($formatter, new ISOCurrencies()); - $money = $parser->parse('1000.005', new Currency('USD')); - - $this->assertSame('100001', $money->getAmount()); - } - - public function it_does_not_support_invalid_decimal() + public function itSupportsFractionDigits(): void { - $formatter = new \NumberFormatter('en_US', \NumberFormatter::DECIMAL); - $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, 3); + $formatter = new NumberFormatter('en_US', NumberFormatter::DECIMAL); + $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, 3); $parser = new IntlLocalizedDecimalParser($formatter, new ISOCurrencies()); - $money = $parser->parse('1000,005', new Currency('USD')); + $money = $parser->parse('1000.005', new Currency('USD')); - $this->assertSame('100001', $money->getAmount()); + self::assertSame('100001', $money->getAmount()); } /** - * @group legacy - * @expectedDeprecation Passing a currency as string is deprecated since 3.1 and will be removed in 4.0. Please pass a Money\Currency instance instead. * @test */ - public function it_accepts_only_a_currency_object() + public function it_does_not_support_invalid_decimal(): void { - $formatter = new \NumberFormatter('en_CA', \NumberFormatter::DECIMAL); + $formatter = new NumberFormatter('en_US', NumberFormatter::DECIMAL); + $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, 3); $parser = new IntlLocalizedDecimalParser($formatter, new ISOCurrencies()); - $parser->parse('1000.00', 'EUR'); + + $this->expectException(ParserException::class); + + $parser->parse('1000,005', new Currency('USD')); } + /** + * @psalm-return non-empty-list + */ public function formattedMoneyExamples() { return [ diff --git a/tests/Parser/IntlMoneyParserTest.php b/tests/Parser/IntlMoneyParserTest.php index 981f63947..274caacf4 100644 --- a/tests/Parser/IntlMoneyParserTest.php +++ b/tests/Parser/IntlMoneyParserTest.php @@ -1,5 +1,7 @@ setPattern('¤#,##0.00;-¤#,##0.00'); - $currencies = $this->prophesize(Currencies::class); + $currencies = $this->createMock(Currencies::class); - $currencies->subunitFor(Argument::allOf( - Argument::type(Currency::class), - Argument::which('getCode', 'USD') - ))->willReturn(2); + $currencies->method('subunitFor') + ->with(self::callback(static fn (Currency $givenCurrency): bool => 'USD' === $givenCurrency->getCode())) + ->willReturn(2); $currencyCode = 'USD'; - $currency = new Currency($currencyCode); + $currency = new Currency($currencyCode); - $parser = new IntlMoneyParser($formatter, $currencies->reveal()); - $this->assertEquals($units, $parser->parse($string, $currency)->getAmount()); + $parser = new IntlMoneyParser($formatter, $currencies); + self::assertEquals($units, $parser->parse($string, $currency)->getAmount()); } /** * @test */ - public function it_cannot_convert_string_to_units() + public function itCannotConvertStringToUnits(): void { - $formatter = new \NumberFormatter('en_US', \NumberFormatter::CURRENCY); + $formatter = new NumberFormatter('en_US', NumberFormatter::CURRENCY); $formatter->setPattern('¤#,##0.00;-¤#,##0.00'); $currencyCode = 'USD'; - $currency = new Currency($currencyCode); - $parser = new IntlMoneyParser($formatter, new ISOCurrencies()); + $currency = new Currency($currencyCode); + $parser = new IntlMoneyParser($formatter, new ISOCurrencies()); $this->expectException(ParserException::class); $parser->parse('THIS_IS_NOT_CONVERTABLE_TO_UNIT', $currency); @@ -56,47 +59,47 @@ public function it_cannot_convert_string_to_units() /** * @test */ - public function it_works_with_all_kinds_of_locales() + public function itWorksWithAllKindsOfLocales(): void { - $formatter = new \NumberFormatter('en_CA', \NumberFormatter::CURRENCY); + $formatter = new NumberFormatter('en_CA', NumberFormatter::CURRENCY); $formatter->setPattern('¤#,##0.00;-¤#,##0.00'); $parser = new IntlMoneyParser($formatter, new ISOCurrencies()); - $money = $parser->parse('$1000.00'); + $money = $parser->parse('$1000.00'); - $this->assertTrue(Money::CAD(100000)->equals($money)); + self::assertTrue(Money::CAD(100000)->equals($money)); } /** * @test */ - public function it_accepts_a_forced_currency() + public function itAcceptsAForcedCurrency(): void { - $formatter = new \NumberFormatter('en_US', \NumberFormatter::CURRENCY); + $formatter = new NumberFormatter('en_US', NumberFormatter::CURRENCY); $formatter->setPattern('¤#,##0.00;-¤#,##0.00'); $currencyCode = 'CAD'; - $currency = new Currency($currencyCode); - $parser = new IntlMoneyParser($formatter, new ISOCurrencies()); - $money = $parser->parse('$1000.00', $currency); + $currency = new Currency($currencyCode); + $parser = new IntlMoneyParser($formatter, new ISOCurrencies()); + $money = $parser->parse('$1000.00', $currency); - $this->assertEquals('100000', $money->getAmount()); - $this->assertEquals('CAD', $money->getCurrency()->getCode()); + self::assertEquals('100000', $money->getAmount()); + self::assertEquals('CAD', $money->getCurrency()->getCode()); } /** * @test */ - public function it_supports_fraction_digits() + public function itSupportsFractionDigits(): void { - $formatter = new \NumberFormatter('en_US', \NumberFormatter::CURRENCY); + $formatter = new NumberFormatter('en_US', NumberFormatter::CURRENCY); $formatter->setPattern('¤#,##0.00;-¤#,##0.00'); - $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, 3); + $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, 3); $parser = new IntlMoneyParser($formatter, new ISOCurrencies()); - $money = $parser->parse('$1000.005'); + $money = $parser->parse('$1000.005'); - $this->assertEquals('100001', $money->getAmount()); + self::assertEquals('100001', $money->getAmount()); } /** @@ -105,33 +108,25 @@ public function it_supports_fraction_digits() * @group segmentation * @test */ - public function it_supports_fraction_digits_with_different_style_and_pattern() + public function itSupportsFractionDigitsWithDifferentStyleAndPattern(): void { - $formatter = new \NumberFormatter('en_US', \NumberFormatter::DECIMAL); + $formatter = new NumberFormatter('en_US', NumberFormatter::DECIMAL); $formatter->setPattern('¤#,##0.00;-¤#,##0.00'); - $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, 3); + $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, 3); $parser = new IntlMoneyParser($formatter, new ISOCurrencies()); - $money = $parser->parse('$1000.005'); + $money = $parser->parse('$1000.005'); - $this->assertEquals('100001', $money->getAmount()); + self::assertEquals('100001', $money->getAmount()); } /** - * @group legacy - * @expectedDeprecation Passing a currency as string is deprecated since 3.1 and will be removed in 4.0. Please pass a Money\Currency instance instead. - * @test + * @psalm-return non-empty-list */ - public function it_accepts_only_a_currency_object() - { - $formatter = new \NumberFormatter('en_CA', \NumberFormatter::CURRENCY); - $formatter->setPattern('¤#,##0.00;-¤#,##0.00'); - - $parser = new IntlMoneyParser($formatter, new ISOCurrencies()); - $parser->parse('$1000.00', 'EUR'); - } - - public function formattedMoneyExamples() + public function formattedMoneyExamples(): array { return [ ['$1000.50', 100050], diff --git a/tests/RoundExamples.php b/tests/RoundExamples.php index 4e5c4e9e8..834aee2bd 100644 --- a/tests/RoundExamples.php +++ b/tests/RoundExamples.php @@ -1,5 +1,7 @@ + */ + public function roundingExamples(): array { return [ - [2.6, Money::ROUND_HALF_EVEN, '3'], - [2.5, Money::ROUND_HALF_EVEN, '2'], - [3.5, Money::ROUND_HALF_EVEN, '4'], - [-2.6, Money::ROUND_HALF_EVEN, '-3'], - [-2.5, Money::ROUND_HALF_EVEN, '-2'], - [-3.5, Money::ROUND_HALF_EVEN, '-4'], - [2.1, Money::ROUND_HALF_ODD, '2'], - [2.5, Money::ROUND_HALF_ODD, '3'], - [3.5, Money::ROUND_HALF_ODD, '3'], - [-2.1, Money::ROUND_HALF_ODD, '-2'], - [-2.5, Money::ROUND_HALF_ODD, '-3'], - [-3.5, Money::ROUND_HALF_ODD, '-3'], - [2, Money::ROUND_HALF_EVEN, '2'], - [2, Money::ROUND_HALF_ODD, '2'], - [-2, Money::ROUND_HALF_ODD, '-2'], - [2.5, Money::ROUND_HALF_DOWN, '2'], - [2.6, Money::ROUND_HALF_DOWN, '3'], - [-2.5, Money::ROUND_HALF_DOWN, '-2'], - [-2.6, Money::ROUND_HALF_DOWN, '-3'], - [2.2, Money::ROUND_HALF_UP, '2'], - [2.5, Money::ROUND_HALF_UP, '3'], - [2, Money::ROUND_HALF_UP, '2'], - [-2.5, Money::ROUND_HALF_UP, '-3'], - [-2, Money::ROUND_HALF_UP, '-2'], - [2, Money::ROUND_HALF_DOWN, '2'], + ['2.6', Money::ROUND_HALF_EVEN, '3'], + ['2.5', Money::ROUND_HALF_EVEN, '2'], + ['3.5', Money::ROUND_HALF_EVEN, '4'], + ['-2.6', Money::ROUND_HALF_EVEN, '-3'], + ['-2.5', Money::ROUND_HALF_EVEN, '-2'], + ['-3.5', Money::ROUND_HALF_EVEN, '-4'], + ['2.1', Money::ROUND_HALF_ODD, '2'], + ['2.5', Money::ROUND_HALF_ODD, '3'], + ['3.5', Money::ROUND_HALF_ODD, '3'], + ['-2.1', Money::ROUND_HALF_ODD, '-2'], + ['-2.5', Money::ROUND_HALF_ODD, '-3'], + ['-3.5', Money::ROUND_HALF_ODD, '-3'], + ['2', Money::ROUND_HALF_EVEN, '2'], + ['2', Money::ROUND_HALF_ODD, '2'], + ['-2', Money::ROUND_HALF_ODD, '-2'], + ['2.5', Money::ROUND_HALF_DOWN, '2'], + ['2.6', Money::ROUND_HALF_DOWN, '3'], + ['-2.5', Money::ROUND_HALF_DOWN, '-2'], + ['-2.6', Money::ROUND_HALF_DOWN, '-3'], + ['2.2', Money::ROUND_HALF_UP, '2'], + ['2.5', Money::ROUND_HALF_UP, '3'], + ['2', Money::ROUND_HALF_UP, '2'], + ['-2.5', Money::ROUND_HALF_UP, '-3'], + ['-2', Money::ROUND_HALF_UP, '-2'], + ['2', Money::ROUND_HALF_DOWN, '2'], ['12.50', Money::ROUND_HALF_DOWN, '12'], ['-12.50', Money::ROUND_HALF_DOWN, '-12'], - [-1.5, Money::ROUND_HALF_UP, '-2'], - [-8328.578947368, Money::ROUND_HALF_UP, '-8329'], - [-8328.5, Money::ROUND_HALF_UP, '-8329'], - [-8328.5, Money::ROUND_HALF_DOWN, '-8328'], - [2.5, Money::ROUND_HALF_POSITIVE_INFINITY, '3'], - [2.6, Money::ROUND_HALF_POSITIVE_INFINITY, '3'], - [-2.5, Money::ROUND_HALF_POSITIVE_INFINITY, '-2'], - [-2.6, Money::ROUND_HALF_POSITIVE_INFINITY, '-3'], - [2, Money::ROUND_HALF_POSITIVE_INFINITY, '2'], + ['-1.5', Money::ROUND_HALF_UP, '-2'], + ['-8328.578947368', Money::ROUND_HALF_UP, '-8329'], + ['-8328.5', Money::ROUND_HALF_UP, '-8329'], + ['-8328.5', Money::ROUND_HALF_DOWN, '-8328'], + ['2.5', Money::ROUND_HALF_POSITIVE_INFINITY, '3'], + ['2.6', Money::ROUND_HALF_POSITIVE_INFINITY, '3'], + ['-2.5', Money::ROUND_HALF_POSITIVE_INFINITY, '-2'], + ['-2.6', Money::ROUND_HALF_POSITIVE_INFINITY, '-3'], + ['2', Money::ROUND_HALF_POSITIVE_INFINITY, '2'], ['12.50', Money::ROUND_HALF_POSITIVE_INFINITY, '13'], ['-12.50', Money::ROUND_HALF_POSITIVE_INFINITY, '-12'], - [-8328.5, Money::ROUND_HALF_POSITIVE_INFINITY, '-8328'], - [2.2, Money::ROUND_HALF_NEGATIVE_INFINITY, '2'], - [2.5, Money::ROUND_HALF_NEGATIVE_INFINITY, '2'], - [2, Money::ROUND_HALF_NEGATIVE_INFINITY, '2'], - [-2.5, Money::ROUND_HALF_NEGATIVE_INFINITY, '-3'], - [-2, Money::ROUND_HALF_NEGATIVE_INFINITY, '-2'], - [-1.5, Money::ROUND_HALF_NEGATIVE_INFINITY, '-2'], - [-8328.578947368, Money::ROUND_HALF_NEGATIVE_INFINITY, '-8329'], - [-8328.5, Money::ROUND_HALF_NEGATIVE_INFINITY, '-8329'], + ['-8328.5', Money::ROUND_HALF_POSITIVE_INFINITY, '-8328'], + ['2.2', Money::ROUND_HALF_NEGATIVE_INFINITY, '2'], + ['2.5', Money::ROUND_HALF_NEGATIVE_INFINITY, '2'], + ['2', Money::ROUND_HALF_NEGATIVE_INFINITY, '2'], + ['-2.5', Money::ROUND_HALF_NEGATIVE_INFINITY, '-3'], + ['-2', Money::ROUND_HALF_NEGATIVE_INFINITY, '-2'], + ['-1.5', Money::ROUND_HALF_NEGATIVE_INFINITY, '-2'], + ['-8328.578947368', Money::ROUND_HALF_NEGATIVE_INFINITY, '-8329'], + ['-8328.5', Money::ROUND_HALF_NEGATIVE_INFINITY, '-8329'], ]; } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 06588f838..fce9e877d 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,3 +1,8 @@ register(new \Money\PHPUnit\Comparator()); +declare(strict_types=1); + +use Money\PHPUnit\Comparator; +use SebastianBergmann\Comparator\Factory; + +Factory::getInstance()->register(new Comparator());