Files
Jonathan Miller 4cc3f5bee4
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Successful in 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Generic: Repo Health / Release configuration (push) Successful in 5s
Generic: Repo Health / Scripts governance (push) Successful in 5s
Generic: Repo Health / Release configuration (pull_request) Successful in 6s
Generic: Repo Health / Scripts governance (pull_request) Successful in 6s
Generic: Repo Health / Repository health (push) Successful in 14s
Generic: Repo Health / Repository health (pull_request) Successful in 12s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 44s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 49s
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been skipped
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been skipped
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
style: fix all PHPCS PSR-12 violations across 74 files (7539 → 0 errors)
- Convert tabs to spaces (3,413 violations)
- Fix line endings, trailing whitespace, brace placement
- Break lines exceeding 150-char absolute limit
- Replace heredoc tab closers with spaces
- Fix empty elseif, forbidden function calls
- Update phpcs.xml: exclude rules inappropriate for CLI scripts
  (SideEffects, MissingNamespace, MultipleClasses, HeaderOrder,
  empty catch blocks)

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 17:07:51 -05:00

460 lines
13 KiB
PHP

<?php
declare(strict_types=1);
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.Enterprise.Config
* INGROUP: MokoStandards.Enterprise
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /lib/Enterprise/Config.php
* BRIEF: Configuration manager
*/
/**
* Configuration Manager - Centralized, environment-aware configuration.
*
* This class provides enterprise-grade configuration management with:
* - Environment variable loading (.env support via phpdotenv)
* - YAML and JSON configuration file parsing
* - Secure secret management
* - Configuration validation
* - Default values with overrides
* - Type-safe configuration access
* - Dot notation for nested values
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* @package MokoStandards\Enterprise
* @version 04.00.04
* @author MokoStandards Team
* @license GPL-3.0-or-later
*/
namespace MokoEnterprise;
use RuntimeException;
/**
* Exception raised when configuration validation fails.
*/
class ConfigValidationError extends RuntimeException
{
}
/**
* Enterprise configuration manager with environment support.
*
* Features:
* - Environment-based configuration
* - Dot notation for nested access (e.g., 'github.rate_limit')
* - Runtime overrides
* - Type-safe getters
* - Default value fallbacks
* - Environment detection (dev/staging/production)
*
* Example:
* ```php
* $config = Config::load();
* $org = $config->get('github.organization');
* $rateLimit = $config->getInt('github.rate_limit', 5000);
* $isProduction = $config->isProduction();
* ```
*/
class Config
{
/** @var array<string, mixed> Default configuration values */
private const DEFAULT_CONFIG = [
'version' => '04.00.04',
'environment' => 'development',
'platform' => 'gitea',
'github' => [
'organization' => 'mokoconsulting-tech',
'rate_limit' => 5000,
'max_retries' => 3,
'timeout' => 30,
],
'gitea' => [
'url' => 'https://git.mokoconsulting.tech',
'organization' => 'mokoconsulting-tech',
'rate_limit' => 5000,
'max_retries' => 3,
'timeout' => 30,
],
'logging' => [
'level' => 'INFO',
'format' => 'json',
'directory' => 'logs',
'retention_days' => 90,
],
'audit' => [
'enabled' => true,
'directory' => 'var/logs/audit',
'max_file_size_mb' => 20,
'retention_days' => 90,
],
'cache' => [
'enabled' => true,
'ttl_seconds' => 300,
],
'circuit_breaker' => [
'enabled' => true,
'threshold' => 5,
'timeout_seconds' => 60,
],
];
/** @var array<string, mixed> Configuration data */
private array $configData;
/** @var string Current environment */
private string $environment;
/** @var array<string, mixed> Runtime override data */
private array $overrideData = [];
public const VERSION = '04.06.00';
/**
* Constructor.
*
* @param array<string, mixed> $configData Configuration data
* @param string $environment Environment name
*/
public function __construct(array $configData, string $environment = 'development')
{
$this->configData = $configData;
$this->environment = $environment;
}
/**
* Load configuration from environment.
*
* @param string|null $env Environment override (null = auto-detect)
* @return self Configuration instance
*/
public static function load(?string $env = null): self
{
// Detect environment from env var or default to development
$env = $env ?? $_ENV['MOKO_ENV'] ?? getenv('MOKO_ENV') ?: 'development';
// Start with default config
$configData = self::DEFAULT_CONFIG;
$configData['environment'] = $env;
// Load from .env file if exists using vlucas/phpdotenv
$repoRoot = dirname(__DIR__, 2);
if (file_exists($repoRoot . '/.env')) {
// Note: In production, you'd use Dotenv::createImmutable() here
// For now, we'll manually parse simple .env files
self::loadEnvFile($repoRoot . '/.env');
}
// Override with environment variables
self::applyEnvironmentOverrides($configData);
return new self($configData, $env);
}
/**
* Load environment variables from .env file.
*
* @param string $envFile Path to .env file
*/
private static function loadEnvFile(string $envFile): void
{
if (!is_readable($envFile)) {
return;
}
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($lines === false) {
return;
}
foreach ($lines as $line) {
// Skip comments
if (str_starts_with(trim($line), '#')) {
continue;
}
// Parse KEY=VALUE format
if (strpos($line, '=') !== false) {
[$key, $value] = explode('=', $line, 2);
$key = trim($key);
$value = trim($value);
// Remove quotes if present
if (preg_match('/^(["\'])(.*)\\1$/', $value, $matches)) {
$value = $matches[2];
}
// Set environment variable
putenv("$key=$value");
$_ENV[$key] = $value;
}
}
}
/**
* Apply environment variable overrides to config.
*
* @param array<string, mixed> &$configData Configuration data to modify
*/
private static function applyEnvironmentOverrides(array &$configData): void
{
// Platform selection: GIT_PLATFORM env var overrides default
if ($platform = getenv('GIT_PLATFORM')) {
$configData['platform'] = strtolower($platform);
}
// GitHub token resolution (in priority order):
// 1. GH_TOKEN env var (GitHub Actions org/repo secret)
// 2. GITHUB_TOKEN env var (GitHub Actions built-in)
// 3. `gh auth token` from the gh CLI (local developer machines)
$token = getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN') ?: self::resolveGhCliToken();
if (!empty($token)) {
$configData['github']['token'] = $token;
}
if ($org = getenv('GITHUB_ORG')) {
$configData['github']['organization'] = $org;
}
// Gitea token resolution: GA_TOKEN env var (Gitea Actions)
$giteaToken = getenv('GA_TOKEN') ?: '';
if (!empty($giteaToken)) {
$configData['gitea']['token'] = $giteaToken;
}
if ($giteaUrl = getenv('GITEA_URL')) {
$configData['gitea']['url'] = rtrim($giteaUrl, '/');
}
if ($giteaOrg = getenv('GITEA_ORG')) {
$configData['gitea']['organization'] = $giteaOrg;
}
// Logging configuration
if ($logLevel = getenv('LOG_LEVEL')) {
$configData['logging']['level'] = $logLevel;
}
}
/**
* Attempt to retrieve a GitHub token from the gh CLI.
*
* Runs `gh auth token` non-interactively (stdin from null device) and
* validates the output matches a known GitHub token prefix before returning
* it. Returns an empty string when gh is not installed, not authenticated,
* or the output is not a recognisable token.
*/
private static function resolveGhCliToken(): string
{
$nullDevice = PHP_OS_FAMILY === 'Windows' ? 'NUL' : '/dev/null';
$proc = proc_open(
['gh', 'auth', 'token'],
[0 => ['file', $nullDevice, 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes
);
if (!is_resource($proc)) {
return '';
}
$output = trim(stream_get_contents($pipes[1]));
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($proc);
// Accept only strings that look like a real GitHub token
return preg_match('/^(ghp_|github_pat_|gho_|ghu_|ghs_)\S+$/', $output) ? $output : '';
}
/**
* Get configuration value with dot notation.
*
* @param string $key Configuration key (e.g., 'github.rate_limit')
* @param mixed $default Default value if key not found
* @return mixed Configuration value
*/
public function get(string $key, mixed $default = null): mixed
{
// Check runtime overrides first
if (array_key_exists($key, $this->overrideData)) {
return $this->overrideData[$key];
}
// Navigate nested configuration using dot notation
$value = $this->configData;
foreach (explode('.', $key) as $part) {
if (is_array($value) && array_key_exists($part, $value)) {
$value = $value[$part];
} else {
return $default;
}
}
return $value;
}
/**
* Set configuration value (runtime override).
*
* @param string $key Configuration key
* @param mixed $value Value to set
*/
public function set(string $key, mixed $value): void
{
$this->overrideData[$key] = $value;
}
/**
* Get integer value.
*
* @param string $key Configuration key
* @param int $default Default value
* @return int Integer value
*/
public function getInt(string $key, int $default = 0): int
{
$value = $this->get($key, $default);
return is_numeric($value) ? (int) $value : $default;
}
/**
* Get string value.
*
* @param string $key Configuration key
* @param string $default Default value
* @return string String value
*/
public function getString(string $key, string $default = ''): string
{
$value = $this->get($key, $default);
return is_scalar($value) ? (string) $value : $default;
}
/**
* Get boolean value.
*
* @param string $key Configuration key
* @param bool $default Default value
* @return bool Boolean value
*/
public function getBool(string $key, bool $default = false): bool
{
$value = $this->get($key, $default);
// Handle string representations of booleans
if (is_string($value)) {
$value = strtolower($value);
if (in_array($value, ['true', '1', 'yes', 'on'], true)) {
return true;
}
if (in_array($value, ['false', '0', 'no', 'off'], true)) {
return false;
}
}
return (bool) $value;
}
/**
* Get entire configuration section.
*
* @param string $section Section name
* @return array<string, mixed> Section data
*/
public function getSection(string $section): array
{
$value = $this->get($section, []);
return is_array($value) ? $value : [];
}
/**
* Get current environment.
*
* @return string Environment name
*/
public function getEnvironment(): string
{
return $this->environment;
}
/**
* Check if production environment.
*
* @return bool True if production
*/
public function isProduction(): bool
{
return in_array($this->environment, ['production', 'prod'], true);
}
/**
* Check if development environment.
*
* @return bool True if development
*/
public function isDevelopment(): bool
{
return in_array($this->environment, ['development', 'dev'], true);
}
/**
* Check if staging environment.
*
* @return bool True if staging
*/
public function isStaging(): bool
{
return in_array($this->environment, ['staging', 'stage'], true);
}
/**
* Get all configuration data.
*
* @return array<string, mixed> All configuration
*/
public function all(): array
{
return array_merge($this->configData, $this->overrideData);
}
/**
* Validate required configuration keys exist.
*
* @param array<string> $requiredKeys Required configuration keys
* @throws ConfigValidationError If validation fails
*/
public function validate(array $requiredKeys): void
{
$missing = [];
foreach ($requiredKeys as $key) {
if ($this->get($key) === null) {
$missing[] = $key;
}
}
if (!empty($missing)) {
throw new ConfigValidationError(
'Missing required configuration keys: ' . implode(', ', $missing)
);
}
}
/**
* String representation.
*
* @return string
*/
public function __toString(): string
{
return "Config(environment='{$this->environment}')";
}
}