| 1 |
<?php |
| 2 |
|
| 3 |
namespace FluentCommunity\Framework\Foundation; |
| 4 |
|
| 5 |
use FluentCommunity\Framework\Support\Arr; |
| 6 |
|
| 7 |
class Config |
| 8 |
{ |
| 9 |
/** |
| 10 |
* Eagerly-set or lazily-loaded config data, keyed by top-level config name. |
| 11 |
* @var array |
| 12 |
*/ |
| 13 |
protected $data = []; |
| 14 |
|
| 15 |
/** |
| 16 |
* Map of top-level key (e.g. 'i18n', 'app') → absolute file path. |
| 17 |
* Files are required on first access to their top-level key. Defers |
| 18 |
* any side-effecting code in config files (notably `__()` calls in |
| 19 |
* config/i18n.php) until consumers actually read them — which always |
| 20 |
* happens after `init`, so WP 6.7+'s `_doing_it_wrong` notice on |
| 21 |
* early textdomain load is no longer triggered by the framework. |
| 22 |
* |
| 23 |
* Eagerly-supplied $data still works (back-compat for tests / direct |
| 24 |
* construction without a file map). |
| 25 |
* |
| 26 |
* @var array<string, string> |
| 27 |
*/ |
| 28 |
protected $files = []; |
| 29 |
|
| 30 |
/** |
| 31 |
* Top-level keys whose backing file has been required. Tracks loaded |
| 32 |
* state separately from $data because a config file may legitimately |
| 33 |
* `return null` / `return []`, and we must not re-require those. |
| 34 |
* |
| 35 |
* @var array<string, bool> |
| 36 |
*/ |
| 37 |
protected $loaded = []; |
| 38 |
|
| 39 |
/** |
| 40 |
* Construct the Config instance. |
| 41 |
* |
| 42 |
* @param array $data Eagerly-populated data (keyed by top-level name). |
| 43 |
* @param array $files Map of top-level name → file path for lazy load. |
| 44 |
*/ |
| 45 |
public function __construct($data, $files = []) |
| 46 |
{ |
| 47 |
$this->data = $data; |
| 48 |
$this->files = $files; |
| 49 |
|
| 50 |
// Anything passed in $data is already resolved. |
| 51 |
foreach (array_keys($data) as $top) { |
| 52 |
$this->loaded[$top] = true; |
| 53 |
} |
| 54 |
} |
| 55 |
|
| 56 |
/** |
| 57 |
* Retrieve all config data. Force-loads every lazy file. |
| 58 |
* |
| 59 |
* @return array |
| 60 |
*/ |
| 61 |
public function all() |
| 62 |
{ |
| 63 |
foreach (array_keys($this->files) as $top) { |
| 64 |
$this->ensureLoaded($top); |
| 65 |
} |
| 66 |
|
| 67 |
return $this->data; |
| 68 |
} |
| 69 |
|
| 70 |
/** |
| 71 |
* Retrieve specific item from config array. |
| 72 |
* |
| 73 |
* @param string $key |
| 74 |
* @param mixed $default |
| 75 |
* @return mixed |
| 76 |
*/ |
| 77 |
public function get($key = null, $default = null) |
| 78 |
{ |
| 79 |
if ($key === null || $key === '') { |
| 80 |
return $this->all(); |
| 81 |
} |
| 82 |
|
| 83 |
$key = $this->resolveKey($key); |
| 84 |
|
| 85 |
$this->ensureLoaded($this->topKey($key)); |
| 86 |
|
| 87 |
return Arr::get($this->data, $key, $default); |
| 88 |
} |
| 89 |
|
| 90 |
/** |
| 91 |
* Get only specific items from the config array. |
| 92 |
* |
| 93 |
* @param string|array $key ($key1, $key2 or [$key1, $key2]) |
| 94 |
* @return array |
| 95 |
*/ |
| 96 |
public function only($key) |
| 97 |
{ |
| 98 |
$keys = array_map(function ($key) { |
| 99 |
return $this->resolveKey($key); |
| 100 |
}, is_array($key) ? $key : func_get_args()); |
| 101 |
|
| 102 |
$result = []; |
| 103 |
|
| 104 |
foreach ($keys as $key) { |
| 105 |
$this->ensureLoaded($this->topKey($key)); |
| 106 |
$result[] = Arr::get($this->data, $key); |
| 107 |
} |
| 108 |
|
| 109 |
return array_values(array_filter($result)); |
| 110 |
} |
| 111 |
|
| 112 |
/** |
| 113 |
* Set an item into the config array on the fly. Forces the underlying |
| 114 |
* file to load first, so the write isn't clobbered by a later lazy load. |
| 115 |
* |
| 116 |
* @param string $key |
| 117 |
* @param mixed $value |
| 118 |
*/ |
| 119 |
public function set($key, $value) |
| 120 |
{ |
| 121 |
$key = $this->resolveKey($key); |
| 122 |
$this->ensureLoaded($this->topKey($key)); |
| 123 |
Arr::set($this->data, $key, $value); |
| 124 |
} |
| 125 |
|
| 126 |
/** |
| 127 |
* Lazy-load a config file the first time its top-level key is touched. |
| 128 |
* Idempotent: subsequent calls are no-ops. |
| 129 |
* |
| 130 |
* @param string $topKey |
| 131 |
* @return void |
| 132 |
*/ |
| 133 |
protected function ensureLoaded($topKey) |
| 134 |
{ |
| 135 |
if (!$topKey || isset($this->loaded[$topKey])) { |
| 136 |
return; |
| 137 |
} |
| 138 |
|
| 139 |
if (isset($this->files[$topKey])) { |
| 140 |
$this->data[$topKey] = require $this->files[$topKey]; |
| 141 |
} |
| 142 |
|
| 143 |
$this->loaded[$topKey] = true; |
| 144 |
} |
| 145 |
|
| 146 |
/** |
| 147 |
* Extract the top-level key from a dotted path. 'app.text_domain' → 'app'. |
| 148 |
* |
| 149 |
* @param string $key |
| 150 |
* @return string |
| 151 |
*/ |
| 152 |
protected function topKey($key) |
| 153 |
{ |
| 154 |
$dot = strpos($key, '.'); |
| 155 |
return $dot === false ? $key : substr($key, 0, $dot); |
| 156 |
} |
| 157 |
|
| 158 |
/** |
| 159 |
* Resolve the config key, add `app.` prefix when the key is short and |
| 160 |
* not a known top-level config name (matches eagerly-loaded data OR a |
| 161 |
* deferred file). Without the file lookup, `get('i18n')` would resolve |
| 162 |
* to `app.i18n` before the i18n config file had been touched. |
| 163 |
* |
| 164 |
* @param string $key |
| 165 |
* @return string |
| 166 |
*/ |
| 167 |
protected function resolveKey($key) |
| 168 |
{ |
| 169 |
if (!$key) return $key; |
| 170 |
|
| 171 |
if ( |
| 172 |
array_key_exists($key, $this->data) |
| 173 |
|| array_key_exists($key, $this->files) |
| 174 |
) { |
| 175 |
return $key; |
| 176 |
} |
| 177 |
|
| 178 |
return str_contains($key, '.') ? $key : "app.{$key}"; |
| 179 |
} |
| 180 |
} |
| 181 |
|