First of all, I’m not trying to do tagless-final in PHP, but rather investigating what’s needed to cover a similar use-case, which would be an embedded DSL which can be evaluated in different ways.
Second point, the approach is very much “tagfull”, since it’s building an AST.
The idea is to use the expression builder pattern to build an AST, and then inject either a live evaluator or a dummy evaluator into the builder class.
The end-goal is to get rid of mocking in the test suite, either by making more functions pure (by deferring effects) or use a “universal mock” (the dummy evaluator).
OCaml is my language of choice, but PHP is what I work in, so for me it’s always interesting to figure out if/how to transfer concepts between the languages. Another example of this is the Option
type vs null flow-checking done in Psalm.
Motivating example (from here):
public static string GetUpperText(string path)
{
if (!File.Exists(path)) return "DEFAULT";
var text = File.ReadAllText(path);
return text.ToUpperInvariant();
}
In PHP with the effect EDSL:
function getUpperText(string $file, St $st)
{
$result = 'DEFAULT';
$st
->if(fileExists($file))
->then(set($result, fileGetContents($file)))
();
return strtoupper($result);
}
In PHP with a mockable class:
function getUpperText(string $file, IO $io)
{
$result = 'DEFAULT';
if ($io->fileExists($file)) {
$result = $io->fileGetContents($file);
}
return strtoupper($result);
}
The St class will build an abstract-syntax tree, which is then evaluated when invoked. It can be injected with either a live evaluator, or a dry-run evaluator which works as both mock, stub and spy.
St can also be used to delay or defer effects - just omit the invoke until later.
The unit test looks like this:
// Instead of mocking return types, set the return values
$returnValues = [
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"
}
*/
The St class scales differently than mocking, so it’s not always sensible to use.
Full code: One universal dry-run mock-spy AST evaluator to rule them all · GitHub