Skip to content

Commit

Permalink
Final RC.5 Update
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesgober committed Dec 5, 2024
1 parent e402f99 commit f0e993d
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 35 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
},
"autoload-dev": {
"psr-4": {
"JG\\Config\\": "tests/"
"JG\\Tests\\": "tests/"
}
},
"scripts": {
Expand Down
40 changes: 28 additions & 12 deletions src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* @link https://github.com/jamesgober/Config
* @license MIT License
* @copyright 2024 James Gober (https://jamesgober.com)
*/
*/
declare(strict_types=1);

namespace JG\Config;
Expand Down Expand Up @@ -346,16 +346,26 @@ public function loadFromStream(StreamInterface $stream): bool
protected function parse(?string $filePath = null): ?array
{
$filePath = $this->resolvePath($filePath);

if (!$filePath || !is_file($filePath)) {
throw new ConfigException("Configuration file not found: {$filePath}");
}


$contents = file_get_contents($filePath);
if ($contents === false) {
throw new ConfigException("Failed to read configuration file: {$filePath}");
}

// Validate UTF-8 encoding
if (!mb_check_encoding($contents, 'UTF-8')) {
throw new ConfigException("File encoding must be UTF-8: {$filePath}");
}

$parser = ConfigParserFactory::createParser($filePath);
if (!$parser) {
throw new ConfigException("No suitable parser found for: {$filePath}");
}

return $parser->parse($filePath);
}

Expand Down Expand Up @@ -538,26 +548,32 @@ public function loadCache(string $filePath): bool
if ($this->cacheLoaded) {
return true;
}

if (!is_file($filePath) || !is_readable($filePath)) {
return false;
}

$data = json_decode(file_get_contents($filePath), true, 512, JSON_THROW_ON_ERROR);

if (!is_array($data['config']) || !is_array($data['groups'])) {

try {
$data = json_decode(file_get_contents($filePath), true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
unlink($filePath); // Cleanup malformed cache
return false;
}

if (!isset($data['config'], $data['groups']) || !is_array($data['config']) || !is_array($data['groups'])) {
unlink($filePath); // Cleanup malformed cache
throw new ConfigException("Invalid cache format: 'config' and 'groups' must be arrays.");
}

if (isset($data['expires']) && $data['expires'] > 0 && time() > $data['expires']) {
$this->deleteCache($filePath);
return false;
}

$this->config = $data['config'];
$this->groups = $data['groups'];
$this->cacheLoaded = true;

return true;
}

Expand Down
5 changes: 5 additions & 0 deletions src/ConfigParserFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ public static function registerParser(string $extension, string $parserClass): v
if (empty($extension)) {
throw new InvalidParserException("File extension cannot be empty.");
}
// Validate and instantiate the parser class
if ($parserClass && is_a($parserClass, ParserInterface::class, true)) {
self::$configParsers[$extension] = $parserClass;
return;
}

if (!is_a($parserClass, ParserInterface::class, true)) {
throw new InvalidParserException(
Expand Down
60 changes: 43 additions & 17 deletions tests/ConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

use JG\Config\Config;
use PHPUnit\Framework\TestCase;
use JG\Config\ConfigParserFactory;
use JG\Config\Exceptions\ConfigException;

class ConfigTest extends TestCase

{
public function testSetConfigPathValid(): void
{
Expand Down Expand Up @@ -210,17 +212,24 @@ public function testMaxDepthExceeded(): void
$config = new Config();
$config->setMaxDepth(2);

$this->expectException(ConfigException::class);
$nestedConfig = [
'level1' => [
'level2' => [
'level3' => [
'level3' => [ // This should exceed max depth
'key' => 'value',
],
],
],
];
$config->add('nested', $nestedConfig);

$this->expectException(ConfigException::class);

// Use reflection to call the protected `flattenArray` directly
$reflection = new \ReflectionClass($config);
$method = $reflection->getMethod('flattenArray');
$method->setAccessible(true);

$method->invoke($config, $nestedConfig, 'nested');
}

public function testEmptyGroupHandling(): void
Expand All @@ -236,30 +245,41 @@ public function testEmptyGroupHandling(): void
public function testNonUtf8File(): void
{
$config = new Config(__DIR__ . '/config');

$filePath = __DIR__ . '/config/non_utf8.json';
file_put_contents($filePath, mb_convert_encoding('{"key":"value"}', 'ISO-8859-1'));

// Simulate non-UTF-8 file
$data = '{"key":"value"}';
file_put_contents($filePath, mb_convert_encoding($data, 'ISO-8859-1'));

// Mock the encoding check
$mockedFilePath = __DIR__ . '/config/mocked_non_utf8.json';
file_put_contents($mockedFilePath, "\x80\x81\x82"); // Invalid UTF-8 sequence

$this->expectException(ConfigException::class);
$config->load('non_utf8.json');
$this->expectExceptionMessage("File encoding must be UTF-8");

// Force the parser to encounter the mocked data
$config->load(basename($mockedFilePath));

// Cleanup
unlink($filePath);
}

public function testFetchWithCustomParser(): void
public function setUp(): void
{
$config = new Config();
ConfigParserFactory::registerParser('custom', CustomParser::class);

$filePath = __DIR__ . '/config/custom_file.custom';
file_put_contents($filePath, "customKey:customValue");
parent::setUp();

$result = $config->fetch($filePath);
$this->assertEquals(['customKey' => 'customValue'], $result);
ConfigParserFactory::registerParser('custom', \JG\Tests\CustomParser::class);
}

public function testCustomParser(): void
{
$config = new Config(__DIR__ . '/config/', false);
$config->load('config.custom');

// Cleanup
unlink($filePath);
$this->assertEquals('value1', $config->get('key1'));
$this->assertEquals('value2', $config->get('key2'));
}

public function testInvalidCacheStructure(): void
Expand All @@ -285,7 +305,13 @@ public function testLoadPerformance(): void
for ($i = 0; $i < 10000; $i++) {
$largeConfig["key{$i}"] = "value{$i}";
}
$config->insert($largeConfig);

// Use reflection to access the protected `insert` method
$reflection = new \ReflectionClass($config);
$method = $reflection->getMethod('insert');
$method->setAccessible(true);

$method->invoke($config, $largeConfig);

$this->assertEquals('value9999', $config->get('key9999'));
}
Expand Down
47 changes: 47 additions & 0 deletions tests/CustomParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace JG\Tests;

use JG\Config\Parsers\ParserInterface;
use JG\Config\Exceptions\ConfigParseException;

class CustomParser implements ParserInterface
{
/**
* Parses a custom configuration file into an associative array.
*
* @param string $filePath Path to the custom configuration file.
* @return array Parsed configuration data as an associative array.
* @throws ConfigParseException If the file cannot be read or parsed.
*/
public function parse(string $filePath): array
{
if (!is_file($filePath) || !is_readable($filePath)) {
throw new ConfigParseException("File not found or unreadable: {$filePath}");
}

$data = file_get_contents($filePath);
if ($data === false) {
throw new ConfigParseException("Failed to read the custom configuration file: {$filePath}");
}

$lines = explode(PHP_EOL, $data);
$output = [];

foreach ($lines as $line) {
$line = trim($line);

if (empty($line) || str_starts_with($line, '#')) {
continue; // Skip empty lines or comments
}

if (!str_contains($line, '->')) {
throw new ConfigParseException("Invalid line format in file {$filePath}: {$line}");
}

[$key, $value] = array_map('trim', explode('->', $line, 2));
$output[$key] = $value;
}
return $output;
}
}
5 changes: 1 addition & 4 deletions tests/Parsers/XmlParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@ public function testXmlParser(): void
$parser = new XmlParser();

$result = $parser->parse($filePath);

// Debugging the result
var_dump($result);


$this->assertIsArray($result);
$this->assertArrayHasKey('app', $result);
$this->assertArrayHasKey('name', $result['app']);
Expand Down
1 change: 0 additions & 1 deletion tests/cache.json

This file was deleted.

2 changes: 2 additions & 0 deletions tests/config/config.custom
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
key1 -> value1
key2 -> value2
1 change: 1 addition & 0 deletions tests/config/mocked_non_utf8.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
���
1 change: 1 addition & 0 deletions tests/config/non_utf8.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"key":"value"}

0 comments on commit f0e993d

Please sign in to comment.