Initial Commit

This commit is contained in:
Netkas 2023-02-23 13:11:50 -05:00
commit bda571fa77
7 changed files with 722 additions and 0 deletions

View file

@ -0,0 +1,376 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace ConfigLib;
use Exception;
use LogLib\Log;
use ncc\Runtime;
use RuntimeException;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
class Configuration
{
/**
* The name of the configuration
*
* @var string
*/
private $Name;
/**
* The path to the configuration file
*
* @var string
*/
private $Path;
/**
* The configuration data
*
* @var array
*/
private $Configuration;
/**
* Indicates if the current instance is modified
*
* @var bool
*/
private $Modified;
/**
* Public Constructor
*
* @param string $name The name of the configuration (e.g. "MyApp" or "net.example.myapp")
*/
public function __construct(string $name='default')
{
// Sanitize $name for file path
$name = strtolower($name);
$name = str_replace('/', '_', $name);
$name = str_replace('\\', '_', $name);
$name = str_replace('.', '_', $name);
// Figure out the path to the configuration file
try
{
/** @noinspection PhpUndefinedClassInspection */
$this->Path = Runtime::getDataPath('net.nosial.configlib') . DIRECTORY_SEPARATOR . $name . '.conf';
}
catch (Exception $e)
{
throw new RuntimeException('Unable to load package "net.nosial.configlib"', $e);
}
// Set the name
$this->Name = $name;
// Default Configuration
$this->Modified = false;
if(file_exists($this->Path))
{
try
{
$this->load(true);
}
catch(Exception $e)
{
Log::error('net.nosial.configlib', sprintf('Unable to load configuration "%s", %s', $this->Name, $e->getMessage()));
throw new RuntimeException(sprintf('Unable to load configuration "%s"', $this->Name), $e);
}
}
else
{
$this->Configuration = [];
}
}
/**
* Validates a key syntax (e.g. "key1.key2.key3")
*
* @param string $input
* @return bool
*/
private static function validateKey(string $input): bool
{
$pattern = '/^([a-zA-Z]+\.?)+$/';
if (preg_match($pattern, $input))
return true;
return false;
}
/**
* Attempts to convert a string to the correct type (int, float, bool, string)
*
* @param $input
* @return float|int|mixed|string
* @noinspection PhpUnusedPrivateMethodInspection
*/
private static function cast($input): mixed
{
if (is_numeric($input))
{
if (str_contains($input, '.'))
return (float)$input;
if (ctype_digit($input))
return (int)$input;
}
elseif (in_array(strtolower($input), ['true', 'false']))
{
return filter_var($input, FILTER_VALIDATE_BOOLEAN);
}
return (string)$input;
}
/**
* Returns a value from the configuration
*
* @param string $key The key to retrieve (e.g. "key1.key2.key3")
* @param mixed|null $default The default value to return if the key is not found
* @return mixed The value of the key or the default value
* @noinspection PhpUnused
*/
public function get(string $key, mixed $default=null): mixed
{
if(!self::validateKey($key))
return $default;
$path = explode('.', $key);
$current = $this->Configuration;
foreach ($path as $key)
{
if (is_array($current) && array_key_exists($key, $current))
{
$current = $current[$key];
}
else
{
return $default;
}
}
// Return the value at the end of the path
return $current;
}
/**
* Sets a value in the configuration
*
* @param string $key The key to set (e.g. "key1.key2.key3")
* @param mixed $value The value to set
* @param bool $create If true, the key will be created if it does not exist
* @return bool True if the value was set, false otherwise
*/
public function set(string $key, mixed $value, bool $create=false): bool
{
if(!self::validateKey($key))
return false;
$path = explode('.', $key);
$current = &$this->Configuration;
// Navigate to the parent of the value to set
foreach ($path as $key)
{
if (is_array($current) && array_key_exists($key, $current))
{
$current = &$current[$key];
}
else
{
if ($create)
{
$current[$key] = [];
$current = &$current[$key];
}
else
{
return false;
}
}
}
// Set the value
$current = $value;
$this->Modified = true;
return true;
}
/**
* Sets the default value for a key if it does not exist
*
* @param string $key
* @param mixed $value
* @return bool
*/
public function setDefault(string $key, mixed $value): bool
{
if($this->exists($key))
return false;
return $this->set($key, $value, true);
}
/**
* Checks if the given key exists in the configuration
*
* @param string $key
* @return bool
*/
public function exists(string $key): bool
{
if(!self::validateKey($key))
return false;
if(!isset($this->Configuration[$key]))
return false;
$path = explode('.', $key);
$current = $this->Configuration;
foreach ($path as $key)
{
if (is_array($current) && array_key_exists($key, $current))
{
$current = $current[$key];
}
else
{
return false;
}
}
return true;
}
/**
* Clears the current configuration data
*
* @return void
* @noinspection PhpUnused
*/
public function clear(): void
{
$this->Configuration = [];
}
/**
* Saves the Configuration File to the disk
*
* @return void
* @throws Exception
*/
public function save(): void
{
if (!$this->Modified)
return;
$json = json_encode($this->Configuration, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$fs = new Filesystem();
try
{
$fs->dumpFile($this->Path, $json);
}
catch (IOException $e)
{
throw new Exception('Unable to write configuration file', $e);
}
$this->Modified = false;
Log::debug('net.nosial.configlib', sprintf('Configuration "%s" saved', $this->Name));
}
/**
* Loads the Configuration File from the disk
*
* @param bool $force
* @return void
* @throws Exception
* @noinspection PhpUnused
*/
public function load(bool $force=false): void
{
if (!$force && !$this->Modified)
return;
$fs = new Filesystem();
if (!$fs->exists($this->Path))
return;
try
{
$json = file_get_contents($this->Path);
}
catch (IOException $e)
{
throw new Exception('Unable to read configuration file', $e);
}
$this->Configuration = json_decode($json, true);
$this->Modified = false;
Log::debug('net.nosial.configlib', 'Loaded configuration file: ' . $this->Path);
}
/**
* Returns the name of the configuration
*
* @return string
* @noinspection PhpUnused
*/
public function getName(): string
{
return $this->Name;
}
/**
* Returns the path of the configuration file on disk
*
* @return string
*/
public function getPath(): string
{
return $this->Path;
}
/**
* @return array
* @noinspection PhpUnused
*/
public function getConfiguration(): array
{
return $this->Configuration;
}
/**
* Public Destructor
*/
public function __destruct()
{
if($this->Modified)
{
try
{
$this->save();
}
catch(Exception $e)
{
Log::error('net.nosial.configlib', sprintf('Unable to save configuration "%s" to disk, %s', $this->Name, $e->getMessage()));
}
}
}
}

213
src/ConfigLib/Program.php Normal file
View file

@ -0,0 +1,213 @@
<?php
namespace ConfigLib;
use Exception;
use OptsLib\Parse;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\Process;
class Program
{
/**
* Main entry point of the program
*
* @return void
*/
public static function main(): void
{
$args = Parse::getArguments();
if(isset($args['help']) || isset($args['h']))
self::help();
if(isset($args['version']) || isset($args['v']))
self::version();
if(isset($args['name']) || isset($args['n']))
{
$configuration_name = $args['name'] ?? $args['n'] ?? null;
$property = $args['property'] ?? $args['p'] ?? null;
$value = $args['value'] ?? $args['v'] ?? null;
$editor = $args['editor'] ?? $args['e'] ?? null;
if($configuration_name === null)
{
print('You must specify a configuration name' . PHP_EOL);
exit(1);
}
$configuration = new Configuration($configuration_name);
if($editor !== null)
{
self::edit($args);
return;
}
if($property === null)
{
print(json_encode($configuration->getConfiguration(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL);
}
else
{
if($value === null)
{
print(json_encode($configuration->get($property, '(not set)'), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL);
return;
}
$configuration->set($property, $value);
try
{
$configuration->save();
}
catch (Exception $e)
{
print($e->getMessage() . PHP_EOL);
exit(1);
}
}
return;
}
self::help();
}
/**
* Prints out the Help information for the program
*
* @return void
*/
private static function help(): void
{
print('Usage: configlib [options]' . PHP_EOL);
print(' -h, --help Displays the help menu' . PHP_EOL);
print(' -v, --version Displays the version of the program' . PHP_EOL);
print(' -n, --name <name> The name of the configuration' . PHP_EOL);
print(' -p, --path <path> The property name to select/read (eg; foo.bar.baz) (Inline)' . PHP_EOL);
print(' -v, --value <value> The value to set the property (Inline)' . PHP_EOL);
print(' -e, --editor <editor> (Optional) The editor to use (eg; nano, vim, notepad) (External)' . PHP_EOL);
print(' --nc (Optional) Disables type casting (eg; \'true\' > True) will always be a string' . PHP_EOL);
print(' --export <file> (Optional) Exports the configuration to a file' . PHP_EOL);
print(' --import <file> (Optional) Imports the configuration from a file' . PHP_EOL);
print('Examples:' . PHP_EOL);
print(' configlib -n com.example.package' . PHP_EOL);
print(' configlib -n com.example.package -e nano' . PHP_EOL);
print(' configlib -n com.example.package -p foo.bar.baz -v 123' . PHP_EOL);
print(' configlib -n com.example.package -p foo.bar.baz -v 123 --nc' . PHP_EOL);
print(' configlib -n com.example.package --export config.json' . PHP_EOL);
print(' configlib -n com.example.package --import config.json' . PHP_EOL);
exit(0);
}
/**
* Edits an existing configuration file or creates a new one if it doesn't exist
*
* @param array $args
* @return void
*/
private static function edit(array $args): void
{
$editor = $args['editor'] ?? 'vi';
if(isset($args['e']))
$editor = $args['e'];
$name = $args['name'] ?? 'default';
if($editor == null)
{
print('No editor specified' . PHP_EOL);
exit(1);
}
// Determine the temporary path to use
$tempPath = null;
if(function_exists('ini_get'))
{
$tempPath = ini_get('upload_tmp_dir');
if($tempPath == null)
$tempPath = ini_get('session.save_path');
if($tempPath == null)
$tempPath = ini_get('upload_tmp_dir');
if($tempPath == null)
$tempPath = sys_get_temp_dir();
}
if($tempPath == null && function_exists('sys_get_temp_dir'))
$tempPath = sys_get_temp_dir();
if($tempPath == null)
{
print('Unable to determine the temporary path to use' . PHP_EOL);
exit(1);
}
// Prepare the temporary file
try
{
$configuration = new Configuration($name);
}
catch (Exception $e)
{
print($e->getMessage() . PHP_EOL);
exit(1);
}
$fs = new Filesystem();
$tempFile = $tempPath . DIRECTORY_SEPARATOR . $name . '.conf';
$fs->copy($configuration->getPath(), $tempFile);
$original_hash = hash_file('sha1', $tempFile);
// Open the editor
try
{
$process = new Process([$editor, $tempFile]);
$process->setTimeout(0);
$process->setTty(true);
$process->run();
}
catch(Exception $e)
{
print('Unable to open the editor, ' . $e->getMessage() . PHP_EOL);
exit(1);
}
finally
{
$fs->remove($tempFile);
}
// Check if the file has changed and if so, update the configuration
if($fs->exists($tempFile))
{
$new_hash = hash_file('sha1', $tempFile);
if($original_hash != $new_hash)
{
$fs->copy($tempFile, $configuration->getPath());
}
else
{
print('No changes detected' . PHP_EOL);
}
$fs->remove($tempFile);
}
}
/**
* Prints out the version of the program
*
* @return void
*/
private static function version(): void
{
print('ConfigLib v1.0.0' . PHP_EOL);
exit(0);
}
}