-## Features
-
-✅ **Advanced Reflection Utilities**
-- Access and modify private and protected properties via reflection.
-- Invoke inaccessible methods to expand testing coverage.
-
-✅ **Cross-Platform String Assertions**
-- Avoid false positives and negatives caused by Windows vs. Unix line ending differences.
-- Normalize line endings for consistent string comparisons across platforms.
-
-✅ **File System Test Management**
-- Recursively clean files and directories for isolated test environments.
-- Safe removal that preserves Git-tracking files (for example, '.gitignore', '.gitkeep').
-
-## Quick start
+
+ Support utilities for PHPUnit-focused development
+ Reflection helpers, line ending normalization, and filesystem cleanup for deterministic tests.
+
-### System requirements
+## Features
-- [`PHP`](https://www.php.net/downloads) 8.1 or higher.
-- [`Composer`](https://getcomposer.org/download/) for dependency management.
-- [`PHPUnit`](https://phpunit.de/) for testing framework integration.
+
+
+
+
### Installation
-#### Method 1: Using [Composer](https://getcomposer.org/download/) (recommended)
-
-Install the extension.
-
```bash
-composer require --dev --prefer-dist php-forge/support:^0.2
-```
-
-#### Method 2: Manual installation
-
-Add to your `composer.json`.
-
-```json
-{
- "require-dev": {
- "php-forge/support": "^0.2"
- }
-}
+composer require php-forge/support:^0.2 --dev
```
-Then run.
+### Quick start
-```bash
-composer update
-```
+This package provides helper classes for PHPUnit tests.
-## Basic Usage
+It supports reflection-based access to non-public members, deterministic string comparisons across platforms, and filesystem cleanup for isolated test environments.
-### Accessing private properties
+#### Accessing private properties
```php
name;
+ $attributeFragment = " data-size=\"{$normalized}\"";
+
+ self::assertSame($expected, $attributeFragment, $message);
+ }
+}
+```
+
+##### Enum instance output (non-HTML)
+
+```php
+name, $normalized);
}
}
```
## Documentation
-For comprehensive testing guidance, see:
+For detailed configuration options and advanced usage.
+
+- [Testing Guide](docs/testing.md)
+- [Development Guide](docs/development.md)
+
+## Package information
-- 🧪 [Testing Guide](docs/testing.md)
+[](https://www.php.net/releases/8.1/en.php)
+[](https://packagist.org/packages/php-forge/support)
+[](https://packagist.org/packages/php-forge/support)
## Quality code
-[](https://github.com/php-forge/support/releases)
-[](https://packagist.org/packages/php-forge/support)
-[](https://codecov.io/gh/php-forge/support)
-[](https://github.com/php-forge/support/actions/workflows/static.yml)
-[](https://github.styleci.io/repos/661073468?branch=main)
+[](https://codecov.io/github/php-forge/support)
+[](https://github.com/php-forge/support/actions/workflows/static.yml)
+[](https://github.com/php-forge/support/actions/workflows/linter.yml)
+[](https://github.styleci.io/repos/779611775?branch=main)
## Our social networks
-[](https://x.com/Terabytesoftw)
+[](https://x.com/Terabytesoftw)
## License
-[](LICENSE.md)
+[](LICENSE)
diff --git a/composer.json b/composer.json
index 9c8063d..ab02bba 100644
--- a/composer.json
+++ b/composer.json
@@ -14,13 +14,14 @@
"php": "^8.1"
},
"require-dev": {
- "infection/infection": "^0.27|^0.31",
- "maglnet/composer-require-checker": "^4.7",
+ "infection/infection": "^0.27|^0.32",
+ "maglnet/composer-require-checker": "^4.1",
+ "phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-strict-rules": "^2.0.3",
"phpunit/phpunit": "^10.5",
- "rector/rector": "^2.1",
- "symplify/easy-coding-standard": "^12.5"
+ "rector/rector": "^2.2",
+ "symplify/easy-coding-standard": "^13.0"
},
"autoload": {
"psr-4": {
@@ -47,10 +48,20 @@
"scripts": {
"check-dependencies": "./vendor/bin/composer-require-checker check",
"ecs": "./vendor/bin/ecs --fix",
- "mutation": "./vendor/bin/infection --threads=4 --ignore-msi-with-no-mutations --only-covered --min-msi=100 --min-covered-msi=100",
- "mutation-static": "./vendor/bin/infection --threads=4 --ignore-msi-with-no-mutations --only-covered --min-msi=100 --min-covered-msi=100 --static-analysis-tool=phpstan",
+ "mutation": "./vendor/bin/infection --threads=4 --ignore-msi-with-no-mutations --min-msi=100 --min-covered-msi=100",
+ "mutation-static": "./vendor/bin/infection --threads=4 --ignore-msi-with-no-mutations --min-msi=100 --min-covered-msi=100 --static-analysis-tool=phpstan --static-analysis-tool-options='--memory-limit=-1'",
"rector": "./vendor/bin/rector process src",
- "static": "./vendor/bin/phpstan --memory-limit=512M",
+ "static": "./vendor/bin/phpstan --memory-limit=-1",
+ "sync-metadata": [
+ "curl -fsSL -o .editorconfig https://raw.githubusercontent.com/yii2-extensions/template/main/.editorconfig",
+ "curl -fsSL -o .gitattributes https://raw.githubusercontent.com/yii2-extensions/template/main/.gitattributes",
+ "curl -fsSL -o .gitignore https://raw.githubusercontent.com/yii2-extensions/template/main/.gitignore",
+ "curl -fsSL -o ecs.php https://raw.githubusercontent.com/yii2-extensions/template/main/ecs.php",
+ "curl -fsSL -o infection.json5 https://raw.githubusercontent.com/yii2-extensions/template/main/infection.json5",
+ "curl -fsSL -o phpstan.neon https://raw.githubusercontent.com/yii2-extensions/template/main/phpstan.neon",
+ "curl -fsSL -o phpunit.xml.dist https://raw.githubusercontent.com/yii2-extensions/template/main/phpunit.xml.dist",
+ "curl -fsSL -o rector.php https://raw.githubusercontent.com/yii2-extensions/template/main/rector.php"
+ ],
"tests": "./vendor/bin/phpunit"
}
}
diff --git a/docs/development.md b/docs/development.md
new file mode 100644
index 0000000..dfb83e9
--- /dev/null
+++ b/docs/development.md
@@ -0,0 +1,43 @@
+# Development
+
+This document describes development workflows and maintenance tasks for the project.
+
+## Sync Metadata
+
+To keep configuration files synchronized with the latest template updates, use the `sync-metadata` command. This command
+downloads the latest configuration files from the template repository.
+
+```bash
+composer run sync-metadata
+```
+
+### Updated Files
+
+This command updates the following configuration files:
+
+| File | Purpose |
+| ------------------ | -------------------------------------------- |
+| `.editorconfig` | Editor settings and code style configuration |
+| `.gitattributes` | Git attributes and file handling rules |
+| `.gitignore` | Git ignore patterns and exclusions |
+| `ecs.php` | Easy Coding Standard configuration |
+| `infection.json5` | Infection mutation testing configuration |
+| `phpstan.neon` | PHPStan static analysis configuration |
+| `phpunit.xml.dist` | PHPUnit test configuration |
+| `rector.php` | Rector refactoring configuration |
+
+### When to Run
+
+Run this command in the following scenarios:
+
+- **Periodic Updates** - Monthly or quarterly to benefit from template improvements.
+- **After Template Updates** - When the template repository has new configuration improvements.
+- **Before Major Releases** - Ensure your project uses the latest best practices.
+- **When Issues Occur** - If configuration files become outdated or incompatible.
+
+### Important Notes
+
+- This command overwrites existing configuration files with the latest versions from the template.
+- Ensure you have committed any custom configuration changes before running this command.
+- Review the updated files after syncing to ensure they work with your specific project needs.
+- Some projects may require customizations after syncing configuration files.
diff --git a/docs/svgs/features-mobile.svg b/docs/svgs/features-mobile.svg
new file mode 100644
index 0000000..1ca7778
--- /dev/null
+++ b/docs/svgs/features-mobile.svg
@@ -0,0 +1,75 @@
+
+
diff --git a/docs/svgs/features.svg b/docs/svgs/features.svg
new file mode 100644
index 0000000..b3a9610
--- /dev/null
+++ b/docs/svgs/features.svg
@@ -0,0 +1,72 @@
+
diff --git a/docs/testing.md b/docs/testing.md
index 2838bf5..c3f1ca8 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -1,50 +1,73 @@
# Testing
-## Checking dependencies
+This package provides a consistent set of [Composer](https://getcomposer.org/) scripts for local validation.
-This package uses [composer-require-checker](https://github.com/maglnet/ComposerRequireChecker) to check if all dependencies are correctly defined in `composer.json`.
+Tool references:
-To run the checker, execute the following command.
+- [Composer Require Checker](https://github.com/maglnet/ComposerRequireChecker) for dependency definition checks.
+- [Easy Coding Standard (ECS)](https://github.com/easy-coding-standard/easy-coding-standard) for coding standards.
+- [Infection](https://infection.github.io/) for mutation testing.
+- [PHPStan](https://phpstan.org/) for static analysis.
+- [PHPUnit](https://phpunit.de/) for unit tests.
-```shell
-composer run check-dependencies
+## Coding standards (ECS)
+
+Run Easy Coding Standard (ECS) and apply fixes.
+
+```bash
+composer run ecs
```
-## Easy coding standard
+## Dependency definition check
-The code is checked with [Easy Coding Standard](https://github.com/easy-coding-standard/easy-coding-standard) and
-[PHP CS Fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer). To run it.
+Verify that runtime dependencies are correctly declared in `composer.json`.
-```shell
-composer run ecs
+```bash
+composer run check-dependencies
```
-## Mutation testing
+## Mutation testing (Infection)
-Mutation testing is checked with [Infection](https://infection.github.io/). To run it.
+Run mutation testing.
-```shell
+```bash
composer run mutation
```
-With PHPStan analysis, it will also check for static analysis issues during mutation testing.
+Run mutation testing with static analysis enabled.
-```shell
+```bash
composer run mutation-static
```
-## Static analysis
+## Static analysis (PHPStan)
-The code is statically analyzed with [PHPStan](https://phpstan.org/). To run static analysis.
+Run static analysis.
-```shell
+```bash
composer run static
```
-## Unit Tests
+## Unit tests (PHPUnit)
+
+Run the full test suite.
+
+```bash
+composer run tests
+```
+
+## Passing extra arguments
+
+Composer scripts support forwarding additional arguments using `--`.
+
+Example: run a specific PHPUnit test or filter by name.
+
+```bash
+composer run tests -- --filter SvgTest
+```
-The code is tested with [PHPUnit](https://phpunit.de/). To run tests.
+Example: run PHPStan with a different memory limit:
-```shell
-composer run test
+```bash
+composer run static -- --memory-limit=512M
```
diff --git a/ecs.php b/ecs.php
index 00781d8..726b0dd 100644
--- a/ecs.php
+++ b/ecs.php
@@ -2,13 +2,11 @@
declare(strict_types=1);
-use PhpCsFixer\Fixer\ClassNotation\ClassDefinitionFixer;
-use PhpCsFixer\Fixer\ClassNotation\OrderedClassElementsFixer;
-use PhpCsFixer\Fixer\ClassNotation\OrderedTraitsFixer;
-use PhpCsFixer\Fixer\ClassNotation\VisibilityRequiredFixer;
-use PhpCsFixer\Fixer\Import\NoUnusedImportsFixer;
-use PhpCsFixer\Fixer\Import\OrderedImportsFixer;
+use PhpCsFixer\Fixer\ClassNotation\{ClassDefinitionFixer, OrderedClassElementsFixer, OrderedTraitsFixer};
+use PhpCsFixer\Fixer\Import\{NoUnusedImportsFixer, OrderedImportsFixer};
+use PhpCsFixer\Fixer\Phpdoc\PhpdocTypesOrderFixer;
use PhpCsFixer\Fixer\StringNotation\SingleQuoteFixer;
+use PhpCsFixer\Fixer\LanguageConstruct\NullableTypeDeclarationFixer;
use Symplify\EasyCodingStandard\Config\ECSConfig;
return ECSConfig::configure()
@@ -33,7 +31,7 @@
'construct',
'destruct',
'magic',
- 'phpunit',
+ 'method_protected_abstract',
'method_public',
'method_protected',
'method_private',
@@ -44,14 +42,19 @@
->withConfiguredRule(
OrderedImportsFixer::class,
[
- 'imports_order' => ['class', 'function', 'const'],
+ 'imports_order' => [
+ 'class',
+ 'function',
+ 'const',
+ ],
'sort_algorithm' => 'alpha',
],
)
->withConfiguredRule(
- VisibilityRequiredFixer::class,
+ PhpdocTypesOrderFixer::class,
[
- 'elements' => [],
+ 'sort_algorithm' => 'none',
+ 'null_adjustment' => 'always_last',
],
)
->withFileExtensions(['php'])
@@ -61,7 +64,7 @@
__DIR__ . '/tests',
],
)
- ->withPhpCsFixerSets(perCS20: true)
+ ->withPhpCsFixerSets(perCS30: true)
->withPreparedSets(
cleanCode: true,
comments: true,
@@ -75,4 +78,9 @@
OrderedTraitsFixer::class,
SingleQuoteFixer::class,
]
+ )
+ ->withSkip(
+ [
+ NullableTypeDeclarationFixer::class,
+ ]
);
diff --git a/infection.json5 b/infection.json5
new file mode 100644
index 0000000..1d33ec4
--- /dev/null
+++ b/infection.json5
@@ -0,0 +1,12 @@
+{
+ $schema: "./vendor/infection/infection/resources/schema.json",
+ logs: {
+ text: "php://stderr",
+ stryker: {
+ report: "main",
+ },
+ },
+ source: {
+ directories: ["src"],
+ },
+}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index e5d7313..521d51e 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,24 +1,24 @@
-
-
- tests
-
-
+
+
+ tests
+
+
-
-
- ./src
-
-
+
+
+ ./src
+
+
diff --git a/src/DirectoryCleaner.php b/src/DirectoryCleaner.php
new file mode 100644
index 0000000..7bcc327
--- /dev/null
+++ b/src/DirectoryCleaner.php
@@ -0,0 +1,77 @@
+getMessage($dirname),
+ );
+ }
+
+ while (($file = readdir($handle)) !== false) {
+ if ($file === '.' || $file === '..') {
+ continue;
+ }
+
+ if ($file === '.gitignore' || $file === '.gitkeep') {
+ continue;
+ }
+
+ $path = $basePath . DIRECTORY_SEPARATOR . $file;
+
+ if (is_dir($path) && is_link($path) === false) {
+ self::clean($path);
+ @rmdir($path);
+ } else {
+ @unlink($path);
+ }
+ }
+
+ closedir($handle);
+ }
+}
diff --git a/src/EnumDataProvider.php b/src/EnumDataProvider.php
new file mode 100644
index 0000000..e7cb4d4
--- /dev/null
+++ b/src/EnumDataProvider.php
@@ -0,0 +1,100 @@
+ $enumClass Enum class name implementing UnitEnum.
+ *
+ * @param string $enumClass Enum class name implementing UnitEnum.
+ * @param string|UnitEnum $attribute Attribute name used to build the expected fragment.
+ * @param bool $asHtml Whether to generate expected output as an attribute fragment or enum instance. Default is
+ * `true`.
+ *
+ * @return array Structured test cases indexed by a normalized enum value key.
+ *
+ * @phpstan-return array
+ */
+ public static function attributeCases(string $enumClass, string|UnitEnum $attribute, bool $asHtml = true): array
+ {
+ $cases = [];
+ $attributeName = is_string($attribute) ? $attribute : sprintf('%s', self::normalizeValue($attribute));
+
+ foreach ($enumClass::cases() as $case) {
+ $normalizedValue = self::normalizeValue($case);
+
+ $key = "enum: {$normalizedValue}";
+ $expected = $asHtml ? " {$attributeName}=\"{$normalizedValue}\"" : $case;
+ $message = $asHtml
+ ? "Should return the '{$attributeName}' attribute value for enum case: {$normalizedValue}."
+ : "Should return the enum instance for case: {$normalizedValue}.";
+
+ $cases[$key] = [
+ $case,
+ [],
+ $expected,
+ $message,
+ ];
+ }
+
+ return $cases;
+ }
+
+ /**
+ * Generates test cases for tag-related enum scenarios.
+ *
+ * Produces a dataset mapping descriptive keys to enum cases and their normalized string values, suitable for data
+ * provider methods in PHPUnit tests.
+ *
+ * @phpstan-param class-string $enumClass Enum class name implementing UnitEnum.
+ *
+ * @param string $enumClass Enum class name implementing UnitEnum.
+ * @param string $category Category label appended to the generated keys.
+ *
+ * @phpstan-return array
+ */
+ public static function tagCases(string $enumClass, string $category): array
+ {
+ $data = [];
+
+ foreach ($enumClass::cases() as $case) {
+ $value = self::normalizeValue($case);
+ $data[sprintf('%s %s tag', $value, $category)] = [$case, $value];
+ }
+
+ return $data;
+ }
+
+ private static function normalizeValue(UnitEnum $enum): string
+ {
+ return match ($enum instanceof BackedEnum) {
+ true => (string) $enum->value,
+ false => $enum->name,
+ };
+ }
+}
diff --git a/src/Exception/Message.php b/src/Exception/Message.php
new file mode 100644
index 0000000..77d239a
--- /dev/null
+++ b/src/Exception/Message.php
@@ -0,0 +1,59 @@
+getMessage('/path/to/dir'));
+ * ```
+ */
+ public function getMessage(int|string ...$argument): string
+ {
+ return sprintf($this->value, ...$argument);
+ }
+}
diff --git a/src/LineEndingNormalizer.php b/src/LineEndingNormalizer.php
new file mode 100644
index 0000000..587d48b
--- /dev/null
+++ b/src/LineEndingNormalizer.php
@@ -0,0 +1,43 @@
+getProperty($propertyName)->getValue($object);
+ }
+
+ /**
+ * Retrieves a property value from a class or object.
+ *
+ * Uses reflection to read `$propertyName` from the provided class name or object instance.
+ *
+ * @param object|string $object Name of the class or object instance from which to retrieve the property value.
+ * @param string $propertyName Name of the property to access.
+ *
+ * @throws ReflectionException if the property does not exist.
+ *
+ * @return mixed Value of the specified property, or `null` when `$propertyName` is empty.
+ *
+ * @phpstan-param class-string|object $object
+ */
+ public static function inaccessibleProperty(string|object $object, string $propertyName): mixed
+ {
+ $class = new ReflectionClass($object);
+
+ if ($propertyName !== '') {
+ $property = $class->getProperty($propertyName);
+ $result = is_string($object) ? $property->getValue() : $property->getValue($object);
+ }
+
+ return $result ?? null;
+ }
+
+ /**
+ * Invokes a method via reflection.
+ *
+ * Uses reflection to invoke `$method` on `$object` with `$args`.
+ *
+ * @param object $object Object instance containing the method to invoke.
+ * @param string $method Name of the method to invoke.
+ * @param array $args Arguments to pass to the method invocation.
+ *
+ * @throws ReflectionException if the method does not exist.
+ *
+ * @return mixed Value of the invoked method, or `null` when `$method` is empty.
+ *
+ * @phpstan-param array $args
+ */
+ public static function invokeMethod(object $object, string $method, array $args = []): mixed
+ {
+ $reflection = new ReflectionObject($object);
+
+ if ($method !== '') {
+ $method = $reflection->getMethod($method);
+ $result = $method->invokeArgs($object, $args);
+ }
+
+ return $result ?? null;
+ }
+
+ /**
+ * Invokes a parent class method via reflection.
+ *
+ * Uses `$parentClass` as the declaring class context to invoke `$method` on `$object` with `$args`.
+ *
+ * @param object $object Object instance containing the method to invoke.
+ * @param string $parentClass Name of the parent class containing the method.
+ * @param string $method Name of the method to invoke.
+ * @param array $args Arguments to pass to the method invocation.
+ *
+ * @throws ReflectionException if the method does not exist.
+ *
+ * @return mixed Value of the invoked method, or `null` when `$method` is empty.
+ *
+ * @phpstan-param class-string $parentClass
+ * @phpstan-param array $args
+ */
+ public static function invokeParentMethod(
+ object $object,
+ string $parentClass,
+ string $method,
+ array $args = [],
+ ): mixed {
+ $reflection = new ReflectionClass($parentClass);
+
+ if ($method !== '') {
+ $method = $reflection->getMethod($method);
+ $result = $method->invokeArgs($object, $args);
+ }
+
+ return $result ?? null;
+ }
+
+ /**
+ * Sets a parent class property value via reflection.
+ *
+ * Uses `$parentClass` as the declaring class context to assign `$value` to `$propertyName` on `$object`.
+ *
+ * This method is a no-op when `$propertyName` is empty.
+ *
+ * @param object $object Object instance whose parent property will be set.
+ * @param string $parentClass Name of the parent class containing the property.
+ * @param string $propertyName Name of the property to set.
+ * @param mixed $value Value to assign to the property.
+ *
+ * @throws ReflectionException if the property does not exist.
+ *
+ * @phpstan-param class-string $parentClass
+ */
+ public static function setInaccessibleParentProperty(
+ object $object,
+ string $parentClass,
+ string $propertyName,
+ mixed $value,
+ ): void {
+ $class = new ReflectionClass($parentClass);
+
+ if ($propertyName !== '') {
+ $property = $class->getProperty($propertyName);
+ $property->setValue($object, $value);
+ }
+
+ unset($class, $property);
+ }
+
+ /**
+ * Sets a property value via reflection.
+ *
+ * Assigns `$value` to `$propertyName` on `$object`.
+ *
+ * This method is a no-op when `$propertyName` is empty.
+ *
+ * @param object $object Object instance whose property will be set.
+ * @param string $propertyName Name of the property to set.
+ * @param mixed $value Value to assign to the property.
+ *
+ * @throws ReflectionException if the property does not exist.
+ */
+ public static function setInaccessibleProperty(
+ object $object,
+ string $propertyName,
+ mixed $value,
+ ): void {
+ $class = new ReflectionClass($object);
+
+ if ($propertyName !== '') {
+ $property = $class->getProperty($propertyName);
+ $property->setValue($object, $value);
+ }
+ }
+}
diff --git a/src/TestSupport.php b/src/TestSupport.php
deleted file mode 100644
index 567c9b2..0000000
--- a/src/TestSupport.php
+++ /dev/null
@@ -1,281 +0,0 @@
-getProperty($propertyName)->getValue($object);
- }
-
- /**
- * Retrieves the value of an inaccessible property from an object or class instance.
- *
- * Uses reflection to access the specified property of the given object or class, allowing tests to inspect or
- * assert the value of private or protected properties that are otherwise inaccessible.
- *
- * This method is useful for verifying the internal state of objects during testing, especially when direct access
- * to the property is not possible due to visibility constraints.
- *
- * @param object|string $object Name of the class or object instance from which to retrieve the property value.
- * @param string $propertyName Name of the property to access.
- *
- * @throws ReflectionException if the property does not exist or is inaccessible.
- *
- * @return mixed Value of the specified property, or `null` if the property name is empty.
- *
- * @phpstan-param class-string|object $object
- */
- public static function inaccessibleProperty(string|object $object, string $propertyName): mixed
- {
- $class = new ReflectionClass($object);
-
- if ($propertyName !== '') {
- $property = $class->getProperty($propertyName);
- $result = is_string($object) ? $property->getValue() : $property->getValue($object);
- }
-
- return $result ?? null;
- }
-
- /**
- * Invokes an inaccessible method on the given object instance with the specified arguments.
- *
- * Uses reflection to access and invoke a private or protected method of the provided object, allowing tests to
- * execute logic that is not publicly accessible.
- *
- * This is useful for verifying internal behavior or side effects during unit testing.
- *
- * @param object $object Object instance containing the method to invoke.
- * @param string $method Name of the method to invoke.
- * @param array $args Arguments to pass to the method invocation.
- *
- * @throws ReflectionException if the method does not exist or is inaccessible.
- *
- * @return mixed Value of the invoked method, or `null` if the method name is empty.
- *
- * @phpstan-param array $args
- */
- public static function invokeMethod(object $object, string $method, array $args = []): mixed
- {
- $reflection = new ReflectionObject($object);
-
- if ($method !== '') {
- $method = $reflection->getMethod($method);
- $result = $method->invokeArgs($object, $args);
- }
-
- return $result ?? null;
- }
-
- /**
- * Invokes an inaccessible method from a parent class on the given object instance with the specified arguments.
- *
- * Uses reflection to access and invoke a private or protected method defined in the specified parent class,
- * allowing tests to execute logic that is not publicly accessible from the child class.
- *
- * This is useful for verifying inherited behavior or side effects during unit testing of subclasses.
- *
- * @param object $object Object instance containing the method to invoke.
- * @param string $parentClass Name of the parent class containing the method.
- * @param string $method Name of the method to invoke.
- * @param array $args Arguments to pass to the method invocation.
- *
- * @throws ReflectionException if the method does not exist or is inaccessible in the parent class.
- *
- * @return mixed Value of the invoked method, or `null` if the method name is empty.
- *
- * @phpstan-param class-string $parentClass
- * @phpstan-param array $args
- */
- public static function invokeParentMethod(
- object $object,
- string $parentClass,
- string $method,
- array $args = [],
- ): mixed {
- $reflection = new ReflectionClass($parentClass);
-
- if ($method !== '') {
- $method = $reflection->getMethod($method);
- $result = $method->invokeArgs($object, $args);
- }
-
- return $result ?? null;
- }
-
- /**
- * Normalizes line endings to Unix style ('\n') for cross-platform string assertions.
- *
- * Converts Windows style ('\r\n') line endings to Unix style ('\n') to ensure consistent string comparisons across
- * different operating systems during testing.
- *
- * This method is useful for eliminating false negatives in assertions caused by platform-specific line endings.
- *
- * @param string $line Input string potentially containing Windows style line endings.
- *
- * @return string String with normalized Unix style line endings.
- */
- public static function normalizeLineEndings(string $line): string
- {
- return str_replace(["\r\n", "\r"], "\n", $line);
- }
-
- /**
- * Removes all files and directories recursively from the specified base path, excluding '.gitignore' and
- * '.gitkeep'.
- *
- * Opens the given directory, iterates through its contents, and removes all files and subdirectories except for
- * special entries ('.', '..', '.gitignore', '.gitkeep').
- *
- * Subdirectories are processed recursively before removal.
- *
- * @param string $basePath Absolute path to the directory whose contents will be removed.
- *
- * @throws RuntimeException if the directory cannot be opened for reading.
- */
- public static function removeFilesFromDirectory(string $basePath): void
- {
- $handle = @opendir($basePath);
-
- if ($handle === false) {
- $dirname = basename($basePath);
- throw new RuntimeException("Unable to open directory: $dirname");
- }
-
- while (($file = readdir($handle)) !== false) {
- if ($file === '.' || $file === '..' || $file === '.gitignore' || $file === '.gitkeep') {
- continue;
- }
-
- $path = $basePath . DIRECTORY_SEPARATOR . $file;
-
- if (is_dir($path)) {
- self::removeFilesFromDirectory($path);
- @rmdir($path);
- } else {
- @unlink($path);
- }
- }
-
- closedir($handle);
- }
-
- /**
- * Sets the value of an inaccessible property on a parent class instance.
- *
- * Uses reflection to assign the specified value to a private or protected property defined in the given parent
- * class, enabling tests to modify internal state that is otherwise inaccessible due to visibility constraints in
- * inheritance scenarios.
- *
- * This method is useful for testing scenarios that require direct manipulation of parent class internals.
- *
- * @param object $object Object instance whose parent property will be set.
- * @param string $parentClass Name of the parent class containing the property.
- * @param string $propertyName Name of the property to set.
- * @param mixed $value Value to assign to the property.
- *
- * @throws ReflectionException if the property does not exist or is inaccessible in the parent class.
- *
- * @phpstan-param class-string $parentClass
- */
- public static function setInaccessibleParentProperty(
- object $object,
- string $parentClass,
- string $propertyName,
- mixed $value,
- ): void {
- $class = new ReflectionClass($parentClass);
-
- if ($propertyName !== '') {
- $property = $class->getProperty($propertyName);
- $property->setValue($object, $value);
- }
-
- unset($class, $property);
- }
-
- /**
- * Sets the value of an inaccessible property on the given object instance.
- *
- * Uses reflection to assign the specified value to a private or protected property of the provided object enabling
- * tests to modify internal state that is otherwise inaccessible due to visibility constraints.
- *
- * This method is useful for testing scenarios that require direct manipulation of object internals.
- *
- * @param object $object Object instance whose property will be set.
- * @param string $propertyName Name of the property to set.
- * @param mixed $value Value to assign to the property.
- *
- * @throws ReflectionException if the property does not exist or is inaccessible.
- */
- public static function setInaccessibleProperty(
- object $object,
- string $propertyName,
- mixed $value,
- ): void {
- $class = new ReflectionClass($object);
-
- if ($propertyName !== '') {
- $property = $class->getProperty($propertyName);
- $property->setValue($object, $value);
- }
-
- unset($class, $property);
- }
-}
diff --git a/tests/DirectoryCleanerTest.php b/tests/DirectoryCleanerTest.php
new file mode 100644
index 0000000..ed3cfb5
--- /dev/null
+++ b/tests/DirectoryCleanerTest.php
@@ -0,0 +1,71 @@
+expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Unable to open directory: non-existing-directory');
+
+ DirectoryCleaner::clean(__DIR__ . '/non-existing-directory');
+ }
+}
diff --git a/tests/EnumDataProviderTest.php b/tests/EnumDataProviderTest.php
new file mode 100644
index 0000000..ff8fb95
--- /dev/null
+++ b/tests/EnumDataProviderTest.php
@@ -0,0 +1,103 @@
+ $enumClass
+ */
+ #[DataProviderExternal(EnumDataProviderProvider::class, 'casesParameters')]
+ public function testCasesGenerateExpectedStructure(
+ string $enumClass,
+ string|UnitEnum $attribute,
+ bool $asHtml,
+ string $expectedKeyCase,
+ string $expectedAttributeCase,
+ string $expectedMessage,
+ ): void {
+ $data = match ($asHtml) {
+ true => EnumDataProvider::attributeCases($enumClass, $attribute),
+ false => EnumDataProvider::attributeCases($enumClass, $attribute, false),
+ };
+
+ self::assertNotEmpty(
+ $data,
+ 'Should return at least one data set.',
+ );
+ self::assertArrayHasKey(
+ $expectedKeyCase,
+ $data,
+ 'Should contain expected enum case in dataset.',
+ );
+ self::assertInstanceOf(
+ $enumClass,
+ $data[$expectedKeyCase][0] ?? null,
+ 'Should return expected enum case instance.',
+ );
+
+ if ($asHtml) {
+ self::assertSame(
+ $expectedAttributeCase,
+ $data[$expectedKeyCase][2],
+ 'Should return expected attribute value for enum case.',
+ );
+ }
+
+ self::assertSame(
+ $expectedMessage,
+ $data[$expectedKeyCase][3],
+ 'Should return expected message for enum case.',
+ );
+ }
+
+ public function testTagCasesGenerateExpectedStructure(): void
+ {
+ $data = EnumDataProvider::tagCases(TestEnum::class, 'element');
+
+ self::assertNotEmpty(
+ $data,
+ 'Should return at least one data set.',
+ );
+ self::assertSame(
+ [
+ 'BAR element tag' => [
+ TestEnum::BAR,
+ 'BAR',
+ ],
+ 'FOO element tag' => [
+ TestEnum::FOO,
+ 'FOO',
+ ],
+ ],
+ $data,
+ 'Should return expected tag cases dataset.',
+ );
+ }
+}
diff --git a/tests/LineEndingNormalizerTest.php b/tests/LineEndingNormalizerTest.php
new file mode 100644
index 0000000..e0aacce
--- /dev/null
+++ b/tests/LineEndingNormalizerTest.php
@@ -0,0 +1,53 @@
+expectException(ReflectionException::class);
+ $this->expectExceptionMessage(
+ 'Property PHPForge\Support\Tests\Stub\TestClass::$nonExistent does not exist',
+ );
+
+ ReflectionHelper::inaccessibleProperty(new TestClass(), 'nonExistent');
+ }
+}
diff --git a/tests/Stub/TestBackedEnum.php b/tests/Stub/TestBackedEnum.php
new file mode 100644
index 0000000..3041e32
--- /dev/null
+++ b/tests/Stub/TestBackedEnum.php
@@ -0,0 +1,19 @@
+
+ */
+ public static function casesParameters(): array
+ {
+ return [
+ 'as enum instance' => [
+ TestEnum::class,
+ 'data-test',
+ false,
+ 'enum: FOO',
+ ' data-test="FOO"',
+ 'Should return the enum instance for case: FOO.',
+ ],
+ 'as html' => [
+ TestEnum::class,
+ 'data-test',
+ true,
+ 'enum: BAR',
+ ' data-test="BAR"',
+ "Should return the 'data-test' attribute value for enum case: BAR.",
+ ],
+ 'attribute as enum instance' => [
+ TestEnum::class,
+ TestEnum::FOO,
+ true,
+ 'enum: FOO',
+ ' FOO="FOO"',
+ "Should return the 'FOO' attribute value for enum case: FOO.",
+ ],
+ ];
+ }
+}
diff --git a/tests/TestSupportTest.php b/tests/TestSupportTest.php
deleted file mode 100644
index 5fd41d5..0000000
--- a/tests/TestSupportTest.php
+++ /dev/null
@@ -1,164 +0,0 @@
-assertFileDoesNotExist(
- "{$dir}/test.txt",
- "File 'test.txt' should not exist after 'removeFilesFromDirectory' method is called.",
- );
- $this->assertFileDoesNotExist(
- "{$dir}/subdir/test.txt",
- "File 'subdir/test.txt' should not exist after 'removeFilesFromDirectory' method is called.",
- );
- }
-
- /**
- * @throws ReflectionException
- */
- public function testReturnInaccessiblePropertyValueWhenPropertyIsPrivate(): void
- {
- self::assertSame(
- 'value',
- self::inaccessibleProperty(new TestClass(), 'property'),
- "Should return the value of the private property 'property' when accessed via reflection.",
- );
- }
-
- /**
- * @throws ReflectionException
- */
- public function testReturnValueWhenInvokingInaccessibleMethod(): void
- {
- $this->assertSame(
- 'value',
- self::invokeMethod(new TestClass(), 'inaccessibleMethod'),
- "Should return 'value' when invoking the inaccessible method 'inaccessibleParentMethod' on 'TestClass' " .
- 'via reflection.',
- );
- }
-
- /**
- * @throws ReflectionException
- */
- public function testReturnValueWhenInvokingInaccessibleParentMethod(): void
- {
- $this->assertSame(
- 'valueParent',
- self::invokeParentMethod(
- new TestClass(),
- TestBaseClass::class,
- 'inaccessibleParentMethod',
- ),
- "Should return 'valueParent' when invoking the inaccessible parent method 'inaccessibleParentMethod' on " .
- "'TestClass' via reflection.",
- );
- }
-
- /**
- * @throws ReflectionException
- */
- public function testSetInaccessibleParentProperty(): void
- {
- $object = new TestClass();
-
- self::setInaccessibleParentProperty($object, TestBaseClass::class, 'propertyParent', 'foo');
-
- $this->assertSame(
- 'foo',
- self::inaccessibleParentProperty($object, TestBaseClass::class, 'propertyParent'),
- "Should return 'foo' after setting the parent property 'propertyParent' via " .
- "'setInaccessibleParentProperty' method.",
- );
- }
-
- /**
- * @throws ReflectionException
- */
- public function testSetInaccessiblePropertySetsValueCorrectly(): void
- {
- $object = new TestClass();
-
- self::setInaccessibleProperty($object, 'property', 'foo');
-
- $this->assertSame(
- 'foo',
- self::inaccessibleProperty($object, 'property'),
- "Should return 'foo' after setting the private property 'property' via 'setInaccessibleProperty' method.",
- );
- }
-
- public function testThrowRuntimeExceptionWhenRemoveFilesFromDirectoryNonExistingDirectory(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Unable to open directory: non-existing-directory');
-
- self::removeFilesFromDirectory(__DIR__ . '/non-existing-directory');
- }
-}