Skip to content

Commit

Permalink
enhance: support more features for ini parser
Browse files Browse the repository at this point in the history
  • Loading branch information
inhere committed Nov 6, 2021
1 parent dc571ff commit 8ca2262
Show file tree
Hide file tree
Showing 2 changed files with 252 additions and 67 deletions.
266 changes: 200 additions & 66 deletions app/Lib/Parser/IniParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@

use Toolkit\Stdlib\Str;
use function explode;
use function implode;
use function is_array;
use function is_numeric;
use function is_string;
use function ltrim;
use function preg_match;
use function rtrim;
Expand All @@ -15,47 +14,135 @@
use function strlen;
use function substr;
use function trim;
use function vdump;

/**
* class IniParser
*/
class IniParser
{
// public bool $parseBool = false;

/**
* current parsed section name.
*
* @var string
*/
private string $sectionName = '';

/**
* @var string
*/
private string $multiLineKey = '';

/**
* @var array
*/
private array $multiLineVal = [];

/**
* the source ini string.
*
* @var string
*/
private string $source;

/**
* parsed data
*
* @var array
*/
private array $data = [];

/**
* allow add interceptors do something before collect value
*
* @var array{callable(mixed, bool):mixed}
* @example an interceptor eg:
*
* ```php
* function (mixed $val, bool $isMultiLine): mixed {
* if ($val === 'SPECIAL') {
* // do something
* return 'Another value';
* }
*
* return $val;
* }
* ```
*/
private array $interceptors = [];

/**
* @param string $source
*
* @return static
*/
public static function new(string $source): self
{
return new self($source);
}

/**
* @param string $str
*
* @return array
*/
public static function parseString(string $str): array
{
return (new self())->parse($str);
return (new self($str))->parse();
}

/**
* parse ini string.
* Class constructor.
*
* @param string $source
*/
public function __construct(string $source)
{
$this->source = $source;
}

/**
* parse ini string
*
* - auto convert data type, eg: int, bool, float
* - ignores commented lines that start with ";" or "#"
* - ignores broken lines that do not have "="
* - supports array values and array value keys
* - enhance: supports inline array value
*
* @param string $str
* - enhance: supports multi inline string. use `'''` or `"""`
* - enhance: supports add interceptor before collect value
*
* @return array
* @url https://www.php.net/manual/en/function.parse-ini-string.php#111845
*/
public function parse(string $str): array
public function parse(): array
{
if (!$str = trim($str)) {
if (!$str = trim($this->source)) {
return [];
}

$ret = [];
$lines = explode("\n", $str);
// reset data.
$this->data = [];

$sectionName = '';
$lines = explode("\n", $str);
foreach ($lines as $line) {
// inside multi line
if ($this->multiLineKey) {
$trimmed = trim($line);

// multi line end.
if ($trimmed === '"""' || $trimmed === "'''") {
$this->collectValue($this->multiLineKey, implode("\n", $this->multiLineVal));
// reset tmp data.
$this->multiLineKey = '';
$this->multiLineVal = [];
} else {
$this->multiLineVal[] = $line;
}
continue;
}

// empty line
if (!$line = trim($line)) {
continue;
Expand All @@ -68,7 +155,7 @@ public function parse(string $str): array

// section line. eg: [arrayName]
if (strlen($line) > 3 && $line[0] === '[' && str_ends_with($line, ']')) {
$sectionName = substr($line, 1, -1);
$this->sectionName = substr($line, 1, -1);
continue;
}

Expand All @@ -81,63 +168,117 @@ public function parse(string $str): array
$key = rtrim($tmp[0]);
$val = ltrim($tmp[1]);

// inline array value. eg: tags=[abc, 234]
if ($val && $val[0] === '[' && str_ends_with($val, ']')) {
$val = Str::toTypedArray(substr($val, 1, - 1));
if ($val === '') {
$this->collectValue($key, $val);
continue;
}

// top field
if (!$sectionName) {
$ret[$key] = $val;
// multi line start.
if ($val === '"""' || $val === "'''") {
$this->multiLineKey = $key;
continue;
}

// in section. eg: [arrayName] -> $sectionName='arrayName'

// remove quote chars
if (
is_string($val) &&
(preg_match("/^\".*\"$/", $val) || preg_match("/^'.*'$/", $val))
) {
// inline array value. eg: tags=[abc, 234]
if ($val && $val[0] === '[' && str_ends_with($val, ']')) {
$val = Str::toTypedArray(substr($val, 1, -1));
} elseif (preg_match("/^\".*\"$/", $val) || preg_match("/^'.*'$/", $val)) {
// remove quote chars
$val = mb_substr($val, 1, -1);
} else {
// auto convert type
$val = Str::toTyped($val, true);
}

// is array sub key.
// eg:
// [] = "arr_elem_one"
// val_arr[] = "arr_elem_one"
// val_arr_two[some_key] = "some_key_value"
$ok = preg_match("/[\w-]{0,64}\[(.*?)]$/", $key, $matches);
if ($ok === 1 && isset($matches[0])) {
[$arrName, $subKey] = explode('[', trim($key, ']'));

if ($arrName !== '') {
if (!isset($ret[$sectionName][$arrName]) || !is_array($ret[$sectionName][$arrName])) {
$ret[$sectionName][$arrName] = [];
}

if ($subKey !== '') { // eg: val_arr[subKey] = "arr_elem_one"
$ret[$sectionName][$arrName][$subKey] = $val;
} else { // eg: val_arr[] = "arr_elem_one"
$ret[$sectionName][$arrName][] = $val;
}
} else {
if (!isset($ret[$sectionName]) || !is_array($ret[$sectionName])) {
$ret[$sectionName] = [];
}

if ($subKey !== '') { // eg: [subKey] = "arr_elem_one"
$ret[$sectionName][$subKey] = $val;
} else { // eg: [] = "arr_elem_one"
$ret[$sectionName][] = $val;
}
$this->collectValue($key, $val);
}

return $this->data;
}

/**
* @param string $key
* @param mixed $val
*/
protected function collectValue(string $key, mixed $val): void
{
// has interceptors
if ($this->interceptors) {
$isMl = $this->multiLineKey !== '';
foreach ($this->interceptors as $fn) {
$val = $fn($val, $isMl);
}
}

// top field
if (!$this->sectionName) {
$this->data[$key] = $val;
return;
}

// in section. eg: [arrayName] -> $sectionName='arrayName'
$sectionName = $this->sectionName;

// is array sub key.
// eg:
// [] = "arr_elem_one"
// val_arr[] = "arr_elem_one"
// val_arr_two[some_key] = "some_key_value"
$ok = preg_match("/[\w-]{0,64}\[(.*?)]$/", $key, $matches);
if ($ok === 1 && isset($matches[0])) {
[$arrName, $subKey] = explode('[', trim($key, ']'));

if ($arrName !== '') {
if (!isset($this->data[$sectionName][$arrName]) || !is_array($this->data[$sectionName][$arrName])) {
$this->data[$sectionName][$arrName] = [];
}

if ($subKey !== '') { // eg: val_arr[subKey] = "arr_elem_one"
$this->data[$sectionName][$arrName][$subKey] = $val;
} else { // eg: val_arr[] = "arr_elem_one"
$this->data[$sectionName][$arrName][] = $val;
}
} else {
$ret[$sectionName][$key] = $val;
if (!isset($this->data[$sectionName]) || !is_array($this->data[$sectionName])) {
$this->data[$sectionName] = [];
}

if ($subKey !== '') { // eg: [subKey] = "arr_elem_one"
$this->data[$sectionName][$subKey] = $val;
} else { // eg: [] = "arr_elem_one"
$this->data[$sectionName][] = $val;
}
}
} else {
$this->data[$sectionName][$key] = $val;
}
}

return $ret;
/**
* @return array
*/
public function getData(): array
{
return $this->data;
}

/**
* @return string
*/
public function getSource(): string
{
return $this->source;
}

/**
* @param callable[] $interceptors
*
* @return IniParser
*/
public function setInterceptors(callable ...$interceptors): self
{
$this->interceptors = $interceptors;
return $this;
}

/**
Expand All @@ -161,18 +302,11 @@ protected function removeQuotes(string $str): string
*/
protected function str2typedList(string $str): array
{
$str = substr($str, 1, - 1);
$str = substr($str, 1, -1);
if (!$str) {
return [];
}

$arr = Str::splitTrimFiltered($str);
foreach ($arr as &$val) {
if (is_numeric($val) && strlen($val) < 11) {
$val = (int)$val;
}
}

return $arr;
return Str::toTypedList($str);
}
}
Loading

0 comments on commit 8ca2262

Please sign in to comment.