Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Run PHPUnit Tests

on:
workflow_dispatch:
pull_request:
push:
branches: [ "main" ]

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: "pages"
cancel-in-progress: false

jobs:
tests:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Cache Composer dependencies
uses: actions/cache@v3
with:
path: /tmp/composer-cache
key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }}

- name: Install dependencies
uses: php-actions/composer@v6
env:
COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ github.token }}"} }'
with:
php_version: '8.4'

- name: Run PHPUnit tests
uses: php-actions/phpunit@v3
env:
XDEBUG_MODE: coverage
with:
php_version: '8.4'
php_extensions: pcov

- name: Ensure minimum code coverage
env:
MINIMUM_COVERAGE: 80
run: |
COVERAGE=$(php -r '
$xml = new SimpleXMLElement(file_get_contents("public/coverage/clover.xml"));
$m = $xml->project->metrics;
$pct = (int) round(((int) $m["coveredstatements"]) * 100 / (int) $m["statements"]);
echo $pct;
')
echo "Coverage: ${COVERAGE}%"
if [ "${COVERAGE}" -lt ${{ env.MINIMUM_COVERAGE }} ]; then
echo "Code coverage below ${{ env.MINIMUM_COVERAGE }}% threshold."
exit 1
fi

- name: Upload artifact
if: github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v3
with:
path: public/coverage

deploy:
if: github.ref == 'refs/heads/main'
needs: tests
environment:
name: code-coverage
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
}
},
"scripts": {
"cs-check": "php-cs-fixer fix --dry-run --diff",
"cs-fix": "php-cs-fixer fix",
"cs-check": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix --dry-run --diff",
"cs-fix": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix",
"mutation-testing": "infection --threads=4",
"pre-commit": [
"@cs-check",
Expand Down
87 changes: 87 additions & 0 deletions src/ArrayAccessConfigTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

/**
* This file is part of php-fast-forward/config.
*
* This source file is subject to the license bundled
* with this source code in the file LICENSE.
*
* @link https://github.com/php-fast-forward/config
* @copyright Copyright (c) 2025 Felipe Sayão Lobato Abreu <github@mentordosnerds.com>
* @license https://opensource.org/licenses/MIT MIT License
*/

namespace FastForward\Config;

use Dflydev\DotAccessData\Data;

/**
* Trait ArrayAccessConfigTrait.
*
* This trait provides array-like access to configuration data.
* It MUST be used in classes that implement \ArrayAccess and provide
* the corresponding methods: `get`, `set`, `has`, and `remove`.
*
* @internal
*
* @see \ArrayAccess
*/
trait ArrayAccessConfigTrait
{
/**
* Determines whether the given offset exists in the configuration data.
*
* This method SHALL return true if the offset is present, false otherwise.
*
* @param mixed $offset the offset to check for existence
*
* @return bool true if the offset exists, false otherwise
*/
public function offsetExists(mixed $offset): bool
{
return $this->has($offset);
}

/**
* Retrieves the value associated with the given offset.
*
* This method MUST return the value mapped to the specified offset.
* If the offset does not exist, behavior SHALL depend on the implementation
* of the `get` method.
*
* @param mixed $offset the offset to retrieve
*
* @return mixed the value at the given offset
*/
public function offsetGet(mixed $offset): mixed
{
return $this->get($offset);
}

/**
* Sets the value for the specified offset.
*
* This method SHALL assign the given value to the specified offset.
*
* @param mixed $offset the offset at which to set the value
* @param mixed $value the value to set
*/
public function offsetSet(mixed $offset, mixed $value): void
{
$this->set($offset, $value);
}

/**
* Unsets the specified offset.
*
* This method SHALL remove the specified offset and its associated value.
*
* @param mixed $offset the offset to remove
*/
public function offsetUnset(mixed $offset): void
{
$this->remove($offset);
}
}
16 changes: 16 additions & 0 deletions src/ArrayConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
*/
final class ArrayConfig implements ConfigInterface
{
use ArrayAccessConfigTrait;

/**
* @var Data internal configuration storage instance
*/
Expand Down Expand Up @@ -111,6 +113,20 @@ public function set(array|ConfigInterface|string $key, mixed $value = null): voi
$this->data->import(ConfigHelper::normalize($key));
}

/**
* Removes a configuration key and its associated value.
*
* If the key does not exist, this method SHALL do nothing.
*
* @param string $key the configuration key to remove
*/
public function remove(string $key): void
{
if ($this->has($key)) {
$this->data->remove($key);
}
}

/**
* Retrieves a traversable set of flattened configuration data.
*
Expand Down
19 changes: 19 additions & 0 deletions src/CachedConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,23 @@ public function set(array|ConfigInterface|string $key, mixed $value = null): voi
$this->cache->set($this->cacheKey, $config->toArray());
}
}

/**
* Retrieves a configuration value by key.
*
* This method MUST return the cached value if it exists, or the default value if not found.
*
* @param string $key the configuration key to retrieve
*
* @return mixed the configuration value or the default value
*/
public function remove(mixed $key): void
{
$config = $this->getConfig();
$config->remove($key);

if ($this->persistent) {
$this->cache->set($this->cacheKey, $config->toArray());
}
}
}
12 changes: 11 additions & 1 deletion src/ConfigInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
* Keys MAY use dot notation to access nested structures, e.g., `my.next.key`
* corresponds to ['my' => ['next' => ['key' => $value]]].
*/
interface ConfigInterface extends \IteratorAggregate
interface ConfigInterface extends \IteratorAggregate, \ArrayAccess
{
/**
* Determines if the specified key exists in the configuration.
Expand Down Expand Up @@ -68,6 +68,16 @@ public function get(string $key, mixed $default = null): mixed;
*/
public function set(array|self|string $key, mixed $value = null): void;

/**
* Removes a configuration key and its associated value.
*
* Dot notation MAY be used to specify nested keys.
* If the key does not exist, this method MUST do nothing.
*
* @param string $key the configuration key to remove
*/
public function remove(string $key): void;

/**
* Exports the configuration as a nested associative array.
*
Expand Down
12 changes: 12 additions & 0 deletions src/LazyLoadConfigTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
*/
trait LazyLoadConfigTrait
{
use ArrayAccessConfigTrait;

/**
* @var null|ConfigInterface holds the loaded configuration instance
*/
Expand Down Expand Up @@ -72,6 +74,16 @@ public function set(array|ConfigInterface|string $key, mixed $value = null): voi
$this->getConfig()->set($key, $value);
}

/**
* Removes a configuration key.
*
* @param string $key the configuration key to remove
*/
public function remove(string $key): void
{
$this->getConfig()->remove($key);
}

/**
* Exports the entire configuration to an array.
*
Expand Down
110 changes: 110 additions & 0 deletions tests/ArrayAccessConfigTraitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

declare(strict_types=1);

/**
* This file is part of php-fast-forward/config.
*
* This source file is subject to the license bundled
* with this source code in the file LICENSE.
*
* @link https://github.com/php-fast-forward/config
* @copyright Copyright (c) 2025 Felipe Sayão Lobato Abreu <github@mentordosnerds.com>
* @license https://opensource.org/licenses/MIT MIT License
*/

use FastForward\Config\ArrayAccessConfigTrait;
use PHPUnit\Framework\Attributes\CoversTrait;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;

/**
* @internal
*/
#[CoversTrait(ArrayAccessConfigTrait::class)]
final class ArrayAccessConfigTraitTest extends TestCase
{
use ProphecyTrait;

public function testOffsetExistsWillCallHasMethod(): void
{
$object = $this->createTraitInstance(has: true);
self::assertTrue($object->offsetExists('key'));

$object = $this->createTraitInstance(has: false);
self::assertFalse($object->offsetExists('key'));
}

public function testOffsetGetWillCallGetMethod(): void
{
$object = $this->createTraitInstance(get: 'foo');
self::assertSame('foo', $object->offsetGet('bar'));
}

public function testOffsetSetWillCallSetMethod(): void
{
$object = $this->createTraitInstance();
$object->offsetSet('alpha', 'beta');

self::assertSame(['alpha' => 'beta'], $object->getSetCalls());
}

public function testOffsetUnsetWillCallRemoveMethod(): void
{
$object = $this->createTraitInstance();
$object->offsetUnset('delta');

self::assertSame(['delta'], $object->getRemoveCalls());
}

private function createTraitInstance(bool $has = false, mixed $get = null): ArrayAccess
{
return new class($has, $get) implements ArrayAccess {
use ArrayAccessConfigTrait;

private array $setCalls = [];

private array $removeCalls = [];

private bool $hasReturn;

private mixed $getReturn;

public function __construct(bool $has, mixed $get)
{
$this->hasReturn = $has;
$this->getReturn = $get;
}

public function has(mixed $offset): bool
{
return $this->hasReturn;
}

public function get(mixed $offset): mixed
{
return $this->getReturn;
}

public function set(mixed $offset, mixed $value): void
{
$this->setCalls[$offset] = $value;
}

public function remove(mixed $offset): void
{
$this->removeCalls[] = $offset;
}

public function getSetCalls(): array
{
return $this->setCalls;
}

public function getRemoveCalls(): array
{
return $this->removeCalls;
}
};
}
}
Loading