Created
July 30, 2025 08:28
-
-
Save olleharstedt/2f75ecdcd9c6b00f06bfc567fcbcc19b to your computer and use it in GitHub Desktop.
Kill the mocking DSL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* DI with high granularity, every side-effect is its own class. | |
* Each side-effect class would be its own mock/stub or anonymous class in the unit-test code. | |
*/ | |
class PushOrderToRemoteAPICommand | |
{ | |
public function __construct( | |
FileLogEffect $fileLog, | |
FetchDbLogByIdEffect $fetchDbLog, | |
UpdateDbLogEffect $updateDbLog, | |
FetchApiConfigEffect $fetchApiConf, | |
FetchCustomerAddressEffect $fetchCustAdr, | |
FetchClerkByIdEffect $fetchStaff, | |
FetchCreditCardDataEffect $fetchCredCard, | |
FetchWarehouseIdEffect $fetchWareId, | |
FetchTranslationsEffect $fetchTrans, | |
PostCurlEffect $postCurl, | |
DeleteOnHoldReceiptEffect $delHold, | |
SetNewOrderIdEffect $setOrd, | |
Registry $registry, | |
) { | |
} | |
public function __invoke() | |
{ | |
$dbLog = $this->fetchDbLog($id); | |
$dbLog->status = 'in_progress'; | |
$this->updateDbLog($dbLog); | |
// ... | |
} | |
} | |
/** | |
* Low granularity. | |
* Leads to complex mocking where you have to setup precise method calls | |
* to the db connections. | |
*/ | |
class PushOrderToRemoteAPICommand | |
{ | |
public function __construct( | |
Connection $db1, | |
Connection $db2, | |
Logger $logger, | |
Curl $curl, | |
Registry $registry, | |
) { | |
} | |
public function __invoke() | |
{ | |
$dbLog = DbLog::fetchById($this->db1, $id); | |
$dbLog->status = 'in_progress'; | |
$dbLog->update($this->db1); | |
// ... | |
} | |
} | |
/** | |
* "Normal" granularity? Inject repository objects. | |
* Lots of mocks. | |
*/ | |
class PushOrderToRemoteAPICommand | |
{ | |
public function __construct( | |
DbLogRepository $dblogrep, | |
CustomerRepository $custrep, | |
ClerkRepository $clerkrep, | |
ApiSettingsRepository $apirep, | |
TranslationRepository $transrep, | |
OrderRepository $orderrep, | |
Logger $logger, | |
Curl $curl, | |
Registry $registry, | |
) { | |
} | |
public function __invoke() | |
{ | |
$dbLog = $this->dblogrep->fetchById($id); | |
$dbLog->status = 'in_progress'; | |
$this->dblogrep->update($dbLog); | |
// ... | |
} | |
} | |
/** | |
* DI where all side-effects are gathered in a separate environmental facade. See below. | |
* Environment facade would be an anonymous class in the test code. | |
*/ | |
class PushOrderToRemoteAPICommand | |
{ | |
public function __construct( | |
PushOrderToRemoteAPIEnv $env, | |
Registry $registry, | |
) { | |
} | |
public function __invoke() | |
{ | |
$dbLog = $env->fetchDbLogEntry($id); | |
$dbLog->status = 'in_progress'; | |
$env->updateDbLogEntry($dbLog); | |
// ... | |
} | |
} | |
/** | |
* Environment facade that wraps all side-effects. | |
*/ | |
class PushOrderToRemoteAPIEnv | |
{ | |
public function __construct( | |
Connection $db1, | |
Connection $db2, | |
Curl $curl, | |
Registry $registry, | |
Logger $logger, | |
) { | |
} | |
public function traceLog() {} | |
public function criticalLog() {} | |
public function fetchDbLogEntry() {} | |
public function updateDbLogEntry() {} | |
public function fetchApiKey() {} | |
public function fetchCreditCardData() {} | |
public function fetchWarehouseId() {} | |
public function fetchTranslations() {} | |
public function fetchCustomerById() {} | |
public function fetchClerkId() {} | |
public function postCurl() {} | |
public function setOrderId() {} | |
public function deleteOnHold() {} | |
} | |
// Small wrapper around Fiber::suspend | |
function perform(Effect $e): mixed | |
{ | |
return Fiber::suspend($e); | |
} | |
/** | |
* No injection, but use a separate command handler which runs the command as a fiber. | |
* Each effect is its own class, but used as algebraic effect instead. | |
* One can also use generators instead of fibers, but then you'd have to propagate the effect manually through the call stack. | |
*/ | |
class PushOrderToRemoteAPICommand | |
{ | |
public function __construct( | |
Registry $registry, | |
) { | |
} | |
public function __invoke() | |
{ | |
$dbLog = perform(new FetchDbLogByIdEffect($id)); | |
$dbLog->status = 'in_progress'; | |
perform(new UpdateDbLogEffect($dbLog)); | |
// ... | |
} | |
} | |
class AlgebraicEffectCommandHandler | |
{ | |
public function __construct(Registry $registry) | |
{ | |
} | |
public function setEffectHandler(EffectHandler $h) {} | |
public function run(): mixed | |
{ | |
$fiber = new Fiber(new PushOrderToRemoteAPICommand($this->registry)); | |
$data = [ | |
'foo' => 'bar' | |
]; | |
$effect = $fiber->start($data); | |
$db1 = OpenDatabase1(); | |
$db2 = OpenDatabase2(); | |
while (!$fiber->isTerminated()) { | |
$data = null; | |
if ($effect instanceof Effect) { | |
// NB: This should be factored out to separate effect handlers, which then are injected. | |
if ($effect instanceof FetchCustomerAddressEffect) { | |
$data = $db->select($effect->sql); | |
} else { | |
throw new RuntimeException('Unsupported effect class'); | |
} | |
} else { | |
// Other Fiber usage? | |
} | |
if ($data) { | |
$effect = $fiber->resume($data); | |
} | |
} | |
return $fiber->getReturn(); | |
} | |
} |
Author
olleharstedt
commented
Aug 14, 2025
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment