diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..d652892
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,15 @@
+*.php text eol=lf
+*.phpt text eol=lf
+/composer.lock export-ignore
+/.github/ export-ignore
+/docs export-ignore
+/tests export-ignore
+/.phive export-ignore
+/benchmark export-ignore
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/.php_cs export-ignore
+/phpstan.neon export-ignore
+/phpunit.xml export-ignore
+/psalm.xml export-ignore
+/infection.json export-ignore
\ No newline at end of file
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..bc3ab5c
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "composer"
+ directory: "/"
+ schedule:
+ interval: "daily"
diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml
new file mode 100644
index 0000000..5634a25
--- /dev/null
+++ b/.github/workflows/mutation.yml
@@ -0,0 +1,66 @@
+name: "Mutation Tests"
+
+on:
+ pull_request:
+ push:
+ branches:
+ - "1.x"
+ schedule:
+ - cron: '* 8 * * *'
+
+jobs:
+ mutation:
+ name: "Mutation Tests"
+
+ runs-on: ${{ matrix.operating-system }}
+
+ strategy:
+ matrix:
+ dependencies:
+ - "locked"
+ php-version:
+ - "7.4"
+ operating-system:
+ - "ubuntu-latest"
+
+ steps:
+ - name: "Checkout"
+ uses: "actions/checkout@v2"
+
+ - name: "Install PHP"
+ uses: "shivammathur/setup-php@v2"
+ with:
+ coverage: pcov
+ tools: phive, composer:v2
+ php-version: "${{ matrix.php-version }}"
+ ini-values: memory_limit=-1
+
+ - name: "Cache dependencies"
+ uses: "actions/cache@v2"
+ with:
+ path: |
+ ~/.composer/cache
+ vendor
+ key: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}"
+ restore-keys: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}"
+
+ - name: "Install lowest dependencies"
+ if: ${{ matrix.dependencies == 'lowest' }}
+ run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest"
+
+ - name: "Install highest dependencies"
+ if: ${{ matrix.dependencies == 'highest' }}
+ run: "composer update --no-interaction --no-progress --no-suggest"
+
+ - name: "Install locked dependencies"
+ if: ${{ matrix.dependencies == 'locked' }}
+ run: "composer install --no-interaction --no-progress --no-suggest"
+
+ - name: "Install tools"
+ run: "phive install --trust-gpg-keys E82B2FB314E9906E,C5095986493B4AA0,CF1A108D0E7AE720,8A03EA3B385DBAA1 --force-accept-unsigned"
+
+ - name: "Mutation Tests"
+ run: "composer test:mutation"
+ env:
+ INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }}
+ STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
\ No newline at end of file
diff --git a/.github/workflows/static-analyze.yml b/.github/workflows/static-analyze.yml
new file mode 100644
index 0000000..4f51fac
--- /dev/null
+++ b/.github/workflows/static-analyze.yml
@@ -0,0 +1,63 @@
+name: "Static Analyze"
+
+on:
+ pull_request:
+ push:
+ branches:
+ - "1.x"
+ schedule:
+ - cron: '* 8 * * *'
+
+jobs:
+ static-analyze:
+ name: "Static Analyze"
+
+ runs-on: ${{ matrix.operating-system }}
+
+ strategy:
+ matrix:
+ dependencies:
+ - "locked"
+ php-version:
+ - "7.4"
+ operating-system:
+ - "ubuntu-latest"
+
+ steps:
+ - name: "Checkout"
+ uses: "actions/checkout@v2"
+
+ - name: "Install PHP"
+ uses: "shivammathur/setup-php@v2"
+ with:
+ coverage: pcov
+ tools: phive, composer:v2
+ php-version: "${{ matrix.php-version }}"
+ ini-values: memory_limit=-1
+
+ - name: "Cache dependencies"
+ uses: "actions/cache@v2"
+ with:
+ path: |
+ ~/.composer/cache
+ vendor
+ key: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}"
+ restore-keys: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}"
+
+ - name: "Install lowest dependencies"
+ if: ${{ matrix.dependencies == 'lowest' }}
+ run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest"
+
+ - name: "Install highest dependencies"
+ if: ${{ matrix.dependencies == 'highest' }}
+ run: "composer update --no-interaction --no-progress --no-suggest"
+
+ - name: "Install locked dependencies"
+ if: ${{ matrix.dependencies == 'locked' }}
+ run: "composer install --no-interaction --no-progress --no-suggest"
+
+ - name: "Install tools"
+ run: "phive install --trust-gpg-keys E82B2FB314E9906E,C5095986493B4AA0,CF1A108D0E7AE720,8A03EA3B385DBAA1 --force-accept-unsigned"
+
+ - name: "Static Analyze"
+ run: "composer static:analyze"
\ No newline at end of file
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..08b8ae7
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,63 @@
+name: "Tests"
+
+on:
+ pull_request:
+ push:
+ branches:
+ - "1.x"
+ schedule:
+ - cron: '* 8 * * *'
+
+jobs:
+ tests:
+ name: "Tests"
+
+ runs-on: ${{ matrix.operating-system }}
+
+ strategy:
+ matrix:
+ dependencies:
+ - "locked"
+ - "lowest"
+ - "highest"
+ php-version:
+ - "7.4"
+ operating-system:
+ - "ubuntu-latest"
+ - "windows-latest"
+
+ steps:
+ - name: "Checkout"
+ uses: "actions/checkout@v2"
+
+ - name: "Install PHP"
+ uses: "shivammathur/setup-php@v2"
+ with:
+ coverage: pcov
+ tools: composer:v2
+ php-version: "${{ matrix.php-version }}"
+ ini-values: memory_limit=-1
+
+ - name: "Cache dependencies"
+ uses: "actions/cache@v2"
+ with:
+ path: |
+ ~/.composer/cache
+ vendor
+ key: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}"
+ restore-keys: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}"
+
+ - name: "Install lowest dependencies"
+ if: ${{ matrix.dependencies == 'lowest' }}
+ run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest"
+
+ - name: "Install highest dependencies"
+ if: ${{ matrix.dependencies == 'highest' }}
+ run: "composer update --no-interaction --no-progress --no-suggest"
+
+ - name: "Install locked dependencies"
+ if: ${{ matrix.dependencies == 'locked' }}
+ run: "composer install --no-interaction --no-progress --no-suggest"
+
+ - name: "Test"
+ run: "composer test"
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..06fdda6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+vendor
+tools
+*.cache
+var
\ No newline at end of file
diff --git a/.phive/phars.xml b/.phive/phars.xml
new file mode 100644
index 0000000..7d22a34
--- /dev/null
+++ b/.phive/phars.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/.php_cs b/.php_cs
new file mode 100644
index 0000000..d3e6651
--- /dev/null
+++ b/.php_cs
@@ -0,0 +1,224 @@
+files()
+ ->in([
+ __DIR__ . '/src',
+ __DIR__ . '/tests'
+ ])
+ ->notName('*.phpt');
+
+if (!\file_exists(__DIR__ . '/var')) {
+ \mkdir(__DIR__ . '/var');
+}
+
+/**
+ * This configuration was taken from https://github.com/sebastianbergmann/phpunit/blob/master/.php_cs.dist
+ * and slightly adjusted.
+ */
+return PhpCsFixer\Config::create()
+ ->setRiskyAllowed(true)
+ ->setCacheFile(__DIR__.'/var/.php_cs.cache')
+ ->setRules([
+ 'align_multiline_comment' => true,
+ 'array_indentation' => true,
+ 'array_syntax' => ['syntax' => 'short'],
+ 'blank_line_after_namespace' => true,
+ 'blank_line_before_statement' => [
+ 'statements' => [
+ 'break',
+ 'continue',
+ 'declare',
+ 'default',
+ 'die',
+ 'do',
+ 'exit',
+ 'for',
+ 'foreach',
+ 'goto',
+ 'if',
+ 'include',
+ 'include_once',
+ 'require',
+ 'require_once',
+ 'return',
+ 'switch',
+ 'throw',
+ 'try',
+ 'while',
+ ],
+ ],
+ 'braces' => true,
+ 'cast_spaces' => true,
+ 'class_attributes_separation' => ['elements' => ['const', 'method', 'property']],
+ 'combine_consecutive_issets' => true,
+ 'combine_consecutive_unsets' => true,
+ 'compact_nullable_typehint' => true,
+ 'concat_space' => ['spacing' => 'one'],
+ 'constant_case' => true,
+ 'declare_equal_normalize' => ['space' => 'none'],
+ 'declare_strict_types' => true,
+ 'dir_constant' => true,
+ 'elseif' => true,
+ 'encoding' => true,
+ 'explicit_indirect_variable' => true,
+ 'explicit_string_variable' => true,
+ 'full_opening_tag' => true,
+ 'fully_qualified_strict_types' => true,
+ 'function_typehint_space' => true,
+ 'function_declaration' => true,
+ 'global_namespace_import' => [
+ 'import_classes' => false,
+ 'import_constants' => false,
+ 'import_functions' => false,
+ ],
+ 'heredoc_to_nowdoc' => true,
+ 'increment_style' => [
+ 'style' => PhpCsFixer\Fixer\Operator\IncrementStyleFixer::STYLE_POST,
+ ],
+ 'indentation_type' => true,
+ 'is_null' => true,
+ 'line_ending' => true,
+ 'list_syntax' => ['syntax' => 'short'],
+ 'logical_operators' => true,
+ 'lowercase_keywords' => true,
+ 'lowercase_static_reference' => true,
+ 'magic_constant_casing' => true,
+ 'magic_method_casing' => true,
+ 'method_argument_space' => ['ensure_fully_multiline' => true],
+ 'modernize_types_casting' => false,
+ 'multiline_comment_opening_closing' => true,
+ 'multiline_whitespace_before_semicolons' => true,
+ 'native_constant_invocation' => false,
+ 'native_function_casing' => false,
+ 'native_function_invocation' => true,
+ 'native_function_type_declaration_casing' => true,
+ 'new_with_braces' => false,
+ 'no_alias_functions' => true,
+ 'no_alternative_syntax' => true,
+ 'no_blank_lines_after_class_opening' => true,
+ 'no_blank_lines_after_phpdoc' => true,
+ 'no_blank_lines_before_namespace' => false,
+ 'no_closing_tag' => true,
+ 'no_empty_comment' => true,
+ 'no_empty_phpdoc' => true,
+ 'no_empty_statement' => true,
+ 'no_extra_blank_lines' => true,
+ 'no_homoglyph_names' => true,
+ 'no_leading_import_slash' => true,
+ 'no_leading_namespace_whitespace' => true,
+ 'no_mixed_echo_print' => ['use' => 'print'],
+ 'no_multiline_whitespace_around_double_arrow' => true,
+ 'no_null_property_initialization' => true,
+ 'no_php4_constructor' => true,
+ 'no_short_bool_cast' => true,
+ 'no_short_echo_tag' => true,
+ 'no_singleline_whitespace_before_semicolons' => true,
+ 'no_spaces_after_function_name' => true,
+ 'no_spaces_around_offset' => true,
+ 'no_spaces_inside_parenthesis' => true,
+ 'no_superfluous_elseif' => true,
+ 'no_superfluous_phpdoc_tags' => false,
+ 'no_trailing_comma_in_list_call' => true,
+ 'no_trailing_comma_in_singleline_array' => true,
+ 'no_trailing_whitespace' => true,
+ 'no_trailing_whitespace_in_comment' => true,
+ 'no_unneeded_control_parentheses' => true,
+ 'no_unneeded_curly_braces' => true,
+ 'no_unneeded_final_method' => true,
+ 'no_unreachable_default_argument_value' => true,
+ 'no_unset_on_property' => true,
+ 'no_unused_imports' => true,
+ 'no_useless_else' => true,
+ 'no_useless_return' => true,
+ 'no_whitespace_before_comma_in_array' => true,
+ 'no_whitespace_in_blank_line' => true,
+ 'non_printable_character' => true,
+ 'normalize_index_brace' => true,
+ 'object_operator_without_whitespace' => true,
+ 'ordered_class_elements' => [
+ 'order' => [
+ 'use_trait',
+ 'constant_public',
+ 'constant_protected',
+ 'constant_private',
+ 'property_public_static',
+ 'property_protected_static',
+ 'property_private_static',
+ 'property_public',
+ 'property_protected',
+ 'property_private',
+ 'construct',
+ 'method_public_static',
+ 'destruct',
+ 'magic',
+ 'phpunit',
+ 'method_public',
+ 'method_protected',
+ 'method_private',
+ 'method_protected_static',
+ 'method_private_static',
+ ],
+ ],
+ 'ordered_imports' => [
+ 'imports_order' => [
+ PhpCsFixer\Fixer\Import\OrderedImportsFixer::IMPORT_TYPE_CONST,
+ PhpCsFixer\Fixer\Import\OrderedImportsFixer::IMPORT_TYPE_FUNCTION,
+ PhpCsFixer\Fixer\Import\OrderedImportsFixer::IMPORT_TYPE_CLASS,
+ ]
+ ],
+ 'ordered_interfaces' => [
+ 'direction' => 'ascend',
+ 'order' => 'alpha',
+ ],
+ 'phpdoc_add_missing_param_annotation' => false,
+ 'phpdoc_align' => ['align' => 'left'],
+ 'phpdoc_annotation_without_dot' => true,
+ 'phpdoc_indent' => true,
+ 'phpdoc_no_access' => true,
+ 'phpdoc_no_empty_return' => true,
+ 'phpdoc_no_package' => true,
+ 'phpdoc_order' => true,
+ 'phpdoc_return_self_reference' => true,
+ 'phpdoc_scalar' => true,
+ 'phpdoc_separation' => true,
+ 'phpdoc_single_line_var_spacing' => true,
+ 'phpdoc_summary' => true,
+ 'phpdoc_to_comment' => false,
+ 'phpdoc_trim' => true,
+ 'phpdoc_trim_consecutive_blank_line_separation' => true,
+ 'phpdoc_types' => ['groups' => ['simple', 'meta']],
+ 'phpdoc_types_order' => true,
+ 'phpdoc_var_without_name' => true,
+ 'pow_to_exponentiation' => true,
+ 'protected_to_private' => true,
+ 'return_assignment' => true,
+ 'return_type_declaration' => ['space_before' => 'one'],
+ 'self_accessor' => true,
+ 'self_static_accessor' => true,
+ 'semicolon_after_instruction' => true,
+ 'set_type_to_cast' => true,
+ 'short_scalar_cast' => true,
+ 'simple_to_complex_string_variable' => true,
+ 'simplified_null_return' => false,
+ 'single_blank_line_at_eof' => true,
+ 'single_import_per_statement' => true,
+ 'single_line_after_imports' => true,
+ 'single_quote' => true,
+ 'standardize_not_equals' => true,
+ 'strict_param' => true,
+ 'ternary_to_null_coalescing' => true,
+ 'trailing_comma_in_multiline_array' => true,
+ 'trim_array_spaces' => true,
+ 'unary_operator_spaces' => true,
+ 'visibility_required' => [
+ 'elements' => [
+ 'const',
+ 'method',
+ 'property',
+ ],
+ ],
+ 'void_return' => true,
+ 'whitespace_after_comma_in_array' => true,
+ ])
+ ->setFinder($finder);
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e69de29
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..5eca452
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,52 @@
+{
+ "name": "aeon-php/rate-limiter",
+ "type": "library",
+ "description": "Aeon rate limiter (throttling) library",
+ "keywords": ["ratelimiter", "throttling", "rate", "limiter"],
+ "prefer-stable": true,
+ "require": {
+ "php": ">=7.4.2",
+ "ext-mbstring": "*",
+ "aeon-php/sleep": ">=0.5.0",
+ "davedevelopment/stiphle": "^0.9.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.4"
+ },
+ "config": {
+ "optimize-autoloader": true,
+ "sort-packages": true
+ },
+ "license": "MIT",
+ "autoload": {
+ "psr-4": {
+ "Aeon\\": [
+ "src/Aeon"
+ ]
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Aeon\\RateLimiter\\Tests\\": "tests/Aeon/RateLimiter/Tests/"
+ }
+ },
+ "scripts": {
+ "build": [
+ "@static:analyze",
+ "@test",
+ "@test:mutation"
+ ],
+ "test": [
+ "phpunit"
+ ],
+ "test:mutation": [
+ "tools/infection -j2"
+ ],
+ "static:analyze": [
+ "tools/psalm --output-format=compact",
+ "tools/phpstan analyze -c phpstan.neon",
+ "tools/php-cs-fixer fix --dry-run"
+ ],
+ "cs:php:fix": "tools/php-cs-fixer fix"
+ }
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..8561f46
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,2281 @@
+{
+ "_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": "f2779b671e7f8ca492a81f60abc2b471",
+ "packages": [
+ {
+ "name": "aeon-php/calendar",
+ "version": "0.11.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/aeon-php/calendar.git",
+ "reference": "c93e225a30b42522a1b3b4b867eaf168886ba4c5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/aeon-php/calendar/zipball/c93e225a30b42522a1b3b4b867eaf168886ba4c5",
+ "reference": "c93e225a30b42522a1b3b4b867eaf168886ba4c5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4.2"
+ },
+ "require-dev": {
+ "ext-bcmath": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-bcmath": "Compare time units with high precision"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Aeon\\": [
+ "src/Aeon"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHP type safe, immutable calendar library",
+ "keywords": [
+ "calendar",
+ "date",
+ "datetime",
+ "immutable",
+ "time"
+ ],
+ "support": {
+ "issues": "https://github.com/aeon-php/calendar/issues",
+ "source": "https://github.com/aeon-php/calendar/tree/0.11.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/norberttech",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-23T20:30:34+00:00"
+ },
+ {
+ "name": "aeon-php/sleep",
+ "version": "0.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/aeon-php/sleep.git",
+ "reference": "29a1c3898a1e2e8510aeb8b998c4b0260758db88"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/aeon-php/sleep/zipball/29a1c3898a1e2e8510aeb8b998c4b0260758db88",
+ "reference": "29a1c3898a1e2e8510aeb8b998c4b0260758db88",
+ "shasum": ""
+ },
+ "require": {
+ "aeon-php/calendar": ">=0.4.0",
+ "php": ">=7.4.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/Aeon/Calendar/System/sleep.php"
+ ],
+ "psr-4": {
+ "Aeon\\": [
+ "src/Aeon"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Improved sleep function that uses simple TimeUnit and decides between \\sleep and \\usleep",
+ "keywords": [
+ "calendar",
+ "holidays",
+ "immutable",
+ "sleep"
+ ],
+ "support": {
+ "issues": "https://github.com/aeon-php/sleep/issues",
+ "source": "https://github.com/aeon-php/sleep/tree/1.x"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/norberttech",
+ "type": "github"
+ }
+ ],
+ "time": "2020-08-18T19:37:58+00:00"
+ },
+ {
+ "name": "davedevelopment/stiphle",
+ "version": "0.9.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/davedevelopment/stiphle.git",
+ "reference": "76151e6474741adee258c1a4860a0460e319563b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/davedevelopment/stiphle/zipball/76151e6474741adee258c1a4860a0460e319563b",
+ "reference": "76151e6474741adee258c1a4860a0460e319563b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.5",
+ "predis/predis": "^1.1"
+ },
+ "suggest": {
+ "doctrine/cache": "~1.0",
+ "predis/predis": "~1.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "Stiphle": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Dave Marshall",
+ "email": "dave.marshall@atstsolutions.co.uk",
+ "homepage": "http://davedevelopment.co.uk"
+ }
+ ],
+ "description": "Simple rate limiting/throttling for php",
+ "homepage": "http://github.com/davedevelopment/stiphle",
+ "keywords": [
+ "rate limit",
+ "rate limiting",
+ "throttle",
+ "throttling"
+ ],
+ "support": {
+ "issues": "https://github.com/davedevelopment/stiphle/issues",
+ "source": "https://github.com/davedevelopment/stiphle/tree/0.9.2"
+ },
+ "time": "2017-08-16T07:58:18+00:00"
+ }
+ ],
+ "packages-dev": [
+ {
+ "name": "doctrine/instantiator",
+ "version": "1.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/instantiator.git",
+ "reference": "f350df0268e904597e3bd9c4685c53e0e333feea"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea",
+ "reference": "f350df0268e904597e3bd9c4685c53e0e333feea",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^6.0",
+ "ext-pdo": "*",
+ "ext-phar": "*",
+ "phpbench/phpbench": "^0.13",
+ "phpstan/phpstan-phpunit": "^0.11",
+ "phpstan/phpstan-shim": "^0.11",
+ "phpunit/phpunit": "^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.2.x-dev"
+ }
+ },
+ "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": "http://ocramius.github.com/"
+ }
+ ],
+ "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.3.x"
+ },
+ "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-05-29T17:27:14+00:00"
+ },
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.10.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5",
+ "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5",
+ "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.x"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-06-29T13:22:24+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v4.10.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "658f1be311a230e0907f5dfe0213742aff0596de"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/658f1be311a230e0907f5dfe0213742aff0596de",
+ "reference": "658f1be311a230e0907f5dfe0213742aff0596de",
+ "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.2"
+ },
+ "time": "2020-09-26T10:30:38+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.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git",
+ "reference": "c6bb6825def89e0a32220f88337f8ceaf1975fa0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/c6bb6825def89e0a32220f88337f8ceaf1975fa0",
+ "reference": "c6bb6825def89e0a32220f88337f8ceaf1975fa0",
+ "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/master"
+ },
+ "time": "2020-06-27T14:39:04+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/prophecy",
+ "version": "1.12.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpspec/prophecy.git",
+ "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/8ce87516be71aae9b956f81906aaf0338e0d8a2d",
+ "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d",
+ "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 <9.3"
+ },
+ "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.12.1"
+ },
+ "time": "2020-09-29T09:10:42+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "9.2.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "6b20e2055f7c29b56cb3870b3de7cc463d7add41"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6b20e2055f7c29b56cb3870b3de7cc463d7add41",
+ "reference": "6b20e2055f7c29b56cb3870b3de7cc463d7add41",
+ "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",
+ "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.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-30T10:46:41+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.4.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "3866b2eeeed21b1b099c4bc0b7a1690ac6fd5baa"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3866b2eeeed21b1b099c4bc0b7a1690ac6fd5baa",
+ "reference": "3866b2eeeed21b1b099c4bc0b7a1690ac6fd5baa",
+ "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",
+ "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.4-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.4.2"
+ },
+ "funding": [
+ {
+ "url": "https://phpunit.de/donate.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-19T09:23:29+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.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "acf76492a65401babcf5283296fa510782783a7a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/acf76492a65401babcf5283296fa510782783a7a",
+ "reference": "acf76492a65401babcf5283296fa510782783a7a",
+ "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.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T17:03:56+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": "symfony/polyfill-ctype",
+ "version": "v1.20.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41",
+ "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.20-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.20.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-10-23T14:02:19+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": "webmozart/assert",
+ "version": "1.9.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webmozart/assert.git",
+ "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
+ "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.3 || ^7.0 || ^8.0",
+ "symfony/polyfill-ctype": "^1.8"
+ },
+ "conflict": {
+ "phpstan/phpstan": "<0.12.20",
+ "vimeo/psalm": "<3.9.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.36 || ^7.5.13"
+ },
+ "type": "library",
+ "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/webmozart/assert/issues",
+ "source": "https://github.com/webmozart/assert/tree/master"
+ },
+ "time": "2020-07-08T17:02:28+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": true,
+ "prefer-lowest": false,
+ "platform": {
+ "php": ">=7.4.2",
+ "ext-mbstring": "*"
+ },
+ "platform-dev": [],
+ "plugin-api-version": "2.0.0"
+}
diff --git a/infection.json b/infection.json
new file mode 100644
index 0000000..c0723ef
--- /dev/null
+++ b/infection.json
@@ -0,0 +1,31 @@
+{
+ "timeout": 1000,
+ "source": {
+ "directories": [
+ "src\/Aeon"
+ ],
+ "excludes": [
+ "/.*\\.phpt/"
+ ]
+ },
+ "logs": {
+ "text": "./var/infection/infection.log",
+ "summary": "./var/infection/infection_summary.log",
+ "debug": "./var/infection/infection_summary.log",
+ "badge": {
+ "branch": "1.x"
+ }
+ },
+ "mutators": {
+ "@default": true,
+ "UnwrapArrayValues": {
+ "ignore": [
+ "Aeon\\RateLimiter\\Storage\\MemoryStorage::hits"
+ ]
+ }
+ },
+ "testFramework": "phpunit",
+ "bootstrap": "./vendor/autoload.php",
+ "minMsi": 86,
+ "minCoveredMsi": 100
+}
\ No newline at end of file
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..d05e1a4
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,6 @@
+parameters:
+ level: max
+ paths:
+ - src
+
+ tmpDir: var/phpstan/cache
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..35d9b57
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+ src
+
+
+
+
+
+ tests/Aeon/RateLimiter/Tests/Functional
+
+
+ tests/Aeon/RateLimiter/Tests/Unit
+
+
+
\ No newline at end of file
diff --git a/psalm.xml b/psalm.xml
new file mode 100644
index 0000000..33f2f7e
--- /dev/null
+++ b/psalm.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Aeon/RateLimiter/Algorithm.php b/src/Aeon/RateLimiter/Algorithm.php
new file mode 100644
index 0000000..19d0d7b
--- /dev/null
+++ b/src/Aeon/RateLimiter/Algorithm.php
@@ -0,0 +1,21 @@
+calendar = $calendar;
+ $this->bucketSize = $bucketSize;
+ $this->leakSize = $leakSize;
+ $this->leakTime = $leakTime;
+ }
+
+ /**
+ * @psalm-suppress PossiblyNullReference
+ */
+ public function hit(string $id, Storage $storage) : void
+ {
+ $hits = $storage->all($id);
+
+ if ($hits->count() >= $this->bucketSize) {
+ /** @phpstan-ignore-next-line */
+ throw new RateLimitException($id, $hits->oldest()->ttlLeft($this->calendar));
+ }
+
+ $ttl = TimeUnit::seconds((int) (\floor($hits->count() / $this->leakSize) * $this->leakTime->inSeconds() + $this->leakTime->inSeconds()));
+
+ $storage->addHit($id, $ttl);
+ }
+
+ /**
+ * @psalm-suppress PossiblyNullReference
+ */
+ public function nextHit(string $id, Storage $storage) : TimeUnit
+ {
+ $hits = $storage->all($id);
+
+ if ($hits->count() >= $this->bucketSize) {
+ /** @phpstan-ignore-next-line */
+ return $hits->oldest()->ttlLeft($this->calendar);
+ }
+
+ return TimeUnit::seconds(0);
+ }
+}
diff --git a/src/Aeon/RateLimiter/Algorithm/SlidingWindowAlgorithm.php b/src/Aeon/RateLimiter/Algorithm/SlidingWindowAlgorithm.php
new file mode 100644
index 0000000..a8bff32
--- /dev/null
+++ b/src/Aeon/RateLimiter/Algorithm/SlidingWindowAlgorithm.php
@@ -0,0 +1,57 @@
+calendar = $calendar;
+ $this->limit = $limit;
+ $this->timeWindow = $timeWindow;
+ }
+
+ /**
+ * @psalm-suppress PossiblyNullReference
+ */
+ public function hit(string $id, Storage $storage) : void
+ {
+ $hits = $storage->all($id);
+
+ if ($hits->count() >= $this->limit) {
+ /** @phpstan-ignore-next-line */
+ throw new RateLimitException($id, $hits->oldest()->ttlLeft($this->calendar));
+ }
+
+ $storage->addHit($id, $this->timeWindow);
+ }
+
+ /**
+ * @psalm-suppress PossiblyNullReference
+ */
+ public function nextHit(string $id, Storage $storage) : TimeUnit
+ {
+ $hits = $storage->all($id);
+
+ if ($hits->count() >= $this->limit) {
+ /** @phpstan-ignore-next-line */
+ return $hits->oldest()->ttlLeft($this->calendar);
+ }
+
+ return TimeUnit::seconds(0);
+ }
+}
diff --git a/src/Aeon/RateLimiter/Exception/RateLimitException.php b/src/Aeon/RateLimiter/Exception/RateLimitException.php
new file mode 100644
index 0000000..b56de5c
--- /dev/null
+++ b/src/Aeon/RateLimiter/Exception/RateLimitException.php
@@ -0,0 +1,32 @@
+inSecondsPrecise() . ' seconds', 0, $previous);
+
+ $this->id = $id;
+ $this->cooldown = $cooldown;
+ }
+
+ public function id() : string
+ {
+ return $this->id;
+ }
+
+ public function cooldown() : TimeUnit
+ {
+ return $this->cooldown;
+ }
+}
diff --git a/src/Aeon/RateLimiter/Exception/RuntimeException.php b/src/Aeon/RateLimiter/Exception/RuntimeException.php
new file mode 100644
index 0000000..d3dd685
--- /dev/null
+++ b/src/Aeon/RateLimiter/Exception/RuntimeException.php
@@ -0,0 +1,11 @@
+id = $id;
+ $this->dateTime = $dateTime;
+ $this->ttl = $ttl;
+ }
+
+ public function expired(Calendar $calendar) : bool
+ {
+ return !$this->dateTime->add($this->ttl)->isAfterOrEqual($calendar->now());
+ }
+
+ public function isOlderThan(self $hit) : bool
+ {
+ return $this->dateTime->isBefore($hit->dateTime);
+ }
+
+ public function ttlLeft(Calendar $calendar) : TimeUnit
+ {
+ $ttlLeft = $calendar->now()->until($this->dateTime->add($this->ttl))->distance();
+
+ return $ttlLeft->isPositive() ? $ttlLeft : TimeUnit::seconds(0);
+ }
+}
diff --git a/src/Aeon/RateLimiter/Hits.php b/src/Aeon/RateLimiter/Hits.php
new file mode 100644
index 0000000..3cd3c33
--- /dev/null
+++ b/src/Aeon/RateLimiter/Hits.php
@@ -0,0 +1,45 @@
+
+ */
+ private array $hits;
+
+ public function __construct(Hit ...$hits)
+ {
+ $this->hits = $hits;
+ }
+
+ public function count() : int
+ {
+ return \count($this->hits);
+ }
+
+ public function oldest() : ?Hit
+ {
+ $oldest = null;
+
+ foreach ($this->hits as $hit) {
+ if ($oldest === null) {
+ $oldest = $hit;
+
+ continue;
+ }
+
+ if ($hit->isOlderThan($oldest)) {
+ $oldest = $hit;
+ }
+ }
+
+ return $oldest;
+ }
+}
diff --git a/src/Aeon/RateLimiter/RateLimiter.php b/src/Aeon/RateLimiter/RateLimiter.php
new file mode 100644
index 0000000..0021556
--- /dev/null
+++ b/src/Aeon/RateLimiter/RateLimiter.php
@@ -0,0 +1,45 @@
+algorithm = $algorithm;
+ $this->storage = $storage;
+ }
+
+ /**
+ * @throws RateLimitException
+ */
+ public function hit(string $id) : void
+ {
+ $this->algorithm->hit($id, $this->storage);
+ }
+
+ public function estimate(string $id) : TimeUnit
+ {
+ return $this->algorithm->nextHit($id, $this->storage);
+ }
+
+ public function throttle(string $id, Process $process) : void
+ {
+ try {
+ $this->algorithm->hit($id, $this->storage);
+ } catch (RateLimitException $rateLimitException) {
+ $process->sleep($rateLimitException->cooldown());
+ $this->algorithm->hit($id, $this->storage);
+ }
+ }
+}
diff --git a/src/Aeon/RateLimiter/Storage.php b/src/Aeon/RateLimiter/Storage.php
new file mode 100644
index 0000000..6fae140
--- /dev/null
+++ b/src/Aeon/RateLimiter/Storage.php
@@ -0,0 +1,14 @@
+>
+ */
+ private array $hits;
+
+ public function __construct(Calendar $calendar)
+ {
+ $this->calendar = $calendar;
+ $this->hits = [];
+ }
+
+ public function addHit(string $id, TimeUnit $ttl) : void
+ {
+ if (!$this->hasFor($id)) {
+ $this->hits[$this->normalize($id)] = [];
+ }
+
+ $this->hits[$this->normalize($id)][] = new Hit($id, $this->calendar->now(), $ttl);
+ }
+
+ public function all(string $id) : Hits
+ {
+ return new Hits(...$this->hits($id));
+ }
+
+ public function count(string $id) : int
+ {
+ return \count($this->hits($id));
+ }
+
+ /**
+ * @return array
+ */
+ private function hits(string $id) : array
+ {
+ if (!$this->hasFor($id)) {
+ return [];
+ }
+
+ $hits = [];
+ /** @var Hit $hit */
+ foreach ($this->hits[$this->normalize($id)] as $i => $hit) {
+ if (!$hit->expired($this->calendar)) {
+ $hits[] = $hit;
+ } else {
+ unset($this->hits[$this->normalize($id)][$i]);
+ }
+ }
+
+ $this->hits[$this->normalize($id)] = \array_values($this->hits[$this->normalize($id)]);
+
+ return $hits;
+ }
+
+ private function hasFor(string $id) : bool
+ {
+ if (!\array_key_exists($this->normalize($id), $this->hits)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private function normalize(string $id) : string
+ {
+ return \mb_strtolower($id);
+ }
+}
diff --git a/tests/Aeon/RateLimiter/Tests/Unit/Algorithm/LeakyBucketAlgorithmTest.php b/tests/Aeon/RateLimiter/Tests/Unit/Algorithm/LeakyBucketAlgorithmTest.php
new file mode 100644
index 0000000..adaf76c
--- /dev/null
+++ b/tests/Aeon/RateLimiter/Tests/Unit/Algorithm/LeakyBucketAlgorithmTest.php
@@ -0,0 +1,60 @@
+setNow(DateTime::fromString('2020-01-01 00:00:00 UTC'));
+
+ $algorithm = new LeakyBucketAlgorithm($calendar, $bucketSize = 5, $leakSize = 2, TimeUnit::seconds(10));
+
+ $algorithm->hit('id', $storage = new MemoryStorage($calendar));
+ $algorithm->hit('id', $storage);
+ $algorithm->hit('id', $storage);
+ $algorithm->hit('id', $storage);
+ $algorithm->hit('id', $storage);
+
+ $this->assertSame(10, $algorithm->nextHit('id', $storage)->inSeconds());
+
+ $calendar->setNow($calendar->now()->add(TimeUnit::seconds(10)->add(TimeUnit::millisecond())));
+
+ $this->assertSame(0, $algorithm->nextHit('id', $storage)->inSeconds());
+
+ $algorithm->hit('id', $storage);
+ $algorithm->hit('id', $storage);
+
+ $this->assertSame('9.999000', $algorithm->nextHit('id', $storage)->inSecondsPrecise());
+ }
+
+ public function test_leaky_bucket_algorithm_with_too_many_hits() : void
+ {
+ $calendar = new GregorianCalendarStub(TimeZone::UTC());
+ $calendar->setNow(DateTime::fromString('2020-01-01 00:00:00 UTC'));
+
+ $algorithm = new LeakyBucketAlgorithm($calendar, $bucketSize = 5, $leakSize = 2, TimeUnit::seconds(10));
+
+ $this->expectException(RateLimitException::class);
+ $this->expectExceptionMessage('Execution "id" was limited for the next 10.000000 seconds');
+
+ $algorithm->hit('id', $storage = new MemoryStorage($calendar));
+ $algorithm->hit('id', $storage);
+ $algorithm->hit('id', $storage);
+ $algorithm->hit('id', $storage);
+ $algorithm->hit('id', $storage);
+ $algorithm->hit('id', $storage);
+ }
+}
diff --git a/tests/Aeon/RateLimiter/Tests/Unit/Algorithm/SlidingWindowAlgorithmTest.php b/tests/Aeon/RateLimiter/Tests/Unit/Algorithm/SlidingWindowAlgorithmTest.php
new file mode 100644
index 0000000..2a19fba
--- /dev/null
+++ b/tests/Aeon/RateLimiter/Tests/Unit/Algorithm/SlidingWindowAlgorithmTest.php
@@ -0,0 +1,87 @@
+hit('hit_id', $memoryStorage = new MemoryStorage($calendar));
+
+ $this->assertSame($algorithm->nextHit('hit_id', $memoryStorage)->inSeconds(), 0);
+ }
+
+ public function test_without_available_hits() : void
+ {
+ $algorithm = new SlidingWindowAlgorithm($calendar = new GregorianCalendarStub(TimeZone::UTC()), 1, TimeUnit::minute());
+ $algorithm->hit('hit_id', $memoryStorage = new MemoryStorage($calendar));
+
+ $this->assertSame($algorithm->nextHit('hit_id', $memoryStorage)->inSeconds(), 59);
+ }
+
+ public function test_hit_without_available_hits() : void
+ {
+ $algorithm = new SlidingWindowAlgorithm($calendar = new GregorianCalendarStub(TimeZone::UTC()), 1, TimeUnit::minute());
+ $calendar->setNow(DateTime::fromString('2020-01-01 00:00:00 UTC'));
+
+ $algorithm->hit('hit_id', $memoryStorage = new MemoryStorage($calendar));
+
+ $this->expectException(RateLimitException::class);
+ $this->expectExceptionMessage('Execution "hit_id" was limited for the next 60.000000 seconds');
+ $this->expectExceptionCode(0);
+
+ $algorithm->hit('hit_id', $memoryStorage);
+ }
+
+ public function test_hit_without_available_hits_exception() : void
+ {
+ $algorithm = new SlidingWindowAlgorithm($calendar = new GregorianCalendarStub(TimeZone::UTC()), 1, TimeUnit::minute());
+ $calendar->setNow(DateTime::fromString('2020-01-01 00:00:00 UTC'));
+
+ $algorithm->hit('hit_id', $memoryStorage = new MemoryStorage($calendar));
+
+ try {
+ $algorithm->hit('hit_id', $memoryStorage);
+ } catch (RateLimitException $e) {
+ $this->assertSame('hit_id', $e->id());
+ $this->assertEquals(TimeUnit::minute(), $e->cooldown());
+ }
+ }
+
+ public function test_resetting_hits() : void
+ {
+ $algorithm = new SlidingWindowAlgorithm($calendar = new GregorianCalendarStub(TimeZone::UTC()), 1, TimeUnit::minute());
+
+ $calendar->setNow(DateTime::fromString('2020-01-01 00:00:00 UTC'));
+
+ $algorithm->hit('hit_id', $memoryStorage = new MemoryStorage($calendar));
+
+ $this->assertSame($algorithm->nextHit('hit_id', $memoryStorage)->inSeconds(), 60);
+
+ $calendar->setNow($calendar->now()->add(TimeUnit::seconds(61)));
+
+ $this->assertSame($algorithm->nextHit('hit_id', $memoryStorage)->inSeconds(), 0);
+
+ $algorithm->hit('hit_id', $memoryStorage);
+
+ $this->assertSame($algorithm->nextHit('hit_id', $memoryStorage)->inSeconds(), 60);
+
+ $calendar->setNow($calendar->now()->add(TimeUnit::seconds(61)));
+
+ $this->assertSame($algorithm->nextHit('hit_id', $memoryStorage)->inSeconds(), 0);
+
+ $algorithm->hit('hit_id', $memoryStorage);
+ }
+}
diff --git a/tests/Aeon/RateLimiter/Tests/Unit/HitTest.php b/tests/Aeon/RateLimiter/Tests/Unit/HitTest.php
new file mode 100644
index 0000000..7909a08
--- /dev/null
+++ b/tests/Aeon/RateLimiter/Tests/Unit/HitTest.php
@@ -0,0 +1,62 @@
+setNow(DateTime::fromString('2020-01-01 00:00:00 UTC'));
+
+ $this->assertFalse($hit->expired($calendar));
+ }
+
+ public function test_expire_when_hit_is_not_expired_but_on_expiration_edge() : void
+ {
+ $hit = new Hit('id', DateTime::fromString('2020-01-01 00:00:00 UTC'), TimeUnit::minute());
+ $calendar = new GregorianCalendarStub(TimeZone::UTC());
+ $calendar->setNow(DateTime::fromString('2020-01-01 00:01:00 UTC'));
+
+ $this->assertFalse($hit->expired($calendar));
+ }
+
+ public function test_expire_when_hit_is_expired() : void
+ {
+ $hit = new Hit('id', DateTime::fromString('2020-01-01 00:00:00 UTC'), TimeUnit::minute());
+ $calendar = new GregorianCalendarStub(TimeZone::UTC());
+ $calendar->setNow(DateTime::fromString('2020-01-01 00:01:01 UTC'));
+
+ $this->assertTrue($hit->expired($calendar));
+ }
+
+ public function test_ttl_left() : void
+ {
+ $hit = new Hit('id', DateTime::fromString('2020-01-01 00:00:00 UTC'), TimeUnit::minute());
+
+ $calendar = new GregorianCalendarStub(TimeZone::UTC());
+ $calendar->setNow(DateTime::fromString('2020-01-01 00:01:01 UTC'));
+
+ $this->assertSame(0, $hit->ttlLeft($calendar)->inSeconds());
+ }
+
+ public function test_ttl_left_after_expiration_date() : void
+ {
+ $hit = new Hit('id', DateTime::fromString('2020-01-01 00:00:00 UTC'), TimeUnit::minute());
+
+ $calendar = new GregorianCalendarStub(TimeZone::UTC());
+ $calendar->setNow(DateTime::fromString('2020-01-01 00:00:30 UTC'));
+
+ $this->assertSame(30, $hit->ttlLeft($calendar)->inSeconds());
+ }
+}
diff --git a/tests/Aeon/RateLimiter/Tests/Unit/HitsTest.php b/tests/Aeon/RateLimiter/Tests/Unit/HitsTest.php
new file mode 100644
index 0000000..08ba193
--- /dev/null
+++ b/tests/Aeon/RateLimiter/Tests/Unit/HitsTest.php
@@ -0,0 +1,35 @@
+assertSame($hits->count(), 2);
+ }
+
+ public function test_oldest_hit() : void
+ {
+ $hits = new Hits(
+ $hit1 = new Hit('id', DateTime::fromString('2020-01-01 03:00:00 UTC'), TimeUnit::minute()),
+ $hit2 = new Hit('id', DateTime::fromString('2020-01-01 00:00:00 UTC'), TimeUnit::minute()),
+ $hit3 = new Hit('id', DateTime::fromString('2020-01-01 01:00:00 UTC'), TimeUnit::minute()),
+ );
+
+ $this->assertSame($hits->oldest(), $hit2);
+ }
+}
diff --git a/tests/Aeon/RateLimiter/Tests/Unit/Storage/MemoryStorageTest.php b/tests/Aeon/RateLimiter/Tests/Unit/Storage/MemoryStorageTest.php
new file mode 100644
index 0000000..f15753e
--- /dev/null
+++ b/tests/Aeon/RateLimiter/Tests/Unit/Storage/MemoryStorageTest.php
@@ -0,0 +1,75 @@
+assertSame($storage->count('id'), 0);
+ }
+
+ public function test_storage_with_hits() : void
+ {
+ $storage = new MemoryStorage($calendar = new GregorianCalendarStub(TimeZone::UTC()));
+
+ $storage->addHit('id', TimeUnit::minute());
+ $calendar->setNow($calendar->now()->add(TimeUnit::seconds(5)));
+ $storage->addHit('id', TimeUnit::minute());
+
+ $this->assertSame($storage->count('id'), 2);
+ }
+
+ public function test_storage_with_non_unicode_id() : void
+ {
+ $storage = new MemoryStorage($calendar = new GregorianCalendarStub(TimeZone::UTC()));
+
+ $storage->addHit('ÄÓ', TimeUnit::minute());
+ $calendar->setNow($calendar->now()->add(TimeUnit::seconds(5)));
+ $storage->addHit('ÄÓ', TimeUnit::minute());
+
+ $this->assertSame($storage->count('Äó'), 2);
+ }
+
+ public function test_storage_with_all_non_expired_hits() : void
+ {
+ $storage = new MemoryStorage($calendar = new GregorianCalendarStub(TimeZone::UTC()));
+
+ $calendar->setNow(DateTime::fromString('2020-01-01 00:00:00 UTC'));
+
+ $storage->addHit('id', TimeUnit::minute());
+
+ $calendar->setNow($calendar->now()->add(TimeUnit::minutes(4)));
+
+ $storage->addHit('id', TimeUnit::minute());
+
+ $calendar->setNow($calendar->now()->add(TimeUnit::minutes(1)));
+
+ $this->assertSame($storage->all('id')->count(), 1);
+ }
+
+ public function test_storage_hits_ttl() : void
+ {
+ $storage = new MemoryStorage($calendar = new GregorianCalendarStub(TimeZone::UTC()));
+
+ $calendar->setNow(DateTime::fromString('2020-01-01 00:00:00 UTC'));
+
+ $storage->addHit('id', TimeUnit::minute());
+ $storage->addHit('id', TimeUnit::minutes(5));
+
+ $calendar->setNow($calendar->now()->add(TimeUnit::minutes(4)));
+
+ $this->assertSame($storage->count('id'), 1);
+ }
+}