Add Index ConfigManager implementation.

This commit is contained in:
flash 2018-01-02 21:26:33 +01:00
parent b8b2a0561b
commit 8c4c1a81d9
5 changed files with 547 additions and 1 deletions

82
src/Application.php Normal file
View file

@ -0,0 +1,82 @@
<?php
namespace Misuzu;
use Misuzu\Config\ConfigManager;
class Application
{
private static $instance = null;
public static function getInstance(): Application
{
if (is_null(static::$instance) || !(static::$instance instanceof Application)) {
throw new \Exception('Invalid instance type.');
}
return static::$instance;
}
public static function start(): Application
{
if (!is_null(static::$instance) || static::$instance instanceof Application) {
throw new \Exception('An Application has already been set up.');
}
static::$instance = new Application;
return static::getInstance();
}
private $templating = null;
private $configuration = null;
protected function __construct()
{
ExceptionHandler::register();
$this->templating = new TemplateEngine;
echo 'hello!';
}
public function __destruct()
{
ExceptionHandler::unregister();
}
public function debug(bool $mode): void
{
ExceptionHandler::debug($mode);
if ($this->hasTemplating()) {
$this->getTemplating()->debug($mode);
}
}
public function hasTemplating(): bool
{
return !is_null($this->templating) && $this->templating instanceof TemplateEngine;
}
public function getTemplating(): TemplateEngine
{
if (!$this->hasTemplating()) {
throw new \Exception('No TemplateEngine instance is available.');
}
return $this->templating;
}
public function hasConfig(): bool
{
return !is_null($this->configuration) && $this->configuration instanceof ConfigManager;
}
public function getConfig(): ConfigManager
{
if (!$this->hasConfig()) {
throw new \Exception('No ConfigManager instance is available.');
}
return $this->configuration;
}
}

View file

@ -0,0 +1,245 @@
<?php
namespace Misuzu\Config;
use Misuzu\IO\File;
use Misuzu\IO\FileStream;
/**
* Handles parsing, reading and setting configuration files.
* @package Aitemu\Config
* @author flashwave <me@flash.moe>
*/
class ConfigManager
{
/**
* Holds the key collection pairs for the sections.
* @var array
*/
private $collection = [];
private $filename = null;
/**
* Creates a file object with the given path and reloads the context.
* @param string $filename
*/
public function __construct(?string $filename = null)
{
if (empty($filename)) {
return;
}
$this->filename = $filename;
if (File::exists($this->filename)) {
$this->load();
} else {
$this->save();
}
}
/**
* Checks if a section or key exists in the config.
* @param string $section
* @param string $key
* @return bool
*/
public function contains(string $section, ?string $key = null): bool
{
if ($key !== null) {
return $this->contains($section) && array_key_exists($key, $this->collection[$section]);
}
return array_key_exists($section, $this->collection);
}
/**
* Removes a section or key and saves.
* @param string $section
* @param string $key
*/
public function remove(string $section, ?string $key = null): void
{
if ($key !== null && $this->contains($section, $key)) {
if (count($this->collection[$section]) < 2) {
$this->remove($section);
return;
}
unset($this->collection[$section][$key]);
} elseif ($this->contains($section)) {
unset($this->collection[$section]);
}
}
/**
* Gets a value from a section in the config.
* @param string $section
* @param string $key
* @param string $type
* @param string $fallback
* @return mixed
*/
public function get(string $section, string $key, string $type = 'string', ?string $fallback = null)
{
$value = null;
if (!$this->contains($section, $key)) {
$this->set($section, $key, $fallback);
return $fallback;
}
$raw = $this->collection[$section][$key];
switch (strtolower($type)) {
case "bool":
case "boolean":
$value = strlen($raw) > 0 && ($raw[0] === '1' || strtolower($raw) === "true");
break;
case "int":
case "integer":
$value = intval($raw);
break;
case "float":
case "double":
$value = floatval($raw);
break;
default:
$value = $raw;
break;
}
return $value;
}
/**
* Sets a configuration value and immediately saves it.
* @param string $section
* @param string $key
* @param mixed $value
*/
public function set(string $section, string $key, $value): void
{
$type = gettype($value);
$store = null;
switch (strtolower($type)) {
case 'boolean':
$store = $value ? '1' : '0';
break;
default:
$store = (string)$value;
break;
}
if (!$this->contains($section)) {
$this->collection[$section] = [];
}
$this->collection[$section][$key] = $store;
}
/**
* Writes the serialised config to file.
*/
public function save(): void
{
if (!empty($this->filename)) {
static::write($this->filename, $this->collection);
}
}
/**
* Calls for a parse of the contents of the config file.
*/
public function load(): void
{
if (!empty($this->filename)) {
$this->collection = static::read($this->filename);
}
}
/**
* Serialises the $this->collection array to the human readable config format.
* @return string
*/
public static function write(string $filename, array $collection): void
{
$file = new FileStream($filename, FileStream::MODE_TRUNCATE, true);
$file->write(sprintf('; Saved on %s%s', date('Y-m-d H:i:s e'), PHP_EOL));
foreach ($collection as $name => $entries) {
if (count($entries) < 1) {
continue;
}
$file->write(sprintf('%1$s[%2$s]%1$s', PHP_EOL, $name));
foreach ($entries as $key => $value) {
$file->write(sprintf('%s = %s%s', $key, $value, PHP_EOL));
}
}
$file->flush();
$file->close();
}
/**
* Parses the config file.
* @param string $config
*/
private static function read(string $filename): array
{
$collection = [];
$section = null;
$key = null;
$value = null;
$file = new FileStream($filename, FileStream::MODE_READ);
$lines = explode("\n", $file->read($file->length));
$file->close();
foreach ($lines as $line) {
$line = trim($line, "\r\n");
$length = strlen($line);
if ($length < 1
|| starts_with($line, '#')
|| starts_with($line, ';')
|| starts_with($line, '//')
) {
continue;
}
if (starts_with($line, '[') && ends_with($line, ']')) {
$section = rtrim(ltrim($line, '['), ']');
if (!isset($collection[$section])) {
$collection[$section] = [];
}
continue;
}
if (strpos($line, '=') !== false) {
$split = explode('=', $line, 2);
if (count($split) < 2) {
continue;
}
$key = trim($split[0]);
$value = trim($split[1]);
if (strlen($key) > 0 && strlen($value) > 0) {
$collection[$section][$key] = $value;
}
}
}
return $collection;
}
}

