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" } */