Add Index ConfigManager implementation.
This commit is contained in:
parent
b8b2a0561b
commit
8c4c1a81d9
5 changed files with 547 additions and 1 deletions
82
src/Application.php
Normal file
82
src/Application.php
Normal 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;
|
||||
}
|
||||
}
|
245
src/Config/ConfigManager.php
Normal file
245
src/Config/ConfigManager.php
Normal 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
151
src/ExceptionHandler.php
Normal 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
67
tests/ConfigTest.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Reference in a new issue