From ef014bd769b00705505b0269526ea1da14edbf72 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Mon, 18 Aug 2025 11:12:11 -0400
Subject: [PATCH 01/13] fix(support): Refactor project structure and update
dependencies.
---
.gitattributes | 27 ++--
.gitignore | 40 +++---
LICENSE | 16 ---
LICENSE.md | 27 ++++
README.md | 130 ++++++++++++--------
composer.json | 28 +++--
docs/testing.md | 27 +++-
ecs.php | 91 +++++++++-----
phpstan.neon | 21 ++++
phpunit.xml.dist | 41 ++++---
psalm.xml | 17 ---
rector.php | 28 +++++
runtime/.gitignore | 2 +
src/Assert.php | 231 +++++++++++++++++++++++++++++------
tests/AssertTest.php | 153 +++++++++++++++++------
tests/Stub/TestBaseClass.php | 23 ++++
tests/Stub/TestClass.php | 23 ++++
17 files changed, 672 insertions(+), 253 deletions(-)
delete mode 100644 LICENSE
create mode 100644 LICENSE.md
create mode 100644 phpstan.neon
delete mode 100644 psalm.xml
create mode 100644 rector.php
create mode 100644 runtime/.gitignore
create mode 100644 tests/Stub/TestBaseClass.php
create mode 100644 tests/Stub/TestClass.php
diff --git a/.gitattributes b/.gitattributes
index 65a6a4e..76da65a 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -21,19 +21,20 @@
*.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
-/.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
-/phpunit.xml.dist export-ignore
-/psalm.xml export-ignore
-/rector.php export-ignore
-/tests export-ignore
diff --git a/.gitignore b/.gitignore
index b5300c1..bb7ff36 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,24 +1,34 @@
-#code coverage
-/code_coverage
+# phpstorm project files
+.idea
+
+# netbeans project files
+nbproject
+
+# zend studio for eclipse project files
+.buildpath
+.project
+.settings
+
+# windows thumbnail cache
+Thumbs.db
+
+# Mac DS_Store Files
+.DS_Store
# composer vendor dir
/vendor
-/composer.lock
-#node_modules
-/node_modules
+# composer lock file
+/composer.lock
-# phpstorm project files
-.idea
+# composer itself is not needed
+composer.phar
-# phpunit
-.phpunit.cache
-.phpunit.result.cache
-phpunit.xml
+# phpunit itself is not needed
phpunit.phar
-#yii3 config packages
-/config/packages
+# local phpunit config
+/phpunit.xml
-# windows thumbnail cache
-Thumbs.db
+# phpunit cache
+.phpunit.result.cache
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index 136b000..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,16 +0,0 @@
-MIT License
-
-Copyright (c) 2024 by Wilmer Arámbula (https://github.com/terabytesoftw) All rights reserved.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
-documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
-rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
-persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
-Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
-COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..a76279a
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,27 @@
+# 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 bb14c8f..9994916 100644
--- a/README.md
+++ b/README.md
@@ -27,143 +27,169 @@
-## Installation
+## Features
-The preferred way to install this extension is through [composer](https://getcomposer.org/download/).
+✅ **Advanced Reflection Utilities**
+- Access and modify private/protected properties via reflection.
+- Invoke inaccessible methods for comprehensive testing coverage.
-Either run
+✅ **Cross-Platform String Assertions**
+- Eliminate false negatives from Windows/Unix line ending differences.
+- Normalize line endings for consistent string comparisons across platforms.
-```shell
+✅ **File System Test Management**
+- Recursive file and directory cleanup for isolated test environments.
+- Safe removal operations preserving Git tracking files.
+
+## Quick start
+
+### System requirements
+
+- [`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 --prefer-dist php-forge/support
```
-or add
+#### Method 2: Manual installation
+
+Add to your `composer.json`.
```json
-"php-forge/support": "^0.1"
+{
+ "require-dev": {
+ "php-forge/support": "^0.1"
+ }
+}
```
-to the require-dev section of your `composer.json` file.
+Then run.
-## Usage
+```bash
+composer update
+```
-### Equals without line ending
+## Basic Usage
+
+### Cross-platform string equality
```php
assertSame('bar', Assert::inaccessibleProperty($object, 'foo'));
+// Access private properties for testing
+$value = Assert::inaccessibleProperty($object, 'secretValue');
+$this->assertSame('hidden', $value);
```
-### Invoke method
+### Invoking protected methods
```php
assertSame('foo', Assert::invokeMethod($object, 'foo'));
+// Test protected method behavior
+$result = Assert::invokeMethod($object, 'calculate', [5, 3]);
+$this->assertSame(8, $result);
```
-### Set inaccessible property
+### Modifying inaccessible properties
```php
assertSame('baz', Assert::inaccessibleProperty($object, 'foo'));
+$newValue = Assert::inaccessibleProperty($object, 'config');
+$this->assertSame('test-mode', $newValue);
```
-### Remove files from directory
+### Test environment cleanup
```php
assertFileDoesNotExist($dir . '/test.txt');
+// Clean up test artifacts (preserves .gitignore and .gitkeep)
+Assert::removeFilesFromDirectory($testDir);
-rmdir(__DIR__ . '/runtime');
+$this->assertFileDoesNotExist($testDir . '/test.txt');
+$this->assertDirectoryDoesNotExist($testDir . '/subdir');
```
-## Support versions
+## Documentation
-[](https://www.php.net/releases/8.1/en.php)
-[](https://yiiframework.com)
+For comprehensive testing guidance, see:
-## Testing
-
-[Check the documentation testing](/docs/testing.md) to learn about testing.
+- 🧪 [Testing Guide](docs/testing.md)
## Our social networks
-[](https://twitter.com/Terabytesoftw)
+[](https://x.com/Terabytesoftw)
## License
+[](LICENSE.md)
+
The MIT License. Please see [License File](LICENSE.md) for more information.
diff --git a/composer.json b/composer.json
index 5eda0d1..98cc91a 100644
--- a/composer.json
+++ b/composer.json
@@ -9,16 +9,18 @@
"testing",
"tests"
],
- "license": "MIT",
+ "license": "BSD-3-Clause",
"require": {
"php": "^8.1",
"phpunit/phpunit": "^10.5"
},
"require-dev": {
+ "infection/infection": "^0.27|^0.31",
"maglnet/composer-require-checker": "^4.7",
- "roave/infection-static-analysis-plugin": "^1.34",
- "symplify/easy-coding-standard": "^12.1",
- "vimeo/psalm": "^5.20"
+ "phpstan/phpstan-strict-rules": "^2.0.3",
+ "symplify/easy-coding-standard": "^12.5",
+ "phpstan/phpstan" : "^2.1",
+ "rector/rector": "^2.1"
},
"autoload": {
"psr-4": {
@@ -38,17 +40,17 @@
"config": {
"sort-packages": true,
"allow-plugins": {
- "infection/extension-installer": true
+ "infection/extension-installer": true,
+ "phpstan/extension-installer": true
}
},
"scripts": {
- "check-dependencies": "composer-require-checker",
- "easy-coding-standard": "ecs --fix",
- "mutation": [
- "Composer\\Config::disableProcessTimeout",
- "roave-infection-static-analysis-plugin --threads=2 --only-covered"
- ],
- "psalm": "psalm",
- "test": "phpunit"
+ "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",
+ "rector": "./vendor/bin/rector process src",
+ "static": "./vendor/bin/phpstan --memory-limit=512M",
+ "tests": "./vendor/bin/phpunit"
}
}
diff --git a/docs/testing.md b/docs/testing.md
index b2aa5df..2838bf5 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -4,31 +4,46 @@
This package uses [composer-require-checker](https://github.com/maglnet/ComposerRequireChecker) to check if all dependencies are correctly defined in `composer.json`.
-To run the checker, execute the following command:
+To run the checker, execute the following command.
```shell
composer run check-dependencies
```
+## Easy coding standard
+
+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.
+
+```shell
+composer run ecs
+```
+
## Mutation testing
-Mutation testing is checked with [Infection](https://infection.github.io/). To run it:
+Mutation testing is checked with [Infection](https://infection.github.io/). To run it.
```shell
composer run mutation
```
+With PHPStan analysis, it will also check for static analysis issues during mutation testing.
+
+```shell
+composer run mutation-static
+```
+
## Static analysis
-The code is statically analyzed with [Psalm](https://psalm.dev/). To run static analysis:
+The code is statically analyzed with [PHPStan](https://phpstan.org/). To run static analysis.
```shell
-composer run psalm
+composer run static
```
-## Unit tests
+## Unit Tests
-The code is tested with [PHPUnit](https://phpunit.de/). To run tests:
+The code is tested with [PHPUnit](https://phpunit.de/). To run tests.
```shell
composer run test
diff --git a/ecs.php b/ecs.php
index 78342b8..00781d8 100644
--- a/ecs.php
+++ b/ecs.php
@@ -5,43 +5,74 @@
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\StringNotation\SingleQuoteFixer;
use Symplify\EasyCodingStandard\Config\ECSConfig;
-use Symplify\EasyCodingStandard\ValueObject\Set\SetList;
-return function (ECSConfig $ecsConfig): void {
- $ecsConfig->paths(
+return ECSConfig::configure()
+ ->withConfiguredRule(
+ ClassDefinitionFixer::class,
+ [
+ 'space_before_parenthesis' => true,
+ ],
+ )
+ ->withConfiguredRule(
+ OrderedClassElementsFixer::class,
+ [
+ 'order' => [
+ 'use_trait',
+ 'constant_public',
+ 'constant_protected',
+ 'constant_private',
+ 'case',
+ 'property_public',
+ 'property_protected',
+ 'property_private',
+ 'construct',
+ 'destruct',
+ 'magic',
+ 'phpunit',
+ 'method_public',
+ 'method_protected',
+ 'method_private',
+ ],
+ 'sort_algorithm' => 'alpha',
+ ],
+ )
+ ->withConfiguredRule(
+ OrderedImportsFixer::class,
+ [
+ 'imports_order' => ['class', 'function', 'const'],
+ 'sort_algorithm' => 'alpha',
+ ],
+ )
+ ->withConfiguredRule(
+ VisibilityRequiredFixer::class,
+ [
+ 'elements' => [],
+ ],
+ )
+ ->withFileExtensions(['php'])
+ ->withPaths(
[
__DIR__ . '/src',
__DIR__ . '/tests',
- ]
- );
-
- // this way you add a single rule
- $ecsConfig->rules(
+ ],
+ )
+ ->withPhpCsFixerSets(perCS20: true)
+ ->withPreparedSets(
+ cleanCode: true,
+ comments: true,
+ docblocks: true,
+ namespaces: true,
+ strict: true,
+ )
+ ->withRules(
[
- OrderedClassElementsFixer::class,
- OrderedTraitsFixer::class,
NoUnusedImportsFixer::class,
+ OrderedTraitsFixer::class,
+ SingleQuoteFixer::class,
]
);
-
- // this way you can add sets - group of rules
- $ecsConfig->sets(
- [
- // run and fix, one by one
- SetList::DOCBLOCK,
- SetList::NAMESPACES,
- SetList::COMMENTS,
- SetList::PSR_12,
- ]
- );
-
- // this way configures a rule
- $ecsConfig->ruleWithConfiguration(
- ClassDefinitionFixer::class,
- [
- 'space_before_parenthesis' => true,
- ],
- );
-};
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..63c0f31
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,21 @@
+includes:
+ - phar://phpstan.phar/conf/bleedingEdge.neon
+
+parameters:
+ level: max
+
+ paths:
+ - src
+ - tests
+
+ tmpDir: %currentWorkingDirectory%/runtime
+
+ # Enable strict advanced checks
+ checkImplicitMixed: true
+ checkBenevolentUnionTypes: true
+ checkUninitializedProperties: true
+ checkMissingCallableSignature: true
+ checkTooWideReturnTypesInProtectedAndPublicMethods: true
+ reportAnyTypeWideningInVarTag: true
+ reportPossiblyNonexistentConstantArrayOffset: true
+ reportPossiblyNonexistentGeneralArrayOffset: true
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index e12bbda..e5d7313 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,19 +1,24 @@
-
-
-
-
-
-
-
- ./tests
-
-
-
-
- ./src
-
-
- ./vendor
-
-
+
+
+
+
+ tests
+
+
+
+
+
+ ./src
+
+
diff --git a/psalm.xml b/psalm.xml
deleted file mode 100644
index 23bfcce..0000000
--- a/psalm.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/rector.php b/rector.php
new file mode 100644
index 0000000..4a7a938
--- /dev/null
+++ b/rector.php
@@ -0,0 +1,28 @@
+parallel();
+
+ $rectorConfig->importNames();
+
+ $rectorConfig->paths(
+ [
+ __DIR__ . '/src',
+ __DIR__ . '/tests',
+ ],
+ );
+
+ $rectorConfig->sets(
+ [
+ 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
+ );
+};
diff --git a/runtime/.gitignore b/runtime/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/runtime/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/src/Assert.php b/src/Assert.php
index 87e26f4..a5bad2b 100644
--- a/src/Assert.php
+++ b/src/Assert.php
@@ -4,14 +4,15 @@
namespace PHPForge\Support;
-use PHPUnit\Framework\TestCase;
use ReflectionClass;
+use ReflectionException;
use ReflectionObject;
use RuntimeException;
use function basename;
use function closedir;
use function is_dir;
+use function is_string;
use function opendir;
use function readdir;
use function rmdir;
@@ -19,16 +20,37 @@
use function unlink;
/**
- * @psalm-suppress PropertyNotSetInConstructor
+ * Assertion utility class for advanced test introspection and manipulation.
+ *
+ * Provides static helper methods for accessing and modifying inaccessible properties and methods invoking parent class
+ * logic, and performing file system cleanup in test environments.
+ *
+ * Extends {@see \PHPUnit\Framework\Assert} to offer additional capabilities for testing private/protected members and
+ * for managing test artifacts, supporting robust and isolated unit tests.
+ *
+ * Key features.
+ * - Access and modify inaccessible (private/protected) properties and methods via reflection.
+ * - Invoke parent class methods and properties for testing inheritance scenarios.
+ * - Normalize line endings for cross-platform string assertions.
+ * - Remove files and directories recursively for test environment cleanup.
+ *
+ * @copyright Copyright (C) 2025 PHPForge.
+ * @license https://opensource.org/license/bsd-3-clause BSD 3-Clause License.
*/
-final class Assert extends TestCase
+final class Assert extends \PHPUnit\Framework\Assert
{
/**
- * Asserting two strings equality ignoring line endings.
+ * Asserts that two strings are equal after normalizing line endings to unix style ('\n').
*
- * @param string $expected The expected string.
- * @param string $actual The actual string.
- * @param string $message The message to display if the assertion fails.
+ * Replaces all windows style ('\r\n') line endings with unix style ('\n') in both the expected and actual strings
+ * before performing the equality assertion.
+ *
+ * This ensures cross-platform consistency in string comparisons where line ending differences may otherwise cause
+ * `false` negatives.
+ *
+ * @param string $expected Expected string value, with any line endings.
+ * @param string $actual Actual string value, with any line endings.
+ * @param string $message Optional failure message to display if the assertion fails. Default is an empty string.
*/
public static function equalsWithoutLE(string $expected, string $actual, string $message = ''): void
{
@@ -39,74 +61,142 @@ public static function equalsWithoutLE(string $expected, string $actual, string
}
/**
- * Gets an inaccessible object property.
+ * Retrieves the value of an inaccessible property from a parent class instance.
+ *
+ * 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.
+ *
+ * @param object $object Object instance from which to retrieve the property value.
+ * @param string|object $className Name or instance of the parent class containing the property.
+ * @param string $propertyName Name of the property to access.
+ *
+ * @throws ReflectionException
+ *
+ * @return mixed Value of the specified parent property.
+ *
+ * @phpstan-param class-string|object $className
+ */
+ public static function inaccessibleParentProperty(
+ object $object,
+ string|object $className,
+ string $propertyName,
+ ): mixed {
+ $class = new ReflectionClass($className);
+
+ return $class->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 $object The object to get the property from.
- * @param string $propertyName The name of the property to get.
+ * @param string|object $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(object $object, string $propertyName): mixed
+ public static function inaccessibleProperty(string|object $object, string $propertyName): mixed
{
$class = new ReflectionClass($object);
- $result = null;
-
if ($propertyName !== '') {
$property = $class->getProperty($propertyName);
-
- /** @psalm-var mixed $result */
- $result = $property->getValue($object);
+ $result = is_string($object) ? $property->getValue() : $property->getValue($object);
}
- return $result;
+ return $result ?? null;
}
/**
- * Invokes an inaccessible method.
+ * Invokes an inaccessible method on the given object instance with the specified arguments.
*
- * @param object $object The object to invoke the method on.
- * @param string $method The name of the method to invoke.
- * @param array $args The arguments to pass to the method.
+ * 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);
- $result = null;
-
if ($method !== '') {
$method = $reflection->getMethod($method);
-
- /** @psalm-var mixed $result */
$result = $method->invokeArgs($object, $args);
}
- return $result;
+ return $result ?? null;
}
/**
- * Sets an inaccessible object property to a designated value.
+ * 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 setInaccessibleProperty(
+ public static function invokeParentMethod(
object $object,
- string $propertyName,
- mixed $value
- ): void {
- $class = new ReflectionClass($object);
+ string $parentClass,
+ string $method,
+ array $args = [],
+ ): mixed {
+ $reflection = new ReflectionClass($parentClass);
- if ($propertyName !== '') {
- $property = $class->getProperty($propertyName);
- $property->setValue($object, $value);
+ if ($method !== '') {
+ $method = $reflection->getMethod($method);
+ $result = $method->invokeArgs($object, $args);
}
- unset($class, $property);
+ return $result ?? null;
}
/**
- * Remove files from the directory.
+ * 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').
*
- * @param string $basePath The directory to remove files from.
+ * Subdirectories are processed recursively before removal.
*
- * @throws RuntimeException
+ * @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
{
@@ -134,4 +224,67 @@ public static function removeFilesFromDirectory(string $basePath): void
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/AssertTest.php b/tests/AssertTest.php
index d102a4e..7e90880 100644
--- a/tests/AssertTest.php
+++ b/tests/AssertTest.php
@@ -5,68 +5,153 @@
namespace PHPForge\Support\Tests;
use PHPForge\Support\Assert;
+use PHPForge\Support\Tests\Stub\{TestBaseClass, TestClass};
use PHPUnit\Framework\TestCase;
+use ReflectionException;
use RuntimeException;
/**
- * @psalm-suppress PropertyNotSetInConstructor
+ * Test suite for {@see Assert} utility methods and reflection helpers.
+ *
+ * Verifies the behavior of assertion and reflection-based utility methods for testing inaccessible properties, parent
+ * properties, and methods, as well as file system operations for test directories.
+ *
+ * These tests ensure correct access and mutation of private/protected members, invocation of inaccessible methods, and
+ * robust file removal logic, including error handling for non-existent directories.
+ *
+ * Test coverage.
+ * - Accessing and asserting values of inaccessible properties and parent properties.
+ * - Ensuring correct exception handling for invalid operations.
+ * - Invoking inaccessible methods and parent methods.
+ * - Removing files from directories and handling missing directories.
+ * - Setting values for inaccessible properties and parent properties.
+ *
+ * @copyright Copyright (C) 2025 Terabytesoftw.
+ * @license https://opensource.org/license/bsd-3-clause BSD 3-Clause License.
*/
final class AssertTest extends TestCase
{
- public function testEqualsWithoutLE(): void
+ public function testEqualsWithoutLEReturnsTrueWhenStringsAreIdenticalWithLineEndings(): void
{
- Assert::equalsWithoutLE('foo' . "\r\n" . 'bar', 'foo' . "\r\n" . 'bar');
+ Assert::equalsWithoutLE(
+ "foo\r\nbar",
+ "foo\r\nbar",
+ "Should return 'true' when both strings are identical including line endings.",
+ );
}
- public function testInaccessibleProperty(): void
+ /**
+ * @throws ReflectionException
+ */
+ public function testInaccessibleParentPropertyReturnsExpectedValue(): void
{
- $object = new class () {
- private string $foo = 'bar';
- };
-
- $this->assertSame('bar', Assert::inaccessibleProperty($object, 'foo'));
+ self::assertSame(
+ 'valueParent',
+ Assert::inaccessibleParentProperty(
+ new TestClass(),
+ TestBaseClass::class,
+ 'propertyParent',
+ ),
+ "Should return the value of the parent property 'propertyParent' when accessed via reflection.",
+ );
}
- public function testInvokeMethod(): void
+ public function testRemoveFilesFromDirectoryRemovesAllFiles(): void
{
- $object = new class () {
- protected function foo(): string
- {
- return 'foo';
- }
- };
-
- $this->assertSame('foo', Assert::invokeMethod($object, 'foo'));
+ $dir = dirname(__DIR__) . '/runtime';
+
+ mkdir("{$dir}/subdir");
+ touch("{$dir}/test.txt");
+ touch("{$dir}/subdir/test.txt");
+
+ Assert::removeFilesFromDirectory($dir);
+
+ $this->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.",
+ );
}
- public function testSetInaccessibleProperty(): void
+ /**
+ * @throws ReflectionException
+ */
+ public function testReturnInaccessiblePropertyValueWhenPropertyIsPrivate(): void
{
- $object = new class () {
- private string $foo = 'bar';
- };
+ self::assertSame(
+ 'value',
+ Assert::inaccessibleProperty(new TestClass(), 'property'),
+ "Should return the value of the private property 'property' when accessed via reflection.",
+ );
+ }
- Assert::setInaccessibleProperty($object, 'foo', 'baz');
+ /**
+ * @throws ReflectionException
+ */
+ public function testReturnValueWhenInvokingInaccessibleMethod(): void
+ {
+ $this->assertSame(
+ 'value',
+ Assert::invokeMethod(new TestClass(), 'inaccessibleMethod'),
+ "Should return 'value' when invoking the inaccessible method 'inaccessibleParentMethod' on 'TestClass' " .
+ 'via reflection.',
+ );
+ }
- $this->assertSame('baz', Assert::inaccessibleProperty($object, 'foo'));
+ /**
+ * @throws ReflectionException
+ */
+ public function testReturnValueWhenInvokingInaccessibleParentMethod(): void
+ {
+ $this->assertSame(
+ 'valueParent',
+ Assert::invokeParentMethod(
+ new TestClass(),
+ TestBaseClass::class,
+ 'inaccessibleParentMethod',
+ ),
+ "Should return 'valueParent' when invoking the inaccessible parent method 'inaccessibleParentMethod' on " .
+ "'TestClass' via reflection.",
+ );
}
- public function testRemoveFilesFromDirectory(): void
+ /**
+ * @throws ReflectionException
+ */
+ public function testSetInaccessibleParentProperty(): void
{
- $dir = __DIR__ . '/runtime';
+ $object = new TestClass();
- mkdir($dir);
- mkdir($dir . '/subdir');
- touch($dir . '/test.txt');
- touch($dir . '/subdir/test.txt');
+ Assert::setInaccessibleParentProperty($object, TestBaseClass::class, 'propertyParent', 'foo');
- Assert::removeFilesFromDirectory($dir);
+ $this->assertSame(
+ 'foo',
+ Assert::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();
- $this->assertFileDoesNotExist($dir . '/test.txt');
+ Assert::setInaccessibleProperty($object, 'property', 'foo');
- rmdir(__DIR__ . '/runtime');
+ $this->assertSame(
+ 'foo',
+ Assert::inaccessibleProperty($object, 'property'),
+ "Should return 'foo' after setting the private property 'property' via 'setInaccessibleProperty' method.",
+ );
}
- public function testRemoveFilesFromDirectoryWithException(): void
+ public function testThrowRuntimeExceptionWhenRemoveFilesFromDirectoryNonExistingDirectory(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Unable to open directory: non-existing-directory');
diff --git a/tests/Stub/TestBaseClass.php b/tests/Stub/TestBaseClass.php
new file mode 100644
index 0000000..8606b59
--- /dev/null
+++ b/tests/Stub/TestBaseClass.php
@@ -0,0 +1,23 @@
+propertyParent;
+ }
+}
diff --git a/tests/Stub/TestClass.php b/tests/Stub/TestClass.php
new file mode 100644
index 0000000..95936f9
--- /dev/null
+++ b/tests/Stub/TestClass.php
@@ -0,0 +1,23 @@
+property;
+ }
+}
From 688c80ba75d7f1c7c5c0aef8ce4f495a334de7b6 Mon Sep 17 00:00:00 2001
From: StyleCI Bot
Date: Mon, 18 Aug 2025 15:12:31 +0000
Subject: [PATCH 02/13] Apply fixes from StyleCI
---
src/Assert.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Assert.php b/src/Assert.php
index a5bad2b..b5ada78 100644
--- a/src/Assert.php
+++ b/src/Assert.php
@@ -70,7 +70,7 @@ public static function equalsWithoutLE(string $expected, string $actual, string
* to parent properties is not possible.
*
* @param object $object Object instance from which to retrieve the property value.
- * @param string|object $className Name or instance of the parent class containing the property.
+ * @param object|string $className Name or instance of the parent class containing the property.
* @param string $propertyName Name of the property to access.
*
* @throws ReflectionException
@@ -98,7 +98,7 @@ public static function inaccessibleParentProperty(
* 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 string|object $object Name of the class or object instance from which to retrieve the property value.
+ * @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.
From 38c45af6e4cbe17f70f0fa49587316aa7a11fa67 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Mon, 18 Aug 2025 11:14:01 -0400
Subject: [PATCH 03/13] fix(support): Refactor project structure and update
dependencies.
---
.gitattributes | 27 ++--
.gitignore | 40 +++---
LICENSE | 16 ---
LICENSE.md | 27 ++++
README.md | 130 ++++++++++++--------
composer.json | 28 +++--
docs/testing.md | 27 +++-
ecs.php | 91 +++++++++-----
phpstan.neon | 21 ++++
phpunit.xml.dist | 41 ++++---
psalm.xml | 17 ---
rector.php | 28 +++++
runtime/.gitignore | 2 +
src/Assert.php | 231 +++++++++++++++++++++++++++++------
tests/AssertTest.php | 153 +++++++++++++++++------
tests/Stub/TestBaseClass.php | 23 ++++
tests/Stub/TestClass.php | 23 ++++
17 files changed, 672 insertions(+), 253 deletions(-)
delete mode 100644 LICENSE
create mode 100644 LICENSE.md
create mode 100644 phpstan.neon
delete mode 100644 psalm.xml
create mode 100644 rector.php
create mode 100644 runtime/.gitignore
create mode 100644 tests/Stub/TestBaseClass.php
create mode 100644 tests/Stub/TestClass.php
diff --git a/.gitattributes b/.gitattributes
index 65a6a4e..76da65a 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -21,19 +21,20 @@
*.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
-/.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
-/phpunit.xml.dist export-ignore
-/psalm.xml export-ignore
-/rector.php export-ignore
-/tests export-ignore
diff --git a/.gitignore b/.gitignore
index b5300c1..bb7ff36 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,24 +1,34 @@
-#code coverage
-/code_coverage
+# phpstorm project files
+.idea
+
+# netbeans project files
+nbproject
+
+# zend studio for eclipse project files
+.buildpath
+.project
+.settings
+
+# windows thumbnail cache
+Thumbs.db
+
+# Mac DS_Store Files
+.DS_Store
# composer vendor dir
/vendor
-/composer.lock
-#node_modules
-/node_modules
+# composer lock file
+/composer.lock
-# phpstorm project files
-.idea
+# composer itself is not needed
+composer.phar
-# phpunit
-.phpunit.cache
-.phpunit.result.cache
-phpunit.xml
+# phpunit itself is not needed
phpunit.phar
-#yii3 config packages
-/config/packages
+# local phpunit config
+/phpunit.xml
-# windows thumbnail cache
-Thumbs.db
+# phpunit cache
+.phpunit.result.cache
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index 136b000..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,16 +0,0 @@
-MIT License
-
-Copyright (c) 2024 by Wilmer Arámbula (https://github.com/terabytesoftw) All rights reserved.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
-documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
-rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
-persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
-Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
-COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..a76279a
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,27 @@
+# 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 bb14c8f..9994916 100644
--- a/README.md
+++ b/README.md
@@ -27,143 +27,169 @@
-## Installation
+## Features
-The preferred way to install this extension is through [composer](https://getcomposer.org/download/).
+✅ **Advanced Reflection Utilities**
+- Access and modify private/protected properties via reflection.
+- Invoke inaccessible methods for comprehensive testing coverage.
-Either run
+✅ **Cross-Platform String Assertions**
+- Eliminate false negatives from Windows/Unix line ending differences.
+- Normalize line endings for consistent string comparisons across platforms.
-```shell
+✅ **File System Test Management**
+- Recursive file and directory cleanup for isolated test environments.
+- Safe removal operations preserving Git tracking files.
+
+## Quick start
+
+### System requirements
+
+- [`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 --prefer-dist php-forge/support
```
-or add
+#### Method 2: Manual installation
+
+Add to your `composer.json`.
```json
-"php-forge/support": "^0.1"
+{
+ "require-dev": {
+ "php-forge/support": "^0.1"
+ }
+}
```
-to the require-dev section of your `composer.json` file.
+Then run.
-## Usage
+```bash
+composer update
+```
-### Equals without line ending
+## Basic Usage
+
+### Cross-platform string equality
```php
assertSame('bar', Assert::inaccessibleProperty($object, 'foo'));
+// Access private properties for testing
+$value = Assert::inaccessibleProperty($object, 'secretValue');
+$this->assertSame('hidden', $value);
```
-### Invoke method
+### Invoking protected methods
```php
assertSame('foo', Assert::invokeMethod($object, 'foo'));
+// Test protected method behavior
+$result = Assert::invokeMethod($object, 'calculate', [5, 3]);
+$this->assertSame(8, $result);
```
-### Set inaccessible property
+### Modifying inaccessible properties
```php
assertSame('baz', Assert::inaccessibleProperty($object, 'foo'));
+$newValue = Assert::inaccessibleProperty($object, 'config');
+$this->assertSame('test-mode', $newValue);
```
-### Remove files from directory
+### Test environment cleanup
```php
assertFileDoesNotExist($dir . '/test.txt');
+// Clean up test artifacts (preserves .gitignore and .gitkeep)
+Assert::removeFilesFromDirectory($testDir);
-rmdir(__DIR__ . '/runtime');
+$this->assertFileDoesNotExist($testDir . '/test.txt');
+$this->assertDirectoryDoesNotExist($testDir . '/subdir');
```
-## Support versions
+## Documentation
-[](https://www.php.net/releases/8.1/en.php)
-[](https://yiiframework.com)
+For comprehensive testing guidance, see:
-## Testing
-
-[Check the documentation testing](/docs/testing.md) to learn about testing.
+- 🧪 [Testing Guide](docs/testing.md)
## Our social networks
-[](https://twitter.com/Terabytesoftw)
+[](https://x.com/Terabytesoftw)
## License
+[](LICENSE.md)
+
The MIT License. Please see [License File](LICENSE.md) for more information.
diff --git a/composer.json b/composer.json
index 5eda0d1..98cc91a 100644
--- a/composer.json
+++ b/composer.json
@@ -9,16 +9,18 @@
"testing",
"tests"
],
- "license": "MIT",
+ "license": "BSD-3-Clause",
"require": {
"php": "^8.1",
"phpunit/phpunit": "^10.5"
},
"require-dev": {
+ "infection/infection": "^0.27|^0.31",
"maglnet/composer-require-checker": "^4.7",
- "roave/infection-static-analysis-plugin": "^1.34",
- "symplify/easy-coding-standard": "^12.1",
- "vimeo/psalm": "^5.20"
+ "phpstan/phpstan-strict-rules": "^2.0.3",
+ "symplify/easy-coding-standard": "^12.5",
+ "phpstan/phpstan" : "^2.1",
+ "rector/rector": "^2.1"
},
"autoload": {
"psr-4": {
@@ -38,17 +40,17 @@
"config": {
"sort-packages": true,
"allow-plugins": {
- "infection/extension-installer": true
+ "infection/extension-installer": true,
+ "phpstan/extension-installer": true
}
},
"scripts": {
- "check-dependencies": "composer-require-checker",
- "easy-coding-standard": "ecs --fix",
- "mutation": [
- "Composer\\Config::disableProcessTimeout",
- "roave-infection-static-analysis-plugin --threads=2 --only-covered"
- ],
- "psalm": "psalm",
- "test": "phpunit"
+ "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",
+ "rector": "./vendor/bin/rector process src",
+ "static": "./vendor/bin/phpstan --memory-limit=512M",
+ "tests": "./vendor/bin/phpunit"
}
}
diff --git a/docs/testing.md b/docs/testing.md
index b2aa5df..2838bf5 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -4,31 +4,46 @@
This package uses [composer-require-checker](https://github.com/maglnet/ComposerRequireChecker) to check if all dependencies are correctly defined in `composer.json`.
-To run the checker, execute the following command:
+To run the checker, execute the following command.
```shell
composer run check-dependencies
```
+## Easy coding standard
+
+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.
+
+```shell
+composer run ecs
+```
+
## Mutation testing
-Mutation testing is checked with [Infection](https://infection.github.io/). To run it:
+Mutation testing is checked with [Infection](https://infection.github.io/). To run it.
```shell
composer run mutation
```
+With PHPStan analysis, it will also check for static analysis issues during mutation testing.
+
+```shell
+composer run mutation-static
+```
+
## Static analysis
-The code is statically analyzed with [Psalm](https://psalm.dev/). To run static analysis:
+The code is statically analyzed with [PHPStan](https://phpstan.org/). To run static analysis.
```shell
-composer run psalm
+composer run static
```
-## Unit tests
+## Unit Tests
-The code is tested with [PHPUnit](https://phpunit.de/). To run tests:
+The code is tested with [PHPUnit](https://phpunit.de/). To run tests.
```shell
composer run test
diff --git a/ecs.php b/ecs.php
index 78342b8..00781d8 100644
--- a/ecs.php
+++ b/ecs.php
@@ -5,43 +5,74 @@
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\StringNotation\SingleQuoteFixer;
use Symplify\EasyCodingStandard\Config\ECSConfig;
-use Symplify\EasyCodingStandard\ValueObject\Set\SetList;
-return function (ECSConfig $ecsConfig): void {
- $ecsConfig->paths(
+return ECSConfig::configure()
+ ->withConfiguredRule(
+ ClassDefinitionFixer::class,
+ [
+ 'space_before_parenthesis' => true,
+ ],
+ )
+ ->withConfiguredRule(
+ OrderedClassElementsFixer::class,
+ [
+ 'order' => [
+ 'use_trait',
+ 'constant_public',
+ 'constant_protected',
+ 'constant_private',
+ 'case',
+ 'property_public',
+ 'property_protected',
+ 'property_private',
+ 'construct',
+ 'destruct',
+ 'magic',
+ 'phpunit',
+ 'method_public',
+ 'method_protected',
+ 'method_private',
+ ],
+ 'sort_algorithm' => 'alpha',
+ ],
+ )
+ ->withConfiguredRule(
+ OrderedImportsFixer::class,
+ [
+ 'imports_order' => ['class', 'function', 'const'],
+ 'sort_algorithm' => 'alpha',
+ ],
+ )
+ ->withConfiguredRule(
+ VisibilityRequiredFixer::class,
+ [
+ 'elements' => [],
+ ],
+ )
+ ->withFileExtensions(['php'])
+ ->withPaths(
[
__DIR__ . '/src',
__DIR__ . '/tests',
- ]
- );
-
- // this way you add a single rule
- $ecsConfig->rules(
+ ],
+ )
+ ->withPhpCsFixerSets(perCS20: true)
+ ->withPreparedSets(
+ cleanCode: true,
+ comments: true,
+ docblocks: true,
+ namespaces: true,
+ strict: true,
+ )
+ ->withRules(
[
- OrderedClassElementsFixer::class,
- OrderedTraitsFixer::class,
NoUnusedImportsFixer::class,
+ OrderedTraitsFixer::class,
+ SingleQuoteFixer::class,
]
);
-
- // this way you can add sets - group of rules
- $ecsConfig->sets(
- [
- // run and fix, one by one
- SetList::DOCBLOCK,
- SetList::NAMESPACES,
- SetList::COMMENTS,
- SetList::PSR_12,
- ]
- );
-
- // this way configures a rule
- $ecsConfig->ruleWithConfiguration(
- ClassDefinitionFixer::class,
- [
- 'space_before_parenthesis' => true,
- ],
- );
-};
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..63c0f31
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,21 @@
+includes:
+ - phar://phpstan.phar/conf/bleedingEdge.neon
+
+parameters:
+ level: max
+
+ paths:
+ - src
+ - tests
+
+ tmpDir: %currentWorkingDirectory%/runtime
+
+ # Enable strict advanced checks
+ checkImplicitMixed: true
+ checkBenevolentUnionTypes: true
+ checkUninitializedProperties: true
+ checkMissingCallableSignature: true
+ checkTooWideReturnTypesInProtectedAndPublicMethods: true
+ reportAnyTypeWideningInVarTag: true
+ reportPossiblyNonexistentConstantArrayOffset: true
+ reportPossiblyNonexistentGeneralArrayOffset: true
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index e12bbda..e5d7313 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,19 +1,24 @@
-
-
-
-
-
-
-
- ./tests
-
-
-
-
- ./src
-
-
- ./vendor
-
-
+
+
+
+
+ tests
+
+
+
+
+
+ ./src
+
+
diff --git a/psalm.xml b/psalm.xml
deleted file mode 100644
index 23bfcce..0000000
--- a/psalm.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/rector.php b/rector.php
new file mode 100644
index 0000000..4a7a938
--- /dev/null
+++ b/rector.php
@@ -0,0 +1,28 @@
+parallel();
+
+ $rectorConfig->importNames();
+
+ $rectorConfig->paths(
+ [
+ __DIR__ . '/src',
+ __DIR__ . '/tests',
+ ],
+ );
+
+ $rectorConfig->sets(
+ [
+ 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
+ );
+};
diff --git a/runtime/.gitignore b/runtime/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/runtime/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/src/Assert.php b/src/Assert.php
index 87e26f4..a5bad2b 100644
--- a/src/Assert.php
+++ b/src/Assert.php
@@ -4,14 +4,15 @@
namespace PHPForge\Support;
-use PHPUnit\Framework\TestCase;
use ReflectionClass;
+use ReflectionException;
use ReflectionObject;
use RuntimeException;
use function basename;
use function closedir;
use function is_dir;
+use function is_string;
use function opendir;
use function readdir;
use function rmdir;
@@ -19,16 +20,37 @@
use function unlink;
/**
- * @psalm-suppress PropertyNotSetInConstructor
+ * Assertion utility class for advanced test introspection and manipulation.
+ *
+ * Provides static helper methods for accessing and modifying inaccessible properties and methods invoking parent class
+ * logic, and performing file system cleanup in test environments.
+ *
+ * Extends {@see \PHPUnit\Framework\Assert} to offer additional capabilities for testing private/protected members and
+ * for managing test artifacts, supporting robust and isolated unit tests.
+ *
+ * Key features.
+ * - Access and modify inaccessible (private/protected) properties and methods via reflection.
+ * - Invoke parent class methods and properties for testing inheritance scenarios.
+ * - Normalize line endings for cross-platform string assertions.
+ * - Remove files and directories recursively for test environment cleanup.
+ *
+ * @copyright Copyright (C) 2025 PHPForge.
+ * @license https://opensource.org/license/bsd-3-clause BSD 3-Clause License.
*/
-final class Assert extends TestCase
+final class Assert extends \PHPUnit\Framework\Assert
{
/**
- * Asserting two strings equality ignoring line endings.
+ * Asserts that two strings are equal after normalizing line endings to unix style ('\n').
*
- * @param string $expected The expected string.
- * @param string $actual The actual string.
- * @param string $message The message to display if the assertion fails.
+ * Replaces all windows style ('\r\n') line endings with unix style ('\n') in both the expected and actual strings
+ * before performing the equality assertion.
+ *
+ * This ensures cross-platform consistency in string comparisons where line ending differences may otherwise cause
+ * `false` negatives.
+ *
+ * @param string $expected Expected string value, with any line endings.
+ * @param string $actual Actual string value, with any line endings.
+ * @param string $message Optional failure message to display if the assertion fails. Default is an empty string.
*/
public static function equalsWithoutLE(string $expected, string $actual, string $message = ''): void
{
@@ -39,74 +61,142 @@ public static function equalsWithoutLE(string $expected, string $actual, string
}
/**
- * Gets an inaccessible object property.
+ * Retrieves the value of an inaccessible property from a parent class instance.
+ *
+ * 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.
+ *
+ * @param object $object Object instance from which to retrieve the property value.
+ * @param string|object $className Name or instance of the parent class containing the property.
+ * @param string $propertyName Name of the property to access.
+ *
+ * @throws ReflectionException
+ *
+ * @return mixed Value of the specified parent property.
+ *
+ * @phpstan-param class-string|object $className
+ */
+ public static function inaccessibleParentProperty(
+ object $object,
+ string|object $className,
+ string $propertyName,
+ ): mixed {
+ $class = new ReflectionClass($className);
+
+ return $class->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 $object The object to get the property from.
- * @param string $propertyName The name of the property to get.
+ * @param string|object $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(object $object, string $propertyName): mixed
+ public static function inaccessibleProperty(string|object $object, string $propertyName): mixed
{
$class = new ReflectionClass($object);
- $result = null;
-
if ($propertyName !== '') {
$property = $class->getProperty($propertyName);
-
- /** @psalm-var mixed $result */
- $result = $property->getValue($object);
+ $result = is_string($object) ? $property->getValue() : $property->getValue($object);
}
- return $result;
+ return $result ?? null;
}
/**
- * Invokes an inaccessible method.
+ * Invokes an inaccessible method on the given object instance with the specified arguments.
*
- * @param object $object The object to invoke the method on.
- * @param string $method The name of the method to invoke.
- * @param array $args The arguments to pass to the method.
+ * 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);
- $result = null;
-
if ($method !== '') {
$method = $reflection->getMethod($method);
-
- /** @psalm-var mixed $result */
$result = $method->invokeArgs($object, $args);
}
- return $result;
+ return $result ?? null;
}
/**
- * Sets an inaccessible object property to a designated value.
+ * 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 setInaccessibleProperty(
+ public static function invokeParentMethod(
object $object,
- string $propertyName,
- mixed $value
- ): void {
- $class = new ReflectionClass($object);
+ string $parentClass,
+ string $method,
+ array $args = [],
+ ): mixed {
+ $reflection = new ReflectionClass($parentClass);
- if ($propertyName !== '') {
- $property = $class->getProperty($propertyName);
- $property->setValue($object, $value);
+ if ($method !== '') {
+ $method = $reflection->getMethod($method);
+ $result = $method->invokeArgs($object, $args);
}
- unset($class, $property);
+ return $result ?? null;
}
/**
- * Remove files from the directory.
+ * 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').
*
- * @param string $basePath The directory to remove files from.
+ * Subdirectories are processed recursively before removal.
*
- * @throws RuntimeException
+ * @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
{
@@ -134,4 +224,67 @@ public static function removeFilesFromDirectory(string $basePath): void
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/AssertTest.php b/tests/AssertTest.php
index d102a4e..7e90880 100644
--- a/tests/AssertTest.php
+++ b/tests/AssertTest.php
@@ -5,68 +5,153 @@
namespace PHPForge\Support\Tests;
use PHPForge\Support\Assert;
+use PHPForge\Support\Tests\Stub\{TestBaseClass, TestClass};
use PHPUnit\Framework\TestCase;
+use ReflectionException;
use RuntimeException;
/**
- * @psalm-suppress PropertyNotSetInConstructor
+ * Test suite for {@see Assert} utility methods and reflection helpers.
+ *
+ * Verifies the behavior of assertion and reflection-based utility methods for testing inaccessible properties, parent
+ * properties, and methods, as well as file system operations for test directories.
+ *
+ * These tests ensure correct access and mutation of private/protected members, invocation of inaccessible methods, and
+ * robust file removal logic, including error handling for non-existent directories.
+ *
+ * Test coverage.
+ * - Accessing and asserting values of inaccessible properties and parent properties.
+ * - Ensuring correct exception handling for invalid operations.
+ * - Invoking inaccessible methods and parent methods.
+ * - Removing files from directories and handling missing directories.
+ * - Setting values for inaccessible properties and parent properties.
+ *
+ * @copyright Copyright (C) 2025 Terabytesoftw.
+ * @license https://opensource.org/license/bsd-3-clause BSD 3-Clause License.
*/
final class AssertTest extends TestCase
{
- public function testEqualsWithoutLE(): void
+ public function testEqualsWithoutLEReturnsTrueWhenStringsAreIdenticalWithLineEndings(): void
{
- Assert::equalsWithoutLE('foo' . "\r\n" . 'bar', 'foo' . "\r\n" . 'bar');
+ Assert::equalsWithoutLE(
+ "foo\r\nbar",
+ "foo\r\nbar",
+ "Should return 'true' when both strings are identical including line endings.",
+ );
}
- public function testInaccessibleProperty(): void
+ /**
+ * @throws ReflectionException
+ */
+ public function testInaccessibleParentPropertyReturnsExpectedValue(): void
{
- $object = new class () {
- private string $foo = 'bar';
- };
-
- $this->assertSame('bar', Assert::inaccessibleProperty($object, 'foo'));
+ self::assertSame(
+ 'valueParent',
+ Assert::inaccessibleParentProperty(
+ new TestClass(),
+ TestBaseClass::class,
+ 'propertyParent',
+ ),
+ "Should return the value of the parent property 'propertyParent' when accessed via reflection.",
+ );
}
- public function testInvokeMethod(): void
+ public function testRemoveFilesFromDirectoryRemovesAllFiles(): void
{
- $object = new class () {
- protected function foo(): string
- {
- return 'foo';
- }
- };
-
- $this->assertSame('foo', Assert::invokeMethod($object, 'foo'));
+ $dir = dirname(__DIR__) . '/runtime';
+
+ mkdir("{$dir}/subdir");
+ touch("{$dir}/test.txt");
+ touch("{$dir}/subdir/test.txt");
+
+ Assert::removeFilesFromDirectory($dir);
+
+ $this->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.",
+ );
}
- public function testSetInaccessibleProperty(): void
+ /**
+ * @throws ReflectionException
+ */
+ public function testReturnInaccessiblePropertyValueWhenPropertyIsPrivate(): void
{
- $object = new class () {
- private string $foo = 'bar';
- };
+ self::assertSame(
+ 'value',
+ Assert::inaccessibleProperty(new TestClass(), 'property'),
+ "Should return the value of the private property 'property' when accessed via reflection.",
+ );
+ }
- Assert::setInaccessibleProperty($object, 'foo', 'baz');
+ /**
+ * @throws ReflectionException
+ */
+ public function testReturnValueWhenInvokingInaccessibleMethod(): void
+ {
+ $this->assertSame(
+ 'value',
+ Assert::invokeMethod(new TestClass(), 'inaccessibleMethod'),
+ "Should return 'value' when invoking the inaccessible method 'inaccessibleParentMethod' on 'TestClass' " .
+ 'via reflection.',
+ );
+ }
- $this->assertSame('baz', Assert::inaccessibleProperty($object, 'foo'));
+ /**
+ * @throws ReflectionException
+ */
+ public function testReturnValueWhenInvokingInaccessibleParentMethod(): void
+ {
+ $this->assertSame(
+ 'valueParent',
+ Assert::invokeParentMethod(
+ new TestClass(),
+ TestBaseClass::class,
+ 'inaccessibleParentMethod',
+ ),
+ "Should return 'valueParent' when invoking the inaccessible parent method 'inaccessibleParentMethod' on " .
+ "'TestClass' via reflection.",
+ );
}
- public function testRemoveFilesFromDirectory(): void
+ /**
+ * @throws ReflectionException
+ */
+ public function testSetInaccessibleParentProperty(): void
{
- $dir = __DIR__ . '/runtime';
+ $object = new TestClass();
- mkdir($dir);
- mkdir($dir . '/subdir');
- touch($dir . '/test.txt');
- touch($dir . '/subdir/test.txt');
+ Assert::setInaccessibleParentProperty($object, TestBaseClass::class, 'propertyParent', 'foo');
- Assert::removeFilesFromDirectory($dir);
+ $this->assertSame(
+ 'foo',
+ Assert::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();
- $this->assertFileDoesNotExist($dir . '/test.txt');
+ Assert::setInaccessibleProperty($object, 'property', 'foo');
- rmdir(__DIR__ . '/runtime');
+ $this->assertSame(
+ 'foo',
+ Assert::inaccessibleProperty($object, 'property'),
+ "Should return 'foo' after setting the private property 'property' via 'setInaccessibleProperty' method.",
+ );
}
- public function testRemoveFilesFromDirectoryWithException(): void
+ public function testThrowRuntimeExceptionWhenRemoveFilesFromDirectoryNonExistingDirectory(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Unable to open directory: non-existing-directory');
diff --git a/tests/Stub/TestBaseClass.php b/tests/Stub/TestBaseClass.php
new file mode 100644
index 0000000..8606b59
--- /dev/null
+++ b/tests/Stub/TestBaseClass.php
@@ -0,0 +1,23 @@
+propertyParent;
+ }
+}
diff --git a/tests/Stub/TestClass.php b/tests/Stub/TestClass.php
new file mode 100644
index 0000000..95936f9
--- /dev/null
+++ b/tests/Stub/TestClass.php
@@ -0,0 +1,23 @@
+property;
+ }
+}
From 235e4ba7b9b962503cf627d7d1a8d6b81bfe77d4 Mon Sep 17 00:00:00 2001
From: StyleCI Bot
Date: Mon, 18 Aug 2025 15:15:23 +0000
Subject: [PATCH 04/13] Apply fixes from StyleCI
---
src/Assert.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Assert.php b/src/Assert.php
index a5bad2b..b5ada78 100644
--- a/src/Assert.php
+++ b/src/Assert.php
@@ -70,7 +70,7 @@ public static function equalsWithoutLE(string $expected, string $actual, string
* to parent properties is not possible.
*
* @param object $object Object instance from which to retrieve the property value.
- * @param string|object $className Name or instance of the parent class containing the property.
+ * @param object|string $className Name or instance of the parent class containing the property.
* @param string $propertyName Name of the property to access.
*
* @throws ReflectionException
@@ -98,7 +98,7 @@ public static function inaccessibleParentProperty(
* 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 string|object $object Name of the class or object instance from which to retrieve the property value.
+ * @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.
From a641441767f20cfceb4e175b5c48fe34d8e5129d Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Mon, 18 Aug 2025 11:18:31 -0400
Subject: [PATCH 05/13] fix(workflows): update workflow configurations and
dependencies.
---
.github/workflows/build.yml | 11 +----------
.github/workflows/dependency-check.yml | 13 +------------
.github/workflows/mutation.yml | 9 +--------
.github/workflows/static.yml | 11 +----------
4 files changed, 4 insertions(+), 40 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d286d28..9b51512 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -6,8 +6,6 @@ on:
- 'CHANGELOG.md'
- '.gitignore'
- '.gitattributes'
- - 'infection.json.dist'
- - 'psalm.xml'
push:
paths-ignore:
@@ -16,18 +14,11 @@ on:
- 'CHANGELOG.md'
- '.gitignore'
- '.gitattributes'
- - 'infection.json.dist'
- - 'psalm.xml'
name: build
jobs:
phpunit:
- uses: php-forge/actions/.github/workflows/phpunit.yml@main
+ uses: php-forge/actions/.github/workflows/phpunit.yml@v1
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- with:
- os: >-
- ['ubuntu-latest', 'windows-latest']
- php: >-
- ['8.1', '8.2', '8.3']
diff --git a/.github/workflows/dependency-check.yml b/.github/workflows/dependency-check.yml
index dde6447..48115b8 100644
--- a/.github/workflows/dependency-check.yml
+++ b/.github/workflows/dependency-check.yml
@@ -6,9 +6,6 @@ on:
- 'CHANGELOG.md'
- '.gitignore'
- '.gitattributes'
- - 'infection.json.dist'
- - 'phpunit.xml.dist'
- - 'psalm.xml'
push:
paths-ignore:
@@ -17,17 +14,9 @@ on:
- 'CHANGELOG.md'
- '.gitignore'
- '.gitattributes'
- - 'infection.json.dist'
- - 'phpunit.xml.dist'
- - 'psalm.xml'
name: dependency-check
jobs:
composer-require-checker:
- uses: php-forge/actions/.github/workflows/composer-require-checker.yml@main
- with:
- os: >-
- ['ubuntu-latest']
- php: >-
- ['8.1']
+ uses: php-forge/actions/.github/workflows/composer-require-checker.yml@v1
diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml
index ef6929b..f66c982 100644
--- a/.github/workflows/mutation.yml
+++ b/.github/workflows/mutation.yml
@@ -6,7 +6,6 @@ on:
- 'CHANGELOG.md'
- '.gitignore'
- '.gitattributes'
- - 'psalm.xml'
push:
paths-ignore:
@@ -15,17 +14,11 @@ on:
- 'CHANGELOG.md'
- '.gitignore'
- '.gitattributes'
- - 'psalm.xml'
name: mutation test
jobs:
mutation:
- uses: php-forge/actions/.github/workflows/roave-infection.yml@main
+ uses: php-forge/actions/.github/workflows/infection.yml@v1
secrets:
STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
- with:
- os: >-
- ['ubuntu-latest']
- php: >-
- ['8.1']
diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml
index a8e7acb..2e31d77 100644
--- a/.github/workflows/static.yml
+++ b/.github/workflows/static.yml
@@ -6,8 +6,6 @@ on:
- 'CHANGELOG.md'
- '.gitignore'
- '.gitattributes'
- - 'infection.json.dist'
- - 'phpunit.xml.dist'
push:
paths-ignore:
@@ -16,16 +14,9 @@ on:
- 'CHANGELOG.md'
- '.gitignore'
- '.gitattributes'
- - 'infection.json.dist'
- - 'phpunit.xml.dist'
name: static analysis
jobs:
psalm:
- uses: php-forge/actions/.github/workflows/psalm.yml@main
- with:
- os: >-
- ['ubuntu-latest']
- php: >-
- ['8.1', '8.2', '8.3']
+ uses: php-forge/actions/.github/workflows/phpstan.yml@v1
From 6cfefe9e1cbf21e11718b6a83463a0c6b6806dcd Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Mon, 18 Aug 2025 11:20:17 -0400
Subject: [PATCH 06/13] fix(workflows): add phpstan configuration to mutation
job.
---
.github/workflows/mutation.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml
index f66c982..091ee70 100644
--- a/.github/workflows/mutation.yml
+++ b/.github/workflows/mutation.yml
@@ -20,5 +20,7 @@ name: mutation test
jobs:
mutation:
uses: php-forge/actions/.github/workflows/infection.yml@v1
+ with:
+ phpstan: true
secrets:
STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
From a01a3b2bd598a696c38546229ab6f9d309fe124b Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Mon, 18 Aug 2025 11:27:09 -0400
Subject: [PATCH 07/13] fix(readme): update badges and add quality code
section.
---
README.md | 28 +++++++++++++++-------------
1 file changed, 15 insertions(+), 13 deletions(-)
diff --git a/README.md b/README.md
index 9994916..a50b713 100644
--- a/README.md
+++ b/README.md
@@ -7,24 +7,18 @@
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
## Features
@@ -184,6 +178,14 @@ For comprehensive testing guidance, see:
- 🧪 [Testing Guide](docs/testing.md)
+## 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)
+
## Our social networks
[](https://x.com/Terabytesoftw)
From f4f031af809a0902484863bc33c7a29c905b5ca9 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Mon, 18 Aug 2025 11:35:54 -0400
Subject: [PATCH 08/13] fix(readme): update usage examples for accessing
private properties and string equality.
---
README.md | 67 +++++++++++++++++++++++++------------------------------
1 file changed, 31 insertions(+), 36 deletions(-)
diff --git a/README.md b/README.md
index a50b713..f74a79b 100644
--- a/README.md
+++ b/README.md
@@ -73,7 +73,7 @@ composer update
## Basic Usage
-### Cross-platform string equality
+### Accessing private properties
```php
assertSame('hidden', $value);
+// normalize line endings for consistent comparisons
+Assert::equalsWithoutLE(
+ "Foo\r\nBar",
+ "Foo\nBar",
+ "Should match regardless of line ending style"
+);
```
### Invoking protected methods
@@ -124,12 +125,13 @@ $object = new class () {
}
};
-// Test protected method behavior
+// test protected method behavior
$result = Assert::invokeMethod($object, 'calculate', [5, 3]);
-$this->assertSame(8, $result);
+
+self::assertSame(8, $result);
```
-### Modifying inaccessible properties
+### Remove files from directory
```php
assertSame('test-mode', $newValue);
+// clean up test artifacts (preserves '.gitignore' and '.gitkeep')
+Assert::removeFilesFromDirectory($testDir);
```
-### Test environment cleanup
+### Set inaccessible property
```php
assertFileDoesNotExist($testDir . '/test.txt');
-$this->assertDirectoryDoesNotExist($testDir . '/subdir');
+self::assertSame('test-mode', $newValue);
```
## Documentation
From 6bbe9d6633e578c9fffc0924089b0e496670aa2c Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Mon, 18 Aug 2025 11:40:28 -0400
Subject: [PATCH 09/13] fix(readme): update infection badge URL and refine
installation command.
---
README.md | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index f74a79b..f97c685 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
-
+
@@ -50,7 +50,7 @@
Install the extension.
```bash
-composer require --prefer-dist php-forge/support
+composer require --dev --prefer-dist php-forge/support:^0.1
```
#### Method 2: Manual installation
@@ -188,5 +188,3 @@ For comprehensive testing guidance, see:
## License
[](LICENSE.md)
-
-The MIT License. Please see [License File](LICENSE.md) for more information.
From 641b601c59bb39c408a8be60618f096bc13af257 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Mon, 18 Aug 2025 11:43:40 -0400
Subject: [PATCH 10/13] chore: remove outdated Copilot documentation and
guidelines.
---
composer.json | 2 +-
copilot/class-documentation.md | 424 -------------------------------
copilot/code-style.md | 185 --------------
copilot/copilot-instructions.md | 52 ----
copilot/general-instructions.md | 97 -------
copilot/project-documentation.md | 192 --------------
copilot/static-analysis.md | 178 -------------
copilot/unit-test.md | 179 -------------
8 files changed, 1 insertion(+), 1308 deletions(-)
delete mode 100644 copilot/class-documentation.md
delete mode 100644 copilot/code-style.md
delete mode 100644 copilot/copilot-instructions.md
delete mode 100644 copilot/general-instructions.md
delete mode 100644 copilot/project-documentation.md
delete mode 100644 copilot/static-analysis.md
delete mode 100644 copilot/unit-test.md
diff --git a/composer.json b/composer.json
index 98cc91a..bb1cb90 100644
--- a/composer.json
+++ b/composer.json
@@ -19,7 +19,7 @@
"maglnet/composer-require-checker": "^4.7",
"phpstan/phpstan-strict-rules": "^2.0.3",
"symplify/easy-coding-standard": "^12.5",
- "phpstan/phpstan" : "^2.1",
+ "phpstan/phpstan": "^2.1",
"rector/rector": "^2.1"
},
"autoload": {
diff --git a/copilot/class-documentation.md b/copilot/class-documentation.md
deleted file mode 100644
index 7ce8648..0000000
--- a/copilot/class-documentation.md
+++ /dev/null
@@ -1,424 +0,0 @@
-## Class Documentation Guidelines
-
-This guide defines PHPDoc standards for classes and methods within the PHP-Press project, also optimized for tools like
-GitHub Copilot.
-
-## Table of Contents
-1. General PHPDoc Rules.
-2. Documentation Flow.
-3. Class Documentation.
-4. Method Documentation.
-5. Property Documentation.
-6. Type Hints and Templates.
-7. Exception Documentation.
-8. Best Practices.
-9. Copilot Optimization Tips.
-
-## General PHPDoc Rules
-
-### Basic Structure
-- All documentation must be in English.
-- All classes, interfaces, traits, and abstract classes must have a PHPDoc block.
-- All `public` and `protected` methods must have PHPDoc.
-- Use complete sentences and end with a period.
-- Explain **the "why"** behind design decisions.
-- Keep it concise but with relevant context.
-- Use proper indentation for multi-line descriptions.
-- Include `@copyright` and `@license` with `{@see LICENSE}`.
-- Document **only what the code actually does**.
-
-### Common Tags
-| Tag | Purpose |
-|---------------|------------------------------------------|
-| `@param` | Describes each input parameter |
-| `@return` | Explains the returned value |
-| `@throws` | Lists exceptions thrown |
-| `@template` | Declares generic templates |
-| `@phpstan-var`| Specifies complex property types |
-
-## Documentation Flow
-
-1. Brief one-line description.
-2. Detailed description with the "why" behind the design.
-3. List of key features with bullets.
-4. Practical usage examples.
-5. References to related classes (`{@see}`).
-6. Copyright and license.
-
-## Class Documentation
-
-### Ejemplo para Clases Regulares
-path: /core/Src/Router/Route.php
-
-```php
-/**
- * Route definition with immutable configuration and matching capabilities.
- *
- * Represents a route with pattern matching, HTTP method restrictions, middleware support, and parameter validation.
- *
- * Routes are created using factory methods for specific HTTP methods and can be configured using immutable setter
- * methods that return new instances with the requested changes.
- *
- * The matching algorithm compares incoming requests against defined route patterns, considering HTTP methods, hostname
- * constraints, path patterns, query parameters, and storing matched parameters for use in actions.
- *
- * Key features.
- * - API versioning with version prefix support (`v1`, `v2`, etc.).
- * - Early validation of regex patterns during route configuration to prevent runtime errors.
- * - Hostname matching with support for exact domains, wildcards, parameter capture, and regex patterns.
- * - HTTP method-specific factory methods (`GET`, `POST`, `PUT`, etc.).
- * - Immutable fluent interface for configuration.
- * - Middleware integration for request processing.
- * - Parameter validation and default values for path segments.
- * - Pattern-based path matching with parameter extraction.
- * - Priority-based route resolution for handling overlapping patterns.
- * - Query parameter validation and extraction with regex patterns.
- *
- * @see \PHPPress\Router\HttpMethod for enum of valid HTTP methods.
- * @see \PHPPress\Router\RouteCollection for collection, lookup, and URL generation.
- * @see \PHPPress\Router\RouteParser for pattern-to-regex conversion and host matching.
- *
- * @copyright Copyright (C) 2023 Terabytesoftw.
- * @license https://opensource.org/license/bsd-3-clause BSD 3-Clause License.
- */
-class Route
-```
-
-### Ejemplo Abstract class
-path: /core/Src/Web/View/Base/AbstractView.php
-
-```php
-/**
- * Base class for view components providing template rendering with layouts and themes.
- *
- * Provides a flexible and extensible foundation for rendering view templates in web applications, supporting
- * context-aware view resolution, event-driven rendering lifecycle, and pluggable renderer registration.
- *
- * This class enables layout wrapping, theme-based path resolution, and parameter inheritance for layouts, making it
- * suitable for complex UI scenarios and modular view architectures.
- *
- * Views are processed through registered renderers based on file extension and can be wrapped in layouts with
- * inherited parameters.
- *
- * The rendering process is event-driven, allowing listeners to modify content before and after rendering.
- *
- * Key features.
- * - Context-aware view path resolution using {@see ViewContextInterface}.
- * - Event-driven rendering lifecycle with before/after hooks.
- * - Filesystem abstraction for view file access.
- * - Flexible renderer registration for multiple file extensions.
- * - Layout support with parameter inheritance and optional disabling.
- * - Pluggable and extensible renderer system.
- * - Theme mapping for dynamic view path resolution.
- *
- * @see \PHPPress\Renderer\PHPEngine for PHP renderer.
- * @see \PHPPress\View\ViewContext for path context handling.
- *
- * @copyright Copyright (C) 2023 Terabytesoftw.
- * @license https://opensource.org/license/bsd-3-clause BSD 3-Clause License.
- */
-abstract class AbstractView extends Component
-```
-
-### Ejemplo para Interfaces
-path: /core/Src/View/ViewContextInterface.php
-
-```php
-/**
- * Interface for providing view resolution context in templates.
- *
- * Defines the contract for supplying directory context to view renderers, enabling them to resolve relative paths
- * during template processing by referencing the base directory of the current view.
- *
- * This allows templates to reference other views using paths relative to their own location, supporting modular and
- * maintainable UI architectures.
- *
- * This interface is essential for context-aware view resolution, ensuring that renderers can accurately locate and
- * include related templates, layouts, or partials regardless of the rendering entry point.
- *
- * Key features.
- * - Directory context for templates.
- * - Implementation agnostic for flexible renderer integration.
- * - Path normalization support for consistent resolution.
- * - Supports modular and reusable view structures.
- * - View path resolution for relative includes.
- *
- * @copyright Copyright (C) 2023 Terabytesoftw.
- * @license https://opensource.org/license/bsd-3-clause BSD 3-Clause License.
- */
-interface ViewContextInterface
-```
-
-### Ejemplo para Excepciones
-path: /core/Src/Router/Exception/RouteNotFoundException.php
-
-```php
-/**
- * Exception thrown when a route can't be found by its identifier.
- *
- * This exception indicates that an attempt to retrieve a named route failed because no route with the specified
- * identifier exists in the route collection. It helps identify missing or misconfigured route definitions during URL
- * generation.
- *
- * The exception uses automatic message prefixing with "Route not found:" for consistent error reporting across the
- * routing system, aiding in quick identification of route definition issues.
- *
- * Thrown in scenarios including.
- * - Dynamic route generation failures.
- * - Group route resolution errors.
- * - Invalid route names.
- * - Missing route definitions.
- * - Route collection corruption.
- *
- * Key features.
- * - Collection state details.
- * - Exception chaining support.
- * - Message enum integration.
- * - Route identifier tracking.
- * - Route suggestion hints.
- *
- * @copyright Copyright (C) 2023 Terabytesoftw.
- * @license https://opensource.org/license/bsd-3-clause BSD 3-Clause License.
- */
-final class RouteNotFoundException extends Exception
-```
-
-Enums Message for standardized error messages for Exceptions
-path: /core/Src/Router/Exception/Message.php
-
-```php
-/**
- * Represents standardized error messages for router exceptions.
- *
- * This enum defines formatted error messages for various error conditions that may occur during router configuration
- * and execution.
- *
- * It provides a consistent and standardized way to present error messages across the routing system.
- *
- * Each case represents a specific type of error, with a message template that can be populated with dynamic values
- * using the {@see \PHPPress\Router\Exception\Message::getMessage()} method.
- *
- * This centralized approach improves the consistency of error messages and simplifies potential internationalization.
- *
- * Key features.
- * - Centralization of an error text for easier maintenance.
- * - Consistent error handling across the routing system.
- * - Integration with specific exception classes.
- * - Message formatting with dynamic parameters.
- * - Standardized error messages for common cases.
- *
- * @copyright Copyright (C) 2023 Terabytesoftw.
- * @license https://opensource.org/license/bsd-3-clause BSD 3-Clause License.
- */
-enum Message: string
-{
- /**
- * Error when host doesn't match a required pattern.
- *
- * Format: "The passed host '%s' of doesn't match the regexp '%s'"
- */
- case HOST_NOT_MATCHED = 'The passed host \'%s\' of does not match the regexp \'%s\'';
-
- /**
- * Returns the formatted message string for the error case.
- *
- * Retrieves the raw message string associated with this error case without parameter interpolation.
- *
- * @param string ...$argument Dynamic arguments to insert into the message.
- *
- * @return string Error message string with interpolated arguments.
- *
- * Usage example:
- * ```php
- * throw new RouterNotFoundException(Message::ROUTE_NOT_FOUND->getMessage('/invalid/path'));
- * ```
- */
- public function getMessage(int|string ...$argument): string
- {
- return sprintf($this->value, ...$argument);
- }
-}
-```
-
-### Ejemplo para Tests
-path: /core/tests/Router/RouteTest.php
-
-```php
-/**
- * Test suite for {@see \PHPPress\Router\Route} class functionality and behavior.
- *
- * Verifies routing component's ability to handle various configurations and matching scenarios.
- *
- * These tests ensure routing features work correctly under different conditions and maintain consistent behavior after
- * code changes.
- *
- * The tests validate complex route matching with parameters, middleware handling, and host configuration, which are
- * essential for proper HTTP request routing in the framework.
- *
- * Test coverage.
- * - Host configuration (domain patterns, subdomain support, hostname matching).
- * - HTTP method handling (normalization, case-insensitivity, method restrictions).
- * - Middleware configuration (single, multiple, order preservation).
- * - Parameter handling (encoded paths, URL-encoded values, parameter extraction).
- * - Path matching (exact match, non-matching paths, suffix handling).
- * - Pattern matching (required and optional segments, path matching).
- * - Priority handling (route priority management).
- * - Query parameters (required, optional, validation, type handling).
- * - Route immutability and builder pattern implementation.
- * - Route name validation (valid names, length constraints, complex patterns).
- * - Route parameters (validation, defaults, pattern matching).
- * - Suffix handling and URL normalization.
- * - URL encoding (percent-encoded paths, proper decoding).
- * - Versioning support (version prefixing, version mismatch handling).
- *
- * @see \PHPPress\Tests\Provider\Router\RouteProvider for test case data providers.
- *
- * @copyright Copyright (C) 2023 Terabytesoftw.
- * @license https://opensource.org/license/bsd-3-clause BSD 3-Clause License.
- */
-#[Group('router')]
-final class RouteTest extends TestCase
-```
-
-## Method Documentation
-
-### Constructor
-```php
-/**
- * Creates a new instance of the {@see \PHPress\Router\RouteCollection} class.
- *
- * @param array $routes Initial routes to populate the collection, indexed by route name.
- *
- * @phpstan-param Route[] $routes
- */
-```
-
-### Regular Methods
-```php
-/**
- * Retrieves all routes in the collection.
- *
- * Provides direct access to the full set of registered routes in their original indexed form.
- *
- * This method is useful for route inspection, debugging, and bulk operations on the entire route collection.
- *
- * @return array Array of all registered routes indexed by name.
- *
- * Usage example:
- * ```php
- * $allRoutes = $collection->all();
- * ```
- *
- * @phpstan-return Route[]
- */
-```
-
-### Protected Methods
-(Ensure that PHPDoc is as detailed as for `public` methods, especially if they influence behavior.)
-
-## Property Documentation
-
-- Avoid `@var` if the type is already declared in PHP.
-- Use `@phpstan-var` for complex arrays.
-
-```php
-/**
- * A map of theme names to their corresponding paths.
- * @phpstan-var array
- */
-private array $themeMap = [];
-```
-
-## Type Hints and Templates
-
-```php
-/**
- * @template T of FilesystemAdapter
- * @param array $config
- * @param Collection $items
- */
-```
-
-```php
-/**
- * @param string|array $paths
- * @return string[]|false
- */
-```
-
-## Exception Documentation (@throws)
-
-All @throws annotations must be written in English, use complete sentences, and follow a standardized structure for
-consistency and tooling compatibility (for example, static analysis, Copilot, IDEs).
-
-### Structure
-
-```php
-@throws ExceptionClass if [specific condition that causes the exception].
-```
-
-- Use if clauses to describe when the exception is thrown.
-- Avoid vague wording like “an error occurs...” — be clear and precise.
-- Use the most specific exception available. Reserve Throwable or RuntimeException for unexpected or generic errors.
-- Keep the description generic unless the method’s context requires specificity.
-
-### Standardized Examples
-
-```php
-@throws BadRequestException if the request is invalid or can't be processed.
-@throws EmptyStackException if the stack is empty when an element is expected.
-@throws FilesystemException if the filesystem operation is invalid or fails.
-@throws InvalidArgumentException if one or more arguments are invalid, of incorrect type or format.
-@throws InvalidConfigException if the configuration is invalid or incomplete.
-@throws InvalidDefinitionException if the definition is invalid or improperly configured.
-@throws InvalidRouteParameterException if the route parameters are invalid or incomplete.
-@throws MiddlewareResolutionException if the middleware handler is invalid or can't be resolved.
-@throws NotInstantiableException if a class or service can't be instantiated.
-@throws RouteAlreadyExistsException if a route with the same name is already registered.
-@throws RouteNotFoundException if the route is not found or undefined.
-@throws RuntimeException if a runtime error prevents the operation from completing successfully.
-@throws TemplateNotFoundException if the template file is missing or can't be located.
-@throws Throwable if an unexpected error occurs during execution.
-@throws ViewNotFoundException if the view or associated renderer is missing or unavailable.
-@throws WidgetStackException if the stack is empty, or if `end()` is called without a `begin()` call.
-matching {@see AbstractBlock::begin()}.
-```
-
-## Best Practices
-
-### Precision
-- Document only existing functionality.
-- Update documentation when changing code.
-- Don't document planned or future features.
-
-### Recommended Structure
-1. Clear purpose.
-2. How it is implemented.
-3. List of features.
-4. Usage example.
-5. Cross-references.
-
-### Bad vs Good Examples
-```php
-// ❌ Incorrecto
-/** Does something */
-function doSomething() {}
-
-// ✅ Correcto
-/**
- * Loads a config file and parses options.
- *
- * @param string $path Path to config file.
- * @return array Parsed options.
- */
-function loadConfig(string $path): array {}
-```
-
-## Copilot Optimization Tips
-- Use descriptive names for functions and variables.
-- Always declare input/output types.
-- Keep PHPDoc updated.
-- Use template annotations (`@template`, `@phpstan-*`).
-- Include practical and realistic usage examples.
-- Emphasize the "why" to improve model suggestions.
-- Use comments within `php` blocks to explain each step.
diff --git a/copilot/code-style.md b/copilot/code-style.md
deleted file mode 100644
index fa56da0..0000000
--- a/copilot/code-style.md
+++ /dev/null
@@ -1,185 +0,0 @@
-# Code Style Guidelines
-
-This guide defines coding conventions and PHP 8.2 usage for the PHP-Press project.
-It complements `class-documentation.md`, `unit-test.md`, and `documentation.md` to improve readability, maintainability,
-and Copilot-assisted development.
-
-## Table of Contents
-1. PHP 8.1 Feature Usage.
-2. Code Style Rules.
-3. Best Practices.
-4. Copilot Optimization Tips.
-
-## PHP 8.2 Feature Usage
-
-### Typed Properties with Union Types
-Use explicit union types when needed:
-```php
-private string|int $value;
-private readonly DependencyResolver $resolver;
-private BundleInterface|null $bundle = null;
-```
-
-### Constructor Property Promotion
-Prefer concise constructor definitions:
-```php
-public function __construct(
- private readonly string $name,
- private readonly int $value = 0,
- private readonly array|null $config = null,
-) {}
-```
-
-### Arrow Functions and Array Operations
-Use arrow functions for transformation. Prefer `foreach` for complex logic or side effects.
-```php
-$transformed = array_map(static fn($x) => $x * 2, $items);
-$hasNegative = array_any($items, static fn($x) => $x < 0);
-```
-
-### Match Expressions
-Use match for explicit value checks:
-```php
-$result = match ($value) {
- 1, 2 => 'low',
- 3 => 'medium',
- default => 'high',
-};
-```
-Avoid `match(true)` constructs.
-
-### Named Arguments
-Use for disambiguation only:
-```php
-// Good
-$object->configure(options: ['cache' => true], resolver: $resolver);
-
-// Prefer ordered when obvious
-$object->configure($resolver, ['cache' => true]);
-```
-
-### First-class Callable Syntax
-```php
-$handler = $object->handle(...);
-$callback = strtolower(...);
-```
-
-### Enums
-Document each case clearly:
-```php
-enum HttpMethod: string {
- case GET = 'GET'; // Retrieves data
- case POST = 'POST'; // Submits data
- // ...
-}
-```
-## Code Style Rules
-
-### Formatting
-- Line length: 120 characters.
-- Indentation: 4 spaces.
-- PSR-12 compliant.
-- Files must end with newline.
-
-### Namespace & Imports
-```php
-declare(strict_types=1);
-
-namespace PHPPress\Component;
-
-use PHPPress\Event\EventDispatcher;
-
-use function array_map;
-use function sprintf;
-```
-Group `use function` separately after classes.
-
-### Class Structure
-- One class per file.
-- PSR-4 structure.
-- Constants > Properties > Methods.
-- Logical grouping.
-
-### Method Design
-- Always declare visibility.
-- Use return/parameter types.
-- Prefer early returns.
-- Keep short, focused methods.
-
-### Properties
-```php
-private readonly EventDispatcherInterface $dispatcher;
-private array $options = ['debug' => false];
-```
-Use `string|null` instead of `?string` for consistency.
-
-### Type Declarations
-- Strict types enabled.
-- Prefer union with `null` instead of nullable (`?`).
-- Use `mixed` sparingly.
-- Document complex types in PHPDoc.
-
-### Function Imports
-```php
-use function array_map;
-use function trim;
-```
-One per line. Alphabetical order.
-
-### Error Handling
-- Use specific exceptions.
-- Use enums for messages.
-- Use `??` operator for fallbacks.
-- Document `@throws` in PHPDoc.
-
-### Arrays
-```php
-$config = [
- 'debug' => true,
- 'cache' => false,
- 'version' => '1.0.0',
-];
-```
-Use short syntax. Align `=>` in multiline.
-
-### Control Structures
-```php
-if ($condition === false) {
- return null;
-}
-```
-- Braces on same line.
-- One space after control keywords.
-- Use guard clauses.
-
-## Best Practices
-- Composition over inheritance.
-- Follow SOLID principles.
-- Immutable objects when possible.
-- Fluent interfaces for configuration.
-- Clear and descriptive naming.
-- Validate inputs early.
-
-### Immutable + Fluent Pattern
-```php
-$route = Route::get('users', '/users', $handler)
- ->withParameters(['id' => '\d+'])
- ->withDefaults(['format' => 'json'])
- ->withHost('api.example.com');
-```
-
-## Copilot Optimization Tips
-- Keep structure and naming predictable.
-- Add inline comments to show intent.
-- Document "why" in PHPDoc.
-- Use examples in comments or docblocks.
-- Use type declarations consistently.
-- Avoid overloading logic in one line.
-
-**Example for Copilot context:**
-```php
-// Create a route with constraints
-$route = Route::get('profile', '/user/{id}', ProfileController::class)
- ->withDefaults(['id' => '1'])
- ->withParameters(['id' => '\d+']);
-```
diff --git a/copilot/copilot-instructions.md b/copilot/copilot-instructions.md
deleted file mode 100644
index b9a9bf4..0000000
--- a/copilot/copilot-instructions.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# Copilot AI Coding Agent Instructions for yii2-extensions/psr-bridge
-
-## Project Overview
-- This is a Yii2 extension providing PSR-7/PSR-15 bridge functionality for Yii2 applications.
-- The codebase is organized by domain: `src/adapter/`, `src/emitter/`, `src/http/`, and `src/worker/`.
-- All code is written for PHP 8.2+ and follows strict type and code style rules (see `copilot/code-style.md`).
-- The extension is designed for compatibility with Yii2, PSR-7, and PSR-15 interfaces.
-
-## Key Architectural Patterns
-- **Adapter Pattern:** `src/adapter/ServerRequestAdapter.php` bridges PSR-7 requests to Yii2's request model.
-- **Immutable Value Objects:** Many classes (e.g., HTTP request/response) are immutable; use fluent methods for configuration.
-- **PSR-4 Autoloading:** Namespace and directory structure strictly follow PSR-4.
-- **Exception Handling:** All exceptions are custom and documented; see `src/emitter/exception/` and `src/http/exception/`.
-- **Testing Mirrors Source:** Tests in `tests/` mirror the `src/` structure and use PHPUnit with full Arrange-Act-Assert.
-
-## Developer Workflows
-- **Build/Static Analysis:**
- - Run static analysis: `vendor\bin\phpstan analyse`
- - Run code style: `vendor\bin\ecs check`
- - Run tests: `vendor\bin\phpunit`
-- **Test Coverage:** 100% coverage is required for PRs. Add tests in `tests/` with the same namespace as the class under test.
-- **Conventional Commits:** All commits use `type(scope): description.` (see `copilot/general-instructions.md`).
-- **CI/CD:** All PRs are validated by GitHub Actions for static analysis, style, and tests.
-
-## Project-Specific Conventions
-- **Type Declarations:** Use `string|null` instead of `?string` for nullable types.
-- **Array Types:** Use `@phpstan-var` for complex array types in PHPDoc.
-- **Documentation:** All classes and methods must have English PHPDoc, following `copilot/class-documentation.md`.
-- **Test Naming:** Test methods use `test` (see `copilot/unit-test.md`).
-- **Data Providers:** Place in `tests/provider/` and suffix with `Provider`.
-- **Error Messages:** Use enums for standardized exception messages.
-
-## Integration Points
-- **PSR-7/PSR-15:** Integrates with any PSR-7 compatible HTTP stack.
-- **Yii2:** Extends and adapts Yii2 core request/response classes.
-- **External Tools:** Uses PHPStan, ECS, PHPUnit, and Codecov for quality and coverage.
-
-## Examples
-- See `src/http/Request.php` for a typical adapter usage pattern.
-- See `tests/http/PSR7RequestTest.php` for test structure and coverage expectations.
-- See `copilot/` for all project-specific coding and documentation standards.
-
-## References
-- [copilot/general-instructions.md](../copilot/general-instructions.md)
-- [copilot/code-style.md](../copilot/code-style.md)
-- [copilot/class-documentation.md](../copilot/class-documentation.md)
-- [copilot/unit-test.md](../copilot/unit-test.md)
-- [copilot/static-analysis.md](../copilot/static-analysis.md)
-
----
-
-If you are unsure about a pattern or workflow, check the `copilot/` directory or ask for clarification in your PR.
diff --git a/copilot/general-instructions.md b/copilot/general-instructions.md
deleted file mode 100644
index 3ca3063..0000000
--- a/copilot/general-instructions.md
+++ /dev/null
@@ -1,97 +0,0 @@
-# General Project Instructions
-
-This document outlines global standards for the PHP-Press project.
-It complements `class-documentation.md`, `unit-test.md`, `documentation.md`, and `code-style.md` to provide a unified
-foundation for team workflows, Copilot guidance, and contributor consistency.
-
-## Table of Contents
-1. Communication
-2. Language & Version
-3. Code Organization
-4. Quality Assurance
-5. Documentation Standards
-6. Contribution Workflow
-7. Reference Documents
-
-## Communication
-
-- Respond to users **in Spanish**.
-- Write all code and documentation **in English**.
-- Ask for clarification if a request or requirement is ambiguous.
-- Use clear, precise phrasing aligned with the **Microsoft Writing Style Guide**.
-- When referencing filesystem paths related to the MCP command system, assume the project root is: `/core`.
-
-## Language & Version
-
-- Use **PHP 8.2** for all backend development.
-- Adopt new features such as:
- - Constructor property promotion.
- - Property hooks.
- - Arrow functions.
- - Enum types.
- - Array utility functions (`array_any`, `array_all`, etc.).
-- See `code-style.md` for usage patterns and examples.
-
-## Code Organization
-
-- Follow **PSR-4 autoloading** and namespace structure.
-- Group components by domain (for example, `Router`, `View`, `Asset`).
-- Keep interfaces in `Interface/` or `Contracts/` folders.
-- Structure tests to mirror the source tree inside the `/tests/` folder.
-- Use the following directory conventions:
- ```text
- /core
- /Src
- /ComponentName
- /tests
- /ComponentName
- ```
-
----
-
-## Quality Assurance
-
-- Use **PHPUnit** for all unit tests.
-- Aim for **100% test coverage**.
-- Configure **PHPStan** at **level 5 or higher** (see `static-analysis.md`).
-- Apply code style fixes using **ECS**.
-
-> Ensure every pull request includes validation for:
-> - Static analysis.
-> - Code style.
-> - Unit tests passing.
-
-## Documentation Standards
-
-- Follow `class-documentation.md` for PHPDoc in classes, methods, and properties.
-- Use `documentation.md` for non-code documentation.
-- Add real-world usage examples in class-level docblocks.
-- Maintain a structured and complete `CHANGELOG.md`.
-- Document exceptions with `@throws` and type declarations.
-
-## Contribution Workflow
-
-- All contributors must:
- - Fork from the `main` branch.
- - Create topic branches per feature/fix.
- - Use **conventional commits** format:
- ```
- type(scope): description.
- ```
- Example: `feat(router): add host validation`.
- - Reference related issues via `#ID` in commits or PR descriptions.
- - Include test coverage for new functionality.
- - Pass all CI checks before requesting review.
-
----
-
-## Reference Documents
-
-For deeper implementation and writing standards, refer to:
-
-- [`class-documentation.md`](./class-documentation.md) — Class and method PHPDoc rules.
-- [`unit-test.md`](./unit-test.md) — Test architecture and PHPUnit conventions.
-- [`documentation.md`](./documentation.md) — General writing and formatting rules.
-- [`code-style.md`](./code-style.md) — PHP code syntax and 8.2 features.
-- [`static-analysis.md`](./static-analysis.md) — PHPStan config, rules, and usage.
-
diff --git a/copilot/project-documentation.md b/copilot/project-documentation.md
deleted file mode 100644
index ce9956e..0000000
--- a/copilot/project-documentation.md
+++ /dev/null
@@ -1,192 +0,0 @@
-# Documentation Guidelines
-
-This guide defines general documentation standards for non-code content in the PHP-Press project and is designed to work
-in tandem with `class-documentation.md` and `unit-test.md`. It is optimized for clarity, maintainability, and
-compatibility with GitHub Copilot.
-
-## Table of Contents
-1. Writing Style.
-2. Technical Writing.
-3. Documentation Types.
-4. Architecture Docs.
-5. Maintenance.
-6. Copilot Optimization Tips.
-
-## Writing Style
-
-### Tone and Voice
-- Use a **professional but conversational** English tone.
-- Write with **clarity and purpose**; prefer **active voice**.
-- Address readers directly ("you").
-- Explain the **"why"** behind important guidance.
-
-### Microsoft Writing Style Compliance
-- Use simple sentence structures (subject + verb + object).
-- Keep content concise and conversational.
-- Prefer contracted forms (for example, "it's", "you're").
-- Use **sentence-style capitalization**.
-- Write numbers as numerals (for example, "5 users").
-- Format dates as "March 26, 2024".
-- Avoid:
- - Sentence fragments
- - Synonyms for a single concept
- - Culturally sensitive terms
- - Long modifier chains
- - Overly complex words
-
-### Formatting Guidelines
-- Use headings to organize sections.
-- Use whitespace for readability.
-- Use bullet points for unordered lists and numbered lists for sequences.
-- Keep paragraphs short (3–4 sentences).
-- Include serial commas in lists.
-
-## Technical Writing
-
-### Code References
-Use backticks \``\` for inline code like:
-
-- `composer.json`
-- `DependencyResolver`
-- `configure()`
-- `$config`
-- `PHP_VERSION`
-- `['debug' => true]`
-
-### Code Examples
-- Use fenced code blocks with language hints.
-- Add context with inline comments.
-- Keep examples **minimal but complete**.
-- Show **input and expected output** when relevant.
-- Follow a **consistent coding style**.
-
-**Example:**
-
-```php
-// Configure the component with options
-$component = new Component(
- [
- 'debug' => true,
- 'cache' => false,
- ],
-);
-
-// Returns: ['status' => 'configured']
-$result = $component->getStatus();
-```
-
-### Links and References
-- Use **relative links** for project docs.
-- Use **descriptive link text**.
-- Include version numbers for dependencies.
-- Link to source code or issues when appropriate.
-
-## Documentation Types
-
-### README Files
-- Project overview and purpose
-- List of key features
-- Quick start guide
-- Installation steps
-- Usage examples
-- Dependencies
-- License information
-
-### API Documentation
-- Purpose and scope
-- Authentication mechanism
-- Request and response format
-- Error structures and codes
-- Rate limiting policies
-- Usage examples per endpoint
-- Reference to external SDKs or libraries
-
-### Tutorials
-- Clear learning objectives
-- Prerequisites list
-- Step-by-step instructions
-- Complete code examples
-- Screenshots or expected output
-- Troubleshooting tips
-- References to deeper docs
-
-### Changelogs
-- Follow **semantic versioning**.
-- Group entries by type:
- - Added
- - Changed
- - Deprecated
- - Removed
- - Fixed
- - Security
-- Provide migration notes if relevant.
-- Link to related issues or PRs.
-
-## Architecture Docs
-
-### Component Documentation
-- Component purpose and context
-- Internal and external dependencies
-- Configuration options and defaults
-- Usage patterns and lifecycle
-- Event triggers and listeners
-- Extensibility points (hooks/plugins)
-- Performance or scaling considerations
-
-### Integration Guides
-- System/environment requirements
-- Setup and configuration
-- Supported platforms or stacks
-- Common scenarios and walkthroughs
-- Error handling and logging
-- Security best practices
-- Links to example projects or templates
-
-## Maintenance
-
-### Version Control
-- Keep docs versioned alongside code.
-- Submit doc updates as part of pull requests.
-- Tag documentation per release.
-- Archive obsolete versions.
-- Track changes in changelogs or history.
-
-### Quality Checks
-- Spellcheck and grammar review
-- Validate links and anchors
-- Run code snippets if executable
-- Refresh outdated screenshots
-- Test configuration instructions
-- Use consistent formatting across files
-
-### File and Folder Organization
-- Match doc folder structure to source tree
-- Add index files or README in folders
-- Add navigation or sidebar links
-- Use version indicators where needed
-- Encourage cross-referencing via `{@see}` or relative links
-
-### Feedback and Improvement
-- Monitor issue trackers and feedback tools
-- Encourage user input for unclear docs
-- Update or reword misunderstood sections
-- Remove or flag outdated content
-- Add examples where readers get stuck
-
-## Copilot Optimization Tips
-- Use **clear and consistent terminology**.
-- Provide **working, well-commented examples**.
-- Declare types when describing parameters or code structure.
-- Keep each section purpose-focused (Copilot benefits from separation).
-- Avoid speculative language (for example, “might do this”) and favor definitiveness.
-- Include real-world usage patterns in documentation.
-- Use consistent phrasing so Copilot can detect doc intent.
-
-**Copilot-aligned example:**
-
-```php
-// Correct way to initialize the router
-$router = new Router();
-$router->register(...);
-$router->dispatch($request);
-```
diff --git a/copilot/static-analysis.md b/copilot/static-analysis.md
deleted file mode 100644
index 0bfa98b..0000000
--- a/copilot/static-analysis.md
+++ /dev/null
@@ -1,178 +0,0 @@
-# Static Analysis Guidelines
-
-This document defines rules for static code analysis to ensure type safety and detect errors early in the PHP-Press project.
-
-## PHPStan Configuration
-
-### Level Settings
-- Use PHPStan level 5 as the minimum requirement.
-- Level 8 is mandatory for core components.
-- Configure per-directory level settings as needed.
-
-### Base Rules
-- Enable strict mode for all files.
-- Require explicit method return types.
-- Require explicit property types.
-- Enable dead code detection.
-- Validate template type constraints.
-
-## Type Declarations
-
-### Property Types
-```php
-/**
- * @var array Registered bundles indexed by class name.
- */
-private array $bundles = [];
-
-/**
- * @var Collection CSS link tags for rendering.
- */
-private array $css = [];
-
-/**
- * @var array, array> Event listeners by event class.
- */
-private array $listeners = [];
-```
-
-### Method Types
-```php
-/**
- * @template T of object
- * @param class-string $class
- * @param array $config
- * @return T
- *
- * @throws InvalidArgumentException
- * @throws ContainerException
- */
-public function create(string $class, array $config = []): object;
-```
-
-### Generic Types
-```php
-/**
- * @template TKey of array-key
- * @template TValue
- *
- * @param array $items
- * @param callable(TValue): bool $predicate
- * @return array
- */
-public function filter(array $items, callable $predicate): array;
-```
-
-### Union Types
-```php
-/**
- * @param array|string|null $paths
- * @return string[]
- *
- * @throws InvalidArgumentException When path is invalid.
- */
-public function resolvePaths(array|string|null $paths): array;
-```
-
-## PHPStan Baseline
-
-### Managing Baseline
-- Generate a baseline for existing issues.
-- Review and document accepted issues.
-- Update the baseline with each major release.
-- Track technical debt in the baseline.
-
-### Baseline Command
-```bash
-vendor/bin/phpstan analyse --generate-baseline
-```
-
-## Custom Rules
-
-### Rule Categories
-- Architectural rules.
-- Naming conventions.
-- Type safety rules.
-- Framework-specific rules.
-
-### Rule Implementation
-```php
-/**
- * @implements Rule
- */
-final class CustomRule implements Rule
-{
- public function getNodeType(): string
- {
- return Node\Stmt\Class_::class;
- }
-
- /**
- * @param Node\Stmt\Class_ $node
- */
- public function processNode(Node $node, Scope $scope): array
- {
- // Rule implementation.
- }
-}
-```
-
-## Error Categories
-
-### Type Safety
-- Undefined methods/properties.
-- Invalid argument types.
-- Incompatible return types.
-- Missing type declarations.
-- Template type mismatches.
-
-### Dead Code
-- Unreachable code paths.
-- Unused private methods.
-- Unused parameters.
-- Redundant conditions.
-- Dead catch blocks.
-
-### Method Calls
-- Unknown method calls.
-- Invalid argument counts.
-- Type compatibility issues.
-- Static call validity.
-- Visibility violations.
-
-### Property Access
-- Undefined properties.
-- Invalid property types.
-- Uninitialized properties.
-- Readonly violations.
-- Visibility checks.
-
-## Best Practices
-
-### Configuration
-- Use `phpstan.neon.dist` as the base configuration.
-- Include `baseline.neon`.
-- Configure for the CI/CD pipeline.
-- Set memory limits appropriately.
-- Enable result caching.
-
-### Error Handling
-- Document intentional suppressions.
-- Use ignore patterns sparingly.
-- Review suppressions regularly.
-- Track technical debt items.
-- Fix issues incrementally.
-
-### Performance
-- Enable parallel analysis.
-- Configure memory limits.
-- Use result caching.
-- Optimize ignore patterns.
-- Analyze incrementally.
-
-### Integration
-- Run in the CI/CD pipeline.
-- Block merges on errors.
-- Generate HTML reports.
-- Track error trends.
-- Review in PRs.
diff --git a/copilot/unit-test.md b/copilot/unit-test.md
deleted file mode 100644
index df60745..0000000
--- a/copilot/unit-test.md
+++ /dev/null
@@ -1,179 +0,0 @@
-# Unit Test Guidelines
-
-## Table of Contents
-1. Test Class Naming.
-2. Test Method Naming.
-3. Test Structure (Arrange-Act-Assert).
-4. Data Providers.
-5. Best Practices.
-6. Copilot Optimization Tips.
-
-## Test Class Naming
-
-### Basic Rules
-- **Clarity and Descriptiveness:** The test class name should clearly reflect the class being tested.
-- **Namespace Structure:** The test class should follow the same namespace structure as the production class,
- located in the `tests/` directory.
-- **Naming Convention:** The class name should match the production class and end with `Test`.
-- **Internal Documentation:** Document each test case and make sure to mock the necessary dependencies.
-
-**Example:**
-```php
-// Class to test in src/AbstractView/View.php
-namespace PHPPress\View;
-
-class View { }
-
-// Test class in tests/AbstractView/ViewTest.php
-namespace PHPPress\Tests\View;
-
-class ViewTest { }
-```
-
-## Test Method Naming
-
-### Recommended Pattern
-Use the format `test` for test methods, where:
-- **:** Describes the main action (Render, Throw, Return, etc.).
-- **:** Indicates the subject under test (View, Layout, etc.).
-- **:** Specifies the condition or scenario.
-
-**Examples:**
-```php
-public function testRenderLayoutWithContext(): void { }
-public function testRenderViewWithTheme(): void { }
-public function testThrowExceptionWhenTemplateInvalid(): void { }
-```
-
-### Organization
-- Group and order test methods alphabetically.
-- Maintain consistent nomenclature within the same test class.
-
-## Test Structure (Arrange-Act-Assert)
-
-Each test should follow the AAA pattern to ensure clarity in the intention and behavior of each test:
-
-```php
-public function testRenderWithParametersReplacesPlaceholders(): void
-{
- // Arrange: Set up the test object and necessary parameters.
- $view = new View($resolver, $dispatcher);
- $parameters = ['title' => 'Test'];
-
- // Act: Execute the main action.
- $result = $view->render('index', $parameters);
-
- // Assert: Validate that the result meets expectations.
- $this->assertStringContainsString(
- 'Test',
- $result,
- 'Rendered view should contain the title parameter.',
- );
-}
-```
-
-## Data Providers
-
-### Location and Convention
-- Place data providers in the `/tests/Provider/` directory.
-- Name the files and classes with a `Provider` suffix (for example, `ConverterCommandProvider`).
-
-### Documentation and Example
-Include complete documentation in each data provider, explaining the structure and purpose of the data.
-
-**Example:**
-path: /core/tests/Provider/Router/RouteProvider.php
-
-```php
-namespace PHPPress\Tests\Provider\Router;
-
-/**
- * Data provider for testing the Router component.
- *
- * Designed to ensure the router component correctly processes all supported configurations and appropriately handles
- * edge cases with proper error messaging.
- *
- * The test data validates complex route configuration scenarios including security-sensitive inputs to prevent
- * potential injection vulnerabilities.
- *
- * The provider organizes test cases with descriptive names for quick identification of failure cases during test
- * execution and debugging sessions.
- *
- * Key features.
- * - Comprehensive test cases for all router features.
- * - Edge case testing for input validation.
- * - Host pattern matching with domain validation.
- * - Named test data sets for clear failure identification.
- * - Parameter pattern validation with regular expressions.
- * - Security scenario testing for potential injection patterns.
- * - URL suffix and pattern matching validation.
- *
- * @copyright Copyright (C) 2024 PHPPress.
- * @license https://opensource.org/license/gpl-3-0 GNU General Public License version 3 or later.
- */
-final class RouteProvider
-```
-
-**Usage in Tests:**
-path: /core/tests/Router/RouteTest.php
-
-```php
-#[DataProviderExternal(RouteProvider::class, 'names')]
-public function testAcceptRouteNamesWhenValid(string $name): void
-{
- $route = Route::to($name, '/test', FactoryHelper::createHandler());
-
- $this->assertSame($name, $route->name, "Route should accept valid route name: '{$name}'.");
-}
-```
-
-## Best Practices
-
-### General Principles
-- **Independence:** Each test should be independent and not depend on the execution order.
-- **Cleanup:** Use `setUp()` and `tearDown()` to clean up global or static states between tests.
-- **Complete Coverage:** Test both positive and negative cases, including edge values and exception scenarios.
-- **Clear Messages:** Assertion messages should explain what was expected and what was obtained, making it easier to
- identify failures.
-
-**Example of Detailed Assertion:**
-```php
-$this->assertSame(
- $expected,
- $actual,
- 'Route should match when URL parameters meet the expected pattern with multiple parameters.'
-);
-$this->assertTrue(
- $this->filesystem->fileExists("{$this->dirFrom}/js/script.js"),
- 'File \'js/script.js\' should exist in the destination after excluding only the strict subdirectory legacy CSS.',
-);
-$this->assertTrue(
- $this->filesystem->copyMatching($this->dir, $this->dirFrom),
- 'Copy matching should return \'true\' when copying all files recursively without patterns.',
-);
-```
-
-### Exceptions in Tests
-- Document in the PHPDoc the conditions under which an exception is expected to be thrown, including clear examples.
-
-**Example:**
-path: /core/tests/Router/RouteTest.php
-
-```php
-public function testThrowExceptionWithInvalidVersion(): void
-{
- $this->expectException(InvalidRouteParameterException::class);
- $this->expectExceptionMessage(Message::INVALID_ROUTE_VERSION->getMessage('invalid'));
-
- Route::get('users', '/users', FactoryHelper::createHandler())->withVersion('invalid');
-}
-```
-
-## Copilot Optimization Tips
-
-- **Clear and Descriptive Names:** Use meaningful names for methods and variables.
-- **Type Declaration:** Always specify parameter and return types in your test methods.
-- **Updated Documentation:** Keep PHPDoc updated with clear and detailed examples.
-- **Code Comments:** Add brief comments explaining each part of the AAA structure in your tests.
-- **Explanation of "Why":** Include in the documentation the rationale behind each test so that tools
- like GitHub Copilot can generate suggestions aligned with the design.
From 85b806bf49fb9e88371f9f0f48c6d9debe61cfc1 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Mon, 18 Aug 2025 11:48:59 -0400
Subject: [PATCH 11/13] fix(readme): improve clarity and consistency in feature
descriptions.
---
README.md | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index f97c685..cf609db 100644
--- a/README.md
+++ b/README.md
@@ -24,16 +24,16 @@
## Features
✅ **Advanced Reflection Utilities**
-- Access and modify private/protected properties via reflection.
-- Invoke inaccessible methods for comprehensive testing coverage.
+- Access and modify private and protected properties via reflection.
+- Invoke inaccessible methods to expand testing coverage.
✅ **Cross-Platform String Assertions**
-- Eliminate false negatives from Windows/Unix line ending differences.
+- Eliminate false positives/negatives caused by Windows vs. Unix line-ending differences.
- Normalize line endings for consistent string comparisons across platforms.
✅ **File System Test Management**
-- Recursive file and directory cleanup for isolated test environments.
-- Safe removal operations preserving Git tracking files.
+- Recursively clean files and directories for isolated test environments.
+- Safe removal that preserves Git-tracking files (for example, '.gitignore', '.gitkeep').
## Quick start
From e8c9a34f7b37fbaeef7e82c10bd495a2cb7df839 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Mon, 18 Aug 2025 12:00:31 -0400
Subject: [PATCH 12/13] fix(readme): update project title for clarity and
improved description of testing capabilities.
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index cf609db..12e2718 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
- Support.
+ Support utilities for enhanced testing capabilities.
From 09b57b4bb9b8b3e9926f16ea2086b88823fb7d2b Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Mon, 18 Aug 2025 12:17:34 -0400
Subject: [PATCH 13/13] Add line to `CHANGELOG.md`.
---
CHANGELOG.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d8d8ea..5222d47 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,9 @@
Change Log
==========
-## 0.1.1 Under development
+## 0.1.1 August 18, 2025
+
+- Bug #11: Refactor project structure and update dependencies (@terabytesoftw)
## 0.1.0 January 21, 2024