From 8c057281dec8d2c663e1f52244572027d91452e4 Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Tue, 6 May 2025 17:17:16 +0200 Subject: [PATCH 1/5] feat: Add CI stubs --- .github/workflows/on-pr-merged.yml | 33 +++++++++++++++++++++++ .github/workflows/on-pr.yml | 43 ++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 .github/workflows/on-pr-merged.yml create mode 100644 .github/workflows/on-pr.yml diff --git a/.github/workflows/on-pr-merged.yml b/.github/workflows/on-pr-merged.yml new file mode 100644 index 0000000..36529bb --- /dev/null +++ b/.github/workflows/on-pr-merged.yml @@ -0,0 +1,33 @@ +name: After-Merge Chores +on: + pull_request: + types: + - closed + # branches: + # - FRAMEWORK_6_0 + workflow_dispatch: + +jobs: + PostMerge: + if: github.event.pull_request.merged == true + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + extensions: bcmath, ctype, curl, dom, gd, gettext, iconv, imagick, json, ldap, mbstring, mysql, opcache, openssl, pcntl, pdo, posix, redis, soap, sockets, sqlite, tokenizer, xmlwriter, xdebug + ini-values: post_max_size=512M, max_execution_time=360 + coverage: xdebug + tools: php-cs-fixer, phpunit:${{ matrix.phpunit-versions }}, composer:v2 + - name: Setup Github Token as composer credential + run: composer config -g github-oauth.github.com ${{ secrets.GITHUB_TOKEN }} + - name: Install dependencies and local tools + run: | + COMPOSER_ROOT_VERSION=dev-FRAMEWORK_6_0 composer config minimum-stability dev + COMPOSER_ROOT_VERSION=dev-FRAMEWORK_6_0 composer config prefer-stable true + COMPOSER_ROOT_VERSION=dev-FRAMEWORK_6_0 composer install --no-interaction --no-progress + diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml new file mode 100644 index 0000000..9e936ba --- /dev/null +++ b/.github/workflows/on-pr.yml @@ -0,0 +1,43 @@ +name: Pull Request Chores +on: + pull_request: + branches: + - FRAMEWORK_6_0 + workflow_dispatch: + +jobs: + CI: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + extensions: bcmath, ctype, curl, dom, gd, gettext, iconv, imagick, json, ldap, mbstring, mysql, opcache, openssl, pcntl, pdo, posix, redis, soap, sockets, sqlite, tokenizer, xmlwriter, xdebug + ini-values: post_max_size=512M, max_execution_time=360 + coverage: xdebug + tools: php-cs-fixer, phpunit:${{ matrix.phpunit-versions }}, composer:v2 + - name: Setup Github Token as composer credential + run: composer config -g github-oauth.github.com ${{ secrets.GITHUB_TOKEN }} + - name: Install dependencies and local tools + run: | + COMPOSER_ROOT_VERSION=dev-FRAMEWORK_6_0 composer config minimum-stability dev + COMPOSER_ROOT_VERSION=dev-FRAMEWORK_6_0 composer config prefer-stable true + COMPOSER_ROOT_VERSION=dev-FRAMEWORK_6_0 composer install --no-interaction --no-progress + + - name: Run PHPUnit + run: vendor/bin/phpunit --testdox + + - name: Run php-cs-fixer + run: vendor/bin/php-cs-fixer check -vvv + + - name: Run phpstan (mandatory level) + run: vendor/bin/phpstan --no-progress + + - name: Run phpstan (level 9, allowed to fail) + run: vendor/bin/phpstan --no-progress --level=9 + continue-on-error: true + From f3b06f018fcb3e2fee3ad641000df01a2aef512f Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Tue, 6 May 2025 17:31:11 +0200 Subject: [PATCH 2/5] fix: Rename mislabeled unit test and raise phpstan compliance --- test/unit/PhpConfigFileTest.php | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/test/unit/PhpConfigFileTest.php b/test/unit/PhpConfigFileTest.php index e5c0ba8..dd56590 100644 --- a/test/unit/PhpConfigFileTest.php +++ b/test/unit/PhpConfigFileTest.php @@ -13,7 +13,7 @@ */ class PhpConfigFileTest extends TestCase { - public function testReadEmptyConfigFile() + public function testReadEmptyConfigFile(): void { $file = new PhpConfigFile( configFilePath: __DIR__ . '/../fixtures/EmptyConfigFile.php', @@ -23,7 +23,7 @@ public function testReadEmptyConfigFile() $this->assertEquals('', $file->getContent()); } - public function testReadEmptyConfigFileWithComment() + public function testReadEmptyConfigFileWithComment(): void { $file = new PhpConfigFile( configFilePath: __DIR__ . '/../fixtures/EmptyConfigFileWithComment.php', @@ -32,7 +32,7 @@ public function testReadEmptyConfigFileWithComment() $this->assertEquals('// To be done', $file->getContent()); } - public function testReadConfigFileDefaultsAndOverridesWorkAsExpected() + public function testReadConfigFileDefaultsAndOverridesWorkAsExpected(): void { $file = new PhpConfigFile( configFilePath: __DIR__ . '/../fixtures/WithPreHeaderAndPostFooterContent.php', @@ -49,7 +49,7 @@ public function testReadConfigFileDefaultsAndOverridesWorkAsExpected() } - public function testReadConfigFileIgnoringBeforeHeaderAndAfterFooter() + public function testReadConfigFileIgnoringBeforeHeaderAndAfterFooter():void { $file = new PhpConfigFile( configFilePath: __DIR__ . '/../fixtures/WithPreHeaderAndPostFooterContent.php', @@ -67,29 +67,33 @@ public function testReadConfigFileIgnoringBeforeHeaderAndAfterFooter() } - public function testNestedModernArrayFormat() + public function testNestedModernArrayFormat(): void { $file = new PhpConfigFile( configFilePath: __DIR__ . '/../fixtures/NestedModernArrayFormat.php', ); $file->readConfigFile(); $allContent = $file->parseContent(); + // @phpstan-ignore-next-line $this->assertNotEmpty($allContent['config']['key3']['subkey1']); + // @phpstan-ignore-next-line $this->assertEquals('subsubvalue1', $allContent['config']['key3']['subkey2']['subsubkey1']); } - public function testClassicHordeFormat() + public function testClassicHordeFormat(): void { $file = new PhpConfigFile( configFilePath: __DIR__ . '/../fixtures/ClassicHordeFormat.php', ); $file->readConfigFile(); $allContent = $file->parseContent(); + // @phpstan-ignore-next-line $this->assertNotEmpty($allContent['conf']['sql']['hostspec']); + // @phpstan-ignore-next-line $this->assertTrue($allContent['conf']['readwritesplit']); } - public function testWriteEmptyFileWithHeaderAndFooter() + public function testWriteEmptyFileWithHeaderAndFooter(): void { $file = new PhpConfigFile( configFilePath: 'deleteme', @@ -98,7 +102,7 @@ public function testWriteEmptyFileWithHeaderAndFooter() ); $file->writeConfigFile([]); $this->assertFileExists('deleteme'); - $contentString = file_get_contents('deleteme'); + $contentString = (string)file_get_contents('deleteme'); $this->assertStringContainsString('/* Begin */', $contentString, 'Header not found'); $this->assertStringContainsString('/* End */', $contentString, 'Footer not found'); $contentValues = $file->parseContent(); @@ -106,7 +110,7 @@ public function testWriteEmptyFileWithHeaderAndFooter() unlink('deleteme'); } - public function testWriteFailure() + public function testReadFailure(): void { $this->expectException(\RuntimeException::class); $file = new PhpConfigFile( From e533a8bf2ced1973eaea034397c90b4bb3e321ba Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Tue, 6 May 2025 17:31:34 +0200 Subject: [PATCH 3/5] fix: eval is not a function --- src/PhpConfigFile.php | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/PhpConfigFile.php b/src/PhpConfigFile.php index 21d4aa1..1c1c7df 100644 --- a/src/PhpConfigFile.php +++ b/src/PhpConfigFile.php @@ -17,7 +17,6 @@ use function trim; use function ltrim; use function get_defined_vars; -use function eval; use function var_export; use function in_array; @@ -56,15 +55,15 @@ public function getContentBetweenHeaderAndFooter(): string return $this->contentBetweenHeaderAndFooter; } - public function readConfigFile() + public function readConfigFile(): self { // Read the config file and parse it into an array - if (!file_exists($this->configFilePath)) { - throw new \RuntimeException("Config file does not exist: {$this->configFilePath}"); + if (!file_exists((string)$this->configFilePath)) { + throw new RuntimeException("Config file does not exist: {$this->configFilePath}"); } - $configContent = file_get_contents($this->configFilePath); + $configContent = file_get_contents((string)$this->configFilePath); if ($configContent === false) { - throw new \RuntimeException("Failed to read config file: {$this->configFilePath}"); + throw new RuntimeException("Failed to read config file: {$this->configFilePath}"); } // Strip leading and trailing php tags $configContent = trim($configContent); @@ -77,31 +76,37 @@ public function readConfigFile() } $this->content = $configContent; // Get everything before $header - $headerStartPos = strpos($configContent, $this->header); + $headerStartPos = strpos($configContent, (string)$this->header); $headerEndPos = 0; $this->contentBeforeHeader = ''; if ($headerStartPos === false) { $headerStartPos = 0; } else { - $headerEndPos = $headerStartPos + strlen($this->header); + $headerEndPos = $headerStartPos + strlen((string)$this->header); $this->contentBeforeHeader = substr($configContent, 0, $headerStartPos); } // Get everything after $footer - $footerStartPos = strpos($configContent, $this->footer); + $footerStartPos = strpos($configContent, (string)$this->footer); if ($footerStartPos === false) { $this->contentAfterFooter = ''; $footerStartPos = strlen($configContent); $footerEndPos = $footerStartPos; } else { - $footerEndPos = $footerStartPos + strlen($this->footer); + $footerEndPos = $footerStartPos + strlen((string)$this->footer); $this->contentAfterFooter = substr($configContent, $footerEndPos); } $this->contentBetweenHeaderAndFooter = substr($configContent, $headerEndPos, $footerStartPos - $headerEndPos); // Parse the content into an array (assuming it's a PHP array) + return $this; } - + /** + * Parse the content between the header and footer into an array + * @param string $area The area to parse from. Can be 'contentBetweenHeaderAndFooter', 'contentBeforeHeader', 'contentAfterFooter', or 'content'. + * @return array The parsed content as an array + * @throws InvalidArgumentException If the area is invalid + */ public function parseContent(string $area = 'content'): array { // TODO: Ensure to prevent any output from eval @@ -117,7 +122,12 @@ public function parseContent(string $area = 'content'): array throw new \InvalidArgumentException("Invalid area to parse from: $area"); } - public function writeConfigFile(array $config): void + /** + * Write the config file with the given array + * @param array $config The config array to write + * @return self + */ + public function writeConfigFile(array $config): self { // Convert the array back to a string $configContent = "footer . "\n" . $this->contentAfterFooter; // Write the content back to the file - file_put_contents($this->configFilePath, $configContent); + file_put_contents((string)$this->configFilePath, $configContent); + return $this; } } From 68c39b5c9bb2663fa3e188dcf2c94bd8d6282af7 Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Tue, 6 May 2025 17:32:08 +0200 Subject: [PATCH 4/5] fix: Add .phpunit.result.cache to gitignore --- .gitignore | 1 + .php-cs-fixer.dist.php | 4 ++-- .phpunit.result.cache | 1 - phpstan.neon | 3 +++ src/PhpConfigFile.php | 15 ++++++++------- test/unit/PhpConfigFileTest.php | 10 +++------- 6 files changed, 17 insertions(+), 17 deletions(-) delete mode 100644 .phpunit.result.cache diff --git a/.gitignore b/.gitignore index da47a4a..5440b8e 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ tools/ .php-cs-fixer.cache .phpunit.cache/ composer.lock +.phpunit.result.cache \ No newline at end of file diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 030ee56..37a104e 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -1,8 +1,7 @@ exclude(['fixtures']); return (new PhpCsFixer\Config()) ->setRules([ diff --git a/.phpunit.result.cache b/.phpunit.result.cache deleted file mode 100644 index 3390630..0000000 --- a/.phpunit.result.cache +++ /dev/null @@ -1 +0,0 @@ -{"version":1,"defects":{"Horde\\PhpConfigFile\\Test\\Unit\\PhpConfigFileTest::testReadConfigFile":4,"Horde\\PhpConfigFile\\Test\\Unit\\PhpConfigFileTest::testWriteConfigFile":4},"times":{"Horde\\PhpConfigFile\\Test\\Unit\\PhpConfigFileTest::testReadConfigFile":0,"Horde\\PhpConfigFile\\Test\\Unit\\PhpConfigFileTest::testWriteConfigFile":0}} \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon index e3d9c03..625d1de 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,4 +1,7 @@ parameters: + ignoreErrors: + - "#Cannot access offset '.*' on mixed#" + level: 5 errorFormat: github treatPhpDocTypesAsCertain: false diff --git a/src/PhpConfigFile.php b/src/PhpConfigFile.php index 1c1c7df..66ccdc3 100644 --- a/src/PhpConfigFile.php +++ b/src/PhpConfigFile.php @@ -7,6 +7,7 @@ use Stringable; use RuntimeException; use InvalidArgumentException; + use function file_exists; use function file_get_contents; use function file_put_contents; @@ -58,10 +59,10 @@ public function getContentBetweenHeaderAndFooter(): string public function readConfigFile(): self { // Read the config file and parse it into an array - if (!file_exists((string)$this->configFilePath)) { + if (!file_exists((string) $this->configFilePath)) { throw new RuntimeException("Config file does not exist: {$this->configFilePath}"); } - $configContent = file_get_contents((string)$this->configFilePath); + $configContent = file_get_contents((string) $this->configFilePath); if ($configContent === false) { throw new RuntimeException("Failed to read config file: {$this->configFilePath}"); } @@ -76,24 +77,24 @@ public function readConfigFile(): self } $this->content = $configContent; // Get everything before $header - $headerStartPos = strpos($configContent, (string)$this->header); + $headerStartPos = strpos($configContent, (string) $this->header); $headerEndPos = 0; $this->contentBeforeHeader = ''; if ($headerStartPos === false) { $headerStartPos = 0; } else { - $headerEndPos = $headerStartPos + strlen((string)$this->header); + $headerEndPos = $headerStartPos + strlen((string) $this->header); $this->contentBeforeHeader = substr($configContent, 0, $headerStartPos); } // Get everything after $footer - $footerStartPos = strpos($configContent, (string)$this->footer); + $footerStartPos = strpos($configContent, (string) $this->footer); if ($footerStartPos === false) { $this->contentAfterFooter = ''; $footerStartPos = strlen($configContent); $footerEndPos = $footerStartPos; } else { - $footerEndPos = $footerStartPos + strlen((string)$this->footer); + $footerEndPos = $footerStartPos + strlen((string) $this->footer); $this->contentAfterFooter = substr($configContent, $footerEndPos); } $this->contentBetweenHeaderAndFooter = substr($configContent, $headerEndPos, $footerStartPos - $headerEndPos); @@ -145,7 +146,7 @@ public function writeConfigFile(array $config): self $this->footer . "\n" . $this->contentAfterFooter; // Write the content back to the file - file_put_contents((string)$this->configFilePath, $configContent); + file_put_contents((string) $this->configFilePath, $configContent); return $this; } } diff --git a/test/unit/PhpConfigFileTest.php b/test/unit/PhpConfigFileTest.php index dd56590..c4d05d4 100644 --- a/test/unit/PhpConfigFileTest.php +++ b/test/unit/PhpConfigFileTest.php @@ -49,7 +49,7 @@ public function testReadConfigFileDefaultsAndOverridesWorkAsExpected(): void } - public function testReadConfigFileIgnoringBeforeHeaderAndAfterFooter():void + public function testReadConfigFileIgnoringBeforeHeaderAndAfterFooter(): void { $file = new PhpConfigFile( configFilePath: __DIR__ . '/../fixtures/WithPreHeaderAndPostFooterContent.php', @@ -64,7 +64,7 @@ public function testReadConfigFileIgnoringBeforeHeaderAndAfterFooter():void $this->assertEquals('not you the other one', $betweenContent['you'], 'Variable you not found in content'); $this->assertEquals('set', $betweenContent['something'], 'Variable something overwritten by footer in content'); $this->assertArrayNotHasKey('me', $betweenContent, 'Variable me found in content but only exists before header and after footer'); - + } public function testNestedModernArrayFormat(): void @@ -74,9 +74,7 @@ public function testNestedModernArrayFormat(): void ); $file->readConfigFile(); $allContent = $file->parseContent(); - // @phpstan-ignore-next-line $this->assertNotEmpty($allContent['config']['key3']['subkey1']); - // @phpstan-ignore-next-line $this->assertEquals('subsubvalue1', $allContent['config']['key3']['subkey2']['subsubkey1']); } @@ -87,9 +85,7 @@ public function testClassicHordeFormat(): void ); $file->readConfigFile(); $allContent = $file->parseContent(); - // @phpstan-ignore-next-line $this->assertNotEmpty($allContent['conf']['sql']['hostspec']); - // @phpstan-ignore-next-line $this->assertTrue($allContent['conf']['readwritesplit']); } @@ -102,7 +98,7 @@ public function testWriteEmptyFileWithHeaderAndFooter(): void ); $file->writeConfigFile([]); $this->assertFileExists('deleteme'); - $contentString = (string)file_get_contents('deleteme'); + $contentString = (string) file_get_contents('deleteme'); $this->assertStringContainsString('/* Begin */', $contentString, 'Header not found'); $this->assertStringContainsString('/* End */', $contentString, 'Footer not found'); $contentValues = $file->parseContent(); From 9c2e6c9393f7c9f392da8b144ce6b48018eee272 Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Tue, 6 May 2025 17:53:58 +0200 Subject: [PATCH 5/5] PHPStan is really picky about ignoring errors which only happen in high levels --- phpstan.neon | 3 --- 1 file changed, 3 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 625d1de..e3d9c03 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,7 +1,4 @@ parameters: - ignoreErrors: - - "#Cannot access offset '.*' on mixed#" - level: 5 errorFormat: github treatPhpDocTypesAsCertain: false