From 8c4c1a81d929115158486bc93f989b8351a73774 Mon Sep 17 00:00:00 2001 From: flashwave Date: Tue, 2 Jan 2018 21:26:33 +0100 Subject: [PATCH] Add Index ConfigManager implementation. --- src/Application.php | 82 ++++++++++++ src/Config/ConfigManager.php | 245 +++++++++++++++++++++++++++++++++++ src/ExceptionHandler.php | 151 +++++++++++++++++++++ tests/ConfigTest.php | 67 ++++++++++ tests/FileSystemTest.php | 3 +- 5 files changed, 547 insertions(+), 1 deletion(-) create mode 100644 src/Application.php create mode 100644 src/Config/ConfigManager.php create mode 100644 src/ExceptionHandler.php create mode 100644 tests/ConfigTest.php diff --git a/src/Application.php b/src/Application.php new file mode 100644 index 00000000..0bab6032 --- /dev/null +++ b/src/Application.php @@ -0,0 +1,82 @@ +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; + } +} diff --git a/src/Config/ConfigManager.php b/src/Config/ConfigManager.php new file mode 100644 index 00000000..459deb99 --- /dev/null +++ b/src/Config/ConfigManager.php @@ -0,0 +1,245 @@ + + */ +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; + } +} diff --git a/src/ExceptionHandler.php b/src/ExceptionHandler.php new file mode 100644 index 00000000..1d133feb --- /dev/null +++ b/src/ExceptionHandler.php @@ -0,0 +1,151 @@ + + */ +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 "
The error has been reported to the developers."; + } + } +} diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php new file mode 100644 index 00000000..cc56f102 --- /dev/null +++ b/tests/ConfigTest.php @@ -0,0 +1,67 @@ +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); + } +} diff --git a/tests/FileSystemTest.php b/tests/FileSystemTest.php index 96c3cd5a..0595a022 100644 --- a/tests/FileSystemTest.php +++ b/tests/FileSystemTest.php @@ -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();