diff --git a/.editorconfig b/.editorconfig index 5e9a93e..518c149 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,8 +10,25 @@ indent_style = space indent_size = 4 trim_trailing_whitespace = true +[*.js] +indent_size = 2 + [*.md] trim_trailing_whitespace = false +[*.php] +ij_php_space_before_short_closure_left_parenthesis = false +ij_php_space_after_type_cast = true + +[*.yaml] +indent_size = 2 + [*.yml] indent_size = 2 + +[*.xml.dist] +indent_size = 2 + +[LICENSE*] +indent_style = unset +indent_size = unset diff --git a/.gitattributes b/.gitattributes index 76da65a..fdf0ca4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -21,20 +21,24 @@ *.gif binary *.ttf binary -# Ignore some meta files when creating an archive of this repository -/.github export-ignore -/.editorconfig export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore -/.phpunit-watcher.yml export-ignore -/.scrutinizer.yml export-ignore -/.styleci.yml export-ignore -/infection.json.dist export-ignore -/phpunit.xml.dist export-ignore -/psalm.xml export-ignore -/tests export-ignore -/docs export-ignore - # Avoid merge conflicts in CHANGELOG # https://about.gitlab.com/2015/02/10/gitlab-reduced-merge-conflicts-by-90-percent-with-changelog-placeholders/ /CHANGELOG.md merge=union + +# Exclude files from the archive +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github export-ignore +/.gitignore export-ignore +/.styleci.yml export-ignore +/codeception.yml export-ignore +/composer-require-checker.json export-ignore +/docs export-ignore +/ecs.php export-ignore +/infection.json* export-ignore +/phpstan*.neon* export-ignore +/phpunit.xml.dist export-ignore +/psalm.xml export-ignore +/rector.php export-ignore +/runtime export-ignore +/tests export-ignore diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md deleted file mode 100644 index 1946f00..0000000 --- a/.github/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,102 +0,0 @@ -# Code of Conduct - -## Our Pledge - -As contributors and maintainers of this project, and in order to keep community open and welcoming, we ask to -respect all community members. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our community include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the overall community - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery, and sexual attention or advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting - -## Enforcement Responsibilities - -Core team members are responsible for clarifying and enforcing our standards of acceptable behavior and will take -appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Core team members have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, -issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for -moderation decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing -the community in public spaces. Examples of representing a project or community include using an official e-mail -address, posting via an official social media account, within project GitHub, official forum or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting core team members. All -complaints will be reviewed and investigated promptly and fairly. - -All core team members are obligated to respect the privacy and security of the reporter of any incident. - -## Enforcement Guidelines - -Core team members will follow these Community Impact Guidelines in determining the consequences for any action they -deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in -the community. - -**Consequence**: A private, written warning from core team members, providing clarity around the nature of the violation -and an explanation of why the behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series of actions. - -**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including -unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding -interactions in community spaces as well as external channels like social media. Violating these terms may lead to -a temporary or permanent ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified -period of time. No public or private interaction with the people involved, including unsolicited interaction with those -enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate -behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at -[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. - -Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. - -For answers to common questions about this code of conduct, see the FAQ at -[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at -[https://www.contributor-covenant.org/translations][translations]. - -[homepage]: https://www.contributor-covenant.org -[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html -[Mozilla CoC]: https://github.com/mozilla/diversity -[FAQ]: https://www.contributor-covenant.org/faq -[translations]: https://www.contributor-covenant.org/translations diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 7e17783..0000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,14 +0,0 @@ -### What steps will reproduce the problem? - -### What is the expected result? - -### What do you get instead? - - -### Additional info - -| Q | A -| ---------------- | --- -| Version | 1.0.? -| PHP version | -| Operating system | diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index cecccf6..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,6 +0,0 @@ -| Q | A -| ------------- | --- -| Is bugfix? | ✔️/❌ -| New feature? | ✔️/❌ -| Breaks BC? | ✔️/❌ -| Fixed issues | diff --git a/.github/ecs.yml b/.github/ecs.yml deleted file mode 100644 index dff0db6..0000000 --- a/.github/ecs.yml +++ /dev/null @@ -1,34 +0,0 @@ -on: - pull_request: - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' - - 'infection.json.dist' - - 'phpunit.xml.dist' - - push: - branches: ['main'] - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' - - 'infection.json.dist' - - 'phpunit.xml.dist' - -name: ecs - -jobs: - easy-coding-standard: - uses: php-forge/actions/.github/workflows/ecs.yml@main - secrets: - AUTH_TOKEN: ${{ secrets.AUTH_TOKEN }} - with: - os: >- - ['ubuntu-latest'] - php: >- - ['8.1'] diff --git a/.github/linters/actionlint.yml b/.github/linters/actionlint.yml new file mode 100644 index 0000000..328407a --- /dev/null +++ b/.github/linters/actionlint.yml @@ -0,0 +1,6 @@ +--- +paths: + .github/workflows/**/*.yml: + ignore: + - '"pull_request" section is alias node but mapping node is expected' + - '"push" section is alias node but mapping node is expected' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9b51512..9fef92b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,24 +1,22 @@ +--- on: - pull_request: + pull_request: &ignore-paths paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' + - ".gitattributes" + - ".gitignore" + - "CHANGELOG.md" + - "docs/**" + - "README.md" - push: - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' + push: *ignore-paths name: build +permissions: + contents: read + jobs: phpunit: - uses: php-forge/actions/.github/workflows/phpunit.yml@v1 + uses: yii2-framework/actions/.github/workflows/phpunit.yml@v1 secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/dependency-check.yml b/.github/workflows/dependency-check.yml index 48115b8..294a347 100644 --- a/.github/workflows/dependency-check.yml +++ b/.github/workflows/dependency-check.yml @@ -1,22 +1,21 @@ +--- on: - pull_request: + pull_request: &ignore-paths paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' + - ".gitattributes" + - ".gitignore" + - "CHANGELOG.md" + - "docs/**" + - "README.md" - push: - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' + push: *ignore-paths + +name: Composer require checker -name: dependency-check +permissions: + contents: read + pull-requests: write jobs: composer-require-checker: - uses: php-forge/actions/.github/workflows/composer-require-checker.yml@v1 + uses: yii2-framework/actions/.github/workflows/composer-require-checker.yml@v1 diff --git a/.github/workflows/ecs.yml b/.github/workflows/ecs.yml new file mode 100644 index 0000000..76f6a37 --- /dev/null +++ b/.github/workflows/ecs.yml @@ -0,0 +1,21 @@ +--- +on: + pull_request: &ignore-paths + paths-ignore: + - ".gitattributes" + - ".gitignore" + - "CHANGELOG.md" + - "docs/**" + - "README.md" + + push: *ignore-paths + +name: ecs + +permissions: + contents: read + pull-requests: write + +jobs: + easy-coding-standard: + uses: yii2-framework/actions/.github/workflows/ecs.yml@v1 diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 0000000..7b46557 --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,17 @@ +--- +on: + - pull_request + - push + +name: linter + +permissions: + checks: write + contents: read + statuses: write + +jobs: + linter: + uses: yii2-framework/actions/.github/workflows/super-linter.yml@v1 + secrets: + AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index 091ee70..91ef2b9 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -1,25 +1,24 @@ +--- on: - pull_request: + pull_request: &ignore-paths paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' + - ".gitattributes" + - ".gitignore" + - "CHANGELOG.md" + - "docs/**" + - "README.md" - push: - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' + push: *ignore-paths name: mutation test +permissions: + contents: read + pull-requests: write + jobs: mutation: - uses: php-forge/actions/.github/workflows/infection.yml@v1 + uses: yii2-framework/actions/.github/workflows/infection.yml@v1 with: phpstan: true secrets: diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 2e31d77..cc1c99d 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -1,22 +1,21 @@ +--- on: - pull_request: + pull_request: &ignore-paths paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' + - ".gitattributes" + - ".gitignore" + - "CHANGELOG.md" + - "docs/**" + - "README.md" - push: - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' + push: *ignore-paths name: static analysis +permissions: + contents: read + pull-requests: write + jobs: - psalm: - uses: php-forge/actions/.github/workflows/phpstan.yml@v1 + phpstan: + uses: yii2-framework/actions/.github/workflows/phpstan.yml@v1 diff --git a/.gitignore b/.gitignore index bb7ff36..d4c5c11 100644 --- a/.gitignore +++ b/.gitignore @@ -1,34 +1,51 @@ -# phpstorm project files -.idea +# codecoverage (if present) +code_coverage -# netbeans project files -nbproject +# codeception (if present) +c3.php -# zend studio for eclipse project files -.buildpath -.project -.settings +# composer +composer.lock -# windows thumbnail cache -Thumbs.db +# gitHub copilot config (if present) +.github/agents/** +.github/copilot-instructions.md +.github/copilot/** +.github/instructions/** +.github/prompts/** +.github/skills/** -# Mac DS_Store Files +# mac ds_store (if present) .DS_Store -# composer vendor dir -/vendor +# netbeans project (if present) +nbproject -# composer lock file -/composer.lock +# node_modules (if present) +node_modules +package-lock.json -# composer itself is not needed -composer.phar +# phpstorm project (if present) +.idea -# phpunit itself is not needed -phpunit.phar +# phpunit (if present) +.phpunit.cache +.phpunit.result.cache +phpunit.xml* -# local phpunit config -/phpunit.xml +# vagrant (if present) +.vagrant -# phpunit cache -.phpunit.result.cache +# vendor +vendor + +# vscode project (if present) +.vscode + +# windows thumbnail cache (if present) +Thumbs.db + +# zend studio for eclipse project (if present) +.buildpath +.project +.settings diff --git a/.styleci.yml b/.styleci.yml index 6245a5b..6cf29b2 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -1,86 +1,87 @@ preset: psr12 risky: true -version: 8 +version: 8.1 finder: - exclude: - - docs - - public - - vendor - - docker - - vargant + exclude: + - docs + - vendor enabled: - - alpha_ordered_traits - - array_indentation - - array_push - - combine_consecutive_issets - - combine_consecutive_unsets - - combine_nested_dirname - - declare_strict_types - - dir_constant - - fully_qualified_strict_types - - function_to_constant - - hash_to_slash_comment - - is_null - - logical_operators - - magic_constant_casing - - magic_method_casing - - method_separation - - modernize_types_casting - - native_function_casing - - native_function_type_declaration_casing - - no_alias_functions - - no_empty_comment - - no_empty_phpdoc - - no_empty_statement - - no_extra_block_blank_lines - - no_short_bool_cast - - no_superfluous_elseif - - no_unneeded_control_parentheses - - no_unneeded_curly_braces - - no_unneeded_final_method - - no_unset_cast - - no_unused_imports - - no_unused_lambda_imports - - no_useless_else - - no_useless_return - - normalize_index_brace - - php_unit_dedicate_assert - - php_unit_dedicate_assert_internal_type - - php_unit_expectation - - php_unit_mock - - php_unit_mock_short_will_return - - php_unit_namespaced - - php_unit_no_expectation_annotation - - phpdoc_no_empty_return - - phpdoc_no_useless_inheritdoc - - phpdoc_order - - phpdoc_property - - phpdoc_scalar - - phpdoc_separation - - phpdoc_singular_inheritdoc - - phpdoc_trim - - phpdoc_trim_consecutive_blank_line_separation - - phpdoc_type_to_var - - phpdoc_types - - phpdoc_types_order - - print_to_echo - - regular_callable_call - - return_assignment - - self_accessor - - self_static_accessor - - set_type_to_cast - - short_array_syntax - - short_list_syntax - - simplified_if_return - - single_quote - - standardize_not_equals - - ternary_to_null_coalescing - - trailing_comma_in_multiline_array - - unalign_double_arrow - - unalign_equals - - empty_loop_body_braces - - integer_literal_case - - union_type_without_spaces + - alpha_ordered_traits + - array_indentation + - array_push + - combine_consecutive_issets + - combine_consecutive_unsets + - combine_nested_dirname + - declare_strict_types + - dir_constant + - empty_loop_body_braces + - function_to_constant + - hash_to_slash_comment + - integer_literal_case + - is_null + - logical_operators + - magic_constant_casing + - magic_method_casing + - method_separation + - modernize_types_casting + - native_function_casing + - native_function_type_declaration_casing + - no_alias_functions + - no_empty_comment + - no_empty_phpdoc + - no_empty_statement + - no_extra_block_blank_lines + - no_short_bool_cast + - no_superfluous_elseif + - no_unneeded_control_parentheses + - no_unneeded_curly_braces + - no_unneeded_final_method + - no_unset_cast + - no_unused_imports + - no_unused_lambda_imports + - no_useless_else + - no_useless_return + - normalize_index_brace + - php_unit_dedicate_assert + - php_unit_dedicate_assert_internal_type + - php_unit_expectation + - php_unit_mock + - php_unit_mock_short_will_return + - php_unit_namespaced + - php_unit_no_expectation_annotation + - phpdoc_no_empty_return + - phpdoc_no_useless_inheritdoc + - phpdoc_order + - phpdoc_property + - phpdoc_scalar + - phpdoc_singular_inheritdoc + - phpdoc_trim + - phpdoc_trim_consecutive_blank_line_separation + - phpdoc_type_to_var + - phpdoc_types + - phpdoc_types_order + - print_to_echo + - regular_callable_call + - return_assignment + - self_accessor + - self_static_accessor + - set_type_to_cast + - short_array_syntax + - short_list_syntax + - simplified_if_return + - single_quote + - standardize_not_equals + - ternary_to_null_coalescing + - trailing_comma_in_multiline_array + - unalign_double_arrow + - unalign_equals + - union_type_without_spaces + +disabled: + - function_declaration + - new_with_parentheses + - psr12_braces + - psr12_class_definition diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dc0acc..de22dc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ -Change Log -========== +# ChangeLog ## 0.2.1 Under development +- Enh #13: Refactor codebase to improve performance (@terabytesoftw) + ## 0.2.0 August 18, 2025 - Bug #11: Refactor project structure and update dependencies (@terabytesoftw) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..efb41e4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +SPDX-License-Identifier: BSD-3-Clause + +BSD 3-Clause License + +Copyright (c) 2024, Terabytesoftw (https://github.com/terabytesoftw/) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index a76279a..0000000 --- a/LICENSE.md +++ /dev/null @@ -1,27 +0,0 @@ -# BSD 3-Clause License - -Copyright © 2008 by Terabytesoftw () -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. -* Neither the name of PHP Forge (Terabytesoftw) nor the names of its - contributors may be used to endorse or promote products derived from this - software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 7a2e317..36b9036 100644 --- a/README.md +++ b/README.md @@ -1,90 +1,61 @@ +

