diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..1671c9b
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,18 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.{yml,yaml}]
+indent_size = 2
+
+[docker-compose.yml]
+indent_size = 4
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..311f4ec
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,12 @@
+docs/ export-ignore
+tests/ export-ignore
+.github/ export-ignore
+.editorconfig export-ignore
+.gitattributes export-ignore
+.gitignore export-ignore
+CHANGELOG.md export-ignore
+infection.json5.dis export-ignore
+phpstan.neon.dist export-ignore
+phpunit.xml.dist export-ignore
+psalm.xml.dist export-ignore
+rector.php export-ignore
diff --git a/.github/workflows/composer-normalize.yml b/.github/workflows/composer-normalize.yml
new file mode 100644
index 0000000..d76f89f
--- /dev/null
+++ b/.github/workflows/composer-normalize.yml
@@ -0,0 +1,30 @@
+on:
+ pull_request_target:
+ types:
+ - opened
+ - edited
+ - synchronize
+ - reopened
+ - ready_for_review
+ - converted_to_draft
+ - review_requested
+ push:
+ branches:
+ - 1.x
+
+jobs:
+ run:
+ name: php-cs-fixer
+ runs-on: ${{ matrix.operating-systems }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ operating-systems:
+ - ubuntu-latest
+ php-versions:
+ - 8.1
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: docker://ergebnis/composer-normalize-action
diff --git a/.github/workflows/composer-validate.yml b/.github/workflows/composer-validate.yml
new file mode 100644
index 0000000..140fea1
--- /dev/null
+++ b/.github/workflows/composer-validate.yml
@@ -0,0 +1,36 @@
+on:
+ pull_request_target:
+ types:
+ - opened
+ - edited
+ - synchronize
+ - reopened
+ - ready_for_review
+ - converted_to_draft
+ - review_requested
+ push:
+ branches:
+ - 1.x
+
+jobs:
+ run:
+ name: php-cs-fixer
+ runs-on: ${{ matrix.operating-systems }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ operating-systems:
+ - ubuntu-latest
+ php-versions:
+ - 8.1
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-versions }}
+ coverage: pcov
+
+ - run: composer validate
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
new file mode 100644
index 0000000..0c1d51e
--- /dev/null
+++ b/.github/workflows/dependency-review.yml
@@ -0,0 +1,14 @@
+name: "Dependency Review"
+on: [pull_request_target]
+
+permissions:
+ contents: read
+
+jobs:
+ dependency-review:
+ runs-on: ubuntu-latest
+ steps:
+ - name: "Checkout Repository"
+ uses: actions/checkout@v3
+ - name: "Dependency Review"
+ uses: actions/dependency-review-action@v2
diff --git a/.github/workflows/pest.yml b/.github/workflows/pest.yml
new file mode 100644
index 0000000..c5d0054
--- /dev/null
+++ b/.github/workflows/pest.yml
@@ -0,0 +1,52 @@
+on:
+ pull_request_target:
+ types:
+ - opened
+ - edited
+ - synchronize
+ - reopened
+ - ready_for_review
+ - converted_to_draft
+ - review_requested
+ push:
+ branches:
+ - 1.x
+
+jobs:
+ run:
+ name: php-cs-fixer
+ runs-on: ${{ matrix.operating-systems }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ operating-systems:
+ - ubuntu-latest
+ php-versions:
+ - 8.1
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-versions }}
+ coverage: pcov
+
+ - id: composer-cache
+ run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
+
+ - uses: actions/cache@v3
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
+ restore-keys: ${{ runner.os }}-composer-
+
+ - run: composer install --no-progress --prefer-dist --optimize-autoloader
+
+ - run: vendor/bin/pest --order-by random --coverage --min=100 --ci
+
+ - uses: codecov/codecov-action@v3
+ with:
+ directory: ./coverage/
+ flags: unittests
diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml
new file mode 100644
index 0000000..b9e4b7f
--- /dev/null
+++ b/.github/workflows/php-cs-fixer.yml
@@ -0,0 +1,47 @@
+on:
+ pull_request_target:
+ types:
+ - opened
+ - edited
+ - synchronize
+ - reopened
+ - ready_for_review
+ - converted_to_draft
+ - review_requested
+ push:
+ branches:
+ - 1.x
+
+jobs:
+ run:
+ name: php-cs-fixer
+ runs-on: ${{ matrix.operating-systems }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ operating-systems:
+ - ubuntu-latest
+ php-versions:
+ - 8.1
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-versions }}
+ coverage: pcov
+
+ - id: composer-cache
+ run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
+
+ - uses: actions/cache@v3
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
+ restore-keys: ${{ runner.os }}-composer-
+
+ - run: composer install --no-progress --prefer-dist --optimize-autoloader
+
+ - run: vendor/bin/php-cs-fixer fix src --dry-run --diff
diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml
new file mode 100644
index 0000000..b25fc8a
--- /dev/null
+++ b/.github/workflows/phpstan.yml
@@ -0,0 +1,47 @@
+on:
+ pull_request_target:
+ types:
+ - opened
+ - edited
+ - synchronize
+ - reopened
+ - ready_for_review
+ - converted_to_draft
+ - review_requested
+ push:
+ branches:
+ - 1.x
+
+jobs:
+ run:
+ name: php-cs-fixer
+ runs-on: ${{ matrix.operating-systems }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ operating-systems:
+ - ubuntu-latest
+ php-versions:
+ - 8.1
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-versions }}
+ coverage: pcov
+
+ - id: composer-cache
+ run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
+
+ - uses: actions/cache@v3
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
+ restore-keys: ${{ runner.os }}-composer-
+
+ - run: composer install --no-progress --prefer-dist --optimize-autoloader
+
+ - run: vendor/bin/phpstan analyze
diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml
new file mode 100644
index 0000000..2b5cfd3
--- /dev/null
+++ b/.github/workflows/psalm.yml
@@ -0,0 +1,47 @@
+on:
+ pull_request_target:
+ types:
+ - opened
+ - edited
+ - synchronize
+ - reopened
+ - ready_for_review
+ - converted_to_draft
+ - review_requested
+ push:
+ branches:
+ - 1.x
+
+jobs:
+ run:
+ name: php-cs-fixer
+ runs-on: ${{ matrix.operating-systems }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ operating-systems:
+ - ubuntu-latest
+ php-versions:
+ - 8.1
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-versions }}
+ coverage: pcov
+
+ - id: composer-cache
+ run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
+
+ - uses: actions/cache@v3
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
+ restore-keys: ${{ runner.os }}-composer-
+
+ - run: composer install --no-progress --prefer-dist --optimize-autoloader
+
+ - run: vendor/bin/psalm
diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml
new file mode 100644
index 0000000..8bac931
--- /dev/null
+++ b/.github/workflows/rector.yml
@@ -0,0 +1,47 @@
+on:
+ pull_request_target:
+ types:
+ - opened
+ - edited
+ - synchronize
+ - reopened
+ - ready_for_review
+ - converted_to_draft
+ - review_requested
+ push:
+ branches:
+ - 1.x
+
+jobs:
+ run:
+ name: php-cs-fixer
+ runs-on: ${{ matrix.operating-systems }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ operating-systems:
+ - ubuntu-latest
+ php-versions:
+ - 8.1
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-versions }}
+ coverage: pcov
+
+ - id: composer-cache
+ run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
+
+ - uses: actions/cache@v3
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
+ restore-keys: ${{ runner.os }}-composer-
+
+ - run: composer install --no-progress --prefer-dist --optimize-autoloader
+
+ - run: vendor/bin/rector process src --dry-run
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4bcfbb0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+vendor/
+coverage/
+composer.lock
+.php-cs-fixer.cache
+.phpunit.result.cache
+.DS_Store
+composer.local.json
+NOTES.md
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
new file mode 100644
index 0000000..9083e98
--- /dev/null
+++ b/.php-cs-fixer.dist.php
@@ -0,0 +1,225 @@
+setRiskyAllowed(true)
+ ->setRules([
+ 'array_indentation' => true,
+ 'array_push' => true,
+ 'array_syntax' => ['syntax' => 'short'],
+ 'assign_null_coalescing_to_coalesce_equal' => true,
+ 'backtick_to_shell_exec' => true,
+ 'binary_operator_spaces' => ['operators' => ['=' => 'align', 'xor' => null, '+=' => 'align_single_space', '===' => 'align_single_space_minimal', '|' => 'single_space', '=>' => 'align_single_space_minimal_by_scope']],
+ 'blank_line_after_namespace' => true,
+ 'blank_line_after_opening_tag' => true,
+ 'blank_line_before_statement' => true,
+ 'braces' => true,
+ 'cast_spaces' => true,
+ 'class_attributes_separation' => ['elements' => ['const' => 'only_if_meta', 'method' => 'one', 'property' => 'only_if_meta', 'trait_import' => 'one', 'case' => 'one']],
+ 'class_definition' => true,
+ 'class_reference_name_casing' => true,
+ 'clean_namespace' => true,
+ 'combine_consecutive_issets' => true,
+ 'combine_consecutive_unsets' => true,
+ 'combine_nested_dirname' => true,
+ 'comment_to_phpdoc' => ['ignored_tags' => ['codeCoverageIgnoreStart', 'codeCoverageIgnoreEnd', 'phpstan-ignore-next-line']],
+ 'compact_nullable_typehint' => true,
+ 'concat_space' => ['spacing' => 'one'],
+ 'constant_case' => ['case' => 'lower'],
+ 'date_time_create_from_format_call' => true,
+ 'declare_equal_normalize' => ['space' => 'none'],
+ 'declare_parentheses' => true,
+ 'declare_strict_types' => true,
+ 'dir_constant' => true,
+ 'doctrine_annotation_array_assignment' => true,
+ 'doctrine_annotation_braces' => true,
+ 'doctrine_annotation_indentation' => true,
+ 'doctrine_annotation_spaces' => true,
+ 'echo_tag_syntax' => ['format' => 'long'],
+ 'elseif' => true,
+ 'empty_loop_body' => true,
+ 'empty_loop_condition' => true,
+ 'encoding' => true,
+ 'ereg_to_preg' => true,
+ 'error_suppression' => true,
+ 'escape_implicit_backslashes' => true,
+ 'explicit_indirect_variable' => true,
+ 'explicit_string_variable' => true,
+ 'final_class' => true,
+ 'final_internal_class' => true,
+ 'fopen_flag_order' => true,
+ 'fopen_flags' => true,
+ 'full_opening_tag' => true,
+ 'fully_qualified_strict_types' => true,
+ 'function_declaration' => true,
+ 'function_to_constant' => true,
+ 'function_typehint_space' => true,
+ 'general_phpdoc_annotation_remove' => true,
+ 'general_phpdoc_tag_rename' => true,
+ 'get_class_to_class_keyword' => true,
+ 'global_namespace_import' => ['import_classes' => true, 'import_constants' => true, 'import_functions' => true],
+ 'group_import' => true,
+ 'heredoc_indentation' => true,
+ 'heredoc_to_nowdoc' => true,
+ 'implode_call' => true,
+ 'include' => true,
+ 'increment_style' => ['style' => 'pre'],
+ 'indentation_type' => true,
+ 'integer_literal_case' => true,
+ 'is_null' => true,
+ 'lambda_not_used_import' => true,
+ 'line_ending' => true,
+ 'linebreak_after_opening_tag' => true,
+ 'list_syntax' => ['syntax' => 'short'],
+ 'logical_operators' => true,
+ 'lowercase_cast' => true,
+ 'lowercase_keywords' => true,
+ 'lowercase_static_reference' => true,
+ 'magic_constant_casing' => true,
+ 'magic_method_casing' => true,
+ 'mb_str_functions' => true,
+ 'method_argument_space' => true,
+ 'method_chaining_indentation' => true,
+ 'modernize_strpos' => true,
+ 'modernize_types_casting' => true,
+ 'multiline_comment_opening_closing' => true,
+ 'multiline_whitespace_before_semicolons' => true,
+ 'native_function_casing' => true,
+ 'native_function_invocation' => true,
+ 'native_function_type_declaration_casing' => true,
+ 'new_with_braces' => true,
+ 'no_alias_functions' => true,
+ 'no_alias_language_construct_call' => true,
+ 'no_alternative_syntax' => true,
+ 'no_binary_string' => true,
+ 'no_blank_lines_after_class_opening' => true,
+ 'no_blank_lines_after_phpdoc' => true,
+ 'no_break_comment' => true,
+ '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' => true,
+ 'no_multiline_whitespace_around_double_arrow' => true,
+ 'no_multiple_statements_per_line' => true,
+ 'no_php4_constructor' => true,
+ 'no_short_bool_cast' => true,
+ 'no_singleline_whitespace_before_semicolons' => true,
+ 'no_space_around_double_colon' => true,
+ 'no_spaces_after_function_name' => true,
+ 'no_spaces_around_offset' => true,
+ 'no_spaces_inside_parenthesis' => true,
+ 'no_superfluous_elseif' => true,
+ 'no_trailing_comma_in_singleline' => true,
+ 'no_trailing_whitespace_in_comment' => true,
+ 'no_trailing_whitespace_in_string' => true,
+ 'no_trailing_whitespace' => true,
+ 'no_unneeded_control_parentheses' => true,
+ 'no_unneeded_curly_braces' => true,
+ 'no_unneeded_final_method' => true,
+ 'no_unneeded_import_alias' => true,
+ 'no_unreachable_default_argument_value' => true,
+ 'no_unset_cast' => true,
+ 'no_unused_imports' => true,
+ 'no_useless_concat_operator' => true,
+ 'no_useless_else' => true,
+ 'no_useless_nullsafe_operator' => true,
+ 'no_useless_return' => true,
+ 'no_useless_sprintf' => true,
+ 'no_whitespace_before_comma_in_array' => true,
+ 'no_whitespace_in_blank_line' => true,
+ 'non_printable_character' => true,
+ 'normalize_index_brace' => true,
+ 'not_operator_with_successor_space' => true,
+ 'nullable_type_declaration_for_default_null_value' => true,
+ 'object_operator_without_whitespace' => true,
+ 'octal_notation' => true,
+ 'operator_linebreak' => true,
+ 'ordered_class_elements' => ['sort_algorithm' => 'alpha', 'order' => ['use_trait', 'case', 'constant', 'constant_private', 'constant_protected', 'constant_public', 'property_private', 'property_private_readonly', 'property_private_static', 'property_protected', 'property_protected_readonly', 'property_protected_static', 'property_public', 'property_public_readonly', 'property_public_static', 'property_static', 'protected', 'construct', 'destruct', 'magic', 'method', 'public', 'method_abstract', 'method_private', 'method_private_abstract', 'method_private_abstract_static', 'method_private_static', 'method_protected', 'method_protected_abstract', 'method_protected_abstract_static', 'method_protected_static', 'method_public', 'method_public_abstract', 'method_public_abstract_static', 'method_public_static', 'method_static', 'phpunit', 'private', 'property']],
+ 'ordered_imports' => ['sort_algorithm' => 'alpha', 'imports_order' => ['const', 'class', 'function']],
+ 'ordered_interfaces' => true,
+ 'ordered_traits' => true,
+ 'php_unit_fqcn_annotation' => true,
+ 'phpdoc_add_missing_param_annotation' => ['only_untyped' => false],
+ 'phpdoc_align' => ['align' => 'vertical'],
+ 'phpdoc_indent' => true,
+ 'phpdoc_inline_tag_normalizer' => true,
+ 'phpdoc_line_span' => true,
+ 'phpdoc_no_access' => true,
+ 'phpdoc_no_empty_return' => true,
+ 'phpdoc_no_package' => true,
+ 'phpdoc_no_useless_inheritdoc' => true,
+ 'phpdoc_order_by_value' => true,
+ 'phpdoc_order' => true,
+ 'phpdoc_return_self_reference' => ['replacements' => ['this' => 'self']],
+ 'phpdoc_scalar' => true,
+ 'phpdoc_separation' => true,
+ 'phpdoc_single_line_var_spacing' => true,
+ 'phpdoc_summary' => true,
+ 'phpdoc_tag_type' => true,
+ 'phpdoc_to_comment' => ['ignored_tags' => ['var']],
+ 'phpdoc_trim_consecutive_blank_line_separation' => true,
+ 'phpdoc_trim' => true,
+ 'phpdoc_types_order' => true,
+ 'phpdoc_types' => true,
+ 'phpdoc_var_annotation_correct_order' => true,
+ 'phpdoc_var_without_name' => true,
+ 'pow_to_exponentiation' => true,
+ 'protected_to_private' => true,
+ 'psr_autoloading' => true,
+ 'random_api_migration' => true,
+ 'regular_callable_call' => true,
+ 'return_assignment' => true,
+ 'return_type_declaration' => ['space_before' => 'none'],
+ 'return_type_declaration' => true,
+ '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_if_return' => true,
+ 'single_blank_line_at_eof' => true,
+ 'single_blank_line_before_namespace' => true,
+ 'single_class_element_per_statement' => true,
+ 'single_line_after_imports' => true,
+ 'single_line_comment_spacing' => true,
+ 'single_line_comment_style' => ['comment_types' => ['hash']],
+ 'single_line_throw' => true,
+ 'single_quote' => true,
+ 'single_space_after_construct' => true,
+ 'single_trait_insert_per_statement' => true,
+ 'space_after_semicolon' => true,
+ 'standardize_increment' => true,
+ 'standardize_not_equals' => true,
+ 'statement_indentation' => true,
+ 'static_lambda' => true,
+ 'strict_comparison' => true,
+ 'strict_param' => true,
+ 'string_length_to_empty' => true,
+ 'string_line_ending' => true,
+ 'switch_case_semicolon_to_colon' => true,
+ 'switch_case_space' => true,
+ 'switch_continue_to_break' => true,
+ 'ternary_operator_spaces' => true,
+ 'ternary_to_elvis_operator' => true,
+ 'ternary_to_null_coalescing' => true,
+ 'trailing_comma_in_multiline' => ['after_heredoc' => true, 'elements' => ['arguments', 'parameters']],
+ 'trim_array_spaces' => true,
+ 'types_spaces' => ['space' => 'single'],
+ 'unary_operator_spaces' => true,
+ 'use_arrow_functions' => true,
+ 'visibility_required' => true,
+ 'void_return' => true,
+ 'whitespace_after_comma_in_array' => true,
+ 'yoda_style' => true,
+ ])
+ ->setFinder(
+ PhpCsFixer\Finder::create()
+ ->exclude('vendor')
+ ->in([__DIR__.'/src/'])
+ )
+;
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..d204606
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+### Added
+
+- Initial release.
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..47239d7
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Evan Sims
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index e752964..0207c1c 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,140 @@
-# http-factory-implementations
\ No newline at end of file
+**Lightweight library that discovers available [PSR-17 HTTP Factory](https://github.com/psr-discovery/http-factory-implementations) implementations by searching for a list of well-known classes that implement the relevant interface, and returns an instance of the first one that is found.**
+
+This package is part of the [psr-discovery/discovery](https://github.com/psr-discovery/discovery) PSR discovery collection, which also supports [PSR-18 HTTP Clients](https://github.com/psr-discovery/http-client-implementations), [PSR-14 Event Dispatchers](https://github.com/psr-discovery/event-dispatcher-implementations), [PSR-11 Containers](https://github.com/psr-discovery/container-implementations), [PSR-6 Cache](https://github.com/psr-discovery/cache-implementations) and [PSR-3 Loggers](https://github.com/psr-discovery/log-implementations).
+
+This is largely intended for inclusion in libraries like SDKs that wish to support PSR-17 Factories without requiring hard dependencies on specific implementations or demanding extra configuration by users.
+
+- [Requirements](#requirements)
+- [Implementations](#implementations)
+- [Installation](#installation)
+- [Usage](#usage)
+- [Handling Failures](#handling-failures)
+- [Exceptions](#exceptions)
+- [Singletons](#singletons)
+- [Mocking Priority](#mocking-priority)
+- [Preferring an Implementation](#preferring-an-implementation)
+- [Using a Specific Implementation](#using-a-specific-implementation)
+
+## Requirements
+
+- PHP 8.0+
+- Composer 2.0+
+
+Successful discovery requires the presence of a compatible implementation in the host application. This library does not install any implementations for you.
+
+## Implementations
+
+The discovery of available implementations is based on a list of well-known libraries that provide the `psr/http-factory-implementation` interface. These include:
+
+- ...
+
+If [a particular implementation](https://packagist.org/providers/psr/http-factory-implementation) is missing that you'd like to see, please open a pull request adding support.
+
+## Installation
+
+```bash
+composer require --dev psr-discovery/http-factory-implementations
+```
+
+## Usage
+
+```php
+use PsrDiscovery\Discovery;
+
+// Returns a PSR-17 RequestFactoryInterface instance
+$requestFactory = Discovery::httpRequestFactory();
+
+// Returns a PSR-17 ResponseFactoryInterface instance
+$responseFactory = Discovery::httpResponseFactory();
+
+// Returns a PSR-17 StreamFactoryInterface instance
+$streamFactory = Discovery::httpStreamFactory();
+
+// Returns a PSR-7 RequestInterface instance
+$request = $requestFactory->createRequest('GET', 'https://example.com');
+```
+
+## Handling Failures
+
+If the library is unable to discover a suitable PSR-17 implementation, the `Discovery::httpRequestFactory()`, `Discovery::httpResponseFactory()` or `Discovery::httpStreamFactory()` discovery methods will simply return `null`. This allows you to handle the failure gracefully, for example by falling back to a default implementation.
+
+Example:
+
+```php
+use PsrDiscovery\Discovery;
+
+$requestFactory = Discovery::httpRequestFactory();
+
+if ($requestFactory === null) {
+ // No suitable HTTP RequestFactory implementation was discovered.
+ // Fall back to a default implementation.
+ $requestFactory = new DefaultRequestFactory();
+}
+```
+
+## Singletons
+
+By default, the `Discovery::httpRequestFactory()`, `Discovery::httpResponseFactory()` or `Discovery::httpStreamFactory()` methods will always return a new instance of the discovered implementation. If you wish to use a singleton instance instead, simply pass `true` to the `$singleton` parameter of the discovery method.
+
+Example:
+
+```php
+use PsrDiscovery\Discovery;
+
+// $httpResponseFactory1 !== $httpResponseFactory2 (default)
+$httpResponseFactory1 = Discovery::httpResponseFactory();
+$httpResponseFactory2 = Discovery::httpResponseFactory();
+
+// $httpResponseFactory1 === $httpResponseFactory2
+$httpResponseFactory1 = Discovery::httpResponseFactory(singleton: true);
+$httpResponseFactory2 = Discovery::httpResponseFactory(singleton: true);
+```
+
+## Mocking Priority
+
+This library will give priority to searching for a known, available mocking library before searching for a real implementation. This is to allow for easier testing of code that uses this library.
+
+The expectation is that these mocking libraries will always be installed as development dependencies, and therefore if they are available, they are intended to be used.
+
+## Preferring an Implementation
+
+If you wish to prefer a specific implementation over others, you can `prefer()` it by package name:
+
+```php
+use PsrDiscovery\Discovery;
+use PsrDiscovery\Implementations\Psr17\RequestFactories;
+
+// Prefer the a specific implementation of PSR-17 over others.
+RequestFactories::prefer('nyholm/psr7');
+
+// Return an instance of Nyholm\Psr7\Factory\Psr17Factory,
+// or the next available from the list of candidates,
+// Returns null if none are discovered.
+$factory = Discovery::httpRequestFactory();
+```
+
+In this case, this will cause the `httpRequestFactory()` method to return the preferred implementation if it is available, otherwise, it will fall back to the default behavior. The same applies to `httpResponseFactory()` and `httpStreamFactory()` when their relevant classes are configured similarly.
+
+Note that assigning a preferred implementation will give it priority over the default preference of mocking libraries.
+
+## Using a Specific Implementation
+
+If you wish to force a specific implementation and ignore the rest of the discovery candidates, you can `use()` its package name:
+
+```php
+use PsrDiscovery\Discovery;
+use PsrDiscovery\Implementations\Psr17\ResponseFactories;
+
+// Only discover a specific implementation of PSR-17.
+ResponseFactories::use('nyholm/psr7');
+
+// Return an instance of Nyholm\Psr7\Factory\Psr17Factory,
+// or null if it is not available.
+$factory = Discovery::httpResponseFactory();
+```
+
+In this case, this will cause the `httpResponseFactory()` method to return the preferred implementation if it is available, otherwise, it will return `null`. The same applies to `httpRequestFactory()` and `httpStreamFactory()` when their relevant classes are configured similarly.
+
+---
+
+This library is not produced or endorsed by, or otherwise affiliated with, the PHP-FIG.
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..18cfc6d
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,91 @@
+{
+ "name": "psr-discovery/http-factory-implementations",
+ "description": "Lightweight library that discovers available PSR-17 HTTP Factory implementations by searching for a list of well-known classes that implement the relevant interface, and returns an instance of the first one that is found.",
+ "license": "MIT",
+ "type": "library",
+ "keywords": [
+ "psr",
+ "discovery",
+ "psr-18"
+ ],
+ "authors": [
+ {
+ "name": "Evan Sims",
+ "email": "hello@evansims.com",
+ "homepage": "https://evansims.com/"
+ }
+ ],
+ "homepage": "https://github.com/psr-discovery/http-factory-implementations",
+ "require": {
+ "php": "^8.0",
+ "psr/http-factory": "^1.0",
+ "psr-discovery/discovery": "@dev"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.14",
+ "infection/infection": "^0.26",
+ "mockery/mockery": "^1.5",
+ "pestphp/pest": "^2.0",
+ "phpstan/phpstan": "^1.10",
+ "phpstan/phpstan-strict-rules": "^1.5",
+ "rector/rector": "^0.15",
+ "vimeo/psalm": "^5.8",
+ "wikimedia/composer-merge-plugin": "^2.0"
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "autoload": {
+ "psr-4": {
+ "PsrDiscovery\\": "src"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "PsrDiscovery\\Tests\\": "tests"
+ }
+ },
+ "config": {
+ "allow-plugins": {
+ "infection/extension-installer": true,
+ "pestphp/pest-plugin": true,
+ "wikimedia/composer-merge-plugin": true
+ },
+ "optimize-autoloader": true,
+ "preferred-install": "dist",
+ "process-timeout": 0,
+ "sort-packages": true
+ },
+ "extra": {
+ "merge-plugin": {
+ "ignore-duplicates": false,
+ "include": [
+ "composer.local.json"
+ ],
+ "merge-dev": true,
+ "merge-extra": false,
+ "merge-extra-deep": false,
+ "merge-scripts": false,
+ "recurse": true,
+ "replace": true
+ }
+ },
+ "scripts": {
+ "mutate": "@php ./vendor/bin/infection --test-framework=pest --show-mutations",
+ "pest:coverage": "@php vendor/bin/pest --order-by random --compact --coverage",
+ "pest": "@php vendor/bin/pest --order-by random --compact",
+ "phpcs:fix": "@php vendor/bin/php-cs-fixer fix src",
+ "phpcs": "@php vendor/bin/php-cs-fixer fix src --dry-run --diff",
+ "phpstan": "@php vendor/bin/phpstan analyze",
+ "psalm:fix": "@php vendor/bin/psalter --issues=all",
+ "psalm": "@php vendor/bin/psalm",
+ "rector:fix": "@php vendor/bin/rector process src",
+ "rector": "@php vendor/bin/rector process src --dry-run",
+ "test": [
+ "@pest",
+ "@phpstan",
+ "@psalm",
+ "@rector",
+ "@phpcs"
+ ]
+ }
+}
diff --git a/docs/.gitkeep b/docs/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/infection.json5.dist b/infection.json5.dist
new file mode 100644
index 0000000..ac2a7ee
--- /dev/null
+++ b/infection.json5.dist
@@ -0,0 +1,11 @@
+{
+ source: {
+ directories: ["src"],
+ },
+ logs: {
+ "text": "infection.log"
+ },
+ mutators: {
+ "@default": true,
+ }
+}
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
new file mode 100644
index 0000000..e79b78a
--- /dev/null
+++ b/phpstan.neon.dist
@@ -0,0 +1,15 @@
+includes:
+ - ./vendor/phpstan/phpstan-strict-rules/rules.neon
+
+parameters:
+ level: max
+
+ paths:
+ - src
+
+ bootstrapFiles:
+
+ ignoreErrors:
+
+ reportUnmatchedIgnoredErrors: true
+ treatPhpDocTypesAsCertain: false
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..4d5c4b3
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,24 @@
+
+
+
+
+ ./src/
+
+
+
+ ./src/Contracts
+ ./src/Exceptions
+
+
+
+
+
+
+
+
+
+
+ tests/Unit
+
+
+
diff --git a/psalm.xml.dist b/psalm.xml.dist
new file mode 100644
index 0000000..8f1837d
--- /dev/null
+++ b/psalm.xml.dist
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/rector.php b/rector.php
new file mode 100644
index 0000000..6ed5929
--- /dev/null
+++ b/rector.php
@@ -0,0 +1,576 @@
+paths([
+ __DIR__ . '/config',
+ __DIR__ . '/src',
+ ]);
+
+ $rectorConfig->ruleWithConfiguration(
+ RenameFunctionRector::class,
+ [
+ 'chop' => 'rtrim',
+ 'doubleval' => 'floatval',
+ 'fputs' => 'fwrite',
+ 'gzputs' => 'gzwrites',
+ 'ini_alter' => 'ini_set',
+ 'is_double' => 'is_float',
+ 'is_integer' => 'is_int',
+ 'is_long' => 'is_int',
+ 'is_real' => 'is_float',
+ 'is_writeable' => 'is_writable',
+ 'join' => 'implode',
+ 'key_exists' => 'array_key_exists',
+ 'mbstrcut' => 'mb_strcut',
+ 'mbstrlen' => 'mb_strlen',
+ 'mbstrpos' => 'mb_strpos',
+ 'mbstrrpos' => 'mb_strrpos',
+ 'mbsubstr' => 'mb_substr',
+ 'pos' => 'current',
+ 'sizeof' => 'count',
+ 'split' => 'explode',
+ 'strchr' => 'strstr',
+ ],
+ );
+
+ $rectorConfig->ruleWithConfiguration(
+ StaticCallToFuncCallRector::class,
+ [
+ new StaticCallToFuncCall('Nette\\Utils\\Strings', 'contains', 'str_contains'),
+ new StaticCallToFuncCall('Nette\\Utils\\Strings', 'endsWith', 'str_ends_with'),
+ new StaticCallToFuncCall('Nette\\Utils\\Strings', 'startsWith', 'str_starts_with'),
+ ],
+ );
+
+ $rectorConfig->ruleWithConfiguration(
+ ArgumentAdderRector::class,
+ [new ArgumentAdder('Nette\\Utils\\Strings', 'replace', 2, 'replacement', '')],
+ );
+
+ $rectorConfig->ruleWithConfiguration(
+ RenameFunctionRector::class,
+ [
+ 'pg_clientencoding' => 'pg_client_encoding',
+ 'pg_cmdtuples' => 'pg_affected_rows',
+ 'pg_errormessage' => 'pg_last_error',
+ 'pg_fieldisnull' => 'pg_field_is_null',
+ 'pg_fieldname' => 'pg_field_name',
+ 'pg_fieldnum' => 'pg_field_num',
+ 'pg_fieldprtlen' => 'pg_field_prtlen',
+ 'pg_fieldsize' => 'pg_field_size',
+ 'pg_fieldtype' => 'pg_field_type',
+ 'pg_freeresult' => 'pg_free_result',
+ 'pg_getlastoid' => 'pg_last_oid',
+ 'pg_loclose' => 'pg_lo_close',
+ 'pg_locreate' => 'pg_lo_create',
+ 'pg_loexport' => 'pg_lo_export',
+ 'pg_loimport' => 'pg_lo_import',
+ 'pg_loopen' => 'pg_lo_open',
+ 'pg_loread' => 'pg_lo_read',
+ 'pg_loreadall' => 'pg_lo_read_all',
+ 'pg_lounlink' => 'pg_lo_unlink',
+ 'pg_lowrite' => 'pg_lo_write',
+ 'pg_numfields' => 'pg_num_fields',
+ 'pg_numrows' => 'pg_num_rows',
+ 'pg_result' => 'pg_fetch_result',
+ 'pg_setclientencoding' => 'pg_set_client_encoding'
+ ],
+ );
+
+ $rectorConfig->ruleWithConfiguration(
+ FunctionArgumentDefaultValueReplacerRector::class,
+ [
+ new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'gte', 'ge'),
+ new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'lte', 'le'),
+ new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, '', '!='),
+ new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, '!', '!='),
+ new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'g', 'gt'),
+ new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'l', 'lt'),
+ new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'gte', 'ge'),
+ new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'lte', 'le'),
+ new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'n', 'ne')
+ ],
+ );
+
+ $rectorConfig->ruleWithConfiguration(
+ FuncCallToConstFetchRector::class,
+ [
+ 'php_sapi_name' => 'PHP_SAPI',
+ 'pi' => 'M_PI'
+ ],
+ );
+
+ $rectorConfig->rules([
+ // AddParamTypeFromPropertyTypeRector::class,
+ // ChangeOrIfReturnToEarlyReturnRector::class,
+ // RemoveAlwaysTrueIfConditionRector::class,
+ // RenameParamToMatchTypeRector::class,
+ AbsolutizeRequireAndIncludePathRector::class,
+ ActionInjectionToConstructorInjectionRector::class,
+ AddArrayDefaultToArrayPropertyRector::class,
+ AddArrowFunctionReturnTypeRector::class,
+ AddClosureReturnTypeRector::class,
+ AddFalseDefaultToBoolPropertyRector::class,
+ AddMethodCallBasedStrictParamTypeRector::class,
+ AddParamBasedOnParentClassMethodRector::class,
+ AddParamTypeBasedOnPHPUnitDataProviderRector::class,
+ AddParamTypeSplFixedArrayRector::class,
+ AddPregQuoteDelimiterRector::class,
+ AddReturnTypeDeclarationBasedOnParentClassMethodRector::class,
+ AddReturnTypeDeclarationFromYieldsRector::class,
+ AddVoidReturnTypeWhereNoReturnRector::class,
+ AndAssignsToSeparateLinesRector::class,
+ ArrayKeyExistsTernaryThenValueToCoalescingRector::class,
+ ArrayKeysAndInArrayToArrayKeyExistsRector::class,
+ ArrayMergeOfNonArraysToSimpleArrayRector::class,
+ ArrayShapeFromConstantArrayReturnRector::class,
+ BinarySwitchToIfElseRector::class,
+ BooleanNotIdenticalToNotIdenticalRector::class,
+ BoolvalToTypeCastRector::class,
+ CallableThisArrayToAnonymousFunctionRector::class,
+ CallUserFuncArrayToVariadicRector::class,
+ CallUserFuncToMethodCallRector::class,
+ CallUserFuncToMethodCallRector::class,
+ CallUserFuncWithArrowFunctionToInlineRector::class,
+ CatchExceptionNameMatchingTypeRector::class,
+ // ChangeAndIfToEarlyReturnRector::class,
+ ChangeArrayPushToArrayAssignRector::class,
+ ChangeGlobalVariablesToPropertiesRector::class,
+ ChangeIfElseValueAssignToEarlyReturnRector::class,
+ ChangeNestedForeachIfsToEarlyContinueRector::class,
+ ChangeNestedIfsToEarlyReturnRector::class,
+ ChangeOrIfContinueToMultiContinueRector::class,
+ ChangeReadOnlyPropertyWithDefaultValueToConstantRector::class,
+ ChangeReadOnlyVariableWithDefaultValueToConstantRector::class,
+ ChangeSwitchToMatchRector::class,
+ ClassOnObjectRector::class,
+ ClassOnThisVariableObjectRector::class,
+ ClassPropertyAssignToConstructorPromotionRector::class,
+ CombinedAssignRector::class,
+ CombineIfRector::class,
+ CommonNotEqualRector::class,
+ CompactToVariablesRector::class,
+ CompleteDynamicPropertiesRector::class,
+ ConsecutiveNullCompareReturnsToNullCoalesceQueueRector::class,
+ ConsistentImplodeRector::class,
+ ConsistentPregDelimiterRector::class,
+ CountArrayToEmptyArrayComparisonRector::class,
+ CountArrayToEmptyArrayComparisonRector::class,
+ EmptyOnNullableObjectToInstanceOfRector::class,
+ EncapsedStringsToSprintfRector::class,
+ ExplicitBoolCompareRector::class,
+ ExplicitMethodCallOverMagicGetSetRector::class,
+ FinalizeClassesWithoutChildrenRector::class,
+ FinalPrivateToPrivateVisibilityRector::class,
+ FlipTypeControlToUseExclusiveTypeRector::class,
+ FloatvalToTypeCastRector::class,
+ ForeachItemsAssignToEmptyArrayToAssignRector::class,
+ ForeachToInArrayRector::class,
+ ForRepeatedCountToOwnVariableRector::class,
+ ForToForeachRector::class,
+ FuncGetArgsToVariadicParamRector::class,
+ FuncGetArgsToVariadicParamRector::class,
+ GetClassToInstanceOfRector::class,
+ GetDebugTypeRector::class,
+ InlineArrayReturnAssignRector::class,
+ InlineConstructorDefaultToPropertyRector::class,
+ InlineIfToExplicitIfRector::class,
+ InlineIsAInstanceOfRector::class,
+ IntvalToTypeCastRector::class,
+ IsAWithStringWithThirdArgumentRector::class,
+ IssetOnPropertyObjectToPropertyExistsRector::class,
+ JoinStringConcatRector::class,
+ LogicalToBooleanRector::class,
+ MakeInheritedMethodVisibilitySameAsParentRector::class,
+ // MixedTypeRector::class,
+ MultipleClassFileToPsr4ClassesRector::class,
+ NarrowUnionTypeDocRector::class,
+ // NewlineAfterStatementRector::class,
+ NewlineBeforeNewAssignSetRector::class,
+ NewStaticToNewSelfRector::class,
+ NormalizeNamespaceByPSR4ComposerAutoloadRector::class,
+ NullableCompareToNullRector::class,
+ OptionalParametersAfterRequiredRector::class,
+ OptionalParametersAfterRequiredRector::class,
+ ParamAnnotationIncorrectNullableRector::class,
+ ParamTypeByMethodCallTypeRector::class,
+ ParamTypeByParentCallTypeRector::class,
+ ParamTypeFromStrictTypedPropertyRector::class,
+ Php8ResourceReturnToObjectRector::class,
+ PostIncDecToPreIncDecRector::class,
+ PreparedValueToEarlyReturnRector::class,
+ PrivatizeFinalClassMethodRector::class,
+ PrivatizeFinalClassPropertyRector::class,
+ // PrivatizeLocalGetterToPropertyRector::class,
+ PropertyTypeFromStrictSetterGetterRector::class,
+ // RecastingRemovalRector::class,
+ RemoveAlwaysElseRector::class,
+ RemoveAlwaysTrueConditionSetInConstructorRector::class,
+ RemoveAndTrueRector::class,
+ // RemoveConcatAutocastRector::class,
+ RemoveDeadConditionAboveReturnRector::class,
+ RemoveDeadContinueRector::class,
+ RemoveDeadIfForeachForRector::class,
+ // RemoveDeadInstanceOfRector::class,
+ RemoveDeadLoopRector::class,
+ RemoveDeadReturnRector::class,
+ RemoveDeadStmtRector::class,
+ RemoveDeadTryCatchRector::class,
+ RemoveDeadZeroAndOneOperationRector::class,
+ RemoveDelegatingParentCallRector::class,
+ RemoveDoubleAssignRector::class,
+ RemoveDoubleUnderscoreInMethodNameRector::class,
+ RemoveDuplicatedArrayKeyRector::class,
+ RemoveDuplicatedCaseInSwitchRector::class,
+ RemoveDuplicatedIfReturnRector::class,
+ RemoveDuplicatedInstanceOfRector::class,
+ RemoveEmptyClassMethodRector::class,
+ RemoveEmptyMethodCallRector::class,
+ RemoveEmptyTestMethodRector::class,
+ RemoveExtraParametersRector::class,
+ RemoveFinalFromConstRector::class,
+ RemoveJustPropertyFetchForAssignRector::class,
+ RemoveJustVariableAssignRector::class,
+ RemoveLastReturnRector::class,
+ RemoveNonExistingVarAnnotationRector::class,
+ RemoveNullPropertyInitializationRector::class,
+ RemoveParentCallWithoutParentRector::class,
+ RemoveParentCallWithoutParentRector::class,
+ RemoveSoleValueSprintfRector::class,
+ RemoveUnreachableStatementRector::class,
+ RemoveUnusedConstructorParamRector::class,
+ RemoveUnusedForeachKeyRector::class,
+ RemoveUnusedNonEmptyArrayBeforeForeachRector::class,
+ RemoveUnusedPrivateClassConstantRector::class,
+ RemoveUnusedPrivateMethodParameterRector::class,
+ RemoveUnusedPrivateMethodRector::class,
+ RemoveUnusedPrivatePropertyRector::class,
+ RemoveUnusedPromotedPropertyRector::class,
+ RemoveUnusedVariableAssignRector::class,
+ RemoveUnusedVariableInCatchRector::class,
+ // RemoveUselessParamTagRector::class,
+ RemoveUselessReturnTagRector::class,
+ RemoveUselessVarTagRector::class,
+ RenameForeachValueVariableToMatchExprVariableRector::class,
+ RenameForeachValueVariableToMatchMethodCallReturnTypeRector::class,
+ // RenamePropertyToMatchTypeRector::class,
+ // RenameVariableToMatchMethodCallReturnTypeRector::class,
+ // RenameVariableToMatchNewTypeRector::class,
+ ReplaceMultipleBooleanNotRector::class,
+ ReturnAnnotationIncorrectNullableRector::class,
+ ReturnBinaryAndToEarlyReturnRector::class,
+ ReturnBinaryOrToEarlyReturnRector::class,
+ ReturnEarlyIfVariableRector::class,
+ ReturnNeverTypeRector::class,
+ ReturnTypeFromReturnDirectArrayRector::class,
+ ReturnTypeFromReturnNewRector::class,
+ ReturnTypeFromStrictBoolReturnExprRector::class,
+ ReturnTypeFromStrictConstantReturnRector::class,
+ ReturnTypeFromStrictNativeCallRector::class,
+ ReturnTypeFromStrictNewArrayRector::class,
+ ReturnTypeFromStrictScalarReturnExprRector::class,
+ ReturnTypeFromStrictScalarReturnExprRector::class,
+ ReturnTypeFromStrictTernaryRector::class,
+ ReturnTypeFromStrictTypedCallRector::class,
+ ReturnTypeFromStrictTypedPropertyRector::class,
+ SeparateMultiUseImportsRector::class,
+ SetStateToStaticRector::class,
+ SetTypeToCastRector::class,
+ ShortenElseIfRector::class,
+ SimplifyArraySearchRector::class,
+ SimplifyBoolIdenticalTrueRector::class,
+ SimplifyConditionsRector::class,
+ SimplifyDeMorganBinaryRector::class,
+ SimplifyEmptyArrayCheckRector::class,
+ SimplifyEmptyCheckOnEmptyArrayRector::class,
+ SimplifyForeachToArrayFilterRector::class,
+ SimplifyForeachToCoalescingRector::class,
+ SimplifyFuncGetArgsCountRector::class,
+ SimplifyIfElseToTernaryRector::class,
+ SimplifyIfElseWithSameContentRector::class,
+ SimplifyIfExactValueReturnValueRector::class,
+ SimplifyIfNotNullReturnRector::class,
+ SimplifyIfNullableReturnRector::class,
+ SimplifyIfReturnBoolRector::class,
+ SimplifyInArrayValuesRector::class,
+ SimplifyMirrorAssignRector::class,
+ SimplifyRegexPatternRector::class,
+ SimplifyStrposLowerRector::class,
+ SimplifyTautologyTernaryRector::class,
+ SimplifyUselessLastVariableAssignRector::class,
+ SimplifyUselessVariableRector::class,
+ SingleInArrayToCompareRector::class,
+ SingularSwitchToIfRector::class,
+ SplitDoubleAssignRector::class,
+ SplitGroupedClassConstantsRector::class,
+ SplitGroupedPropertiesRector::class,
+ SplitListAssignToSeparateLineRector::class,
+ StaticArrowFunctionRector::class,
+ StaticClosureRector::class,
+ StrContainsRector::class,
+ StrEndsWithRector::class,
+ StrictArraySearchRector::class,
+ StringableForToStringRector::class,
+ // StringClassNameToClassConstantRector::class,
+ StrlenZeroToIdenticalEmptyStringRector::class,
+ StrStartsWithRector::class,
+ StrvalToTypeCastRector::class,
+ SwitchNegatedTernaryRector::class,
+ SymplifyQuoteEscapeRector::class,
+ TernaryConditionVariableAssignmentRector::class,
+ TernaryEmptyArrayArrayDimFetchToCoalesceRector::class,
+ TernaryFalseExpressionToIfRector::class,
+ TernaryToBooleanOrFalseToBooleanAndRector::class,
+ ThrowWithPreviousExceptionRector::class,
+ TokenGetAllToObjectRector::class,
+ TypedPropertyFromAssignsRector::class,
+ TypedPropertyFromStrictConstructorRector::class,
+ TypedPropertyFromStrictGetterMethodReturnTypeRector::class,
+ TypedPropertyFromStrictSetUpRector::class,
+ // UnionTypesRector::class,
+ UnnecessaryTernaryExpressionRector::class,
+ UnSpreadOperatorRector::class,
+ UnusedForeachValueToArrayKeysRector::class,
+ UnwrapFutureCompatibleIfPhpVersionRector::class,
+ UnwrapSprintfOneArgumentRector::class,
+ UseClassKeywordForClassNameResolutionRector::class,
+ UseIdenticalOverEqualWithSameTypeRector::class,
+ UseIncrementAssignRector::class,
+ VarAnnotationIncorrectNullableRector::class,
+ VarConstantCommentRector::class,
+ VarToPublicPropertyRector::class,
+ VersionCompareFuncCallToConstantRector::class,
+ WrapEncapsedVariableInCurlyBracesRector::class,
+ ]);
+};
diff --git a/src/Contracts/Implementations/Psr17/FactoriesContract.php b/src/Contracts/Implementations/Psr17/FactoriesContract.php
new file mode 100644
index 0000000..4069eed
--- /dev/null
+++ b/src/Contracts/Implementations/Psr17/FactoriesContract.php
@@ -0,0 +1,38 @@
+add(CandidateEntity::create(
+ package: 'psr-mock/http-factory-implementation',
+ version: '^1.0',
+ builder: static fn (string $class = '\PsrMock\Psr17\RequestFactory'): object => new $class(),
+ ));
+
+ // nyholm/psr7 1.2+ is PSR-17 compatible.
+ self::$candidates->add(CandidateEntity::create(
+ package: 'nyholm/psr7',
+ version: '^1.2',
+ builder: static fn (string $class = '\Nyholm\Psr7\Factory\Psr17Factory'): object => new $class(),
+ ));
+
+ // guzzlehttp/psr7 1.6+ is PSR-17 compatible.
+ self::$candidates->add(CandidateEntity::create(
+ package: 'guzzlehttp/psr7',
+ version: '^1.6',
+ builder: static fn (string $class = '\GuzzleHttp\Psr7\HttpFactory'): object => new $class(),
+ ));
+
+ // zendframework/zend-diactoros 2.0+ is PSR-17 compatible. (Caution: Abandoned!)
+ self::$candidates->add(CandidateEntity::create(
+ package: 'zendframework/zend-diactoros',
+ version: '^2.0',
+ builder: static fn (string $class = '\Zend\Diactoros\RequestFactory'): object => new $class(),
+ ));
+
+ // http-interop/http-factory-guzzle 1.0+ is PSR-17 compatible.
+ self::$candidates->add(CandidateEntity::create(
+ package: 'http-interop/http-factory-guzzle',
+ version: '^1.0',
+ builder: static fn (string $class = '\Http\Factory\Guzzle\RequestFactory'): object => new $class(),
+ ));
+
+ // laminas/laminas-diactoros 2.0+ is PSR-17 compatible
+ self::$candidates->add(CandidateEntity::create(
+ package: 'laminas/laminas-diactoros',
+ version: '^2.0',
+ builder: static fn (string $class = '\Laminas\Diactoros\RequestFactory'): object => new $class(),
+ ));
+
+ // slim/psr7 1.0+ is PSR-17 compatible.
+ self::$candidates->add(CandidateEntity::create(
+ package: 'slim/psr7',
+ version: '^1.0',
+ builder: static fn (string $class = '\Slim\Psr7\Factory\RequestFactory'): object => new $class(),
+ ));
+
+ // typo3/core 10.0+ is PSR-17 compatible.
+ self::$candidates->add(CandidateEntity::create(
+ package: 'typo3/core',
+ version: '^10.0',
+ builder: static fn (string $class = '\TYPO3\CMS\Core\Http\RequestFactory'): object => new $class(),
+ ));
+
+ // nimbly/capsule 1.0+ is PSR-17 compatible.
+ self::$candidates->add(CandidateEntity::create(
+ package: 'nimbly/capsule',
+ version: '^1.0',
+ builder: static fn (string $class = '\Nimbly\Capsule\Factory\RequestFactory'): object => new $class(),
+ ));
+
+ // httpsoft/http-message 1.0+ is PSR-17 compatible.
+ self::$candidates->add(CandidateEntity::create(
+ package: 'httpsoft/http-message',
+ version: '^1.0',
+ builder: static fn (string $class = '\HttpSoft\Message\RequestFactory'): object => new $class(),
+ ));
+
+ return self::$candidates;
+ }
+
+ /**
+ * @psalm-suppress MoreSpecificReturnType,LessSpecificReturnStatement
+ */
+ public static function discover(): ?RequestFactoryInterface
+ {
+ if (null !== self::$using) {
+ return self::$using;
+ }
+
+ return Discover::httpRequestFactory();
+ }
+
+ public static function prefer(string $package): void
+ {
+ self::$candidates ??= CandidatesCollection::create();
+ parent::prefer($package);
+ self::use(null);
+ }
+
+ public static function set(CandidatesCollection $candidates): void
+ {
+ self::$candidates ??= CandidatesCollection::create();
+ parent::set($candidates);
+ self::use(null);
+ }
+
+ public static function singleton(): ?RequestFactoryInterface
+ {
+ if (null !== self::$using) {
+ return self::$using;
+ }
+
+ return self::$singleton ??= self::discover();
+ }
+
+ public static function use(?RequestFactoryInterface $instance): void
+ {
+ self::$singleton = $instance;
+ self::$using = $instance;
+ }
+}
diff --git a/src/Implementations/Psr17/ResponseFactories.php b/src/Implementations/Psr17/ResponseFactories.php
new file mode 100644
index 0000000..4fd3fc2
--- /dev/null
+++ b/src/Implementations/Psr17/ResponseFactories.php
@@ -0,0 +1,150 @@
+add(CandidateEntity::create(
+ package: 'psr-mock/http-factory-implementation',
+ version: '^1.0',
+ builder: static fn (string $class = '\PsrMock\Psr17\ResponseFactory'): object => new $class(),
+ ));
+
+ // nyholm/psr7 1.2+ is PSR-17 compatible.
+ self::$candidates->add(CandidateEntity::create(
+ package: 'nyholm/psr7',
+ version: '^1.2',
+ builder: static fn (string $class = '\Nyholm\Psr7\Factory\Psr17Factory'): object => new $class(),
+ ));
+
+ // guzzlehttp/psr7 1.6+ is PSR-17 compatible.
+ self::$candidates->add(CandidateEntity::create(
+ package: 'guzzlehttp/psr7',
+ version: '^1.6',
+ builder: static fn (string $class = '\GuzzleHttp\Psr7\HttpFactory'): object => new $class(),
+ ));
+
+ // zendframework/zend-diactoros 2.0+ is PSR-17 compatible. (Caution: Abandoned!)
+ self::$candidates->add(CandidateEntity::create(
+ package: 'zendframework/zend-diactoros',
+ version: '^2.0',
+ builder: static fn (string $class = '\Zend\Diactoros\ResponseFactory'): object => new $class(),
+ ));
+
+ // http-interop/http-factory-guzzle 1.0+ is PSR-17 compatible.
+ self::$candidates->add(CandidateEntity::create(
+ package: 'http-interop/http-factory-guzzle',
+ version: '^1.0',
+ builder: static fn (string $class = '\Http\Factory\Guzzle\ResponseFactory'): object => new $class(),
+ ));
+
+ // laminas/laminas-diactoros 2.0+ is PSR-17 compatible
+ self::$candidates->add(CandidateEntity::create(
+ package: 'laminas/laminas-diactoros',
+ version: '^2.0',
+ builder: static fn (string $class = '\Laminas\Diactoros\ResponseFactory'): object => new $class(),
+ ));
+
+ // slim/psr7 1.0+ is PSR-17 compatible.
+ self::$candidates->add(CandidateEntity::create(
+ package: 'slim/psr7',
+ version: '^1.0',
+ builder: static fn (string $class = '\Slim\Psr7\Factory\ResponseFactory'): object => new $class(),
+ ));
+
+ // typo3/core 10.0+ is PSR-17 compatible.
+ self::$candidates->add(CandidateEntity::create(
+ package: 'typo3/core',
+ version: '^10.0',
+ builder: static fn (string $class = '\TYPO3\CMS\Core\Http\ResponseFactory'): object => new $class(),
+ ));
+
+ // nimbly/capsule 1.0+ is PSR-17 compatible.
+ self::$candidates->add(CandidateEntity::create(
+ package: 'nimbly/capsule',
+ version: '^1.0',
+ builder: static fn (string $class = '\Nimbly\Capsule\Factory\ResponseFactory'): object => new $class(),
+ ));
+
+ // httpsoft/http-message 1.0+ is PSR-17 compatible.
+ self::$candidates->add(CandidateEntity::create(
+ package: 'httpsoft/http-message',
+ version: '^1.0',
+ builder: static fn (string $class = '\HttpSoft\Message\ResponseFactory'): object => new $class(),
+ ));
+
+ return self::$candidates;
+ }
+
+ /**
+ * @psalm-suppress MoreSpecificReturnType,LessSpecificReturnStatement
+ */
+ public static function discover(): ?ResponseFactoryInterface
+ {
+ if (null !== self::$using) {
+ return self::$using;
+ }
+
+ return Discover::httpResponseFactory();
+ }
+
+ public static function prefer(string $package): void
+ {
+ self::$candidates ??= CandidatesCollection::create();
+ parent::prefer($package);
+ self::use(null);
+ }
+
+ public static function set(CandidatesCollection $candidates): void
+ {
+ self::$candidates ??= CandidatesCollection::create();
+ parent::set($candidates);
+ self::use(null);
+ }
+
+ public static function singleton(): ?ResponseFactoryInterface
+ {
+ if (null !== self::$using) {
+ return self::$using;
+ }
+
+ return self::$singleton ??= self::discover();
+ }
+
+ public static function use(?ResponseFactoryInterface $instance): void
+ {
+ self::$singleton = $instance;
+ self::$using = $instance;
+ }
+}
diff --git a/src/Implementations/Psr17/StreamFactories.php b/src/Implementations/Psr17/StreamFactories.php
new file mode 100644
index 0000000..384d7b5
--- /dev/null
+++ b/src/Implementations/Psr17/StreamFactories.php
@@ -0,0 +1,150 @@
+add(CandidateEntity::create(
+ package: 'psr-mock/http-factory-implementation',
+ version: '^1.0',
+ builder: static fn (string $class = '\PsrMock\Psr17\StreamFactory'): object => new $class(),
+ ));
+
+ // nyholm/psr7 1.2+ is PSR-17 compatible.
+ self::$candidates->add(CandidateEntity::create(
+ package: 'nyholm/psr7',
+ version: '^1.2',
+ builder: static fn (string $class = '\Nyholm\Psr7\Factory\Psr17Factory'): object => new $class(),
+ ));
+
+ // guzzlehttp/psr7 1.6+ is PSR-17 compatible.
+ self::$candidates->add(CandidateEntity::create(
+ package: 'guzzlehttp/psr7',
+ version: '^1.6',
+ builder: static fn (string $class = '\GuzzleHttp\Psr7\HttpFactory'): object => new $class(),
+ ));
+
+ // zendframework/zend-diactoros 2.0+ is PSR-17 compatible. (Caution: Abandoned!)
+ self::$candidates->add(CandidateEntity::create(
+ package: 'zendframework/zend-diactoros',
+ version: '^2.0',
+ builder: static fn (string $class = '\Zend\Diactoros\StreamFactory'): object => new $class(),
+ ));
+
+ // http-interop/http-factory-guzzle 1.0+ is PSR-17 compatible.
+ self::$candidates->add(CandidateEntity::create(
+ package: 'http-interop/http-factory-guzzle',
+ version: '^1.0',
+ builder: static fn (string $class = '\Http\Factory\Guzzle\StreamFactory'): object => new $class(),
+ ));
+
+ // laminas/laminas-diactoros 2.0+ is PSR-17 compatible
+ self::$candidates->add(CandidateEntity::create(
+ package: 'laminas/laminas-diactoros',
+ version: '^2.0',
+ builder: static fn (string $class = '\Laminas\Diactoros\StreamFactory'): object => new $class(),
+ ));
+
+ // slim/psr7 1.0+ is PSR-17 compatible.
+ self::$candidates->add(CandidateEntity::create(
+ package: 'slim/psr7',
+ version: '^1.0',
+ builder: static fn (string $class = '\Slim\Psr7\Factory\StreamFactory'): object => new $class(),
+ ));
+
+ // typo3/core 10.0+ is PSR-17 compatible.
+ self::$candidates->add(CandidateEntity::create(
+ package: 'typo3/core',
+ version: '^10.0',
+ builder: static fn (string $class = '\TYPO3\CMS\Core\Http\StreamFactory'): object => new $class(),
+ ));
+
+ // nimbly/capsule 1.0+ is PSR-17 compatible.
+ self::$candidates->add(CandidateEntity::create(
+ package: 'nimbly/capsule',
+ version: '^1.0',
+ builder: static fn (string $class = '\Nimbly\Capsule\Factory\StreamFactory'): object => new $class(),
+ ));
+
+ // httpsoft/http-message 1.0+ is PSR-17 compatible.
+ self::$candidates->add(CandidateEntity::create(
+ package: 'httpsoft/http-message',
+ version: '^1.0',
+ builder: static fn (string $class = '\HttpSoft\Message\StreamFactory'): object => new $class(),
+ ));
+
+ return self::$candidates;
+ }
+
+ /**
+ * @psalm-suppress MoreSpecificReturnType,LessSpecificReturnStatement
+ */
+ public static function discover(): ?StreamFactoryInterface
+ {
+ if (null !== self::$using) {
+ return self::$using;
+ }
+
+ return Discover::httpStreamFactory();
+ }
+
+ public static function prefer(string $package): void
+ {
+ self::$candidates ??= CandidatesCollection::create();
+ parent::prefer($package);
+ self::use(null);
+ }
+
+ public static function set(CandidatesCollection $candidates): void
+ {
+ self::$candidates ??= CandidatesCollection::create();
+ parent::set($candidates);
+ self::use(null);
+ }
+
+ public static function singleton(): ?StreamFactoryInterface
+ {
+ if (null !== self::$using) {
+ return self::$using;
+ }
+
+ return self::$singleton ??= self::discover();
+ }
+
+ public static function use(?StreamFactoryInterface $instance): void
+ {
+ self::$singleton = $instance;
+ self::$using = $instance;
+ }
+}