Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for escaping delimiter while parsing path #111

Merged
merged 35 commits into from
Aug 19, 2022
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0de0b4b
Add support for escaping delimiter while parsing path
arogachev Aug 3, 2022
d97ee34
Update CHANGELOG.md
arogachev Aug 3, 2022
e45b9aa
Fix tests for PHP 7.4
arogachev Aug 3, 2022
939f0c9
Handle undesired escaping (in data and results)
arogachev Aug 4, 2022
40ceaee
Restructure and additional edge case (positioning)
arogachev Aug 4, 2022
3ee3fb9
Apply fixes from StyleCI
StyleCIBot Aug 4, 2022
7cf4b12
Added passed tests with delimiter positioning
arogachev Aug 4, 2022
eb930e2
Add argument for unescaping delimiter
arogachev Aug 6, 2022
ce9a7fc
Add data set for unescape delimiter
arogachev Aug 6, 2022
e31136a
Fix typo
arogachev Aug 10, 2022
f452fdf
Invert naming of $unescapeDelimiter
arogachev Aug 10, 2022
d4ba76f
Fix wording
arogachev Aug 15, 2022
c83ddf7
More detailed changelog
arogachev Aug 15, 2022
d4f5a09
Fix linebreak [skip ci]
arogachev Aug 15, 2022
bc3ccad
Fix example
arogachev Aug 15, 2022
0b653e0
Handle case when delimiter matches escape symbol
arogachev Aug 16, 2022
f592af6
Fix unexpected behavior for duplicate delimiter
arogachev Aug 16, 2022
13c018c
Apply fixes from StyleCI
StyleCIBot Aug 16, 2022
dc44c15
Add option to configure escape char
arogachev Aug 16, 2022
dc2d877
Merge remote-tracking branch 'origin/escape-delimiter' into escape-de…
arogachev Aug 16, 2022
e73c76a
Apply fixes from StyleCI
StyleCIBot Aug 16, 2022
895855d
Rename: doEscapeDelimiter -> preserveDelimiterEscaping
arogachev Aug 16, 2022
e77b1a1
Merge remote-tracking branch 'origin/escape-delimiter' into escape-de…
arogachev Aug 16, 2022
a4ea6e2
Apply fixes from StyleCI
StyleCIBot Aug 16, 2022
fd86bf6
Apply suggestions from code review
samdark Aug 17, 2022
9f9e604
Reorder and regroup test data sets
arogachev Aug 17, 2022
1244b8d
Reorder and regroup test data sets, part 2
arogachev Aug 17, 2022
3097084
Add test with escaped backslash + dot
arogachev Aug 17, 2022
3b38f53
Change input data of data set
arogachev Aug 17, 2022
6d00e7b
Fix namespace
vjik Aug 19, 2022
ce33b34
Fix
vjik Aug 19, 2022
8049067
change order of checks
vjik Aug 19, 2022
20d2262
improve
vjik Aug 19, 2022
7f059bf
fix empty path
vjik Aug 19, 2022
26099ca
changelog
vjik Aug 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@

## 2.0.1 under development

- no changes in this release.
- Enh #111: Add support for escaping delimiter while parsing path (@arogachev)
arogachev marked this conversation as resolved.
Show resolved Hide resolved
- New #111: Add `ArrayHelper::parsePath()` method (@arogachev)

## 2.0.0 October 23, 2021

- New #91: Add `ArrayHelper::group()` that groups the array according to a specified key (sagittaracc)
- New #91: Add `ArrayHelper::group()` that groups the array according to a specified key (@sagittaracc)
- New #96: Add support for iterable objects to `ArrayHelper::map()`, `ArrayHelper::index()`, `ArrayHelper::group()`,
`ArrayHelper::htmlEncode()` and `ArrayHelper::htmlDecode` (vjik)
- Chg #99: Finalize `ArrayHelper` and `ArraySorter` (vjik)
`ArrayHelper::htmlEncode()` and `ArrayHelper::htmlDecode` (@vjik)
- Chg #99: Finalize `ArrayHelper` and `ArraySorter` (@vjik)
- Bug #101: Fix incorrect default value returned from `ArrayHelper::getValue()` when key does not exist and
default is array (vjik)
default is array (@vjik)

