Skip to content

Commit

Permalink
New common helper get_by_path and req_by_path
Browse files Browse the repository at this point in the history
  • Loading branch information
acelot committed Sep 7, 2018
1 parent 311e0fd commit a203b66
Show file tree
Hide file tree
Showing 4 changed files with 298 additions and 3 deletions.
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@
}
],
"require": {
"php": "^7.2"
"php": "^7.2",
"ext-mbstring": "*"
},
"require-dev": {
"ext-mbstring": "*",
"phpunit/phpunit": "^7.0"
},
"autoload": {
"files": [
"src/array_helpers.php",
"src/common_helpers.php",
"src/date_helpers.php",
"src/string_helpers.php"
]
Expand Down
99 changes: 99 additions & 0 deletions src/common_helpers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php declare(strict_types=1);

namespace Acelot\Helpers;

const PATH_SPEC = "/->\w+|->{'[^']+'}|\[\d+\]|\['[^']+'\]|\[#first\]|\[#last\]|(?<error>.+)/u";

/**
* Same as `req_by_path`, but returns default value if path not found.
*
* @param mixed $var
* @param string $path
* @param mixed $default
*
* @return mixed
* @throws \InvalidArgumentException
*/
function get_by_path($var, string $path, $default = null)
{
try {
return req_by_path($var, $path);
} catch (\OutOfBoundsException $e) {
return $default;
}
}

/**
* Returns nested value of any array or an object along a specific path.
* Throws an OutOfBoundsException if path not found.
*
* @param mixed $var
* @param string $path
*
* @return mixed
* @throws \InvalidArgumentException
* @throws \OutOfBoundsException
*/
function req_by_path($var, string $path)
{
if ($path === '') {
return $var;
}

if (!preg_match_all(PATH_SPEC, $path, $matches)) {
throw new \InvalidArgumentException('Invalid path');
}

if (!empty(array_filter($matches['error']))) {
throw new \InvalidArgumentException('Invalid path');
}

$pointer = &$var;

foreach ($matches[0] as $part) {
// Objects
if (mb_strcut($part, 0, 2) === '->') {
if (!is_object($pointer)) {
throw new \OutOfBoundsException('Path not found');
}

if (mb_strcut($part, 2, 1) === '{') {
$prop = mb_strcut($part, 4, -2);
} else {
$prop = mb_strcut($part, 2);
}

if (!property_exists($pointer, $prop)) {
throw new \OutOfBoundsException('Path not found');
}
$pointer = &$pointer->{$prop};
continue;
}

// Arrays
if (mb_strcut($part, 0, 1) === '[') {
if (!is_array($pointer)) {
throw new \OutOfBoundsException();
}

$key = trim($part, '[]');
if ($key === '#first') {
reset($pointer);
$key = key($pointer);
} elseif ($key === '#last') {
end($pointer);
$key = key($pointer);
} else {
$key = trim($key, '\'');
}

if (!array_key_exists($key, $pointer)) {
throw new \OutOfBoundsException();
}
$pointer = &$pointer[$key];
continue;
}
}

return $pointer;
}
1 change: 0 additions & 1 deletion tests/ArrayHelpersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
use function Acelot\Helpers\array_to_object;
use function Acelot\Helpers\is_array_flat;
use function Acelot\Helpers\is_array_scalar;
use Acelot\Helpers\Tests\Fixtures\Collection;
use PHPUnit\Framework\TestCase;

class ArrayHelpersTest extends TestCase
Expand Down
196 changes: 196 additions & 0 deletions tests/CommonHelpersTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
<?php declare(strict_types=1);

namespace Acelot\Helpers\Tests;

use function Acelot\Helpers\get_by_path;
use function Acelot\Helpers\req_by_path;
use PHPUnit\Framework\TestCase;

class CommonHelpersTest extends TestCase
{
public function reqByPathProvider()
{
return [
[
[],
'',
[]
],
[
['я' => 1],
"['я']",
1
],
[
['a' => 1, 2, 3],
"[#first]",
1
],
[
['a' => 1, 2, 3],
"[#last]",
3
],
[
['a' => ['b' => 2]],
"['a']['b']",
2
],
[
['a' => 1, 2, 3],
"[0]",
2
],
[
[1, 2, 3, [10, 20, 30]],
"[3][1]",
20
],
[
[1, 2, 3, [10, 20, 30]],
"[3][#last]",
30
],
[
(object)['a' => 1],
"->a",
1
],
[
(object)['a' => (object)['b' => 2]],
"->a->b",
2
],
[
(object)['a' => [1, 2, 3]],
"->a[0]",
1
],
[
(object)['a' => [1, 2, 3]],
"->a[#last]",
3
],
[
(object)['a' => ['b' => [1, 2]]],
"->a['b'][#first]",
1
],
[
(object)['#first' => [1, 2, 3]],
"->{'#first'}[#first]",
1
],
[
['a' => true, 'b' => (object)['c' => [1, 2, (object)['d' => 'hello']]]],
"['b']->c[#last]->d",
'hello'
]
];
}

/**
* @dataProvider reqByPathProvider
*
* @param mixed $var
* @param string $path
* @param bool $expected
*/
public function testReqByPath($var, string $path, $expected)
{
try {
$this->assertEquals($expected, req_by_path($var, $path));
} catch (\OutOfBoundsException $e) {
$this->fail();
}
}

public function reqByPathInvalidProvider()
{
return [
[
[],
"['a']",
null,
null
],
[
[],
'->a',
1,
1
],
[
['a' => 1, 2, 3],
"[2]",
100,
100
],
[
['a' => ['b' => 2]],
"['a']->b",
null,
null
],
[
[1, 2, 3, [10, 20, 30]],
"[3][#last][0]",
null,
null
],
[
(object)['a' => 1],
"->b",
null,
null
],
[
(object)['a' => (object)['b' => 2]],
"->a[#first]",
null,
null
],
[
(object)['a' => [1, 2, 3]],
"['a']",
null,
null
]
];
}

/**
* @dataProvider reqByPathInvalidProvider
*
* @param mixed $var
* @param string $path
*/
public function testReqByPathInvalid($var, string $path)
{
$this->expectException(\OutOfBoundsException::class);
req_by_path($var, $path);
}

/**
* @dataProvider reqByPathInvalidProvider
*
* @param mixed $var
* @param string $path
*/
public function getByPathInvalid($var, string $path, $default, $expected)
{
$this->assertEquals($expected, get_by_path($var, $path, $default));
}

public function testInvalidPath()
{
$this->expectException(\InvalidArgumentException::class);
req_by_path(null, 'a');
req_by_path(null, '1');
req_by_path(null, '[0]a');
req_by_path(null, '->a[a]');
req_by_path(null, '[a]->->');
req_by_path(null, '[[a]]');
req_by_path(null, '->%a');
}
}

0 comments on commit a203b66

Please sign in to comment.