Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
ConfigContainer
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
4 / 4
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 has
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 get
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 isResolvedByContainer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(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 * @copyright Copyright (c) 2025-2026 Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
12 * @license   https://opensource.org/licenses/MIT MIT License
13 *
14 * @see       https://github.com/php-fast-forward/config
15 * @see       https://github.com/php-fast-forward
16 * @see       https://datatracker.ietf.org/doc/html/rfc2119
17 */
18
19namespace FastForward\Config\Container;
20
21use FastForward\Config\ConfigInterface;
22use FastForward\Config\Exception\ContainerNotFoundException;
23use Psr\Container\ContainerInterface;
24
25/**
26 * Class ConfigContainer.
27 *
28 * Provides a PSR-11 compatible container interface for accessing configuration values.
29 *
30 * This container implementation SHALL resolve configuration keys by prefix using the alias "config".
31 * For example, a request for "config.db.host" will attempt to fetch the key "db.host" from the underlying ConfigInterface.
32 *
33 * Identifiers such as 'config', ConfigInterface::class, and the concrete config class MUST return the configuration instance itself.
34 * Requests for unknown or invalid identifiers MUST result in a ContainerNotFoundException.
35 */
36  class ConfigContainer implements ContainerInterface
37{
38    /**
39     * @var string the standard identifier for retrieving the configuration object
40     */
41    public const string ALIAS = 'config';
42
43    /**
44     * Constructs a new ConfigContainer instance.
45     *
46     * This constructor SHALL wrap an existing ConfigInterface instance and expose it
47     * through PSR-11 `get()` and `has()` methods with namespace-style key resolution.
48     *
49     * @param ConfigInterface $config the configuration instance to expose as a container
50     */
51    public function __construct(
52        private ConfigInterface $config,
53    ) {}
54
55    /**
56     * Determines whether the container can return an entry for the given identifier.
57     *
58     * This method SHALL return true if:
59     * - The identifier matches known internal bindings (alias, interface, or class).
60     * - The identifier is prefixed with 'config' and corresponds to an existing key in the configuration.
61     *
62     * @param string $id identifier of the entry to look for
63     *
64     * @return bool true if the entry can be resolved; false otherwise
65     */
66    public function has(string $id): bool
67    {
68        if ($this->isResolvedByContainer($id)) {
69            return true;
70        }
71
72        if (! str_starts_with($id, self::ALIAS)) {
73            return false;
74        }
75
76        return $this->config->has(mb_substr($id, mb_strlen(self::ALIAS) + 1));
77    }
78
79    /**
80     * Retrieves an entry of the container by its identifier.
81     *
82     * This method SHALL resolve identifiers in the following order:
83     * - If the identifier matches 'config', ConfigInterface::class, or the concrete config class,
84     *   it SHALL return the ConfigInterface instance itself.
85     * - If the identifier is prefixed with 'config.', the suffix SHALL be used to query the configuration.
86     *   If the configuration key exists, its value SHALL be returned.
87     * - If the identifier cannot be resolved, a ContainerNotFoundException MUST be thrown.
88     *
89     * @param string $id identifier of the entry to retrieve
90     *
91     * @return mixed the value associated with the identifier
92     *
93     * @throws ContainerNotFoundException if the identifier cannot be resolved
94     */
95    public function get(string $id)
96    {
97        if (self::class === $id) {
98            return $this;
99        }
100
101        if ($this->isResolvedByContainer($id)) {
102            return $this->config;
103        }
104
105        if (str_starts_with($id, self::ALIAS)) {
106            $id = mb_substr($id, mb_strlen(self::ALIAS) + 1);
107
108            if ($this->config->has($id)) {
109                return $this->config->get($id);
110            }
111        }
112
113        throw ContainerNotFoundException::forKey($id);
114    }
115
116    /**
117     * Determines whether the given identifier is resolved internally by the container itself.
118     *
119     * This method SHALL match the identifier against:
120     * - the alias "config"
121     * - the ConfigInterface::class string
122     * - the concrete class of the injected configuration instance
123     *
124     * @param string $id the identifier to check
125     *
126     * @return bool true if the identifier is resolved internally; false otherwise
127     */
128    private function isResolvedByContainer(string $id): bool
129    {
130        return \in_array($id, [self::ALIAS, ConfigInterface::class, $this->config::class], true);
131    }
132}