Skip to content

Commit 27408cb

Browse files
authored
Merge pull request #1 from php-fast-forward/feature/array-access
- Add ArrayAccess interface to ConfigInterface; - Add GitHub Action to code coverage
2 parents eccc7bb + d2efe6c commit 27408cb

13 files changed

+469
-13
lines changed

.github/workflows/tests.yml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: Run PHPUnit Tests
2+
3+
on:
4+
workflow_dispatch:
5+
pull_request:
6+
push:
7+
branches: [ "main" ]
8+
9+
permissions:
10+
contents: read
11+
pages: write
12+
id-token: write
13+
14+
concurrency:
15+
group: "pages"
16+
cancel-in-progress: false
17+
18+
jobs:
19+
tests:
20+
runs-on: ubuntu-latest
21+
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- name: Cache Composer dependencies
26+
uses: actions/cache@v3
27+
with:
28+
path: /tmp/composer-cache
29+
key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
30+
31+
- name: Install dependencies
32+
uses: php-actions/composer@v6
33+
env:
34+
COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ github.token }}"} }'
35+
with:
36+
php_version: '8.4'
37+
38+
- name: Run PHPUnit tests
39+
uses: php-actions/phpunit@v3
40+
env:
41+
XDEBUG_MODE: coverage
42+
with:
43+
php_version: '8.4'
44+
php_extensions: pcov
45+
46+
- name: Ensure minimum code coverage
47+
env:
48+
MINIMUM_COVERAGE: 80
49+
run: |
50+
COVERAGE=$(php -r '
51+
$xml = new SimpleXMLElement(file_get_contents("public/coverage/clover.xml"));
52+
$m = $xml->project->metrics;
53+
$pct = (int) round(((int) $m["coveredstatements"]) * 100 / (int) $m["statements"]);
54+
echo $pct;
55+
')
56+
echo "Coverage: ${COVERAGE}%"
57+
if [ "${COVERAGE}" -lt ${{ env.MINIMUM_COVERAGE }} ]; then
58+
echo "Code coverage below ${{ env.MINIMUM_COVERAGE }}% threshold."
59+
exit 1
60+
fi
61+
62+
- name: Upload artifact
63+
if: github.ref == 'refs/heads/main'
64+
uses: actions/upload-pages-artifact@v3
65+
with:
66+
path: public/coverage
67+
68+
deploy:
69+
if: github.ref == 'refs/heads/main'
70+
needs: tests
71+
environment:
72+
name: code-coverage
73+
url: ${{ steps.deployment.outputs.page_url }}
74+
runs-on: ubuntu-latest
75+
steps:
76+
- name: Deploy to GitHub Pages
77+
id: deployment
78+
uses: actions/deploy-pages@v4

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@
4949
}
5050
},
5151
"scripts": {
52-
"cs-check": "php-cs-fixer fix --dry-run --diff",
53-
"cs-fix": "php-cs-fixer fix",
52+
"cs-check": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix --dry-run --diff",
53+
"cs-fix": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix",
5454
"mutation-testing": "infection --threads=4",
5555
"pre-commit": [
5656
"@cs-check",

src/ArrayAccessConfigTrait.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of php-fast-forward/config.
7+
*
8+
* This source file is subject to the license bundled
9+
* with this source code in the file LICENSE.
10+
*
11+
* @link https://github.com/php-fast-forward/config
12+
* @copyright Copyright (c) 2025 Felipe Sayão Lobato Abreu <[email protected]>
13+
* @license https://opensource.org/licenses/MIT MIT License
14+
*/
15+
16+
namespace FastForward\Config;
17+
18+
use Dflydev\DotAccessData\Data;
19+
20+
/**
21+
* Trait ArrayAccessConfigTrait.
22+
*
23+
* This trait provides array-like access to configuration data.
24+
* It MUST be used in classes that implement \ArrayAccess and provide
25+
* the corresponding methods: `get`, `set`, `has`, and `remove`.
26+
*
27+
* @internal
28+
*
29+
* @see \ArrayAccess
30+
*/
31+
trait ArrayAccessConfigTrait
32+
{
33+
/**
34+
* Determines whether the given offset exists in the configuration data.
35+
*
36+
* This method SHALL return true if the offset is present, false otherwise.
37+
*
38+
* @param mixed $offset the offset to check for existence
39+
*
40+
* @return bool true if the offset exists, false otherwise
41+
*/
42+
public function offsetExists(mixed $offset): bool
43+
{
44+
return $this->has($offset);
45+
}
46+
47+
/**
48+
* Retrieves the value associated with the given offset.
49+
*
50+
* This method MUST return the value mapped to the specified offset.
51+
* If the offset does not exist, behavior SHALL depend on the implementation
52+
* of the `get` method.
53+
*
54+
* @param mixed $offset the offset to retrieve
55+
*
56+
* @return mixed the value at the given offset
57+
*/
58+
public function offsetGet(mixed $offset): mixed
59+
{
60+
return $this->get($offset);
61+
}
62+
63+
/**
64+
* Sets the value for the specified offset.
65+
*
66+
* This method SHALL assign the given value to the specified offset.
67+
*
68+
* @param mixed $offset the offset at which to set the value
69+
* @param mixed $value the value to set
70+
*/
71+
public function offsetSet(mixed $offset, mixed $value): void
72+
{
73+
$this->set($offset, $value);
74+
}
75+
76+
/**
77+
* Unsets the specified offset.
78+
*
79+
* This method SHALL remove the specified offset and its associated value.
80+
*
81+
* @param mixed $offset the offset to remove
82+
*/
83+
public function offsetUnset(mixed $offset): void
84+
{
85+
$this->remove($offset);
86+
}
87+
}

src/ArrayConfig.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
*/
3131
final class ArrayConfig implements ConfigInterface
3232
{
33+
use ArrayAccessConfigTrait;
34+
3335
/**
3436
* @var Data internal configuration storage instance
3537
*/
@@ -111,6 +113,20 @@ public function set(array|ConfigInterface|string $key, mixed $value = null): voi
111113
$this->data->import(ConfigHelper::normalize($key));
112114
}
113115

116+
/**
117+
* Removes a configuration key and its associated value.
118+
*
119+
* If the key does not exist, this method SHALL do nothing.
120+
*
121+
* @param string $key the configuration key to remove
122+
*/
123+
public function remove(string $key): void
124+
{
125+
if ($this->has($key)) {
126+
$this->data->remove($key);
127+
}
128+
}
129+
114130
/**
115131
* Retrieves a traversable set of flattened configuration data.
116132
*

src/CachedConfig.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,23 @@ public function set(array|ConfigInterface|string $key, mixed $value = null): voi
8787
$this->cache->set($this->cacheKey, $config->toArray());
8888
}
8989
}
90+
91+
/**
92+
* Retrieves a configuration value by key.
93+
*
94+
* This method MUST return the cached value if it exists, or the default value if not found.
95+
*
96+
* @param string $key the configuration key to retrieve
97+
*
98+
* @return mixed the configuration value or the default value
99+
*/
100+
public function remove(mixed $key): void
101+
{
102+
$config = $this->getConfig();
103+
$config->remove($key);
104+
105+
if ($this->persistent) {
106+
$this->cache->set($this->cacheKey, $config->toArray());
107+
}
108+
}
90109
}

src/ConfigInterface.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
* Keys MAY use dot notation to access nested structures, e.g., `my.next.key`
2828
* corresponds to ['my' => ['next' => ['key' => $value]]].
2929
*/
30-
interface ConfigInterface extends \IteratorAggregate
30+
interface ConfigInterface extends \IteratorAggregate, \ArrayAccess
3131
{
3232
/**
3333
* Determines if the specified key exists in the configuration.
@@ -68,6 +68,16 @@ public function get(string $key, mixed $default = null): mixed;
6868
*/
6969
public function set(array|self|string $key, mixed $value = null): void;
7070

71+
/**
72+
* Removes a configuration key and its associated value.
73+
*
74+
* Dot notation MAY be used to specify nested keys.
75+
* If the key does not exist, this method MUST do nothing.
76+
*
77+
* @param string $key the configuration key to remove
78+
*/
79+
public function remove(string $key): void;
80+
7181
/**
7282
* Exports the configuration as a nested associative array.
7383
*

src/LazyLoadConfigTrait.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
*/
2525
trait LazyLoadConfigTrait
2626
{
27+
use ArrayAccessConfigTrait;
28+
2729
/**
2830
* @var null|ConfigInterface holds the loaded configuration instance
2931
*/
@@ -72,6 +74,16 @@ public function set(array|ConfigInterface|string $key, mixed $value = null): voi
7274
$this->getConfig()->set($key, $value);
7375
}
7476

77+
/**
78+
* Removes a configuration key.
79+
*
80+
* @param string $key the configuration key to remove
81+
*/
82+
public function remove(string $key): void
83+
{
84+
$this->getConfig()->remove($key);
85+
}
86+
7587
/**
7688
* Exports the entire configuration to an array.
7789
*
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of php-fast-forward/config.
7+
*
8+
* This source file is subject to the license bundled
9+
* with this source code in the file LICENSE.
10+
*
11+
* @link https://github.com/php-fast-forward/config
12+
* @copyright Copyright (c) 2025 Felipe Sayão Lobato Abreu <[email protected]>
13+
* @license https://opensource.org/licenses/MIT MIT License
14+
*/
15+
16+
use FastForward\Config\ArrayAccessConfigTrait;
17+
use PHPUnit\Framework\Attributes\CoversTrait;
18+
use PHPUnit\Framework\TestCase;
19+
use Prophecy\PhpUnit\ProphecyTrait;
20+
21+
/**
22+
* @internal
23+
*/
24+
#[CoversTrait(ArrayAccessConfigTrait::class)]
25+
final class ArrayAccessConfigTraitTest extends TestCase
26+
{
27+
use ProphecyTrait;
28+
29+
public function testOffsetExistsWillCallHasMethod(): void
30+
{
31+
$object = $this->createTraitInstance(has: true);
32+
self::assertTrue($object->offsetExists('key'));
33+
34+
$object = $this->createTraitInstance(has: false);
35+
self::assertFalse($object->offsetExists('key'));
36+
}
37+
38+
public function testOffsetGetWillCallGetMethod(): void
39+
{
40+
$object = $this->createTraitInstance(get: 'foo');
41+
self::assertSame('foo', $object->offsetGet('bar'));
42+
}
43+
44+
public function testOffsetSetWillCallSetMethod(): void
45+
{
46+
$object = $this->createTraitInstance();
47+
$object->offsetSet('alpha', 'beta');
48+
49+
self::assertSame(['alpha' => 'beta'], $object->getSetCalls());
50+
}
51+
52+
public function testOffsetUnsetWillCallRemoveMethod(): void
53+
{
54+
$object = $this->createTraitInstance();
55+
$object->offsetUnset('delta');
56+
57+
self::assertSame(['delta'], $object->getRemoveCalls());
58+
}
59+
60+
private function createTraitInstance(bool $has = false, mixed $get = null): ArrayAccess
61+
{
62+
return new class($has, $get) implements ArrayAccess {
63+
use ArrayAccessConfigTrait;
64+
65+
private array $setCalls = [];
66+
67+
private array $removeCalls = [];
68+
69+
private bool $hasReturn;
70+
71+
private mixed $getReturn;
72+
73+
public function __construct(bool $has, mixed $get)
74+
{
75+
$this->hasReturn = $has;
76+
$this->getReturn = $get;
77+
}
78+
79+
public function has(mixed $offset): bool
80+
{
81+
return $this->hasReturn;
82+
}
83+
84+
public function get(mixed $offset): mixed
85+
{
86+
return $this->getReturn;
87+
}
88+
89+
public function set(mixed $offset, mixed $value): void
90+
{
91+
$this->setCalls[$offset] = $value;
92+
}
93+
94+
public function remove(mixed $offset): void
95+
{
96+
$this->removeCalls[] = $offset;
97+
}
98+
99+
public function getSetCalls(): array
100+
{
101+
return $this->setCalls;
102+
}
103+
104+
public function getRemoveCalls(): array
105+
{
106+
return $this->removeCalls;
107+
}
108+
};
109+
}
110+
}

0 commit comments

Comments
 (0)