Compare commits
7 Commits
ff0b4515ff
...
2b619d767b
Author | SHA1 | Date |
---|---|---|
Adam Pippin | 2b619d767b | 3 years ago |
Adam Pippin | 41b26006ac | 3 years ago |
Adam Pippin | 22aa9e068f | 3 years ago |
Adam Pippin | f58cd9129d | 3 years ago |
Adam Pippin | 27774c84c6 | 3 years ago |
Adam Pippin | 5d0afd4984 | 3 years ago |
Adam Pippin | 01b036b284 | 3 years ago |
23 changed files with 1331 additions and 614 deletions
@ -0,0 +1,342 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
namespace App\Cfnpp\Expression; |
||||
|
|
||||
|
use App\Util\GraphNode; |
||||
|
use App\Util\Stack; |
||||
|
|
||||
|
/** |
||||
|
* Represents an expression and handles tokenizing, parsing, and executing |
||||
|
* expressions. |
||||
|
* |
||||
|
* @author Adam Pippin <hello@adampippin.ca> |
||||
|
*/ |
||||
|
class Expression |
||||
|
{ |
||||
|
/** |
||||
|
* References to all Token implementations this class can handle. |
||||
|
* @var string[] |
||||
|
*/ |
||||
|
public const TOKEN_TYPES = [ |
||||
|
Token\NumericLiteral::class, |
||||
|
Token\OperatorUnary::class, |
||||
|
Token\OperatorBinary::class, |
||||
|
Token\StringLiteral::class, |
||||
|
Token\Variable::class |
||||
|
]; |
||||
|
|
||||
|
/** |
||||
|
* Solved expression this represents. |
||||
|
* @var GraphNode[] |
||||
|
*/ |
||||
|
protected $nodes; |
||||
|
|
||||
|
/** |
||||
|
* Referencing to our compilation state for accessing variables/parameters. |
||||
|
* @var \App\Engine\IOptions |
||||
|
*/ |
||||
|
protected $options; |
||||
|
|
||||
|
/** |
||||
|
* Create a new expression. |
||||
|
* |
||||
|
* Expression is tokenized, parsed, and solved on creation. |
||||
|
* |
||||
|
* @param string $expression |
||||
|
* @param \App\Engine\IOptions $options |
||||
|
*/ |
||||
|
public function __construct(string $expression, \App\Engine\IOptions $options) |
||||
|
{ |
||||
|
$tokens = static::tokenize($expression); |
||||
|
$nodes = static::parse($tokens); |
||||
|
$this->nodes = static::solve($nodes, $options->getVariables(), array_keys($options->getParameters())); |
||||
|
$this->options = $options; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check whether this expression is 'complete'. |
||||
|
* |
||||
|
* Complete is defined as having been solved down to a single value, or array |
||||
|
* of single values none of which are still tokens. That is, we have a set of |
||||
|
* computed scalars and no unresolved var/param references. |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function isComplete(): bool |
||||
|
{ |
||||
|
// I think this works? If we resolved down to a flat set of values and |
||||
|
// all are scalar, then we're done? |
||||
|
|
||||
|
$complete = true; |
||||
|
|
||||
|
foreach ($this->nodes as $node) |
||||
|
{ |
||||
|
if ($node->getValue() instanceof Token || |
||||
|
$node->hasChildren()) |
||||
|
{ |
||||
|
$complete = false; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $complete; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check how many values are contained in the solution. |
||||
|
* |
||||
|
* @return int |
||||
|
*/ |
||||
|
public function count(): int |
||||
|
{ |
||||
|
return sizeof($this->nodes); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Assuming the solution contains only a single value, fetch it. |
||||
|
* |
||||
|
* @return mixed |
||||
|
*/ |
||||
|
public function getValue() |
||||
|
{ |
||||
|
return $this->nodes[0]->getValue(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Fetch the solution as an array. |
||||
|
* |
||||
|
* Only valid if the solution is complete |
||||
|
* |
||||
|
* @return mixed[] |
||||
|
*/ |
||||
|
public function toArray(): array |
||||
|
{ |
||||
|
return static::unwrap($this->nodes); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convert an incomplete solution into a CloudFormation condition using |
||||
|
* CloudFormation intrinsic functions. |
||||
|
* |
||||
|
* @return mixed[] |
||||
|
*/ |
||||
|
public function toCloudformation(): array |
||||
|
{ |
||||
|
return static::cloudformation($this->nodes); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convert an expression string into a series of tokens. |
||||
|
* |
||||
|
* @param string $expression |
||||
|
* @return Token[] |
||||
|
*/ |
||||
|
protected static function tokenize(string $expression): array |
||||
|
{ |
||||
|
$tokens = []; |
||||
|
while (strlen($expression) > 0) |
||||
|
{ |
||||
|
foreach (static::TOKEN_TYPES as $token_class) |
||||
|
{ |
||||
|
if ($token_class::isToken($expression)) |
||||
|
{ |
||||
|
$tokens[] = $token_class::getToken($expression); |
||||
|
|
||||
|
if (strlen($expression) > 0 && substr($expression, 0, 1) != ' ') |
||||
|
{ |
||||
|
throw new \Exception('incompletely consumed token'); |
||||
|
} |
||||
|
$expression = substr($expression, 1); |
||||
|
continue 2; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
throw new \Exception('unparseable value'); |
||||
|
} |
||||
|
|
||||
|
return $tokens; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Build a tree out of a series of tokens. |
||||
|
* |
||||
|
* @param Token[] $tokens |
||||
|
* @return GraphNode[] |
||||
|
*/ |
||||
|
protected static function parse(array $tokens): array |
||||
|
{ |
||||
|
$stack = new Stack(); |
||||
|
|
||||
|
foreach ($tokens as $token) |
||||
|
{ |
||||
|
if ($token instanceof TokenLiteral) |
||||
|
{ |
||||
|
$stack->push(new GraphNode($token)); |
||||
|
} |
||||
|
elseif ($token instanceof TokenUnary) |
||||
|
{ |
||||
|
$node = new GraphNode($token); |
||||
|
$node->appendChild($stack->pop()); |
||||
|
$stack->push($node); |
||||
|
} |
||||
|
elseif ($token instanceof TokenBinary) |
||||
|
{ |
||||
|
$node = new GraphNode($token); |
||||
|
$node->appendChild($stack->pop()); |
||||
|
// @phan-suppress-next-line PhanPluginDuplicateAdjacentStatement |
||||
|
$node->appendChild($stack->pop()); |
||||
|
$stack->push($node); |
||||
|
} |
||||
|
} |
||||
|
return $stack->get(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Solve a tree of tokens by inserting all variable values and wherever possible |
||||
|
* resolving all functions. |
||||
|
* |
||||
|
* @param GraphNode[] $nodes parsed tree |
||||
|
* @param array<string,mixed> $variables variable names and values |
||||
|
* @param string[] $parameters parameter names |
||||
|
* @return GraphNode[] |
||||
|
*/ |
||||
|
protected static function solve(array $nodes, array $variables = [], array $parameters = []): array |
||||
|
{ |
||||
|
$root = new GraphNode(); |
||||
|
foreach ($nodes as $node) |
||||
|
{ |
||||
|
$root->appendChild($node); |
||||
|
} |
||||
|
static::fillVariables($root->getChildren(), $variables, $parameters); |
||||
|
static::collapse($root->getChildren()); |
||||
|
return $root->getChildren(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convert a tree of of nodes into something that could be a valid CloudFormation |
||||
|
* condition. |
||||
|
* |
||||
|
* @param GraphNode[] $nodes |
||||
|
* @throws \Exception if the remaining nodes contain functions that cannot be expressed in cloudformation |
||||
|
* @return mixed[] |
||||
|
*/ |
||||
|
protected static function cloudformation(array $nodes): array |
||||
|
{ |
||||
|
foreach ($nodes as $node) |
||||
|
{ |
||||
|
$node->walk(static function(GraphNode $node): void { |
||||
|
if (is_scalar($node->getValue())) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
if (!($node->getValue() instanceof ICloudformationNative)) |
||||
|
{ |
||||
|
throw new \Exception('Token '.basename(get_class($node->getValue())).' is not natively supported by CloudFormation.'); |
||||
|
} |
||||
|
|
||||
|
$node->setValue($node->getValue()->toCloudformation($node->getChildren())); |
||||
|
$node->clearChildren(); |
||||
|
}); |
||||
|
} |
||||
|
if (sizeof($nodes) > 1) |
||||
|
{ |
||||
|
throw new \Exception('Expression cannot be converted to CloudFormation -- contains multiple nodes'); |
||||
|
} |
||||
|
return $nodes[0]->getValue(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Fill all variable values by replacing variable tokens with their actual values. |
||||
|
* |
||||
|
* @param GraphNode[] $nodes |
||||
|
* @param array<string,mixed> $variables variable values |
||||
|
* @param string[] $parameters parameter names |
||||
|
* @throws \Exception if a reference is made to an undefined variable |
||||
|
* @return void |
||||
|
*/ |
||||
|
protected static function fillVariables(array $nodes, array $variables, array $parameters): void |
||||
|
{ |
||||
|
foreach ($nodes as $node) |
||||
|
{ |
||||
|
$node->walk(static function(GraphNode $node) use ($variables, $parameters): void { |
||||
|
if ($node->getValue() instanceof Token\Variable) |
||||
|
{ |
||||
|
$var_name = $node->getValue()->getName(); |
||||
|
if (in_array($var_name, $parameters)) |
||||
|
{ |
||||
|
$node->getParent()->replaceChild($node, new GraphNode(new Token\Parameter($var_name))); |
||||
|
} |
||||
|
elseif (!isset($variables[$var_name])) |
||||
|
{ |
||||
|
throw new \Exception('Undefined variable: '.$var_name); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
$node->setValue($variables[$var_name]); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 'collapse' a node tree by executing nodes. |
||||
|
* |
||||
|
* @param GraphNode[] $nodes |
||||
|
* @return void |
||||
|
*/ |
||||
|
protected static function collapse(array $nodes): void |
||||
|
{ |
||||
|
foreach ($nodes as $node) |
||||
|
{ |
||||
|
$node->walk(static function(GraphNode $node): void { |
||||
|
if ($node->getValue() instanceof Token\Parameter) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if ($node->getValue() instanceof Token) |
||||
|
{ |
||||
|
$result = $node->getValue()->execute($node->getChildren()); |
||||
|
if (is_scalar($result)) |
||||
|
{ |
||||
|
$node->setValue($result); |
||||
|
$node->clearChildren(); |
||||
|
} |
||||
|
elseif ($result instanceof Token) |
||||
|
{ |
||||
|
$node->setValue($result); |
||||
|
$node->clearChildren(); |
||||
|
} |
||||
|
elseif ($result instanceof GraphNode) |
||||
|
{ |
||||
|
$node->getParent()->replaceChild($node, $result); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Unwrap an array of nodes by returning their values. |
||||
|
* |
||||
|
* Not valid if any nodes still contain children, as the assumption is that |
||||
|
* those nodes are unresolved. |
||||
|
* |
||||
|
* @param GraphNode[] $nodes |
||||
|
* @return mixed[] |
||||
|
*/ |
||||
|
protected static function unwrap(array $nodes) |
||||
|
{ |
||||
|
return array_map(/** @return mixed */ static function(GraphNode $node) { |
||||
|
if ($node->hasChildren()) |
||||
|
{ |
||||
|
throw new \Exception('Cannot unwrap node: still has children'); |
||||
|
} |
||||
|
|
||||
|
return $node->getValue(); |
||||
|
}, $nodes); |
||||
|
} |
||||
|
} |
@ -0,0 +1,22 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
namespace App\Cfnpp\Expression; |
||||
|
|
||||
|
/** |
||||
|
* Token type that supports being converted directly to a CloudFormation intrinsic. |
||||
|
* |
||||
|
* @author Adam Pippin <hello@adampippin.ca> |
||||
|
*/ |
||||
|
interface ICloudformationNative |
||||
|
{ |
||||
|
/** |
||||
|
* Convert this token to a CloudFormation intrinsic, formatted as a simple |
||||
|
* PHP array. |
||||
|
* |
||||
|
* @param ?\App\Util\GraphNode[] $arguments requested token parameters |
||||
|
* @return mixed[] |
||||
|
*/ |
||||
|
public function toCloudformation(?array $arguments = null): array; |
||||
|
} |
@ -0,0 +1,40 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
namespace App\Cfnpp\Expression; |
||||
|
|
||||
|
/** |
||||
|
* Token parsed out of an expression. |
||||
|
* |
||||
|
* @author Adam Pippin <hello@adampippin.ca> |
||||
|
*/ |
||||
|
abstract class Token |
||||
|
{ |
||||
|
/** |
||||
|
* Determine whether the passed stream contains an instance of this token. |
||||
|
* |
||||
|
* @param string $stream |
||||
|
* @return bool |
||||
|
*/ |
||||
|
abstract public static function isToken(string $stream): bool; |
||||
|
|
||||
|
/** |
||||
|
* Consume a token from the passed stream. |
||||
|
* |
||||
|
* This method is expected to modify the stream to remove all |
||||
|
* consumed characters |
||||
|
* |
||||
|
* @param string $stream |
||||
|
* @return Token |
||||
|
*/ |
||||
|
abstract public static function getToken(string &$stream): Token; |
||||
|
|
||||
|
/** |
||||
|
* Execute the token given the requested parameters. |
||||
|
* |
||||
|
* @param ?\App\Util\GraphNode[] $arguments |
||||
|
* @return \App\Util\GraphNode|Token|scalar|null |
||||
|
*/ |
||||
|
abstract public function execute(?array $arguments = null); |
||||
|
} |
@ -0,0 +1,181 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
namespace App\Cfnpp\Expression\Token; |
||||
|
|
||||
|
use App\Cfnpp\Expression\Token; |
||||
|
use App\Cfnpp\Expression\TokenBinary; |
||||
|
use App\Cfnpp\Expression\ICloudformationNative; |
||||
|
|
||||
|
/** |
||||
|
* Basic comparison operators that take two parameters. |
||||
|
* |
||||
|
* @author Adam Pippin <hello@adampippin.ca> |
||||
|
*/ |
||||
|
class OperatorBinary extends TokenBinary implements ICloudformationNative |
||||
|
{ |
||||
|
/** |
||||
|
* List of valid operators. |
||||
|
* @var string[] |
||||
|
*/ |
||||
|
public const OPERATORS = [ |
||||
|
'and', |
||||
|
'or', |
||||
|
'eq' |
||||
|
]; |
||||
|
|
||||
|
/** |
||||
|
* The operator this instance represents. |
||||
|
* @var string |
||||
|
*/ |
||||
|
protected $operator; |
||||
|
|
||||
|
/** |
||||
|
* New binary operator. |
||||
|
* |
||||
|
* @param string $operator |
||||
|
*/ |
||||
|
public function __construct(string $operator) |
||||
|
{ |
||||
|
$this->operator = $operator; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the operator this instance represents. |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getOperator(): string |
||||
|
{ |
||||
|
return $this->operator; |
||||
|
} |
||||
|
|
||||
|
public static function isToken(string $stream): bool |
||||
|
{ |
||||
|
foreach (static::OPERATORS as $operator) |
||||
|
{ |
||||
|
if (strlen($stream) >= strlen($operator) && |
||||
|
substr($stream, 0, strlen($operator)) == $operator) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public static function getToken(string &$stream): Token |
||||
|
{ |
||||
|
foreach (static::OPERATORS as $operator) |
||||
|
{ |
||||
|
if (strlen($stream) >= strlen($operator) && |
||||
|
substr($stream, 0, strlen($operator)) == $operator) |
||||
|
{ |
||||
|
$operator = substr($stream, 0, strlen($operator)); |
||||
|
$stream = substr($stream, strlen($operator)); |
||||
|
return new OperatorBinary($operator); |
||||
|
} |
||||
|
} |
||||
|
throw new \Exception('Could not parse OperatorBinary'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Execute the operator. |
||||
|
* |
||||
|
* Suppressing accesses to arguments since this is guaranteed valid |
||||
|
* unless there are bugs in Expression. |
||||
|
* @suppress PhanTypeArraySuspiciousNullable |
||||
|
* @param ?\App\Util\GraphNode[] $arguments |
||||
|
* @return \App\Util\GraphNode|Token|scalar|null |
||||
|
*/ |
||||
|
public function execute(?array $arguments = null) |
||||
|
{ |
||||
|
$value1 = $arguments[0]->getValue(); |
||||
|
$value2 = $arguments[1]->getValue(); |
||||
|
|
||||
|
switch ($this->getOperator()) |
||||
|
{ |
||||
|
case 'and': |
||||
|
if (is_scalar($value1) && is_scalar($value2)) |
||||
|
{ |
||||
|
return $value1 && $value2; |
||||
|
} |
||||
|
elseif (is_scalar($value1)) |
||||
|
{ |
||||
|
return $value1 ? $value2 : false; |
||||
|
} |
||||
|
elseif (is_scalar($value2)) |
||||
|
{ |
||||
|
return $value2 ? $value1 : false; |
||||
|
} |
||||
|
elseif ($value1 instanceof Parameter && $value2 instanceof Parameter && $value1->getName() == $value2->getName()) |
||||
|
{ |
||||
|
return $value1; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
|
||||
|
case 'or': |
||||
|
if (is_scalar($value1) && is_scalar($value2)) |
||||
|
{ |
||||
|
return $value1 || $value2; |
||||
|
} |
||||
|
elseif (is_scalar($value1)) |
||||
|
{ |
||||
|
return $value1 ? true : $value1; |
||||
|
} |
||||
|
elseif (is_scalar($value2)) |
||||
|
{ |
||||
|
return $value2 ? true : $value1; |
||||
|
} |
||||
|
elseif ($value1 instanceof Parameter && $value2 instanceof Parameter && $value1->getName() == $value2->getName()) |
||||
|
{ |
||||
|
return $value1; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
|
||||
|
case 'eq': |
||||
|
if (is_scalar($value1) && is_scalar($value2)) |
||||
|
{ |
||||
|
return $arguments[0] == $arguments[1]; |
||||
|
} |
||||
|
elseif ($value1 instanceof Parameter && $value2 instanceof Parameter && $value1->getName() == $value2->getName()) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
|
||||
|
default: |
||||
|
throw new \Exception('Missing implementation for unary operator: '.$this->getOperator()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convert this token into a CloudFormation intrinsic. |
||||
|
* |
||||
|
* Suppressing accesses to arguments since this is guaranteed valid |
||||
|
* unless there are bugs in Expression. |
||||
|
* @suppress PhanTypeArraySuspiciousNullable |
||||
|
* @param ?\App\Util\GraphNode[] $arguments |
||||
|
* @return mixed[] |
||||
|
*/ |
||||
|
public function toCloudformation(?array $arguments = null): array |
||||
|
{ |
||||
|
$value1 = $arguments[0]->getValue(); |
||||
|
$value2 = $arguments[1]->getValue(); |
||||
|
|
||||
|
switch ($this->getOperator()) |
||||
|
{ |
||||
|
case 'and': |
||||
|
return ['Fn::And' => [$value1, $value2]]; |
||||
|
case 'or': |
||||
|
return ['Fn::Or' => [$value1, $value2]]; |
||||
|
case 'eq': |
||||
|
return ['Fn::Equals' => [$value1, $value2]]; |
||||
|
default: |
||||
|
throw new \Exception('Missing implementation for unary operator: '.$this->getOperator()); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,121 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
namespace App\Cfnpp\Expression\Token; |
||||
|
|
||||
|
use App\Cfnpp\Expression\Token; |
||||
|
use App\Cfnpp\Expression\TokenUnary; |
||||
|
use App\Cfnpp\Expression\ICloudformationNative; |
||||
|
|
||||
|
/** |
||||
|
* Basic operators that take one parameter. |
||||
|
* |
||||
|
* @author Adam Pippin <hello@adampippin.ca> |
||||
|
*/ |
||||
|
class OperatorUnary extends TokenUnary implements ICloudformationNative |
||||
|
{ |
||||
|
/** |
||||
|
* List of valid operators. |
||||
|
* @var string[] |
||||
|
*/ |
||||
|
public const OPERATORS = [ |
||||
|
'not' |
||||
|
]; |
||||
|
|
||||
|
/** |
||||
|
* The operator this instance represents. |
||||
|
* @var string |
||||
|
*/ |
||||
|
protected $operator; |
||||
|
|
||||
|
/** |
||||
|
* New unary operator. |
||||
|
* |
||||
|
* @param string $operator |
||||
|
*/ |
||||
|
public function __construct($operator) |
||||
|
{ |
||||
|
$this->operator = $operator; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the operator this instance represents. |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getOperator() |
||||
|
{ |
||||
|
return $this->operator; |
||||
|
} |
||||
|
|
||||
|
public static function isToken(string $stream): bool |
||||
|
{ |
||||
|
foreach (static::OPERATORS as $operator) |
||||
|
{ |
||||
|
if (strlen($stream) >= strlen($operator) && |
||||
|
substr($stream, 0, strlen($operator)) == $operator) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public static function getToken(string &$stream): Token |
||||
|
{ |
||||
|
foreach (static::OPERATORS as $operator) |
||||
|
{ |
||||
|
if (strlen($stream) >= strlen($operator) && |
||||
|
substr($stream, 0, strlen($operator)) == $operator) |
||||
|
{ |
||||
|
$operator = substr($stream, 0, strlen($operator)); |
||||
|
$stream = substr($stream, strlen($operator)); |
||||
|
return new OperatorUnary($operator); |
||||
|
} |
||||
|
} |
||||
|
throw new \Exception('Could not parse OperatorUnary'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Execute the operator. |
||||
|
* |
||||
|
* Suppressing accesses to arguments since this is guaranteed valid |
||||
|
* unless there are bugs in Expression. |
||||
|
* @suppress PhanTypeArraySuspiciousNullable |
||||
|
* @param ?\App\Util\GraphNode[] $arguments |
||||
|
* @return \App\Util\GraphNode|Token|scalar|null |
||||
|
*/ |
||||
|
public function execute(?array $arguments = null) |
||||
|
{ |
||||
|
$value = $arguments[0]->getValue(); |
||||
|
switch ($this->getOperator()) |
||||
|
{ |
||||
|
case 'not': |
||||
|
return is_scalar($value) ? !$value : null; |
||||
|
default: |
||||
|
throw new \Exception('Missing implementation for unary operator: '.$this->getOperator()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convert this token into a CloudFormation intrinsic. |
||||
|
* |
||||
|
* Suppressing accesses to arguments since this is guaranteed valid |
||||
|
* unless there are bugs in Expression. |
||||
|
* @suppress PhanTypeArraySuspiciousNullable |
||||
|
* @param ?\App\Util\GraphNode[] $arguments |
||||
|
* @return mixed[] |
||||
|
*/ |
||||
|
public function toCloudformation(?array $arguments = null): array |
||||
|
{ |
||||
|
$value = $arguments[0]->getValue(); |
||||
|
switch ($this->getOperator()) |
||||
|
{ |
||||
|
case 'not': |
||||
|
return ['Fn::Not' => [$value]]; |
||||
|
default: |
||||
|
throw new \Exception('Missing implementation for unary operator: '.$this->getOperator()); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,91 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
namespace App\Cfnpp\Expression\Token; |
||||
|
|
||||
|
use App\Cfnpp\Expression\Token; |
||||
|
use App\Cfnpp\Expression\TokenLiteral; |
||||
|
use App\Cfnpp\Expression\ICloudformationNative; |
||||
|
|
||||
|
/** |
||||
|
* Represents a reference to a CloudFormation parameter. |
||||
|
* |
||||
|
* @author Adam Pippin <hello@adampippin.ca> |
||||
|
*/ |
||||
|
class Parameter extends TokenLiteral implements ICloudformationNative |
||||
|
{ |
||||
|
/** |
||||
|
* Name of the referenced parameter. |
||||
|
* @var string |
||||
|
*/ |
||||
|
protected $name; |
||||
|
|
||||
|
/** |
||||
|
* New parameter. |
||||
|
* @string $name |
||||
|
* @param string $name |
||||
|
*/ |
||||
|
public function __construct(string $name) |
||||
|
{ |
||||
|
$this->name = $name; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the name of the parameter. |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getName(): string |
||||
|
{ |
||||
|
return $this->name; |
||||
|
} |
||||
|
|
||||
|
public static function isToken(string $stream): bool |
||||
|
{ |
||||
|
return (bool)preg_match('/^[A-Za-z]$/', $stream[0]); |
||||
|
} |
||||
|
|
||||
|
public static function getToken(string &$stream): Token |
||||
|
{ |
||||
|
$buffer = ''; |
||||
|
$buffer = $stream[0]; |
||||
|
for ($i = 1; $i < strlen($stream); $i++) |
||||
|
{ |
||||
|
if (preg_match('/^[A-Za-z0-9]$/', $stream[$i])) |
||||
|
{ |
||||
|
$buffer .= $stream[$i]; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$stream = substr($stream, strlen($buffer)); |
||||
|
|
||||
|
return new Parameter($buffer); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the value of this token. |
||||
|
* |
||||
|
* @param ?\App\Util\GraphNode[] $arguments |
||||
|
* @return \App\Util\GraphNode|Token|scalar|null |
||||
|
*/ |
||||
|
public function execute(?array $arguments = null) |
||||
|
{ |
||||
|
throw new \Exception('Unreplaced parameter'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return the cloudformation representation of a reference to a parameter. |
||||
|
* |
||||
|
* @param ?\App\Util\GraphNode[] $arguments |
||||
|
* @return mixed[] |
||||
|
*/ |
||||
|
public function toCloudformation(?array $arguments = null): array |
||||
|
{ |
||||
|
return ['Ref' => $this->getName()]; |
||||
|
} |
||||
|
} |
@ -0,0 +1,12 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
namespace App\Cfnpp\Expression; |
||||
|
|
||||
|
/** |
||||
|
* A token that takes two arguments. |
||||
|
*/ |
||||
|
abstract class TokenBinary extends Token |
||||
|
{ |
||||
|
} |
@ -0,0 +1,12 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
namespace App\Cfnpp\Expression; |
||||
|
|
||||
|
/** |
||||
|
* A token that takes no arguments. |
||||
|
*/ |
||||
|
abstract class TokenLiteral extends Token |
||||
|
{ |
||||
|
} |
@ -0,0 +1,12 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
namespace App\Cfnpp\Expression; |
||||
|
|
||||
|
/** |
||||
|
* A token that takes one argument. |
||||
|
*/ |
||||
|
abstract class TokenUnary extends Token |
||||
|
{ |
||||
|
} |
@ -1,231 +0,0 @@ |
|||||
<?php |
|
||||
|
|
||||
declare(strict_types=1); |
|
||||
|
|
||||
namespace App\Engine\Cfnpp\Expression; |
|
||||
|
|
||||
/** |
|
||||
* Expression that can be evaluated. |
|
||||
* |
|
||||
* @author Adam Pippin <hello@adampippin.ca> |
|
||||
*/ |
|
||||
class Expression |
|
||||
{ |
|
||||
/** |
|
||||
* List of Tokens in the expression. |
|
||||
* @var Token[] |
|
||||
*/ |
|
||||
protected $tokens; |
|
||||
|
|
||||
/** |
|
||||
* Evaluation stack. |
|
||||
* @var mixed[] |
|
||||
*/ |
|
||||
protected $stack; |
|
||||
|
|
||||
/** |
|
||||
* Variables that can be referenced in the expression. |
|
||||
* @var array<string,mixed> |
|
||||
*/ |
|
||||
protected $variables; |
|
||||
|
|
||||
/** |
|
||||
* Create a new expression. |
|
||||
* |
|
||||
* @param Token[] $tokens |
|
||||
*/ |
|
||||
public function __construct(array $tokens) |
|
||||
{ |
|
||||
$this->tokens = $tokens; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Find all referenced variables so we can do proper ordering of variable |
|
||||
* block evaluate. |
|
||||
* |
|
||||
* @return string[] |
|
||||
*/ |
|
||||
public function getReferencedVariables(): array |
|
||||
{ |
|
||||
$variables = []; |
|
||||
foreach ($this->tokens as $token) |
|
||||
{ |
|
||||
if ($token instanceof TokenVariable) |
|
||||
{ |
|
||||
$variables[] = $token->getName(); |
|
||||
} |
|
||||
} |
|
||||
return array_values(array_unique($variables)); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Evaluate the tokens contained in this expression. |
|
||||
* |
|
||||
* @param array<string,mixed> $variables variables that can be referenced |
|
||||
* @return mixed if expression evaluated down to a single value a scalar, otherwise an array |
|
||||
*/ |
|
||||
public function evaluate(array $variables = []) |
|
||||
{ |
|
||||
$this->variables = $variables; |
|
||||
$this->stack = []; |
|
||||
$tokens = $this->tokens; |
|
||||
|
|
||||
while (sizeof($tokens)) |
|
||||
{ |
|
||||
$token = array_shift($tokens); |
|
||||
assert(isset($token)); |
|
||||
|
|
||||
$token_class = get_class($token); |
|
||||
$token_name = substr($token_class, strrpos($token_class, '\\') + 1); |
|
||||
$func = 'evaluate'.$token_name; |
|
||||
if (method_exists($this, $func)) |
|
||||
{ |
|
||||
$this->{$func}($token); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
throw new \Exception('Unhandled expression token type: '.basename(get_class($token))); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (sizeof($this->stack) == 1) |
|
||||
{ |
|
||||
return end($this->stack); |
|
||||
} |
|
||||
|
|
||||
return $this->stack; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Evaluate and mutate the stack given a numeric literal. |
|
||||
* |
|
||||
* @param TokenNumericLiteral $token |
|
||||
* @return void |
|
||||
*/ |
|
||||
protected function evaluateTokenNumericLiteral(TokenNumericLiteral $token): void |
|
||||
{ |
|
||||
$this->push($token->getValue()); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Evaluate and mutate the stack given a string literal. |
|
||||
* |
|
||||
* @param TokenStringLiteral $token |
|
||||
* @return void |
|
||||
*/ |
|
||||
protected function evaluateTokenStringLiteral(TokenStringLiteral $token): void |
|
||||
{ |
|
||||
$this->push($token->getValue()); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Evaluate and mutate the stack given a comparison operator. |
|
||||
* |
|
||||
* $this->pop() == $this->pop() is valid. |
|
||||
* @suppress PhanPluginDuplicateExpressionBinaryOp |
|
||||
* @param TokenOperator $token |
|
||||
* @return void |
|
||||
*/ |
|
||||
protected function evaluateTokenOperator(TokenOperator $token): void |
|
||||
{ |
|
||||
switch ($token->getOperator()) |
|
||||
{ |
|
||||
case 'eq': |
|
||||
$this->push($this->pop() == $this->pop()); |
|
||||
break; |
|
||||
case 'neq': |
|
||||
$this->push($this->pop() != $this->pop()); |
|
||||
break; |
|
||||
case 'gt': |
|
||||
$this->push($this->pop(1) > $this->pop()); |
|
||||
break; |
|
||||
case 'gte': |
|
||||
$this->push($this->pop(1) >= $this->pop()); |
|
||||
break; |
|
||||
case 'lt': |
|
||||
$this->push($this->pop(1) < $this->pop()); |
|
||||
break; |
|
||||
case 'lte': |
|
||||
$this->push($this->pop(1) <= $this->pop()); |
|
||||
break; |
|
||||
case 'and': |
|
||||
$var1 = $this->pop(); |
|
||||
$var2 = $this->pop(); |
|
||||
$this->push($var1 && $var2); |
|
||||
break; |
|
||||
case 'or': |
|
||||
$var1 = $this->pop(); |
|
||||
$var2 = $this->pop(); |
|
||||
$this->push($var1 || $var2); |
|
||||
break; |
|
||||
default: |
|
||||
throw new \Exception('Unhandled comparison operator: '.$token->getOperator()); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Evaluate and mutate the stack given a function token. |
|
||||
* |
|
||||
* @param TokenFunction $token |
|
||||
* @return void |
|
||||
*/ |
|
||||
protected function evaluateTokenFunction(TokenFunction $token): void |
|
||||
{ |
|
||||
switch ($token->getFunction()) |
|
||||
{ |
|
||||
case 'concat': |
|
||||
$this->push($this->pop(1).$this->pop()); |
|
||||
break; |
|
||||
case 'concat*': |
|
||||
while (sizeof($this->stack) > 1) |
|
||||
{ |
|
||||
$this->push($this->pop(1).$this->pop()); |
|
||||
} |
|
||||
break; |
|
||||
default: |
|
||||
throw new \Exception('Unhandled function: '.$token->getFunction()); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Evaluate and mutate the stack given a variable reference. |
|
||||
* |
|
||||
* @param TokenVariable $token |
|
||||
* @return void |
|
||||
*/ |
|
||||
protected function evaluateTokenVariable(TokenVariable $token) |
|
||||
{ |
|
||||
$name = $token->getName(); |
|
||||
if (!isset($this->variables[$name])) |
|
||||
{ |
|
||||
throw new \Exception('Undefined variable: '.$name); |
|
||||
} |
|
||||
$this->push($this->variables[$name]); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Pop an item off the stack. |
|
||||
* |
|
||||
* @param int $offset offset from the end of the stack to pop from |
|
||||
* @return mixed |
|
||||
*/ |
|
||||
protected function pop(int $offset = 0) |
|
||||
{ |
|
||||
if (sizeof($this->stack) < $offset + 1) |
|
||||
{ |
|
||||
throw new \Exception('Expression stack underflow!'); |
|
||||
} |
|
||||
return array_splice($this->stack, -1 * ($offset + 1), 1)[0]; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Push an item onto the end of the stack. |
|
||||
* |
|
||||
* @param mixed $value |
|
||||
* @return void |
|
||||
*/ |
|
||||
protected function push($value): void |
|
||||
{ |
|
||||
array_push($this->stack, $value); |
|
||||
} |
|
||||
} |
|
@ -1,61 +0,0 @@ |
|||||
<?php |
|
||||
|
|
||||
declare(strict_types=1); |
|
||||
|
|
||||
namespace App\Engine\Cfnpp\Expression; |
|
||||
|
|
||||
/** |
|
||||
* Parse a string into a series of expression tokens. |
|
||||
* |
|
||||
* @author Adam Pippin <hello@adampippin.ca> |
|
||||
*/ |
|
||||
class Parser |
|
||||
{ |
|
||||
/** |
|
||||
* List of class names of tokens this parser will parse. |
|
||||
* |
|
||||
* @var string[] |
|
||||
*/ |
|
||||
public const TOKEN_TYPES = [ |
|
||||
TokenFunction::class, |
|
||||
TokenNumericLiteral::class, |
|
||||
TokenOperator::class, |
|
||||
TokenStringLiteral::class, |
|
||||
TokenVariable::class |
|
||||
]; |
|
||||
|
|
||||
/** |
|
||||
* Parse a string into an expression. |
|
||||
* |
|
||||
* @param string $value |
|
||||
* @return Expression |
|
||||
*/ |
|
||||
public function parse(string $value): Expression |
|
||||
{ |
|
||||
$original_value = $value; |
|
||||
$value .= ' '; |
|
||||
$tokens = []; |
|
||||
while (strlen($value) > 0) |
|
||||
{ |
|
||||
foreach (static::TOKEN_TYPES as $token_class) |
|
||||
{ |
|
||||
if ($token_class::isToken($value)) |
|
||||
{ |
|
||||
$tokens[] = $token_class::getToken($value); |
|
||||
|
|
||||
if (substr($value, 0, 1) != ' ') |
|
||||
{ |
|
||||
throw new \Exception('Incompletely consumed token at offset '.(strlen($original_value) - strlen($value)).' in expression '.$original_value); |
|
||||
} |
|
||||
|
|
||||
$value = substr($value, 1); |
|
||||
continue 2; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
throw new \Exception('Unparseable value at offset '.(strlen($original_value) - strlen($value)).' in expression '.$original_value); |
|
||||
} |
|
||||
|
|
||||
return new Expression($tokens); |
|
||||
} |
|
||||
} |
|
@ -1,32 +0,0 @@ |
|||||
<?php |
|
||||
|
|
||||
declare(strict_types=1); |
|
||||
|
|
||||
namespace App\Engine\Cfnpp\Expression; |
|
||||
|
|
||||
/** |
|
||||
* Token representing a distinct part of an expression. |
|
||||
* |
|
||||
* @author Adam Pippin <hello@adampippin.ca> |
|
||||
*/ |
|
||||
abstract class Token |
|
||||
{ |
|
||||
/** |
|
||||
* Check whether the stream passed in contains a valid token of this |
|
||||
* type. |
|
||||
* |
|
||||
* @param string $stream |
|
||||
* @return bool |
|
||||
*/ |
|
||||
abstract public static function isToken(string $stream): bool; |
|
||||
|
|
||||
/** |
|
||||
* Consume a token from the stream. |
|
||||
* |
|
||||
* Only valid if isToken is true. |
|
||||
* |
|
||||
* @param string $stream |
|
||||
* @return Token |
|
||||
*/ |
|
||||
abstract public static function getToken(string &$stream): Token; |
|
||||
} |
|
@ -1,90 +0,0 @@ |
|||||
<?php |
|
||||
|
|
||||
declare(strict_types=1); |
|
||||
|
|
||||
namespace App\Engine\Cfnpp\Expression; |
|
||||
|
|
||||
/** |
|
||||
* Token representing a function. |
|
||||
* |
|
||||
* @author Adam Pippin <hello@adampippin.ca> |
|
||||
*/ |
|
||||
class TokenFunction extends Token |
|
||||
{ |
|
||||
/** |
|
||||
* List of valid functions this can parse. |
|
||||
* @var string[] |
|
||||
*/ |
|
||||
public const FUNCTIONS = [ |
|
||||
'concat*', |
|
||||
'concat' |
|
||||
]; |
|
||||
|
|
||||
/** |
|
||||
* Function this token represents. |
|
||||
* @var string |
|
||||
*/ |
|
||||
protected $function; |
|
||||
|
|
||||
/** |
|
||||
* Create a new function token. |
|
||||
* |
|
||||
* @param string $function |
|
||||
*/ |
|
||||
public function __construct($function) |
|
||||
{ |
|
||||
$this->function = $function; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Get the function this token represents. |
|
||||
* |
|
||||
* @return string |
|
||||
*/ |
|
||||
public function getFunction(): string |
|
||||
{ |
|
||||
return $this->function; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Determine whether a function token can be parsed from a stream. |
|
||||
* |
|
||||
* @param string $stream |
|
||||
* @return bool |
|
||||
*/ |
|
||||
public static function isToken(string $stream): bool |
|
||||
{ |
|
||||
foreach (static::FUNCTIONS as $function) |
|
||||
{ |
|
||||
if (strlen($stream) > strlen($function) && |
|
||||
substr($stream, 0, strlen($function)) == $function) |
|
||||
{ |
|
||||
return true; |
|
||||
} |
|
||||
} |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Parse a function token from a stream. |
|
||||
* |
|
||||
* Returns token, and modifies stream to remove all consumed characters |
|
||||
* |
|
||||
* @param string $stream |
|
||||
* @return Token |
|
||||
*/ |
|
||||
public static function getToken(string &$stream): Token |
|
||||
{ |
|
||||
foreach (static::FUNCTIONS as $function) |
|
||||
{ |
|
||||
if (strlen($stream) > strlen($function) && |
|
||||
substr($stream, 0, strlen($function)) == $function) |
|
||||
{ |
|
||||
$function = substr($stream, 0, strlen($function)); |
|
||||
$stream = substr($stream, strlen($function)); |
|
||||
return new TokenFunction($function); |
|
||||
} |
|
||||
} |
|
||||
throw new \Exception('Could not parse function token!'); |
|
||||
} |
|
||||
} |
|
@ -1,96 +0,0 @@ |
|||||
<?php |
|
||||
|
|
||||
declare(strict_types=1); |
|
||||
|
|
||||
namespace App\Engine\Cfnpp\Expression; |
|
||||
|
|
||||
/** |
|
||||
* Token representing a comparison operator. |
|
||||
* |
|
||||
* @author Adam Pippin <hello@adampippin.ca> |
|
||||
*/ |
|
||||
class TokenOperator extends Token |
|
||||
{ |
|
||||
/** |
|
||||
* List of valid operators this can parse. |
|
||||
* @var string[] |
|
||||
*/ |
|
||||
public const OPERATORS = [ |
|
||||
'eq', |
|
||||
'neq', |
|
||||
'gt', |
|
||||
'gte', |
|
||||
'lt', |
|
||||
'lte', |
|
||||
'and', |
|
||||
'or' |
|
||||
]; |
|
||||
|
|
||||
/** |
|
||||
* Operator this token represents. |
|
||||
* @var string |
|
||||
*/ |
|
||||
protected $operator; |
|
||||
|
|
||||
/** |
|
||||
* Create a new operator token. |
|
||||
* |
|
||||
* @param string $operator |
|
||||
*/ |
|
||||
public function __construct($operator) |
|
||||
{ |
|
||||
$this->operator = $operator; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Get the operator this token represents. |
|
||||
* |
|
||||
* @return string |
|
||||
*/ |
|
||||
public function getOperator(): string |
|
||||
{ |
|
||||
return $this->operator; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Determine whether a operator token can be parsed from a stream. |
|
||||
* |
|
||||
* @param string $stream |
|
||||
* @return bool |
|
||||
*/ |
|
||||
public static function isToken(string $stream): bool |
|
||||
{ |
|
||||
foreach (static::OPERATORS as $operator) |
|
||||
{ |
|
||||
if (strlen($stream) > strlen($operator) && |
|
||||
substr($stream, 0, strlen($operator)) == $operator) |
|
||||
{ |
|
||||
return true; |
|
||||
} |
|
||||
} |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Parse an operator token from a stream. |
|
||||
* |
|
||||
* Returns token, and modifies stream to remove all consumed characters |
|
||||
* |
|
||||
* @param string $stream |
|
||||
* @return Token |
|
||||
*/ |
|
||||
public static function getToken(string &$stream): Token |
|
||||
{ |
|
||||
foreach (static::OPERATORS as $operator) |
|
||||
{ |
|
||||
if (strlen($stream) > strlen($operator) && |
|
||||
substr($stream, 0, strlen($operator)) == $operator) |
|
||||
{ |
|
||||
$operator = substr($stream, 0, strlen($operator)); |
|
||||
$stream = substr($stream, strlen($operator)); |
|
||||
return new TokenOperator($operator); |
|
||||
} |
|
||||
} |
|
||||
throw new \Exception('Could not parse operator token!'); |
|
||||
} |
|
||||
} |
|
@ -0,0 +1,212 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
namespace App\Util; |
||||
|
|
||||
|
/** |
||||
|
* Represents a node in a tree. |
||||
|
* |
||||
|
* @author Adam Pippin <hello@adampippin.ca> |
||||
|
*/ |
||||
|
class GraphNode |
||||
|
{ |
||||
|
/** |
||||
|
* Parent of this node. |
||||
|
* @var GraphNode |
||||
|
*/ |
||||
|
protected $parent; |
||||
|
|
||||
|
/** |
||||
|
* Value of this node. |
||||
|
* @var mixed |
||||
|
*/ |
||||
|
protected $value; |
||||
|
|
||||
|
/** |
||||
|
* Children of this node. |
||||
|
* @var GraphNode[] |
||||
|
*/ |
||||
|
protected $children; |
||||
|
|
||||
|
/** |
||||
|
* Create a new graph node. |
||||
|
* |
||||
|
* @param mixed $value |
||||
|
*/ |
||||
|
public function __construct($value = null) |
||||
|
{ |
||||
|
$this->value = $value; |
||||
|
$this->children = []; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set the value of this node. |
||||
|
* |
||||
|
* @param mixed $value |
||||
|
* @return void |
||||
|
*/ |
||||
|
public function setValue($value): void |
||||
|
{ |
||||
|
$this->value = $value; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the value of this node. |
||||
|
* |
||||
|
* @return mixed |
||||
|
*/ |
||||
|
public function getValue() |
||||
|
{ |
||||
|
return $this->value; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set the parent node of this node. |
||||
|
* |
||||
|
* @param GraphNode $node |
||||
|
* @return void |
||||
|
*/ |
||||
|
public function setParent(GraphNode $node): void |
||||
|
{ |
||||
|
$this->parent = $node; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Determine whether this node has a parent. |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function hasParent(): bool |
||||
|
{ |
||||
|
return isset($this->parent); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get this node's parent node. |
||||
|
* |
||||
|
* @return GraphNode |
||||
|
*/ |
||||
|
public function getParent(): GraphNode |
||||
|
{ |
||||
|
return $this->parent; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Count how many child nodes this node has. |
||||
|
* |
||||
|
* @return int |
||||
|
*/ |
||||
|
public function countChildren(): int |
||||
|
{ |
||||
|
return sizeof($this->children); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Determine whether this node has any children. |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function hasChildren(): bool |
||||
|
{ |
||||
|
return sizeof($this->children) > 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Fetch all of this node's children. |
||||
|
* |
||||
|
* @return GraphNode[] |
||||
|
*/ |
||||
|
public function getChildren(): array |
||||
|
{ |
||||
|
return $this->children; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Add a node to this child's list of children. |
||||
|
* |
||||
|
* @param GraphNode $child |
||||
|
* @return void |
||||
|
*/ |
||||
|
public function appendChild(GraphNode $child): void |
||||
|
{ |
||||
|
$this->children[] = $child; |
||||
|
$child->setParent($this); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Replace a child node with another node. |
||||
|
* |
||||
|
* @param GraphNode $original |
||||
|
* @param GraphNode $new |
||||
|
* @return void |
||||
|
*/ |
||||
|
public function replaceChild(GraphNode $original, GraphNode $new): void |
||||
|
{ |
||||
|
for ($i = 0; $i < sizeof($this->children); $i++) |
||||
|
{ |
||||
|
if ($this->children[$i] === $original) |
||||
|
{ |
||||
|
$this->children[$i] = $new; |
||||
|
$new->setParent($this); |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Remove all children of this node. |
||||
|
* |
||||
|
* @return void |
||||
|
*/ |
||||
|
public function clearChildren(): void |
||||
|
{ |
||||
|
$this->children = []; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Add a child node by value. |
||||
|
* |
||||
|
* @param mixed $value value to add as a child |
||||
|
* @return GraphNode the node created as a child |
||||
|
*/ |
||||
|
public function add($value): GraphNode |
||||
|
{ |
||||
|
$this->children[] = new GraphNode($value); |
||||
|
end($this->children)->setParent($this); |
||||
|
return end($this->children); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Walk through this node and all children, calling callback on each node. |
||||
|
* |
||||
|
* Callback is called on a node's children before a node |
||||
|
* |
||||
|
* @param callable $callback callback(GraphNode $node): void |
||||
|
* @return void |
||||
|
*/ |
||||
|
public function walk(callable $callback) |
||||
|
{ |
||||
|
static::walkNodes([$this], $callback); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Internal function for recursively visiting all nodes. |
||||
|
* |
||||
|
* @param GraphNode[] $nodes |
||||
|
* @param callable $callback |
||||
|
* @return void |
||||
|
*/ |
||||
|
protected static function walkNodes(array $nodes, callable $callback) |
||||
|
{ |
||||
|
foreach ($nodes as $node) |
||||
|
{ |
||||
|
if ($node->hasChildren()) |
||||
|
{ |
||||
|
static::walkNodes($node->getChildren(), $callback); |
||||
|
} |
||||
|
|
||||
|
$callback($node); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,73 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
namespace App\Util; |
||||
|
|
||||
|
/** |
||||
|
* Basic stack data structure. |
||||
|
* |
||||
|
* @author Adam Pippin <hello@adampippin.ca> |
||||
|
*/ |
||||
|
class Stack |
||||
|
{ |
||||
|
/** |
||||
|
* Stack data. |
||||
|
* @var mixed[] |
||||
|
*/ |
||||
|
protected $stack; |
||||
|
|
||||
|
/** |
||||
|
* Create a new stack. |
||||
|
*/ |
||||
|
public function __construct() |
||||
|
{ |
||||
|
$this->stack = []; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Count how many items this stack contains. |
||||
|
* |
||||
|
* @return int |
||||
|
*/ |
||||
|
public function count(): int |
||||
|
{ |
||||
|
return sizeof($this->stack); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get all items in this stack. |
||||
|
* |
||||
|
* @return mixed[] |
||||
|
*/ |
||||
|
public function get(): array |
||||
|
{ |
||||
|
return $this->stack; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Pop an item off of this stack. |
||||
|
* |
||||
|
* @param int $offset offset from the end to pop if not the last element |
||||
|
* @return mixed |
||||
|
*/ |
||||
|
public function pop(int $offset = 0) |
||||
|
{ |
||||
|
if (sizeof($this->stack) < $offset + 1) |
||||
|
{ |
||||
|
throw new \Exception('Stack underflow!'); |
||||
|
} |
||||
|
return array_splice($this->stack, -1 * ($offset + 1), 1)[0]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Push an item onto the end of the stack. |
||||
|
* |
||||
|
* @param mixed $value |
||||
|
* @return void |
||||
|
*/ |
||||
|
public function push($value): void |
||||
|
{ |
||||
|
array_push($this->stack, $value); |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue