How is it possible to compare two nodes excluding protected attributes? (PHP-Parser)

247 Views Asked by At

I use PHP-Parser in my project. I would like to compare two nodes, using PHPUnit's assertEquals function.

Despite the nodes are the same, it gives a false result. The reason is, that one of the nodes contains two protected attributes, and the other does not:

["attributes":protected]=>
array(2) {
  ["startLine"]=>
  int(2)
  ["endLine"]=>
  int(2)
}

Is it possible to compare the nodes excluding protected attributes?


Example data

The first object:

array(1) {
  [0]=>
  object(PhpParser\Node\Stmt\Expression)#5924 (2) {
    ["expr"]=>
    object(PhpParser\Node\Expr\Assign)#5923 (3) {
      ["var"]=>
      object(PhpParser\Node\Expr\Variable)#5918 (2) {
        ["name"]=>
        string(1) "x"
        ["attributes":protected]=>
        array(2) {
          ["startLine"]=>
          int(2)
          ["endLine"]=>
          int(2)
        }
      }
      ["expr"]=>
      object(PhpParser\Node\Expr\ArrayDimFetch)#5922 (3) {
        ["var"]=>
        object(PhpParser\Node\Expr\Variable)#5919 (2) {
          ["name"]=>
          string(3) "arr"
          ["attributes":protected]=>
          array(2) {
            ["startLine"]=>
            int(2)
            ["endLine"]=>
            int(2)
          }
        }
        ["dim"]=>
        object(PhpParser\Node\Scalar\String_)#5934 (2) {
          ["value"]=>
          string(3) "FOO"
          ["attributes":protected]=>
          array(0) {
          }
        }
        ["attributes":protected]=>
        array(2) {
          ["startLine"]=>
          int(2)
          ["endLine"]=>
          int(2)
        }
      }
      ["attributes":protected]=>
      array(2) {
        ["startLine"]=>
        int(2)
        ["endLine"]=>
        int(2)
      }
    }
    ["attributes":protected]=>
    array(2) {
      ["startLine"]=>
      int(2)
      ["endLine"]=>
      int(2)
    }
  }
}

The second object:

array(1) {
  [0]=>
  object(PhpParser\Node\Stmt\Expression)#5930 (2) {
    ["expr"]=>
    object(PhpParser\Node\Expr\Assign)#5929 (3) {
      ["var"]=>
      object(PhpParser\Node\Expr\Variable)#250 (2) {
        ["name"]=>
        string(1) "x"
        ["attributes":protected]=>
        array(2) {
          ["startLine"]=>
          int(2)
          ["endLine"]=>
          int(2)
        }
      }
      ["expr"]=>
      object(PhpParser\Node\Expr\ArrayDimFetch)#5928 (3) {
        ["var"]=>
        object(PhpParser\Node\Expr\Variable)#5926 (2) {
          ["name"]=>
          string(3) "arr"
          ["attributes":protected]=>
          array(2) {
            ["startLine"]=>
            int(2)
            ["endLine"]=>
            int(2)
          }
        }
        ["dim"]=>
        object(PhpParser\Node\Scalar\String_)#5927 (2) {
          ["value"]=>
          string(3) "FOO"
          ["attributes":protected]=>
          array(3) {
            ["startLine"]=>
            int(2)
            ["endLine"]=>
            int(2)
            ["kind"]=>
            int(1)
          }
        }
        ["attributes":protected]=>
        array(2) {
          ["startLine"]=>
          int(2)
          ["endLine"]=>
          int(2)
        }
      }
      ["attributes":protected]=>
      array(2) {
        ["startLine"]=>
        int(2)
        ["endLine"]=>
        int(2)
      }
    }
    ["attributes":protected]=>
    array(2) {
      ["startLine"]=>
      int(2)
      ["endLine"]=>
      int(2)
    }
  }
}

Note the PhpParser\Node\Scalar\String_ object with ["value"]=> string(3) "FOO"

3

There are 3 best solutions below

5
Antony Thompson On

Instances of objects will always not be equal. You could write a compare function like the other answers, but simply encoding as JSON will hide all the protected properties and keep the public ones. Then comparing the strings will be enough.

An example here

http://sandbox.onlinephpfunctions.com/code/174b9bf5317a200dd42a83c082d3c95558baae90

0
Matt Raines On

I haven't been able to test this with your exact data, but the concept should work.

/**
 * Test if two variables are the same, excluding protected properties of objects.
 * @return boolean
 */
function compare_public($a, $b) {
    // If $a and $b are different things, they're not the same :)
    if (gettype($a) != gettype($b)) {
        return false;
    }
    if (is_array($a)) {
        // If $a and $b have different lengths, they're not the same.
        if (count($a) != count($b)) {
            return false;
        }
        // Call this function recursively to compare each element of $a and $b.
        // If any returns false, $a and $b are not the same.
        return count(array_filter(array_map(compare_public, $a, $b))) == count($a);
    } elseif (is_object($a)) {
        // If $a and $b are different classes, they're not the same.
        if (get_class($a) != get_class($b)) {
            return false;
        }
        // Use reflection to find all the public properties and compare them.
        // Return false if any property is different.
        $c = new ReflectionClass(get_class($a));
        foreach ($c->getProperties(ReflectionProperty::IS_PUBLIC) as $p) {
            if (!compare_public($a->{$p->name}, $b->{$p->name})) { return false; }
        }
        // All the properties matched. Return true.
        return true;
    } else {
        // Straightforward comparison for non-array, non-objects.
        return $a === $b;
    }
}

Here's a contrived example.

class TestObject
{
    public $a;
    public $b;
    protected $c;

    public function __construct($a, $b, $c)
    {
        $this->a = $a; $this->b = $b; $this->c = $c;
    }
}
//                                             V      V    <- protected properties
$first  = [new TestObject(new TestObject(1, 2, 3), 2, 3)];
$second = [new TestObject(new TestObject(1, 2, 4), 2, 4)];

var_dump($first, $second, compare_public($first, $second));

Output:

array(1) {
  [0]=>
  object(TestObject)#1 (3) {
    ["a"]=>
    object(TestObject)#2 (3) {
      ["a"]=>
      int(1)
      ["b"]=>
      int(2)
      ["c":protected]=>
      int(3)
    }
    ["b"]=>
    int(2)
    ["c":protected]=>
    int(3)
  }
}
array(1) {
  [0]=>
  object(TestObject)#3 (3) {
    ["a"]=>
    object(TestObject)#4 (3) {
      ["a"]=>
      int(1)
      ["b"]=>
      int(2)
      ["c":protected]=>
      int(4)
    }
    ["b"]=>
    int(2)
    ["c":protected]=>
    int(4)
  }
}
bool(true)
0
rishipuri On

You can remove the attribute by using a Node Traverser. See documentation here.

Your Node Traverser would look something like this,

use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;

class MyNodeVisitor extends NodeVisitorAbstract
{
    public function leaveNode(Node $node) {
        // You might want to do additional checks here
        $node->setAttributes([]);
    }
}

That will remove all the protected attributes set by PHP-Parser.

Although as one of the comments suggests you won't be able to compare the Nodes using PHPUnit assert as the both Node instances are not same. But you can probably code up a custom assert for your test.