FriendlyConfig – prosty, czytelny format konfiguracji z szybkim cache w PHP
W wielu projektach konfiguracja kończy jako:
- rozbudowany YAML (wrażliwy na wcięcia),
- zagnieżdżona tablica PHP (mało przyjazna dla nietechnicznych),
- albo JSON (czytelny, ale mało ergonomiczny do ręcznej edycji).
Ten artykuł pokazuje kompletne rozwiązanie:
FriendlyConfig (.fcfg) – liniowy, maksymalnie czytelny format + szybki loader z cache do natywnego PHP.
1. Założenia formatu
Projektując format, przyjęliśmy:
- jedna linia = jedna deklaracja
- brak wcięć zależnych od poziomu
- brak przecinków
- brak nawiasów klamrowych
- czytelność dla osób nietechnicznych
- łatwy parser
2. Składnia FriendlyConfig
Podstawowy przykład
# Aplikacja
app.name = Moja Aplikacja
app.env = production
app.debug = false
app.port = 8080
# Baza danych
database.host = localhost
database.port = 3306
database.user = root
database.password = "tajne hasło"
# Lista administratorów
admins[] = jan
admins[] = anna
admins[] = piotr
Reguły
1. Klucze z kropkami = struktura
database.host = localhost
↓
[
'database' => [
'host' => 'localhost'
]
]
2. Listy przez []
admins[] = jan
admins[] = anna
↓
[
'admins' => ['jan', 'anna']
]
3. Automatyczne typowanie
| Wartość | Typ |
|---|---|
true |
bool |
false |
bool |
null |
null |
123 |
int |
12.5 |
float |
"tekst" |
string |
| bez cudzysłowów | string |
3. Implementacja parsera w PHP
Parser składa się z trzech części:
- Parsowanie linii
- Rozpoznawanie typów
- Budowanie struktury z kluczy kropkowych
Kompletna implementacja
<?php
class FriendlyConfig
{
public static function load(string $path): array
{
$cacheFile = $path . '.cache.php';
if (self::isCacheValid($path, $cacheFile)) {
return require $cacheFile;
}
$config = self::parseFile($path);
self::writeCache($cacheFile, $config);
return $config;
}
private static function isCacheValid(string $source, string $cache): bool
{
if (!is_file($cache)) {
return false;
}
return filemtime($cache) >= filemtime($source);
}
private static function writeCache(string $cacheFile, array $config): void
{
$export = var_export($config, true);
$content = <<<PHP
<?php
// Auto-generated cache file. Do not edit.
return {$export};
PHP;
file_put_contents($cacheFile, $content, LOCK_EX);
}
public static function parseFile(string $path): array
{
if (!is_file($path)) {
throw new InvalidArgumentException("Config file not found: {$path}");
}
return self::parseString(file_get_contents($path));
}
public static function parseString(string $content): array
{
$lines = preg_split('/\r\n|\r|\n/', $content);
$config = [];
foreach ($lines as $lineNumber => $line) {
$line = trim($line);
if ($line === '' || str_starts_with($line, '#')) {
continue;
}
if (!str_contains($line, '=')) {
throw new RuntimeException("Invalid syntax on line " . ($lineNumber + 1));
}
[$key, $value] = array_map('trim', explode('=', $line, 2));
$value = self::parseValue($value);
self::setValue($config, $key, $value);
}
return $config;
}
private static function parseValue(string $value): mixed
{
if (
(str_starts_with($value, '"') && str_ends_with($value, '"')) ||
(str_starts_with($value, "'") && str_ends_with($value, "'"))
) {
return substr($value, 1, -1);
}
if ($value === 'true') return true;
if ($value === 'false') return false;
if ($value === 'null') return null;
if (ctype_digit($value)) return (int)$value;
if (is_numeric($value)) return (float)$value;
return $value;
}
private static function setValue(array &$config, string $key, mixed $value): void
{
$isArray = str_ends_with($key, '[]');
$key = $isArray ? substr($key, 0, -2) : $key;
$segments = explode('.', $key);
$current = &$config;
foreach ($segments as $index => $segment) {
$isLast = $index === array_key_last($segments);
if ($isLast) {
if ($isArray) {
if (!isset($current[$segment]) || !is_array($current[$segment])) {
$current[$segment] = [];
}
$current[$segment][] = $value;
} else {
$current[$segment] = $value;
}
} else {
if (!isset($current[$segment]) || !is_array($current[$segment])) {
$current[$segment] = [];
}
$current = &$current[$segment];
}
}
}
}
4. Mechanizm cache – dlaczego to szybkie?
Pierwsze uruchomienie
- Parsowanie
.fcfg -
Generacja pliku:
config.fcfg.cache.php -
Zapis:
return [ ... ];
Kolejne uruchomienia
require cache.php- brak parsowania
- OPcache może dodatkowo zoptymalizować plik
Efekt:
Wydajność praktycznie jak natywny plik konfiguracyjny PHP.
5. Architektura rozwiązania
config.fcfg
↓
FriendlyConfig::load()
↓
Jeśli cache aktualny → require
Jeśli nie → parse + rebuild cache
To wzorzec stosowany m.in. w systemach kompilujących konfigurację do kodu wykonywalnego.
6. Zalety rozwiązania
Czytelność dla ludzi
Format liniowy jest intuicyjny.
Brak problemów z wcięciami
W przeciwieństwie do YAML.
Brak przecinków i nawiasów
Zero syntaktycznych pułapek.
Wydajność produkcyjna
Po kompilacji to zwykłe return [...].
Prosty parser
~150 linii czystego PHP.
7. Możliwe rozszerzenia (production-grade)
Jeśli projekt urośnie, można dodać:
include = base.fcfg${ENV:DB_HOST}- walidację schematu
- strict mode (zakaz nadpisywania kluczy)
- immutable Config object
- preload kompatybilny z OPcache
- wsparcie wielu plików środowiskowych
8. Podsumowanie
FriendlyConfig to kompromis między:
- prostotą
.env - strukturą YAML
- wydajnością natywnego PHP
Daje:
czytelność dla człowieka
prostotę implementacji
wydajność produkcyjną