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: ->files()
44: ->in($directory)
45: ->include('/\.php$/')
46: ->doNotRecurse();
47:
48: $items = [];
49: foreach ($files as $file) {
50: $basename = $file->getBasename('.php');
51: $file = (string) $file;
52: if (Regex::match('/[\s.]|^[0-9]+$/', $basename)) {
53: throw new InvalidConfigurationException(sprintf(
54: 'Invalid configuration file name: %s',
55: $file,
56: ));
57: }
58: $values = require $file;
59: if (!is_array($values)) {
60: throw new InvalidConfigurationException(sprintf(
61: 'Invalid configuration file: %s',
62: $file,
63: ));
64: }
65: $items[$basename] = $values;
66: }
67:
68: ksort($items, \SORT_NATURAL);
69: $this->Items = $items;
70:
71: return $this;
72: }
73:
74: /**
75: * Check if a configuration value exists
76: */
77: public function has(string $key): bool
78: {
79: return Arr::has($this->Items, $key);
80: }
81:
82: /**
83: * Get a configuration value
84: *
85: * @param mixed $default
86: * @return mixed
87: * @throws OutOfRangeException if `$key` is not configured and no `$default`
88: * is given.
89: */
90: public function get(string $key, $default = null)
91: {
92: if (func_num_args() > 1) {
93: return Arr::get($this->Items, $key, $default);
94: }
95: return Arr::get($this->Items, $key);
96: }
97:
98: /**
99: * Get multiple configuration values
100: *
101: * @param iterable<string> $keys
102: * @param mixed $default
103: * @return array<string,mixed>
104: * @throws OutOfRangeException if a key is not configured and no `$default`
105: * is given.
106: */
107: public function getMultiple(iterable $keys, $default = null): array
108: {
109: if (func_num_args() > 1) {
110: foreach ($keys as $key) {
111: $values[$key] = Arr::get($this->Items, $key, $default);
112: }
113: } else {
114: foreach ($keys as $key) {
115: $values[$key] = Arr::get($this->Items, $key);
116: }
117: }
118: return $values ?? [];
119: }
120:
121: /**
122: * Get all configuration values
123: *
124: * @return array<string,mixed[]>
125: */
126: public function all(): array
127: {
128: return $this->Items;
129: }
130:
131: /**
132: * @internal
133: *
134: * @param string $offset
135: */
136: public function offsetExists($offset): bool
137: {
138: return Arr::get($this->Items, $offset, null) !== null;
139: }
140:
141: /**
142: * @internal
143: *
144: * @param string $offset
145: * @return mixed
146: * @disregard P1038
147: */
148: #[ReturnTypeWillChange]
149: public function offsetGet($offset)
150: {
151: return Arr::get($this->Items, $offset);
152: }
153:
154: /**
155: * @internal
156: *
157: * @param string|null $offset
158: * @param mixed $value
159: * @return never
160: */
161: public function offsetSet($offset, $value): void
162: {
163: throw new LogicException('Configuration values cannot be changed at runtime');
164: }
165:
166: /**
167: * @internal
168: *
169: * @param string $offset
170: * @return never
171: */
172: public function offsetUnset($offset): void
173: {
174: throw new LogicException('Configuration values cannot be changed at runtime');
175: }
176: }
177: