commit 1295f234e43499d27d5b53b39da3fe3508d77320 Author: Christian Todt Date: Wed Sep 16 14:22:55 2026 +0200 Add IBAN validator with QA workflows diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ef0f717 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,262 @@ +# Do not search in parent directories. +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 120 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_visual_guides = 80,120 + +[composer.{json,lock}] +indent_size = 4 + +[{*.php}] +ij_php_align_assignments = false +ij_php_align_class_constants = false +ij_php_align_enum_cases = false +ij_php_align_group_field_declarations = false +ij_php_align_inline_comments = false +ij_php_align_key_value_pairs = false +ij_php_align_match_arm_bodies = false +ij_php_align_multiline_array_initializer_expression = false +ij_php_align_multiline_binary_operation = false +ij_php_align_multiline_chained_methods = false +ij_php_align_multiline_extends_list = false +ij_php_align_multiline_for = true +ij_php_align_multiline_parameters = false +ij_php_align_multiline_parameters_in_calls = false +ij_php_align_multiline_ternary_operation = false +ij_php_align_named_arguments = false +ij_php_align_phpdoc_comments = false +ij_php_align_phpdoc_param_names = false +ij_php_anonymous_brace_style = end_of_line +ij_php_api_weight = 28 +ij_php_array_initializer_new_line_after_left_brace = true +ij_php_array_initializer_right_brace_on_new_line = true +ij_php_array_initializer_wrap = on_every_item +ij_php_assignment_wrap = normal +ij_php_attributes_wrap = normal +ij_php_author_weight = 28 +ij_php_binary_operation_sign_on_next_line = false +ij_php_binary_operation_wrap = normal +ij_php_blank_lines_after_class_header = 1 +ij_php_blank_lines_after_function = 1 +ij_php_blank_lines_after_imports = 1 +ij_php_blank_lines_after_opening_tag = 0 +ij_php_blank_lines_after_package = 1 +ij_php_blank_lines_around_class = 1 +ij_php_blank_lines_around_constants = 1 +ij_php_blank_lines_around_enum_cases = 0 +ij_php_blank_lines_around_field = 1 +ij_php_blank_lines_around_method = 1 +ij_php_blank_lines_before_class_end = 1 +ij_php_blank_lines_before_imports = 1 +ij_php_blank_lines_before_method_body = 0 +ij_php_blank_lines_before_package = 1 +ij_php_blank_lines_before_return_statement = 1 +ij_php_blank_lines_between_imports = 0 +ij_php_block_brace_style = end_of_line +ij_php_call_parameters_new_line_after_left_paren = true +ij_php_call_parameters_right_paren_on_new_line = true +ij_php_call_parameters_wrap = on_every_item +ij_php_catch_on_new_line = true +ij_php_category_weight = 28 +ij_php_class_brace_style = end_of_line +ij_php_comma_after_last_argument = false +ij_php_comma_after_last_array_element = true +ij_php_comma_after_last_closure_use_var = false +ij_php_comma_after_last_match_arm = false +ij_php_comma_after_last_parameter = false +ij_php_concat_spaces = true +ij_php_copyright_weight = 28 +ij_php_deprecated_weight = 4 +ij_php_do_while_brace_force = always +ij_php_else_if_style = as_is +ij_php_else_on_new_line = true +ij_php_example_weight = 28 +ij_php_extends_keyword_wrap = off +ij_php_extends_list_wrap = off +ij_php_fields_default_visibility = private +ij_php_filesource_weight = 28 +ij_php_finally_on_new_line = true +ij_php_for_brace_force = always +ij_php_for_statement_new_line_after_left_paren = false +ij_php_for_statement_right_paren_on_new_line = false +ij_php_for_statement_wrap = off +ij_php_force_empty_methods_in_one_line = false +ij_php_force_short_declaration_array_style = true +ij_php_getters_setters_naming_style = camel_case +ij_php_getters_setters_order_style = getters_first +ij_php_global_weight = 28 +ij_php_group_use_wrap = on_every_item +ij_php_if_brace_force = always +ij_php_if_lparen_on_next_line = false +ij_php_if_rparen_on_next_line = false +ij_php_ignore_weight = 28 +ij_php_import_sorting = alphabetic +ij_php_indent_break_from_case = true +ij_php_indent_case_from_switch = true +ij_php_indent_code_in_php_tags = false +ij_php_internal_weight = 28 +ij_php_keep_blank_lines_after_lbrace = 1 +ij_php_keep_blank_lines_before_right_brace = 1 +ij_php_keep_blank_lines_in_code = 1 +ij_php_keep_blank_lines_in_declarations = 1 +ij_php_keep_control_statement_in_one_line = false +ij_php_keep_first_column_comment = false +ij_php_keep_indents_on_empty_lines = false +ij_php_keep_line_breaks = false +ij_php_keep_rparen_and_lbrace_on_one_line = true +ij_php_keep_simple_classes_in_one_line = false +ij_php_keep_simple_methods_in_one_line = false +ij_php_lambda_brace_style = end_of_line +ij_php_license_weight = 28 +ij_php_line_comment_add_space = false +ij_php_line_comment_at_first_column = true +ij_php_link_weight = 28 +ij_php_lower_case_boolean_const = false +ij_php_lower_case_keywords = true +ij_php_lower_case_null_const = false +ij_php_method_brace_style = end_of_line +ij_php_method_call_chain_wrap = on_every_item +ij_php_method_parameters_new_line_after_left_paren = true +ij_php_method_parameters_right_paren_on_new_line = true +ij_php_method_parameters_wrap = on_every_item +ij_php_method_weight = 28 +ij_php_modifier_list_wrap = false +ij_php_multiline_chained_calls_semicolon_on_new_line = true +ij_php_namespace_brace_style = 1 +ij_php_new_line_after_php_opening_tag = true +ij_php_null_type_position = in_the_end +ij_php_package_weight = 28 +ij_php_param_weight = 1 +ij_php_parameters_attributes_wrap = normal +ij_php_parentheses_expression_new_line_after_left_paren = false +ij_php_parentheses_expression_right_paren_on_new_line = false +ij_php_phpdoc_blank_line_before_tags = true +ij_php_phpdoc_blank_lines_around_parameters = true +ij_php_phpdoc_keep_blank_lines = true +ij_php_phpdoc_param_spaces_between_name_and_description = 1 +ij_php_phpdoc_param_spaces_between_tag_and_type = 1 +ij_php_phpdoc_param_spaces_between_type_and_name = 1 +ij_php_phpdoc_use_fqcn = true +ij_php_phpdoc_wrap_long_lines = true +ij_php_place_assignment_sign_on_next_line = false +ij_php_place_parens_for_constructor = 1 +ij_php_property_read_weight = 28 +ij_php_property_weight = 28 +ij_php_property_write_weight = 28 +ij_php_return_type_on_new_line = false +ij_php_return_weight = 2 +ij_php_see_weight = 5 +ij_php_since_weight = 28 +ij_php_sort_phpdoc_elements = true +ij_php_space_after_colon = true +ij_php_space_after_colon_in_enum_backed_type = true +ij_php_space_after_colon_in_named_argument = true +ij_php_space_after_colon_in_return_type = true +ij_php_space_after_comma = true +ij_php_space_after_for_semicolon = true +ij_php_space_after_quest = true +ij_php_space_after_type_cast = true +ij_php_space_after_unary_not = false +ij_php_space_before_array_initializer_left_brace = false +ij_php_space_before_catch_keyword = true +ij_php_space_before_catch_left_brace = true +ij_php_space_before_catch_parentheses = true +ij_php_space_before_class_left_brace = true +ij_php_space_before_closure_left_parenthesis = true +ij_php_space_before_colon = true +ij_php_space_before_colon_in_enum_backed_type = false +ij_php_space_before_colon_in_named_argument = false +ij_php_space_before_colon_in_return_type = false +ij_php_space_before_comma = false +ij_php_space_before_do_left_brace = true +ij_php_space_before_else_keyword = true +ij_php_space_before_else_left_brace = true +ij_php_space_before_finally_keyword = true +ij_php_space_before_finally_left_brace = true +ij_php_space_before_for_left_brace = true +ij_php_space_before_for_parentheses = true +ij_php_space_before_for_semicolon = false +ij_php_space_before_if_left_brace = true +ij_php_space_before_if_parentheses = true +ij_php_space_before_method_call_parentheses = false +ij_php_space_before_method_left_brace = true +ij_php_space_before_method_parentheses = false +ij_php_space_before_quest = true +ij_php_space_before_short_closure_left_parenthesis = false +ij_php_space_before_switch_left_brace = true +ij_php_space_before_switch_parentheses = true +ij_php_space_before_try_left_brace = true +ij_php_space_before_unary_not = false +ij_php_space_before_while_keyword = true +ij_php_space_before_while_left_brace = true +ij_php_space_before_while_parentheses = true +ij_php_space_between_ternary_quest_and_colon = false +ij_php_spaces_around_additive_operators = true +ij_php_spaces_around_arrow = false +ij_php_spaces_around_assignment_in_declare = true +ij_php_spaces_around_assignment_operators = true +ij_php_spaces_around_bitwise_operators = true +ij_php_spaces_around_equality_operators = true +ij_php_spaces_around_logical_operators = true +ij_php_spaces_around_multiplicative_operators = true +ij_php_spaces_around_null_coalesce_operator = true +ij_php_spaces_around_pipe_in_union_type = false +ij_php_spaces_around_relational_operators = true +ij_php_spaces_around_shift_operators = true +ij_php_spaces_around_unary_operator = false +ij_php_spaces_around_var_within_brackets = false +ij_php_spaces_within_array_initializer_braces = false +ij_php_spaces_within_brackets = false +ij_php_spaces_within_catch_parentheses = false +ij_php_spaces_within_for_parentheses = false +ij_php_spaces_within_if_parentheses = false +ij_php_spaces_within_method_call_parentheses = false +ij_php_spaces_within_method_parentheses = false +ij_php_spaces_within_parentheses = false +ij_php_spaces_within_short_echo_tags = true +ij_php_spaces_within_switch_parentheses = false +ij_php_spaces_within_while_parentheses = false +ij_php_special_else_if_treatment = false +ij_php_subpackage_weight = 28 +ij_php_ternary_operation_signs_on_next_line = true +ij_php_ternary_operation_wrap = on_every_item +ij_php_throws_weight = 3 +ij_php_todo_weight = 6 +ij_php_treat_multiline_arrays_and_lambdas_multiline = false +ij_php_unknown_tag_weight = 28 +ij_php_upper_case_boolean_const = true +ij_php_upper_case_null_const = true +ij_php_uses_weight = 28 +ij_php_var_weight = 0 +ij_php_variable_naming_style = camel_case +ij_php_version_weight = 28 +ij_php_while_brace_force = always +ij_php_while_on_new_line = false + +[{*.neon,*.neon.dist,*neon.template}] +indent_style = tab +tab_width = 4 + +[{*.yaml,*.yml}] +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_line_comment_add_space = false +ij_yaml_line_comment_add_space_on_reformat = false +ij_yaml_line_comment_at_first_column = false +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/.github/workflows/phpcs.yml b/.github/workflows/phpcs.yml new file mode 100644 index 0000000..b2f5f58 --- /dev/null +++ b/.github/workflows/phpcs.yml @@ -0,0 +1,55 @@ +name: PHP_CodeSniffer + +permissions: {} + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + paths: + - '**.php' + - tools/phpcs/composer.json + - phpcs.xml.dist + - .github/workflows/phpcs.yml + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + phpcs: + runs-on: ubuntu-latest + name: PHP_CodeSniffer + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: 8.5 + coverage: none + tools: cs2pr + env: + fail-fast: true + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('tools/phpcs/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer composer-phpcs -- update --no-progress --prefer-dist + + - name: Run PHP_CodeSniffer + run: composer phpcs -- -q --report=checkstyle | cs2pr diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..0715ae2 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,83 @@ +name: PHPStan + +permissions: {} + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + paths: + - '**.php' + - composer.json + - tools/phpstan/composer.json + - phpstan.neon.dist + - .github/workflows/phpstan.yml + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + phpstan: + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental }} + strategy: + fail-fast: false + matrix: + php-versions: ['8.1', '8.5'] + prefer: ['prefer-stable', 'prefer-lowest'] + experimental: [false] + include: + - php-versions: 8.6 + prefer: prefer-source + experimental: true + name: PHPStan with PHP ${{ matrix.php-versions }} ${{ matrix.prefer }} + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: ${{ matrix.php-versions }} + coverage: none + env: + fail-fast: true + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ matrix.prefer }}-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer-${{ matrix.prefer }}- + + - name: Install dependencies + env: + MATRIX_EXPERIMENTAL: ${{ matrix.experimental }} + MATRIX_PREFER: ${{ matrix.prefer }} + run: | + if [ "${MATRIX_PREFER}" = "prefer-lowest" ]; then + export COMPOSER_NO_BLOCKING=1 + fi + + if [ "${MATRIX_EXPERIMENTAL}" == "true" ]; then + sed -E -i composer.json \ + -e 's/"minimum-stability":[^,]*/"minimum-stability": "dev"/' \ + -e 's/"prefer-stable":[^,]*/"prefer-stable": false/' + export COMPOSER_IGNORE_PLATFORM_REQ="php+" + fi + + composer update --no-progress --${MATRIX_PREFER} --optimize-autoloader + composer composer-phpunit -- update --no-progress --optimize-autoloader + composer composer-phpstan -- update --no-progress --optimize-autoloader + + - name: Run PHPStan + run: composer phpstan -- analyse diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 0000000..5d53828 --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,93 @@ +name: PHPUnit + +permissions: {} + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + paths: + - '**.php' + - composer.json + - tools/phpunit/composer.json + - phpunit.xml.dist + - .github/workflows/phpunit.yml + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + phpunit: + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental }} + strategy: + fail-fast: false + matrix: + php-versions: ['8.1', '8.5'] + prefer: ['prefer-stable', 'prefer-lowest'] + experimental: [false] + include: + - php-versions: 8.6 + prefer: prefer-source + experimental: true + name: PHPUnit with PHP ${{ matrix.php-versions }} ${{ matrix.prefer }} + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: ${{ matrix.php-versions }} + extensions: bcmath, curl, fileinfo, gd, intl, mbstring, mysqli, xml, zip + coverage: pcov + env: + fail-fast: true + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ matrix.prefer }}-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer-${{ matrix.prefer }}- + + - name: Install dependencies + env: + MATRIX_EXPERIMENTAL: ${{ matrix.experimental }} + MATRIX_PREFER: ${{ matrix.prefer }} + run: | + if [ "${MATRIX_PREFER}" = "prefer-lowest" ]; then + export COMPOSER_NO_BLOCKING=1 + fi + + if [ "${MATRIX_EXPERIMENTAL}" == "true" ]; then + sed -E -i composer.json \ + -e 's/"minimum-stability":[^,]*/"minimum-stability": "dev"/' \ + -e 's/"prefer-stable":[^,]*/"prefer-stable": false/' + export COMPOSER_IGNORE_PLATFORM_REQ="php+" + fi + + composer update --no-progress --${MATRIX_PREFER} --optimize-autoloader + composer composer-phpunit -- update --no-progress --optimize-autoloader + + - name: Setup problem matchers for PHP + env: + RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} + run: echo "::add-matcher::${RUNNER_TOOL_CACHE}/php.json" + + - name: Setup problem matchers for PHPUnit + env: + RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} + run: echo "::add-matcher::${RUNNER_TOOL_CACHE}/phpunit.json" + + - name: Run PHPUnit + run: composer phpunit diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml new file mode 100644 index 0000000..15e3c6b --- /dev/null +++ b/.github/workflows/smoke.yml @@ -0,0 +1,54 @@ +name: Smoke + +permissions: {} + +on: + workflow_dispatch: + inputs: + greeting: + description: 'Text passed from the first job to the second' + required: false + type: string + default: hello + push: + branches: + - main + +jobs: + produce: + runs-on: ubuntu-latest + name: Produce an output + outputs: + greeting: ${{ steps.greeting.outputs.value }} + + steps: + - name: Show the runner + run: | + uname -a + cat /etc/os-release + id + echo "event=${GITHUB_EVENT_NAME} ref=${GITHUB_REF} sha=${GITHUB_SHA}" + echo "server=${GITHUB_SERVER_URL} repository=${GITHUB_REPOSITORY}" + command -v sudo docker node php composer || true + + - name: Set the output + id: greeting + env: + INPUTS_GREETING: ${{ inputs.greeting || 'hello' }} + run: echo "value=${INPUTS_GREETING} from ${GITHUB_REPOSITORY}" >> $GITHUB_OUTPUT + + - name: Write the step summary + run: echo "### Smoke test ran on ${RUNNER_OS}" >> $GITHUB_STEP_SUMMARY + + consume: + runs-on: ubuntu-latest + name: Consume the output + needs: produce + + steps: + - name: Print the output of the first job + env: + GREETING: ${{ needs.produce.outputs.greeting }} + run: | + test -n "${GREETING}" + echo "${GREETING}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1115a60 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/.phpcs.cache +/.phpunit.result.cache +/.phpstan/ +/composer.lock +/phpstan.neon +/tools/*/vendor/ +/tools/*/composer.lock +/vendor/ diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..4762d6e --- /dev/null +++ b/composer.json @@ -0,0 +1,64 @@ +{ + "name": "systopia/pipelinetest", + "description": "Test project for GitHub workflows on Gitea Actions", + "type": "library", + "license": "AGPL-3.0-or-later", + "authors": [ + { + "name": "SYSTOPIA GmbH", + "email": "info@systopia.de", + "homepage": "https://www.systopia.de" + } + ], + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "sort-packages": true + }, + "require": { + "php": "^8.1" + }, + "autoload": { + "psr-4": { + "Systopia\\PipelineTest\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Systopia\\PipelineTest\\": "tests/" + } + }, + "scripts": { + "composer-phpcs": [ + "@composer --working-dir=tools/phpcs" + ], + "composer-phpstan": [ + "@composer --working-dir=tools/phpstan" + ], + "composer-phpunit": [ + "@composer --working-dir=tools/phpunit" + ], + "composer-tools": [ + "@composer-phpcs", + "@composer-phpstan", + "@composer-phpunit" + ], + "phpcs": [ + "@php tools/phpcs/vendor/bin/phpcs" + ], + "phpcbf": [ + "@php tools/phpcs/vendor/bin/phpcbf" + ], + "phpstan": [ + "@php tools/phpstan/vendor/bin/phpstan -v" + ], + "phpunit": [ + "@php tools/phpunit/vendor/bin/simple-phpunit --coverage-text" + ], + "test": [ + "@phpcs", + "@phpstan", + "@phpunit" + ] + } +} diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..17c4c16 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,78 @@ + + + CiviCRM coding standard with some additional changes + + src + tests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..abf3e08 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,24 @@ +parameters: + paths: + - src + - tests + scanFiles: + - tools/phpunit/vendor/bin/.phpunit/phpunit/src/Framework/TestCase.php + scanDirectories: + - tools/phpunit/vendor/bin/.phpunit/phpunit/src/Framework + bootstrapFiles: + - tools/phpunit/vendor/bin/.phpunit/phpunit/vendor/autoload.php + - vendor/autoload.php + level: 9 + checkTooWideReturnTypesInProtectedAndPublicMethods: true + checkUninitializedProperties: true + checkMissingCallableSignature: true + treatPhpDocTypesAsCertain: false + exceptions: + check: + missingCheckedExceptionInThrows: true + tooWideThrowType: true + checkedExceptionClasses: + - \Webmozart\Assert\InvalidArgumentException + implicitThrows: false + tmpDir: .phpstan diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..ca61cae --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,27 @@ + + + + + + + + + + + ./tests + + + + + + src + + + + diff --git a/src/IbanValidator.php b/src/IbanValidator.php new file mode 100644 index 0000000..34db5dc --- /dev/null +++ b/src/IbanValidator.php @@ -0,0 +1,51 @@ + 20, + 'BE' => 16, + 'CH' => 21, + 'DE' => 22, + 'ES' => 24, + 'FR' => 27, + 'IT' => 27, + 'LU' => 20, + 'NL' => 18, + ]; + + public function isValid(string $iban): bool { + $normalized = $this->normalize($iban); + + if (1 !== preg_match('/^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/', $normalized)) { + return FALSE; + } + + if (strlen($normalized) !== (self::LENGTHS[substr($normalized, 0, 2)] ?? NULL)) { + return FALSE; + } + + return 1 === $this->mod97(substr($normalized, 4) . substr($normalized, 0, 4)); + } + + public function normalize(string $iban): string { + return strtoupper(str_replace([' ', '-'], '', $iban)); + } + + private function mod97(string $rearranged): int { + $remainder = 0; + foreach (str_split($rearranged) as $character) { + $digits = ctype_digit($character) ? $character : (string) (ord($character) - 55); + foreach (str_split($digits) as $digit) { + $remainder = ($remainder * 10 + (int) $digit) % 97; + } + } + + return $remainder; + } + +} diff --git a/tests/IbanValidatorTest.php b/tests/IbanValidatorTest.php new file mode 100644 index 0000000..cf3b3dc --- /dev/null +++ b/tests/IbanValidatorTest.php @@ -0,0 +1,58 @@ +isValid($iban)); + } + + /** + * @return array + */ + public static function validIbans(): array { + return [ + 'germany' => ['DE89370400440532013000'], + 'grouped' => ['DE89 3704 0044 0532 0130 00'], + 'lowercase' => ['de89370400440532013000'], + 'austria' => ['AT611904300234573201'], + 'netherlands with letters' => ['NL91ABNA0417164300'], + ]; + } + + /** + * @dataProvider invalidIbans + */ + public function testAnInvalidIban(string $iban): void { + static::assertFalse((new IbanValidator())->isValid($iban)); + } + + /** + * @return array + */ + public static function invalidIbans(): array { + return [ + 'wrong checksum' => ['DE88370400440532013000'], + 'too short' => ['DE8937040044053201300'], + 'unknown country' => ['XX89370400440532013000'], + 'invalid character' => ['DE89370400440532013!00'], + 'empty' => [''], + ]; + } + + public function testNormalizeStripsSeparatorsAndUppercases(): void { + static::assertSame('DE89370400440532013000', (new IbanValidator())->normalize(' de89 3704-0044 0532 0130 00 ')); + } + +} diff --git a/tools/phpcs/composer.json b/tools/phpcs/composer.json new file mode 100644 index 0000000..980e4b9 --- /dev/null +++ b/tools/phpcs/composer.json @@ -0,0 +1,11 @@ +{ + "repositories": [ + { + "type": "git", + "url": "https://github.com/civicrm/coder.git" + } + ], + "require": { + "drupal/coder": "dev-8.x-2.x-civi" + } +} diff --git a/tools/phpstan/composer.json b/tools/phpstan/composer.json new file mode 100644 index 0000000..ce2fee2 --- /dev/null +++ b/tools/phpstan/composer.json @@ -0,0 +1,19 @@ +{ + "require": { + "kcs/phpstan-strict-rules": "^2", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2", + "phpstan/phpstan-beberlei-assert": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-phpunit": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpstan/phpstan-webmozart-assert": "^2", + "voku/phpstan-rules": "^3.6" + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true + }, + "sort-packages": true + } +} diff --git a/tools/phpunit/composer.json b/tools/phpunit/composer.json new file mode 100644 index 0000000..ab64ce0 --- /dev/null +++ b/tools/phpunit/composer.json @@ -0,0 +1,13 @@ +{ + "require": { + "symfony/phpunit-bridge": "^7" + }, + "scripts": { + "post-install-cmd": [ + "@php vendor/bin/simple-phpunit install --configuration ../../phpunit.xml.dist" + ], + "post-update-cmd": [ + "@php vendor/bin/simple-phpunit install --configuration ../../phpunit.xml.dist" + ] + } +}