151
src/ExceptionHandler.php Normal file
View file

@ -0,0 +1,151 @@
<?php
namespace Misuzu;
use ErrorException;
use Throwable;
/**
* Handles displaying and reporting exceptions after being registered.
* @package Misuzu
* @author flashwave <me@flash.moe>
*/
class ExceptionHandler
{
/**
* Url to which report objects will be POSTed.
* @var string
*/
private static $reportUrl = null;
/**
* Whether debug mode is active.
* If true (or in CLI) a backtrace will be displayed.
* If false a user friendly, non-exposing error page will be displayed.
* @var bool
*/
private static $debugMode = false;
/**
* Internal bool used to prevent an infinite loop when the templating engine is not available.
* @var bool
*/
private static $failSafe = false;
/**
* Registers the exception handler and make it so all errors are thrown as ErrorExceptions.
*/
public static function register(): void
{
set_exception_handler([static::class, 'exception']);
set_error_handler([static::class, 'error']);
}
/**
* Same as above except unregisters
*/
public static function unregister(): void
{
restore_exception_handler();
restore_error_handler();
}
/**
* Turns debug mode on or off.
* @param bool $mode
*/
public static function debug(bool $mode): void
{
static::$debugMode = $mode;
}
/**
* The actual handler for rendering and reporting exceptions.
* Checks if the exception is extends on HttpException,
* if not an attempt will be done to report it.
* @param Throwable $exception
*/
public static function exception(Throwable $exception): void
{
$is_http = is_subclass_of($exception, HttpException::class);
$report = !static::$debugMode && !$is_http && static::$reportUrl !== null;
if ($report) {
static::report($exception);
}
static::render($exception, $report);
}
/**
* Converts regular errors to ErrorException instances.
* @param int $severity
* @param string $message
* @param string $file
* @param int $line
* @throws ErrorException
*/
public static function error(int $severity, string $message, string $file, int $line): void
{
throw new ErrorException($message, 0, $severity, $file, $line);
}
/**
* Shoots a POST request to the report URL.
* @todo Implement this (depends on Aitemu\Net\WebClient).
* @param Throwable $exception
*/
private static function report(Throwable $exception): void
{
// send POST request with json encoded exception to destination
}
/**
* Renders exceptions.
* In debug or cli mode a backtrace is displayed.
* Otherwise if the error extends on HttpException the respective error code is set.
* If the View alias is still available the script will attempt to render a path 'errors/{error code}.twig'.
* @param Throwable $exception
* @param bool $reported
*/
private static function render(Throwable $exception, bool $reported): void
{
$in_cli = PHP_SAPI === 'cli';
$is_http = false;//$exception instanceof HttpException;
if ($in_cli || (!$is_http && static::$debugMode)) {
if (!$in_cli) {
http_response_code(500);
header('Content-Type: text/plain');
}
echo $exception;
return;
}
$http_code = $is_http ? $exception->httpCode : 500;
http_response_code($http_code);
static::$failSafe = true;
/*if (!static::$failSafe && View::available()) {
static::$failSafe = true;
$template = "errors.{$http_code}";
$namespace = View::findNamespace($template);
if ($namespace !== null) {
echo View::render("@{$namespace}.{$template}", compact('reported'));
return;
}
}*/
if ($is_http) {
echo "Error {$http_code}";
return;
}
echo "Something broke!";
if ($reported) {
echo "<br>The error has been reported to the developers.";
}
}
}

