build: M1 Dockerized development stack with PHP skeleton, config/logging, Tailwind assets, and wrappers
Implements MeshVault M1 (issues #6–11): - Four-service Docker Compose stack (nginx, PHP-FPM 8.3, PostgreSQL 17, Node assets) - Slim 4 + PHP-DI routing with escaped PHP views and production/dev error handling - Immutable validated AppConfig with redacted diagnostics - JSON stdout Monolog logging with job-scoped context - Tailwind CSS v4 deterministic locked builds with template scanning - POSIX and PowerShell dev wrappers (up/down/test/lint/assets/watch) - Startup config validation fails before FPM serves as unprivileged www-data - migrate/seed explicitly unavailable until M2 (#12); #10 remains open Verified via clean-start acceptance on remote host (keith@192.168.2.23).
This commit is contained in:
1 parent
c2747a5762
commit
d3e8cf8fae
39 files changed
+5291
-38
No files matched your search
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MeshVault\Config;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use JsonSerializable;
|
||||
use SensitiveParameter;
|
||||
|
||||
final readonly class AppConfig implements JsonSerializable
|
||||
{
|
||||
private function __construct(
|
||||
public string $environment,
|
||||
public string $libraryPath,
|
||||
public string $dbHost,
|
||||
public int $dbPort,
|
||||
public string $dbName,
|
||||
public string $dbUser,
|
||||
#[SensitiveParameter] public string $dbPassword,
|
||||
public string $logLevel,
|
||||
) {
|
||||
}
|
||||
|
||||
/** Values are explicit strings, as returned by getenv(); no implicit .env loading. */
|
||||
public static function fromEnvironment(#[SensitiveParameter] array $environment): self
|
||||
{
|
||||
$required = static function (string $key) use ($environment): string {
|
||||
$value = $environment[$key] ?? null;
|
||||
if (!is_string($value) || trim($value) === '' || str_contains($value, "\0")) {
|
||||
throw new InvalidArgumentException($key . ' must be a non-empty string without NUL bytes.');
|
||||
}
|
||||
return $value;
|
||||
};
|
||||
$choice = static function (string $key, string $default, array $choices) use ($environment): string {
|
||||
$value = array_key_exists($key, $environment) ? $environment[$key] : $default;
|
||||
if (!is_string($value) || !in_array($value, $choices, true)) {
|
||||
throw new InvalidArgumentException($key . ' must be one of: ' . implode(', ', $choices) . '.');
|
||||
}
|
||||
return $value;
|
||||
};
|
||||
$appEnvironment = $choice('MESHVAULT_ENV', 'production', ['development', 'test', 'production']);
|
||||
$level = $choice('MESHVAULT_LOG_LEVEL', 'info', [
|
||||
'debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency',
|
||||
]);
|
||||
$port = array_key_exists('MESHVAULT_DB_PORT', $environment) ? $environment['MESHVAULT_DB_PORT'] : '5432';
|
||||
if (!is_string($port) || !preg_match('/^[0-9]{1,5}$/D', $port) || (int) $port < 1 || (int) $port > 65535) {
|
||||
throw new InvalidArgumentException('MESHVAULT_DB_PORT must be decimal digits in the range 1..65535.');
|
||||
}
|
||||
$path = $required('MESHVAULT_LIBRARY_DIR');
|
||||
$absolute = str_starts_with($path, '/')
|
||||
|| (PHP_OS_FAMILY === 'Windows' && preg_match('~^(?:[A-Za-z]:[\\\\/]|\\\\\\\\)~', $path));
|
||||
if (!$absolute || preg_match('/[\x00-\x1f\x7f]/', $path)) {
|
||||
throw new InvalidArgumentException('MESHVAULT_LIBRARY_DIR must be an absolute local directory path.');
|
||||
}
|
||||
clearstatcache(true, $path);
|
||||
if (!is_dir($path) || !is_writable($path)) {
|
||||
throw new InvalidArgumentException(
|
||||
'MESHVAULT_LIBRARY_DIR must exist as a directory writable by the runtime user.'
|
||||
);
|
||||
}
|
||||
$database = [];
|
||||
foreach (['MESHVAULT_DB_HOST', 'MESHVAULT_DB_NAME', 'MESHVAULT_DB_USER'] as $key) {
|
||||
$value = $required($key);
|
||||
if (trim($value) !== $value || preg_match('/[\x00-\x1f\x7f]/', $value)) {
|
||||
throw new InvalidArgumentException($key . ' must not contain controls or surrounding whitespace.');
|
||||
}
|
||||
$database[$key] = $value;
|
||||
}
|
||||
return new self(
|
||||
$appEnvironment,
|
||||
$path,
|
||||
$database['MESHVAULT_DB_HOST'],
|
||||
(int) $port,
|
||||
$database['MESHVAULT_DB_NAME'],
|
||||
$database['MESHVAULT_DB_USER'],
|
||||
$required('MESHVAULT_DB_PASSWORD'),
|
||||
$level,
|
||||
);
|
||||
}
|
||||
|
||||
/** Diagnostic representations intentionally contain no configuration values. */
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return ['configuration' => '[redacted]'];
|
||||
}
|
||||
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return $this->jsonSerialize();
|
||||
}
|
||||
|
||||
public function __serialize(): array
|
||||
{
|
||||
return $this->jsonSerialize();
|
||||
}
|
||||
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
throw new \LogicException('AppConfig must be created from explicit environment input.');
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user