diff --git a/README.md b/README.md index 062f9dc..85346df 100644 --- a/README.md +++ b/README.md @@ -17,40 +17,57 @@ composer require jbzoo/data ### Comparison with pure PHP -Action | JBZoo/Data | Simple PHP Array ---------------------- | ------------------------------------------------- | -------------------------------------------- -Create | `$d = data($someData)` | `$ar = [/* ... */];` -Supported formats | Array, Object, ArrayObject, JSON, INI, Yml | Array -Load form file | *.php, *.ini, *.yml, *.json, serialized | - -Get value or default | `$d->get('key', 42)` | `array_key_exists('k', $ar) ? $ar['k'] : 42` -Get undefined #1 | `$d->get('undefined')` (no any notice) | `$ar['undefined'] ?? null` -Get undefined #2 | `$d->find('undefined')` | `$ar['und'] ?? null` -Get undefined #3 | `$d->undefined === null` (no any notice) | - -Get undefined #4 | `$d['undefined'] === null` (no any notice) | - -Get undefined #5 | `$d['undef']['undef'] === null` (no any notice) | - -Comparing #1 | `$d->get('key') === $someVar` | `$ar['key'] === $someVar` -Comparing #2 | `$d->is('key', $someVar)` | - -Comparing #3 | `$d->is('key', $someVar, true)` (strict) | - -Like array | `$d['key']` | `$ar['key']` -Like object #1 | `$d->key` | - -Like object #2 | `$d->get('key')` | - -Like object #3 | `$d->find('key')` | - -Like object #4 | `$d->offsetGet('key')` | - -Isset #1 | `isset($d['key'])` | `isset($ar['key'])` -Isset #2 | `isset($d->key)` | `array_key_exists('key', $ar)` -Isset #3 | `$d->has('key')` | - -Nested key #1 | `$d->find('inner.inner.prop', $default)` | `$ar['inner']['inner']['prop']` (error?) -Nested key #2 | `$d->inner['inner']['prop']` | - -Nested key #3 | `$d['inner']['inner']['prop']` | - -Export to Serialized | `echo (new Data([/* ... */]))` | `echo serialize([/* ... */])` -Export to JSON | `echo (new JSON([/* ... */]))` (readable) | `echo json_encode([/* ... */])` -Export to Yml | `echo (new Yml ([/* ... */]))` (readable) | - -Export to Ini | `echo (new Ini([/* ... */]))` (readable) | - -Export to PHP Code | `echo (new PHPArray ([/* ... */]))` (readable) | - -JSON | **+** | - -Filters | **+** | - -Search | **+** | - -Flatten Recursive | **+** | - +-------------------------------------------------------------------------------- +| Action | JBZoo/Data | Pure PHP way | +|:-------------------------------------|:------------------------------------------------|:-----------------------------------------| +| Create | `$d = data($someData)` | `$ar = [/* ... */];` | +| Supported formats | Array, Object, ArrayObject, JSON, INI, Yml | Array | +| Load form file | *.php, *.ini, *.yml, *.json, serialized | - | +| Get value or default | `$d->get('key', 42)` | `$ar['key'] ?? 42` | +| Get undefined #1 | `$d->get('undefined')` (no any notice) | `$ar['undefined'] ?? null` | +| Get undefined #2 | `$d->find('undefined')` | `$ar['und'] ?? null` | +| Get undefined #3 | `$d->undefined === null` (no any notice) | - | +| Get undefined #4 | `$d['undefined'] === null` (no any notice) | - | +| Get undefined #5 | `$d['undef']['undef'] === null` (no any notice) | - | +| Comparing #1 | `$d->get('key') === $someVar` | `$ar['key'] === $someVar` | +| Comparing #2 | `$d->is('key', $someVar)` | - | +| Comparing #3 | `$d->is('key', $someVar, true)` (strict) | - | +| Like array | `$d['key']` | `$ar['key']` | +| Like object #1 | `$d->key` | - | +| Like object #2 | `$d->get('key')` | - | +| Like object #3 | `$d->find('key')` | - | +| Like object #4 | `$d->offsetGet('key')` | - | +| Isset #1 | `isset($d['key'])` | `isset($ar['key'])` | +| Isset #2 | `isset($d->key)` | `array_key_exists('key', $ar)` | +| Isset #3 | `$d->has('key')` | - | +| Nested key #1 | `$d->find('inner.inner.prop', $default)` | `$ar['inner']['inner']['prop']` (error?) | +| Nested key #2 | `$d->inner['inner']['prop']` | - | +| Nested key #3 | `$d['inner']['inner']['prop']` | - | +| Export to Serialized | `echo data([/* ... */])` | `echo serialize([/* ... */])` | +| Export to JSON | `echo (json([/* ... */]))` (readable) | `echo json_encode([/* ... */])` | +| Export to Yml | `echo yml([/* ... */])` (readable) | - | +| Export to Ini | `echo ini([/* ... */])` (readable) | - | +| Export to PHP Code | `echo phpArray([/* ... */])` (readable) | - | +| JSON | **+** | - | +| Filters | **+** | - | +| Search | **+** | - | +| Flatten Recursive | **+** | - | +| Set Value | `$d['value'] = 42` | $ar['value'] = 42 | +| Set Nested Value | `$d->set('q.w.e.r.t.y') = 42` | $ar['q']['w']['e']['r']['t']['y'] = 42 | +| Set Nested Value (if it's undefined) | `$d->set('q.w.e.r.t.y') = 42` | PHP Notice errors... | + + +### Know your data + +```php +$json = json('{ "some": "thing", "number": 42 }'); +dump($json->getSchema(); +// [ +// "some" => "string", +// "number" => "int" +// ] + +``` #### Methods diff --git a/composer.json b/composer.json index e065394..1e7ca06 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "require-dev" : { "jbzoo/toolbox-dev" : "^7.0", - "jbzoo/utils" : "^7.0", + "jbzoo/utils" : "^7.1.1", "symfony/yaml" : ">=4.4", "symfony/polyfill-ctype" : ">=1.27.0", diff --git a/src/AbstractData.php b/src/AbstractData.php index 4878269..3242ef6 100644 --- a/src/AbstractData.php +++ b/src/AbstractData.php @@ -16,6 +16,7 @@ namespace JBZoo\Data; +use JBZoo\Utils\Arr; use JBZoo\Utils\Filter; /** @@ -98,14 +99,25 @@ public function get(string $key, mixed $default = null, mixed $filter = null): m /** * Set a value in the data. - * @param string $name The key used to set the value - * @param mixed $value The value to set + * @param string $pathKey The key used to set the value + * @param mixed $value The value to set + * @param string $separator The separator to use when searching for sub keys. Default is '.' + * + * @psalm-suppress UnsafeInstantiation */ - public function set(string $name, mixed $value): static + public function set(string $pathKey, mixed $value, string $separator = '.'): self { - $this->offsetSet($name, $value); + if (\str_contains($pathKey, $separator) && $separator !== '') { + $keys = \explode($separator, $pathKey); + } else { + $keys = [$pathKey]; + } - return $this; + $arrayCopy = $this->getArrayCopy(); + self::setNestedValue($arrayCopy, $keys, $value); + + // @phpstan-ignore-next-line + return new static($arrayCopy); } /** @@ -254,6 +266,11 @@ public function is(string $key, mixed $compareWith = true, bool $strictMode = fa return $value == $compareWith; } + public function getSchema(): array + { + return Arr::getSchema($this->getArrayCopy()); + } + /** * Filter value before return. */ @@ -288,6 +305,21 @@ protected static function isMulti(array $array): bool return \count($arrayCount) > 0; } + private static function setNestedValue(array &$array, array $keys, mixed $value): void + { + $key = \array_shift($keys); + + if (\count($keys) === 0) { + $array[$key] = $value; + } else { + if (!isset($array[$key]) || !\is_array($array[$key])) { + $array[$key] = []; + } + + self::setNestedValue($array[$key], $keys, $value); + } + } + private static function checkDeprecatedFilter(string $prefix, mixed $filter): void { if (!\is_string($filter)) { diff --git a/tests/DataPackageTest.php b/tests/DataPackageTest.php index 19976ca..e5bb710 100644 --- a/tests/DataPackageTest.php +++ b/tests/DataPackageTest.php @@ -16,6 +16,8 @@ namespace JBZoo\PHPUnit; +use JBZoo\Markdown\Table; + final class DataPackageTest extends \JBZoo\Codestyle\PHPUnit\AbstractPackageTest { protected string $packageName = 'Data'; @@ -25,4 +27,192 @@ protected function setUp(): void parent::setUp(); $this->excludedPathsForCopyrights[] = 'resource'; } + + public function testComparativetablelInReadme(): void + { + $rows = [ + [ + 'Create', + '`$d = data($someData)`', + '`$ar = [/* ... */];`', + ], + [ + 'Supported formats', + 'Array, Object, ArrayObject, JSON, INI, Yml', + 'Array', + ], + [ + 'Load form file', + '*.php, *.ini, *.yml, *.json, serialized', + '-', + ], + [ + 'Get value or default', + "`\$d->get('key', 42)`", + "`\$ar['key'] ?? 42`", + ], + [ + 'Get undefined #1', + "`\$d->get('undefined')` (no any notice)", + "`\$ar['undefined'] ?? null`", + ], + [ + 'Get undefined #2', + '`$d->find(\'undefined\')`', + '`$ar[\'und\'] ?? null`', + ], + [ + 'Get undefined #3', + '`$d->undefined === null` (no any notice)', + '-', + ], + [ + 'Get undefined #4', + "`\$d['undefined'] === null` (no any notice)", + '-', + ], + [ + 'Get undefined #5', + "`\$d['undef']['undef'] === null` (no any notice)", + '-', + ], + [ + 'Comparing #1', + "`\$d->get('key') === \$someVar`", + '`$ar[\'key\'] === $someVar`', + ], + [ + 'Comparing #2', + "`\$d->is('key', \$someVar)`", + '-', + ], + [ + 'Comparing #3', + "`\$d->is('key', \$someVar, true)` (strict)", + '-', + ], + [ + 'Like array', + "`\$d['key']`", + "`\$ar['key']`", + ], + [ + 'Like object #1', + '`$d->key`', + '-', + ], + [ + 'Like object #2', + "`\$d->get('key')`", + '-', + ], + [ + 'Like object #3', + "`\$d->find('key')`", + '-', + ], + [ + 'Like object #4', + '`$d->offsetGet(\'key\')`', + '-', + ], + [ + 'Isset #1', + "`isset(\$d['key'])`", + "`isset(\$ar['key'])`", + ], + [ + 'Isset #2', + '`isset($d->key)`', + "`array_key_exists('key', \$ar)`", + ], + [ + 'Isset #3', + "`\$d->has('key')`", + '-', + ], + [ + 'Nested key #1', + '`$d->find(\'inner.inner.prop\', $default)`', + "`\$ar['inner']['inner']['prop']` (error?)", + ], + [ + 'Nested key #2', + "`\$d->inner['inner']['prop']`", + '-', + ], + [ + 'Nested key #3', + "`\$d['inner']['inner']['prop']`", + '-', + ], + [ + 'Export to Serialized', + '`echo data([/* ... */])`', + '`echo serialize([/* ... */])`', + ], + [ + 'Export to JSON', + '`echo (json([/* ... */]))` (readable)', + '`echo json_encode([/* ... */])`', + ], + [ + 'Export to Yml', + '`echo yml([/* ... */])` (readable)', + '-', + ], + [ + 'Export to Ini', + '`echo ini([/* ... */])` (readable)', + '-', + ], + [ + 'Export to PHP Code', + '`echo phpArray([/* ... */])` (readable)', + '-', + ], + [ + 'JSON', + '**+**', + '-', + ], + [ + 'Filters', + '**+**', + '-', + ], + [ + 'Search', + '**+**', + '-', + ], + [ + 'Flatten Recursive', + '**+**', + '-', + ], + [ + 'Set Value', + "`\$d['value'] = 42`", + "\$ar['value'] = 42", + ], + [ + 'Set Nested Value', + "`\$d->set('q.w.e.r.t.y') = 42`", + "\$ar['q']['w']['e']['r']['t']['y'] = 42", + ], + [ + "Set Nested Value (if it's undefined)", + "`\$d->set('q.w.e.r.t.y') = 42`", + 'PHP Notice errors...', + ], + ]; + + $table = (new Table()) + ->setHeaders(['Action', 'JBZoo/Data', 'Pure PHP way']) + ->setAlignments([Table::ALIGN_LEFT, Table::ALIGN_LEFT, Table::ALIGN_LEFT]) + ->appendRows($rows); + + isFileContains($table->render(), PROJECT_ROOT . '/README.md'); + } } diff --git a/tests/DataTest.php b/tests/DataTest.php index 775eab0..d263ab1 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -237,27 +237,6 @@ public function testGet(): void isNull($data->get('undefined', null)); } - public function testSet(): void - { - // methods - $data = new Data($this->test); - is(10, $data->get('number')); - $data->set('number', 'qqq'); - is('qqq', $data->get('number')); - - // like array - $data = new Data($this->test); - is(10, $data['number']); - $data['number'] = 'qqq'; - is('qqq', $data['number']); - - // like object - $data = new Data($this->test); - is(10, $data->number); - $data->number = 'qqq'; - is('qqq', $data->number); - } - public function testFind(): void { $data = new Data($this->test); @@ -502,4 +481,135 @@ public function testEmptySeparator(): void $data->find('array_not_empty.123', null, null, ''); } + + public function testSetDirectly(): void + { + // methods + $data = new Data($this->test); + is(10, $data->get('number')); + $newData = $data->set('number', 'qqq'); + is('qqq', $newData->get('number')); + + // like array + $data = new Data($this->test); + is(10, $data['number']); + $data['number'] = 'qqq'; + is('qqq', $data['number']); + + // like object + $data = new Data($this->test); + is(10, $data->number); + $data->number = 'qqq'; + is('qqq', $data->number); + } + + public function testSetNestedWithClonning(): void + { + $data = new Data(['original' => 123]); + isSame(['original' => 123], $data->getArrayCopy()); + + $newData = $data->set('value', 42); + isSame(['original' => 123, 'value' => 42], $newData->getArrayCopy()); + + $newData = $data->set('nested.value', 'qwerty'); + isSame(['original' => 123, 'nested' => ['value' => 'qwerty']], $newData->getArrayCopy()); + + $newData = $data->set('nested|value', 'qwerty', '|'); + isSame(['original' => 123, 'nested' => ['value' => 'qwerty']], $newData->getArrayCopy()); + + $newData = $data->set('nested|value', 'qwerty'); + isSame(['original' => 123, 'nested|value' => 'qwerty'], $newData->getArrayCopy()); + + $newData = $data + ->set('value', 43) + ->set('nested.value', 'Qqqq'); + isSame(['original' => 123, 'value' => 43, 'nested' => ['value' => 'Qqqq']], $newData->getArrayCopy()); + + $newData = $data->set('nested.value', []); + isSame(['original' => 123, 'nested' => ['value' => []]], $newData->getArrayCopy()); + + $newData = $data + ->set('nested.value', []) + ->set('nested.value', 'Qqqq'); + isSame(['original' => 123, 'nested' => ['value' => 'Qqqq']], $newData->getArrayCopy()); + + $newData = $data + ->set('nested.value', []) + ->set('nested', 42); + isSame(['original' => 123, 'nested' => 42], $newData->getArrayCopy()); + + $newData = $data + ->set('nested', 42) + ->set('nested.value', []); + isSame(['original' => 123, 'nested' => ['value' => []]], $newData->getArrayCopy()); + + $newData = $data + ->set('nested.value', []) + ->set('nested.value', 42); + isSame(['original' => 123, 'nested' => ['value' => 42]], $newData->getArrayCopy()); + + $newData = $data->set('a.b.c.d.e', 111); + isSame(['original' => 123, 'a' => ['b' => ['c' => ['d' => ['e' => 111]]]]], $newData->getArrayCopy()); + + $newData = $data->set('.', 111); + isSame(['original' => 123, '' => ['' => 111]], $newData->getArrayCopy()); + + $newData = $data->set('..', 111); + isSame(['original' => 123, '' => ['' => ['' => 111]]], $newData->getArrayCopy()); + + $newData = $data->set('.qwerty', 111); + isSame(['original' => 123, '' => ['qwerty' => 111]], $newData->getArrayCopy()); + + $newData = $data->set('qwerty.', 111); + isSame(['original' => 123, 'qwerty' => ['' => 111]], $newData->getArrayCopy()); + + $newData = $data->set('q..q', 111); + isSame(['original' => 123, 'q' => ['' => ['q' => 111]]], $newData->getArrayCopy()); + + $newData = $data->set('', 111); + isSame(['original' => 123, '' => 111], $newData->getArrayCopy()); + + isTrue(true); + } + + public function testGetSchema(): void + { + $data = new Data($this->test); + isSame( + [ + 'string-zero' => 'string', + 'string-empty' => 'string', + 'string' => 'string', + 'number-zero' => 'int', + 'number' => 'int', + 'bool-true' => 'bool', + 'bool-false' => 'bool', + 'null' => 'null', + 'array_empty' => [], + 'array_not_empty' => [123 => 'string'], + 'objects' => '\\stdClass', + 'sub' => [ + 'sub' => 'string', + 'sub.sub' => 'string', + ], + 'array' => [ + 'sub' => 'string', + 'sub-sub' => [ + 'key-1' => 'string', + 'sub' => ['key-sub' => 'string'], + ], + ], + 'data' => '\\JBZoo\\Data\\Data', + 'nested' => [ + 'value-1' => 'string', + 'value-2' => 'string', + 'sub' => ['qwerty' => 'string'], + ], + 'nested.value-1' => 'string', + 'nested.value-2' => 'string', + 'nested.sub.qwerty' => 'string', + ], + $data->getSchema(), + ); + } }