## 1.0.1 February 10, 2021

- Chg: Update yiisoft/strings dependency (samdark)
- Chg: Update yiisoft/strings dependency (@samdark)

## 1.0.0 February 02, 2021

Expand Down
59 changes: 51 additions & 8 deletions src/ArrayHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
use function is_int;
use function is_object;
use function is_string;
use function sprintf;
use function str_replace;
use function strlen;

/**
* Yii array helper provides static methods allowing you to deal with arrays more efficiently.
Expand Down Expand Up @@ -428,21 +431,27 @@ public static function setValueByPath(array &$array, $path, $value, string $deli

/**
* @param array|float|int|string $path The path of where do you want to write a value to `$array`.
* The path can be described by a string when each key should be separated by delimiter.
* The path can be described by a string when each key should be separated by delimiter. If a path item contains
* delimiter, it can be escaped with "\" (backslash) or a custom delimiter can be used.
* You can also describe the path as an array of keys.
* @param string $delimiter A separator, used to parse string $key for embedded object property retrieving. Defaults
* @param string $delimiter A separator, used to parse string key for embedded object property retrieving. Defaults
* to "." (dot).
* @param string $escapeCharacter An escape character, used to escape delimiter. Defaults to "\" (backslash).
* @param bool $preserveDelimiterEscaping Whether to preserve delimiter escaping in the items of final array (in
* case of using string as an input). When `false`, "\" (backslashes) are removed. For a "." as delimiter, "."
* becomes "\.". Defaults to `false`.
*
* @psalm-param ArrayPath $path
*
* @return array|float|int|string
* @psalm-return ArrayKey
*/
private static function parsePath($path, string $delimiter)
{
if (is_string($path)) {
return explode($delimiter, $path);
}
public static function parsePath(
$path,
string $delimiter = '.',
string $escapeCharacter = '\\',
bool $preserveDelimiterEscaping = false
) {
if (is_array($path)) {
$newPath = [];
foreach ($path as $key) {
Expand All @@ -456,7 +465,41 @@ private static function parsePath($path, string $delimiter)
}
return $newPath;
}
return $path;

if (!is_string($path)) {
return $path;
}

if (strlen($delimiter) !== 1) {
throw new InvalidArgumentException('Only 1 character is allowed for delimiter.');
}

if (strlen($escapeCharacter) !== 1) {
throw new InvalidArgumentException('Only 1 escape character is allowed.');
}

if ($delimiter === $escapeCharacter) {
throw new InvalidArgumentException('Delimiter and escape character must be different.');
}

if (($path[0] ?? '') === $delimiter) {
throw new InvalidArgumentException('Delimiter can\'t be at the very beginning.');
}

if (substr($path, -1) === $delimiter && substr($path, -2) !== '\\' . $delimiter) {
throw new InvalidArgumentException('Delimiter can\'t be at the very end.');
}

$pattern = sprintf('/(?<!\\%s)\%s/', $escapeCharacter, $delimiter);
$matches = preg_split($pattern, $path);

if ($preserveDelimiterEscaping === true) {
return $matches;
}

return array_map(static function (string $key) use ($delimiter, $escapeCharacter): string {
return str_replace($escapeCharacter . $delimiter, $delimiter, $key);
}, $matches);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion tests/ArrayHelper/ArrayHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public function testIsSubset(): void
$this->assertFalse(ArrayHelper::isSubset(new ArrayObject([1]), ['1', 'b'], true));
}

public function testGetObjectVars()
public function testGetObjectVars(): void
{
$this->assertSame([
'id' => 123,
Expand Down
29 changes: 29 additions & 0 deletions tests/ArrayHelper/GetValueByPathTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace ArrayHelper;
vjik marked this conversation as resolved.
Show resolved Hide resolved

use PHPUnit\Framework\TestCase;
use Yiisoft\Arrays\ArrayHelper;

final class GetValueByPathTest extends TestCase
{
public function getValueByPathDataProvider(): array
{
return [
[['key1' => ['key2' => ['key3' => 'value']]], 'key1.key2.key3', '.', 'value'],
[['key1.key2' => ['key3' => 'value']], 'key1.key2.key3', '.', null],
[['key1.key2' => ['key3' => 'value']], 'key1\.key2.key3', '.', 'value'],
[['key1:key2' => ['key3' => 'value']], 'key1\:key2:key3', ':', 'value'],
arogachev marked this conversation as resolved.
Show resolved Hide resolved
];
}

/**
* @dataProvider getValueByPathDataProvider
*/
public function testGetValueByPath(array $array, string $path, string $delimiter, ?string $expectedValue): void
{
$this->assertSame($expectedValue, ArrayHelper::getValueByPath($array, $path, null, $delimiter));
}
}
87 changes: 87 additions & 0 deletions tests/ArrayHelper/ParsePathTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

namespace ArrayHelper;
vjik marked this conversation as resolved.
Show resolved Hide resolved

use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use Yiisoft\Arrays\ArrayHelper;

final class ParsePathTest extends TestCase
{
public function parsePathDataProvider(): array
{
return [
['key1.key2.key3', '.', '\\', false, ['key1', 'key2', 'key3']],
['key1..key2..key3', '.', '\\', false, ['key1', '', 'key2', '', 'key3']],
['key1...key2...key3', '.', '\\', false, ['key1', '', '', 'key2', '', '', 'key3']],
['key1\.key2.key3', '.', '\\', false, ['key1.key2', 'key3']],
['\.key1.key2', '.', '\\', false, ['.key1', 'key2']],
['key1.key2\.', '.', '\\', false, ['key1', 'key2.']],
['key1\..\.key2\..\.key3', '.', '\\', false, ['key1.', '.key2.', '.key3']],
['key1\\\.', '.', '\\', false, ['key1\.']],

['key1\:key2:key3', ':', '\\', false, ['key1:key2', 'key3']],

['key1\.key2.key3', '.', '\\', true, ['key1\.key2', 'key3']],

['key1\\key2\\key3', '\\', '/', false, ['key1', 'key2', 'key3']],
['key1\\\\key2\\\\key3', '\\', '/', false, ['key1', '', 'key2', '', 'key3']],
['key1\\\\\\key2\\\\\\key3', '\\', '/', false, ['key1', '', '', 'key2', '', '', 'key3']],
['key1/\\\\/\key2/\\\\/\key3', '\\', '/', false, ['key1\\', '\\key2\\', '\\key3']],
];
vjik marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @dataProvider parsePathDataProvider
*/
public function testParsePath(
string $path,
string $delimiter,
string $escapeCharacter,
bool $preserveDelimiterEscaping,
array $expectedPath
): void {
$actualPath = ArrayHelper::parsePath($path, $delimiter, $escapeCharacter, $preserveDelimiterEscaping);
$this->assertSame($expectedPath, $actualPath);
}

public function testParsePathWithLongDelimiter(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Only 1 character is allowed for delimiter.');

ArrayHelper::parsePath('key1..key2.key3', '..');
}

public function testParsePathWithLongEscapeCharacter(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Only 1 escape character is allowed.');

ArrayHelper::parsePath('key1.key2.key3', '.', '//');
}

public function testParsePathWithDelimiterEqualsEscapeCharacter(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Delimiter and escape character must be different.');

ArrayHelper::parsePath('key1.key2.key3', '.', '.');
}

public function testParsePathWithDelimiterAtBeginning(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Delimiter can\'t be at the very beginning.');
ArrayHelper::parsePath('.key1.key2');
}

public function testParsePathWithDelimiterAtEnd(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Delimiter can\'t be at the very end.');
ArrayHelper::parsePath('key1.key2.');
}
}