- + PHP Forge -

Support utilities for enhanced testing capabilities.

+

Support


+

- - PHP Version - - PHPUnit + PHPUnit - Infection - - - Static Analysis - + Mutation Testing + + + PHPStan +

-## Features - -✅ **Advanced Reflection Utilities** -- Access and modify private and protected properties via reflection. -- Invoke inaccessible methods to expand testing coverage. - -✅ **Cross-Platform String Assertions** -- Avoid false positives and negatives caused by Windows vs. Unix line ending differences. -- Normalize line endings for consistent string comparisons across platforms. - -✅ **File System Test Management** -- Recursively clean files and directories for isolated test environments. -- Safe removal that preserves Git-tracking files (for example, '.gitignore', '.gitkeep'). - -## Quick start +

+ Support utilities for PHPUnit-focused development
+ Reflection helpers, line ending normalization, and filesystem cleanup for deterministic tests. +

-### System requirements +## Features -- [`PHP`](https://www.php.net/downloads) 8.1 or higher. -- [`Composer`](https://getcomposer.org/download/) for dependency management. -- [`PHPUnit`](https://phpunit.de/) for testing framework integration. + + + Feature Overview + ### Installation -#### Method 1: Using [Composer](https://getcomposer.org/download/) (recommended) - -Install the extension. - ```bash -composer require --dev --prefer-dist php-forge/support:^0.2 -``` - -#### Method 2: Manual installation - -Add to your `composer.json`. - -```json -{ - "require-dev": { - "php-forge/support": "^0.2" - } -} +composer require php-forge/support:^0.2 --dev ``` -Then run. +### Quick start -```bash -composer update -``` +This package provides helper classes for PHPUnit tests. -## Basic Usage +It supports reflection-based access to non-public members, deterministic string comparisons across platforms, and filesystem cleanup for isolated test environments. -### Accessing private properties +#### Accessing private properties ```php name; + $attributeFragment = " data-size=\"{$normalized}\""; + + self::assertSame($expected, $attributeFragment, $message); + } +} +``` + +##### Enum instance output (non-HTML) + +```php +name, $normalized); } } ``` ## Documentation -For comprehensive testing guidance, see: +For detailed configuration options and advanced usage. + +- [Testing Guide](docs/testing.md) +- [Development Guide](docs/development.md) + +## Package information -- 🧪 [Testing Guide](docs/testing.md) +[![PHP](https://img.shields.io/badge/%3E%3D8.1-777BB4.svg?style=for-the-badge&logo=php&logoColor=white)](https://www.php.net/releases/8.1/en.php) +[![Latest Stable Version](https://img.shields.io/packagist/v/php-forge/support.svg?style=for-the-badge&logo=packagist&logoColor=white&label=Stable)](https://packagist.org/packages/php-forge/support) +[![Total Downloads](https://img.shields.io/packagist/dt/php-forge/support.svg?style=for-the-badge&logo=composer&logoColor=white&label=Downloads)](https://packagist.org/packages/php-forge/support) ## Quality code -[![Latest Stable Version](https://poser.pugx.org/php-forge/support/v)](https://github.com/php-forge/support/releases) -[![Total Downloads](https://poser.pugx.org/php-forge/support/downloads)](https://packagist.org/packages/php-forge/support) -[![codecov](https://codecov.io/gh/php-forge/support/graph/badge.svg?token=Upc4yA23YN)](https://codecov.io/gh/php-forge/support) -[![phpstan-level](https://img.shields.io/badge/PHPStan%20level-max-blue)](https://github.com/php-forge/support/actions/workflows/static.yml) -[![StyleCI](https://github.styleci.io/repos/661073468/shield?branch=main)](https://github.styleci.io/repos/661073468?branch=main) +[![Codecov](https://img.shields.io/codecov/c/github/php-forge/support.svg?style=for-the-badge&logo=codecov&logoColor=white&label=Coverage)](https://codecov.io/github/php-forge/support) +[![PHPStan Level Max](https://img.shields.io/badge/PHPStan-Level%20Max-4F5D95.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/php-forge/support/actions/workflows/static.yml) +[![Super-Linter](https://img.shields.io/github/actions/workflow/status/php-forge/support/linter.yml?style=for-the-badge&label=Super-Linter&logo=github)](https://github.com/php-forge/support/actions/workflows/linter.yml) +[![StyleCI](https://img.shields.io/badge/StyleCI-Passed-44CC11.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.styleci.io/repos/779611775?branch=main) ## Our social networks -[![X](https://img.shields.io/badge/follow-@terabytesoftw-1DA1F2?logo=x&logoColor=1DA1F2&labelColor=555555&style=flat)](https://x.com/Terabytesoftw) +[![Follow on X](https://img.shields.io/badge/-Follow%20on%20X-1DA1F2.svg?style=for-the-badge&logo=x&logoColor=white&labelColor=000000)](https://x.com/Terabytesoftw) ## License -[![License](https://img.shields.io/github/license/php-forge/support?cacheSeconds=0)](LICENSE.md) +[![License](https://img.shields.io/badge/License-BSD--3--Clause-brightgreen.svg?style=for-the-badge&logo=opensourceinitiative&logoColor=white&labelColor=555555)](LICENSE) diff --git a/composer.json b/composer.json index 9c8063d..ab02bba 100644 --- a/composer.json +++ b/composer.json @@ -14,13 +14,14 @@ "php": "^8.1" }, "require-dev": { - "infection/infection": "^0.27|^0.31", - "maglnet/composer-require-checker": "^4.7", + "infection/infection": "^0.27|^0.32", + "maglnet/composer-require-checker": "^4.1", + "phpstan/extension-installer": "^1.4", "phpstan/phpstan": "^2.1", "phpstan/phpstan-strict-rules": "^2.0.3", "phpunit/phpunit": "^10.5", - "rector/rector": "^2.1", - "symplify/easy-coding-standard": "^12.5" + "rector/rector": "^2.2", + "symplify/easy-coding-standard": "^13.0" }, "autoload": { "psr-4": { @@ -47,10 +48,20 @@ "scripts": { "check-dependencies": "./vendor/bin/composer-require-checker check", "ecs": "./vendor/bin/ecs --fix", - "mutation": "./vendor/bin/infection --threads=4 --ignore-msi-with-no-mutations --only-covered --min-msi=100 --min-covered-msi=100", - "mutation-static": "./vendor/bin/infection --threads=4 --ignore-msi-with-no-mutations --only-covered --min-msi=100 --min-covered-msi=100 --static-analysis-tool=phpstan", + "mutation": "./vendor/bin/infection --threads=4 --ignore-msi-with-no-mutations --min-msi=100 --min-covered-msi=100", + "mutation-static": "./vendor/bin/infection --threads=4 --ignore-msi-with-no-mutations --min-msi=100 --min-covered-msi=100 --static-analysis-tool=phpstan --static-analysis-tool-options='--memory-limit=-1'", "rector": "./vendor/bin/rector process src", - "static": "./vendor/bin/phpstan --memory-limit=512M", + "static": "./vendor/bin/phpstan --memory-limit=-1", + "sync-metadata": [ + "curl -fsSL -o .editorconfig https://raw.githubusercontent.com/yii2-extensions/template/main/.editorconfig", + "curl -fsSL -o .gitattributes https://raw.githubusercontent.com/yii2-extensions/template/main/.gitattributes", + "curl -fsSL -o .gitignore https://raw.githubusercontent.com/yii2-extensions/template/main/.gitignore", + "curl -fsSL -o ecs.php https://raw.githubusercontent.com/yii2-extensions/template/main/ecs.php", + "curl -fsSL -o infection.json5 https://raw.githubusercontent.com/yii2-extensions/template/main/infection.json5", + "curl -fsSL -o phpstan.neon https://raw.githubusercontent.com/yii2-extensions/template/main/phpstan.neon", + "curl -fsSL -o phpunit.xml.dist https://raw.githubusercontent.com/yii2-extensions/template/main/phpunit.xml.dist", + "curl -fsSL -o rector.php https://raw.githubusercontent.com/yii2-extensions/template/main/rector.php" + ], "tests": "./vendor/bin/phpunit" } } diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..dfb83e9 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,43 @@ +# Development + +This document describes development workflows and maintenance tasks for the project. + +## Sync Metadata + +To keep configuration files synchronized with the latest template updates, use the `sync-metadata` command. This command +downloads the latest configuration files from the template repository. + +```bash +composer run sync-metadata +``` + +### Updated Files + +This command updates the following configuration files: + +| File | Purpose | +| ------------------ | -------------------------------------------- | +| `.editorconfig` | Editor settings and code style configuration | +| `.gitattributes` | Git attributes and file handling rules | +| `.gitignore` | Git ignore patterns and exclusions | +| `ecs.php` | Easy Coding Standard configuration | +| `infection.json5` | Infection mutation testing configuration | +| `phpstan.neon` | PHPStan static analysis configuration | +| `phpunit.xml.dist` | PHPUnit test configuration | +| `rector.php` | Rector refactoring configuration | + +### When to Run + +Run this command in the following scenarios: + +- **Periodic Updates** - Monthly or quarterly to benefit from template improvements. +- **After Template Updates** - When the template repository has new configuration improvements. +- **Before Major Releases** - Ensure your project uses the latest best practices. +- **When Issues Occur** - If configuration files become outdated or incompatible. + +### Important Notes + +- This command overwrites existing configuration files with the latest versions from the template. +- Ensure you have committed any custom configuration changes before running this command. +- Review the updated files after syncing to ensure they work with your specific project needs. +- Some projects may require customizations after syncing configuration files. diff --git a/docs/svgs/features-mobile.svg b/docs/svgs/features-mobile.svg new file mode 100644 index 0000000..1ca7778 --- /dev/null +++ b/docs/svgs/features-mobile.svg @@ -0,0 +1,75 @@ + + + +
+ +
+
+

