Skip to content

Commit d54a476

Browse files
committed
Add PSR-11 Container support
1 parent ab717b6 commit d54a476

File tree

5 files changed

+288
-0
lines changed

5 files changed

+288
-0
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"php": "^8.1",
1414
"dflydev/dot-access-data": "^3.0",
1515
"laminas/laminas-config-aggregator": "^1.13",
16+
"psr/container": "^1.0||^2.0",
1617
"psr/simple-cache": "^3.0"
1718
},
1819
"require-dev": {

src/Container/ConfigContainer.php

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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 <github@mentordosnerds.com>
13+
* @license https://opensource.org/licenses/MIT MIT License
14+
*/
15+
16+
namespace FastForward\Config\Container;
17+
18+
use FastForward\Config\ConfigInterface;
19+
use FastForward\Config\Exception\ContainerNotFoundExceptionInterface;
20+
use Psr\Container\ContainerInterface;
21+
22+
/**
23+
* Class ConfigContainer.
24+
*
25+
* Provides a PSR-11 compatible container interface for accessing configuration values.
26+
* This container MAY be used in dependency injection systems where configuration keys
27+
* should be resolvable via standard container access.
28+
*/
29+
final class ConfigContainer implements ContainerInterface
30+
{
31+
/**
32+
* @const string The standard identifier for retrieving the configuration object.
33+
*/
34+
public const ALIAS = 'config';
35+
36+
/**
37+
* Constructs a new ConfigContainer instance.
38+
*
39+
* This constructor SHALL wrap an existing ConfigInterface instance and expose it
40+
* through PSR-11 `get()` and `has()` methods.
41+
*
42+
* @param ConfigInterface $config the configuration instance to expose as a container
43+
*/
44+
public function __construct(
45+
private ConfigInterface $config,
46+
) {}
47+
48+
/**
49+
* Determines if the container can return an entry for the given identifier.
50+
*
51+
* @param string $id identifier of the entry to look for
52+
*
53+
* @return bool TRUE if the entry is known or exists in the configuration, FALSE otherwise
54+
*/
55+
public function has(string $id): bool
56+
{
57+
return $this->isResolvedByContainer($id)
58+
|| $this->config->has($id);
59+
}
60+
61+
/**
62+
* Retrieves an entry of the container by its identifier.
63+
*
64+
* If the ID matches the container itself or the config class, it SHALL return itself.
65+
* Otherwise, it SHALL retrieve the value from the configuration.
66+
* If the key is not found, it MUST throw a ContainerNotFoundExceptionInterface.
67+
*
68+
* @param string $id identifier of the entry to retrieve
69+
*
70+
* @return mixed the value associated with the identifier
71+
*
72+
* @throws ContainerNotFoundExceptionInterface if the identifier is not found
73+
*/
74+
public function get(string $id)
75+
{
76+
if ($this->isResolvedByContainer($id)) {
77+
return $this;
78+
}
79+
80+
if ($this->config->has($id)) {
81+
return $this->config->get($id);
82+
}
83+
84+
throw ContainerNotFoundExceptionInterface::forKey($id);
85+
}
86+
87+
/**
88+
* Determines whether the given identifier is resolved internally by the container itself.
89+
*
90+
* This method SHALL be used to check if the requested identifier corresponds to:
91+
* - the container alias (e.g., 'config'),
92+
* - the Config interface (`FastForward\Config\ConfigInterface`),
93+
* - or the concrete configuration key.
94+
*
95+
* If any of these conditions match, the container MUST resolve the request by returning itself.
96+
*
97+
* @param string $id the identifier being checked for internal resolution
98+
*
99+
* @return bool TRUE if the container SHALL resolve the identifier itself; FALSE otherwise
100+
*/
101+
private function isResolvedByContainer(string $id): bool
102+
{
103+
return \in_array($id, [self::ALIAS, ConfigInterface::class, $this->config::class], true);
104+
}
105+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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 <github@mentordosnerds.com>
13+
* @license https://opensource.org/licenses/MIT MIT License
14+
*/
15+
16+
namespace FastForward\Config\Exception;
17+
18+
use Psr\Container\NotFoundExceptionInterface;
19+
20+
/**
21+
* Class ContainerNotFoundExceptionInterface.
22+
*
23+
* Exception thrown when a configuration key is not found in the container.
24+
* This class MUST implement the PSR-11 NotFoundExceptionInterface.
25+
*/
26+
final class ContainerNotFoundExceptionInterface extends \Exception implements NotFoundExceptionInterface
27+
{
28+
/**
29+
* Creates a new exception instance for a missing configuration key.
30+
*
31+
* This factory method SHOULD be used when a key lookup fails in the ConfigContainer.
32+
*
33+
* @param string $key the key that was not found
34+
*
35+
* @return self a new instance of the exception describing the missing key
36+
*/
37+
public static function forKey(string $key): self
38+
{
39+
return new self(\sprintf('Config key "%s" not found.', $key));
40+
}
41+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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 <github@mentordosnerds.com>
13+
* @license https://opensource.org/licenses/MIT MIT License
14+
*/
15+
16+
namespace FastForward\Config\Tests\Container;
17+
18+
use FastForward\Config\ArrayConfig;
19+
use FastForward\Config\ConfigInterface;
20+
use FastForward\Config\Container\ConfigContainer;
21+
use FastForward\Config\Exception\ContainerNotFoundExceptionInterface;
22+
use PHPUnit\Framework\Attributes\CoversClass;
23+
use PHPUnit\Framework\Attributes\Test;
24+
use PHPUnit\Framework\Attributes\UsesClass;
25+
use PHPUnit\Framework\TestCase;
26+
27+
/**
28+
* @internal
29+
*/
30+
#[CoversClass(ConfigContainer::class)]
31+
#[UsesClass(ArrayConfig::class)]
32+
#[UsesClass(ContainerNotFoundExceptionInterface::class)]
33+
final class ConfigContainerTest extends TestCase
34+
{
35+
#[Test]
36+
public function testHasReturnsTrueForKnownInternalIdentifiers(): void
37+
{
38+
$config = new ArrayConfig();
39+
$container = new ConfigContainer($config);
40+
41+
self::assertTrue($container->has(ConfigContainer::ALIAS));
42+
self::assertTrue($container->has(ConfigInterface::class));
43+
self::assertTrue($container->has($config::class));
44+
}
45+
46+
#[Test]
47+
public function testHasReturnsTrueIfConfigContainsKey(): void
48+
{
49+
$key = uniqid('key_', true);
50+
$value = uniqid('val_', true);
51+
52+
$config = new ArrayConfig([$key => $value]);
53+
$container = new ConfigContainer($config);
54+
55+
self::assertTrue($container->has($key));
56+
}
57+
58+
#[Test]
59+
public function testHasReturnsFalseForUnknownKey(): void
60+
{
61+
$config = new ArrayConfig();
62+
$container = new ConfigContainer($config);
63+
64+
self::assertFalse($container->has(uniqid('missing_', true)));
65+
}
66+
67+
#[Test]
68+
public function testGetReturnsContainerForInternalIdentifiers(): void
69+
{
70+
$config = new ArrayConfig();
71+
$container = new ConfigContainer($config);
72+
73+
self::assertSame($container, $container->get(ConfigContainer::ALIAS));
74+
self::assertSame($container, $container->get(ConfigInterface::class));
75+
self::assertSame($container, $container->get($config::class));
76+
}
77+
78+
#[Test]
79+
public function testGetReturnsConfigValueForKey(): void
80+
{
81+
$key = uniqid('env_', true);
82+
$value = uniqid('value_', true);
83+
84+
$config = new ArrayConfig([$key => $value]);
85+
$container = new ConfigContainer($config);
86+
87+
self::assertSame($value, $container->get($key));
88+
}
89+
90+
#[Test]
91+
public function testGetThrowsExceptionForUnknownKey(): void
92+
{
93+
$this->expectException(ContainerNotFoundExceptionInterface::class);
94+
95+
$config = new ArrayConfig();
96+
$container = new ConfigContainer($config);
97+
98+
$container->get(uniqid('unknown_', true));
99+
}
100+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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 <github@mentordosnerds.com>
13+
* @license https://opensource.org/licenses/MIT MIT License
14+
*/
15+
16+
namespace FastForward\Config\Tests\Exception;
17+
18+
use FastForward\Config\Exception\ContainerNotFoundExceptionInterface;
19+
use PHPUnit\Framework\Attributes\CoversClass;
20+
use PHPUnit\Framework\Attributes\Test;
21+
use PHPUnit\Framework\TestCase;
22+
use Psr\Container\NotFoundExceptionInterface;
23+
24+
/**
25+
* @internal
26+
*/
27+
#[CoversClass(ContainerNotFoundExceptionInterface::class)]
28+
final class ContainerNotFoundExceptionInterfaceTest extends TestCase
29+
{
30+
#[Test]
31+
public function testForKeyReturnsExpectedMessage(): void
32+
{
33+
$key = uniqid('missing_', true);
34+
35+
$exception = ContainerNotFoundExceptionInterface::forKey($key);
36+
37+
self::assertInstanceOf(ContainerNotFoundExceptionInterface::class, $exception);
38+
self::assertInstanceOf(NotFoundExceptionInterface::class, $exception);
39+
self::assertSame(\sprintf('Config key "%s" not found.', $key), $exception->getMessage());
40+
}
41+
}

0 commit comments

Comments
 (0)