I've been looking for a MongoDb-like ( http://docs.mongodb.org/manual/applications/read/#find, docs.mongodb.org/manual/reference/operators/ ) query expression object evaluation function implementation or a class. It may cover not all the advanced features, and should have extensible architecture.
MongoDB-like query expression objects are easy for understanding and usage, providing ability to write clean, self-explaining code, because both query and objects to search in, are associative arrays.
Basically talking its a convenient function to extract information from php arrays. Knowing the array structure(the arrayPath), it will allow to perform operations on multidimensional arrays data, without the need for multiple nested loops.
If you are not familiar with MongoDb, take a look at a given expression object and array to search in.
I wrote it as JSON string for simplicity. The object contents makes no sense, just showng the MongoDb query syntax.
{
"name": "Mongo",
"type": "db",
"arch": {
"$in": [
"x86",
"x64"
]
},
"version": {
"$gte": 22
},
"released": {
"$or": {
"$lt": 2013,
"$gt": 2012
}
}
}
[
{
"name": "Mongo",
"type": "db",
"release": {
"arch": "x86",
"version": 22,
"year": 2012
}
},
{
"name": "Mongo",
"type": "db",
"release": {
"arch": "x64",
"version": 21,
"year": 2012
}
},
{
"name": "Mongo",
"type": "db",
"release": {
"arch": "x86",
"version": 23,
"year": 2013
}
}
]
So, with the help of the function, we should be able to issue the following query to the target array.
$found=findLikeMongo($array, $queryExpr); //resulting in a $array[0] value;
//@return found array
$arrayPath=getPathFromMongo($array, $queryExpr);// resulting in array("0")
//@return array path, represented as an array where entries are consecutive keys.
I found that goessner.net/articles/JsonPath/ could possibly cover my needs(not being an exact match because it uses Xpath-like expressions), the caveat is, that it heavily relies on regular expressions and string parsing, what will definitely slow it down compared to array only(JSON like) implementation.
Also I've found a similar question here, @stackoverflow
Evaluating MongoDB-like JSON Queries in PHP.
The resulting answer was to use some SPL functions, which I am used
to avoid most of the time.
Wonder if the author had came up with function, he had been trying to
develop.
The possible arrayPath implementation was found on thereisamoduleforthat.com/content/dealing-deep-arrays-php, thus the lack of this implementation, is that it relies on pointers.
I know its not a trivial question with a oneliner answer, that's why I'm asking it before starting the actual development of my own class.
I appreciate architecture tips, related or similar code, which may be a good practice example for building php "if..else" expressions on the fly.emphasized text
@Baba provided an excellent class, which is written with the use of SPL. I wonder how to rewrite this code without SPL.
The created ArrayQuery class is published on Github, consider checking-out the repository for updates.
In brief-
$m = new MongoClient(); // connect
$db = $m->testmongo; // select a database
$collection = $db->data;
$loops=100;
for ($i=0; $i<$loops; $i++) {
$d = $collection->find(array("release.year" => 2013));
}
print_r( iterator_to_array($d) );
include('data.php');
include('phpmongo-spl.php');
$s = new ArrayCollection($array, array("release.year" => 2013),false);
$loops=100;
for ($i=0; $i<$loops; $i++) {
$d = $s->parse();
}
print_r( $d );
The SPL class parse() function has been slightly modified to return the value after execution, it could be also be modified to accept expression, but it's not essential for profiling purposes as the expression is being reevaluated every time.
include('data.php');
include('phpmongo-raw.php');
$s = new ArrayStandard($array);
$loops=100;
for ($i=0; $i<$loops; $i++) {
$d = $s->find(array("release.year" => 2013));
}
print_r( $d );
<?php
include('data.php');
include('../chequer2/Chequer.php');
$query=array("release.year" => 2013);
$loops=100;
for ($i=0; $i<$loops; $i++) {
$result=Chequer::shorthand('(.release.year > 2012) ? (.) : NULL')
->walk($array);
}
print_r($result);
?>
$json = '[{
"name":"Mongo",
"type":"db",
"release":{
"arch":"x86",
"version":22,
"year":2012
}
},
{
"name":"Mongo",
"type":"db",
"release":{
"arch":"x64",
"version":21,
"year":2012
}
},
{
"name":"Mongo",
"type":"db",
"release":{
"arch":"x86",
"version":23,
"year":2013
}
},
{
"key":"Diffrent",
"value":"cool",
"children":{
"tech":"json",
"lang":"php",
"year":2013
}
}
]';
$array = json_decode($json, true);
<!doctype html>
<html>
<head>
<style>
body {margin : 0px}
</style>
</head>
<body>
<div class="forp"></div>
<?php
register_shutdown_function(
function() {
// next code can be append to PHP scripts in dev mode
?>
<script src="../forp-ui/js/forp.min.js"></script>
<script>
(function(f) {
f.find(".forp")
.each(
function(el) {
el.css('margin:50px;height:300px;border:1px solid #333');
}
)
.forp({
stack : <?php echo json_encode(forp_dump()); ?>,
//mode : "fixed"
})
})(forp);
</script>
<?php
}
);
// start forp
forp_start();
// our PHP script to profile
include($_GET['profile']);
// stop forp
forp_end();
?>
</body>
</html>
@baba has given a great raw PHP version of a class implementing MongoDB-like query expression object evaluation, but the output structure differs a bit, I mean the dot notation in the nested array output( [release.arch] => x86 ), instead of regular arrays( [release] => Array([arch] => x86) ). I would appreciate your tip how to make the class fully compatible with mongoDB in this order, as it seems its strictly tied to the raw PHP class implementation.
=======================================================================
Answer:
What you want is very easy, All you need is 2 corrections
in the current code input and output loop and you would get your new format.
What do i mean ?
A. Changed
foreach ( $array as $part ) {
$this->flatten[] = $this->convert($part);
}
To
foreach ( $array as $k => $part ) {
$this->flatten[$k] = $this->convert($part);
}
B. Changed
foreach ( $this->flatten as $data ) {
$this->check($find, $data, $type) and $f[] = $data;
}
To:
foreach ( $this->flatten as $k => $data ) {
$this->check($find, $data, $type) and $f[] = $this->array[$k];
}
New Array for resting
$json = '[
{
"name": "Mongo",
"release": {
"arch": "x86",
"version": 22,
"year": 2012
},
"type": "db"
},
{
"name": "Mongo",
"release": {
"arch": "x64",
"version": 21,
"year": 2012
},
"type": "db"
},
{
"name": "Mongo",
"release": {
"arch": "x86",
"version": 23,
"year": 2013
},
"type": "db"
},
{
"name": "MongoBuster",
"release": {
"arch": [
"x86",
"x64"
],
"version": 23,
"year": 2013
},
"type": "db"
},
{
"children": {
"dance": [
"one",
"two",
{
"three": {
"a": "apple",
"b": 700000,
"c": 8.8
}
}
],
"lang": "php",
"tech": "json",
"year": 2013
},
"key": "Diffrent",
"value": "cool"
}
]';
$array = json_decode($json, true);
Simple Test
$s = new ArrayStandard($array);
print_r($s->find(array("release.arch"=>"x86")));
Output
Array
(
[0] => Array
(
[name] => Mongo
[type] => db
[release] => Array
(
[arch] => x86
[version] => 22
[year] => 2012
)
)
[1] => Array
(
[name] => Mongo
[type] => db
[release] => Array
(
[arch] => x86
[version] => 23
[year] => 2013
)
)
)
If you also want to retain original array key position
you can have
foreach ( $this->flatten as $k => $data ) {
$this->check($find, $data, $type) and $f[$k] = $this->array[$k];
}
Just for Fun Part
A. Support for regex
Just for fun i added support for $regex
with alias $preg
or $match
which means you can have
print_r($s->find(array("release.arch" => array('$regex' => "/4$/"))));
Or
print_r($s->find(array("release.arch" => array('$regex' => "/4$/"))));
Output
Array
(
[1] => Array
(
[name] => Mongo
[type] => db
[release] => Array
(
[arch] => x64
[version] => 21
[year] => 2012
)
)
)
B. Use Simple array like queries
$queryArray = array(
"release" => array(
"arch" => "x86"
)
);
$d = $s->find($s->convert($queryArray));
$s->convert($queryArray)
has converted
Array
(
[release] => Array
(
[arch] => x86
)
)
To
Array
(
[release.arch] => x86
)
C. Modulus $mod
print_r($s->find(array(
"release.version" => array(
'$mod' => array(
23 => 0
)
)
)));
//Checks release.version % 23 == 0 ;
D. Count elements with $size
print_r($s->find(array(
"release.arch" => array(
'$size' => 2
)
)));
// returns count(release.arch) == 2;
E. Check if it matches all element in array $all
print_r($s->find(array(
"release.arch" => array(
'$all' => array(
"x86",
"x64"
)
)
)));
Output
Array
(
[3] => Array
(
[name] => MongoBuster
[release] => Array
(
[arch] => Array
(
[0] => x86
[1] => x64
)
[version] => 23
[year] => 2013
)
[type] => db
)
)
F. If you are not sure of the element key name then you ca use $has
its like the opposite
of $in
print_r($s->find(array(
"release" => array(
'$has' => "x86"
)
)));
=======================================================================
@Baba provided an excellent class, which is written with the use of SPL. I wonder how to rewrite this code without SPL. The reason is that calling this class multiple times will give function overhead, that can be avoided rewriting it in raw PHP, and maybe using goto statement in final version, to avoid recursive function calls.
=======================================================================
Since you don't want SPL
and functions .. it took a while but i was able to come up with alternative class that is also flexible and easy to use
To avoid loading the array multiple times you declare it once :
$array = json_decode($json, true);
$s = new ArrayStandard($array);
A. Find where release.year
is 2013
$d = $s->find(array(
"release.year" => "2013"
));
print_r($d);
Output
Array
(
[0] => Array
(
[name] => Mongo
[type] => db
[release.arch] => x86
[release.version] => 23
[release.year] => 2013
)
)
B. For the first time you can run complex $and
or $or
statement like find where release.arch
= x86
and release.year
= 2012
$d = $s->find(array(
"release.arch" => "x86",
"release.year" => "2012"
), ArrayStandard::COMPLEX_AND);
print_r($d);
Output
Array
(
[0] => Array
(
[name] => Mongo
[type] => db
[release.arch] => x86
[release.version] => 22
[release.year] => 2012
)
)
C. Imagine a much more complex query
$d = $s->find(array(
"release.year" => array(
'$in' => array(
"2012",
"2013"
)
),
"release.version" => array(
'$gt' => 22
),
"release.arch" => array(
'$func' => function ($a) {
return $a == "x86";
}
)
), ArrayStandard::COMPLEX_AND);
print_r($d);
Output
Array
(
[0] => Array
(
[name] => Mongo
[type] => db
[release.arch] => x86
[release.version] => 23
[release.year] => 2013
)
)
The new Modified class
class ArrayStandard {
const COMPLEX_OR = 1;
const COMPLEX_AND = 2;
private $array;
private $tokens;
private $found;
function __construct(array $array) {
$this->array = $array;
foreach ( $array as $k => $item ) {
$this->tokens[$k] = $this->tokenize($item);
}
}
public function getTokens() {
return $this->tokens;
}
public function convert($part) {
return $this->tokenize($part, null, false);
}
public function find(array $find, $type = 1) {
$f = array();
foreach ( $this->tokens as $k => $data ) {
$this->check($find, $data, $type) and $f[$k] = $this->array[$k];
}
return $f;
}
private function check($find, $data, $type) {
$o = $r = 0; // Obigation & Requirement
foreach ( $data as $key => $value ) {
if (isset($find[$key])) {
$r ++;
$options = $find[$key];
if (is_array($options)) {
reset($options);
$eK = key($options);
$eValue = current($options);
if (strpos($eK, '$') === 0) {
$this->evaluate($eK, $value, $eValue) and $o ++;
} else {
throw new InvalidArgumentException('Missing "$" in expession key');
}
} else {
$this->evaluate('$eq', $value, $options) and $o ++;
}
}
}
if ($o === 0)
return false;
if ($type == self::COMPLEX_AND and $o !== $r)
return false;
return true;
}
private function getValue(array $path) {
return count($path) > 1 ? $this->getValue(array_slice($path, 1), $this->array[$path[0]]) : $this->array[$path[0]];
}
private function tokenize($array, $prefix = '', $addParent = true) {
$paths = array();
$px = empty($prefix) ? null : $prefix . ".";
foreach ( $array as $key => $items ) {
if (is_array($items)) {
$addParent && $paths[$px . $key] = json_encode($items);
foreach ( $this->tokenize($items, $px . $key) as $k => $path ) {
$paths[$k] = $path;
}
} else {
$paths[$px . $key] = $items;
}
}
return $paths;
}
private function evaluate($func, $a, $b) {
$r = false;
switch ($func) {
case '$eq' :
$r = $a == $b;
break;
case '$not' :
$r = $a != $b;
break;
case '$gte' :
case '$gt' :
if ($this->checkType($a, $b)) {
$r = $a > $b;
}
break;
case '$lte' :
case '$lt' :
if ($this->checkType($a, $b)) {
$r = $a < $b;
}
break;
case '$in' :
if (! is_array($b))
throw new InvalidArgumentException('Invalid argument for $in option must be array');
$r = in_array($a, $b);
break;
case '$has' :
if (is_array($b))
throw new InvalidArgumentException('Invalid argument for $has array not supported');
$a = @json_decode($a, true) ? : array();
$r = in_array($b, $a);
break;
case '$all' :
$a = @json_decode($a, true) ? : array();
if (! is_array($b))
throw new InvalidArgumentException('Invalid argument for $all option must be array');
$r = count(array_intersect_key($a, $b)) == count($b);
break;
case '$regex' :
case '$preg' :
case '$match' :
$r = (boolean) preg_match($b, $a, $match);
break;
case '$size' :
$a = @json_decode($a, true) ? : array();
$r = (int) $b == count($a);
break;
case '$mod' :
if (! is_array($b))
throw new InvalidArgumentException('Invalid argument for $mod option must be array');
list($x, $y) = each($b);
$r = $a % $x == 0;
break;
case '$func' :
case '$fn' :
case '$f' :
if (! is_callable($b))
throw new InvalidArgumentException('Function should be callable');
$r = $b($a);
break;
default :
throw new ErrorException("Condition not valid ... Use \$fn for custom operations");
break;
}
return $r;
}
private function checkType($a, $b) {
if (is_numeric($a) && is_numeric($b)) {
$a = filter_var($a, FILTER_SANITIZE_NUMBER_FLOAT);
$b = filter_var($b, FILTER_SANITIZE_NUMBER_FLOAT);
}
if (gettype($a) != gettype($b)) {
return false;
}
return true;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With