1: <?php declare(strict_types=1);
2:
3: namespace Salient\Core;
4:
5: use Salient\Contract\Core\Instantiable;
6: use Salient\Core\Exception\InvalidConfigurationException;
7: use Salient\Utility\Arr;
8: use Salient\Utility\File;
9: use Salient\Utility\Regex;
10: use ArrayAccess;
11: use LogicException;
12: use OutOfRangeException;
13: use ReturnTypeWillChange;
14:
15: /**
16: * @api
17: *
18: * @implements ArrayAccess<string,mixed>
19: */
20: final class ConfigurationManager implements ArrayAccess, Instantiable
21: {
22: /** @var array<string,mixed[]> */
23: private array $Items = [];
24:
25: /**
26: * @internal
27: *
28: * @param array<string,mixed[]> $items
29: */
30: public function __construct(array $items = [])
31: {
32: $this->Items = $items;
33: }
34:
35: /**
36: * Load values from files in a directory
37: *
38: * @return $this
39: */
40: public function loadDirectory(string $directory): self
41: {
42: $files = File::find()
43: ->in($directory)
44: ->include('/\.php$/')
45: ->doNotRecurse();
46:
47: $items = [];
48: foreach ($files as $file) {
49: $basename = $file->getBasename('.php');
50: $file = (string) $file;
51: if (Regex::match('/[\s.]|^[0-9]+$/', $basename)) {
52: throw new InvalidConfigurationException(sprintf(
53: 'Invalid configuration file name: %s',
54: $file,
55: ));
56: }
57: $values = require $file;
58: if (!is_array($values)) {
59: throw new InvalidConfigurationException(sprintf(
60: 'Invalid configuration file: %s',
61: $file,
62: ));
63: }
64: $items[$basename] = $values;
65: }
66:
67: ksort($items, \SORT_NATURAL);
68: $this->Items = $items;
69:
70: return $this;
71: }
72:
73: /**
74: * Check if a configuration value exists
75: */
76: public function has(string $key): bool
77: {
78: return Arr::has($this->Items, $key);
79: }
80:
81: /**
82: * Get a configuration value
83: *
84: * @param mixed $default
85: * @return mixed
86: * @throws OutOfRangeException if `$key` is not configured and no `$default`
87: * is given.
88: */
89: public function get(string $key, $default = null)
90: {
91: if (func_num_args() > 1) {
92: return Arr::get($this->Items, $key, $default);
93: }
94: return Arr::get($this->Items, $key);
95: }
96:
97: /**
98: * Get multiple configuration values
99: *
100: * @param iterable<string> $keys
101: * @param mixed $default
102: * @return array<string,mixed>
103: * @throws OutOfRangeException if a key is not configured and no `$default`
104: * is given.
105: */
106: public function getMultiple(iterable $keys, $default = null): array
107: {
108: if (func_num_args() > 1) {
109: foreach ($keys as $key) {
110: $values[$key] = Arr::get($this->Items, $key, $default);
111: }
112: } else {
113: foreach ($keys as $key) {
114: $values[$key] = Arr::get($this->Items, $key);
115: }
116: }
117: return $values ?? [];
118: }
119:
120: /**
121: * Get all configuration values
122: *
123: * @return array<string,mixed[]>
124: */
125: public function all(): array
126: {
127: return $this->Items;
128: }
129:
130: /**
131: * @internal
132: *
133: * @param string $offset
134: */
135: public function offsetExists($offset): bool
136: {
137: return Arr::get($this->Items, $offset, null) !== null;
138: }
139:
140: /**
141: * @internal
142: *
143: * @param string $offset
144: * @return mixed
145: * @disregard P1038
146: */
147: #[ReturnTypeWillChange]
148: public function offsetGet($offset)
149: {
150: return Arr::get($this->Items, $offset);
151: }
152:
153: /**
154: * @internal
155: *
156: * @param string|null $offset
157: * @param mixed $value
158: * @return never
159: */
160: public function offsetSet($offset, $value): void
161: {
162: throw new LogicException('Configuration values cannot be changed at runtime');
163: }
164:
165: /**
166: * @internal
167: *
168: * @param string $offset
169: * @return never
170: */
171: public function offsetUnset($offset): void
172: {
173: throw new LogicException('Configuration values cannot be changed at runtime');
174: }
175: }
176: