Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
95 changes: 95 additions & 0 deletions lib/PhpParser/NodeVisitor/CallableVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php
namespace PhpParser\NodeVisitor;

use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;

/**
* This class can be used to quickly develop some code, without creating a visitor class.
*
* You can set a closure for the methods the traverser calls, and will be executed when the traverser calls them.
*
* @method void setBeforeTraverse(callable $callable);
* @method void setEnterNode(callable $callable);
* @method void setLeaveNode(callable $callable);
* @method void setAfterTraverse(callable $callable);
*/
class CallableVisitor extends NodeVisitorAbstract
{
/**
* @var array
*/
private $callables = [
'beforeTraverse' => null,
'enterNode' => null,
'leaveNode' => null,
'afterTraverse' => null,
];

/**
* @param array $nodes
*/
public function beforeTraverse(array $nodes)
{
return $this->callCallable(__FUNCTION__, $nodes);
}

/**
* @param Node $node
*/
public function enterNode(Node $node)
{
return $this->callCallable(__FUNCTION__, $node);
}

/**
* @param Node $node
*/
public function leaveNode(Node $node)
{
return $this->callCallable(__FUNCTION__, $node);
}

/**
* @param array $nodes
*/
public function afterTraverse(array $nodes)
{
return $this->callCallable(__FUNCTION__, $nodes);
}

/**
* Call a closure for given callable name.
*
* @param $callableName
* @param $argument
*/
private function callCallable($callableName, $argument)
{
if (!empty($this->callables[$callableName])) {
return $this->callables[$callableName]($argument);
}
}

/**
* Sets the callable for one of the methods the traverser calls.
*
* @param $method
* @param $arguments
* @return bool
*/
public function __call($method, $arguments)
{
$name = lcfirst(str_replace('set', '', $method));

if (!isset($arguments[0]) || !is_callable($arguments[0])) {
return false;
}

if (array_key_exists($name, $this->callables)) {
$this->callables[$name] = $arguments[0];
}

return true;
}
}
158 changes: 158 additions & 0 deletions test/PhpParser/NodeVisitor/CallableVisitorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<?php
namespace PhpParser\NodeVisitor;

use PhpParser\Node;
use PhpParser\NodeTraverser;
use PHPUnit\Framework\TestCase;
use PhpParser\Node\Expr;

/**
* Class CallableVisitorTest
* @package PhpParser\NodeVisitor
*/
class CallableVisitorTest extends TestCase
{
/**
* @var int
*/
private $enterNode = 0;

/**
* @var int
*/
private $leaveNode = 0;

/**
* @var int
*/
private $beforeTraverse = 0;

/**
* @var int
*/
private $afterTraverse = 0;

/**
* @var array
*/
private $order = [];

/**
* Reset all values.
*/
protected function setUp()
{
$this->enterNode = 0;
$this->leaveNode = 0;
$this->beforeTraverse = 0;
$this->afterTraverse = 0;

$this->order = [];
}

/**
* Test if the CallableVisitor works as expected with all callables set.
*/
public function testWithClosures()
{
$this->callableVisitorTest(function (Node $node) {
$this->order[] = 'enterNode';
$this->enterNode++;
}, function (Node $node) {
$this->order[] = 'leaveNode';
$this->leaveNode++;
}, function ($array) {
$this->order[] = 'beforeTraverse';
$this->beforeTraverse++;
}, function ($array) {
$this->order[] = 'afterTraverse';
$this->afterTraverse++;
});

$this->assertSame([
$this->enterNode,
$this->leaveNode,
$this->beforeTraverse,
$this->afterTraverse
], [6, 6, 1, 1]);

$this->assertSame([
'beforeTraverse',
'enterNode',
'enterNode',
'enterNode',
'leaveNode',
'enterNode',
'enterNode',
'leaveNode',
'enterNode',
'leaveNode',
'leaveNode',
'leaveNode',
'leaveNode',
'afterTraverse',
], $this->order);
}

/**
* Test if the CallableVisitor works as expected with all enter node/traverse callables set.
*/
public function testPartiallyFilled()
{
$this->callableVisitorTest(function (Node $node) {
$this->order[] = 'enterNode';
$this->enterNode++;
}, null, function ($array) {
$this->order[] = 'beforeTraverse';
$this->beforeTraverse++;
}, null);

$this->assertSame([
$this->enterNode,
$this->leaveNode,
$this->beforeTraverse,
$this->afterTraverse
], [6, 0, 1, 0]);

$this->assertSame([
'beforeTraverse',
'enterNode',
'enterNode',
'enterNode',
'enterNode',
'enterNode',
'enterNode',
], $this->order);
}

/**
* @param $enterNode
* @param $leaveNode
* @param $beforeTravers
* @param $afterTravers
*/
private function callableVisitorTest($enterNode, $leaveNode, $beforeTravers, $afterTravers)
{
$traverser = new NodeTraverser();
$visitor = new CallableVisitor();

$visitor->setEnterNode($enterNode);
if (is_callable($enterNode)) {
$visitor->setLeaveNode($leaveNode);
}

$visitor->setBeforeTraverse($beforeTravers);
if (is_callable($afterTravers)) {
$visitor->setAfterTraverse($afterTravers);
}

$traverser->addVisitor($visitor);

$assign = new Expr\Assign(new Expr\Variable('a'), new Expr\BinaryOp\Concat(
new Expr\Variable('b'), new Expr\Variable('c')
));
$stmts = [new Node\Stmt\Expression($assign)];

$traverser->traverse($stmts);
}
}