67
tests/ConfigTest.php Normal file
View file

@ -0,0 +1,67 @@
<?php
namespace MisuzuTests;
use PHPUnit\Framework\TestCase;
use Misuzu\Config\ConfigManager;
define('CONFIG_FILE', sys_get_temp_dir() . '/MisuzuConfigTest' . time() . '.ini');
class ConfigTest extends TestCase
{
public function testMemoryConfigManager()
{
$config = new ConfigManager();
$this->assertInstanceOf(ConfigManager::class, $config);
$config->set('TestCat', 'string_val', 'test', 'string');
$config->set('TestCat', 'int_val', 25, 'int');
$config->set('TestCat', 'bool_val', true, 'bool');
$this->assertEquals('test', $config->get('TestCat', 'string_val', 'string'));
$this->assertEquals(25, $config->get('TestCat', 'int_val', 'int'));
$this->assertEquals(true, $config->get('TestCat', 'bool_val', 'bool'));
}
public function testConfigCreateSet()
{
$config = new ConfigManager(CONFIG_FILE);
$this->assertInstanceOf(ConfigManager::class, $config);
$config->set('TestCat', 'string_val', 'test', 'string');
$config->set('TestCat', 'int_val', 25, 'int');
$config->set('TestCat', 'bool_val', true, 'bool');
$this->assertEquals('test', $config->get('TestCat', 'string_val', 'string'));
$this->assertEquals(25, $config->get('TestCat', 'int_val', 'int'));
$this->assertEquals(true, $config->get('TestCat', 'bool_val', 'bool'));
$config->save();
}
public function testConfigReadGet()
{
$config = new ConfigManager(CONFIG_FILE);
$this->assertInstanceOf(ConfigManager::class, $config);
$this->assertEquals('test', $config->get('TestCat', 'string_val', 'string'));
$this->assertEquals(25, $config->get('TestCat', 'int_val', 'int'));
$this->assertEquals(true, $config->get('TestCat', 'bool_val', 'bool'));
}
public function testConfigRemove()
{
$config = new ConfigManager(CONFIG_FILE);
$this->assertInstanceOf(ConfigManager::class, $config);
$this->assertTrue($config->contains('TestCat', 'string_val'));
$config->remove('TestCat', 'string_val');
$config->save();
$config->load();
$this->assertFalse($config->contains('TestCat', 'string_val'));
// tack this onto here, deletes the entire file because we're done with it
\Misuzu\IO\File::delete(CONFIG_FILE);
}
}

View file

@ -45,7 +45,8 @@ class FileSystemTest extends TestCase
$file = new FileStream(WORKING_DIR . '/file', FileStream::MODE_TRUNCATE);
$this->assertInstanceOf(FileStream::class, $file);
$file->write('misuzu');
$file->write('mis');
$file->write('uzu');
$this->assertEquals(6, $file->length);
$file->close();