Advanced reflection utilities

+

Access and modify private or protected properties for controlled test setups.

+
+
+

Cross-platform string assertions

+

Normalize line endings to avoid Windows vs. Unix comparison differences.

+
+
+

Enum dataset generation

+

Generate deterministic datasets for enum-based attribute test cases via EnumDataProvider.

+
+
+

Filesystem test management

+

Remove test artifacts while preserving Git-tracked placeholders such as .gitignore and .gitkeep.

+
+
+

Invoke inaccessible methods

+

Call non-public methods to verify behavior without changing production visibility.

+
+
+

PHPUnit-focused helpers

+

Use helper classes to share test utilities across the suite.

+
+
+
+
+
diff --git a/docs/svgs/features.svg b/docs/svgs/features.svg new file mode 100644 index 0000000..b3a9610 --- /dev/null +++ b/docs/svgs/features.svg @@ -0,0 +1,72 @@ + + +
+ +
+
+

Advanced reflection utilities

+

Access and modify private or protected properties for controlled test setups.

+
+
+

Cross-platform string assertions

+

Normalize line endings to avoid Windows vs. Unix comparison differences.

+
+
+

Enum dataset generation

+

Generate deterministic datasets for enum-based attribute test cases via EnumDataProvider.

+
+
+

Filesystem test management

