Add repo hygiene rules and ignore secrets

This commit is contained in:
2026-02-03 06:55:39 +01:00
parent 88732a8ae7
commit 6035bc1715
52 changed files with 3295 additions and 25 deletions

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace App\Config;
final class ConfigLoader
{
private ConfigValidator $validator;
private string $baseDir;
/** @var array<string,array<string,mixed>> */
private array $cache = [];
public function __construct(ConfigValidator $validator, ?string $baseDir = null)
{
$this->validator = $validator;
$this->baseDir = $baseDir ?? dirname(__DIR__, 3) . '/config';
}
/**
* @return array<string,mixed>
*/
public function planetClasses(): array
{
return $this->load('planet_classes.json', function (array $config): void {
$this->validator->validatePlanetClasses($config, 'planet_classes.json');
});
}
/**
* @return array<string,mixed>
*/
public function races(): array
{
return $this->load('races.json', function (array $config): void {
$this->validator->validateRaces($config, 'races.json');
});
}
/**
* @return array<string,mixed>
*/
public function blueprintsBuildings(): array
{
return $this->load('blueprints_buildings.json', function (array $config): void {
$this->validator->validateBlueprintsBuildings($config, 'blueprints_buildings.json');
});
}
/**
* @param callable(array<string,mixed>):void $validate
* @return array<string,mixed>
*/
private function load(string $file, callable $validate): array
{
if (isset($this->cache[$file])) {
return $this->cache[$file];
}
$path = rtrim($this->baseDir, '/') . '/' . $file;
if (!file_exists($path)) {
throw new \RuntimeException("Config-Datei nicht gefunden: {$path}");
}
$raw = file_get_contents($path);
if ($raw === false) {
throw new \RuntimeException("Config-Datei konnte nicht gelesen werden: {$path}");
}
$data = json_decode($raw, true, flags: JSON_THROW_ON_ERROR);
if (!is_array($data)) {
throw new \RuntimeException("Config-Datei muss ein JSON-Objekt sein: {$path}");
}
$validate($data);
$this->cache[$file] = $data;
return $data;
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Config;
final class ConfigValidationException extends \RuntimeException
{
/** @var string[] */
private array $errors;
/**
* @param string[] $errors
*/
public function __construct(string $file, array $errors)
{
$this->errors = $errors;
$message = "Config-Validation fehlgeschlagen: {$file}\n- " . implode("\n- ", $errors);
parent::__construct($message);
}
/**
* @return string[]
*/
public function getErrors(): array
{
return $this->errors;
}
}

View File

@@ -0,0 +1,197 @@
<?php
declare(strict_types=1);
namespace App\Config;
final class ConfigValidator
{
/**
* @param array<string,mixed> $config
*/
public function validatePlanetClasses(array $config, string $file): void
{
$errors = [];
foreach (['resources', 'weights', 'tiers', 'classes', 'global_bounds'] as $key) {
if (!array_key_exists($key, $config)) {
$errors[] = "Fehlender Key '{$key}'";
}
}
if (isset($config['resources']) && (!is_array($config['resources']) || $config['resources'] === [])) {
$errors[] = "'resources' muss ein nicht-leeres Array sein";
}
$expected = $this->expectedResources();
if (isset($config['resources']) && is_array($config['resources'])) {
$missing = array_diff($expected, $config['resources']);
if ($missing) {
$errors[] = "'resources' fehlt: " . implode(', ', $missing);
}
}
if (isset($config['weights']) && !is_array($config['weights'])) {
$errors[] = "'weights' muss ein Objekt sein";
}
if (isset($config['weights']) && is_array($config['weights'])) {
foreach ($expected as $res) {
if (!isset($config['weights'][$res]) || !is_numeric($config['weights'][$res])) {
$errors[] = "'weights.{$res}' muss numerisch sein";
}
}
}
if (isset($config['global_bounds']) && is_array($config['global_bounds'])) {
if (!isset($config['global_bounds']['min']) || !is_numeric($config['global_bounds']['min'])) {
$errors[] = "'global_bounds.min' muss numerisch sein";
}
if (!isset($config['global_bounds']['max']) || !is_numeric($config['global_bounds']['max'])) {
$errors[] = "'global_bounds.max' muss numerisch sein";
}
}
if (isset($config['tiers']) && is_array($config['tiers'])) {
foreach ($config['tiers'] as $tierKey => $tier) {
if (!is_array($tier)) {
$errors[] = "Tier '{$tierKey}' muss ein Objekt sein";
continue;
}
if (!isset($tier['target_score']) || !is_numeric($tier['target_score'])) {
$errors[] = "Tier '{$tierKey}': 'target_score' fehlt oder ist nicht numerisch";
}
if (!isset($tier['epsilon']) || !is_numeric($tier['epsilon'])) {
$errors[] = "Tier '{$tierKey}': 'epsilon' fehlt oder ist nicht numerisch";
}
}
}
if (isset($config['classes']) && is_array($config['classes'])) {
foreach ($config['classes'] as $classKey => $class) {
if (!is_array($class)) {
$errors[] = "Klasse '{$classKey}' muss ein Objekt sein";
continue;
}
if (!isset($class['spawn_weight']) || !is_numeric($class['spawn_weight'])) {
$errors[] = "Klasse '{$classKey}': 'spawn_weight' fehlt oder ist nicht numerisch";
}
if (isset($class['constraints']) && !is_array($class['constraints'])) {
$errors[] = "Klasse '{$classKey}': 'constraints' muss ein Objekt sein";
}
if (isset($class['constraints']) && is_array($class['constraints'])) {
foreach ($class['constraints'] as $res => $constraint) {
if (!is_array($constraint)) {
$errors[] = "Constraint '{$classKey}.{$res}' muss ein Objekt sein";
continue;
}
if (isset($constraint['min']) && !is_numeric($constraint['min'])) {
$errors[] = "Constraint '{$classKey}.{$res}.min' muss numerisch sein";
}
if (isset($constraint['max']) && !is_numeric($constraint['max'])) {
$errors[] = "Constraint '{$classKey}.{$res}.max' muss numerisch sein";
}
}
}
if (isset($class['temperature_range_c'])) {
$range = $class['temperature_range_c'];
if (!is_array($range) || count($range) !== 2) {
$errors[] = "Klasse '{$classKey}': 'temperature_range_c' muss ein Array [min,max] sein";
} else {
[$min, $max] = array_values($range);
if (!is_numeric($min) || !is_numeric($max)) {
$errors[] = "Klasse '{$classKey}': 'temperature_range_c' Werte müssen numerisch sein";
} elseif ((float)$min > (float)$max) {
$errors[] = "Klasse '{$classKey}': 'temperature_range_c' min darf nicht größer als max sein";
}
}
}
}
}
if ($errors) {
throw new ConfigValidationException($file, $errors);
}
}
/**
* @param array<string,mixed> $config
*/
public function validateRaces(array $config, string $file): void
{
$errors = [];
if (!isset($config['races']) || !is_array($config['races'])) {
$errors[] = "'races' muss ein Objekt sein";
}
if (isset($config['races']) && is_array($config['races'])) {
foreach ($config['races'] as $raceKey => $race) {
if (!is_array($race)) {
$errors[] = "Race '{$raceKey}' muss ein Objekt sein";
continue;
}
if (empty($race['name'])) {
$errors[] = "Race '{$raceKey}': 'name' fehlt";
}
if (isset($race['modifiers']) && !is_array($race['modifiers'])) {
$errors[] = "Race '{$raceKey}': 'modifiers' muss ein Objekt sein";
}
}
}
if ($errors) {
throw new ConfigValidationException($file, $errors);
}
}
/**
* @param array<string,mixed> $config
*/
public function validateBlueprintsBuildings(array $config, string $file): void
{
$errors = [];
if (!isset($config['blueprints']) || !is_array($config['blueprints'])) {
$errors[] = "'blueprints' muss ein Array sein";
}
if (isset($config['blueprints']) && is_array($config['blueprints'])) {
foreach ($config['blueprints'] as $idx => $bp) {
if (!is_array($bp)) {
$errors[] = "Blueprint #{$idx} muss ein Objekt sein";
continue;
}
foreach (['kind', 'key', 'name'] as $req) {
if (empty($bp[$req])) {
$errors[] = "Blueprint #{$idx}: '{$req}' fehlt";
}
}
if (isset($bp['effects']) && !is_array($bp['effects'])) {
$errors[] = "Blueprint '{$bp['key'] ?? $idx}': 'effects' muss ein Array sein";
}
if (isset($bp['requirements']) && !is_array($bp['requirements'])) {
$errors[] = "Blueprint '{$bp['key'] ?? $idx}': 'requirements' muss ein Array sein";
}
if (isset($bp['access']) && !is_array($bp['access'])) {
$errors[] = "Blueprint '{$bp['key'] ?? $idx}': 'access' muss ein Objekt sein";
}
}
}
if ($errors) {
throw new ConfigValidationException($file, $errors);
}
}
/**
* @return string[]
*/
public function expectedResources(): array
{
return [
'metal',
'alloy',
'crystals',
'energy',
'credits',
'population',
'water',
'deuterium',
'food',
];
}
}