From f5a939bbd1e08bb9f3007776ff3eb2e86ea65c4d Mon Sep 17 00:00:00 2001 From: Olajide Date: Thu, 11 Dec 2025 19:42:41 +0100 Subject: [PATCH 1/6] setup workflows --- .github/php-ci.yml | 114 ++++++++++++++++++++++ .gitignore | 9 ++ .php-cs-fixer.dist.php | 30 ++++++ composer.json | 38 +++++++- phpcs.xml | 38 ++++++++ phpstan.neon | 32 ++++++ phpunit.xml | 48 +++++++++ src/Libraries/Settings.php | 21 ++-- src/Models/SettingModel.php | 1 - tests/SettingsTest.php | 156 +++++------------------------- tests/Stubs/SettingConfigStub.php | 15 +++ tests/Stubs/SettingModelStub.php | 87 +++++++++++++++++ 12 files changed, 444 insertions(+), 145 deletions(-) create mode 100644 .github/php-ci.yml create mode 100644 .php-cs-fixer.dist.php create mode 100644 phpcs.xml create mode 100644 phpstan.neon create mode 100644 phpunit.xml create mode 100644 tests/Stubs/SettingConfigStub.php create mode 100644 tests/Stubs/SettingModelStub.php diff --git a/.github/php-ci.yml b/.github/php-ci.yml new file mode 100644 index 0000000..3673bf6 --- /dev/null +++ b/.github/php-ci.yml @@ -0,0 +1,114 @@ +name: Github Action for CI4 Settings and Feature Flag Library + +on: + push: + branches: [ "main", "develop" ] + pull_request: + branches: [ "main", "develop" ] + # Manual runs from the Actions tab + workflow_dispatch: + +jobs: + tests: + name: PHP ${{ matrix.php }} + deps: ${{ matrix.deps }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php: [ "8.2", "8.3" ] + deps: [ "lowest", "stable" ] + + env: + # Typical CI4 testing env values + APP_ENV: testing + CI_ENVIRONMENT: testing + + steps: + # --- 1. Checkout code --------------------------------------------------- + - name: Checkout repository + uses: actions/checkout@v4 + + # --- 2. Setup PHP ------------------------------------------------------- + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mbstring, intl, json, pdo_mysql, sqlite3 + ini-values: | + memory_limit=512M + coverage: none + tools: composer + + # --- 3. Cache Composer dependencies ------------------------------------ + - name: Get Composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-php-${{ matrix.php }}-${{ matrix.deps }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php-${{ matrix.php }}-${{ matrix.deps }}-composer- + + # --- 4. Install dependencies ------------------------------------------- + - name: Install Composer dependencies + run: | + if [ "${{ matrix.deps }}" = "lowest" ]; then + composer update --prefer-lowest --no-interaction --no-progress + else + composer install --no-interaction --no-progress --prefer-dist + fi + + # --- 5. Prepare .env for CodeIgniter 4---------------------- + # name it `.env.github-ci` + - name: Prepare .env + run: | + if [ -f .env.github-ci ]; then + cp .env.github-ci .env + elif [ -f env ]; then + cp env .env + fi + + # --- 6. Basic PHP syntax check ----------------------------------------- + - name: PHP syntax check (lint) + run: | + find src tests app -type f -name "*.php" 2>/dev/null \ + | xargs -r -n 1 php -l + + # --- 7. Run unit tests (PHPUnit) --------------------------------------- + - name: Run PHPUnit tests + run: | + if [ -f vendor/bin/phpunit ]; then + vendor/bin/phpunit --testdox + else + echo "PHPUnit not found. Did you require it as a dev dependency?" >&2 + exit 1 + fi + + # --- 8. Static analysis (PHPStan) - optional --------------------------- + - name: Run PHPStan (if configured) + if: ${{ hashFiles('phpstan.neon*') != '' }} + run: | + if [ -f vendor/bin/phpstan ]; then + vendor/bin/phpstan analyse src tests --no-progress + fi + + # --- 9. Coding standards (PHPCS) - optional ---------------------------- + - name: Run PHPCS (if configured) + if: ${{ hashFiles('phpcs.xml*') != '' }} + run: | + if [ -f vendor/bin/phpcs ]; then + vendor/bin/phpcs + fi + + # --- 10. PHP-CS-Fixer (dry-run) - optional ----------------------------- + - name: Run PHP-CS-Fixer (dry-run, if configured) + if: ${{ hashFiles('.php-cs-fixer.dist.php', '.php-cs-fixer.php') != '' }} + run: | + if [ -f vendor/bin/phpcbf ]; then + vendor/bin/phpcbf fix --dry-run --diff + fi diff --git a/.gitignore b/.gitignore index 9f11b75..246f6f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,10 @@ .idea/ +vendor/ +var/ +*.cache +composer.lock +.DS_Store +.vscode/ +phpunit +build/ +phpunit*.xml \ No newline at end of file diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..5860e0e --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,30 @@ +setParallelConfig(ParallelConfigFactory::detect()) // @TODO 4.0 no need to call this manually + ->setRiskyAllowed(false) + ->setRules([ + '@auto' => true + ]) + // 💡 by default, Fixer looks for `*.php` files excluding `./vendor/` - here, you can groom this config + ->setFinder( + (new Finder()) + // 💡 root folder to check + ->in(__DIR__) + // 💡 additional files, eg bin entry file + // ->append([__DIR__.'/bin-entry-file']) + // 💡 folders to exclude, if any + // ->exclude([/* ... */]) + // 💡 path patterns to exclude, if any + // ->notPath([/* ... */]) + // 💡 extra configs + // ->ignoreDotFiles(false) // true by default in v3, false in v4 or future mode + // ->ignoreVCSIgnored(true) // true by default + ) +; diff --git a/composer.json b/composer.json index 71b05a9..b8fdb85 100644 --- a/composer.json +++ b/composer.json @@ -8,8 +8,40 @@ "Jide\\Settings\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Jide\\Settings\\Tests\\": "tests/" + } + }, "require": { - "php": "^8.0", - "codeigniter4/framework": "^4.0" - } + "php": "^8.2" + }, + "require-dev": { + "codeigniter4/framework": "^4.0", + "phpunit/phpunit": "^10.0", + "phpstan/phpstan": "^1.11", + "phpstan/phpstan-phpunit": "^1.4", + "squizlabs/php_codesniffer": "^3.9", + "friendsofphp/php-cs-fixer": "^3.0", + "systemsdk/phpcpd": "^7.0.1" + }, + "scripts": { + "test": "phpunit --testdox", + "analyse": "phpstan analyse src tests", + "deduplicate": "phpcpd src/ tests/", + "cs": "phpcs", + "cs-fix": "phpcbf", + "style": "@cs-fix", + "ci": [ + "@cs", + "@deduplicate", + "@analyse", + "@test" + ] + }, + "config": { + "sort-packages": true + }, + "minimum-stability": "stable", + "prefer-stable": true } diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..34334cf --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,38 @@ + + + + Coding standard for the CI4 Settings / Feature Flag library. + Applies PSR-12 to src/ and tests/ while ignoring vendor and build artifacts. + + + + src + tests + + + vendor/* + var/* + build/* + .phpunit.cache/* + .phpstan/* + + + + + + + + + + + + + + + + + + + + + diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..6f2260b --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,32 @@ +includes: + # Extra rules for PHPUnit test classes (optional but nice) + - vendor/phpstan/phpstan-phpunit/extension.neon + +parameters: + level: 5 + + paths: + - src + - tests + + tmpDir: var/phpstan + + # Ensure Composer autoload is loaded so all classes are known + bootstrapFiles: + - vendor/autoload.php + - vendor/codeigniter4/framework/system/Test/bootstrap.php + + # Some stricter checks that are very useful + checkMissingIterableValueType: true + checkGenericClassInNonGenericObjectType: true + checkMissingVarTagTypehint: true + reportUnmatchedIgnoredErrors: true + + # Option to relax PHPDoc certainty + treatPhpDocTypesAsCertain: false + + # Prevents CodeIgniter dynamic properties/magic, + # from causing PHPStan to scream too much + #ignoreErrors: + #- '#^Call to an undefined method CodeIgniter\\.*#' + #- '#^Access to an undefined property CodeIgniter\\.*#' diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..0553c13 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,48 @@ + + + + + tests + + + + + + src + + + vendor + tests + + + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Settings.php b/src/Libraries/Settings.php index 27febdb..89fa51b 100644 --- a/src/Libraries/Settings.php +++ b/src/Libraries/Settings.php @@ -26,7 +26,7 @@ public function __construct(?SettingsConfig $config = null, ?SettingModel $model /** * Get a setting by key (supports dot notation). */ - public function get(string $key, $default = null): mixed + public function get(string $key, mixed $default = null): mixed { $all = $this->all(); @@ -37,7 +37,6 @@ public function get(string $key, $default = null): mixed * Set a setting (create or update). */ public function set(string $key, mixed $value, ?string $group = null, bool $isFeature = false): bool - { $type = $this->detectType($value); $value = $this->prepareValue($value, $type); @@ -211,7 +210,7 @@ protected function prepareValue($value, string $type): string return match ($type) { 'bool' => $value ? '1' : '0', 'json' => json_encode($value), - default => (string)$value, + default => (string) $value, }; } @@ -300,14 +299,14 @@ protected function arrayHas(array $array, string $path): bool * Set a value into an array using dot path. * If \$mergeArrays is true and both existing and new values are arrays, they will be merged. */ - protected function arraySet(array & $array, string $path, $value, bool $mergeArrays = false): void + protected function arraySet(array &$array, string $path, $value, bool $mergeArrays = false): void { if ($path === '') { return; } // If no dots just set top-level key - if ( ! str_contains($path, '.')) { + if (! str_contains($path, '.')) { if ($mergeArrays && isset($array[$path]) && is_array($array[$path]) && is_array($value)) { $array[$path] = $this->arrayMergeRecursiveDistinct($array[$path], $value); } else { @@ -317,7 +316,7 @@ protected function arraySet(array & $array, string $path, $value, bool $mergeArr } $segments = explode('.', $path); - $current =& $array; + $current = & $array; foreach ($segments as $i => $seg) { if ($i === count($segments) - 1) { @@ -334,17 +333,17 @@ protected function arraySet(array & $array, string $path, $value, bool $mergeArr $current[$seg] = []; } - $current =& $current[$seg]; + $current = & $current[$seg]; } } - protected function arrayUnset(array & $array, string $path): void + protected function arrayUnset(array &$array, string $path): void { if ($path === '') { return; } - if ( ! str_contains($path, '.')) { + if (! str_contains($path, '.')) { if (array_key_exists($path, $array)) { unset($array[$path]); } @@ -353,14 +352,14 @@ protected function arrayUnset(array & $array, string $path): void $segments = explode('.', $path); $stack = []; - $current =& $array; + $current = & $array; foreach ($segments as $seg) { if (!is_array($current) || !array_key_exists($seg, $current)) { return; // nothing to unset } $stack[] = [&$current, $seg]; - $current =& $current[$seg]; + $current = & $current[$seg]; } // unset last diff --git a/src/Models/SettingModel.php b/src/Models/SettingModel.php index a342d88..fd10c6a 100644 --- a/src/Models/SettingModel.php +++ b/src/Models/SettingModel.php @@ -18,5 +18,4 @@ class SettingModel extends Model 'group', 'is_feature', ]; - } diff --git a/tests/SettingsTest.php b/tests/SettingsTest.php index 2e414d4..2672268 100644 --- a/tests/SettingsTest.php +++ b/tests/SettingsTest.php @@ -1,143 +1,41 @@ whereKey = $val; - return $this; - } - - public function select($cols) - { - return $this; - } - - public function first() - { - if ($this->whereKey === null) { - return null; - } - foreach ($this->rows as $row) { - if (($row['key'] ?? null) === $this->whereKey) { - return $row; - } - } - return null; - } - - public function findAll(?int $limit = null, int $offset = 0) - { - return $this->rows; - } - - public function insert($data = null, bool $returnID = true) - { - $id = $this->rows ? (max(array_column($this->rows, 'id')) + 1) : 1; - $data['id'] = $id; - $this->rows[] = $data; - return $id; - } - - public function update($id = null, $data = null): bool - { - foreach ($this->rows as $i => $row) { - if (($row['id'] ?? null) == $id) { - $this->rows[$i] = array_merge($row, $data); - $this->rows[$i]['id'] = $id; - return true; - } - } - return false; - } - - public function delete($id = null, bool $purge = false): bool - { - foreach ($this->rows as $i => $row) { - if (($row['id'] ?? null) == $id) { - array_splice($this->rows, $i, 1); - return true; - } - } - return false; - } - - // Helpers for tests - public function setRows(array $rows): void - { - $this->rows = $rows; - } - - public function getRows(): array - { - return $this->rows; - } -} - -/** - * Minimal config stub that disables cache for predictable tests. - */ -class SettingsConfigStub extends SettingsConfig -{ - public function __construct() - { - $this->useCache = false; - $this->cacheKey = 'settings_test'; - $this->cacheTTL = 60; - } -} - -/** - * PHPUnit tests for Jide\Settings\Libraries\Settings - * File location: app/ThirdParty/ci4-settings/test/SettingsTest.php - */ final class SettingsTest extends TestCase { public function testSetAndGetScalar() { $model = new SettingModelStub(); - $config = new SettingsConfigStub(); - + $config = new SettingConfigStub(); + $settings = new Settings($config, $model); - + $ok = $settings->set('app.name', 'My App'); $this->assertTrue($ok, 'set should return true'); - + $value = $settings->get('app.name'); $this->assertSame('My App', $value); } - + //test setting and getting a nested JSON key public function testSetAndGetNestedJsonKey() { $model = new SettingModelStub(); - $config = new SettingsConfigStub(); - + $config = new SettingConfigStub(); + $settings = new Settings($config, $model); $ok = $settings->set('app.mail.host', 'smtp.example'); $this->assertTrue($ok, 'set should return true for nested key'); - + $value = $settings->get('app.mail.host'); $this->assertSame('smtp.example', $value); - + $ok = $settings->set('mail', [ 'host' => 'smtp.example.com', 'port' => 587, @@ -145,20 +43,18 @@ public function testSetAndGetNestedJsonKey() 'username' => 'user@example.com', 'password' => 'secret', ]); - + $this->assertTrue($ok, 'set should return true for nested key'); - + $value = $settings->get('mail.host'); $this->assertSame('smtp.example.com', $value); - - } - + public function testDeleteNestedJsonKeyUpdatesParent() { $model = new SettingModelStub(); - $config = new SettingsConfigStub(); - + $config = new SettingConfigStub(); + // seed a parent JSON row $model->setRows([ [ @@ -168,28 +64,28 @@ public function testDeleteNestedJsonKeyUpdatesParent() 'type' => 'json', ], ]); - + $settings = new Settings($config, $model); - + // delete nested key $deleted = $settings->delete('app.mail.host'); $this->assertTrue($deleted, 'delete should return true for nested removal'); - + $rows = $model->getRows(); $this->assertCount(1, $rows, 'parent row should remain (with updated JSON)'); $parent = $rows[0]; - + $decoded = json_decode($parent['value'], true); $this->assertArrayNotHasKey('host', $decoded); $this->assertSame('noreply', $decoded['user']); $this->assertSame('json', $parent['type']); } - + public function testFeatureEnabledReadsBool() { $model = new SettingModelStub(); - $config = new SettingsConfigStub(); - + $config = new SettingConfigStub(); + $model->setRows([ [ 'id' => 1, @@ -198,9 +94,9 @@ public function testFeatureEnabledReadsBool() 'type' => 'bool', ], ]); - + $settings = new Settings($config, $model); - + $this->assertTrue($settings->featureEnabled('coolFeature')); $this->assertTrue($settings->featureEnabled('feature.coolFeature')); $this->assertFalse($settings->featureEnabled('nonexistent', false)); diff --git a/tests/Stubs/SettingConfigStub.php b/tests/Stubs/SettingConfigStub.php new file mode 100644 index 0000000..f081de2 --- /dev/null +++ b/tests/Stubs/SettingConfigStub.php @@ -0,0 +1,15 @@ +useCache = false; + $this->cacheKey = 'settings_test'; + $this->cacheTTL = 60; + } +} diff --git a/tests/Stubs/SettingModelStub.php b/tests/Stubs/SettingModelStub.php new file mode 100644 index 0000000..60b3eb7 --- /dev/null +++ b/tests/Stubs/SettingModelStub.php @@ -0,0 +1,87 @@ +whereKey = $val; + return $this; + } + + public function select($cols) + { + return $this; + } + + public function first() + { + if ($this->whereKey === null) { + return null; + } + foreach ($this->rows as $row) { + if (($row['key'] ?? null) === $this->whereKey) { + return $row; + } + } + return null; + } + + public function findAll(?int $limit = null, int $offset = 0) + { + return $this->rows; + } + + public function insert($data = null, bool $returnID = true) + { + $id = $this->rows ? (max(array_column($this->rows, 'id')) + 1) : 1; + $data['id'] = $id; + $this->rows[] = $data; + return $id; + } + + public function update($id = null, $data = null): bool + { + foreach ($this->rows as $i => $row) { + if (($row['id'] ?? null) == $id) { + $this->rows[$i] = array_merge($row, $data); + $this->rows[$i]['id'] = $id; + return true; + } + } + return false; + } + + public function delete($id = null, bool $purge = false): bool + { + foreach ($this->rows as $i => $row) { + if (($row['id'] ?? null) == $id) { + array_splice($this->rows, $i, 1); + return true; + } + } + return false; + } + + // Helpers for tests + public function setRows(array $rows): void + { + $this->rows = $rows; + } + + public function getRows(): array + { + return $this->rows; + } +} From b19cd7bafde73bc47723ef29c544da66362b894b Mon Sep 17 00:00:00 2001 From: Olajide Date: Thu, 11 Dec 2025 20:12:48 +0100 Subject: [PATCH 2/6] fix issues with workflows --- .github/{php-ci.yml => workflows/action.yml} | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) rename .github/{php-ci.yml => workflows/action.yml} (94%) diff --git a/.github/php-ci.yml b/.github/workflows/action.yml similarity index 94% rename from .github/php-ci.yml rename to .github/workflows/action.yml index 3673bf6..ee8a254 100644 --- a/.github/php-ci.yml +++ b/.github/workflows/action.yml @@ -10,8 +10,7 @@ on: jobs: tests: - name: PHP ${{ matrix.php }} - deps: ${{ matrix.deps }} + name: "PHP ${{ matrix.php }} (deps: ${{ matrix.deps }})" runs-on: ubuntu-latest strategy: @@ -105,8 +104,8 @@ jobs: vendor/bin/phpcs fi - # --- 10. PHP-CS-Fixer (dry-run) - optional ----------------------------- - - name: Run PHP-CS-Fixer (dry-run, if configured) + # --- 10. PHP-CBF (dry-run) - optional ----------------------------- + - name: Run PHP-CBF (dry-run, if configured) if: ${{ hashFiles('.php-cs-fixer.dist.php', '.php-cs-fixer.php') != '' }} run: | if [ -f vendor/bin/phpcbf ]; then From fe108ab694f442aa3125083e7d785c9ee807c61a Mon Sep 17 00:00:00 2001 From: Olajide Date: Thu, 11 Dec 2025 20:28:13 +0100 Subject: [PATCH 3/6] fix --- phpunit.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml b/phpunit.xml index 0553c13..f8ff167 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -8,7 +8,7 @@ executionOrder="depends,defects" beStrictAboutChangesToGlobalState="true" failOnRisky="true" - failOnWarning="true" + failOnWarning="false" > From c7302faf0727e6411544574abcfdc5bd8e7c8228 Mon Sep 17 00:00:00 2001 From: Olajide Date: Thu, 11 Dec 2025 20:36:45 +0100 Subject: [PATCH 4/6] fix workflow issues --- .github/workflows/action.yml | 4 ++-- phpunit.xml | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index ee8a254..2f58e64 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -16,8 +16,8 @@ jobs: strategy: fail-fast: false matrix: - php: [ "8.2", "8.3" ] - deps: [ "lowest", "stable" ] + php: [ "8.2" ] + deps: [ "stable" ] env: # Typical CI4 testing env values diff --git a/phpunit.xml b/phpunit.xml index f8ff167..76b432c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,7 +7,7 @@ colors="true" executionOrder="depends,defects" beStrictAboutChangesToGlobalState="true" - failOnRisky="true" + displayDetailsOnTestsThatTriggerWarnings="true" failOnWarning="false" > @@ -28,9 +28,7 @@ - - - + From f23273c4b4e230feab30bc9dd4ca09fe6ff244bb Mon Sep 17 00:00:00 2001 From: Olajide Date: Thu, 11 Dec 2025 20:46:51 +0100 Subject: [PATCH 5/6] fix for workflow --- .github/workflows/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index 2f58e64..1f6d488 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -109,5 +109,5 @@ jobs: if: ${{ hashFiles('.php-cs-fixer.dist.php', '.php-cs-fixer.php') != '' }} run: | if [ -f vendor/bin/phpcbf ]; then - vendor/bin/phpcbf fix --dry-run --diff + vendor/bin/phpcbf fi From fe856fd9043061586e61f280a6ddeb13714746da Mon Sep 17 00:00:00 2001 From: Olajide Date: Thu, 11 Dec 2025 20:49:01 +0100 Subject: [PATCH 6/6] fix for workflow --- .github/workflows/action.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index 1f6d488..f092f6f 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -1,8 +1,6 @@ name: Github Action for CI4 Settings and Feature Flag Library on: - push: - branches: [ "main", "develop" ] pull_request: branches: [ "main", "develop" ] # Manual runs from the Actions tab