Skip to content

Commit c043944

Browse files
committed
Release version 1.0.0-RC17
1 parent 222bd0b commit c043944

File tree

20 files changed

+380
-399
lines changed

20 files changed

+380
-399
lines changed

docs-build/mint.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@
244244
"group": "Release Notes",
245245
"pages": [
246246
"release-notes/versions",
247+
"release-notes/v1.0.0-RC17",
247248
"release-notes/v1.0.0-RC16",
248249
"release-notes/v1.0.0-RC15",
249250
"release-notes/v1.0.0-RC14",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- (utils) New, simplified Settings class with bugs fixed.

docs/release-notes/v1.0.0-RC17.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- (utils) New, simplified Settings class with bugs fixed.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- (utils) New, simplified Settings class with bugs fixed.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- (utils) New, simplified Settings class with bugs fixed.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- (utils) New, simplified Settings class with bugs fixed.

packages/config/src/Settings.php

Lines changed: 91 additions & 221 deletions
Original file line numberDiff line numberDiff line change
@@ -1,258 +1,128 @@
1-
<?php
1+
<?php declare(strict_types=1);
2+
23
namespace Cognesy\Config;
34

5+
use Adbar\Dot;
46
use Cognesy\Config\Exceptions\MissingSettingException;
57
use Cognesy\Config\Exceptions\NoSettingsFileException;
6-
use Exception;
7-
use InvalidArgumentException;
8-
use RuntimeException;
98

10-
class Settings
9+
/**
10+
* Lightweight, read-only config loader.
11+
*
12+
* Search order (first match wins):
13+
* 1. Path set via Settings::setPath('/dir') ← highest priority
14+
* 2. INSTRUCTOR_CONFIG_PATHS or INSTRUCTOR_CONFIG_PATH (comma-separated)
15+
* 3. Internal defaults listed in DEFAULT_PATHS
16+
*
17+
* Usage:
18+
* Settings::setPath(__DIR__.'/config'); // optional override
19+
* $dsn = Settings::get('database', 'dsn'); // dot-notation supported
20+
*
21+
* Implementation notes:
22+
* • All data is cached per-group in static $cache (Dot objects).
23+
* • Settings::flush() clears both cache _and_ custom override.
24+
* • locate() walks the final path list once per group; O(N) worst-case.
25+
* • Throws:
26+
* – NoSettingsFileException when group file missing
27+
* – MissingSettingException when key absent and no default given
28+
*/
29+
final class Settings
1130
{
12-
/**
13-
* @var ?string The path to the configuration files.
14-
*/
15-
static private ?string $path = null;
16-
17-
/**
18-
* @var array<string> The default path to the configuration files.
19-
*/
20-
static private array $defaultPaths = [
31+
private const DEFAULT_PATHS = [
2132
'config/',
2233
'vendor/cognesy/instructor-php/config/',
34+
'vendor/cognesy/instructor-struct/config/',
35+
'vendor/cognesy/instructor-polyglot/config/',
36+
'vendor/cognesy/instructor-config/config/',
2337
];
2438

25-
/**
26-
* @var array The loaded settings.
27-
*/
28-
static private array $settings = [];
29-
30-
// STATIC ////////////////////////////////////////////////////////////////////
39+
private static array $customPaths = [];
40+
private static array $cache = []; // group => Dot
3141

32-
/**
33-
* Sets the path to the configuration files and clears the loaded settings.
34-
*
35-
* @param string $path The new path to the configuration files.
36-
*/
37-
public static function setPath(string $path) : void {
38-
self::$path = self::resolvePath($path);
39-
self::$settings = [];
42+
/** Highest-priority override */
43+
public static function setPath(string $dir): void {
44+
self::$customPaths = [self::dir($dir)];
45+
self::$cache = [];
4046
}
4147

42-
/**
43-
* Gets the current path to the configuration files.
44-
*
45-
* @return string The current path to the configuration files.
46-
*/
47-
public static function getPath(?string $group = null): string {
48-
return self::$path
49-
?? self::getFirstValidPath(
50-
paths: $_ENV['INSTRUCTOR_CONFIG_PATHS']
51-
?? $_ENV['INSTRUCTOR_CONFIG_PATH']
52-
?? self::$defaultPaths,
53-
group: $group,
54-
);
48+
/** Drop all cached groups (tests, hot-reload) */
49+
public static function flush(): void {
50+
self::$cache = [];
51+
self::$customPaths = [];
5552
}
5653

57-
/**
58-
* Gets the default path to the configuration files.
59-
* @return array<string> The default paths to the configuration files.
60-
* @throws Exception
61-
*/
62-
public static function getDefaultPaths(): array {
63-
return self::$defaultPaths;
64-
}
65-
66-
/**
67-
* Gets a setting value by group and key.
68-
*
69-
* @param string $group The settings group.
70-
* @param string $key The settings key.
71-
* @param mixed $default The default value if the key is not found.
72-
* @return mixed The setting value.
73-
* @throws Exception If the group is not provided or the key is not found and no default value is provided.
74-
*/
75-
public static function get(string $group, string $key, mixed $default = null) : mixed {
76-
if (empty($group)) {
77-
throw new InvalidArgumentException("Settings group not provided");
54+
public static function has(string $group, ?string $key = null): bool {
55+
if ($key === null) {
56+
return self::locate($group) !== null;
7857
}
7958

80-
if (!self::isGroupLoaded($group)) {
81-
self::$settings[$group] = dot(self::loadGroup($group));
82-
}
83-
84-
if ($default === null && !self::has($group, $key)) {
85-
throw new MissingSettingException("Settings key not found: $key in group: $group and no default value provided");
86-
}
87-
return self::$settings[$group]->get($key, $default);
59+
$dot = self::load($group, false);
60+
return $dot?->has($key) ?? false;
8861
}
8962

90-
/**
91-
* Checks if a setting exists by group and key.
92-
* If key is not provided, it checks if the group exists.
93-
*
94-
* @param string $group The settings group.
95-
* @param ?string $key The settings key.
96-
* @return bool True if the setting exists, false otherwise.
97-
* @throws Exception If the group is not provided.
98-
*/
99-
public static function has(string $group, ?string $key = null) : bool {
100-
if (empty($group)) {
101-
throw new RuntimeException("Settings group not provided");
102-
}
103-
104-
if (empty($key)) {
105-
return self::hasGroup($group);
106-
}
107-
108-
if (!self::isGroupLoaded($group)) {
109-
self::$settings[$group] = dot(self::loadGroup($group));
110-
}
111-
112-
return self::$settings[$group]->has($key);
113-
}
114-
115-
/**
116-
* Checks if a settings group exists.
117-
*
118-
* @param string $group The settings group.
119-
* @return bool True if the group exists, false otherwise.
120-
* @throws Exception If the group is not provided.
121-
*/
122-
public static function hasGroup(string $group) : bool {
123-
if (empty($group)) {
124-
throw new RuntimeException("Settings group not provided");
125-
}
126-
127-
if (!self::isGroupLoaded($group)) {
128-
self::$settings[$group] = dot(self::loadGroup($group));
63+
public static function get(string $group, string $key, mixed $default = null): mixed {
64+
$dot = self::load($group);
65+
if (!$dot->has($key)) {
66+
if (func_num_args() === 3) {
67+
return $default;
68+
}
69+
throw new MissingSettingException("Key '$key' missing in '$group'");
12970
}
130-
131-
return !empty(self::$settings[$group]);
71+
return $dot->get($key);
13272
}
13373

134-
/**
135-
* Gets a settings group.
136-
*
137-
* @param string $group The settings group.
138-
* @return mixed The settings group.
139-
* @throws Exception If the group is not provided or the settings file is not found.
140-
*/
141-
public static function getGroup(string $group) : mixed {
142-
if (empty($group)) {
143-
throw new RuntimeException("Settings group not provided");
144-
}
74+
// ----------------------------------------------------------------
14575

146-
if (!self::isGroupLoaded($group)) {
147-
self::$settings[$group] = dot(self::loadGroup($group));
76+
private static function load(string $group, bool $throw = true): ?Dot {
77+
if (isset(self::$cache[$group])) {
78+
return self::$cache[$group];
14879
}
149-
150-
return self::$settings[$group];
151-
}
152-
153-
/**
154-
* Sets a setting value by group and key.
155-
*
156-
* @param string $group The settings group.
157-
* @param string $key The settings key.
158-
* @param mixed $value The value to set.
159-
*/
160-
public static function set(string $group, string $key, mixed $value) : void {
161-
if (!self::isGroupLoaded($group)) {
162-
self::$settings[$group] = dot(self::loadGroup($group));
80+
$file = self::locate($group);
81+
if ($file === null) {
82+
if ($throw) {
83+
throw new NoSettingsFileException("Config '$group' not found in any search path");
84+
}
85+
return null;
16386
}
164-
165-
self::$settings[$group] = self::$settings[$group]->set($key, $value);
87+
/** @var array $data */
88+
$data = require $file;
89+
return self::$cache[$group] = new Dot($data);
16690
}
16791

168-
/**
169-
* Unsets a settings group.
170-
*
171-
* @param string $group The settings group.
172-
*/
173-
public static function unset(string $group) : void {
174-
if (self::isGroupLoaded($group)) {
175-
self::$settings[$group] = [];
92+
/** Return full path to file or null */
93+
private static function locate(string $group): ?string {
94+
foreach (self::paths() as $dir) {
95+
$file = $dir . $group . '.php';
96+
if (is_file($file)) {
97+
return $file;
98+
}
17699
}
100+
return null;
177101
}
178102

179-
// INTERNAL //////////////////////////////////////////////////////////////////
180-
181-
/**
182-
* Checks if a settings group is loaded.
183-
*
184-
* @param string $group The settings group.
185-
* @return bool True if the group is loaded, false otherwise.
186-
*/
187-
private static function isGroupLoaded(string $group) : bool {
188-
return isset(self::$settings[$group]) && (self::$settings[$group] !== null);
189-
}
190-
191-
/**
192-
* Loads a settings group from a file.
193-
*
194-
* @param string $group The settings group.
195-
* @return array The loaded settings group.
196-
* @throws Exception If the settings file is not found.
197-
*/
198-
private static function loadGroup(string $group) : array {
199-
$rootPath = self::getPath($group);
200-
201-
// Ensure the rootPath ends with a directory separator
202-
$rootPath = rtrim($rootPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
203-
204-
$path = $rootPath . $group . '.php';
205-
206-
if (!file_exists($path)) {
207-
throw new NoSettingsFileException("Settings file not found: $path");
103+
/** Final search path list, in order */
104+
private static function paths(): array {
105+
if (self::$customPaths) {
106+
return self::$customPaths;
208107
}
209-
210-
return require $path;
211-
}
212-
213-
/**
214-
* Resolves a given path to an absolute path.
215-
*
216-
* @param string $path The path to resolve.
217-
* @return string The resolved absolute path.
218-
*/
219-
private static function resolvePath(string $path): string {
220-
$path = self::isAbsolutePath($path) ? $path : BasePath::get($path);
221-
// if path does not end with DIRECTORY_SEPARATOR, add it
222-
return rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
108+
$env = getenv('INSTRUCTOR_CONFIG_PATHS')
109+
?: ($_ENV['INSTRUCTOR_CONFIG_PATHS'] ?? '')
110+
?: getenv('INSTRUCTOR_CONFIG_PATH')
111+
?: ($_ENV['INSTRUCTOR_CONFIG_PATH'] ?? '');
112+
$envPaths = array_filter(array_map(fn($p) => self::dir($p), explode(',', $env)));
113+
return array_unique([...$envPaths, ...array_map(fn($p) => self::dir($p), self::DEFAULT_PATHS)]);
223114
}
224115

225-
private static function getFirstValidPath(string|array $paths, ?string $group = null): string {
226-
$paths = is_array($paths) ? $paths : explode(',', $paths);
227-
if (empty($paths)) {
228-
throw new InvalidArgumentException("No settings paths provided");
116+
/** Normalise dir string, always with trailing slash */
117+
private static function dir(string $dir): string {
118+
$dir = rtrim($dir);
119+
if ($dir === '') {
120+
return '';
229121
}
230-
231-
foreach ($paths as $path) {
232-
$resolvedPath = self::resolvePath($path);
233-
if (is_dir($resolvedPath)) {
234-
// if $group is not provided, return the resolved path
235-
if (empty($group)) {
236-
return $resolvedPath;
237-
}
238-
// check if group file exists
239-
$groupPath = $resolvedPath . $group . '.php';
240-
if (file_exists($groupPath)) {
241-
return $resolvedPath;
242-
}
243-
}
122+
// relative → project base
123+
if (!str_starts_with($dir, '/') && !preg_match('/^[A-Z]:\\\\/i', $dir)) {
124+
$dir = BasePath::get($dir);
244125
}
245-
246-
throw new NoSettingsFileException("No valid settings path found in: " . implode(', ', $paths));
247-
}
248-
249-
/**
250-
* Checks if a given path is absolute.
251-
*
252-
* @param string $path The path to check.
253-
* @return bool True if the path is absolute, false otherwise.
254-
*/
255-
private static function isAbsolutePath(string $path): bool {
256-
return str_starts_with($path, '/') || preg_match('/^[a-zA-Z]:\\\\/', $path) === 1;
126+
return rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
257127
}
258128
}

0 commit comments

Comments
 (0)