Skip to content

Instantly share code, notes, and snippets.

@olleharstedt
Created March 2, 2022 23:41
Show Gist options
  • Select an option

  • Save olleharstedt/e18004ad82e57e18047690596781a05a to your computer and use it in GitHub Desktop.

Select an option

Save olleharstedt/e18004ad82e57e18047690596781a05a to your computer and use it in GitHub Desktop.

Revisions

  1. olleharstedt created this gist Mar 2, 2022.
    274 changes: 274 additions & 0 deletions tmp6.php
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,274 @@
    <?php

    namespace Tmp6;

    use InvalidArgumentException;

    interface Node
    {
    }

    class If_ implements Node
    {
    public $if;
    public $then;

    public function __construct(Node|Callable $if)
    {
    $this->if = $if;
    }

    public function setThen(Node $if)
    {
    $this->then = $if;
    }
    }

    class FileExists implements Node
    {
    public $file;
    public function __construct($file)
    {
    $this->file = $file;
    }
    }

    function fileExists(string $file)
    {
    return new FileExists($file);
    }

    class FileGetContents implements Node
    {
    public $file;
    public function __construct($file)
    {
    $this->file = $file;
    }
    }

    function fileGetContents(string $file)
    {
    return new FileGetContents($file);
    }

    class Set implements Node
    {
    public $var;
    public $val;
    public function __construct(mixed &$var, mixed $val)
    {
    $this->var = &$var;
    $this->val = $val;
    }
    }

    function set(&$var, $val)
    {
    return new Set($var, $val);
    }

    interface EvaluatorInterface
    {
    public function evalNode(Node $node);
    }

    class NodeEvaluator implements EvaluatorInterface
    {
    /**
    * @return mixed
    */
    public function evalNode(Node $node)
    {
    $className = get_class_name($node::class);
    switch ($className) {
    case "If_":
    if ($this->evalNode($node->if)) {
    $this->evalNode($node->then);
    } elseif (!empty($node->else)) {
    $this->evalNode($node->else);
    }
    break;
    case "FileExists":
    return file_exists($node->file);
    case "FileGetContents":
    return file_get_contents($node->file);
    case "Set":
    if ($node->val instanceof Node) {
    $node->var = $this->evalNode($node->val);
    } elseif (gettype($node->val) === 'string') {
    $node->var = $node->val;
    } else {
    throw new InvalidArgumentException('Unknown type of val in set: ' . gettype($node->val));
    }
    break;
    default:
    throw new InvalidArgumentException('Unsupported node type: ' . $className);
    }
    }
    }

    class DryRunEvaluator implements EvaluatorInterface
    {
    public $log = [];
    public $returnValues = [];

    public function __construct(array $returnValues)
    {
    $this->returnValues = $returnValues;
    }

    /**
    * @return mixed
    */
    public function evalNode($node)
    {
    $className = get_class_name($node::class);
    switch ($className) {
    case "If_":
    $this->log[] = "Evaluating if";
    if ($this->evalNode($node->if)) {
    $this->log[] = "Evaluating then";
    $this->evalNode($node->then);
    } elseif (!empty($node->else)) {
    $this->log[] = "Evaluating else";
    $this->evalNode($node->else);
    }
    break;
    case "FileExists":
    $this->log[] = "File exists: arg1 = " . $node->file;
    return array_pop($this->returnValues);
    case "FileGetContents":
    $this->log[] = "File get contents: arg1 = " . $node->file;
    return array_pop($this->returnValues);
    case "Set":
    if ($node->val instanceof Node) {
    $val = $this->evalNode($node->val);
    $node->var = $val;
    $this->log[] = "Set var to: " . $val;
    } elseif (gettype($node->val) === 'string') {
    $node->var = $node->val;
    $this->log[] = "Set var to: " . $node->val;
    } else {
    throw new InvalidArgumentException('Unknown type of val in set: ' . gettype($node->val));
    }
    break;
    default:
    throw new InvalidArgumentException('Unsupported node type: ' . $className);
    }
    }
    }

    class St
    {
    public $queue = [];
    public $ev;

    public function __construct(EvaluatorInterface $ev)
    {
    $this->ev = $ev;
    }

    public function if($if)
    {
    $this->queue[] = new If_($if);
    return $this;
    }

    public function then($if)
    {
    $i = \count($this->queue) - 1;
    if ($this->queue[$i] instanceof If_) {
    $this->queue[$i]->setThen($if);
    } else {
    throw new InvalidArgumentException('then must come after if');
    }
    return $this;
    }

    public function run($ev)
    {
    foreach ($this->queue as $node) {
    $ev->evalNode($node);
    }
    }

    public function set($var, $value)
    {
    return $this;
    }

    public function __invoke()
    {
    foreach ($this->queue as $node) {
    $this->ev->evalNode($node);
    }
    }
    }

    function get_class_name($classname)
    {
    if ($pos = strrpos($classname, '\\'))
    return substr($classname, $pos + 1);
    return $pos;
    }

    /*
    Use-case from: https://blog.ploeh.dk/2016/09/26/decoupling-decisions-from-effects/
    public static string GetUpperText(string path)
    {
    if (!File.Exists(path)) return "DEFAULT";
    var text = File.ReadAllText(path);
    return text.ToUpperInvariant();
    }
    */

    // Using an expression builder
    function getUpperText(string $file, St $st)
    {
    $result = 'DEFAULT';
    $st
    ->if(fileExists($file))
    ->then(set($result, fileGetContents($file)))
    ();
    return strtoupper($result);
    }

    // Using a mock
    function getUpperTextMock(string $file, IO $io)
    {
    $result = 'DEFAULT';
    if ($io->fileExists($file)) {
    $result = $io->fileGetContents($file);
    }
    return strtoupper($result);
    }

    // Instead of mocking return types, set the return values
    $returnValues = array_reverse(
    [
    true,
    'Some example file content, bla bla bla'
    ]
    );
    $ev = new DryRunEvaluator($returnValues);
    $st = new St($ev);
    $text = getUpperText('moo.txt', $st);
    // Output: string(38) "SOME EXAMPLE FILE CONTENT, BLA BLA BLA"
    var_dump($text);
    // Instead of a spy, you can inspect the dry-run log
    var_dump($ev->log);
    /* Output:
    array(5) {
    [0] =>
    string(13) "Evaluating if"
    [1] =>
    string(27) "File exists: arg1 = moo.txt"
    [2] =>
    string(15) "Evaluating then"
    [3] =>
    string(33) "File get contents: arg1 = moo.txt"
    [4] =>
    string(50) "Set var to: Some example file content, bla bla bla"
    }
    */