Task: #53687 Support instance-specific cfg write targets and add coverage V2 #4

Merged
norb merged 11 commits from 53687/task-support-instance-specific-cfg-write-target-v2 into master 2026-03-30 15:09:21 +00:00
Showing only changes of commit 29d1da4ac5 - Show all commits

Task: #53687 Align cfg brace style to project conventions

Alejandro Sosa 2026-03-26 12:10:55 +01:00

90
bin/cfg
View file

@ -24,18 +24,23 @@ foreach ($autoloadFiles as $autoloadFile) {
function readJsonInputFile(string $path): array function readJsonInputFile(string $path): array
alejandro.sosa marked this conversation as resolved Outdated

I don't really like this .. seems over complicating the code. It is just for beging able to have different order of arguments?

not that important to me .. and probably better put inside the input helper library or replace it altogether .. IIRC the one used is not maintained anymore

I don't really like this .. seems over complicating the code. It is just for beging able to have different order of arguments? not that important to me .. and probably better put inside the input helper library or replace it altogether .. IIRC the one used is not maintained anymore

@norb wrote in #4 (comment):

I don't really like this .. seems over complicating the code. It is just for beging able to have different order of arguments?

not that important to me .. and probably better put inside the input helper library or replace it altogether .. IIRC the one used is not maintained anymore

You're right. After reviewing it again with the alias cleanup in place, I realized we can keep this much simpler. The pre-parse block was added as a defensive workaround while multiple option variants were still in play, but with --siteDir now unified and a couple of small follow-up fixes, the standard parser flow is sufficient. I removed that extra layer to keep the code cleaner and easier to maintain.

@norb wrote in https://code.verua.online/rabe/Util-Settings/pulls/4#issuecomment-3732: > I don't really like this .. seems over complicating the code. It is just for beging able to have different order of arguments? > > not that important to me .. and probably better put inside the input helper library or replace it altogether .. IIRC the one used is not maintained anymore You're right. After reviewing it again with the alias cleanup in place, I realized we can keep this much simpler. The pre-parse block was added as a defensive workaround while multiple option variants were still in play, but with --siteDir now unified and a couple of small follow-up fixes, the standard parser flow is sufficient. I removed that extra layer to keep the code cleaner and easier to maintain.
alejandro.sosa marked this conversation as resolved Outdated

can you do this as validators? e.g.

->validator(new Input\Validator(

can you do this as validators? e.g. https://code.verua.online/rabe/helpers-cli-input/src/commit/c6fe64321ef06633c89bc055252f22d6ff676d52/example/example.php#L35
{ {
if (!is_readable($path)) { if (!is_readable($path))
{
throw new \RuntimeException("Input file is not readable: $path"); throw new \RuntimeException("Input file is not readable: $path");
} }
$content = file_get_contents($path); $content = file_get_contents($path);
if ($content === false) { if ($content === false)
{
throw new \RuntimeException("Can not read input file: $path"); throw new \RuntimeException("Can not read input file: $path");
} }
try { try
{
$data = json_decode($content, true, 512, JSON_THROW_ON_ERROR); $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $ex) { }
catch (\JsonException $ex)
{
throw new \RuntimeException(sprintf( throw new \RuntimeException(sprintf(
'Input JSON is invalid (%s): %s', 'Input JSON is invalid (%s): %s',
(string)$ex->getCode(), (string)$ex->getCode(),
@ -43,7 +48,8 @@ function readJsonInputFile(string $path): array
)); ));
} }
if (!is_array($data)) { if (!is_array($data))
{
throw new \RuntimeException('Input JSON must decode to an object/array'); throw new \RuntimeException('Input JSON must decode to an object/array');
} }
@ -54,17 +60,21 @@ function readJsonInputFile(string $path): array
// The input helper library does not handle this case reliably. // The input helper library does not handle this case reliably.
$preparsedSiteDir = null; $preparsedSiteDir = null;
$rawArgv = $_SERVER['argv'] ?? $GLOBALS['argv'] ?? null; $rawArgv = $_SERVER['argv'] ?? $GLOBALS['argv'] ?? null;
if (is_array($rawArgv) && !empty($rawArgv)) { if (is_array($rawArgv) && !empty($rawArgv))
{
$filteredArgv = [$rawArgv[0]]; $filteredArgv = [$rawArgv[0]];
for ($idx = 1; $idx < count($rawArgv); $idx++) { for ($idx = 1; $idx < count($rawArgv); $idx++)
{
$arg = $rawArgv[$idx]; $arg = $rawArgv[$idx];
if ($arg === '--siteDir') { if ($arg === '--siteDir')
{
$preparsedSiteDir = $rawArgv[$idx + 1] ?? ''; $preparsedSiteDir = $rawArgv[$idx + 1] ?? '';
if (isset($rawArgv[$idx + 1])) $idx++; if (isset($rawArgv[$idx + 1])) $idx++;
continue; continue;
} }
if (str_starts_with($arg, '--siteDir=')) { if (str_starts_with($arg, '--siteDir='))
{
$preparsedSiteDir = substr($arg, strlen('--siteDir=')); $preparsedSiteDir = substr($arg, strlen('--siteDir='));
continue; continue;
} }
@ -224,9 +234,12 @@ $usage = Cli\manpage( basename(__FILE__), $version,
// Get the supplied input. Passing the collection will make the handler bind values // Get the supplied input. Passing the collection will make the handler bind values
// and validate the input according to our collection // and validate the input according to our collection
try { try
{
$argv = Input\InputHandlerFactory::build('Argv', $collection); $argv = Input\InputHandlerFactory::build('Argv', $collection);
} catch (\Exception $ex) { }
catch (\Exception $ex)
{
echo $usage; echo $usage;
if (isset($argv[1])) { if (isset($argv[1])) {
@ -277,29 +290,37 @@ if ($pkgPath = $argv->find('pkgPath')) $cfg->pkgPath(rtrim($pkgPath, '/').'/');
$site = null; $site = null;
$siteFlag = 0x01; $siteFlag = 0x01;
$siteInput = ($preparsedSiteDir !== null) ? $preparsedSiteDir : $argv->find('siteDir'); $siteInput = ($preparsedSiteDir !== null) ? $preparsedSiteDir : $argv->find('siteDir');
if ($siteInput !== null && $siteInput !== false) { if ($siteInput !== null && $siteInput !== false)
alejandro.sosa marked this conversation as resolved Outdated

probably most of this should be done in the validator as well

probably most of this should be done in the validator as well
{
$siteInput = trim((string)$siteInput); $siteInput = trim((string)$siteInput);
// Accept both "owner_xyz" and "config/owner_xyz" and normalize to site key. // Accept both "owner_xyz" and "config/owner_xyz" and normalize to site key.
if (str_starts_with($siteInput, $appPath.'config/')) { if (str_starts_with($siteInput, $appPath.'config/'))
{
$siteInput = substr($siteInput, strlen($appPath.'config/')); $siteInput = substr($siteInput, strlen($appPath.'config/'));
} elseif (str_starts_with($siteInput, 'config/')) { }
elseif (str_starts_with($siteInput, 'config/'))
{
$siteInput = substr($siteInput, strlen('config/')); $siteInput = substr($siteInput, strlen('config/'));
} }
$siteInput = trim($siteInput, '/'); $siteInput = trim($siteInput, '/');
if ($siteInput === '') { if ($siteInput === '')
{
fwrite(STDERR, 'Option --siteDir is empty.'.PHP_EOL); fwrite(STDERR, 'Option --siteDir is empty.'.PHP_EOL);
exit(1); exit(1);
} }
// Block directory traversal and hidden-dot segments. // Block directory traversal and hidden-dot segments.
if (str_contains($siteInput, '..') || preg_match('~(^|/)\.(?:/|$)~', $siteInput)) { if (str_contains($siteInput, '..') || preg_match('~(^|/)\.(?:/|$)~', $siteInput))
{
fwrite(STDERR, "Invalid directory in --siteDir: '$siteInput'.".PHP_EOL); fwrite(STDERR, "Invalid directory in --siteDir: '$siteInput'.".PHP_EOL);
exit(1); exit(1);
} }
// Allow only predictable path segments for instance directories. // Allow only predictable path segments for instance directories.
foreach (explode('/', $siteInput) as $part) { foreach (explode('/', $siteInput) as $part)
if ($part === '' || !preg_match('/^[A-Za-z0-9._-]+$/', $part)) { {
if ($part === '' || !preg_match('/^[A-Za-z0-9._-]+$/', $part))
{
fwrite(STDERR, "Invalid directory name segment '$part' in --siteDir.".PHP_EOL); fwrite(STDERR, "Invalid directory name segment '$part' in --siteDir.".PHP_EOL);
exit(1); exit(1);
} }
@ -310,14 +331,17 @@ if ($siteInput !== null && $siteInput !== false) {
$cfg->site($site); $cfg->site($site);
} }
try { try
{
if (is_readable($cfg->buildFileName('default'))) { if (is_readable($cfg->buildFileName('default'))) {
$cfg->load(); $cfg->load();
} }
elseif (is_readable($cfgFile = $cfg->buildFileName())) { elseif (is_readable($cfgFile = $cfg->buildFileName())) {
$cfg->load(require($cfgFile)); $cfg->load(require($cfgFile));
} }
} catch (\Throwable $e) { }
catch (\Throwable $e)
{
fwrite(STDERR, "Error: ".$e->getMessage().PHP_EOL); fwrite(STDERR, "Error: ".$e->getMessage().PHP_EOL);
exit(1); exit(1);
} }
@ -355,24 +379,32 @@ case 'write':
// Write needs exactly one payload source: // Write needs exactly one payload source:
// either SETTING (key=value) or --in (JSON file). // either SETTING (key=value) or --in (JSON file).
// This avoids ambiguous input precedence and empty writes. // This avoids ambiguous input precedence and empty writes.
if ($inputFile && $settings['key'] !== '') { if ($inputFile && $settings['key'] !== '')
alejandro.sosa marked this conversation as resolved Outdated

probably this one too?

probably this one too?
{
fwrite(STDERR, 'Please use either SETTING or --in, not both.'.PHP_EOL); fwrite(STDERR, 'Please use either SETTING or --in, not both.'.PHP_EOL);
exit(1); exit(1);
} }
if (!$inputFile && $settings['key'] === '') { if (!$inputFile && $settings['key'] === '')
{
fwrite(STDERR, 'Nothing to write: provide SETTING or --in.'.PHP_EOL); fwrite(STDERR, 'Nothing to write: provide SETTING or --in.'.PHP_EOL);
exit(1); exit(1);
} }
$path = ($settings['key'] !== '') ? explode(':', $settings['key']) : []; $path = ($settings['key'] !== '') ? explode(':', $settings['key']) : [];
if ($inputFile) { if ($inputFile)
try { {
try
{
$setting2write = readJsonInputFile($inputFile); $setting2write = readJsonInputFile($inputFile);
} catch (\Throwable $e) { }
catch (\Throwable $e)
{
fwrite(STDERR, $e->getMessage().PHP_EOL); fwrite(STDERR, $e->getMessage().PHP_EOL);
exit(1); exit(1);
} }
} else { }
else
{
$setting2write = $settings['value']; $setting2write = $settings['value'];
} }
@ -390,14 +422,16 @@ case 'write':
} }
$targetDir = dirname($file); $targetDir = dirname($file);
if (!is_dir($targetDir) && !mkdir($targetDir, 0775, true) && !is_dir($targetDir)) { if (!is_dir($targetDir) && !mkdir($targetDir, 0775, true) && !is_dir($targetDir))
{
fwrite(STDERR, "Can not create directory: $targetDir".PHP_EOL); fwrite(STDERR, "Can not create directory: $targetDir".PHP_EOL);
exit(1); exit(1);
} }
$writeCfg = $cfg->create($setting2write); $writeCfg = $cfg->create($setting2write);
// var_dump($writeCfg->toArray()); // var_dump($writeCfg->toArray());
try { try
{
(new SettingsWriter($writeCfg, '', $writeType))->write(); (new SettingsWriter($writeCfg, '', $writeType))->write();
echo "Written modified settings to: $file".PHP_EOL; echo "Written modified settings to: $file".PHP_EOL;
} }