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

18
LICENSE Normal file
View file

@ -0,0 +1,18 @@
Copyright 2022-2023 Nosial All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

12
README.md Normal file
View file

@ -0,0 +1,12 @@
# ConfigLib
ConfigLib is a PHP library for managing configuration files and storing it
in NCC's data, while providing a command line interface for running functions
such as editing configuration files inline or importing/exporting
configuration files.
One of the biggest advantages of using something like ConfigLib is that
it will allow for more complicated software to be configured more easily
by following the documented instructions on how to alter configuration
files, optionally you could use a builtin editor to edit the configuration
file manually.

7
main Normal file
View file

@ -0,0 +1,7 @@
<?php
require('ncc');
import('net.nosial.configlib', 'latest');
\ConfigLib\Program::main();

84
project.json Normal file
View file

@ -0,0 +1,84 @@
{
"project": {
"compiler": {
"extension": "php",
"minimum_version": "8.0",
"maximum_version": "8.2"
},
"update_source": {
"source": "nosial/libs.config@n64",
"repository": {
"name": "n64",
"type": "gitlab",
"host": "git.n64.cc",
"ssl": true
}
},
"options": {
"create_symlink": true
}
},
"execution_policies":[
{
"name": "main",
"runner": "php",
"execute": {
"target": "main",
"working_directory": "%CWD%",
"tty": true
}
}
],
"assembly": {
"name": "ConfigLib",
"package": "net.nosial.configlib",
"company": "Nosial",
"copyright": "Copyright (c) 2022-2023 Nosial",
"description": "ConfigLib is a library for reading and writing configuration files via the NCC Runtime API",
"version": "1.0.0",
"uuid": "9347259e-8e4d-11ed-85a7-fd07cf28ef35"
},
"build": {
"source_path": "src",
"default_configuration": "release",
"main": "main",
"dependencies": [
{
"name": "net.nosial.optslib",
"version": "latest",
"source_type": "remote",
"source": "nosial/libs.opts=latest@n64"
},
{
"name": "net.nosial.loglib",
"version": "latest",
"source_type": "remote",
"source": "nosial/libs.log=latest@n64"
},
{
"name": "com.symfony.filesystem",
"version": "latest",
"source_type": "remote",
"source": "symfony/filesystem=latest@composer"
},
{
"name": "com.symfony.yaml",
"version": "latest",
"source_type": "remote",
"source": "symfony/yaml=latest@composer"
},
{
"name": "com.symfony.process",
"version": "latest",
"source_type": "remote",
"source": "symfony/process=latest@composer"
}
],
"configurations": [
{
"name": "release",
"output_path": "build/release"
}
]
}
}

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);
}
}

12
tests/default.php Normal file
View file

@ -0,0 +1,12 @@
<?php
require 'ncc';
import('net.nosial.configlib');
$config = new \ConfigLib\Configuration('test');
$config->setDefault('database.host', '127.0.0.1');
$config->setDefault('database.port', 3306);
$config->setDefault('database.username', 'root');
$config->setDefault('database.password', null);
$config->setDefault('database.name', 'test');