Skip to content

Instantly share code, notes, and snippets.

@olleharstedt
Created July 30, 2025 08:28
Show Gist options
  • Select an option

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

Select an option

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

Revisions

  1. olleharstedt created this gist Jul 30, 2025.
    200 changes: 200 additions & 0 deletions nomock.php
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,200 @@
    <?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();
    }
    }