+

Remove test artifacts while preserving Git-tracked placeholders such as .gitignore and .gitkeep.

+
+
+

Invoke inaccessible methods

+

Call non-public methods to verify behavior without changing production visibility.

+
+
+

PHPUnit-focused helpers

+

Use helper classes to share test utilities across the suite.

+
+
+
+
+
diff --git a/docs/testing.md b/docs/testing.md index 2838bf5..c3f1ca8 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1,50 +1,73 @@ # Testing -## Checking dependencies +This package provides a consistent set of [Composer](https://getcomposer.org/) scripts for local validation. -This package uses [composer-require-checker](https://github.com/maglnet/ComposerRequireChecker) to check if all dependencies are correctly defined in `composer.json`. +Tool references: -To run the checker, execute the following command. +- [Composer Require Checker](https://github.com/maglnet/ComposerRequireChecker) for dependency definition checks. +- [Easy Coding Standard (ECS)](https://github.com/easy-coding-standard/easy-coding-standard) for coding standards. +- [Infection](https://infection.github.io/) for mutation testing. +- [PHPStan](https://phpstan.org/) for static analysis. +- [PHPUnit](https://phpunit.de/) for unit tests. -```shell -composer run check-dependencies +## Coding standards (ECS) + +Run Easy Coding Standard (ECS) and apply fixes. + +```bash +composer run ecs ``` -## Easy coding standard +## Dependency definition check -The code is checked with [Easy Coding Standard](https://github.com/easy-coding-standard/easy-coding-standard) and -[PHP CS Fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer). To run it. +Verify that runtime dependencies are correctly declared in `composer.json`. -```shell -composer run ecs +```bash +composer run check-dependencies ``` -## Mutation testing +## Mutation testing (Infection) -Mutation testing is checked with [Infection](https://infection.github.io/). To run it. +Run mutation testing. -```shell +```bash composer run mutation ``` -With PHPStan analysis, it will also check for static analysis issues during mutation testing. +Run mutation testing with static analysis enabled. -```shell +```bash composer run mutation-static ``` -## Static analysis +## Static analysis (PHPStan) -The code is statically analyzed with [PHPStan](https://phpstan.org/). To run static analysis. +Run static analysis. -```shell +```bash composer run static ``` -## Unit Tests +## Unit tests (PHPUnit) + +Run the full test suite. + +```bash +composer run tests +``` + +## Passing extra arguments + +Composer scripts support forwarding additional arguments using `--`. + +Example: run a specific PHPUnit test or filter by name. + +```bash +composer run tests -- --filter SvgTest +``` -The code is tested with [PHPUnit](https://phpunit.de/). To run tests. +Example: run PHPStan with a different memory limit: -```shell -composer run test +```bash +composer run static -- --memory-limit=512M ``` diff --git a/ecs.php b/ecs.php index 00781d8..726b0dd 100644 --- a/ecs.php +++ b/ecs.php @@ -2,13 +2,11 @@ declare(strict_types=1); -use PhpCsFixer\Fixer\ClassNotation\ClassDefinitionFixer; -use PhpCsFixer\Fixer\ClassNotation\OrderedClassElementsFixer; -use PhpCsFixer\Fixer\ClassNotation\OrderedTraitsFixer; -use PhpCsFixer\Fixer\ClassNotation\VisibilityRequiredFixer; -use PhpCsFixer\Fixer\Import\NoUnusedImportsFixer; -use PhpCsFixer\Fixer\Import\OrderedImportsFixer; +use PhpCsFixer\Fixer\ClassNotation\{ClassDefinitionFixer, OrderedClassElementsFixer, OrderedTraitsFixer}; +use PhpCsFixer\Fixer\Import\{NoUnusedImportsFixer, OrderedImportsFixer}; +use PhpCsFixer\Fixer\Phpdoc\PhpdocTypesOrderFixer; use PhpCsFixer\Fixer\StringNotation\SingleQuoteFixer; +use PhpCsFixer\Fixer\LanguageConstruct\NullableTypeDeclarationFixer; use Symplify\EasyCodingStandard\Config\ECSConfig; return ECSConfig::configure() @@ -33,7 +31,7 @@ 'construct', 'destruct', 'magic', - 'phpunit', + 'method_protected_abstract', 'method_public', 'method_protected', 'method_private', @@ -44,14 +42,19 @@ ->withConfiguredRule( OrderedImportsFixer::class, [ - 'imports_order' => ['class', 'function', 'const'], + 'imports_order' => [ + 'class', + 'function', + 'const', + ], 'sort_algorithm' => 'alpha', ], ) ->withConfiguredRule( - VisibilityRequiredFixer::class, + PhpdocTypesOrderFixer::class, [ - 'elements' => [], + 'sort_algorithm' => 'none', + 'null_adjustment' => 'always_last', ], ) ->withFileExtensions(['php']) @@ -61,7 +64,7 @@ __DIR__ . '/tests', ], ) - ->withPhpCsFixerSets(perCS20: true) + ->withPhpCsFixerSets(perCS30: true) ->withPreparedSets( cleanCode: true, comments: true, @@ -75,4 +78,9 @@ OrderedTraitsFixer::class, SingleQuoteFixer::class, ] + ) + ->withSkip( + [ + NullableTypeDeclarationFixer::class, + ] ); diff --git a/infection.json5 b/infection.json5 new file mode 100644 index 0000000..1d33ec4 --- /dev/null +++ b/infection.json5 @@ -0,0 +1,12 @@ +{ + $schema: "./vendor/infection/infection/resources/schema.json", + logs: { + text: "php://stderr", + stryker: { + report: "main", + }, + }, + source: { + directories: ["src"], + }, +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index e5d7313..521d51e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,24 +1,24 @@ - - - tests - - + + + tests + + - - - ./src - - + + + ./src + + diff --git a/src/DirectoryCleaner.php b/src/DirectoryCleaner.php new file mode 100644 index 0000000..7bcc327 --- /dev/null +++ b/src/DirectoryCleaner.php @@ -0,0 +1,77 @@ +getMessage($dirname), + ); + } + + while (($file = readdir($handle)) !== false) { + if ($file === '.' || $file === '..') { + continue; + } + + if ($file === '.gitignore' || $file === '.gitkeep') { + continue; + } + + $path = $basePath . DIRECTORY_SEPARATOR . $file; + + if (is_dir($path) && is_link($path) === false) { + self::clean($path); + @rmdir($path); + } else { + @unlink($path); + } + } + + closedir($handle); + } +} diff --git a/src/EnumDataProvider.php b/src/EnumDataProvider.php new file mode 100644 index 0000000..e7cb4d4 --- /dev/null +++ b/src/EnumDataProvider.php @@ -0,0 +1,100 @@ + $enumClass Enum class name implementing UnitEnum. + * + * @param string $enumClass Enum class name implementing UnitEnum. + * @param string|UnitEnum $attribute Attribute name used to build the expected fragment. + * @param bool $asHtml Whether to generate expected output as an attribute fragment or enum instance. Default is + * `true`. + * + * @return array Structured test cases indexed by a normalized enum value key. + * + * @phpstan-return array + */ + public static function attributeCases(string $enumClass, string|UnitEnum $attribute, bool $asHtml = true): array + { + $cases = []; + $attributeName = is_string($attribute) ? $attribute : sprintf('%s', self::normalizeValue($attribute)); + + foreach ($enumClass::cases() as $case) { + $normalizedValue = self::normalizeValue($case); + + $key = "enum: {$normalizedValue}"; + $expected = $asHtml ? " {$attributeName}=\"{$normalizedValue}\"" : $case; + $message = $asHtml + ? "Should return the '{$attributeName}' attribute value for enum case: {$normalizedValue}." + : "Should return the enum instance for case: {$normalizedValue}."; + + $cases[$key] = [ + $case, + [], + $expected, + $message, + ]; + } + + return $cases; + } + + /** + * Generates test cases for tag-related enum scenarios. + * + * Produces a dataset mapping descriptive keys to enum cases and their normalized string values, suitable for data + * provider methods in PHPUnit tests. + * + * @phpstan-param class-string $enumClass Enum class name implementing UnitEnum. + * + * @param string $enumClass Enum class name implementing UnitEnum. + * @param string $category Category label appended to the generated keys. + * + * @phpstan-return array + */ + public static function tagCases(string $enumClass, string $category): array + { + $data = []; + + foreach ($enumClass::cases() as $case) { + $value = self::normalizeValue($case); + $data[sprintf('%s %s tag', $value, $category)] = [$case, $value]; + } + + return $data; + } + + private static function normalizeValue(UnitEnum $enum): string + { + return match ($enum instanceof BackedEnum) { + true => (string) $enum->value, + false => $enum->name, + }; + } +} diff --git a/src/Exception/Message.php b/src/Exception/Message.php new file mode 100644 index 0000000..77d239a --- /dev/null +++ b/src/Exception/Message.php @@ -0,0 +1,59 @@ +getMessage('/path/to/dir')); + * ``` + */ + public function getMessage(int|string ...$argument): string + { + return sprintf($this->value, ...$argument); + } +} diff --git a/src/LineEndingNormalizer.php b/src/LineEndingNormalizer.php new file mode 100644 index 0000000..587d48b --- /dev/null +++ b/src/LineEndingNormalizer.php @@ -0,0 +1,43 @@ +getProperty($propertyName)->getValue($object); + } + + /** + * Retrieves a property value from a class or object. + * + * Uses reflection to read `$propertyName` from the provided class name or object instance. + * + * @param object|string $object Name of the class or object instance from which to retrieve the property value. + * @param string $propertyName Name of the property to access. + * + * @throws ReflectionException if the property does not exist. + * + * @return mixed Value of the specified property, or `null` when `$propertyName` is empty. + * + * @phpstan-param class-string|object $object + */ + public static function inaccessibleProperty(string|object $object, string $propertyName): mixed + { + $class = new ReflectionClass($object); + + if ($propertyName !== '') { + $property = $class->getProperty($propertyName); + $result = is_string($object) ? $property->getValue() : $property->getValue($object); + } + + return $result ?? null; + } + + /** + * Invokes a method via reflection. + * + * Uses reflection to invoke `$method` on `$object` with `$args`. + * + * @param object $object Object instance containing the method to invoke. + * @param string $method Name of the method to invoke. + * @param array $args Arguments to pass to the method invocation. + * + * @throws ReflectionException if the method does not exist. + * + * @return mixed Value of the invoked method, or `null` when `$method` is empty. + * + * @phpstan-param array $args + */ + public static function invokeMethod(object $object, string $method, array $args = []): mixed + { + $reflection = new ReflectionObject($object); + + if ($method !== '') { + $method = $reflection->getMethod($method); + $result = $method->invokeArgs($object, $args); + } + + return $result ?? null; + } + + /** + * Invokes a parent class method via reflection. + * + * Uses `$parentClass` as the declaring class context to invoke `$method` on `$object` with `$args`. + * + * @param object $object Object instance containing the method to invoke. + * @param string $parentClass Name of the parent class containing the method. + * @param string $method Name of the method to invoke. + * @param array $args Arguments to pass to the method invocation. + * + * @throws ReflectionException if the method does not exist. + * + * @return mixed Value of the invoked method, or `null` when `$method` is empty. + * + * @phpstan-param class-string $parentClass + * @phpstan-param array $args + */ + public static function invokeParentMethod( + object $object, + string $parentClass, + string $method, + array $args = [], + ): mixed { + $reflection = new ReflectionClass($parentClass); + + if ($method !== '') { + $method = $reflection->getMethod($method); + $result = $method->invokeArgs($object, $args); + } + + return $result ?? null; + } + + /** + * Sets a parent class property value via reflection. + * + * Uses `$parentClass` as the declaring class context to assign `$value` to `$propertyName` on `$object`. + * + * This method is a no-op when `$propertyName` is empty. + * + * @param object $object Object instance whose parent property will be set. + * @param string $parentClass Name of the parent class containing the property. + * @param string $propertyName Name of the property to set. + * @param mixed $value Value to assign to the property. + * + * @throws ReflectionException if the property does not exist. + * + * @phpstan-param class-string $parentClass + */ + public static function setInaccessibleParentProperty( + object $object, + string $parentClass, + string $propertyName, + mixed $value, + ): void { + $class = new ReflectionClass($parentClass); + + if ($propertyName !== '') { + $property = $class->getProperty($propertyName); + $property->setValue($object, $value); + } + + unset($class, $property); + } + + /** + * Sets a property value via reflection. + * + * Assigns `$value` to `$propertyName` on `$object`. + * + * This method is a no-op when `$propertyName` is empty. + * + * @param object $object Object instance whose property will be set. + * @param string $propertyName Name of the property to set. + * @param mixed $value Value to assign to the property. + * + * @throws ReflectionException if the property does not exist. + */ + public static function setInaccessibleProperty( + object $object, + string $propertyName, + mixed $value, + ): void { + $class = new ReflectionClass($object); + + if ($propertyName !== '') { + $property = $class->getProperty($propertyName); + $property->setValue($object, $value); + } + } +} diff --git a/src/TestSupport.php b/src/TestSupport.php deleted file mode 100644 index 567c9b2..0000000 --- a/src/TestSupport.php +++ /dev/null @@ -1,281 +0,0 @@ -getProperty($propertyName)->getValue($object); - } - - /** - * Retrieves the value of an inaccessible property from an object or class instance. - * - * Uses reflection to access the specified property of the given object or class, allowing tests to inspect or - * assert the value of private or protected properties that are otherwise inaccessible. - * - * This method is useful for verifying the internal state of objects during testing, especially when direct access - * to the property is not possible due to visibility constraints. - * - * @param object|string $object Name of the class or object instance from which to retrieve the property value. - * @param string $propertyName Name of the property to access. - * - * @throws ReflectionException if the property does not exist or is inaccessible. - * - * @return mixed Value of the specified property, or `null` if the property name is empty. - * - * @phpstan-param class-string|object $object - */ - public static function inaccessibleProperty(string|object $object, string $propertyName): mixed - { - $class = new ReflectionClass($object); - - if ($propertyName !== '') { - $property = $class->getProperty($propertyName); - $result = is_string($object) ? $property->getValue() : $property->getValue($object); - } - - return $result ?? null; - } - - /** - * Invokes an inaccessible method on the given object instance with the specified arguments. - * - * Uses reflection to access and invoke a private or protected method of the provided object, allowing tests to - * execute logic that is not publicly accessible. - * - * This is useful for verifying internal behavior or side effects during unit testing. - * - * @param object $object Object instance containing the method to invoke. - * @param string $method Name of the method to invoke. - * @param array $args Arguments to pass to the method invocation. - * - * @throws ReflectionException if the method does not exist or is inaccessible. - * - * @return mixed Value of the invoked method, or `null` if the method name is empty. - * - * @phpstan-param array $args - */ - public static function invokeMethod(object $object, string $method, array $args = []): mixed - { - $reflection = new ReflectionObject($object); - - if ($method !== '') { - $method = $reflection->getMethod($method); - $result = $method->invokeArgs($object, $args); - } - - return $result ?? null; - } - - /** - * Invokes an inaccessible method from a parent class on the given object instance with the specified arguments. - * - * Uses reflection to access and invoke a private or protected method defined in the specified parent class, - * allowing tests to execute logic that is not publicly accessible from the child class. - * - * This is useful for verifying inherited behavior or side effects during unit testing of subclasses. - * - * @param object $object Object instance containing the method to invoke. - * @param string $parentClass Name of the parent class containing the method. - * @param string $method Name of the method to invoke. - * @param array $args Arguments to pass to the method invocation. - * - * @throws ReflectionException if the method does not exist or is inaccessible in the parent class. - * - * @return mixed Value of the invoked method, or `null` if the method name is empty. - * - * @phpstan-param class-string $parentClass - * @phpstan-param array $args - */ - public static function invokeParentMethod( - object $object, - string $parentClass, - string $method, - array $args = [], - ): mixed { - $reflection = new ReflectionClass($parentClass); - - if ($method !== '') { - $method = $reflection->getMethod($method); - $result = $method->invokeArgs($object, $args); - } - - return $result ?? null; - } - - /** - * Normalizes line endings to Unix style ('\n') for cross-platform string assertions. - * - * Converts Windows style ('\r\n') line endings to Unix style ('\n') to ensure consistent string comparisons across - * different operating systems during testing. - * - * This method is useful for eliminating false negatives in assertions caused by platform-specific line endings. - * - * @param string $line Input string potentially containing Windows style line endings. - * - * @return string String with normalized Unix style line endings. - */ - public static function normalizeLineEndings(string $line): string - { - return str_replace(["\r\n", "\r"], "\n", $line); - } - - /** - * Removes all files and directories recursively from the specified base path, excluding '.gitignore' and - * '.gitkeep'. - * - * Opens the given directory, iterates through its contents, and removes all files and subdirectories except for - * special entries ('.', '..', '.gitignore', '.gitkeep'). - * - * Subdirectories are processed recursively before removal. - * - * @param string $basePath Absolute path to the directory whose contents will be removed. - * - * @throws RuntimeException if the directory cannot be opened for reading. - */ - public static function removeFilesFromDirectory(string $basePath): void - { - $handle = @opendir($basePath); - - if ($handle === false) { - $dirname = basename($basePath); - throw new RuntimeException("Unable to open directory: $dirname"); - } - - while (($file = readdir($handle)) !== false) { - if ($file === '.' || $file === '..' || $file === '.gitignore' || $file === '.gitkeep') { - continue; - } - - $path = $basePath . DIRECTORY_SEPARATOR . $file; - - if (is_dir($path)) { - self::removeFilesFromDirectory($path); - @rmdir($path); - } else { - @unlink($path); - } - } - - closedir($handle); - } - - /** - * Sets the value of an inaccessible property on a parent class instance. - * - * Uses reflection to assign the specified value to a private or protected property defined in the given parent - * class, enabling tests to modify internal state that is otherwise inaccessible due to visibility constraints in - * inheritance scenarios. - * - * This method is useful for testing scenarios that require direct manipulation of parent class internals. - * - * @param object $object Object instance whose parent property will be set. - * @param string $parentClass Name of the parent class containing the property. - * @param string $propertyName Name of the property to set. - * @param mixed $value Value to assign to the property. - * - * @throws ReflectionException if the property does not exist or is inaccessible in the parent class. - * - * @phpstan-param class-string $parentClass - */ - public static function setInaccessibleParentProperty( - object $object, - string $parentClass, - string $propertyName, - mixed $value, - ): void { - $class = new ReflectionClass($parentClass); - - if ($propertyName !== '') { - $property = $class->getProperty($propertyName); - $property->setValue($object, $value); - } - - unset($class, $property); - } - - /** - * Sets the value of an inaccessible property on the given object instance. - * - * Uses reflection to assign the specified value to a private or protected property of the provided object enabling - * tests to modify internal state that is otherwise inaccessible due to visibility constraints. - * - * This method is useful for testing scenarios that require direct manipulation of object internals. - * - * @param object $object Object instance whose property will be set. - * @param string $propertyName Name of the property to set. - * @param mixed $value Value to assign to the property. - * - * @throws ReflectionException if the property does not exist or is inaccessible. - */ - public static function setInaccessibleProperty( - object $object, - string $propertyName, - mixed $value, - ): void { - $class = new ReflectionClass($object); - - if ($propertyName !== '') { - $property = $class->getProperty($propertyName); - $property->setValue($object, $value); - } - - unset($class, $property); - } -} diff --git a/tests/DirectoryCleanerTest.php b/tests/DirectoryCleanerTest.php new file mode 100644 index 0000000..ed3cfb5 --- /dev/null +++ b/tests/DirectoryCleanerTest.php @@ -0,0 +1,71 @@ +expectException(RuntimeException::class); + $this->expectExceptionMessage('Unable to open directory: non-existing-directory'); + + DirectoryCleaner::clean(__DIR__ . '/non-existing-directory'); + } +} diff --git a/tests/EnumDataProviderTest.php b/tests/EnumDataProviderTest.php new file mode 100644 index 0000000..ff8fb95 --- /dev/null +++ b/tests/EnumDataProviderTest.php @@ -0,0 +1,103 @@ + $enumClass + */ + #[DataProviderExternal(EnumDataProviderProvider::class, 'casesParameters')] + public function testCasesGenerateExpectedStructure( + string $enumClass, + string|UnitEnum $attribute, + bool $asHtml, + string $expectedKeyCase, + string $expectedAttributeCase, + string $expectedMessage, + ): void { + $data = match ($asHtml) { + true => EnumDataProvider::attributeCases($enumClass, $attribute), + false => EnumDataProvider::attributeCases($enumClass, $attribute, false), + }; + + self::assertNotEmpty( + $data, + 'Should return at least one data set.', + ); + self::assertArrayHasKey( + $expectedKeyCase, + $data, + 'Should contain expected enum case in dataset.', + ); + self::assertInstanceOf( + $enumClass, + $data[$expectedKeyCase][0] ?? null, + 'Should return expected enum case instance.', + ); + + if ($asHtml) { + self::assertSame( + $expectedAttributeCase, + $data[$expectedKeyCase][2], + 'Should return expected attribute value for enum case.', + ); + } + + self::assertSame( + $expectedMessage, + $data[$expectedKeyCase][3], + 'Should return expected message for enum case.', + ); + } + + public function testTagCasesGenerateExpectedStructure(): void + { + $data = EnumDataProvider::tagCases(TestEnum::class, 'element'); + + self::assertNotEmpty( + $data, + 'Should return at least one data set.', + ); + self::assertSame( + [ + 'BAR element tag' => [ + TestEnum::BAR, + 'BAR', + ], + 'FOO element tag' => [ + TestEnum::FOO, + 'FOO', + ], + ], + $data, + 'Should return expected tag cases dataset.', + ); + } +} diff --git a/tests/LineEndingNormalizerTest.php b/tests/LineEndingNormalizerTest.php new file mode 100644 index 0000000..e0aacce --- /dev/null +++ b/tests/LineEndingNormalizerTest.php @@ -0,0 +1,53 @@ +expectException(ReflectionException::class); + $this->expectExceptionMessage( + 'Property PHPForge\Support\Tests\Stub\TestClass::$nonExistent does not exist', + ); + + ReflectionHelper::inaccessibleProperty(new TestClass(), 'nonExistent'); + } +} diff --git a/tests/Stub/TestBackedEnum.php b/tests/Stub/TestBackedEnum.php new file mode 100644 index 0000000..3041e32 --- /dev/null +++ b/tests/Stub/TestBackedEnum.php @@ -0,0 +1,19 @@ + + */ + public static function casesParameters(): array + { + return [ + 'as enum instance' => [ + TestEnum::class, + 'data-test', + false, + 'enum: FOO', + ' data-test="FOO"', + 'Should return the enum instance for case: FOO.', + ], + 'as html' => [ + TestEnum::class, + 'data-test', + true, + 'enum: BAR', + ' data-test="BAR"', + "Should return the 'data-test' attribute value for enum case: BAR.", + ], + 'attribute as enum instance' => [ + TestEnum::class, + TestEnum::FOO, + true, + 'enum: FOO', + ' FOO="FOO"', + "Should return the 'FOO' attribute value for enum case: FOO.", + ], + ]; + } +} diff --git a/tests/TestSupportTest.php b/tests/TestSupportTest.php deleted file mode 100644 index 5fd41d5..0000000 --- a/tests/TestSupportTest.php +++ /dev/null @@ -1,164 +0,0 @@ -assertFileDoesNotExist( - "{$dir}/test.txt", - "File 'test.txt' should not exist after 'removeFilesFromDirectory' method is called.", - ); - $this->assertFileDoesNotExist( - "{$dir}/subdir/test.txt", - "File 'subdir/test.txt' should not exist after 'removeFilesFromDirectory' method is called.", - ); - } - - /** - * @throws ReflectionException - */ - public function testReturnInaccessiblePropertyValueWhenPropertyIsPrivate(): void - { - self::assertSame( - 'value', - self::inaccessibleProperty(new TestClass(), 'property'), - "Should return the value of the private property 'property' when accessed via reflection.", - ); - } - - /** - * @throws ReflectionException - */ - public function testReturnValueWhenInvokingInaccessibleMethod(): void - { - $this->assertSame( - 'value', - self::invokeMethod(new TestClass(), 'inaccessibleMethod'), - "Should return 'value' when invoking the inaccessible method 'inaccessibleParentMethod' on 'TestClass' " . - 'via reflection.', - ); - } - - /** - * @throws ReflectionException - */ - public function testReturnValueWhenInvokingInaccessibleParentMethod(): void - { - $this->assertSame( - 'valueParent', - self::invokeParentMethod( - new TestClass(), - TestBaseClass::class, - 'inaccessibleParentMethod', - ), - "Should return 'valueParent' when invoking the inaccessible parent method 'inaccessibleParentMethod' on " . - "'TestClass' via reflection.", - ); - } - - /** - * @throws ReflectionException - */ - public function testSetInaccessibleParentProperty(): void - { - $object = new TestClass(); - - self::setInaccessibleParentProperty($object, TestBaseClass::class, 'propertyParent', 'foo'); - - $this->assertSame( - 'foo', - self::inaccessibleParentProperty($object, TestBaseClass::class, 'propertyParent'), - "Should return 'foo' after setting the parent property 'propertyParent' via " . - "'setInaccessibleParentProperty' method.", - ); - } - - /** - * @throws ReflectionException - */ - public function testSetInaccessiblePropertySetsValueCorrectly(): void - { - $object = new TestClass(); - - self::setInaccessibleProperty($object, 'property', 'foo'); - - $this->assertSame( - 'foo', - self::inaccessibleProperty($object, 'property'), - "Should return 'foo' after setting the private property 'property' via 'setInaccessibleProperty' method.", - ); - } - - public function testThrowRuntimeExceptionWhenRemoveFilesFromDirectoryNonExistingDirectory(): void - { - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Unable to open directory: non-existing-directory'); - - self::removeFilesFromDirectory(__DIR__ . '/non-existing-directory'); - } -}