-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New common helper
get_by_path
and req_by_path
- Loading branch information
Showing
4 changed files
with
298 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} | ||
} |