Compare commits
18 Commits
Author | SHA1 | Date |
---|---|---|
Adam Pippin | 326e856a2b | 3 years ago |
Adam Pippin | 0265db1946 | 3 years ago |
Adam Pippin | 6f90b5d222 | 3 years ago |
Adam Pippin | 8dd7b8d1f0 | 3 years ago |
Adam Pippin | 4294c5b007 | 3 years ago |
Adam Pippin | 6ddf1531af | 3 years ago |
Adam Pippin | 706242074d | 3 years ago |
Adam Pippin | 9e5adfb9ce | 3 years ago |
Adam Pippin | a3fe71526d | 3 years ago |
Adam Pippin | 9a99fc315b | 3 years ago |
Adam Pippin | b38a37d048 | 3 years ago |
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 |
25 changed files with 1501 additions and 632 deletions
@ -0,0 +1,372 @@ |
|||
<?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\BooleanLiteral::class, |
|||
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 |
|||
*/ |
|||
public function __construct(string $expression) |
|||
{ |
|||
$tokens = static::tokenize($expression); |
|||
$this->nodes = static::parse($tokens); |
|||
} |
|||
|
|||
/** |
|||
* Solve this expression down to the minimal set of nodes we can |
|||
* |
|||
* @param \App\Engine\IOptions $options |
|||
* @return void |
|||
*/ |
|||
public function solve(\App\Engine\IOptions $options): void |
|||
{ |
|||
$this->nodes = static::_solve($this->nodes, $options->getVariables(), array_keys($options->getParameters())); |
|||
} |
|||
|
|||
/** |
|||
* 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); |
|||
} |
|||
|
|||
/** |
|||
* Examine the expression to determine which variables are referenced so we |
|||
* can figure out the dependencies between them |
|||
* |
|||
* @return string[] names of variables referenced |
|||
*/ |
|||
public function getReferencedVariables(): array |
|||
{ |
|||
$variables = []; |
|||
foreach ($this->nodes as $node) |
|||
{ |
|||
$node->walk(function(GraphNode $node) use (&$variables): void { |
|||
if ($node->getValue() instanceof Token\Variable) |
|||
{ |
|||
$variables[] = $node->getValue()->getName(); |
|||
} |
|||
}); |
|||
} |
|||
return array_values(array_unique($variables)); |
|||
} |
|||
|
|||
/** |
|||
* 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,75 @@ |
|||
<?php |
|||
|
|||
declare(strict_types=1); |
|||
|
|||
namespace App\Cfnpp\Expression\Token; |
|||
|
|||
use App\Cfnpp\Expression\Token; |
|||
use App\Cfnpp\Expression\TokenLiteral; |
|||
|
|||
/** |
|||
* A boolean literal (true/false). |
|||
* |
|||
* @author Adam Pippin <hello@adampippin.ca> |
|||
*/ |
|||
class BooleanLiteral extends TokenLiteral |
|||
{ |
|||
/** |
|||
* Value of this literal. |
|||
* @var bool |
|||
*/ |
|||
protected $value; |
|||
|
|||
/** |
|||
* New boolean literal. |
|||
* |
|||
* @param bool $value |
|||
*/ |
|||
public function __construct(bool $value) |
|||
{ |
|||
$this->value = $value; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of this literal. |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function getValue(): bool |
|||
{ |
|||
return $this->value; |
|||
} |
|||
|
|||
public static function isToken(string $stream): bool |
|||
{ |
|||
return |
|||
(strlen($stream) >= 4 && strtolower(substr($stream, 0, 4)) == 'true') || |
|||
(strlen($stream) >= 5 && strtolower(substr($stream, 0, 5)) == 'false'); |
|||
} |
|||
|
|||
public static function getToken(string &$stream): Token |
|||
{ |
|||
if (strlen($stream) >= 4 && strtolower(substr($stream, 0, 4)) == 'true') |
|||
{ |
|||
$stream = substr($stream, 4); |
|||
return new BooleanLiteral(true); |
|||
} |
|||
elseif (strlen($stream) >= 5 && strtolower(substr($stream, 0, 5)) == 'false') |
|||
{ |
|||
$stream = substr($stream, 5); |
|||
return new BooleanLiteral(false); |
|||
} |
|||
throw new \Exception('Unparseable boolean'); |
|||
} |
|||
|
|||
/** |
|||
* Get the value of this token. |
|||
* |
|||
* @param ?\App\Util\GraphNode[] $arguments |
|||
* @return \App\Util\GraphNode|Token|scalar|null |
|||
*/ |
|||
public function execute(?array $arguments = null) |
|||
{ |
|||
return $this->getValue(); |
|||
} |
|||
} |
@ -0,0 +1,211 @@ |
|||
<?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', |
|||
'concat', |
|||
'select' |
|||
]; |
|||
|
|||
/** |
|||
* 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 ? $arguments[1] : false; |
|||
} |
|||
elseif (is_scalar($value2)) |
|||
{ |
|||
return $value2 ? $arguments[0] : 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 : $arguments[1]; |
|||
} |
|||
elseif (is_scalar($value2)) |
|||
{ |
|||
return $value2 ? true : $arguments[0]; |
|||
} |
|||
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; |
|||
|
|||
case 'concat': |
|||
if (is_scalar($value1) && is_scalar($value2)) |
|||
{ |
|||
return $value2.$value1; |
|||
} |
|||
elseif ($value1 instanceof Parameter && is_scalar($value2) && empty($value2)) |
|||
{ |
|||
return $value1; |
|||
} |
|||
elseif ($value2 instanceof Parameter && is_scalar($value1) && empty($value1)) |
|||
{ |
|||
return $value2; |
|||
} |
|||
|
|||
return null; |
|||
|
|||
case 'select': |
|||
if (is_scalar($value1) && is_array($value2)) |
|||
{ |
|||
return $value2[$value1]; |
|||
} |
|||
|
|||
return null; |
|||
|
|||
default: |
|||
throw new \Exception('Missing implementation for binary 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]]; |
|||
case 'concat': |
|||
return ['Fn::Join' => ['', [$value2, $value1]]]; |
|||
case 'select': |
|||
return ['Fn::Select' => [$value1, $value2]]; |
|||
default: |
|||
throw new \Exception('Operator cannot be applied to parameters: '.$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