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 -[![PHP81](https://img.shields.io/badge/PHP-%3E%3D8.1-787CB5)](https://www.php.net/releases/8.1/en.php) -[![Yii30](https://img.shields.io/badge/Yii%20version-3.0-blue)](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 -[![Twitter](https://img.shields.io/badge/twitter-follow-1DA1F2?logo=twitter&logoColor=1DA1F2&labelColor=555555?style=flat)](https://twitter.com/Terabytesoftw) +[![X](https://img.shields.io/badge/follow-@terabytesoftw-1DA1F2?logo=x&logoColor=1DA1F2&labelColor=555555&style=flat)](https://x.com/Terabytesoftw) ## License +[![License](https://img.shields.io/github/license/php-forge/support?cacheSeconds=0)](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 -[![PHP81](https://img.shields.io/badge/PHP-%3E%3D8.1-787CB5)](https://www.php.net/releases/8.1/en.php) -[![Yii30](https://img.shields.io/badge/Yii%20version-3.0-blue)](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 -[![Twitter](https://img.shields.io/badge/twitter-follow-1DA1F2?logo=twitter&logoColor=1DA1F2&labelColor=555555?style=flat)](https://twitter.com/Terabytesoftw) +[![X](https://img.shields.io/badge/follow-@terabytesoftw-1DA1F2?logo=x&logoColor=1DA1F2&labelColor=555555&style=flat)](https://x.com/Terabytesoftw) ## License +[![License](https://img.shields.io/github/license/php-forge/support?cacheSeconds=0)](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 @@

+ + PHP Version + PHPUnit - - Codecov - Infection - - - Psalm - - - Psalm Coverage - - - Style ci - + + + Static Analysis +

## Features @@ -184,6 +178,14 @@ For comprehensive testing guidance, see: - 🧪 [Testing Guide](docs/testing.md) +## Quality code + +[![Latest Stable Version](https://poser.pugx.org/php-forge/support/v)](https://github.com/php-forge/support/releases) +[![Total Downloads](https://poser.pugx.org/php-forge/support/downloads)](https://packagist.org/packages/php-forge/support) +[![codecov](https://codecov.io/gh/php-forge/support/graph/badge.svg?token=Upc4yA23YN)](https://codecov.io/gh/php-forge/support) +[![phpstan-level](https://img.shields.io/badge/PHPStan%20level-max-blue)](https://github.com/php-forge/support/actions/workflows/static.yml) +[![StyleCI](https://github.styleci.io/repos/661073468/shield?branch=main)](https://github.styleci.io/repos/661073468?branch=main) + ## Our social networks [![X](https://img.shields.io/badge/follow-@terabytesoftw-1DA1F2?logo=x&logoColor=1DA1F2&labelColor=555555&style=flat)](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 @@ PHPUnit - Infection + Infection Static Analysis @@ -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](https://img.shields.io/github/license/php-forge/support?cacheSeconds=0)](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