From f9a32e1bfd9a77b09b630cecf335a143e94dd331 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Mon, 19 Jan 2026 12:00:18 -0300
Subject: [PATCH 1/9] feat(support): Refactor codebase to improve performance.
---
.editorconfig | 17 ++
.gitattributes | 32 ++-
.github/CODE_OF_CONDUCT.md | 102 -------
.github/ISSUE_TEMPLATE.md | 14 -
.github/PULL_REQUEST_TEMPLATE.md | 6 -
.github/ecs.yml | 34 ---
.github/workflows/build.yml | 26 +-
.github/workflows/dependency-check.yml | 29 +-
.github/workflows/ecs.yml | 21 ++
.github/workflows/linter.yml | 17 ++
.github/workflows/mutation.yml | 27 +-
.github/workflows/static.yml | 29 +-
.gitignore | 63 +++--
CHANGELOG.md | 5 +-
LICENSE | 31 +++
LICENSE.md | 27 --
README.md | 251 +++++++++++++-----
composer.json | 28 +-
docs/development.md | 43 +++
docs/svgs/features-mobile.svg | 75 ++++++
docs/svgs/features.svg | 72 +++++
docs/testing.md | 67 +++--
ecs.php | 30 ++-
infection.json5 | 12 +
phpunit.xml.dist | 38 +--
rector.php | 10 +-
runtime/.gitignore | 2 +-
src/EnumDataGenerator.php | 88 ++++++
src/TestSupport.php | 116 ++++----
tests/EnumDataGeneratorTest.php | 150 +++++++++++
tests/Stub/TestEnum.php | 17 ++
.../Provider/EnumDataGeneratorProvider.php | 55 ++++
tests/Support/runtime/.gitignore | 0
tests/Support/runtime/.gitkeep | 0
tests/TestSupportTest.php | 56 ++--
35 files changed, 1095 insertions(+), 495 deletions(-)
delete mode 100644 .github/CODE_OF_CONDUCT.md
delete mode 100644 .github/ISSUE_TEMPLATE.md
delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md
delete mode 100644 .github/ecs.yml
create mode 100644 .github/workflows/ecs.yml
create mode 100644 .github/workflows/linter.yml
create mode 100644 LICENSE
delete mode 100644 LICENSE.md
create mode 100644 docs/development.md
create mode 100644 docs/svgs/features-mobile.svg
create mode 100644 docs/svgs/features.svg
create mode 100644 infection.json5
create mode 100644 src/EnumDataGenerator.php
create mode 100644 tests/EnumDataGeneratorTest.php
create mode 100644 tests/Stub/TestEnum.php
create mode 100644 tests/Support/Provider/EnumDataGeneratorProvider.php
create mode 100644 tests/Support/runtime/.gitignore
create mode 100644 tests/Support/runtime/.gitkeep
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/workflows/build.yml b/.github/workflows/build.yml
index 9b51512..3254d3d 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@main
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/dependency-check.yml b/.github/workflows/dependency-check.yml
index 48115b8..e69d7d1 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@main
diff --git a/.github/workflows/ecs.yml b/.github/workflows/ecs.yml
new file mode 100644
index 0000000..e18f15c
--- /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@main
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..99d7c1c 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@main
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/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..b58db59 100644
--- a/README.md
+++ b/README.md
@@ -1,82 +1,69 @@
+
-
+
-
Support utilities for enhanced testing capabilities.
+ Support
+
-
-
-
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+ Support utilities for PHPUnit-focused development
+ Reflection helpers, line ending normalization, and filesystem cleanup for deterministic tests.
## Features
-✅ **Advanced Reflection Utilities**
+
+
+
+
+
+**Advanced Reflection Utilities**
- Access and modify private and protected properties via reflection.
- Invoke inaccessible methods to expand testing coverage.
-✅ **Cross-Platform String Assertions**
+**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**
+**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
-
-### System requirements
+- Safe removal that preserves Git-tracking files (for example, `.gitignore`, `.gitkeep`).
-- [`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.
+**Enum Test Data Generation**
+- Generate structured, deterministic datasets for enum-based attribute scenarios via `PHPForge\Support\EnumDataGenerator`.
### 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 the `PHPForge\Support\TestSupport` trait 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
+
diff --git a/docs/svgs/features.svg b/docs/svgs/features.svg
new file mode 100644
index 0000000..d971e80
--- /dev/null
+++ b/docs/svgs/features.svg
@@ -0,0 +1,72 @@
+
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/rector.php b/rector.php
index 4a7a938..2225bf2 100644
--- a/rector.php
+++ b/rector.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-return static function (Rector\Config\RectorConfig $rectorConfig): void {
+return static function (\Rector\Config\RectorConfig $rectorConfig): void {
$rectorConfig->parallel();
$rectorConfig->importNames();
@@ -16,13 +16,13 @@
$rectorConfig->sets(
[
- Rector\Set\ValueObject\SetList::PHP_81,
- Rector\Set\ValueObject\LevelSetList::UP_TO_PHP_81,
- Rector\Set\ValueObject\SetList::TYPE_DECLARATION,
+ \Rector\Set\ValueObject\SetList::PHP_81,
+ \Rector\Set\ValueObject\LevelSetList::UP_TO_PHP_81,
+ \Rector\Set\ValueObject\SetList::TYPE_DECLARATION,
],
);
$rectorConfig->rule(
- Rector\CodeQuality\Rector\BooleanAnd\SimplifyEmptyArrayCheckRector::class
+ \Rector\CodeQuality\Rector\BooleanAnd\SimplifyEmptyArrayCheckRector::class
);
};
diff --git a/runtime/.gitignore b/runtime/.gitignore
index d6b7ef3..c96a04f 100644
--- a/runtime/.gitignore
+++ b/runtime/.gitignore
@@ -1,2 +1,2 @@
*
-!.gitignore
+!.gitignore
\ No newline at end of file
diff --git a/src/EnumDataGenerator.php b/src/EnumDataGenerator.php
new file mode 100644
index 0000000..e6c8134
--- /dev/null
+++ b/src/EnumDataGenerator.php
@@ -0,0 +1,88 @@
+ $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 cases(string $enumClass, string|UnitEnum $attribute, bool $asHtml = true): array
+ {
+ $cases = [];
+ $attributeName = is_string($attribute) ? $attribute : sprintf('%s', Enum::normalizeValue($attribute));
+
+ foreach ($enumClass::cases() as $case) {
+ $normalizedValue = Enum::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 Structured test cases indexed by descriptive keys.
+ */
+ public static function tagCases(string $enumClass, string $category): array
+ {
+ $data = [];
+
+ foreach ($enumClass::cases() as $case) {
+ $value = (string) Enum::normalizeValue($case);
+ $data[sprintf('%s %s tag', $value, $category)] = [$case, $value];
+ }
+
+ return $data;
+ }
+}
diff --git a/src/TestSupport.php b/src/TestSupport.php
index 567c9b2..628755b 100644
--- a/src/TestSupport.php
+++ b/src/TestSupport.php
@@ -20,14 +20,13 @@
use function unlink;
/**
- * Trait providing utilities for testing inaccessible properties, methods, and filesystem cleanup.
+ * Test support utilities for PHPUnit suites.
*
- * Supplies static helper methods for normalizing line endings, accessing or modifying private/protected properties and
- * methods (including those inherited from parent classes), invoking inaccessible methods, and recursively removing
- * files from directories.
+ * Provides reflection-based helpers to read and write non-public properties and invoke non-public methods, including
+ * members declared on parent classes.
*
- * These utilities are designed to facilitate comprehensive unit testing by enabling assertions and manipulations that
- * would otherwise be restricted by visibility constraints or platform differences.
+ * Also provides helpers for normalizing line endings to `"\n"` and for removing files from directories while
+ * preserving `.gitignore` and `.gitkeep`.
*
* @copyright Copyright (C) 2025 Terabytesoftw.
* @license https://opensource.org/license/bsd-3-clause BSD 3-Clause License.
@@ -35,25 +34,21 @@
trait TestSupport
{
/**
- * Retrieves the value of an inaccessible property from a parent class instance.
+ * Retrieves a property value from a specified class context.
*
- * Uses reflection to access the specified property of the given parent class, allowing tests to inspect or assert
- * the value of private or protected properties inherited from parent classes.
- *
- * This method is useful for verifying the internal state of objects in inheritance scenarios where direct access
- * to parent properties is not possible.
+ * Uses reflection to read `$propertyName` from `$object` using `$className` as the declaring class context.
*
* @param object $object Object instance from which to retrieve the property value.
- * @param object|string $className Name or instance of the parent class containing the property.
+ * @param object|string $className Name or instance of the class that declares the property.
* @param string $propertyName Name of the property to access.
*
* @throws ReflectionException
*
- * @return mixed Value of the specified parent property.
+ * @return mixed Value of the specified property.
*
* @phpstan-param class-string|object $className
*/
- public static function inaccessibleParentProperty(
+ private static function inaccessibleParentProperty(
object $object,
string|object $className,
string $propertyName,
@@ -64,24 +59,20 @@ public static function inaccessibleParentProperty(
}
/**
- * 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.
+ * Retrieves a property value from a class or object.
*
- * 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.
+ * 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 or is inaccessible.
+ * @throws ReflectionException if the property does not exist.
*
- * @return mixed Value of the specified property, or `null` if the property name is empty.
+ * @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
+ private static function inaccessibleProperty(string|object $object, string $propertyName): mixed
{
$class = new ReflectionClass($object);
@@ -94,24 +85,21 @@ public static function inaccessibleProperty(string|object $object, string $prope
}
/**
- * Invokes an inaccessible method on the given object instance with the specified arguments.
+ * Invokes a method via reflection.
*
- * 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.
+ * 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 or is inaccessible.
+ * @throws ReflectionException if the method does not exist.
*
- * @return mixed Value of the invoked method, or `null` if the method name is empty.
+ * @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
+ private static function invokeMethod(object $object, string $method, array $args = []): mixed
{
$reflection = new ReflectionObject($object);
@@ -124,26 +112,23 @@ public static function invokeMethod(object $object, string $method, array $args
}
/**
- * 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.
+ * Invokes a parent class method via reflection.
*
- * This is useful for verifying inherited behavior or side effects during unit testing of subclasses.
+ * 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 or is inaccessible in the parent class.
+ * @throws ReflectionException if the method does not exist.
*
- * @return mixed Value of the invoked method, or `null` if the method name is empty.
+ * @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(
+ private static function invokeParentMethod(
object $object,
string $parentClass,
string $method,
@@ -160,36 +145,32 @@ public static function invokeParentMethod(
}
/**
- * Normalizes line endings to Unix style ('\n') for cross-platform string assertions.
+ * Normalizes line endings to `"\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.
+ * Converts `"\r\n"` and `"\r"` to `"\n"` to keep string comparisons deterministic across operating systems.
*
* @param string $line Input string potentially containing Windows style line endings.
*
- * @return string String with normalized Unix style line endings.
+ * @return string String with normalized line endings.
*/
- public static function normalizeLineEndings(string $line): string
+ private 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'.
+ * Removes directory contents recursively while preserving `.gitignore` and `.gitkeep`.
*
- * Opens the given directory, iterates through its contents, and removes all files and subdirectories except for
- * special entries ('.', '..', '.gitignore', '.gitkeep').
+ * Iterates through `$basePath` and removes all files and subdirectories except `.`, `..`, `.gitignore`, and
+ * `.gitkeep`. Subdirectories are processed recursively before removal.
*
- * Subdirectories are processed recursively before removal.
+ * File and directory removals are attempted with error suppression.
*
* @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
+ private static function removeFilesFromDirectory(string $basePath): void
{
$handle = @opendir($basePath);
@@ -199,7 +180,11 @@ public static function removeFilesFromDirectory(string $basePath): void
}
while (($file = readdir($handle)) !== false) {
- if ($file === '.' || $file === '..' || $file === '.gitignore' || $file === '.gitkeep') {
+ if ($file === '.' || $file === '..') {
+ continue;
+ }
+
+ if ($file === '.gitignore' || $file === '.gitkeep') {
continue;
}
@@ -217,24 +202,22 @@ public static function removeFilesFromDirectory(string $basePath): void
}
/**
- * Sets the value of an inaccessible property on a parent class instance.
+ * Sets a parent class property value via reflection.
*
- * 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.
+ * Uses `$parentClass` as the declaring class context to assign `$value` to `$propertyName` on `$object`.
*
- * This method is useful for testing scenarios that require direct manipulation of parent class internals.
+ * 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 or is inaccessible in the parent class.
+ * @throws ReflectionException if the property does not exist.
*
* @phpstan-param class-string $parentClass
*/
- public static function setInaccessibleParentProperty(
+ private static function setInaccessibleParentProperty(
object $object,
string $parentClass,
string $propertyName,
@@ -251,20 +234,19 @@ public static function setInaccessibleParentProperty(
}
/**
- * Sets the value of an inaccessible property on the given object instance.
+ * Sets a property value via reflection.
*
- * 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.
+ * Assigns `$value` to `$propertyName` on `$object`.
*
- * This method is useful for testing scenarios that require direct manipulation of object internals.
+ * 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 or is inaccessible.
+ * @throws ReflectionException if the property does not exist.
*/
- public static function setInaccessibleProperty(
+ private static function setInaccessibleProperty(
object $object,
string $propertyName,
mixed $value,
diff --git a/tests/EnumDataGeneratorTest.php b/tests/EnumDataGeneratorTest.php
new file mode 100644
index 0000000..c7d2a10
--- /dev/null
+++ b/tests/EnumDataGeneratorTest.php
@@ -0,0 +1,150 @@
+ $enumClass
+ */
+ #[DataProviderExternal(EnumDataGeneratorProvider::class, 'casesParameters')]
+ public function testCasesGenerateExpectedStructure(
+ string $enumClass,
+ string|UnitEnum $attribute,
+ bool $asHtml,
+ ): void {
+ $data = match ($asHtml) {
+ true => EnumDataGenerator::cases($enumClass, $attribute),
+ false => EnumDataGenerator::cases($enumClass, $attribute, false),
+ };
+
+ self::assertNotEmpty($data, 'Should return at least one data set.');
+
+ $attributeName = is_string($attribute) ? $attribute : sprintf('%s', Enum::normalizeValue($attribute));
+
+ foreach ($enumClass::cases() as $case) {
+ $expectedKey = 'enum: ' . Enum::normalizeValue($case);
+
+ self::assertArrayHasKey(
+ $expectedKey,
+ $data,
+ "Should include a dataset key for enum case '{$case->name}'.",
+ );
+
+ $row = $data[$expectedKey] ?? null;
+
+ self::assertIsArray(
+ $row,
+ "Should return a dataset array for enum case '{$case->name}'.",
+ );
+ self::assertCount(
+ 4,
+ $row,
+ "Should return a 4-tuple dataset for enum case '{$case->name}'.",
+ );
+
+ /** @var array{UnitEnum, array, string|UnitEnum, string} $row */
+ [$value, $attributes, $expected, $message] = $row;
+
+ self::assertSame(
+ $case,
+ $value,
+ "Should store the enum case instance for '{$case->name}'.",
+ );
+ self::assertSame(
+ [],
+ $attributes,
+ "Should store an empty attributes array for '{$case->name}'.",
+ );
+
+ $expectedValue = Enum::normalizeValue($case);
+
+ if ($asHtml) {
+ self::assertSame(
+ " {$attributeName}=\"{$expectedValue}\"",
+ $expected,
+ "Should generate expected attribute fragment for '{$case->name}'.",
+ );
+ $expectedMessage = "Should return the '{$attributeName}' attribute value for enum case: {$expectedValue}.";
+ } else {
+ self::assertSame(
+ $case,
+ $expected,
+ "Should return the enum instance as expected output for '{$case->name}'.",
+ );
+ $expectedMessage = "Should return the enum instance for case: {$expectedValue}.";
+ }
+
+ self::assertSame(
+ $expectedMessage,
+ $message,
+ "Should store the expected message for '{$case->name}'.",
+ );
+ }
+ }
+
+ /**
+ * @phpstan-param class-string $enumClass
+ */
+ #[DataProviderExternal(EnumDataGeneratorProvider::class, 'tagCasesParameters')]
+ public function testTagCasesGenerateExpectedStructure(string $enumClass, string $category): void
+ {
+ $data = EnumDataGenerator::tagCases($enumClass, $category);
+
+ self::assertNotEmpty($data, 'Should return at least one data set.');
+
+ foreach ($enumClass::cases() as $case) {
+ $expectedKey = Enum::normalizeValue($case) . " {$category} tag";
+
+ self::assertArrayHasKey(
+ $expectedKey,
+ $data,
+ "Should include a dataset key for enum case '{$case->name}'.",
+ );
+
+ $row = $data[$expectedKey] ?? null;
+
+ self::assertIsArray(
+ $row,
+ "Should return a dataset array for enum case '{$case->name}'.",
+ );
+ self::assertCount(
+ 2,
+ $row,
+ "Should return a 2-tuple dataset for enum case '{$case->name}'.",
+ );
+
+ /** @var array{UnitEnum, string} $row */
+ [$value, $normalizedValue] = $row;
+
+ self::assertSame(
+ $case,
+ $value,
+ "Should store the enum case instance for '{$case->name}'.",
+ );
+ self::assertSame(
+ Enum::normalizeValue($case),
+ $normalizedValue,
+ "Should store normalized value for '{$case->name}'.",
+ );
+ }
+ }
+}
diff --git a/tests/Stub/TestEnum.php b/tests/Stub/TestEnum.php
new file mode 100644
index 0000000..7576e60
--- /dev/null
+++ b/tests/Stub/TestEnum.php
@@ -0,0 +1,17 @@
+
+ */
+ public static function casesParameters(): array
+ {
+ return [
+ 'as html' => [
+ TestEnum::class,
+ 'data-test',
+ true,
+ ],
+ 'as enum instance' => [
+ TestEnum::class,
+ 'data-test',
+ false,
+ ],
+ 'attribute as enum instance' => [
+ TestEnum::class,
+ TestEnum::Foo,
+ true,
+ ],
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ public static function tagCasesParameters(): array
+ {
+ return [
+ 'default category' => [
+ TestEnum::class,
+ 'element',
+ ],
+ ];
+ }
+}
diff --git a/tests/Support/runtime/.gitignore b/tests/Support/runtime/.gitignore
new file mode 100644
index 0000000..e69de29
diff --git a/tests/Support/runtime/.gitkeep b/tests/Support/runtime/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/TestSupportTest.php b/tests/TestSupportTest.php
index 5fd41d5..fd8a534 100644
--- a/tests/TestSupportTest.php
+++ b/tests/TestSupportTest.php
@@ -50,33 +50,59 @@ public function testInaccessibleParentPropertyReturnsExpectedValue(): void
);
}
+ public function testNormalizeLineEndingsReturnsUnchangedStringWhenNoNormalizationNeeded(): void
+ {
+ self::assertSame(
+ "foo\nbar",
+ self::normalizeLineEndings("foo\nbar"),
+ 'Should return the original string when no normalization is required.',
+ );
+ }
+
public function testNormalizeLineEndingsWhenStringsAreIdenticalWithLineEndings(): void
{
self::assertSame(
+ "foo\nbar",
self::normalizeLineEndings("foo\r\nbar"),
- self::normalizeLineEndings("foo\nbar"),
- "Should produce the same normalized string for 'CRLF' and 'LF' inputs.",
+ "Should normalize 'CRLF' inputs to 'LF'.",
);
}
public function testRemoveFilesFromDirectoryRemovesAllFiles(): void
{
- $dir = dirname(__DIR__) . '/runtime';
+ $dir = __DIR__ . '/Support/runtime';
+ if (is_dir($dir)) {
+ unlink("{$dir}/.gitignore");
+ unlink("{$dir}/.gitkeep");
+ rmdir($dir);
+ }
+
+ mkdir($dir, 0777, true);
mkdir("{$dir}/subdir");
touch("{$dir}/test.txt");
touch("{$dir}/subdir/test.txt");
+ touch("{$dir}/.gitignore");
+ touch("{$dir}/.gitkeep");
self::removeFilesFromDirectory($dir);
- $this->assertFileDoesNotExist(
+ self::assertFileDoesNotExist(
"{$dir}/test.txt",
"File 'test.txt' should not exist after 'removeFilesFromDirectory' method is called.",
);
- $this->assertFileDoesNotExist(
+ self::assertFileDoesNotExist(
"{$dir}/subdir/test.txt",
"File 'subdir/test.txt' should not exist after 'removeFilesFromDirectory' method is called.",
);
+ self::assertFileExists(
+ "{$dir}/.gitignore",
+ "File '.gitignore' should remain after 'removeFilesFromDirectory' method is called.",
+ );
+ self::assertFileExists(
+ "{$dir}/.gitkeep",
+ "File '.gitkeep' should remain after 'removeFilesFromDirectory' method is called.",
+ );
}
/**
@@ -96,11 +122,11 @@ public function testReturnInaccessiblePropertyValueWhenPropertyIsPrivate(): void
*/
public function testReturnValueWhenInvokingInaccessibleMethod(): void
{
- $this->assertSame(
+ self::assertSame(
'value',
self::invokeMethod(new TestClass(), 'inaccessibleMethod'),
- "Should return 'value' when invoking the inaccessible method 'inaccessibleParentMethod' on 'TestClass' " .
- 'via reflection.',
+ "Should return 'value' when invoking the inaccessible method 'inaccessibleParentMethod' on 'TestClass' "
+ . 'via reflection.',
);
}
@@ -109,15 +135,15 @@ public function testReturnValueWhenInvokingInaccessibleMethod(): void
*/
public function testReturnValueWhenInvokingInaccessibleParentMethod(): void
{
- $this->assertSame(
+ self::assertSame(
'valueParent',
self::invokeParentMethod(
new TestClass(),
TestBaseClass::class,
'inaccessibleParentMethod',
),
- "Should return 'valueParent' when invoking the inaccessible parent method 'inaccessibleParentMethod' on " .
- "'TestClass' via reflection.",
+ "Should return 'valueParent' when invoking the inaccessible parent method 'inaccessibleParentMethod' on "
+ . "'TestClass' via reflection.",
);
}
@@ -130,11 +156,11 @@ public function testSetInaccessibleParentProperty(): void
self::setInaccessibleParentProperty($object, TestBaseClass::class, 'propertyParent', 'foo');
- $this->assertSame(
+ self::assertSame(
'foo',
self::inaccessibleParentProperty($object, TestBaseClass::class, 'propertyParent'),
- "Should return 'foo' after setting the parent property 'propertyParent' via " .
- "'setInaccessibleParentProperty' method.",
+ "Should return 'foo' after setting the parent property 'propertyParent' via "
+ . "'setInaccessibleParentProperty' method.",
);
}
@@ -147,7 +173,7 @@ public function testSetInaccessiblePropertySetsValueCorrectly(): void
self::setInaccessibleProperty($object, 'property', 'foo');
- $this->assertSame(
+ self::assertSame(
'foo',
self::inaccessibleProperty($object, 'property'),
"Should return 'foo' after setting the private property 'property' via 'setInaccessibleProperty' method.",
From 4911dbc93586162acd04b7f57cc4a5e8c2034aec Mon Sep 17 00:00:00 2001
From: StyleCI Bot
Date: Mon, 19 Jan 2026 15:00:36 +0000
Subject: [PATCH 2/9] Apply fixes from StyleCI
---
rector.php | 10 +++++-----
src/EnumDataGenerator.php | 2 ++
2 files changed, 7 insertions(+), 5 deletions(-)
diff --git a/rector.php b/rector.php
index 2225bf2..4a7a938 100644
--- a/rector.php
+++ b/rector.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-return static function (\Rector\Config\RectorConfig $rectorConfig): void {
+return static function (Rector\Config\RectorConfig $rectorConfig): void {
$rectorConfig->parallel();
$rectorConfig->importNames();
@@ -16,13 +16,13 @@
$rectorConfig->sets(
[
- \Rector\Set\ValueObject\SetList::PHP_81,
- \Rector\Set\ValueObject\LevelSetList::UP_TO_PHP_81,
- \Rector\Set\ValueObject\SetList::TYPE_DECLARATION,
+ Rector\Set\ValueObject\SetList::PHP_81,
+ Rector\Set\ValueObject\LevelSetList::UP_TO_PHP_81,
+ Rector\Set\ValueObject\SetList::TYPE_DECLARATION,
],
);
$rectorConfig->rule(
- \Rector\CodeQuality\Rector\BooleanAnd\SimplifyEmptyArrayCheckRector::class
+ Rector\CodeQuality\Rector\BooleanAnd\SimplifyEmptyArrayCheckRector::class
);
};
diff --git a/src/EnumDataGenerator.php b/src/EnumDataGenerator.php
index e6c8134..aca3ff8 100644
--- a/src/EnumDataGenerator.php
+++ b/src/EnumDataGenerator.php
@@ -29,6 +29,7 @@ final class EnumDataGenerator
* `true`) or the enum case instance (when `$asHtml` is `false`).
*
* @phpstan-param class-string $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`.
@@ -69,6 +70,7 @@ public static function cases(string $enumClass, string|UnitEnum $attribute, bool
* 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.
*
From d80918bf28edc1df4c7586a381b5abb982290486 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Mon, 19 Jan 2026 12:07:29 -0300
Subject: [PATCH 3/9] Fix Linter test.
---
.github/linters/actionlint.yml | 7 +++++++
README.md | 15 ---------------
runtime/.gitignore | 2 +-
3 files changed, 8 insertions(+), 16 deletions(-)
create mode 100644 .github/linters/actionlint.yml
diff --git a/.github/linters/actionlint.yml b/.github/linters/actionlint.yml
new file mode 100644
index 0000000..785d850
--- /dev/null
+++ b/.github/linters/actionlint.yml
@@ -0,0 +1,7 @@
+---
+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'
+ - "section is alias node but mapping node is expected"
diff --git a/README.md b/README.md
index b58db59..1be003b 100644
--- a/README.md
+++ b/README.md
@@ -32,21 +32,6 @@
-**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`).
-
-**Enum Test Data Generation**
-- Generate structured, deterministic datasets for enum-based attribute scenarios via `PHPForge\Support\EnumDataGenerator`.
-
### Installation
```bash
diff --git a/runtime/.gitignore b/runtime/.gitignore
index c96a04f..d6b7ef3 100644
--- a/runtime/.gitignore
+++ b/runtime/.gitignore
@@ -1,2 +1,2 @@
*
-!.gitignore
\ No newline at end of file
+!.gitignore
From 20b9b4221cfac811591ceacb184f87010109aa33 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Mon, 19 Jan 2026 12:21:52 -0300
Subject: [PATCH 4/9] Apply fixed Coderabbitai review.
---
.github/workflows/build.yml | 2 +-
.github/workflows/dependency-check.yml | 2 +-
.github/workflows/ecs.yml | 2 +-
.github/workflows/static.yml | 2 +-
.styleci.yml | 159 +++++++++++++------------
tests/TestSupportTest.php | 66 +++++-----
6 files changed, 117 insertions(+), 116 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3254d3d..9fef92b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -17,6 +17,6 @@ permissions:
jobs:
phpunit:
- uses: yii2-framework/actions/.github/workflows/phpunit.yml@main
+ 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 e69d7d1..294a347 100644
--- a/.github/workflows/dependency-check.yml
+++ b/.github/workflows/dependency-check.yml
@@ -18,4 +18,4 @@ permissions:
jobs:
composer-require-checker:
- uses: yii2-framework/actions/.github/workflows/composer-require-checker.yml@main
+ uses: yii2-framework/actions/.github/workflows/composer-require-checker.yml@v1
diff --git a/.github/workflows/ecs.yml b/.github/workflows/ecs.yml
index e18f15c..76f6a37 100644
--- a/.github/workflows/ecs.yml
+++ b/.github/workflows/ecs.yml
@@ -18,4 +18,4 @@ permissions:
jobs:
easy-coding-standard:
- uses: yii2-framework/actions/.github/workflows/ecs.yml@main
+ uses: yii2-framework/actions/.github/workflows/ecs.yml@v1
diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml
index 99d7c1c..cc1c99d 100644
--- a/.github/workflows/static.yml
+++ b/.github/workflows/static.yml
@@ -18,4 +18,4 @@ permissions:
jobs:
phpstan:
- uses: yii2-framework/actions/.github/workflows/phpstan.yml@main
+ uses: yii2-framework/actions/.github/workflows/phpstan.yml@v1
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/tests/TestSupportTest.php b/tests/TestSupportTest.php
index fd8a534..9297689 100644
--- a/tests/TestSupportTest.php
+++ b/tests/TestSupportTest.php
@@ -70,39 +70,39 @@ public function testNormalizeLineEndingsWhenStringsAreIdenticalWithLineEndings()
public function testRemoveFilesFromDirectoryRemovesAllFiles(): void
{
- $dir = __DIR__ . '/Support/runtime';
-
- if (is_dir($dir)) {
- unlink("{$dir}/.gitignore");
- unlink("{$dir}/.gitkeep");
- rmdir($dir);
+ $dir = sys_get_temp_dir() . '/php-forge-support-' . bin2hex(random_bytes(8));
+ mkdir($dir);
+
+ try {
+ mkdir("{$dir}/subdir");
+ touch("{$dir}/test.txt");
+ touch("{$dir}/subdir/test.txt");
+ touch("{$dir}/.gitignore");
+ touch("{$dir}/.gitkeep");
+
+ self::removeFilesFromDirectory($dir);
+
+ self::assertFileDoesNotExist(
+ "{$dir}/test.txt",
+ "File 'test.txt' should not exist after 'removeFilesFromDirectory' method is called.",
+ );
+ self::assertFileDoesNotExist(
+ "{$dir}/subdir/test.txt",
+ "File 'subdir/test.txt' should not exist after 'removeFilesFromDirectory' method is called.",
+ );
+ self::assertFileExists(
+ "{$dir}/.gitignore",
+ "File '.gitignore' should remain after 'removeFilesFromDirectory' method is called.",
+ );
+ self::assertFileExists(
+ "{$dir}/.gitkeep",
+ "File '.gitkeep' should remain after 'removeFilesFromDirectory' method is called.",
+ );
+ } finally {
+ @unlink("{$dir}/.gitignore");
+ @unlink("{$dir}/.gitkeep");
+ @rmdir($dir);
}
-
- mkdir($dir, 0777, true);
- mkdir("{$dir}/subdir");
- touch("{$dir}/test.txt");
- touch("{$dir}/subdir/test.txt");
- touch("{$dir}/.gitignore");
- touch("{$dir}/.gitkeep");
-
- self::removeFilesFromDirectory($dir);
-
- self::assertFileDoesNotExist(
- "{$dir}/test.txt",
- "File 'test.txt' should not exist after 'removeFilesFromDirectory' method is called.",
- );
- self::assertFileDoesNotExist(
- "{$dir}/subdir/test.txt",
- "File 'subdir/test.txt' should not exist after 'removeFilesFromDirectory' method is called.",
- );
- self::assertFileExists(
- "{$dir}/.gitignore",
- "File '.gitignore' should remain after 'removeFilesFromDirectory' method is called.",
- );
- self::assertFileExists(
- "{$dir}/.gitkeep",
- "File '.gitkeep' should remain after 'removeFilesFromDirectory' method is called.",
- );
}
/**
@@ -159,7 +159,7 @@ public function testSetInaccessibleParentProperty(): void
self::assertSame(
'foo',
self::inaccessibleParentProperty($object, TestBaseClass::class, 'propertyParent'),
- "Should return 'foo' after setting the parent property 'propertyParent' via "
+ "Should return 'value' when invoking the inaccessible method 'inaccessibleMethod' on 'TestClass' "
. "'setInaccessibleParentProperty' method.",
);
}
From f5a9bd5f36f0e7dc54c7d1603343c00fff6da0f9 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Mon, 19 Jan 2026 12:25:10 -0300
Subject: [PATCH 5/9] Apply fixed Coderabbitai review.
---
.github/linters/actionlint.yml | 1 -
1 file changed, 1 deletion(-)
diff --git a/.github/linters/actionlint.yml b/.github/linters/actionlint.yml
index 785d850..328407a 100644
--- a/.github/linters/actionlint.yml
+++ b/.github/linters/actionlint.yml
@@ -4,4 +4,3 @@ paths:
ignore:
- '"pull_request" section is alias node but mapping node is expected'
- '"push" section is alias node but mapping node is expected'
- - "section is alias node but mapping node is expected"
From 7a858173b55a2acbe0c23feab70ac2429cf65c6e Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Mon, 19 Jan 2026 12:40:12 -0300
Subject: [PATCH 6/9] Apply fixed Coderabbitai review.
---
tests/TestSupportTest.php | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/tests/TestSupportTest.php b/tests/TestSupportTest.php
index 9297689..fbc6d11 100644
--- a/tests/TestSupportTest.php
+++ b/tests/TestSupportTest.php
@@ -125,7 +125,7 @@ public function testReturnValueWhenInvokingInaccessibleMethod(): void
self::assertSame(
'value',
self::invokeMethod(new TestClass(), 'inaccessibleMethod'),
- "Should return 'value' when invoking the inaccessible method 'inaccessibleParentMethod' on 'TestClass' "
+ "Should return 'value' when invoking the inaccessible method 'inaccessibleMethod' on 'TestClass' "
. 'via reflection.',
);
}
@@ -159,8 +159,8 @@ public function testSetInaccessibleParentProperty(): void
self::assertSame(
'foo',
self::inaccessibleParentProperty($object, TestBaseClass::class, 'propertyParent'),
- "Should return 'value' when invoking the inaccessible method 'inaccessibleMethod' on 'TestClass' "
- . "'setInaccessibleParentProperty' method.",
+ "Should return 'foo' after setting parent property 'propertyParent' "
+ . "via 'setInaccessibleParentProperty' method.",
);
}
From bea54c404b29c80d9aafc59863ac25696fcb8375 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Mon, 19 Jan 2026 17:32:39 -0300
Subject: [PATCH 7/9] ffeat(support): Refactoring all code.
---
README.md | 59 +++---
composer.json | 3 +-
docs/svgs/features-mobile.svg | 4 +-
docs/svgs/features.svg | 4 +-
src/DirectoryCleaner.php | 76 +++++++
...DataGenerator.php => EnumDataProvider.php} | 38 ++--
src/Exception/Message.php | 59 ++++++
src/LineEndingNormalizer.php | 43 ++++
src/{TestSupport.php => ReflectionHelper.php} | 99 ++-------
tests/DirectoryCleanerTest.php | 71 +++++++
tests/EnumDataGeneratorTest.php | 150 --------------
tests/EnumDataProviderTest.php | 103 ++++++++++
tests/LineEndingNormalizerTest.php | 52 +++++
tests/ReflectionHelperTest.php | 121 +++++++++++
tests/Stub/TestBackedEnum.php | 19 ++
tests/Stub/TestEnum.php | 7 +-
tests/Stub/TestIntBackedEnum.php | 19 ++
...vider.php => EnumDataProviderProvider.php} | 27 ++-
tests/Support/runtime/.gitignore | 0
tests/Support/runtime/.gitkeep | 0
tests/TestSupportTest.php | 190 ------------------
21 files changed, 660 insertions(+), 484 deletions(-)
create mode 100644 src/DirectoryCleaner.php
rename src/{EnumDataGenerator.php => EnumDataProvider.php} (68%)
create mode 100644 src/Exception/Message.php
create mode 100644 src/LineEndingNormalizer.php
rename src/{TestSupport.php => ReflectionHelper.php} (66%)
create mode 100644 tests/DirectoryCleanerTest.php
delete mode 100644 tests/EnumDataGeneratorTest.php
create mode 100644 tests/EnumDataProviderTest.php
create mode 100644 tests/LineEndingNormalizerTest.php
create mode 100644 tests/ReflectionHelperTest.php
create mode 100644 tests/Stub/TestBackedEnum.php
create mode 100644 tests/Stub/TestIntBackedEnum.php
rename tests/Support/Provider/{EnumDataGeneratorProvider.php => EnumDataProviderProvider.php} (61%)
delete mode 100644 tests/Support/runtime/.gitignore
delete mode 100644 tests/Support/runtime/.gitkeep
delete mode 100644 tests/TestSupportTest.php
diff --git a/README.md b/README.md
index 1be003b..36b9036 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ composer require php-forge/support:^0.2 --dev
### Quick start
-This package provides the `PHPForge\Support\TestSupport` trait for PHPUnit tests.
+This package provides helper classes for PHPUnit tests.
It supports reflection-based access to non-public members, deterministic string comparisons across platforms, and filesystem cleanup for isolated test environments.
@@ -51,12 +51,11 @@ It supports reflection-based access to non-public members, deterministic string
declare(strict_types=1);
-use PHPForge\Support\TestSupport;
+use PHPForge\Support\ReflectionHelper;
use PHPUnit\Framework\TestCase;
final class AccessPrivatePropertyTest extends TestCase
{
- use TestSupport;
public function testInaccessibleProperty(): void
{
@@ -64,7 +63,7 @@ final class AccessPrivatePropertyTest extends TestCase
private string $secretValue = 'hidden';
};
- $value = self::inaccessibleProperty($object, 'secretValue');
+ $value = ReflectionHelper::inaccessibleProperty($object, 'secretValue');
self::assertSame('hidden', $value);
}
@@ -78,12 +77,11 @@ final class AccessPrivatePropertyTest extends TestCase
declare(strict_types=1);
-use PHPForge\Support\TestSupport;
+use PHPForge\Support\ReflectionHelper;
use PHPUnit\Framework\TestCase;
final class InvokeProtectedMethodTest extends TestCase
{
- use TestSupport;
public function testInvokeMethod(): void
{
@@ -94,7 +92,7 @@ final class InvokeProtectedMethodTest extends TestCase
}
};
- $result = self::invokeMethod($object, 'calculate', [5, 3]);
+ $result = ReflectionHelper::invokeMethod($object, 'calculate', [5, 3]);
self::assertSame(8, $result);
}
@@ -108,18 +106,17 @@ final class InvokeProtectedMethodTest extends TestCase
declare(strict_types=1);
-use PHPForge\Support\TestSupport;
+use PHPForge\Support\LineEndingNormalizer;
use PHPUnit\Framework\TestCase;
final class NormalizeLineEndingsTest extends TestCase
{
- use TestSupport;
public function testNormalizedComparison(): void
{
self::assertSame(
- self::normalizeLineEndings("Foo\r\nBar"),
- self::normalizeLineEndings("Foo\nBar"),
+ LineEndingNormalizer::normalize("Foo\r\nBar"),
+ LineEndingNormalizer::normalize("Foo\nBar"),
);
}
}
@@ -132,12 +129,11 @@ final class NormalizeLineEndingsTest extends TestCase
declare(strict_types=1);
-use PHPForge\Support\TestSupport;
+use PHPForge\Support\DirectoryCleaner;
use PHPUnit\Framework\TestCase;
final class RemoveFilesFromDirectoryTest extends TestCase
{
- use TestSupport;
public function testCleanup(): void
{
@@ -148,7 +144,7 @@ final class RemoveFilesFromDirectoryTest extends TestCase
file_put_contents($testDir . '/artifact.txt', 'test');
file_put_contents($testDir . '/.gitignore', "*\n");
- self::removeFilesFromDirectory($testDir);
+ DirectoryCleaner::clean($testDir);
self::assertFileDoesNotExist($testDir . '/artifact.txt');
self::assertFileExists($testDir . '/.gitignore');
@@ -167,12 +163,11 @@ final class RemoveFilesFromDirectoryTest extends TestCase
declare(strict_types=1);
-use PHPForge\Support\TestSupport;
+use PHPForge\Support\ReflectionHelper;
use PHPUnit\Framework\TestCase;
final class SetInaccessiblePropertyTest extends TestCase
{
- use TestSupport;
public function testSetProperty(): void
{
@@ -180,18 +175,18 @@ final class SetInaccessiblePropertyTest extends TestCase
private string $config = 'default';
};
- self::setInaccessibleProperty($object, 'config', 'test-mode');
+ ReflectionHelper::setInaccessibleProperty($object, 'config', 'test-mode');
- $newValue = self::inaccessibleProperty($object, 'config');
+ $newValue = ReflectionHelper::inaccessibleProperty($object, 'config');
self::assertSame('test-mode', $newValue);
}
}
```
-#### Enum data generator
+#### Enum data provider
-Use `PHPForge\Support\EnumDataGenerator` to build deterministic datasets from `UnitEnum::cases()` and `UIAwesome\Html\Helper\Enum::normalizeValue()`.
+Use `PHPForge\Support\EnumDataProvider` to build deterministic datasets from `UnitEnum::cases()` and normalized enum values.
##### Attribute fragment output (HTML)
@@ -202,10 +197,9 @@ declare(strict_types=1);
namespace App\Tests;
-use PHPForge\Support\EnumDataGenerator;
+use PHPForge\Support\EnumDataProvider;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
-use UIAwesome\Html\Helper\Enum;
use UnitEnum;
enum Size
@@ -214,17 +208,17 @@ enum Size
case Large;
}
-final class EnumDataGeneratorHtmlTest extends TestCase
+final class EnumDataProviderHtmlTest extends TestCase
{
public static function provideEnumAttributes(): array
{
- return EnumDataGenerator::cases(Size::class, 'data-size', true);
+ return EnumDataProvider::attributeCases(Size::class, 'data-size', true);
}
#[DataProvider('provideEnumAttributes')]
public function testBuildsAttributeFragment(UnitEnum $case, array $args, string $expected, string $message): void
{
- $normalized = (string) Enum::normalizeValue($case);
+ $normalized = $case->name;
$attributeFragment = " data-size=\"{$normalized}\"";
self::assertSame($expected, $attributeFragment, $message);
@@ -241,7 +235,7 @@ declare(strict_types=1);
namespace App\Tests;
-use PHPForge\Support\EnumDataGenerator;
+use PHPForge\Support\EnumDataProvider;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use UnitEnum;
@@ -252,11 +246,11 @@ enum State
case Disabled;
}
-final class EnumDataGeneratorEnumTest extends TestCase
+final class EnumDataProviderEnumTest extends TestCase
{
public static function provideEnumInstances(): array
{
- return EnumDataGenerator::cases(State::class, 'state', false);
+ return EnumDataProvider::attributeCases(State::class, 'state', false);
}
#[DataProvider('provideEnumInstances')]
@@ -276,10 +270,9 @@ declare(strict_types=1);
namespace App\Tests;
-use PHPForge\Support\EnumDataGenerator;
+use PHPForge\Support\EnumDataProvider;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
-use UIAwesome\Html\Helper\Enum;
enum Heading
{
@@ -287,17 +280,17 @@ enum Heading
case H2;
}
-final class EnumDataGeneratorTagCasesTest extends TestCase
+final class EnumDataProviderTagCasesTest extends TestCase
{
public static function provideTags(): array
{
- return EnumDataGenerator::tagCases(Heading::class, 'heading');
+ return EnumDataProvider::tagCases(Heading::class, 'heading');
}
#[DataProvider('provideTags')]
public function testProvidesNormalizedTag(Heading $case, string $normalized): void
{
- self::assertSame((string) Enum::normalizeValue($case), $normalized);
+ self::assertSame($case->name, $normalized);
}
}
```
diff --git a/composer.json b/composer.json
index 4602a9e..ab02bba 100644
--- a/composer.json
+++ b/composer.json
@@ -11,8 +11,7 @@
],
"license": "BSD-3-Clause",
"require": {
- "php": "^8.1",
- "ui-awesome/html-helper": "^0.3"
+ "php": "^8.1"
},
"require-dev": {
"infection/infection": "^0.27|^0.32",
diff --git a/docs/svgs/features-mobile.svg b/docs/svgs/features-mobile.svg
index ff6dfe5..1ca7778 100644
--- a/docs/svgs/features-mobile.svg
+++ b/docs/svgs/features-mobile.svg
@@ -55,7 +55,7 @@
Enum dataset generation
-
Generate deterministic datasets for enum-based attribute test cases via EnumDataGenerator.
+
Generate deterministic datasets for enum-based attribute test cases via EnumDataProvider.
Filesystem test management
@@ -67,7 +67,7 @@
PHPUnit-focused helpers
-
Use the TestSupport trait to share test utilities across the suite.
+
Use helper classes to share test utilities across the suite.
diff --git a/docs/svgs/features.svg b/docs/svgs/features.svg
index d971e80..b3a9610 100644
--- a/docs/svgs/features.svg
+++ b/docs/svgs/features.svg
@@ -52,7 +52,7 @@
Enum dataset generation
-
Generate deterministic datasets for enum-based attribute test cases via EnumDataGenerator.
+
Generate deterministic datasets for enum-based attribute test cases via EnumDataProvider.
Filesystem test management
@@ -64,7 +64,7 @@
PHPUnit-focused helpers
-
Use the TestSupport trait to share test utilities across the suite.
+
Use helper classes to share test utilities across the suite.
diff --git a/src/DirectoryCleaner.php b/src/DirectoryCleaner.php
new file mode 100644
index 0000000..e2c17b5
--- /dev/null
+++ b/src/DirectoryCleaner.php
@@ -0,0 +1,76 @@
+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)) {
+ self::clean($path);
+ @rmdir($path);
+ } else {
+ @unlink($path);
+ }
+ }
+
+ closedir($handle);
+ }
+}
diff --git a/src/EnumDataGenerator.php b/src/EnumDataProvider.php
similarity index 68%
rename from src/EnumDataGenerator.php
rename to src/EnumDataProvider.php
index aca3ff8..e7cb4d4 100644
--- a/src/EnumDataGenerator.php
+++ b/src/EnumDataProvider.php
@@ -4,23 +4,24 @@
namespace PHPForge\Support;
-use UIAwesome\Html\Helper\Enum;
+use BackedEnum;
use UnitEnum;
use function sprintf;
/**
- * Utility class for generating structured test data for enum-based attribute scenarios.
+ * Utility class for generating structured test data from enum cases.
*
- * Provides a standardized API for producing PHPUnit data sets built from `UnitEnum::cases()` and normalized values
- * produced by {@see Enum::normalizeValue()}.
+ * Provides deterministic PHPUnit datasets built from `UnitEnum::cases()` and normalized enum values.
*
- * The generated data sets can be used to verify attribute fragments or enum instance handling in consumer tests.
+ * Key features.
+ * - Builds attribute fragment datasets via {@see EnumDataProvider::attributeCases()}.
+ * - Builds tag datasets via {@see EnumDataProvider::tagCases()}.
*
* @copyright Copyright (C) 2026 Terabytesoftw.
* @license https://opensource.org/license/bsd-3-clause BSD 3-Clause License.
*/
-final class EnumDataGenerator
+final class EnumDataProvider
{
/**
* Generates test cases for enum-based attribute scenarios.
@@ -32,19 +33,20 @@ final class EnumDataGenerator
*
* @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`.
+ * @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 cases(string $enumClass, string|UnitEnum $attribute, bool $asHtml = true): array
+ public static function attributeCases(string $enumClass, string|UnitEnum $attribute, bool $asHtml = true): array
{
$cases = [];
- $attributeName = is_string($attribute) ? $attribute : sprintf('%s', Enum::normalizeValue($attribute));
+ $attributeName = is_string($attribute) ? $attribute : sprintf('%s', self::normalizeValue($attribute));
foreach ($enumClass::cases() as $case) {
- $normalizedValue = Enum::normalizeValue($case);
+ $normalizedValue = self::normalizeValue($case);
$key = "enum: {$normalizedValue}";
$expected = $asHtml ? " {$attributeName}=\"{$normalizedValue}\"" : $case;
@@ -66,25 +68,33 @@ public static function cases(string $enumClass, string|UnitEnum $attribute, bool
/**
* 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.
+ * 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 Structured test cases indexed by descriptive keys.
+ * @phpstan-return array
*/
public static function tagCases(string $enumClass, string $category): array
{
$data = [];
foreach ($enumClass::cases() as $case) {
- $value = (string) Enum::normalizeValue($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 @@
+ $args
*/
- private static function invokeMethod(object $object, string $method, array $args = []): mixed
+ public static function invokeMethod(object $object, string $method, array $args = []): mixed
{
$reflection = new ReflectionObject($object);
@@ -128,7 +126,7 @@ private static function invokeMethod(object $object, string $method, array $args
* @phpstan-param class-string $parentClass
* @phpstan-param array $args
*/
- private static function invokeParentMethod(
+ public static function invokeParentMethod(
object $object,
string $parentClass,
string $method,
@@ -144,63 +142,6 @@ private static function invokeParentMethod(
return $result ?? null;
}
- /**
- * Normalizes line endings to `"\n"` for cross-platform string assertions.
- *
- * Converts `"\r\n"` and `"\r"` to `"\n"` to keep string comparisons deterministic across operating systems.
- *
- * @param string $line Input string potentially containing Windows style line endings.
- *
- * @return string String with normalized line endings.
- */
- private static function normalizeLineEndings(string $line): string
- {
- return str_replace(["\r\n", "\r"], "\n", $line);
- }
-
- /**
- * Removes directory contents recursively while preserving `.gitignore` and `.gitkeep`.
- *
- * Iterates through `$basePath` and removes all files and subdirectories except `.`, `..`, `.gitignore`, and
- * `.gitkeep`. Subdirectories are processed recursively before removal.
- *
- * File and directory removals are attempted with error suppression.
- *
- * @param string $basePath Absolute path to the directory whose contents will be removed.
- *
- * @throws RuntimeException if the directory cannot be opened for reading.
- */
- private 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 === '..') {
- continue;
- }
-
- if ($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 a parent class property value via reflection.
*
@@ -217,7 +158,7 @@ private static function removeFilesFromDirectory(string $basePath): void
*
* @phpstan-param class-string $parentClass
*/
- private static function setInaccessibleParentProperty(
+ public static function setInaccessibleParentProperty(
object $object,
string $parentClass,
string $propertyName,
@@ -246,7 +187,7 @@ private static function setInaccessibleParentProperty(
*
* @throws ReflectionException if the property does not exist.
*/
- private static function setInaccessibleProperty(
+ public static function setInaccessibleProperty(
object $object,
string $propertyName,
mixed $value,
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/EnumDataGeneratorTest.php b/tests/EnumDataGeneratorTest.php
deleted file mode 100644
index c7d2a10..0000000
--- a/tests/EnumDataGeneratorTest.php
+++ /dev/null
@@ -1,150 +0,0 @@
- $enumClass
- */
- #[DataProviderExternal(EnumDataGeneratorProvider::class, 'casesParameters')]
- public function testCasesGenerateExpectedStructure(
- string $enumClass,
- string|UnitEnum $attribute,
- bool $asHtml,
- ): void {
- $data = match ($asHtml) {
- true => EnumDataGenerator::cases($enumClass, $attribute),
- false => EnumDataGenerator::cases($enumClass, $attribute, false),
- };
-
- self::assertNotEmpty($data, 'Should return at least one data set.');
-
- $attributeName = is_string($attribute) ? $attribute : sprintf('%s', Enum::normalizeValue($attribute));
-
- foreach ($enumClass::cases() as $case) {
- $expectedKey = 'enum: ' . Enum::normalizeValue($case);
-
- self::assertArrayHasKey(
- $expectedKey,
- $data,
- "Should include a dataset key for enum case '{$case->name}'.",
- );
-
- $row = $data[$expectedKey] ?? null;
-
- self::assertIsArray(
- $row,
- "Should return a dataset array for enum case '{$case->name}'.",
- );
- self::assertCount(
- 4,
- $row,
- "Should return a 4-tuple dataset for enum case '{$case->name}'.",
- );
-
- /** @var array{UnitEnum, array, string|UnitEnum, string} $row */
- [$value, $attributes, $expected, $message] = $row;
-
- self::assertSame(
- $case,
- $value,
- "Should store the enum case instance for '{$case->name}'.",
- );
- self::assertSame(
- [],
- $attributes,
- "Should store an empty attributes array for '{$case->name}'.",
- );
-
- $expectedValue = Enum::normalizeValue($case);
-
- if ($asHtml) {
- self::assertSame(
- " {$attributeName}=\"{$expectedValue}\"",
- $expected,
- "Should generate expected attribute fragment for '{$case->name}'.",
- );
- $expectedMessage = "Should return the '{$attributeName}' attribute value for enum case: {$expectedValue}.";
- } else {
- self::assertSame(
- $case,
- $expected,
- "Should return the enum instance as expected output for '{$case->name}'.",
- );
- $expectedMessage = "Should return the enum instance for case: {$expectedValue}.";
- }
-
- self::assertSame(
- $expectedMessage,
- $message,
- "Should store the expected message for '{$case->name}'.",
- );
- }
- }
-
- /**
- * @phpstan-param class-string $enumClass
- */
- #[DataProviderExternal(EnumDataGeneratorProvider::class, 'tagCasesParameters')]
- public function testTagCasesGenerateExpectedStructure(string $enumClass, string $category): void
- {
- $data = EnumDataGenerator::tagCases($enumClass, $category);
-
- self::assertNotEmpty($data, 'Should return at least one data set.');
-
- foreach ($enumClass::cases() as $case) {
- $expectedKey = Enum::normalizeValue($case) . " {$category} tag";
-
- self::assertArrayHasKey(
- $expectedKey,
- $data,
- "Should include a dataset key for enum case '{$case->name}'.",
- );
-
- $row = $data[$expectedKey] ?? null;
-
- self::assertIsArray(
- $row,
- "Should return a dataset array for enum case '{$case->name}'.",
- );
- self::assertCount(
- 2,
- $row,
- "Should return a 2-tuple dataset for enum case '{$case->name}'.",
- );
-
- /** @var array{UnitEnum, string} $row */
- [$value, $normalizedValue] = $row;
-
- self::assertSame(
- $case,
- $value,
- "Should store the enum case instance for '{$case->name}'.",
- );
- self::assertSame(
- Enum::normalizeValue($case),
- $normalizedValue,
- "Should store normalized value for '{$case->name}'.",
- );
- }
- }
-}
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..f44ffad
--- /dev/null
+++ b/tests/LineEndingNormalizerTest.php
@@ -0,0 +1,52 @@
+
+ * @return array
*/
public static function casesParameters(): array
{
return [
- 'as html' => [
+ 'as enum instance' => [
TestEnum::class,
'data-test',
- true,
+ false,
+ 'enum: FOO',
+ ' data-test="FOO"',
+ 'Should return the enum instance for case: FOO.',
],
- 'as enum instance' => [
+ 'as html' => [
TestEnum::class,
'data-test',
- false,
+ 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,
+ TestEnum::FOO,
true,
+ 'enum: FOO',
+ ' FOO="FOO"',
+ "Should return the 'FOO' attribute value for enum case: FOO.",
],
];
}
diff --git a/tests/Support/runtime/.gitignore b/tests/Support/runtime/.gitignore
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/Support/runtime/.gitkeep b/tests/Support/runtime/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/TestSupportTest.php b/tests/TestSupportTest.php
deleted file mode 100644
index fbc6d11..0000000
--- a/tests/TestSupportTest.php
+++ /dev/null
@@ -1,190 +0,0 @@
-expectException(RuntimeException::class);
- $this->expectExceptionMessage('Unable to open directory: non-existing-directory');
-
- self::removeFilesFromDirectory(__DIR__ . '/non-existing-directory');
- }
-}
From 30bc1e38c2e19e007f636dcaaf43c141c9ef40c9 Mon Sep 17 00:00:00 2001
From: StyleCI Bot
Date: Mon, 19 Jan 2026 20:32:59 +0000
Subject: [PATCH 8/9] Apply fixes from StyleCI
---
tests/LineEndingNormalizerTest.php | 1 +
1 file changed, 1 insertion(+)
diff --git a/tests/LineEndingNormalizerTest.php b/tests/LineEndingNormalizerTest.php
index f44ffad..e0aacce 100644
--- a/tests/LineEndingNormalizerTest.php
+++ b/tests/LineEndingNormalizerTest.php
@@ -32,6 +32,7 @@ public function testNormalizeConvertsLiteralBackslashNToNewline(): void
"Should normalize literal '\\n' sequences to 'LF'.",
);
}
+
public function testNormalizeReturnsUnchangedStringWhenNoNormalizationNeeded(): void
{
self::assertSame(
From 8c7d22f22bf6e6565413910614bde0438ed46039 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Mon, 19 Jan 2026 17:51:16 -0300
Subject: [PATCH 9/9] Apply fixed Coderabbitai review.
---
src/DirectoryCleaner.php | 3 ++-
src/ReflectionHelper.php | 2 --
tests/ReflectionHelperTest.php | 18 ++++++++++++++++++
.../Provider/EnumDataProviderProvider.php | 13 -------------
4 files changed, 20 insertions(+), 16 deletions(-)
diff --git a/src/DirectoryCleaner.php b/src/DirectoryCleaner.php
index e2c17b5..7bcc327 100644
--- a/src/DirectoryCleaner.php
+++ b/src/DirectoryCleaner.php
@@ -10,6 +10,7 @@
use function basename;
use function closedir;
use function is_dir;
+use function is_link;
use function opendir;
use function readdir;
use function rmdir;
@@ -63,7 +64,7 @@ public static function clean(string $basePath): void
$path = $basePath . DIRECTORY_SEPARATOR . $file;
- if (is_dir($path)) {
+ if (is_dir($path) && is_link($path) === false) {
self::clean($path);
@rmdir($path);
} else {
diff --git a/src/ReflectionHelper.php b/src/ReflectionHelper.php
index 870c505..96f326e 100644
--- a/src/ReflectionHelper.php
+++ b/src/ReflectionHelper.php
@@ -198,7 +198,5 @@ public static function setInaccessibleProperty(
$property = $class->getProperty($propertyName);
$property->setValue($object, $value);
}
-
- unset($class, $property);
}
}
diff --git a/tests/ReflectionHelperTest.php b/tests/ReflectionHelperTest.php
index 5661182..fd46888 100644
--- a/tests/ReflectionHelperTest.php
+++ b/tests/ReflectionHelperTest.php
@@ -44,6 +44,14 @@ public function testInaccessibleParentPropertyReturnsExpectedValue(): void
);
}
+ public function testInaccessiblePropertyReturnsNullForEmptyPropertyName(): void
+ {
+ self::assertNull(
+ ReflectionHelper::inaccessibleProperty(new TestClass(), ''),
+ 'Should return null when property name is empty.',
+ );
+ }
+
/**
* @throws ReflectionException
*/
@@ -118,4 +126,14 @@ public function testSetInaccessiblePropertySetsValueCorrectly(): void
"Should return 'foo' after setting the private property 'property' via 'setInaccessibleProperty' method.",
);
}
+
+ public function testThrowReflectionExceptionForNonExistentProperty(): void
+ {
+ $this->expectException(ReflectionException::class);
+ $this->expectExceptionMessage(
+ 'Property PHPForge\Support\Tests\Stub\TestClass::$nonExistent does not exist',
+ );
+
+ ReflectionHelper::inaccessibleProperty(new TestClass(), 'nonExistent');
+ }
}
diff --git a/tests/Support/Provider/EnumDataProviderProvider.php b/tests/Support/Provider/EnumDataProviderProvider.php
index a3a66b7..4a0a47f 100644
--- a/tests/Support/Provider/EnumDataProviderProvider.php
+++ b/tests/Support/Provider/EnumDataProviderProvider.php
@@ -48,17 +48,4 @@ public static function casesParameters(): array
],
];
}
-
- /**
- * @return array
- */
- public static function tagCasesParameters(): array
- {
- return [
- 'default category' => [
- TestEnum::class,
- 'element',
- ],
- ];
- }
}